Flask, micro framework web
Hace un tiempo que vengo utilizando Flask como framework, periodo que coincide con mi retorno a desarrollar después de un largo rato. Mis primeras impresiones fueron bastante favorables, y creo que entre las principales causas estaban su sencillez y buena documentación ideal para superar mi abstinencia de desarrollo.
Flask se describe como un micro framework y para sustentar este extraño nombre que mejor que analizar las decisiones que se tomaron al diseñar el framework entre las que destacan manter la simplicidad en el desarrollo y no tomar muchas decisiones por el desarrollador que en lo concreto se traduce en un kit de herramientas que incluye muy pocas cosas, y aunque esto último parece a primera vista ser algo negativo en la práctica no lo es.
Se incluyen dentro del framework una excelente linea base de funcionalidades que están presentes dentro de la mayoría de nuestros desarrollos como son enrutamiento, manejo de plantillas, manejo de errores HTTP, testing, entre otras además incluye un herramienta de depuración que permite acceder a un shell Python cuando se presentan los problemas.
Si bien las cosas que se incluyen por defecto son pocas es posible extender Flask y para ello dentro del mismo sitio podemos encontrar extensiones para trabajar con base de datos, administración, OpenId, forms y muchas otras.
Los pilares de Flask son dos herramientas ampliamente utilizadas: Werkzeug y Jinga2, el primero es una librería WSGI el estándar Python para comunicar frameworks y servidores Web, el segundo es un motor para manejar plantillas de alto rendimiento que según benckmarks sería mejor que el que incluye DJango.
La simplicidad del framwork se puede observar desde el primer ejemplo:
from flask import Flask app = Flask(__name__) @app.route('/') def index(): return 'Index Page' @app.route('/hello') def hello(): return 'Hello World' if __name__ == "__main__": app.run()
A primera vista una de las cosas que destacan es el enrutamiento el cuál esta basado en decoradores lo que hace muy fácil de entender el código. Otro punto es la ausencia del contexto request en las vistas, esto se debe a que existe una instancia global del objeto request la cuál esta asociada al hilo de ejecución de nuestra aplicación mediante la utilización de ThreadLocal, esto si bien presenta algunos inconveniente la mayoría de nuestras aplicaciones trabajara sin problemas con esto.
Encuentro Linux y Startechconf
Las últimas semanas han sucedido dos eventos que me han tenido del todo entretenido, El Encuentro Linux 2011 y la Startechconf. En ambos he participado de distinta forma pero de ambos he rescatado grandes cosas que me hacen estar contento y esperar con ansia las próximos próximas versiones.
En el Encuentro Linux 2011 me toco participar como charlista, junto a @felipeDev expusimos nuestro tema "Desarrollo Ágil de Aplicaciones Móviles". Esta experiencia fue de un crecimiento personal enorma ya que jamás antes había dado una charla en un evento, desde el minuto de la postulación aprendes ya que debes resumir el tema en poca lineas para presentarlo al equipo a cargo de evaluar si tu charla merece la pena estar. Lo que siguió después fue un periodo de intenso nerviosismo ya que la charla implicaba no solo dominar el tema sino que presentarlo de una forma que fuera comprensible a un público para mi desconocido. En lo personal tuve que ensayar y tratar de corregir mi modulación y mi presentación sufrió ajustes hasta el último minuto.
Desde mi llegada a Puerto Montt la diversion comenzó al compartir con un grupo humano de primer nivel en una organización impecable, me toco conocer a personas interesantes no sólo en los aspectos técnicos, pude acompañar y disfrutar del resto de las charlas. El tan esperado momento de mi charla llego y con él vino un relajo y al final salió todo excelente.
En Startechconf me toco estar del lado opuesto, como asistente puede empaparme de los conocimientos que entregaron unos charlistas de primer nivel, fue una tremenda conferencia y se noto el gran esfuerzo que puso el equipo organizador. Los charlas mostraron temas tan interesantes como CCS3, HTML5, Ruby, Lean Startup entre muchos otros.
Ha pasado un mes desde que deje mi vida de asalariado pero la libertad que me ha dado este nuevo periodo me ha permitido retomar mis grandes pasiones y aunque es muy temprano para evaluar mi decisión desde el punto de vista economico debo decir que en lo personal me siento creciendo por primera vez desde hace mucho tiempo, entonces la invitación es a participar de la comunidad y aprovechar la gran cantidad que hoy existe de interactuar y aprender, de seguro se sentirán tan afortunados como yo me siento hoy.
Django con Admin MultiSite
Tengo que implementar una aplicación que requiere la administración autónoma de datos por cliente, revisando el administrador de DJango encontré que soporta la administración de múltiples sitios pero requiere de algunos ajustes para que los usuarios no vean los datos entre si.
Lo primero y más fácil es incluir como llave foránea el modelo site a nuestros modelos:
from django.contrib.sites.models import Site class Empresa(models.Model): site = models.ForeignKey(Site) nombre = models.CharField(max_length=100)
Ahora modificamos el admin para que elimine la selección del sitio y se grabe automáticamente el sitio asociado al usuario que autenticado:
class EmpresaAdmin(admin.ModelAdmin): exclude = ['site'] def save_model(self, request, obj, form, change): obj.site = Site.objects.get(pk=setting.SITE_ID) obj.save()
Además filtramos los datos para que se muestren sólo aquellos datos asociados al usuario autenticado.
def queryset(self, request): qs = super(EmpresaAdmin, self).queryset(request) if request.user.is_superuser: return qs return qs.filter(site__pk=settings.SITE_ID)
Lo anterior se repite con cada modelo que quieras multi "sitio". Hasta aquí funciona más o menos la cosa, y que SITE_ID es una variable en “duro” y lo que nos interesaría es modificarla por cada usuario que se autentica o en base al dominio consultado, yo optare por la segunda opción, para ello modificare el archivo settings.py:
cambiar la linea:
SITE_ID = 1por:
SITE_ID = SiteIDHook()
Ahora tenemos que definir la clase SiteIDHook como sigue:
from threading import local SITE_THREAD_INFO = local() class SiteIDHook: def __int__(self): try: return SITE_THREAD_INFO.SITE_ID except AttributeError: SITE_THREAD_INFO.SITE_ID = 1 return SITE_THREAD_INFO.SITE_ID def __hash__(self): return self.__int__() def get(self): return SITE_THREAD_INFO.SITE_ID def set(self, value): SITE_THREAD_INFO.SITE_ID = value
La información del sitio ahora se almacena en una variable del thread local, ahora sólo tenemos que cambiar el valor de esta variable de forma dinamica para ello creamos un nuevo middleware:
import settings class SiteDetectionMiddleware: def process_request(self, request): ### Aquí alguna lógica para detectar el sitio ### ### por ahora sólo estableceré un valor en duro ### settings.SITE_ID.set(1) Registramos el nuevo middleware: MIDDLEWARE_CLASSES = ( 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'auth.middleware.SiteDetectionMiddleware', # Se agrega esta linea )
Por último me interesa que los usuarios se autentiquen con su correo, así deseo que se de la impresión de que no se repiten los nombres de usuario para cada cliente, para ello creare un archivo el archivo auth/backends.py con el siguiente código:
class SiteDomainModelBackend(ModelBackend): def authenticate(self, username=None, password=None): if '@' in username: kwargs = {'email': username} else: kwargs = {'username': username} try: user = User.objects.get(**kwargs) if user.check_password(password): return user except User.DoesNotExist: return None
Y registramos el nuevo backend:
AUTHENTICATION_BACKENDS = ( 'django.contrib.auth.backends.ModelBackend', 'proyecto.auth.backends.SiteDomainModelBackend', )
Y bueno ya tenemos el administrador multi sitio salvo por un problema: el modelo usuario, aunque existe la posibilidad de extender el modelo y agregar la llave hacia el modelo "Site" se debe modificar algunas cosas para que tome por defecto la nueva clase, esto me ha dado algunos problemas y no lo tengo resuelto aún.
Pueden revisar los siguientes enlaces que tome como referencia y tienen algunos de estos ejemplos:
Documentación en DJango para múltiples sitios
Extending the django user model with inheritance
Doing more with the Django admin
Add a button to Django admin to login as a user
Lista de deseos
Comienza el año y vienen las reflexiones sobre el año que dejamos atrás, cosas que hicimos y los momentos gratos que tuvimos, en todo ámbito. Vienen también los buenos deseos, las metas y objetivos que cumpliremos este año que inicia.
El último cuarto del año que paso estuvo lleno de cosas nuevas, las reuniones organizadas por un grupo de entusiastas chilenos que emergen del ya cansado, temeroso y muchas veces vendido mundillo informático nacional me han hecho entusiasmarme, recuperar esta pasión por desarrollar aplicaciones y aprender nuevas cosas, sentir que hay vida inteligente allá afuera, que hay esperanza por sobre las grandes corporaciones y por sobre la desidia y mediocridad imperante en el medio.
Claro quedan muchas cosas por aprender, todo esta cambiando y si quieres ser parte del mercado en los próximos años no te queda otra alternativa que cambiar también, razones existen muchas pero te invito a hacerlo simplemente por gusto, por diversión, el resto llegará sólo.
Yo por mi parte aquí dejo algunas de las cosas que me gustaría aprender este año, la lista esta incompleta en absoluto pero espero ir llenándola en el transcurso de las próximas semanas y volveré a ella a través de todo el año y espero tachar todos los conceptos en ella o al menos la mayoría, se viene un año lleno de desafíos y diversión, espero se atrevan a acompañarme en tan entretenido desafío.
Lista de deseos 2011:
- Aprender OpenID y todas las apis de autenticación
- Aprender FubuMVC para enseñarles algo interesante a los programadores .NET en la pega
- Retomar Ruby on rails para no perderme en los meetups
- Aprender GIT
- Aprender Jython para utilizar el poder de java con python
- CoffeScript por que se ve bueno
- Spring
- Appclerator Titanium
- Hadoop
- Tocar guitarra de una vez
- Hablar en inglés (bien)
Implementando autenticación con DJango+Google
He estado jugando estos días con la implementación de Google para autenticar sitios Web, por alguna razón no se me hizo fácil al principio pero después de leer un rato y averiguar más ya hay humo blanco, es de esas cosas que al principio las encuentras complejas y luego te das cuenta que no podía ser más fácil, como siempre gracias a la comunidad mediante librerías que facilitan el trabajo.
Google basa su API en el estandar OpenID que brinda métodos descentralizados de autenticación, permitiendos entre otras cosas autenticarnos a múltiples sitios utilizando las mismas credenciales. A modo general el esquema OpenID se compone de tres elementos principales: El proveedor de OpenID, que es quien nos facilita el servicio de autenticación, el Relying Party, que vendría siendo el tercero o quien quiera autenticar a un usuario, y por último se encuentra el usuario.
En mi caso yo estoy realizando como ejemplo un sitio Web que necesita autenticar a usuarios, pero no quiero crear mi propio registro de usuarios y sistema de autenticación, en su lugar voy a utilizar a Google como proveedor OpenID. La implementación la estoy realizando DJango y me he bajado la librería django-openid-auth. Existen varias otras pero es la que he encontrado más sencilla y funcional.
Una de las primeras cosas que realice fue registrar el dominio en Google, no estoy seguro si es requerido para utilizar los servicios de autenticación pero en mi caso deseo utilizar otros servicios futuro que si requieren una autorización de domino para comenzar a operar como GMail o Google Calendars.
Cuando te bajas las librerías de django-openid-auth, la cosa se vuelve casi monótona, claro y si es así te preguntaras porqué lo escribo... bueno una vez que entiendes es fácil pero hay varias cosas que pasan por debajo que es mejor comprender.
Lo primero es presentarle la aplicación a DJango, para ello la inscribimos dentro de las aplicaciones instaladas en el archivo settings.py:
INSTALLED_APPS = ( 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django_openid_auth', )
Luego nos corresponde agregar un Backend especial para que DJango pueda realizar la autenticación con OpenID. Los Backend le informan a DJango o más bien implementan la lógica propia del proceso de autenticación, por ejemplo el backend estándar es manejado por DJango contra una base de datos utilizando el modelo User.
# Nada de esto existe, así que debemos agregar todo el codigo AUTHENTICATION_BACKENDS = ( 'django_openid_auth.auth.OpenIDBackend', 'django.contrib.auth.backends.ModelBackend', )
Cuando un usuario nuevo llega a registrase a nuestro sitio, DJango intentará autenticar al usuario utilizando OpenIDBackend, como la autenticación y el registro de usuarios son dos cosas diferentes DJango arrojará un error indicándonos que el usuario no existe, una forma de solucionar este detalle es agregar la siguiente línea para que DJango cree automáticamente los usuarios:
OPENID_CREATE_USERS = TrueEsta última línea unida al OpenIDBackend me molesta particularmente ya que en mi caso particular quería crear una autenticación independiente a la de DJango, esto me creará a los usuarios que se registren como usuarios entandar en DJango.
Lo siguiente es decirle a nuestra implementación que actualice los datos del usuarios DJango con los que encuentre en el proveedor OpenID, esto dependerá de cada proveedor pero hay algunos datos básicos que nos entrega la implementación de Google como son el correo y el nombre del usuario. Si queremos que esto suceda de forma automática agregamos lo siguiente:
OPENID_UPDATE_DETAILS_FROM_SREG = TrueConfiguramos la página de retorno para cuando el proceso de autenticación termine:
LOGIN_REDIRECT_URL = '/'Por último informamos la URL de nuestro proveedor OpenID:
OPENID_SSO_SERVER_URL = 'https://www.google.com/accounts/o8/id'Extendiendo manage.py para sincornizar con Flex
Estamos haciendo un sistema que tendrá como interfaces algunas pantallas en Flex por lo que ha surgido la necesidad de publicar algunos servicios remotos utilizando PyAMF. En Flex utilizamos Cairngorm como MVC, este requiere la definición de un archivo de servicios Services.xml que estoy intendando generar utilizando Python y que mejor que hacerlo utilizando el mismo estilo que utiliza DJango para la sincronización de la base de datos. Por ello estoy intenando generar una opción nueva el comando manage.py de la aplicación DJango:
manage.py syncflex
Gracias a la flexibilidad de DJango esto es bastante fácil nada más debes crear una clase que debes llamar Command que extienda a la clase BaseCommand y ubicarla en paquete managment/commands bajo alguna de tus aplicaciones en mi caso yo estoy haciendo un pequeño ERP así que la ubique en la ruta flex/managment/commands:

En mi caso dado que mi comando no tiene argumentos extendere la clase NoArgsCommand :
class Command(NoArgsCommand): help = "Sync Flex Commands" requires_model_validation = False def handle_noargs(self, **options): # Your code here
El método handle_noargs es el que nos permite ejecutar las acciones realizadas por nuestro comando cuando es invocado, en mi caso la implementación es la siguiente (algo básica por ahora):
def handle_noargs(self, **options): from pyamf.remoting.gateway.django import DjangoGateway import sys project = settings.ROOT_URLCONF.split('.')[0] #import types for app in models.get_apps(): if project == app.__name__.split('.')[0]: app_name = app.__name__.split('.')[-2] package = project + '.' + app_name + '.gateway' try: mod = __import__(package, globals(), locals(), ['gateway'], -1) for d in dir(mod): attr = getattr(mod, d) try: if isinstance(attr, DjangoGateway): self.processClass(d, attr) except TypeError: pass except ImportError: pass
Como se puede ver su funcionamiento se basa en introspección, para ello busco un módulo llamado gateway dentro de mis aplicaciones DJango una vez que encuentro el módulo busco en él las clases que extiendan a DjangoGateway para generar el archivo Services.xml. Un ejemplo de mi archivo gateway.py es:
from pyamf.remoting.gateway.django import DjangoGateway from django.contrib.auth.models import User from sampleapp.contabilidad.models import Comprobante from sampleapp.gateway.amf import AMFGateway class ComprobanteGateway(AMFGateway): def obtenerComprobantes(self, request, data): # Implemetnar pass class CuentaGateway(AMFGateway): def obtenerCuentas(self, request, data): # Implemetnar pass contabilidadGateway = DjangoGateway({'gwComprobante' : ComprobanteGateway, 'gwCuenta' : CuentaGateway,})
Si vemos la implementación del método processClass tenemos lo siguiente:
def processClass(self, name, gateway): import inspect print ("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" "<cairngorm:ServiceLocator xmlns:mx=\"http://www.adobe.com/2006/mxml\"" "xmlns:cairngorm=\"com.adobe.cairngorm.business.*\">\n") for g in gateway.services: print ("<mx:RemoteObject id=\"" + g + "\" " "\n\t\tdestination=\"" + g + "\" " "\n\t\tendpoint=\"" + settings.REMOTING_URL + "/" + name + "\" " "\n\t\tshowBusyCursor=\"true\" " "\n\t\tresult=\"event.token.resultHandler( event );\" " "\n\t\tfault=\"event.token.faultHandler( event );\">") service = gateway.services[g] for m in service.getMethods(): print "\t<mx:method name=\"" + m + "\" />" print "</mx:RemoteObject>\n" print "</cairngorm:ServiceLocator>"
Con eso ya estamos casí listos sólo nos falta agregar nuestra aplicación para que DJango la conozca y definir la variable REMOTING_URL para ello sólo editamos nuestro archivo settings.py:
INSTALLED_APPS = ( 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.sites', 'django.contrib.admin', 'sampleapp.contabilidad', 'sampleapp.compra', 'sampleapp.agricola', 'sampleapp.rrhh', 'sampleapp.maquinaria', 'sampleapp.comun', 'sampleapp.actividad', 'sampleapp.flex', ) # -------------------------------------------------- # My customs settings # -------------------------------------------------- REMOTING_URL = "http://localhost:8080/remoting"
Con eso ya estamos listo ahora sólo ejecutamos manage.py syncflex en la consola:
manage.py syncflex
Aquí una muestra del resultado:
<?xml version="1.0" encoding="utf-8"?> <cairngorm:ServiceLocator xmlns:mx="http://www.adobe.com/2006/mxml"xmlns:cairngorm="com.adobe.cairngorm.business.*"> <mx:RemoteObject id="gwCuenta" destination="gwCuenta" endpoint="http://localhost:8080/remoting/contabilidadGateway" showBusyCursor="true" result="event.token.resultHandler( event );" fault="event.token.faultHandler( event );"> <mx:method name="obtenerCuentas" /> </mx:RemoteObject> <mx:RemoteObject id="gwComprobante" destination="gwComprobante" endpoint="http://localhost:8080/remoting/contabilidadGateway" showBusyCursor="true" result="event.token.resultHandler( event );" fault="event.token.faultHandler( event );"> <mx:method name="obtenerComprobantes" /> </mx:RemoteObject> </cairngorm:ServiceLocator>
Complementos de búsqueda con OpenSearch
Hace poco escribí algunas extensiones utilizando OpenSearch. OpenSearch es un conjunto de estándares que permiten la publicación de los resultados de los sistemas de búsqueda permitiendo de este modo la sindicación y la agregación de contenidos entre diferentes motores y sistema de búsqueda. OpenSearch se compone de 3 partes, un formato para la descripción de motores de búsqueda, un formato para la descripción de los resultados de búsqueda llamado OpenSearch Response y finalmente los agregadores o sitios capaces de mostrar resultados en formato OpenSearch.
Navegadores como IE7 y Firefox soportan ya OpenSearch, es decir, permiten la incorporación de complementos de búsqueda mediante este estándar, al mismo tiempo cada uno de estos navegadores extiende las capacidades estándares de OpenSearch agregándole sus propios sabores.
La utilización de OpenSearch permite incorporar algo de usabilidad a tus sitios Web, si es que, los mismos presentan buscadores a los usuarios. Yo en mi caso particular utilizo un diccionario en inglés de la empresa WordMagic, este diccionario presenta un buscador en su portada mediante el cuál puedes acceder a las palabras del diccionario. Dado que a mí me resultaba bastante molesto ingresar al sitio para realizar la búsqueda es que me decidí a hacer este complemento, además es muy sencillo hacerlo y lo puedes incorporar también en tus sitios. A continuación veremos este ejemplo:
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/" xmlns:moz="http://www.mozilla.org/2006/browser/search/"> <ShortName>WordMagic</ShortName> <Description>Word Magic Software, Inc. es una compañia dedicada al desarrollo de software con sede en Houston, Texas.</Description> <InputEncoding>UTF-8</InputEncoding> <Image width="16" height="16" type="image/x-icon">http://wordmagicsoft.com/favicon.ico</Image> <Url type="text/html" method="POST" template="http://wordmagicsoft.com/diccionario/search.php"> <Param name="word_to_search" value="{searchTerms}"/> <Param name="search_option" value="es-en"/> </Url> </OpenSearchDescription>
Los parámetros especificados son los siguientes:
- OpenSearchDescription: Este es el nodo principal de un documento OpenSearch.
- ShortName: Un pequeño titulo que identifique al buscador.
- Description: Una descripción más larga acerca del buscador.
- InputEncoding: El encoding que soporta nuestro el buscador.
- Image: La url de algún icono representativo de nuestro buscador.
- URL: Describe la interfaz para nuestro buscador, o sea aquella que responderá a las búsquedas. El atributo template permite especificar la url y el atributo method permite indicar si se hará un GET o un POST a la url.
Eso es todo! Guardamos el contenido en un archivo xml, en mi caso le puse wordmagic.xml y ya esta! ahora solo nos queda agregarlo a nuestro navegador, para ello utilizaremos un pequeño javascript, para ello puedes construirte una página con lo siguiente:
<SCRIPT LANGUAGE="JavaScript"> <!-- function addWordMagicSearch() { window.sidebar.addSearchEngine("http://developer.cl/opensearch/wordmagic.xml", "http://wordmagicsoft.com/favicon.ico", "WordMagic", ""); } //--> </SCRIPT>
Luego solo tienes que agregar la llamada en el HTML:
<a href="#" onclick="addWordMagicSearch();">Agregar búsqueda en WordMagic</a>
Mira aquí esta el mio.
Twitter, algo útil despues de todo.
Antes pensaba que twitter era una perdida de tiempo ¿para que diablos alguien seguirá a otros en una especie de reality virtual con sólo 140 caracteres por escena? Ahora lo llevo utilizando una par de días y lo encuentro de una simpleza similar a del.icio.us pero con un potencial de igual magnitud: acceso a información de primera línea, una mirada sobre el hombro de gente que la lleva en determinados temas. Me quede pensando en esto luego de descubrir Watin ojeando twitter. Con una rápida mirada te enteras del diario vivir de gente que consideras un referente, sin quererlo una especie de consejo, un mini-blog, en fin, una herramienta útil para un aprendiz de Jedi como yo.
Watin, mejor que Selenium pero sólo para .NET
Acabo de encontrar a Watin, una herramienta OpenSource para la automatización de pruebas mediante la imitación o grabación de las secuencias de interacción sobre una página Web, según un experto en el tema es mejor que Selenium, al parece su uso es bastante sencillo y soporta Internet Explorer y Firefox. Lamentablemente sirve solo para aplicaciones .NET, así que por ahora no me queda otra que seguir intentando configurar Selenium.
Documentación que entorpece la calidad
¿Hasta cuando será suficiente documentar el código? Uno de nuestros clientes revisa detenidamente el código fuente en las entregas, o por los menos a primera vista eso parece, sin embargo, me he dado cuenta que el foco principal de la revisión es la documentación del código, esto prosupuesto no tiene nada de extraño ni de malo, pero en mi opinión se ha perdido un aspecto importante de la revisión: la calidad del código entregado.
Cada día me encuentro con código duplicado, extensas funciones y métodos o nulo control de errores, es evidente que las revisiones ponen mas énfasis en la forma que en el fondo, dejando atrás los muchos otros aspectos que hablan acerca de la calidad del software. Bajo este planteamiento los mejores códigos serán aquellos que están más documentados, sin considerar aspectos como el tamaño, la complejidad de los algoritmos, el uso eficiente de recursos, entre muchas otras cosas.
Hoy en día el escenario global resalta otras metodologías y prácticas, una de ellas es el refactoring, definido de forma simple como la mejora del diseño del código existente, el cuál debido al acoplamiento existente entre la documentación y su código, se ve entorpecido; en cierto sentido la documentación desincentiva la iniciativa de refactorizar poniendo barreras a la mejora.
Es indudable que la documentación del código nos ahorrara varios dolores de cabeza y frustración pero cuando nos nubla la vista y adormece nuestros sentidos es hora de preocuparse.








