Welcome to FullPy’s documentation!

FullPy is a high-level Python module for developping client-server web application. Here are the main features:

  • Both client and server are written in Python, and can share pieces of code. FullPy uses Brython for client-side execution of Python in the web browser.

  • Semantic-aware data persistance using OWL ontologies instead of a database. FullPy uses Owlready2 for managing ontologies and automatically storing them in a SQLite3 database.

  • Remote function calls between client and server, with object serialization. FullPy can use both Ajax (single way client->server calls) or WebSockets (client->server and server->client calls)

  • FullPy also provides many high-level services, such as authentication, translation support, HTML widget system, etc.

  • FullPy can run over multiple backend: Flask, Werkzeug and Gunicorn (only Gunicorn is supported for WebSockets).

Table of content

Installing FullPy

FullPy can be installed with ‘pip’, the Python Package Installer.

Installation from terminal (Bash under Linux or DOS under Windows)

You can use the following Bash / DOS commands to install FullPy in a terminal:

pip install fullpy

If you don’t have the permissions for writing in system files, you can install FullPy in your user directory with this command:

pip install --user fullpy

Installation in Spyder, IDLE, or any other Python console

You can use the following Python commands to install FullPy from a Python console (including those found in Spyder3 or IDLE):

>>> import pip.__main__
>>> pip.__main__._main(["install", "--user", "fullpy"])

Manual installation

FullPy can also be installed manually in 3 steps:

# Uncompress the FullPy-0.1.tar.gz source release file (or any other version), for example in C:\ under Windows

# Rename the directory C:\FullPy-0.1 as C:\fullpy

# Add the C:\ directory in your PYTHONPATH; this can be done in Python as follows:

