A REPL environment for working with WAMP routers in an interactive fashion built using the Autobahn-Python library.
pip install autobahn-python-repl
APR requires Python 3.6 to run. If you are not using Python 3.6 in your WAMP project then it is recommend you create a Python 3.6 virtual environment and install the REPL there.
- Run the
autobahn_python_repl
script installed by this package - Run
python -m opendna.autobahn.repl.repl
Once the REPL has started you will be presented with a standard PtPython prompt and environment. In order to begin connecting to a WAMP router enter:
>>> my_connection = connect_to(uri='ws://HOST:PORT', realm='MY_REALM') Generating connection to MY_REALM@ws://HOST:PORT with name g9jZlZeh
You will see that connect_to
generated an internal name for the connection.
You can access the connection via this internal name by entering:
>>> connections.g9jZlZeh <opendna.autobahn.repl.connections.Connection object at 0x6fc2901ab0f0>
It is also possible to provide a custom internal name for the connection when
you call connect_to
as follows:
>>> connect_to(uri='ws://HOST:PORT', realm='MY_REALM', name='my_connection') Generating connection to MY_REALM@ws://HOST:PORT with name my_connection
You can now access the connection by entering:
>>> connections.my_connection <opendna.autobahn.repl.connections.Connection object at 0x2ac690dab0f0> >>> connections['my_connection'] <opendna.autobahn.repl.connections.Connection object at 0x2ac690dab0f0>
Note that the Connection
object is not actually a concrete connection to
the WAMP router, it is merely a storage container for connection related
details that is used to create Session
objects which represent actual
connections to the WAMP router.
connect_to
accepts the follows arguments:
uri
: Required. A WAMP router URI stringrealm
: Optional. A WAMP realm stringextra
: Optional. A dictionary of data to be supplied to the WAMPApplicationSession
.``__init__`` method. Not useful unless you are working with a customApplicationSessions
class. See Extending for more details on this.serializer
: Optional. A list of WAMP serializers to use. Serializers must implementautobahn.wamp.interfaces.ISerializer
ssl
: Optional. Boolean orssl.SSLContenxt
instance. Can usually be ignored unless you are planning to connect use TLS authentication for aSession
proxy
: Optional. A dictionary providing details for a proxy server. Must havehost
andport
keysname
: Optional. A name for the connection
Once you have a Connection
instance you can use it to create a Session
instance, opening a WAMP session in the process:
>>> my_session = my_connection.session() Generating anonymous session to MY_REALM@ws://HOST:PORT with name bKP5ajz0
You can access this session via its auto-generated name like so:
>>> my_connection.sessions.bKP5ajz0 <opendna.autobahn.repl.sessions.Session object at 0x14c2b01a40fd> >>> my_connection.sessions['bKP5ajz0'] <opendna.autobahn.repl.sessions.Session object at 0x14c2b01a40fd>
session
also accepts a name parameter that you can use to avoid using an
auto-generated name.
By default calling session
will open a WAMP-Anonymous session with the router.
It is also possible to specify the authentication method or methods that will be used:
>>> ticket_session = my_connection.session('ticket', authid='your_authid', ticket='YOUR_AUTHENTICATION_TICKET') Generating ticket session to MY_REALM@ws://HOST:PORT with name SOME_NAME >>> mixed_session = my_connection.session(['ticket', 'anonymous'], authid='your_authid', ticket='YOUR_AUTHENTICATION_TICKET') Generating ['ticket', 'anonymous'] session to MY_REALM@ws://HOST:PORT with name SOME_OTHER_NAME
ticket_session will use WAMP-Ticket authentication only while mixed_session will try WAMP-Ticket first before falling back to WAMP-Anonymous.
While WAMP provides a number a authentication methods, only four of are handled
at the session level (as opposed to the transport level). Calling the session
method with a specific authentication method may imply the use of certain additional
parameters. These are detailed below:
- WAMP-Anonymous: No parameters required. Note that
authid
will be ignored if it is supplied - WAMP-Ticket:
authid
andticket
parameters required - WAMP-CRA:
authid
andsecret
parameters required - WAMP-Cryptosign:
authid
andkey
parameters required.key
needs to be an instance ofautobahn.wamp.cryptosign.SigningKey
The Connection.session
method accepts the following arguments:
authmethods
: Optional. String or list of strings. Valid authentication method strings are:anonymous
,ticket
,wampcra
,cryptosign
,cookie
andtls
authid
: String. Optional for WAMP-Anonymous authentication, required for all other methodsauthrole
: String. Optional. Requested roleauthextra
: Dictionary. Optional. Data to be passed along to the authenticator. Useful for providing additional data to a dynamic authenticatorresumable
: Boolean. Optional. Should the session be resumed later if it disconnectsresume_session
: Integer. Optional. ID of Session to resumeresume_token
: String. Optional. Token for resuming session specified byresume_session
In order to perform WAMP RPC calls you need to create a Call
instance. This is
done using a Session
instance:
>>> my_call = my_session.call('endpoint_uri') Generating a call to endpoint endpoint_uri with name i9BcEagW
You can access this call by it's autogenerated name like so:
>>> my_session.calls.i9BcEagW <opendna.autobahn.repl.rpc.Call object at 0xa452bd1a6f2> >>> my_session.calls['i9BcEagW'] <opendna.autobahn.repl.rpc.Call object at 0xa452bd1a6f2>
call
also accepts a custom name parameter to bypass the use of an autogenerated
name. Furthermore, the call
method accepts any keyword-arguments you can
supply to the autobahn.wamp.types.CallOptions constructor.
A Call
instance is itself callable and can be invoked in order to produce an
Invocation
instance. Creating an Invocation
initiates the process of
sending the WAMP RPC call using the Session
instance associated with the
Call
instance that is the parent of the Invocation
:
>>> my_invocation = my_call(True, False, parm3=None, parm4={'something': 'or other'}) Invoking endpoint_uri with name Wax3JdBx Invocation of endpoint_uri with name Wax3JdBx starting Invocation of endpoint_uri with name Wax3JdBx succeeded
Depending on how long it takes for the remote end-point to execute, the message
indicating success or failure may not appear immediately. You will note that
the Invocation
also receives a auto-generated name which can be used to access
it from the Call
instance like so:
>>> my_call.invocations.Wax3JdBx <opendna.autobahn.repl.rpc.Invocation object at 0xd456bc1aef5> >>> my_call.invocations['Wax3JdBx'] <opendna.autobahn.repl.rpc.Invocation object at 0xd456bc1aef5>
The Invocation
instance exposes three important properties that can be
used to access the results of the WAMP Call:
result
will contain the result of the WAMP Call if it succeeded orNone
if it failed or hasn't completed yetexception
will contain the result of the WAMP Call if it failed orNone
if it succeeded or hasn't completed yetprogress
is a list which is used to store progressive results if the target WAMP end-point emits them. See https://crossbar.io/docs/Progressive-Call-Results/ for more details on this
Finally, an Invocation
instance is itself callable. Calling an Invocation
will
produce a new Invocation
instance attached to the parent Call
of the called Invocation
.
The behaviour of the arguments and keyword arguments when calling an Invocation
is quite specific
and affects the creation of the new Invocation
as follows:
Positional arguments will replace the corresponding positional arguments from the parent
Invocation
in the newInvocation
unless the positional argument is a reference to the singleton objectopendna.autobahn.repl.utils.Keep
To illustrate this consider the following input scenario:>>> my_call = my_session.call('some_endpoint') >>> invocation1 = my_call(1,2,3) >>> invocation2 = invocation1(3, Keep, 1) >>> invocation3 = my_call(3,2,1)
In this scenario
invocation2
andinvocation3
are identicalIf the number of positional arguments supplied is less than was supplied to the parent
Invocation
then the missing positional arguments will be substituted in from the parentInvocation
as ifKeep
had been used in their positionsIf the number of position arguments supplied is greater than was supplied to the parent
Invocation
then the additional positional arguments will be ignoredAny keyword arguments will replace the corresponding keyword arguments from the parent
Invocation
:>>> my_call = my_session.call('some_endpoint') >>> invocation1 = my_call(x=True, y=False) >>> invocation2 = invocation1(y=True) >>> invocation3 = my_call(x=True, y=True)
In this scenario
invocation2
andinvocation3
are identical
In order to handle calls to WAMP RPC end-points you need to create a
Registration
instance:
>>> my_registration = my_session.register('endpoint_uri') Generating registration for endpoint_uri with name Rx3mmt2e Registration of endpoint_uri with name Rx3mmt2e starting Registration of endpoint_uri with name Rx3mmt2e succeeded
You can access this registration by it's autogenerated name like so:
>>> my_session.registrations.Rx3mmt2e <opendna.autobahn.repl.rpc.Registration object at 0x7fc89015b0f0> >>> my_session.registrations['Rx3mmt2e'] <opendna.autobahn.repl.rpc.Registration object at 0x7fc89015b0f0>
You can also provide a a custom name parameter to bypass the use of an autogenerated
name. Furthermore, the register
method accepts any keyword-arguments you can
supply to the autobahn.wamp.types.RegisterOptions constructor.
Once a registration has succeeded it is available for calling as described in
the Calls and Invocations section. By default the Registration
class
provides a default handler for incoming calls which records the input parameters
along with the date and time of the call using a a Registration..Hit
instance.
This Hit
is a namedtuple
providing three attributes: timestamp, args
and kwargs. When the registration is the target of a call the console will output text like:
End-point endpoint_uri named Rx3mmt2e hit at 2017-12-01 22:04:10.030438. Hit named jqD8TxFp stored
Hits stored on a registration can be accessed using either the auto-generated name or via a numeric index (hits are stored in the order they are received):
>>> my_registration.hits[0] Hit(timestamp=datetime.datetime(2017, 12, 1, 22, 4, 10, 30438), args=(1, 2, 3, False, True, {}), kwargs={'x': None}) >>> my_registration.hits.jqD8TxFp Hit(timestamp=datetime.datetime(2017, 12, 1, 22, 4, 10, 30438), args=(1, 2, 3, False, True, {}), kwargs={'x': None})
When creating a Registration
it is also possible to specify a custom handler
which is used in addition to the default handler for incoming calls. This custom
handler may be either a standard function or an async function and is called
after the hit is stored by the Registration
instance. Additionally, the result
of the custom handler will be returned to the caller (the default handler will return
None
in the event that no custom handler is supplied):
>>> import asyncio >>> async def test(*args, **kwargs): await asyncio.sleep(5) print(args, kwargs) return True >>> my_registration = my_session.register('endpoint_uri', test) Generating registration for endpoint_uri with name Rx3mmt2e Registration of endpoint_uri with name Rx3mmt2e starting Registration of endpoint_uri with name Rx3mmt2e succeeded >>> invocation = my_session.call('endpoint_uri')(1,2,3,False,True,{},x=None) Generating call to endpoint_uri with name shejtoeU Invoking endpoint_uri with name dgSHC77i Invocation of endpoint_uri with name dgSHC77i starting End-point endpoint_uri named Rx3mmt2e hit at 2017-12-01 22:04:10.030438. Hit named jqD8TxFp stored (1, 2, 3, False, True, {}) {'x': None} Invocation of endpoint_uri with name dgSHC77i succeeded >>> invocation.result True
It is also possible to deregister an existing registration:
>>> my_registration.deregister() Deregistration of endpoint_uri with name Rx3mmt2e starting Deregistration of endpoint_uri with name Rx3mmt2e succeeded
In order to emit WAMP PubSub events you need to create a Publisher
instance:
>>> my_publisher = my_session.publish('topic_uri') Generating publisher for topic_uri with name YunLGYwr
You can access this publisher by it's autogenerated name like so:
>>> my_session.publishers.YunLGYwr <opendna.autobahn.repl.pubsub.Publisher object at 0x7fe1ec20a160> >>> my_session.publishers['YunLGYwr'] <opendna.autobahn.repl.pubsub.Publisher object at 0x7fe1ec20a160>
You can also provide a a custom name parameter to bypass the use of an autogenerated
name. Furthermore, the publish
method accepts any keyword-arguments you can
supply to the autobahn.wamp.types.PublishOptions constructor.
A Publisher
instance is itself callable and can be invoked in order to produce an
Publication
instance. Creating a Publication
initiates the process of
sending the WAMP PubSub event using the Session
instance associated with the
Publisher
instance that is the parent of the Publication
:
>>> my_publication = my_publisher(a=True, b=False) Publication to topic_uri with name CHrYRIn8 starting Publication to topic_uri with name CHrYRIn8 succeeded
You will note that the Publication
also receives a auto-generated name which
can be used to access it from the parent Publisher
instance like so:
>>> my_publisher.publications.CHrYRIn8 <opendna.autobahn.repl.pubsub.Publication object at 0x7fe1f496a5c0> >>> my_publisher.publications['CHrYRIn8'] <opendna.autobahn.repl.pubsub.Publication object at 0x7fe1f496a5c0>
The Publication
instance exposes two important properties that can be
used to access the results of the WAMP PubSub event emission:
result
will contain the result of the WAMP PubSub event emission if theacknowledge
boolean parameter supplied to thepublish
was set toTrue
. In all other instances it will containNone
exception
will contain the exception result of the WAMP PubSub event emission if it failed orNone
if no failure was detected
Finally, a Publication
instance is itself callable. Calling a Publication
will
produce a new Publication
instance attached to the parent Publisher
of the
called Publication
. The behaviour of the arguments and keyword arguments when
calling a Publication
is quite specific and affects the creation of the new
Publication
as follows:
Positional arguments will replace the corresponding positional arguments from the parent
Publication
in the newPublication
unless the positional argument is a reference to the singleton objectopendna.autobahn.repl.utils.Keep
To illustrate this consider the following input scenario:>>> my_publisher = my_session.publish('some_topic') >>> publication1 = my_publisher(1,2,3) >>> publication2 = publication1(3, Keep, 1) >>> publication3 = my_publisher(3,2,1)
In this scenario
publication2
andpublication3
are identicalIf the number of positional arguments supplied is less than was supplied to the parent
Publication
then the missing positional arguments will be substituted in from the parentPublication
as ifKeep
had been used in their positionsIf the number of position arguments supplied is greater than was supplied to the parent
Publication
then the additional positional arguments will be ignoredAny keyword arguments will replace the corresponding keyword arguments from the parent
Publication
:>>> my_publisher = my_session.publish('some_topic') >>> publication1 = my_publisher(x=True, y=False) >>> publication2 = publication1(y=True) >>> publication3 = my_publisher(x=True, y=True)
In this scenario
publication2
andpublication3
are identical
In order to subscribe to WAMP PubSub topics you need to create a Subscription
instance:
>>> my_subscription = my_session.subscribe('topic_uri') Generating subscription for topic_uri with name bIMq6XcO Subscription to topic_uri with name bIMq6XcO starting Subscription to topic_uri with name bIMq6XcO succeeded
You can access this subscription by it's autogenerated name like so:
>>> my_session.subscriptions.bIMq6XcO <opendna.autobahn.repl.pubsub.Subscription object at 0x7fe1f5f9aef0> >>> my_session.subscriptions['bIMq6XcO'] <opendna.autobahn.repl.pubsub.Subscription object at 0x7fe1f5f9aef0>
You can also provide a a custom name parameter to bypass the use of an autogenerated
name. Furthermore, the subscribe
method accepts any keyword-arguments you can
supply to the autobahn.wamp.types.SubscribeOptions constructor.
Once a subscription has succeeded it will be notified of WAMP PubSub events
emitted as described in the Publishers and Publications section. Note, however,
that by default a subscription to a topic will only receive events emitted by
other sessions. The exclude_me parameter for the Publisher
must be set to
True
if you wish to test publication and subscription to a given topic within
a single Session
.
The Subscription
class provides a default handler for incoming events which
records the input parameters along with the date and time of the call using a
Subscription.Hit
instance. This Event
is a namedtuple
providing three
attributes: timestamp, args and kwargs. When the subscription receives an
event the console will output text like:
Event named s3X0Sbhc received at 2017-12-03 21:59:55.437068 on topic topic_uri named bIMq6XcO
Events stored on a subscription can be accessed using either the auto-generated name or via a numeric index (hits are stored in the order they are received):
>>> my_subscription.events[0] Event(timestamp=datetime.datetime(2017, 12, 1, 22, 4, 10, 30438), args=(1, 2, 3, False, True, {}), kwargs={'x': None}) >>> my_subscription.events.jqD8TxFp Event(timestamp=datetime.datetime(2017, 12, 1, 22, 4, 10, 30438), args=(1, 2, 3, False, True, {}), kwargs={'x': None})
When creating a Subscription
it is also possible to specify a custom handler
which is used in addition to the default handler for incoming events. This custom
handler may be either a standard function or an async function and is called
after the event is stored by the Subscription
instance:
>>> async def test(*args, **kwargs): print(args, kwargs) >>> my_subscription = my_session.subscribe('topic_uri', test) Generating subscription for topic_uri with name bIMq6XcO Subscription to topic_uri with name bIMq6XcO starting Subscription to topic_uri with name bIMq6XcO succeeded >>> publication = my_session.publish('topic_uri', exclude_me=False)(1,2,3,False,True,{},x=None) Generating publisher for topic_uri with name VVjZjvF5 Publication to topic_uri with name sjfuAGSm starting Publication to topic_uri with name sjfuAGSm succeeded Event named ZbbzBrxJ received at 2017-12-03 22:18:10.383218 on topic topic_uri named bIMq6XcO (1, 2, 3, False, True, {}) {'x': None} >>> my_subscription.events.ZbbzBrxJ Event(timestamp=datetime.datetime(2017, 12, 3, 22, 18, 10, 383218), args=(1, 2, 3, False, True, {}), kwargs={'x': None})
It is also possible to unsubscribe from a topic:
>>> my_subscription.unsubscribe() Unsubscription from topic_uri with name bIMq6XcO starting Unsubscription from topic_uri with name bIMq6XcO succeeded
TBD
TBD
TBD
TBD
- Improved UI with custom panes/tabs/views for examining Calls, Invocations, Publishers, Publications, Registrations and Subscriptions
deregister
/Unsubscribe
should clean up theRegistration
/Subscription
instance- Support usage in other REPLs
- You tell me!
- Autobahn-Python for providing the secret WAMP sauce
- PtPython for providing the secret REPL sauce
- Jedi for providing PtPython with the secret code completion sauce
- PromptToolkit for providing PtPython with the prompt secret sauce