Contenido


Construcción de una Aplicación de Conversación con Pyramid, SQLDB y IBM Cloud

Utilice SQLAlchemy para integrarse a los servicios de base de datos en IBM Cloud

Comments

Si posee conocimientos básicos de Python y desea desarrollar aplicaciones web desde cero y desplegarlas en® IBM Cloud®, entonces, este tutorial es para usted. Aprenda acerca de desarrollo con la infraestructura de Pyramid y su modelo de seguridad, comunicación en tiempo real con la captación de Socket.IO e integración con SQLAlchemy. SQLAlchemy ofrece una manera fácil de integrar servicios de bases de datos al entorno de IBM Cloud.

Al añadir una función a la vez de manera sistemática, usted crea una aplicación de conversación Chatter en donde los usuarios pueden intercambiar mensajes en tiempo real.Los mensajes se almacenan en una base de datos. Luego de iniciar sesión, los usuarios ven los últimos 10 mensajes intercambiados en la aplicación. El inicio de sesión es solo para la identificación— sin contraseñas, pero el enfoque se puede aplicar para requisitos típicos de autenticación y autorización.

Qué necesita para construir su aplicación

Ejecutar la aplicaciónObtener el Código

Paso 1. Instalar Pyramid y el cliente de CF

Para la instalación de Ubuntu/Debian:

  1. Descargue el cliente de CF como Stable Installers para Debian.
  2. Como raíz, instale el cliente de CF: dpkg -i cf-cli_*.deb.
  3. Como raíz, instale el gestor de paquetes, el entorno de desarrollo y el entorno virtual de Python: apt-get install python-pip python-dev python-virtualenv.
  4. Como usuario regular, configure un entorno virtual personal: virtualenv $HOME/venv.
  5. Active su entorno virtual: . $HOME/venv/bin/activate.

    Suministrar el archivo activo garantiza que Python y pip se utilicen desde el entorno virtual.

  6. Instale la infraestructura de Pyramid: pip install pyramid.

Para la instalación en Windows® :

  1. Descargue el cliente de CF como Stable Installers para Windows.
  2. Descomprima y ejecute el archivo cf_installer.exe.
  3. Descargue el instalador de Python MSI para su plataforma.
  4. Al instalar el archivo descargado, seleccione Instalar solo para mi en la primera página y añadaAñadir python.exe a la ruta en la página Personalizar Python.
  5. Descargue e instale MS Visual C++ Compiler para Python 2.7.
  6. Abra un prompt de MS-DOS e instale la infraestructura de Pyramid: pip install pyramid.

Paso 2. Crear un primer borrador de Chatter

Pyramid proporciona distintas plantillas (matrices). Utilice la matriz de SQLAlchemy para crear un proyecto de Pyramid para este ejemplo:

 pcreate -s alchemy Chatter cd Chatter

Cambié el directorio actual por el directorio creado porque este tutorial asume rutas relacionadas a ese directorio.

Utilice los siguientes archivos:

ArchivoDescripción
development.iniArchivo de configuración para ejecutar la aplicación en modo de desarrollo
production.iniArchivo de configuración para ejecutar la aplicación en modo de producción
setup.pyScript para configurar la aplicación antes de que pueda ejecutarse y para empaquetarla para distribución
chatter/__init__.pyContiene el código para configurar la aplicación
chatter/models.pyContiene el modelo de aplicación (clases que manejan el acceso a una base de datos)
chatter/views.pyContiene métodos view-callable que controlan la lógica de la aplicación
chatter/static directoryContiene recursos estáticos utilizados por la aplicación
chatter/templates directoryContiene plantillas de página utilizadas por la aplicación

El proyecto utiliza paquetes de referencia a sí mismo. Para que las referencias funcionen correctamente, instálelas en modo de desarrollo, lo que también instala todas las dependencias extras descritas en la lista exigida en el archivo setup.py:

 python setup.py develop

Inicie la aplicación:

pserve production.ini

Este comando informa el proceso que ejecuta la aplicación y el URL desde el cual se puede acceder a la aplicación, luego, espera para ser detenido con la combinación de teclas <Ctrl>-C.

La configuración de servidor predeterminada en production.ini es escuchar en el puerto 6543 (consulte el parámetro port en el archivo), así que acceda a ella desde un navegador en http://localhost:6543.

En vez de una página sofisticada, el navegador muestra un mensaje que dice que las tablas de la base de datos no han sido inicializadas. En lugar de un paso separado para crear las tablas de la base de datos, para esta aplicación, recomiendo que lo haga al inicializar la aplicación. En el archivo chatter/__init__.py, cambie la línea Base.metadata.bind = engine a:

 Base.metadata.create_all(bind = engine)

Se crean todas las tablas que extienden la clase Base pero solo si aún no existen. De manera predeterminada, el archivo production.ini tiene sqlalchemy.url = sqlite:///%(here)s/Chatter.sqlite especificada para la base de datos, lo que significa que todos los datos se almacenan en el archivo Chatter.sqlite. Siempre que cambie el modelo, solo debe eliminar ese archivo antes de iniciar la aplicación.

