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