Messaging

Sending and Responding

In order to send and respond to messages xbahn utilizes an event system.

Server

import xbahn.connection
import xbahn.connection.link
import xbahn.message

import signal

if __name__ == "__main__":

    # listen using zmq REP sockect over TCP (default)
    connection = xbahn.connection.listen("zmq://0.0.0.0:7050/rep")
    link = xbahn.connection.link.Link(respond=connection, receive=connection)

    def handle_incoming_message(message=None, wire=None, event_origin=None):
        print(message.data) # Test
        print(message.args) # ["an arg"]
        print(message.kwargs) # {"something":"extra"}
        # send response
        wire.respond(message, xbahn.message.Message("Message '%s' received!" % message.data))

    # bind handle_incoming_message to receive event 
    link.on("receive", handle_incoming_message)

    print("Server running!")

    # run until ctrl+c
    connection.wait_for_signal(signal.SIGINT)

Client

import xbahn.connection
import xbahn.connection.link
import xbahn.message

if __name__ == "__main__":

    # connect to api server using zmq REQ sockect over TCP (default)
    connection = xbahn.connection.connect("zmq://0.0.0.0:7050/req")
    link = xbahn.connection.link.Link(send=connection, receive=connection)

    # print whatever message we get back from the other end
    def handle_response(message=None, wire=None, event_origin=None):
        print(message.data)
    link.on("receive", handle_response)

    # send a message to the other side and wait for response (using the main wire)
    link.main.send_and_wait("test", xbahn.message.Message("Test", "an arg", something="extra"))

You could also attach a handler to the "response" event of the message itself

def handle_response(**kwargs):
    original_message = kwargs.get("event_origin")
    response_message = kwargs.get("message")
    print("Message received response:", response_message.data)

message = xbahn.message.Message("Test message!")
message.on("repsonse", handle_response)

link.main.send_and_wait("test", message)

Xbahn Pathing

At the core of xbahn lies a message routing system working with '.' delimited paths.

a.b.c

Server

import xbahn.connection
import xbahn.connection.link
import xbahn.message

import signal

if __name__ == "__main__":

    # listen using zmq REP sockect over TCP (default)
    connection = xbahn.connection.listen("zmq://0.0.0.0:7050/rep")
    link = xbahn.connection.link.Link(respond=connection, receive=connection)

    def handle_incoming_person(message=None, wire=None, event_origin=None):
        print("Message arrived", "PERSON", message.data, message.path)

    def handle_incoming_address(message=None, wire=None, event_origin=None):
        print("Message arrived", "ADDRESS", message.data, message.path)

    # handle messages sent to 'person' path
    link.on("receive_person", handle_incoming_person)

    # handle messages sent to 'address' path
    link.on("receive_address", handle_incoming_address)

    print("Server running!")

    # run until ctrl+c
    connection.wait_for_signal(signal.SIGINT)

Client

import xbahn.connection
import xbahn.connection.link
import xbahn.message

if __name__ == "__main__":

    # connect to api server using zmq REQ sockect over TCP (default)
    connection = xbahn.connection.connect("zmq://0.0.0.0:7050/req")
    link = xbahn.connection.link.Link(send=connection, receive=connection)

    # send message to "person" path
    link.main.send("person", xbahn.message.Message("John Smith"))

    # send message to "address" path
    link.main.send("address", xbahn.message.Message("Some Street, 123"))

    # send message to "address" sub path
    link.main.send("address.usa", xbahn.message.Message("Another Street, 123"))

The result on the server

Server running!
Message arrived PERSON John Smith person
Message arrived ADDRESS Some Street, 123 address
Message arrived ADDRESS Another Street, 123 address.usa

API

A few examples to get you started with the xbahn API, using zmq as a handler.

All examples were tested with python 3.4

One way communication

It should be noted, when i refer to one-way communication here i am talking about who can initiate requests, not whether or not the second party is able to respond to the request of the first.

In other words, one way communication allows you to implement a simple request -> reply line of comminucation where the second party (in this case the server) is only allows to communicate to the first party (in this case the client) through responses to requests made by the first party..

Server

import xbahn.api
import xbahn.connection
import xbahn.shortcuts

import signal

class Server(xbahn.api.Server):

    @xbahn.api.expose
    def ping(self):
        """
        Something to call for our client
        """
        return "pong!"

    @xbahn.api.expose
    def multiply(self, a, b):
        """
        Multiply a with b and return result to client
        """
        return int(a) * int(b)


if __name__ == "__main__":

    # listen using zmq REP sockect over TCP (default)
    connection = xbahn.connection.listen("zmq://0.0.0.0:7050/rep")

    # run api server on connection
    server = xbahn.shortcuts.api_server(connection, Server)

    print("Server running!")

    # clean exit on ctrl+c
    connection.wait_for_signal(signal.SIGINT)

Client

import xbahn.shortcuts
import xbahn.connection

if __name__ == "__main__":

    # connect to api server using zmq REQ sockect over TCP (default)
    connection = xbahn.connection.connect("zmq://0.0.0.0:7050/req")

    # run api client on our connection
    client = xbahn.shortcuts.api_client(connection)

    print("ping!")
    # send "ping" call to server and print the response
    # by default this will block until a response is received or the
    # request fails.
    print(client.ping()) # pong!

    # multiply 7 and 6 and print the result
    print("7*6 =",client.multiply(7,6))

