Lista 11-31: Agenda con banco de datos completo

##############################################################################
# Parte del libro Introducción a la programación con Python
# Autor: Nilo Ney Coutinho Menezes
# Editora Novatec (c) 2015 - ISBN 978-85-7522-250-8
# Primera edición - Mayo/2016
# Sitio: http://www.librodepython.com
#
# Archivo: lista\capítulo 11\11.31 - Agenda con banco de datos completo.py
# Descripción: Agenda con banco de datos completo
##############################################################################

import sys
import sqlite3
import os.path
from functools import total_ordering

BANCO = """
create table tipos(id integer primary key autoincrement,
                                     descripción text);
create table nombres(id integer primary key autoincrement,
                                         nombre text);
create table teléfonos(id integer primary key autoincrement,
                                             id_nombre integer,
                                             número text,
                                             id_tipo integer);
insert into tipos(descripción) values ("Celular");
insert into tipos(descripción) values ("Fijo");
insert into tipos(descripción) values ("Fax");
insert into tipos(descripción) values ("Casa");
insert into tipos(descripción) values ("Trabajo");
"""


def nulo_o_vacío(texto):
    return texto == None or not texto.strip()


def valida_franja_entero(pregunta, inicio, fin, estándar=None):
    while True:
        try:
            entrada = input(pregunta)
            if nulo_o_vacío(entrada) and estándar != None:
                entrada = estándar
            valor = int(entrada)
            if inicio <= valor <= fin:
                return(valor)
        except ValueError:
            print("Valor inválido, favor digitar entre %d y %d" %
                  (inicio, fin))


def valida_franja_entero_o_blanco(pregunta, inicio, fin):
    while True:
        try:
            entrada = input(pregunta)
            if nulo_o_vacío(entrada):
                return None
            valor = int(entrada)
            if inicio <= valor <= fin:
                return(valor)
        except ValueError:
            print("Valor inválido, favor digitar entre %d y %d" %
                  (inicio, fin))


class ListaÚnica:
    def __init__(self, elem_class):
        self.lista = []
        self.elem_class = elem_class

    def __len__(self):
        return len(self.lista)

    def __iter__(self):
        return iter(self.lista)

    def __getitem__(self, p):
        return self.lista[p]

    def indiceVálido(self, i):
        return i >= 0 and i < len(self.lista)

    def adiciona(self, elem):
        if self.investigación(elem) == -1:
            self.lista.append(elem)

    def remove(self, elem):
        self.lista.remove(elem)

    def investigación(self, elem):
        self.verifica_tipo(elem)
        try:
            return self.lista.index(elem)
        except ValueError:
            return -1

    def verifica_tipo(self, elem):
        if type(elem) != self.elem_class:
            raise TypeError("Tipo inválido")

    def ordena(self, clave=None):
            self.lista.sort(key=clave)


class DBListaÚnica(ListaÚnica):
    def __init__(self, elem_class):
        super().__init__(elem_class)
        self.borrados = []

    def remove(self, elem):
        if elem.id is not None:
            self.borrados.append(elem.id)
            super().remove(elem)

    def limpia(self):
            self.borrados = []


@total_ordering
class Nombre:
    def __init__(self, nombre):
        self.nombre = nombre

    def __str__(self):
        return self.nombre

    def __repr__(self):
        return "<Clase {3} en 0x{0:x} Nombre: {1} Clave: {2}>".format(
            id(self), self.__nombre, self.__clave, type(self).__name__)

    def __eq__(self, otro):
        return self.nombre == otro.nombre

    def __lt__(self, otro):
        return self.nombre < otro.nombre

    @property
    def nombre(self):
        return self.__nombre

    @nombre.setter
    def nombre(self, valor):
        if nulo_o_vacío(valor):
            raise ValueError("Nombre no puede ser nulo ni en blanco")
        self.__nombre = valor
        self.__clave = Nombre.CreaClave(valor)

    @property
    def clave(self):
        return self.__clave

    @staticmethod
    def CreaClave(nombre):
        return nombre.strip().lower()


class DBNome(Nombre):
    def __init__(self, nombre, id_=None):
        super().__init__(nombre)
        self.id = id_