import sys
sys.path.append("C:\")
import fullpy

Starting a new project with FullPy

Directory architecture

To start a new project, follow these simple steps:

  • Create a directory for your project

  • Create a “static” subdirectory in that directory

  • Copy the following files in the “static” subdirectory:

    • “fullpy.css” (from fullpy/static)

    • “brython.js” and “brython_stdlib.js” (from Brython)

  • Create a “server.py” and “client.py” Python scripts in your project directory

That’s all! You should obtain the following hierarchy:

  • project_directory/
    • static/
      • brython.js

      • brython_stdlib.js

      • fullpy.css

    • client.py

    • server.py

The two next subsections give an example of an Hello World FullPy web application (the code can be found in the “demo/demo_1_hello_world_ajax” directory of FullPy).

Hello World server example

import sys, os, os.path
from fullpy.server import *

class MyWebApp(ServerSideWebapp):
  def __init__(self):
    ServerSideWebapp.__init__(self)
    self.name          = "demo"
    self.url           = "/index.html"
    self.title         = "FullPy demo"
    self.static_folder = os.path.join(os.path.dirname(__file__), "static")

    self.use_python_client(os.path.join(os.path.dirname(__file__), "client.py"))
    self.use_ajax(debug = True)

  @rpc # Mark the function as remotely callable by the client (RPC = remote procedure call)
  def server_hello(self, session):
    return "Hello world!"

from fullpy.server.gunicorn_backend import *
serve_forever([MyWebApp()], "http://127.0.0.1:5000")

Hello World client example

from fullpy.client import *

class MyWebApp(ClientSideWebapp):
  def on_started(self):
    def done(response):
      html = HTML("""FullPy Demo loaded Ok! Server says: '%s'.""" % response)
      html.show()
    webapp.server_hello(done) # Call the server_hello() remote function on the server

MyWebApp()

Running the web application

To run your web application, simply execute the server.py Python script.

FullPy will automatically compile the client part of the web application into Javascript, if needed.

python3 ./server.py

Then, open the following address in your web browser: http://127.0.0.1:5000/demo/index.html

Server application

The server application should import fullpy.server, subclass fullpy.server.ServerSideWebapp, create an instance of the subclass and pass it to server_forever(), as in the following example:

import sys, os, os.path
from fullpy.server import *

class MyWebApp(ServerSideWebapp):
  def __init__(self):
    ServerSideWebapp.__init__(self)
    self.name          = "demo"
    self.url           = "/index.html"
    self.title         = "FullPy demo"
    self.static_folder = os.path.join(os.path.dirname(__file__), "static")
    self.js            = []
    self.css           = []
    self.favicon       = "icon.png"

    self.use_python_client(os.path.join(os.path.dirname(__file__), "client.py"))
    self.use_ajax(debug = True)

from fullpy.server.gunicorn_backend import *
serve_forever([MyWebApp()], "http://127.0.0.1:5000")

In the subclass of ServerSideWebapp, you need to reimplement __init__(). Your __init__() should call the super implementation, then define some properties and finally call some use_XXX() methods.

The following properties are available:

  • name (mandatory): the name of the web app.

  • url (mandatory): the URL of the web app page. The full URL with be the concatenation of the server address, the name property and the url property (in the example above, “http://127.0.0.1:5000/demo/index.html”).

  • title (mandatory): the title of the web app (showed in the web browser’s window titlebar).

  • static_folder (mandatory): the local directory where static files are stored.

  • js (optional): a list of additional Javascript files used by the client. Notice that FullPy automatically adds Brython Javascript files as needed. Javascript files are expected to be found in the static directory.

  • css (optional): a list of CSS files used by the client. Notice that FullPy automatically adds “fullpy.css”. CSS files are expected to be found in the static directory.

  • favicon (optional): the “favicon” image file, showed in the web browser. The favicon is expected to be found in the static directory.

use_XXX() methods are used to enable various features of FullPy. The following use_XXX() methods are available:

use_python_client(client_file, force_brython_compilation=False, minify_python_code=False)

Use a Brython-compiled Python client. The client is automatically compiled to Javascript as needed.

  • client_file: the path to the client Python script (e.g. client.py).

  • force_brython_compilation: if True, compile the client even if its sources have not been modified.

  • minify_python_code: if True, minify the Python source using the “python_minifier” module.

use_ontology_quadstore(world=None)

Use an Owlready2 quadstore for persistent data and semantics.

  • world: the Owlready2 World to use (if None, owlread2.default_world is used).

use_session(session_class=None, group_class=None, auth=True, client_reloadable_session=True, session_max_duration=3888000.0, session_max_memory_duration=1296000.0)

Use sessions. A session is an object available on the server application, and created for each connected client. If a given client calls several remote functions on the server, each call will be associated with the same session. It thus allows storing client-specific information on the server-side.

FullPy support both anonymous sessions (automatically created by the client) and authentified sessions (with login and password). However, the use of authentified sessions requires an ontology quadstore, for storing users and their logins and passwords.

In addition, FullPy support both in-memory sessions (lost when the server is stopped and restarted) and persistent sessions (stored in the ontology quadstore). The use of persistent sessions requires an ontology quadstore.

  • session_class: the Session class to use. If None, use the default, in-memory, Session class. You can provide your own Session class, to reimplement some methods, store additional per-session data, and/or create a persistent Session.

  • group_class: the Group class to use. If None, use the default, in-memory, Group class. You can provide your own Group class, to reimplement some methods, store additional per-group data, and/or create a persistent Group.

  • auth: if True, support authentified sessions (which requires an ontology quadstore).

  • client_reloadable_session: if True, allows the client to reload and reuse the previous session.

  • session_max_duration: after that duration (in seconds), sessions are closed and destroyed. Default value corresponds to 45 days.

  • session_max_memory_duration: after that duration (in seconds), sessions are removed from the memory (but still kept in the ontology quadstore, if persistent sessions are used). Default value corresponds to 15 days.

use_ajax(debug=False)

Use Ajax, allowing client->server remote function calls. On the contrary, the server cannot call remote functions on the client with Ajax.

  • debug: if True, debugging information is written in the console each time a remote function is called.

use_websocket(debug=False)

Use WebSocket, allowing both client->server and server->client remote function calls. WebSockets require the use of sessions (anonymous or authentified).

  • debug: if True, debugging information is written in the console each time a remote function is called (for both the client and the server).

The following combination of use_XXX() methods are allowed:

  • use_python_client(); use_ajax():

    Simple Ajax-based web application, without sessions nor data persistance.

  • use_python_client(); use_ontology_quadstore(); use_ajax():

    Ajax-based web application, with data persistance but without sessions. Can be used e.g. for a dynamic website based on an ontology.

  • use_python_client(); use_session(auth = False); use_ajax():

    Ajax-based web application, with anonymous sessions but without data persistance. Anonymous sessions can be used e.g. for keeping user preferences (such as language) during navigation, but the preferences will be lost when the user closes the web browser.

  • use_python_client(); use_ontology_quadstore(); use_session(); use_ajax():

    Ajax-based web application, with both sessions and data persistance. Sessions can be anonymous or authentified.

  • use_python_client(); use_session(auth = False); use_websocket():

    WebSocket-based web application, with anonymous sessions but without data persistance.

  • use_python_client(); use_ontology_quadstore(); use_session(); use_websocket():

    WebSocket-based web application, with both sessions and data persistance. Sessions can be anonymous or authentified.

Additionnally, when using WebSocket, you need to enable GEvent and to use the Gunicorn backend, as in the following example:

from gevent import monkey
monkey.patch_all()

import sys, os, os.path
from fullpy.server import *

class MyWebApp(ServerSideWebapp):
  def __init__(self):
    ServerSideWebapp.__init__(self)
    self.name          = "demo"
    self.url           = "/index.html"
    self.title         = "FullPy demo"
    self.static_folder = os.path.join(os.path.dirname(__file__), "static")

    self.use_python_client(os.path.join(os.path.dirname(__file__), "client.py"))
    self.use_websocket(debug = True)

from fullpy.server.gunicorn_backend import *
serve_forever([MyWebApp()], "http://127.0.0.1:5000")

Finally, ClientSideWebapp has the following methods that can be reimplemented:

get_initial_data(url_params)

Create the initial data sent to the client. By default, no initial data is sent, but you can override this method to send some initial data. Initial data will be incorporated in the HTML webpage. They will be encoded with repr() (NB FullPy do not use the serializer to encode initial data, so as you may send a dictionary, and then decode its content one piece at a time, when needed).

  • url_params: a dictionary with the parameters found in the query part of the URL.

Client application

The client application should import fullpy.client, subclass fullpy.server.ClientSideWebapp, and create an instance of the subclass, as in the following example:

from fullpy.client import *

class MyWebApp(ClientSideWebapp):
  def on_started(self):
    html = HTML("""FullPy Demo loaded Ok!""")
    html.show()

MyWebApp()

The web application object can be accessed anywhere with the webapp global built-in variable.

ClientSideWebapp instances have the following attributes:

  • url_params: a dictionary with the parameters found in the query part of the URL.

  • initial_data: the initial data sent by the server (if any).

ClientSideWebapp has the following methods that can be reimplemented:

on_started()

Called once, when the web app starts.

on_session_opened(user_login, user_class, client_data)

Called when a session is opened, and after on_started(). Notice that it is also called for anonymous sessions (in that case, user_login is empty).

  • user_login: the login of the user.

  • user_class: the name of the class of the user (as a string; usefull if you define several subclasses of User).

  • client_data: the additional client data sent by the server (if any).

on_connexion_lost()

Called when the connexion to the server is lost.

on_session_lost()

Called when the session is lost (e.g. it has expired).

Backends

FullPy supports several backend servers.

Gunicorn backend

The Gunicorn backend supports both Ajax and WebSockets web apps. Ajax web apps can be run with or without GEvent. WebSockets web apps automatically use Gevent.

from fullpy.server.gunicorn_backend import *
serve_forever([MyWebApp()], "http://127.0.0.1:5000", url_prefix = "", flask_app = None, log_file = None, nb_process = 1, max_nb_websockect = 5000, worker_class = None, use_gevent = False, gunicorn_options = None)

Werkzeug backend

The Werkzeug backend supports only Ajax web apps.

from fullpy.server.werkzeug_backend import *
serve_forever([MyWebApp()], "http://127.0.0.1:5000", url_prefix = "", flask_app = None, log_file = None, nb_process = 1, werkzeug_options = None)

Flask backend

The Flask backend supports only Ajax web apps.

It is not exactely a backend: it just create a Flask application for the web app (or add the web app to an existent Flask application). Then, it is up to you to choose any Flask-compatible server (i.e. any WSGI server).

from fullpy.server.werkzeug_backend import *
flask_app = serve_forever([MyWebApp()], "http://127.0.0.1:5000", url_prefix = "", flask_app = None)

Remote function calls (RPC, remote procedure call)

Creating a remotely-callable function

In the Webapp class, a remotely-callable function can be defined by using the @rpc decorator.

Server-side

Remotely-callable functions defined in the server must be prefixed by server_. Their first argument is always the session object (which is None if there is no session), and additional arguments are allowed.

Here is an example:

class MyWebApp(ServerSideWebapp):
  def __init__(self):
    [...]

  @rpc # Mark the function as remotely callable by the client
  def server_remote_function(self, session, argument1, argument2):
    return argument1 + argument2
Client-side

Remotely-callable functions defined in the client are supported only if you use WebSockets; they must be prefixed by client_. Contrary to server ones, they have no session argument.

Here is an example:

class MyWebApp(ClientSideWebapp):
  def __init__(self):
    [...]

  @rpc # Mark the function as remotely callable by the server
  def client_remote_function(self, argument1, argument2):
    return argument1 + argument2

Calling a remote function

When calling a a remotely-callable function, the first argument is always a callback function that will be called with the returned value. You may pass None as callback if you don’t need the returned value.

Client-side

In the client, remotely-callable server functions can be called directly on the webapp.

Here are examples, with and without callback:

def done(response):
  print(response)
webapp.server_remote_function(done, 2, 3)

webapp.server_remote_function(None, 2, 3)
Server-side

In the server, remotely-callable client functions can be called at three levels:

  • directly on the webapp: in that case, the function is executed for all connected clients.

  • on a Session object: in that case, the function is executed for the corresponding client.

  • on a Group object: in that case, the function is executed for all clients in that Group.

Here are examples:

def done(response):
  print(response)
webapp.client_remote_function(done, 2, 3)

session.client_remote_function(done, 2, 3)

group.client_remote_function(done, 2, 3)

Serialization and supported datatypes

FullPy uses its own object serializer for serializing remote functions arguments and return values. It supports all basic Python datatypes (including int, float, str, tuple, list and dict) and can be extended for serializing Python objects and/or OWL ontology entities. It produce a JSON compatible serialization if the encoded data is JSON compatible; however, it also supports non-JSON feature, such as Python tuples and dictionaries with non-string keys.

For more information on the serializer, please refer to Sessions.

WebSocket example

The two next subsections give an example of an Hello World FullPy web application with WebSocket (the code can be found in the “demo/demo_1_hello_world_websocket” directory of FullPy). We have already seen a similar ajax example (see Starting a new project with FullPy).

Hello World server example
from gevent import monkey
monkey.patch_all()

import sys, os, os.path
from fullpy.server import *

class MyWebApp(ServerSideWebapp):
  def __init__(self):
    ServerSideWebapp.__init__(self)
    self.name          = "demo"
    self.url           = "/index.html"
    self.title         = "FullPy demo"
    self.static_folder = os.path.join(os.path.dirname(__file__), "static")

    self.use_python_client(os.path.join(os.path.dirname(__file__), "client.py"))
    self.use_websocket(debug = True)

  @rpc # Mark the function as remotely callable by the client (RPC = remote procedure call)
  def server_hello(self, session): # The name of server-side functions MUST starts with "server_"
    def f():
      gevent.sleep(2.0) # Wait 2 seconds
      session.client_update_speech(None, "Goodbye!") # Call the client_update_speech() remote function on the client
    gevent.spawn(f) # Execute f() in a separate "greenlet" microthread, in parallel

    return "Hello world!"

from fullpy.server.gunicorn_backend import *
serve_forever([MyWebApp()], "http://127.0.0.1:5000")

Hello World client example

from fullpy.client import *

class MyWebApp(ClientSideWebapp):
  def on_started(self, url_params):
    def done(response):
      html = HTML("""FullPy Demo loaded Ok! Server says: '<span id="server_speech">%s</span>'.""" % response)
      html.show()
    webapp.server_hello(done) # Call the server_hello() remote function on the server

  @rpc # Mark the function as remotely callable by the server (RPC = remote procedure call)
  def client_update_speech(self, text): # The name of client-side functions MUST starts with "client_"
    html = HTML(text)
    html.show(container = "server_speech") # Update the content of the "server_speech" HTML tag

MyWebApp()

HTML widget system

Creating HTML pieces

In the client, the HTML class can be used for creating and displaying pieces of HTML, and widgets.

A simple, fixed, piece of HTML code can be created as follows:

html = HTML("""<div>This is a <b>piece</b> of HTML</div>""")

It can also be created over several lines, as follows:

html = HTML()
html << """<div>First part</div>"""
html << """<div>Second part</div>"""
html << """<div id="part3">Third part</div>"""

The bind() method can be used to bind function to Javascript events.

bind(html_id, event, func)

Bind a callback function to an event, for the given HTML id. Notice that the actual binding may not occur immediately, but when the HTML piece will be displayed in the web browser. For more information on the available events and the func argument, please refer to Brython documentation.

  • html_id: the ID the HTML element.

  • event: The name of the event.

  • func: a callable that will be called when the event occur.

Here is an example:

def on_click(event):
  print("CLICKED!")

html.bind("part3", click, on_click)

Creating HTML widgets

Finally, you can subclass HTML to create your own widget.

Here is an example:

class MyWidget(HTML):
  def build(self, builder):
    self << """<div>First part</div>"""
    self << """<div>Second part</div>"""
    self << """<input id="ok_%s" type="button" value="Ok"></input>""" % id(self)
    self.bind("ok_%s" % id(self), "click", self.on_ok)

  def on_ok(self, event):
    print("OK clicked")

HTML widgets, i.e. subclasses of HTML, can also be inserted inside HTML pieces (including other widgets). For example:

html = HTML()
html << """<div>Plain HTML</div>"""
html << MyWidget()

Displaying an HTML piece

The following methods of HTML objects can be used to display a HTML piece:

show(container='main_content')

Show the piece of HTML in the web browser.

  • container: the HTML ID of the element that will display the HTML piece. By default, it is displayed in the entire HTML page.

show_replace(replaced_id)

Show the piece of HTML in the web browser, replacing the element of the given ID.

  • replaced_id: the HTML ID of the element that will replaced by the HTML piece.

show_popup(add_close_button=True, allow_close=True, container='popup_window')

Show the piece of HTML in the web browser, in a popup window.

  • add_close_button: if True, add a close button “X” at the top-right of the popup window.

  • allow_close: if True, the popup window is closed when the user click outside the window or press escape.

  • container: the HTML ID of the node that will used to display the popup window.

Finally, hide_popup() can be used to close the popup window.

hide_popup(event=None, container='popup_window')

Hide the current popup window.

  • event: No-op argument (only present in order to allow the use of hide_popup() as a callback to Javascript events).

  • container: the HTML ID of the node that is used to display the popup window.

Refreshing an HTML piece

A common trick is to use show_replace() for refreshing an HTML widget, as in the following example:

class MyRefreshableWidget(HTML):
  def build(self, builder):
    self << """<div id="widget_%s">""" % id(self)
    self << """... [add content of the widget here]"""
    self << """</div>"

  def refresh(self):
    self.show_refresh("widget_%s" % id(self))

Translations support

XXX this documentation is still under construction

Creating a persistent object model Using ontologies

XXX this documentation is still under construction

Please refer to the Owlready2 documentation here :

https://owlready2.readthedocs.io/en/latest/

Sessions

XXX this documentation is still under construction

Groups

XXX this documentation is still under construction

Sessions

XXX this documentation is still under construction

Demos

The demo/ directory in FullPy sources includes several demo web apps.

Multi-room chat

This demo web app is a multi-room, multi-user chat system. It uses:

  • WebSocket, allowing the server to call the client(s) when new messages are available,

  • Session, allowing the server to store per-client data (as required by WebSocket),

  • Group, each group representing a chat room, associated with one or more user sessions,

  • Ontology quadstore, for data persistence, including chat messages but also sessions and groups.

_images/chat.png
Server

First, the server app enables GEvent (mandatory for WebSocket) and import GEvent, FullPy and Owlready among other modules:

from gevent import monkey
monkey.patch_all()

import sys, os, os.path, datetime, gevent
from owlready2 import *
from fullpy.server import *

Second, we create a new World for storing persistent data. Data is stored in /tmp/demo_chat.sqlite3 (you may need to adapt the filename according to your system).

We load the FullPy ontology (which define a few classes, like User, Session and Group), and create a new ontology “chat.owl”.

world       = World(filename = "/tmp/demo_chat.sqlite3")
fullpy_onto = get_fullpy_onto(world)
chat_onto   = world.get_ontology("http://test.org/chat.owl")

Third, within the chat ontology, we subclass User, Session and Group.

The Group subclass is named ChatRoom, it has an internal name (name attribute, automatically generated by Owlready) and a user-flrendly label (label attribute).

We also create a simple object model for chat:
  • the Message class correspond to a message in the chat, the as_tuple() method is used to serialize a message as a tuple,

  • the text data property indicate the text of a message,

  • the date data property indicate the date of a message,

  • the messages object property indicate the list of messages associated with a given Group.

with chat_onto:
  class MyUser(fullpy_onto.User): pass

  class MySession(fullpy_onto.Session): pass

  class ChatRoom(fullpy_onto.Group): pass

  class Message(Thing):
    def as_tuple(self):
      return ("%s/%s/%s %s:%s" % (self.date.day, self.date.month, self.date.year,
                                  self.date.hour, self.date.minute), self.user.login, self.text)

  class text(Message >> str, FunctionalProperty): pass
  class date(Message >> datetime.datetime, FunctionalProperty): pass
  class messages(ChatRoom >> Message): pass

Fourth, we create 3 users and 3 chat rooms:

chat_onto.MyUser(login = "user1", password = "123")
chat_onto.MyUser(login = "user2", password = "123")
chat_onto.MyUser(login = "user3", password = "123")

chat_onto.ChatRoom(label = ["Python programming"])
chat_onto.ChatRoom(label = ["Bird watching"])
chat_onto.ChatRoom(label = ["Bike riding"])

Fifth, we create the web app server class by subclassing ServerSideWebapp.

In __init__() we call use_python_client(), use_ontology_quadstore(), use_session() and use_websocket().

class MyWebApp(ServerSideWebapp):
  def __init__(self):
    ServerSideWebapp.__init__(self)
    self.name          = "demo_4"
    self.title         = "FullPy demo"
    self.url           = "/index.html"
    self.static_folder = os.path.join(os.path.dirname(__file__), "..", "static")
    self.js            = []
    self.css           = ["demo_4.css"]

    self.use_python_client(os.path.join(os.path.dirname(__file__), "client.py"))
    self.use_ontology_quadstore(world)
    self.use_session(chat_onto.MySession, chat_onto.ChatRoom)
    self.use_websocket(debug = True)

Sixth, we define four remotely-callable methods:

  • server_get_chat_room_names(): returns a dict mapping chat room names to their labels.

@rpc
def server_get_chat_room_names(self, session):
  return { chat_room.name : chat_room.label.first() for chat_room in ChatRoom.instances() }
  • server_join_chat_room(): joins the given chat room (indicated by its name).

    The current client (identified by its session) joins the given chat room (and implicitely leaves the current one). This method calls session.join_group(). It returns the list of messages in the chat room.

@rpc
def server_join_chat_room(self, session, chat_room_name):
  if session.groups: session.quit_group(session.groups[0])
  chat_room = chat_onto[chat_room_name]
  session.join_group(chat_room)
  return [message.as_tuple() for message in sorted(chat_room.messages, key = lambda message: message.date)]
  • server_create_chat_room(): creates a new chat room.

    This method create a new instance of the ChatRoom class. The new chat room is automatically stored in the ontology quadstore. Finally, it calls client_new_chat_room() for all clients, in order to inform them of the existence of the new chat room.

@rpc
def server_create_chat_room(self, session, chat_room_label):
  with chat_onto:
    chat_room = chat_onto.ChatRoom(label = chat_room_label)
  self.client_new_chat_room(None, chat_room.name, chat_room_label)
  • server_add_message(): adds a new message in the current session’s chat room.

    This method create a new instance of the Message class, and add it to the session’s current chat room. Finally, it calls client_new_message() for all client in the chat room (remember that ChatRoom inherit from Group, and client_xxx() remote functions can be called on a Group).

@rpc
 def server_add_message(self, session, text):
   chat_room = session.groups[0]
   with chat_onto:
     message = chat_onto.Message(date = datetime.datetime.now(), user = session.user, text = text)
     chat_room.messages.append(message)
   chat_room.client_new_message(None, *message.as_tuple())

Seventh, we run the web app with GUnicorn:

from fullpy.server.gunicorn_backend import *
serve_forever([MyWebApp()], "http://127.0.0.1:5000")
Client

First, we import Fullpy:

from fullpy.client import *
from fullpy.client.auth import *

Second, we create the client web app by subclassing ClientSideWebapp. In __init__(), we create two attributes, chat_room (the name of the current chat room) and messages (the list of messages in the current chat room).

class MyWebApp(ClientSideWebapp):
  def __init__(self):
    ClientSideWebapp.__init__(self)
    self.chat_room = None
    self.messages  = []

Third, we override on_session_opened(). This methods is called when a new session is opened (anonymous or authentified).

If the session is anonymous (i.e. user_login is empty), we show a LoginDialog to prompt user for login.

Otherwise, we call server_get_chat_room_names() on the server, in order to obtain the dict of available chat rooms. When the dict is obtained, done() is called back, and we store the dict and call select_chat_room() to select by default the first chat room.

def on_session_opened(self, user_login, user_class, client_data):
  if not user_login:
    LoginDialog(None).show_popup()
    return

  def done(chat_room_names):
    self.chat_room_names = chat_room_names
    self.select_chat_room(sorted(chat_room_names)[0])
  self.server_get_chat_room_names(done)

Fourth, we define the select_chat_room() method. It calls server_join_chat_room() on the server and, when done, it stores the chat room name, the current messages returned by the sever, and call create_html().

def select_chat_room(self, name):
  def done(messages):
    self.chat_room = name
    self.messages  = messages
    self.create_html()
  self.server_join_chat_room(done, name)

Fifth, we define the create_html() method. It creates the main HTML interface, which is composed of 3 HTML widgets (chat room list on the left, message view on the right, and entry box at the bottom right). It assembles the 3 widgets inside an HTML table, and shows it.

def create_html(self):
  self.chat_room_list = ChatRoomList()
  self.message_view   = MessageView()
  self.entry_box      = EntryBox()

  self.main_html = HTML()
  self.main_html << """<table id="chat_table" cellspacing="0"><tr><td>"""
  self.main_html << self.chat_room_list
  self.main_html << """</td><td>"""
  self.main_html << self.message_view
  self.main_html << self.entry_box
  self.main_html << """</td></tr></table>"""
  self.main_html.show()

Sixth, we define the client_new_chat_room() remotely-callable method. It is called by the server whenever a new chat room has been created. In that case, we update the dict of chat room names, and we refresh the chat room list HTML widget.

@rpc
def client_new_chat_room(self, chat_room_name, chat_room_label):
  webapp.chat_room_names[chat_room_name] = chat_room_label
  webapp.chat_room_list.refresh()

Seventh, we define the client_new_message() remotely-callable method. It is called by the server whenever a new message is available in the current chat room. In that case, we add the message to the list of current messages, and we call add_message() on the message view HTML widget.

@rpc
def client_new_message(self, date, user_login, message_text):
  webapp.messages.append((date, user_login, message_text))
  webapp.message_view.add_message(date, user_login, message_text)

The ChatRoomList HTML widget shows the list of available chat rooms. It also has a “Create new room…” button. When clicked, it creates and shows an instance of the NewRoomDialog HTML widget.

class ChatRoomList(HTML):
  def build(self, builder):
    self << """<div id="chat_room_list"><div class="title">FullPy Chat rooms :</div>"""
    for name, label in sorted(webapp.chat_room_names.items(), key = lambda i: i[1]):
      if name == webapp.chat_room:
        self << """<div id="chat_room_%s" class="chat_room selected">%s</div>""" % (name, label)
      else:
        self << """<div id="chat_room_%s" class="chat_room">%s</div>""" % (name, label)
      def on_click(event, name = name):
        webapp.select_chat_room(name)
      self.bind("chat_room_%s" % name, "click", on_click)
    self << """<input id="new_room" type="button" value="Create new room..."></input>"""
    self.bind("new_room", "click", self.on_new_room)
    self << """</div>"""

  def on_new_room(self, event): NewRoomDialog().show_popup()

  def refresh(self): self.show_replace("chat_room_list")

The NewRoomDialog HTML widget ask the user for the label of the new room. If the “Ok” button is clicked, it calls the server_create_chat_room() remotely-callable function.

class NewRoomDialog(HTML):
  def build(self, builder):
    self << """<h2>Create new chat room :</h2>"""
    self << """Room label : <input id="chat_room_label" type="text"></input><br/><br/>"""
    self << """<input id="ok" type="button" value="Ok"></input>"""
    self.bind("ok", "click", self.on_ok)

  def on_ok(self, event):
    chat_room_label = document["chat_room_label"].value.strip()
    webapp.server_create_chat_room(None, chat_room_label)
    hide_popup()

The MessageView HTML widget show the list of messages. It has a add_message() method, that is used to add new message.

class MessageView(HTML):
  def build(self, builder):
    self << """<div id="message_view">"""
    if webapp.messages:
      for date, user_login, message_text in webapp.messages:
        self << self.message_to_html(date, user_login, message_text)
    self << """</div>"""

  def message_to_html(self, date, user_login, message_text):
    if user_login == webapp.user_login:
      html = """<div class="message self">"""
    else:
      html = """<div class="message">"""
    html += """<div class="message_header">%s (%s) :</div>""" % (user_login, date)
    html += """<div class="message_content">%s</div></div>""" % message_text
    return html

  def add_message(self, date, user_login, message_text):
    document["message_view"].insertAdjacentHTML("beforeend", self.message_to_html(date, user_login, message_text))

The EntryBox HTML widget is an input field that can be used to enter new messages. When a new message is entered, it call the server_add_message() remotely-callable function.

class EntryBox(HTML):
  def build(self, builder):
    self << """<div id="entry_box">"""
    self << """<table id="entry_table"><tr><td>Say&nbsp;something:</td>"""
    self << """<td id="entry_td"><input id="entry" type="text"></input></td>"""
    self << """<td><input id="send" type="button" value="Send"></input></td></tr></table>"""
    self << """</div>"""
    self.bind("entry", "keypress", self.on_keypress)
    self.bind("send",  "click",    self.on_send)

  def on_keypress(self, event):
    if event.key == "Enter": self.on_send(event)

  def on_send(self, event):
    text = document["entry"].value.strip()
    if text:
      webapp.server_add_message(None, text)

Finally, we instanciate the web app:

MyWebApp()