Two way comminucation

Two way comminucation means both parties may initiate requests to each other. Once the client connects to the server, the server will automatically establish a connection to the client responder, to allow it to initiate requests to the client as well.

Server

import xbahn.api
import xbahn.connection
import xbahn.shortcuts

import signal

# We extend ClientAwareServer because for two-way communication
# to work, the server needs to be.. well.. aware of the clients
# connected to it
class Server(xbahn.api.ClientAwareServer):

    @xbahn.api.expose
    def ping(self):
        """
        Something to call for our client
        """
        return "pong!"


if __name__ == "__main__":

    # listen using zmq REP sockect over TCP (default)
    connection = xbahn.connection.listen("zmq://0.0.0.0:7050/rep")

    # run api server on connection
    server = xbahn.shortcuts.api_server(connection, Server)

    # every time a new client makes contact call touch() on the client
    server.on("new_client", lambda client, **kw: client.touch())
    print("Server running!")

    # clean exit on ctrl+c
    connection.wait_for_signal(signal.SIGINT)

Client

import xbahn.api
import xbahn.connection
import xbahn.shortcuts

class Client(xbahn.api.Client):

    touched = False

    @xbahn.api.expose
    def touch(self):
        """
        Something for the server to call
        """
        self.touched = True
        return True

if __name__ == "__main__":

    # connect to api server using zmq REQ sockect over TCP (default)
    connection = xbahn.connection.connect("zmq://0.0.0.0:7050/req")

    # since this client should also be able to receive messages and respond
    # as part of the two-way communcation, we need to define a secondary
    # connection for that, using zmq REP socket over TCP (default)
    connection_responder = xbahn.connection.listen("zmq://0.0.0.0:7051/rep")

    # run client on our connections
    client = xbahn.shortcuts.api_client_two_way(
        connection,
        connection_responder,
        client_class=Client
    )

    print("ping!")
    # send "ping" call to server and print the response
    # by default this will block until a response is received or the
    # request fails.
    print(client.ping()) # pong!

    # since the server calls client.touch() every time a new client
    # makes contact, our client should now reflect that
    print("Touched?", client.touched)

    # we are done
    connection_responder.destroy()

API Widgets

API Widgets are a modular way to attach functionality to an api communicator (client or server)

Unaware Server

A normal api server will have common widget instances shared by all clients

Server

import xbahn.api
import xbahn.connection
import xbahn.shortcuts

import signal

# we are going to need a server class to register
# our widget on.
class Server(xbahn.api.Server):
    pass

# We register a widget on our server with the 
# name 'demo', the client will need to use the 
# same name when defining it's widget
@Server.widget('demo')
class DemoWidget(xbahn.api.Widget):

    num = 0

    @xbahn.api.expose
    def set_number(self, num):
        """
        Something for our client widget to call
        """
        self.num = num

    @xbahn.api.expose
    def get_number(self):
        """
        Returns the value in self.num
        """
        return self.num

if __name__ == "__main__":

    # listen using zmq REP sockect over TCP (default)
    connection = xbahn.connection.listen("zmq://0.0.0.0:7050/rep")

    # run api server on connection
    server = xbahn.shortcuts.api_server(connection, Server)

    print("Server running!")

    # clean exit on ctrl+c
    connection.wait_for_signal(signal.SIGINT)

Client

import xbahn.shortcuts
import xbahn.connection
import xbahn.api

# we are going to need a client class to register
# our widget on
class Client(xbahn.api.Client):
    pass

# We register a widget on our client with the name
# 'demo', this corresponse with name for the widget
# we defined on the server side
@Client.widget('demo')
class DemoWidget(xbahn.api.Widget):
    pass

if __name__ == "__main__":

    # connect to api server using zmq REQ sockect over TCP (default)
    connection = xbahn.connection.connect("zmq://0.0.0.0:7050/req")

    # run api client on our connection
    client = xbahn.shortcuts.api_client(connection)

    # instantiate widget using the client
    widget_a = DemoWidget(client, "widget_a")
    widget_a.set_number(123)
    print("Number on the server widget (A)", widget_a.get_number()) # 123

    # lets make another widget
    widget_b = DemoWidget(client, "widget_b")

    # since it's id 'widget_b' is different its not sharing the same
    # instance on the remote end as widget_a, meaning its number is unset
    print("Number on the server widget (B)", widget_b.get_number()) # 0

    # this one shares the same id as widget_a, meaning it will be 
    # connected to the same instance on the remote end
    widget_c = DemoWidget(client, "widget_a")
    print("Number on the server widget (C through A)", widget_c.get_number()) # 123

Widget Aware Server

A widget-aware server groups widgets by client connection, meaning each client only has access to the widgets it spawns.

Additionally, it also allows for the server to call functions on the client side widget (similar to API two-way communication example)

Server

import xbahn.api
import xbahn.connection
import xbahn.shortcuts

import signal