@total_ordering
class TipoTeléfono:
    def __init__(self, tipo):
        self.tipo = tipo

    def __str__(self):
        return "({0})".format(self.tipo)

    def __eq__(self, otro):
        if otro is None:
            return False
        return self.tipo == otro.tipo

    def __lt__(self, otro):
        return self.tipo < otro.tipo


class DBTipoTeléfono(TipoTeléfono):
    def __init__(self, id_, tipo):
        super().__init__(tipo)
        self.id = id_


class Teléfono:
    def __init__(self, número, tipo=None):
        self.número = número
        self.tipo = tipo

    def __str__(self):
        if self.tipo != None:
            tipo = self.tipo
        else:
            tipo = ""
        return "{0} {1}".format(self.número, tipo)

    def __eq__(self, otro):
        return self.número == otro.número and (
            (self.tipo == otro.tipo) or (
             self.tipo == None or otro.tipo == None))

    @property
    def número(self):
        return self.__número

    @número.setter
    def número(self, valor):
        if nulo_o_vacío(valor):
            raise ValueError("Número no puede ser None o en blanco")
        self.__número = valor


class DBTeléfono(Teléfono):
    def __init__(self, número, tipo=None, id_=None, id_nombre=None):
        super().__init__(número, tipo)
        self.id = id_
        self.id_nombre = id_nombre


class DBTeléfonos(DBListaÚnica):
    def __init__(self):
        super().__init__(DBTeléfono)


class DBTiposTeléfono(ListaÚnica):
    def __init__(self):
        super().__init__(DBTipoTeléfono)


class DBDadoAgenda:
    def __init__(self, nombre):
        self.nombre = nombre
        self.teléfonos = DBTeléfonos()

    @property
    def nombre(self):
        return self.__nombre

    @nombre.setter
    def nombre(self, valor):
        if type(valor) != DBNome:
            raise TypeError("nombre debe ser una instancia de la clase DBNome")
        self.__nombre = valor

    def investigaciónTeléfono(self, teléfono):
        posición = self.teléfonos.investigación(DBTeléfono(teléfono))
        if posición == -1:
            return None
        else:
            return self.teléfonos[posición]