Como no hay un paso separado para crear tablas en la base de datos, elimine el directorio chatter/scripts:
rm -rf chatter/scripts (Unix) o
rmdir /s/q chatter\scripts (Windows)

En este tutorial, actualizará archivos estáticos, así que vamos a deshabilitar su memoria caché (la habilitará al final). En chatter/__init__.py en la llamada config.add_static_view() , cambie el parámetro cache_max_age=3600 a cache_max_age=0.

Inicie la aplicación y acceda a ella a través de un navegador, en donde podrá ver la página de presentación de la matriz Pyramid SQLAlchemy.

Añada la clase Message al modelo

Los mensajes en la aplicación se almacenan en una base de datos. La estructura de datos que se debe utilizar está establecida en el archivo chatter/models.py. El archivo ya contiene un ejemplo de clase, pero debe cambiarlo para algo más útil para su finalidad. Elimine todas las líneas que empiezan por class MyModel(Base): hasta el final del archivo. Y añada las siguientes líneas:

 import datetime from sqlalchemy import ( Sequence, DateTime, ) class Message(Base): __tablename__ = 'messages' id = Column(Integer, Sequence('msid', optional=True), primary_key=True) text = Column(Text, nullable=False) created = Column(DateTime, default=datetime.datetime.utcnow) def __init__(self, text): self.text = text

Este código establece una clase para acceder a la tabla 'messages' (indicada por el atributo __tablename__ ), que contiene tres columnas: id (clave primaria, tipo entero), text (tipo texto) y created (tipo FechaHora). La clase extiende la clase Base (declarada en el archivo chatter/model/meta.py) para integrarla a la infraestructura de SQLAlchemy y añadirla a los metadatos conservados por la instancia Base . La llamada Sequence() hace que la columna id tenga un valor con incremento automático.

El atributo created utiliza el valor predeterminado de la hora en que se creó la instancia. Yo utilicé datetime.datetime.utcnow para almacenar la hora con un valor independiente del huso horario. Además, observe que hay una referencia a una función y no una llamada a esa función. Si la transformó en llamada al cambiarla a datetime.datetime.utcnow(), el valor predeterminado es la declaración de hora cuando se inicializó la claseMessage en lugar de la hora de cada instancia Message .

Ahora que eliminó MyModel, elimine Chatter.sqlite.

Adición del mensaje view callable

View callable en Pyramid procesa solicitudes de artefactos en respuestas. Cambie el contenido del archivo chatter/views.py al siguiente:

 from pyramid.httpexceptions import HTTPFound from pyramid.view import view_config import transaction from .models import ( DBSession, Message, ) @view_config(route_name='home', renderer='templates/messages.pt') def index(request): messages = DBSession.query(Message).order_by(Message.created)[-10:] return {'messages': messages} @view_config(route_name='send') def send(request): text = request.params.get('message') with transaction.manager: message = Message(text) DBSession.add(message) return HTTPFound(location=request.route_url('home'))

El index invocable está asociado a la ruta'home', que es la asociada de manera predeterminada al / URL de la aplicación. Recupera hasta los últimos 10 mensajes desde la base de datos con la clase Message establecida en el archivo chatter/model.py y los devuelve como un parámetro 'message' para la plantilla de representación templates/messages.pt.

El send invocable está asociado a la ruta'send' (que será añadida en el siguiente paso). Este procesa solicitudes enviadas desde el formulario en el archivo templates/messages.pt con solo insertar una nueva fila utilizando las clases DBSession y Message y redirigiendo la solicitud de vuelta a la ruta 'home' . Redirigir luego del tipo de envío POST es una buena práctica para evitar envíos duplicados del formulario cuando un usuario recarga la página. Yo utilicé route_url para encontrar la ubicación de redirección para hacer que el código sea independiente de lo que la ruta de URL 'home' está asignada.

Añada la asignación de la ruta 'send' al archivo chatter/__init__.py (añádala antes de la línea config.scan() ):

 config.add_route('send', '/send')

Adición de la plantilla messages.pt