# we are going to need a server class to register
# our widget on. This time we use the WidgetAwareServer
# as base
class Server(xbahn.api.WidgetAwareServer):
    pass

# We register a widget on our server with the 
# name 'demo', the client will need to use the 
# same name when defining it's widget
@Server.widget('demo')
class DemoWidget(xbahn.api.Widget):

    _poked = False

    @xbahn.api.expose
    def poked(self):
        """
        Returns self._poked
        """
        return self._poked

    @xbahn.api.expose
    def poke(self):
        """
        Something for the client widget to call
        """
        self._poked = True

        # now call slap() on the client
        self.slap()


if __name__ == "__main__":

    # listen using zmq REP sockect over TCP (default)
    connection = xbahn.connection.listen("zmq://0.0.0.0:7050/rep")

    # run api server on connection
    server = xbahn.shortcuts.api_server(connection, Server)
    print("Server running!")

    # clean exit on ctrl+c
    connection.wait_for_signal(signal.SIGINT)

Client

import xbahn.shortcuts
import xbahn.connection
import xbahn.api

# we are going to need a client class to register
# our widget on
class Client(xbahn.api.Client):
    pass

# We register a widget on our client with the name
# 'demo', this corresponse with name for the widget
# we defined on the server side
@Client.widget('demo')
class DemoWidget(xbahn.api.Widget):

    _slapped = False

    @xbahn.api.expose
    def slapped(self):
        """
        Returns self._slapped
        """
        return self._slapped

    @xbahn.api.expose
    def slap(self):
        """
        Something for the server widget to call
        """
        self._slapped = True

if __name__ == "__main__":

    # connect to api server using zmq REQ sockect over TCP (default)
    connection = xbahn.connection.connect("zmq://0.0.0.0:7050/req")

    # since this client should also be able to receive messages and respond
    # as part of the two-way communcation, we need to define a secondary
    # connection for that, using zmq REP socket over TCP (default)
    connection_responder = xbahn.connection.listen("zmq://0.0.0.0:7051/rep")

    # run client on our connections
    client = xbahn.shortcuts.api_client_two_way(
        connection,
        connection_responder,
        client_class=Client
    )

    # instantiate widget using the client
    widget_a = DemoWidget(client, "widget_a")

    # call "poke" on the server widget
    widget_a.poke()

    # during poke() the server should have called slap() on the client widget
    print("Slapped (a)?", widget_a.slapped()) # True (called local)
    print("Poked (a)?", widget_a.poked()) # True (called remote)

    # lets connect another widget on a separate client
    client_b = xbahn.shortcuts.api_client(connection, client_class=Client)
    widget_a_b = DemoWidget(client_b, "widget_a")
    # Widget aware servers are also aware of clients and each client will
    # maintain its own widget instance on the remote side
    print("Poked (b)?", widget_a_b.poked()) # False (called remote)

    # we are done
    connection_responder.destroy()

Engineer

Engineer is a cli built upon click and xbahn, it allows you to call xbahn expose functions via a command line interface.

In order to expose api functionality to engineer the Widget system is used. Here is a quick and dirty example.

import signal

import xbahn.api as api
import xbahn.engineer as engineer
import xbahn.connection.link as link
import xbahn.connection.zmq

from xbahn.connection import listen

class Server(api.Server):
    status = "ok"

    def do(self, what):
        return "did %s" % what


@Server.widget('engineer')
class Engineer(engineer.ServerWidget):
    @engineer.expose()
    def status(self):
        return self.comm.status

    @engineer.argument("what")
    @engineer.expose()
    def do_something(self, what):
        """
        Do the task specified in [WHAT]
        """
        return self.comm.do(what)

    @engineer.option("--extra/--no-extra", default=False, help="include extra info")
    @engineer.expose()
    def show(self, extra=False):
        rv = ["Basic"]
        if extra:
            rv.append("Extra!!")
        return rv

if __name__ == "__main__":
    conn = listen("zmq://0.0.0.0:7050/rep")
    lnk = link.Link()
    lnk.wire("main", receive=conn, respond=conn)
    server = Server(link=lnk)
    print("Server running ...")
    conn.wait_for_signal(signal.SIGINT)

Usage

Passing --help without any other options or arguments

engineer --help
Usage: engineer [OPTIONS] HOST COMMAND [ARGS]...

  run an engineer exposed action on a remote xbahn host

Options:
  --version             Show the version and exit.
  --debug / --no-debug  Show debug information
  --help                Show this message and exit.

Commands:
  shell  open an engineer shell

Passing --help with the host argument will show you what commands the server will accept

engineer zmq://0.0.0.0:7050/req --help
Usage: engineer [OPTIONS] HOST COMMAND [ARGS]...

  run an engineer exposed action on a remote xbahn host

Options:
  --version             Show the version and exit.
  --debug / --no-debug  Show debug information
  --help                Show this message and exit.

Commands:
  do_something  Do the task specified in [WHAT]
  show
  status
  shell         open an engineer shell

Calling the do_something command

engineer zmq://0.0.0.0:7050/req do_something "say hello"
zmq://0.0.0.0:7050/req: do_something> did say hello