class DBAgenda:
    def __init__(self, banco):
        self.tiposTeléfono = DBTiposTeléfono()
        self.banco = banco
        nuevo = not os.path.isfile(banco)
        self.conexión = sqlite3.connect(banco)
        self.conexión.row_factory = sqlite3.Row
        if nuevo:
            self.crea_banco()
        self.cargaTipos()

    def cargaTipos(self):
        for tipo in self.conexión.execute("select * from tipos"):
            id_ = tipo["id"]
            descripción = tipo["descripción"]
            self.tiposTeléfono.adiciona(DBTipoTeléfono(id_, descripción))

    def crea_banco(self):
        self.conexión.executescript(BANCO)

    def investigaciónNombre(self, nombre):
        if not isinstance(nombre, DBNome):
            raise TypeError("nombre debe ser del tipo DBNome")
        encontrado = self.conexión.execute("""select count(*)
                                             from nombres where nombre = ?""",
                                           (nombre.nombre,)).fetchone()
        if(encontrado[0] > 0):
            return self.carga_por_nombre(nombre)
        else:
            return None

    def carga_por_id(self, id):
        consulta = self.conexión.execute(
            "select * from nombres where id = ?", (id,))
        return carga(consulta.fetchone())

    def carga_por_nombre(self, nombre):
        consulta = self.conexión.execute(
            "select * from nombres where nombre = ?", (nombre.nombre,))
        return self.carga(consulta.fetchone())

    def carga(self, consulta):
        if consulta is None:
            return None
        nuevo = DBDadoAgenda(DBNome(consulta["nombre"], consulta["id"]))
        for teléfono in self.conexión.execute(
                "select * from teléfonos where id_nombre = ?",
                (nuevo.nombre.id,)):
            ntel = DBTeléfono(teléfono["número"], None,
                              teléfono["id"], teléfono["id_nombre"])
        for tipo in self.tiposTeléfono:
            if tipo.id == teléfono["id_tipo"]:
                ntel.tipo = tipo
                break
        nuevo.teléfonos.adiciona(ntel)
        return nuevo

    def lista(self):
        consulta = self.conexión.execute(
            "select * from nombres order by nombre")
        for registro in consulta:
            yield self.carga(registro)

    def nuevo(self, registro):
        try:
            cur = self.conexión.cursor()
            cur.execute("insert into nombres(nombre) values (?)",
                        (str(registro.nombre),))
            registro.nombre.id = cur.lastrowid
            for teléfono in registro.teléfonos:
                cur.execute("""insert into teléfonos(número,
                                                     id_tipo, id_nombre) values (?,?,?)""",
                            (teléfono.número, teléfono.tipo.id,
                             registro.nombre.id))
                teléfono.id = cur.lastrowid
            self.conexión.commit()
        except:
            self.conexión.rollback()
            raise
        finally:
            cur.close()

    def actualiza(self, registro):
        try:
            cur = self.conexión.cursor()
            cur.execute("update nombres set nombre=? where id = ?",
                        (str(registro.nombre), registro.nombre.id))
            for teléfono in registro.teléfonos:
                if teléfono.id is None:
                    cur.execute("""insert into teléfonos(número,
                                                         id_tipo, id_nombre)
                                                         values (?,?,?)""",
                                (teléfono.número, teléfono.tipo.id,
                                 registro.nombre.id))
                    teléfono.id = cur.lastrowid
                else:
                    cur.execute("""update teléfonos set número=?,
                                                        id_tipo=?, id_nombre=?
                                                    where id = ?""",
                                (teléfono.número, teléfono.tipo.id,
                                 registro.nombre.id, teléfono.id))
                    for borrado in registro.teléfonos.borrados:
                        cur.execute("delete from teléfonos where id = ?", (borrado,))
                    self.conexión.commit()
                    registro.teléfonos.limpia()
        except:
            self.conexión.rollback()
            raise
        finally:
            cur.close()

    def borra(self, registro):
        try:
            cur = self.conexión.cursor()
            cur.execute("delete from teléfonos where id_nombre = ?",
                        (registro.nombre.id,))
            cur.execute("delete from nombres where id = ?",
                        (registro.nombre.id,))
            self.conexión.commit()
        except:
            self.conexión.rollback()
            raise
        finally:
            cur.close()


class Menú:
    def __init__(self):
        self.opciones = [["Salir", None]]

    def adicionaopción(self, nombre, función):
        self.opciones.append([nombre, función])

    def exhibe(self):
        print("====")
        print("Menú")
        print("====\n")
        for i, opción in enumerate(self.opciones):
            print("[{0}] - {1}".format(i, opción[0]))
        print()

    def ejecute(self):
        while True:
            self.exhibe()
            elija = valida_franja_entero("Elija una opción: ",
                                         0, len(self.opciones) - 1)
            if elija == 0:
                break
            self.opciones[elija][1]()