Para evitar tener archivos que no va a usar, elimine todas las plantillas y archivos estáticos antes de proceder:
rm -f chatter/templates/* chatter/static/* (UNIX) o
del /q chatter\templates chatter\static (Windows)

Está allí de manera predeterminada, así que utilicemos el motor de representación Chameleon para las plantillas. Cree un archivo chatter/templates/messages.pt (la extensión .pt indica que se debe procesar con el motor Chameleon) con el siguiente contenido:

 <html> <head> <title>Messages</title> <link rel="stylesheet" type="text/css" href="/static/messages.css"/> </head> <body> <div id="chatlog"> <ul> <li tal:repeat="m messages" tal:content="m.text" /> </ul> </div> <hr/> <form action="${request.route_url('send')}" method="post"> <input id="send" name="send" type="submit" value="Send" /> <input id="message" name="message" type="text" /> </form> </body> </html>

Esta plantilla crea una página que contiene el listado de todos los mensajes transmitidos a ella desde el view invocable y un formulario para enviar mensajes a la ruta 'send' . El atributo tal:repeat="m messages" es una manera que Chameleon tiene de crear un elemento <li> para cada artículo del arreglo 'messages' utilizando la variable 'm' para cada artículo. El atributo tal:content="m.text" le dice al motor de plantillas que coloque el valor de "m.text" dentro del elemento (entre <li> y </li>).

Para que el texto se parezca más a un intercambio de mensajes que a una lista de artículos desordenados, cree la hoja de estilo chatter/static/messages.css con el siguiente contenido:

 #chatlog > ul { padding: 0 0; list-style-type: none; }

OK, esa fue la primera iteración de la aplicación. Iníciela con el comando pserve production.ini y acceda a ella a través de http://localhost:6543. Al hacer clic en el botón Enviar, se muestran los últimos 10 mensajes desde la base de datos. Si más de un usuario accede a esta página, verán los mensajes de los demás.

Utilice la etiqueta step2-first-draft en el repositorio Git para ver el código hasta este punto.

Paso 3. Hacer que Chatter sea más dinámico con SocketIO

Cuando más de un usuario envía mensajes, la aplicación (hasta ahora) no es muy útil porque cada usuario solo ve los demás mensajes cuando envía los suyos. Hagamos que los mensajes aparezcan de manera inmediata para todos los usuarios conectados al servidor. Debe añadir soporte para la captación de SocketIO (originalmente desarrollada para NodeJS).

Cree un script de inicio

Primero, ajuste cómo se inicia la aplicación. El comando pserve no le deja elegir un servidor, así que debe tener su propio script de inicio. Cree el archivo serve.py en el directorio actual con las siguientes líneas:

 import os from paste.deploy import loadapp from socketio.server import serve from gevent import monkey; monkey.patch_all() if __name__ == '__main__': app = loadapp('config:production.ini', relative_to=os.getcwd()) try: serve(app, host='0.0.0.0') except KeyboardInterrupt: pass

También deberá instalar el paquete que contiene socketio. Añada la línea gevent-socketio, a la lista requires[] en el archivo setup.py y ejecute python setup.py develop nuevamente.

Este script utiliza la implementación de SocketIOServer en lugar de la predeterminada de WSGIServer para escuchar las solicitudes. Ahora, pruebe el script al ejecutar python serve.py para ver si la aplicación aún se comporta de la misma manera.

Actualice el lado del servidor de SocketIO

Para cambiar cómo el send invocable maneja las solicitudes entrantes, sustituya la declaración del método send (empezando por la línea @view_config(route_name='send') hasta el final del archivo) en chatter/views.py con:

 from socketio import socketio_manage from socketio.mixins import BroadcastMixin from socketio.namespace import BaseNamespace from pyramid.response import Response @view_config(route_name='send') def send(request): socketio_manage(request.environ, {'/chat': ChatNamespace}, request) return Response('') class ChatNamespace(BroadcastMixin, BaseNamespace): def on_chat(self, text): with transaction.manager: message = Message(text) DBSession.add(message) self.broadcast_event('chat', text)

Las solicitudes a la ruta send ahora se manejan de una manera SocketIO especial, delegándole el manejo de las solicitudes en el espacio de nombre /chat a la clase ChatNamespace . A su vez, ChatNamespace maneja eventos de chat al añadir mensajes a la base de datos y transmitirles texto de mensaje a todos los clientes conectados al servidor.

Las solicitudes de SocketIO se envían a través de una ruta especial que comienza por /socket.io/, así que también ajuste la correlación que tiene para la ruta send en chatter/__init__.py. Cambie la línea config.add_route('send', '/send') a:

 config.add_route('send', '/socket.io/*remaining')

Actualice el lado del cliente de SocketIO

Los cambios anteriores se encargaron del lado del servidor de la comunicación de SocketIO. Para el cliente, ajuste la plantilla y añada JavaScript para manejar el intercambio de SocketIO.

Cambie la declaración del formulario (entre los elementos de <form> y </form> ) en chatter/templates/messages.pl a:

 <form id="chatform"> <input id="send" name="send" type="submit" value="Send" /> <input id="message" name="message" type="text" /> </form> <script src="//cdn.jsdelivr.net/jquery/2.1.1/jquery.min.js"></script> <script src="//cdnjs.cloudflare.com/ajax/libs/socket.io/0.9.16/socket.io.min.js"></script> <script src="/static/chat.js" type="text/javascript"></script>

Yo añadí id al formulario de envío para que el código de JavaScript lo encuentre y añadí un conjunto de archivos de JavaScript. jquery.min.js está allí para simplificar la búsqueda de elementos en la página.

Nota: Utilice la versión 0.9.16 de socket.io.js porque la implementación de gevent-socketio soporta, o es compatible con, esa versión. La versión 1.0 de socket.io.js introdujo cambios que no son compatibles con las versiones anteriores de SocketIO.

El script chat.js se encarga de SocketIO para la aplicación, así que créelo en chatter/static/chat.js con:

 $(document).ready(function() { var socket = io.connect('/chat') $(window).bind("beforeunload", function() { socket.disconnect(); }); var chatlog = $("#chatlog>ul") var addMessage = function(data) { chatlog.append($('<li>').append(data)); } socket.on("chat", function(e) { addMessage(e); chatlog.scrollTop(chatlog.height()); }); $("#chatform").submit(function(e) { // don't allow the form to submit e.preventDefault(); // send out the "chat" event with the textbox as the only argument socket.emit("chat", $("#message").val()); $("#message").val(""); }); });

El script:

  • Se conecta al espacio de nombre '/chat' (el mismo espacio al que ChatNamespace está asignado en el send view invocable).
  • Escucha eventos de conversación (transmitidos por ChatNameserver en el método on_chat ) para agregar el elemento'<li>' con los datos recibidos a chatlog.
  • Emite eventos de conversación (manejados por ChatNameserver con el método on_chat ) cuando se envía chatform .

Si inicia la aplicación ahora con python serve.py, los mensajes de todos los usuarios conectados a la aplicación se mostrarán inmediatamente. Observe que chatform es introducido con cada nuevo mensaje, lo que no ocurre normalmente en las aplicaciones de conversación. Añada las propiedades de estilo necesarias a chatter/static/messages.css (el nuevo contenido del archivo):

 #chatlog > ul { padding: 0 0; list-style-type: none; height: 200pt; overflow-y: scroll; } #message { width: 80%; }

Ahora, se debería parecer más a una aplicación de conversación.

Utilice la etiqueta step3-add-socketio en el repositorio Git para ver el código hasta este punto.

Paso 4. Añadir usuarios a Chatter

Exija que los usuarios inicien sesión

Para exigir el inicio de sesión antes de que los usuarios se puedan unir a la conversación:

  1. Habilite las políticas de autenticación y autorización para su aplicación.
  2. Exija permiso para acceder a los view invocables.
  3. Asigne usuarios al permiso principal.
  4. Añada una vista de inicio de sesión para que los usuarios se puedan autenticar.

Añada las siguientes líneas a chatter/__init__.py antes de la declaración de la función main:

 from pyramid.security import Allow, Everyone from pyramid.authentication import AuthTktAuthenticationPolicy from pyramid.authorization import ACLAuthorizationPolicy class Root: __acl__ = [ (Allow, 'group:view', 'view') ] def __init__(self, request): pass def groupfinder(userid, request): return ['group:view']

Este código añade la clase Root , que se utiliza como contexto para todas las solicitudes de la aplicación. Le otorga a los miembros de 'group:view' el permiso 'view' y le prohíbe el acceso a los demás. Se añade la función groupfinder para recuperar una lista de grupos a la cual userid pertenece. Todos los usuarios de la aplicación pertenecen a 'group:view'.

Para empezar a utilizar estos objetos en la aplicación, en el mismo archivo (chatter/__init__.py) en la función main cambie la línea config = Configurator(settings=settings) a:

 config = Configurator(root_factory=Root, settings=settings) config.set_authentication_policy(AuthTktAuthenticationPolicy( 'chattersecret', groupfinder, hashalg='sha512')) config.set_authorization_policy(ACLAuthorizationPolicy()) config.set_default_permission('view') config.add_route('logout', '/logout')

La clase Root ahora está configurada como un contexto raíz para la aplicación. El código también configura la política para usar los cookies auth_tkt para la información de autenticación (debido a la instancia AuthTktAuthenticationPolicy ), que es encriptada con 'chattersecret' como palabra clave (debe ser única para cada aplicación) y utiliza el algoritmo hash 'sha512' . Utiliza el método groupfinder como devolución de llamada para encontrar los grupos a los cuales pertenecen los usuarios utilizando la información de autenticación. El código utiliza ACL para la política de autenticación, lo que significa que se especifica el acceso a un objeto de contexto como el atributo de clase'__acl__' .

Si intenta ejecutar la aplicación ahora, recibirá el error 403 Forbidden porque no ofreció una manera para que los usuarios realicen la autenticación. Debe añadir forbidden_view a chatter/views.py. Añada las siguientes líneas al final del archivo:

 from pyramid.view import forbidden_view_config from pyramid.security import remember, forget @forbidden_view_config(renderer='templates/login.pt') def login(request): error = '' if 'login' in request.params: username = request.params['username'] if not username: error = "user name can not be empty" else: headers = remember(request, username) return HTTPFound(location=request.route_url('home'), headers=headers) return {'error': error} @view_config(route_name="logout") def logout(request): headers = forget(request) return HTTPFound(location=request.route_url('home'), headers=headers)

El decorador forbidden_view_config hace que la aplicación llame a view invocable en lugar de devolver la página de error prohibido predeterminada. El mismo invocable también se usa al enviar la página de inicio de sesión. La presencia de 'login' (el nombre del botón de envío en login.pt) se utiliza para diferenciar estos dos eventos. Por ahora, vamos a garantizar que el nombre de usuario proporcionado no sea una cadena de caracteres vacía. Si se satisface esa condición, se almacena la información de usuario y la devolución de llamada se redirige a'home'. De lo contrario, vuelve a la página de inicio de sesión con un mensaje de error.

El logout invocable le permite al usuario solicitar ser "olvidado".

Añada la plantilla para la página de inicio de sesión en el archivo chatter/templates/login.pt:

 <html> <head> <title>Login</title> <link rel="stylesheet" type="text/css" href="/static/login.css" /> </head> <body> <span id="error" tal:condition="error" tal:content="error" /> <form method="post"> Username: <input id="username" name="username" type="text" /> <input id="login" name="login" value="Login" type="submit" /> </form> </body> </html>

tal:condition="error" tal:content="error" significa que este elemento span solo se añade si el valor error no está vacío. El contenido del elemento es el valor de error. En todos los demás aspectos, esta plantilla es un formulario de inicio de sesión sencillo.

Para destacar los mensajes de error, cree el archivo chatter/static/login.css con:

 #error { color: red; }

Los usuarios deben poder cerrar sesión, así que añada lo siguiente a chatter/templates/messages.pl luego de chatform:

 <form action="${request.route_url('logout')}" method="post"> ${request.authenticated_userid}: <input id="logout" type="submit" value="Logout" /> </form>

Ahora, debería poder iniciar sesión en la aplicación con solo especificar su nombre.

Utilice la etiqueta step4a-add-login en el repositorio Git para ver el código hasta este punto.

Añada la clase user al modelo

Hasta ahora, estos cambios aún no asocian los mensajes a su originador. Usted debe añadir usuarios al modelo y asociarles mensajes. En chatter/models.py, se muestran a continuación las clases User añadida y Message actualizada (sustituya las líneas que empiezan por class Message(Base) hasta el final del archivo):

 from sqlalchemy import ( Unicode, ForeignKey, ) from sqlalchemy.orm import relation class Message(Base): __tablename__ = 'messages' id = Column(Integer, Sequence('msid', optional=True), primary_key=True) text = Column(Text, nullable=False) created = Column(DateTime, default=datetime.datetime.utcnow) userid = Column(Integer, ForeignKey('users.id'), nullable=False) def __init__(self, user, text): self.user = user self.text = text class User(Base): __tablename__ = 'users' id = Column(Integer, Sequence('usid', optional=True), primary_key=True) name = Column(Unicode(255)) messages = relation(Message, backref="user") def __init__(self, name): self.name = name

Observe la adición del atributo userid como ForeignKey para la columna 'users.id' , indicado a través del nombre de tabla usado por la clase User . La misma relación se utiliza en la declaración del atributomessages de la clase User . La llamada a relation(Message) crea un atributo de consulta que devuelve un arreglo de instancias Message cuya clave foránea coincide con la clave primaria de User. La opción backref="user" causa la adición de un atributo user en la clase Message , lo que devuelve la instancia User a la cual se refiere la clave foránea.

Se modificó el modelo de base de datos, por lo tanto, recree la base de datos. Simplemente, elimine la base de datos anterior (archivo Chatter.sqlite) y se recreará con el modelo adecuado cuando se vuelva a ejecutar la aplicación.

Para ajustar los login y messages invocables, para que tomen en cuenta la información de usuario y para evitar la duplicación de usuario, almacene userid en lugar de username en la solicitud. En chatter/views.py, cambie la línea headers = remember(request, username) en la declaración del método login a:

 with transaction.manager: users = DBSession.query(User).filter_by(name=username).all() if users: user = users[0] else: user = User(name=username) DBSession.add(user) DBSession.flush() headers = remember(request, user.id)

La llamada remember debe estar en el ámbito dewith transaction.manager o no podrá acceder a los atributos de instancia User . Además, se añade la llamada a DBSession.flush() para transmitir una solicitud de adición de usuario a la base de datos a fin de crear un nuevo id de usuario y, así, hacerlo disponible para su recuperación desde la instancia user .

El siguiente cambio se debe hacer al código que maneja los eventos de chat porque ahora la instancia Message exige que se especifique el usuario. El método on_chat actualizado de la clase ChatNamespace debe tener este aspecto:

 def on_chat(self, text): with transaction.manager: user = DBSession.query(User).get(self.request.authenticated_userid) if user: message = Message(user, text) DBSession.add(message) self.broadcast_event('chat', text)

Corrija el nombre que utilizó antes del botón de Logout . Para ello, envíe el nombre del usuario como otra entrada a la plantilla. Modifique la declaración del método index a:

 from .models import User @view_config(route_name='home', renderer='templates/messages.pt') def index(request): user = DBSession.query(User).get(request.authenticated_userid) if user: messages = DBSession.query(Message).order_by(Message.created)[-10:] return {'messages': messages, 'username': user.name} else: return logout(request)

También debe actualizar chatter/templates/messages.pt con la siguiente línea para el botón de envío logout :

 ${username}: <input id="logout" type="submit" value="Logout" />

La aplicación funciona como antes pero, ahora, almacena la asociación entre el usuario y el mensaje.

Utilice la etiqueta step4b-user-info en el repositorio Git para ver el código hasta este punto.

Añada datos del usuario y hora al evento de conversación

El objetivo de tener una asociación entre el usuario y el mensaje es tenerla disponible en el resultado. Ajustemos la solicitud de SocketIO para tener la información disponible en el navegador.

Usted tiene una estructura más complicada que una simple cadena de caracteres transmitida al cliente, así que también formateemos los mensajes de historial iniciales del cliente. Para ajustar esto y evitar la duplicación de código, modifique la solicitud chat para que siempre envíe un arreglo de objetos de mensaje en lugar de solo un objeto. Modifique la declaración de la clase ChatNamespace en chatter/views.py por la siguiente:

 def mToD(message): return {'who': message.user.name, 'when': message.created.isoformat(), 'what': message.text} class ChatNamespace(BroadcastMixin, BaseNamespace): def recv_disconnect(self): self.disconnect(silent=True) def on_history(self): self.emit('chat', [mToD(m) for m in DBSession.query(Message).order_by( Message.created)[-10:]]) def on_chat(self, text): with transaction.manager: user = DBSession.query(User).get(self.request.authenticated_userid) if user: message = Message(user, text) DBSession.add(message) DBSession.flush() self.broadcast_event('chat', [mToD(message)])

También deberá eliminar la adición de mensajes en el métodoindex . Modifíquelo a:

 def index(request): user = DBSession.query(User).get(request.authenticated_userid) if user: return {'username': user.name} else: return logout(request)

También elimine la adición en chatter/templates/messages.pt. Elimine la siguiente línea:

 <li tal:repeat="m messages" tal:content="m.text" />

A continuación, mostramos las modificaciones correspondientes en chatter/static/chat.js (solo las líneas modificadas con la declaración de la función addMessage y el manejador de eventos chat ):

 var prevDateString = ''; var addMessage = function(data) { currDate = new Date(data.when); currDateString = currDate.toDateString() if (currDateString !== prevDateString) { chatlog.append($('<li>').attr('class', 'date').text(currDateString)); prevDateString = currDateString; } chatlog.append($('<li>').append( $('<span>').attr('class', 'when').text("["+currDate.toLocaleTimeString()+ "]")).append(" ").append( $('<span>').attr('class', 'who').text(data.who + ":")).append(" ").append( $('<span>').attr('class', 'what').text(data.what))); } socket.on("chat", function(e) { for (i=0; i<e.length; i++) { addMessage(e[i]); } chatlog.scrollTop(chatlog.height()); }); socket.emit("history")

Para ver mejor la diferencia entre las distintas partes del mensaje, añada las siguientes líneas a chatter/static/messages.css:

 .date { text-align: center; font-size: 140%; } .who { font-weight: bold; } .when { font-style: italic; color: gray; }

Pruebe la aplicación ahora con python serve.py.

Utilice la etiqueta step4c-display-usertime en el repositorio Git para ver el código hasta este punto.

Añada la información de quien está conectado

En la mayoría de las aplicaciones de conversación, los usuarios pueden ver a los participantes de la conversación. Para lograr esto en su aplicación, añada la nueva clase de tabla Connect para contener información de conexión. Añada la siguiente declaración de clase al archivo chatter/models.py:

 from sqlalchemy import Boolean class Connect(Base): __tablename__ = 'connects' id = Column(Integer, Sequence('cnid', optional=True), primary_key=True) userid = Column(Integer, ForeignKey('users.id'), nullable=False) time = Column(DateTime, default=datetime.datetime.utcnow) ison = Column(Boolean) def __init__(self, user, ison): self.userid = user.id self.ison = ison

Esta tabla posee registros de cada evento de conexión y desconexión de un usuario. Como la declaración de las tablas existentes en el modelo no cambió, no hay necesidad de borrar Chatter.sqlite. La nueva tabla se añade a las tablas existentes cuando la aplicación se ejecute nuevamente.

La información de estatus del usuario se envía al cliente en un evento status . Los datos del evento contienen un arreglo de usuarios y su estatus actual: 'on' o 'off'. Añada el manejo del eventoconnect con el método recv_disconnect y actualice los métodos recv_disconnect y on_history en chatter/views.py, como se muestra a continuación (el método on_chat debe permanecer igual):

 from sqlalchemy import func from .models import Connect lastIdQuery = DBSession.query(func.max(Connect.id).label('max')).group_by( Connect.userid).subquery() connQuery = DBSession.query(Connect).join( lastIdQuery, Connect.id == lastIdQuery.c.max).subquery() connectedUsers = DBSession.query(User).join(connQuery).filter(connQuery.c.ison) def disconn_all(): with transaction.manager: for user in connectedUsers: DBSession.add(Connect(user, False)) def uToD(user, ison): return {'id': user.id, 'name': user.name, 'ison': ison} class ChatNamespace(BroadcastMixin, BaseNamespace): def recv_connect(self): self._connect_event(True) def recv_disconnect(self): self._connect_event(False) self.disconnect(silent=True) def _connect_event(self, ison): with transaction.manager: user = DBSession.query(User).get(self.request.authenticated_userid) if user: DBSession.add(Connect(user, ison)) self.broadcast_event('status', [uToD(user, ison)]) def on_history(self): self.emit('chat', [mToD(m) for m in DBSession.query(Message).order_by( Message.created)[-10:]]) self.emit('status', [uToD(u, True) for u in connectedUsers])

Para aclarar:

  • Luego de los eventos connect y disconnect , se añade un nuevo registro a la tabla Conectar y el evento status se transmite a todos los usuarios.
  • Luego de una solicitud de historial por parte de un cliente, se envía una lista de los usuarios conectados actualmente con el evento status y el evento chat .
  • Con respecto a connectedUsers, lastIdQuery devuelve ID de registros de conexión máxima de cada usuario. connQuery devuelve registros de la tabla Conectar correspondientes a esos ID (el último registro de cada usuario). connectedUsers devuelve los usuarios con el valor ison como True en su último registro en la tabla Conectar.

Para prevenir que el mismo usuario esté conectado de manera simultánea más de una vez, actualice la consulta y verificación user en el método login , en el mismo chatter/views.py, como se muestra a continuación (la condición else debe permanecer igual):

 users = DBSession.query(User, connQuery.c.ison).join( connQuery).filter(User.name==username).all() if users: user = users[0][0] if users[0][1]: return {'error': 'User "%s" is already connected' % user.name} else:

Al reiniciar el servidor, marque todos los usuarios desconectados antes que sus clientes se conecten nuevamente. Para ello, se debe llamar al método disconn_all() introducido anteriormente al inicializar la aplicación. Añada las siguientes líneas a chatter/__init__.py luego de la llamada create_all() que crea todas las tablas:

 from .views import disconn_all disconn_all()

En el cliente, el registro de mensaje está lado a lado con la lista de usuarios. Cambie <div id="chatlog"> en chatter/templates/messsages.pt a la tabla:

 <table> <tr> <td id="chatlog"> <ul/> </td> <td id="users"> <ul/> </td> </tr> </table>

Para que esta vista sea razonable, añada las siguientes entradas a chatter/static/messages.css:

 table { width: 100%; } #chatlog { width: 80%; vertical-align: top; } #users { vertical-align: top; } #users > ul { padding: 0 0; list-style-type: none; }

Añada el manejo de los eventos de status , para añadir y eliminar usuarios según sus estatus deison , a chatter/static/chat.js:

 var users = $("#users>ul"); var updateStatus = function(data) { user = $("#user-" + data.id); if ((user.length === 0) === data.ison) { if (data.ison) { users.append($('<li>').text(data.name).attr('id', "user-" + data.id)); } else { user.remove(); } } } socket.on("status", function(e) { for (i=0; i<e.length; i++) { updateStatus(e[i]); } });

En este punto, la aplicación muestra los usuarios conectados actualmente. Ha finalizado la etapa de modificación de archivos estáticos, de modo que cache_max_age=0 puede volverse a modificar para cache_max_age=3600 en chatter/__init__.py.

Utilice la etiqueta step4-add-users en el repositorio Git para ver el código hasta este punto.

Paso 5. Desplegar Chatter en IBM Cloud

Es realmente sencillo desplegar una aplicación en IBM Cloud. Para facilitar el despliegue de la aplicación en IBM Cloud reiteradamente, cree un archivo manifest.yml con:

 --- applications: - name: TestChatter memory: 128M buildpack: https://github.com/cloudfoundry/cf-buildpack-python.git command: python serve.py

Esto le dice al cliente de CF que introduzca la aplicación denominada TestChatter con el buildpack de Python, 128M, y que utilice serve.py como el script de inicio.

Para garantizar que no ingresen archivos innecesarios a IBM Cloud con la aplicación, cree un archivo .cfignore con:

 Chatter.sqlite dist

Manejo de VCAP_APP_PORT

Para que la aplicación funcione en IBM Cloud, debe escuchar en el puerto adecuado. Añada el parámetro port a la llamada serve en serve.py:

 serve(app, host='0.0.0.0', port=os.getenv('VCAP_APP_PORT', 6543))

Ahora, inicie sesión, introduzca la aplicación en IBM Cloud y acceda a ella a través del URL de la aplicación:

 cf login -a https://api.ng.bluemix.net cf push

Manejo de VCAP_SERVICES

Si se reinicia la aplicación, se perderá el registro de la conversación. Para que esté disponible de manera permanente, utilice los servicios de base de datos de IBM Cloud. Modifique la manera como se inicializa el motor de base de datos al cambiar la línea engine = engine_from_config(settings, 'sqlalchemy.') en chatter/__init__.py a:

 engine = None import os if os.environ.has_key("VCAP_SERVICES"): import json services = json.loads(os.environ["VCAP_SERVICES"]) for service in reduce(lambda x,y: x+y, services.values(), []): if service['name'] == 'messages': from sqlalchemy import create_engine engine = create_engine(service['credentials']['uri'], pool_recycle=3600) if not engine: engine = engine_from_config(settings, 'sqlalchemy.')

Este código utiliza un servicio denominado 'messages' para recuperar el URI de base de datos que se debe utilizar. Periódicamente, los servicios cierran conexiones de larga duración. Para manejar esa situación de manera adecuada, recicle las conexiones (cierre y abra) de manera periódica. pool_recycle=3600 realiza ese reciclaje para conexiones abiertas durante más de una hora. Además del URI, también necesita añadir controladores para la base de datos. Se añaden en setup.py en la lista requires[]. Añada PostgreSQL con la línea psycopg2 . Ahora, se puede utilizar la aplicación con bases de datos de PostgreSQL:

 cf create-service postgresql 100 messages cf bind-service TestChatter messages cf push

Observe que ahora los datos se mantienen, incluso si se reinicia la aplicación:

 cf restart TestChatter

Añada soporte a DB2

DB2 no posee un cliente de fuente abierta, así que añada las bibliotecas necesarias directamente a la aplicación. Descargue Data Server Driver Package para Linux/x86-64 64 bit desde Fix Packs para IBM Data Server Client Packages y agréguelo a la raíz de la aplicación. Utilice la descarga de Linux/x86-64 incluso si está desarrollando la aplicación en una plataforma distinta, ya que la aplicación se ejecutará en esa plataforma en IBM Cloud. La versión v10.5 Fix Pack 5 del controlador es v10.5fp5_linuxx64_dsdriver.tar.gz

Añada la instalación de los paquetes de DB2 a setup.py. El pip package manager predeterminado no maneja archivos preempaquetados, por lo tanto utilice easy_install en su lugar. Añada las siguientes líneas al archivo setup.py antes de la llamada setup() :

 if "-".join(os.uname()[0::4]) == "Linux-x86_64": from glob import glob from subprocess import call if not os.path.isdir('./dsdriver'): db2_drivers=glob("./v*_linuxx64_dsdriver.tar.gz") if db2_drivers: import tarfile print "Using %s" % db2_drivers[0] tar = tarfile.open(db2_drivers[0]) tar.extractall() tar.close() call(["bash", "./dsdriver/installDSDriver"]) for f in db2_drivers: os.remove(f) import shutil for d in os.listdir("./dsdriver"): if not d in ["lib", "python"]: f = os.path.join("./dsdriver", d) if os.path.isdir(f): shutil.rmtree(f) else: os.remove(f) if os.path.isdir('./dsdriver/python/python64'): call(["easy_install", "-N", glob("./dsdriver/python/python64/ibm_db-*-linux-x86_64.egg")[0]]) call(["easy_install", "-N", glob("./dsdriver/python/python64/ibm_db_sa-*.egg")[0]])

Observe que:

  • La instalación ocurre solo si el sistema operativo es realmente el adecuado para el controlador. La extracción de los archivos solo ocurre si no se ha realizado anteriormente.
  • Las opciones -N le dicen a easy_install que no verifiquen la red en busca de nuevos paquetes.
  • glob se utiliza para que el código funcione con cualquier versión de los paquetes.
  • Dividir la instalación del controlador y la instalación de los paquetes de Python en dos condiciones if separadas les permiten a los usuarios con acceso a Linux acelerar la inicialización inicial de la aplicación al incluir el controlador ya instalado en la aplicación.
  • Eliminar los archivos al final del primero de los bloquesif reduce el consumo de disco de la aplicación en la nube.

Finalmente, añada la biblioteca nativa de DB2 a la ruta de carga de bibliotecas. Esto se realiza en el entorno de la aplicación, así que actualicemos el archivo manifest.yml con la siguiente línea:

 env: LD_LIBRARY_PATH: /app/dsdriver/lib:$LD_LIBRARY_PATH

La aplicación ya está lista para utilizar DB2 como un servicio (primero, elimine cualquier instancia de servicio anterior):

 cf unbind-service TestChatter messages cf delete-service -f messages cf create-service sqldb sqldb_free messages cf bind-service TestChatter messages cf push

Utilice la etiqueta step5-bluemixify en el repositorio Git para ver el código hasta este punto.

Conclusión

Este tutorial le mostró como:

  • Crear una aplicación basada en Pyramid y captar su modelo de base de datos de una manera independiente de la base de datos.
  • Habilitar la actualización dinámica de la página con la captación de SocketIO.
  • Aplicar la autenticación del usuario antes de usar la aplicación.
  • Desplegar la aplicación en el entorno de IBM Cloud.
  • Utilizar los servicios de base de datos disponibles en IBM Cloud.

También proporcionó la base para crear aplicaciones basadas en Python en el entorno de IBM Cloud.

En su estado actual, la aplicación de ejemplo contiene información útil que no está disponible a través de la interfaz. Considere esta una invitación para utilizar las capacidades adquiridas en este tutorial para extender aún más la aplicación. Algunas ideas: permita el desplazamiento a través de todos los mensajes almacenados, añada la búsqueda por los mensajes o muestre la popularidad de la aplicación por el número de usuarios que la visitan todos los días.


Recursos para Descargar


Temas relacionados


Comentarios

Inicie Sesión o Regístrese para agregar comentarios.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=90
Zone=Cloud computing
ArticleID=1019972
ArticleTitle=Construcción de una Aplicación de Conversación con Pyramid, SQLDB y IBM Cloud
publish-date=12082015