class AppAgenda:
    @staticmethod
    def pide_nombre():
        return(input("Nombre: "))

    @staticmethod
    def pide_teléfono():
        return(input("Teléfono: "))

    @staticmethod
    def muestra_datos(datos):
        print("Nombre: %s" % datos.nombre)
        for teléfono in datos.teléfonos:
            print("Teléfono: %s" % teléfono)
        print()

    @staticmethod
    def muestra_datos_teléfono(datos):
        print("Nombre: %s" % datos.nombre)
        for i, teléfono in enumerate(datos.teléfonos):
            print("{0} - Teléfono: {1}".format(i, teléfono))
        print()

    def __init__(self, banco):
        self.agenda = DBAgenda(banco)
        self.menú = Menú()
        self.menú.adicionaopción("Nuevo", self.nuevo)
        self.menú.adicionaopción("Altera", self.altera)
        self.menú.adicionaopción("Borra", self.borra)
        self.menú.adicionaopción("Lista", self.lista)
        self.ultimo_nombre = None

    def pide_tipo_teléfono(self, estándar=None):
        for i, tipo in enumerate(self.agenda.tiposTeléfono):
            print(" {0} - {1} ".format(i, tipo), end=None)
            t = valida_franja_entero("Tipo: ", 0,
                                     len(self.agenda.tiposTeléfono) - 1, estándar)
        return self.agenda.tiposTeléfono[t]

    def investigación(self, nombre):
        if type(nombre) == str:
            nombre = DBNome(nombre)
        dato = self.agenda.investigaciónNombre(nombre)
        return dato

    def nuevo(self):
        nuevo = AppAgenda.pide_nombre()
        if nulo_o_vacío(nuevo):
            return
        nombre = DBNome(nuevo)
        if self.investigación(nombre) != None:
            print("¡Nombre ya existe!")
            return
        registro = DBDadoAgenda(nombre)
        self.menú_teléfonos(registro)
        self.agenda.nuevo(registro)

    def borra(self):
        nombre = AppAgenda.pide_nombre()
        if(nulo_o_vacío(nombre)):
            return
        p = self.investigación(nombre)
        if p != None:
            self.agenda.borra(p)
        else:
            print("Nombre no encontrado.")

    def altera(self):
        nombre = AppAgenda.pide_nombre()
        if(nulo_o_vacío(nombre)):
            return
        p = self.investigación(nombre)
        if p != None:
            print("\nEncontrado:\n")
            AppAgenda.muestra_datos(p)
            print("Digite enter en caso de que no quiera alterar el nombre")
            nuevo = AppAgenda.pide_nombre()
            if not nulo_o_vacío(nuevo):
                    p.nombre.nombre = nuevo
            self.menú_teléfonos(p)
            self.agenda.actualiza(p)
        else:
                print("¡Nombre no encontrado!")

    def menú_teléfonos(self, datos):
        while True:
            print("\nEditando teléfonos\n")
            AppAgenda.muestra_datos_teléfono(datos)
            if(len(datos.teléfonos) > 0):
                print("\n[A] - alterar\n[D] - borrar\n", end="")
                print("[N] - nuevo\n[S] - salir\n")
                operación = input("Elija una operación: ")
                operación = operación.lower()
                if operación not in ["a", "d", "n", "s"]:
                        print("Operación inválida. Digite A, D, N o S")
                        continue
                if operación == 'a' and len(datos.teléfonos) > 0:
                    self.altera_teléfonos(datos)
                elif operación == 'd' and len(datos.teléfonos) > 0:
                    self.borra_teléfono(datos)
                elif operación == 'n':
                    self.nuevo_teléfono(datos)
                elif operación == "s":
                    break

    def nuevo_teléfono(self, datos):
        teléfono = AppAgenda.pide_teléfono()
        if nulo_o_vacío(teléfono):
            return
        if datos.investigaciónTeléfono(teléfono) != None:
            print("Teléfono ya existe")
        tipo = self.pide_tipo_teléfono()
        datos.teléfonos.adiciona(DBTeléfono(teléfono, tipo))

    def borra_teléfono(self, datos):
        t = valida_franja_entero_o_blanco(
            "Digite la posición del número a borrar, enter para salir: ",
            0, len(datos.teléfonos) - 1)
        if t == None:
            return
        datos.teléfonos.remove(datos.teléfonos[t])

    def altera_teléfonos(self, datos):
        t = valida_franja_entero_o_blanco(
            "Digite la posición del número a alterar, enter para salir: ",
            0, len(datos.teléfonos) - 1)
        if t == None:
            return
        teléfono = datos.teléfonos[t]
        print("Teléfono: %s" % teléfono)
        print("Digite enter en caso de que no quiera alterar el número")
        nuevoteléfono = AppAgenda.pide_teléfono()
        if not nulo_o_vacío(nuevoteléfono):
            teléfono.número = nuevoteléfono
        print("Digite enter en caso de que no quiera alterar el tipo")
        teléfono.tipo = self.pide_tipo_teléfono(
            self.agenda.tiposTeléfono.investigación(teléfono.tipo))

    def lista(self):
        print("\nAgenda")
        print("-" * 60)
        for e in self.agenda.lista():
            AppAgenda.muestra_datos(e)
        print("-" * 60)

    def ejecute(self):
        self.menú.ejecute()


if __name__ == "__main__":
    if len(sys.argv) > 1:
        app = AppAgenda(sys.argv[1])
        app.ejecute()
else:
    print("Error: nombre del banco de datos no informado")
    print("           agenda.py nombre_del_banco")
Haga clic aquí para bajar el archivo