OpenDNA Autobahn-Python REPL

A REPL environment for working with WAMP routers in an interactive fashion built using the Autobahn-Python library.

Contents

  1. Installation_

  2. Usage_

    1. Starting the REPL_
    2. Connections_
    3. Sessions_
    4. Calls and Invocations_
    5. Registrations_
    6. Publishers and Publications_
    7. Subscriptions_
  3. Extending_

    1. PtPython config module_
    2. REPL class substitution_
  4. REPL API_

  5. Roadmap_

  6. Credits_

Installation

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.

Usage

Starting the REPL

1. Run the ``autobahn_python_repl`` script installed by this package
2. Run ``python -m opendna.autobahn.repl.repl``

Connections
```````````
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 [email protected]://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 [email protected]://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 string
* ``realm``: Optional. A WAMP realm string
* ``extra``: Optional. A dictionary of data to be supplied to the WAMP
  ``ApplicationSession``.``__init__`` method. Not useful unless you are
  working with a custom ``ApplicationSessions`` class. See *Extending* for
  more details on this.
* ``serializer``: Optional. A list of WAMP serializers to use. Serializers must
  implement ``autobahn.wamp.interfaces.ISerializer``
* ``ssl``: Optional. Boolean or ``ssl.SSLContenxt`` instance. Can usually
  be ignored unless you are planning to connect use TLS authentication for a
  ``Session``
* ``proxy``: Optional. A dictionary providing details for a proxy server. Must
  have ``host`` and ``port`` keys
* ``name``: Optional. A name for the connection

Sessions
````````
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 [email protected]://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 [email protected]://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 [email protected]://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`` and ``ticket`` parameters required
* WAMP-CRA: ``authid`` and ``secret`` parameters required
* WAMP-Cryptosign: ``authid`` and ``key`` parameters required. ``key`` needs to be an instance of ``autobahn.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`` and ``tls``
* ``authid``: String. Optional for WAMP-Anonymous authentication, required for all other methods
* ``authrole``: String. Optional. Requested role
* ``authextra``: Dictionary. Optional. Data to be passed along to the authenticator. Useful
  for providing additional data to a dynamic authenticator
* ``resumable``: Boolean. Optional. Should the session be resumed later if it disconnects
* ``resume_session``: Integer. Optional. ID of Session to resume
* ``resume_token``: String. Optional. Token for resuming session specified by ``resume_session``

Calls and Invocations
`````````````````````
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`_.

.. _autobahn.wamp.types.CallOptions constructor: https://autobahn.readthedocs.io/en/latest/reference/autobahn.wamp.html#autobahn.wamp.types.CallOptions

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 or ``None`` if it failed or hasn't completed yet
* ``exception`` will contain the result of the WAMP Call if it failed or ``None`` if it succeeded or hasn't completed yet
* ``progress`` 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 new ``Invocation`` unless the positional argument is a reference to the singleton object ``opendna.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`` and ``invocation3`` are identical

* If 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 parent ``Invocation`` as if ``Keep`` had been used in their
  positions

* If the number of position arguments supplied is greater than was supplied to the parent ``Invocation`` then the
  additional positional arguments will be ignored

* Any 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`` and ``invocation3`` are identical

Registrations
`````````````
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`_.

.. _autobahn.wamp.types.RegisterOptions constructor: https://autobahn.readthedocs.io/en/latest/reference/autobahn.wamp.html#autobahn.wamp.types.RegisterOptions

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

Publishers and Publications
```````````````````````````
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`_.

.. _autobahn.wamp.types.PublishOptions constructor: https://autobahn.readthedocs.io/en/latest/reference/autobahn.wamp.html#autobahn.wamp.types.PublishOptions

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 the ``acknowledge`` boolean
  parameter supplied to the ``publish`` was set to ``True``. In all other instances it will contain ``None``
* ``exception`` will contain the exception result of the WAMP PubSub event emission if it failed or ``None``
  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 new ``Publication`` unless the positional argument is a reference to the singleton object ``opendna.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`` and ``publication3`` are identical

* If 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 parent ``Publication`` as if ``Keep`` had been used in their
  positions

* If the number of position arguments supplied is greater than was supplied to the parent ``Publication`` then the
  additional positional arguments will be ignored

* Any 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`` and ``publication3`` are identical

Subscriptions
`````````````
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`_.

.. _autobahn.wamp.types.SubscribeOptions constructor: https://autobahn.readthedocs.io/en/latest/reference/autobahn.wamp.html#autobahn.wamp.types.SubscribeOptions

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

Extending
---------
TBD

PtPython config module

TBD

REPL class substitution

TBD


REPL API
--------
TBD


Roadmap
-------

* Improved UI with custom panes/tabs/views for examining Calls, Invocations,
  Publishers, Publications, Registrations and Subscriptions
* ``deregister``/``Unsubscribe`` should clean up the ``Registration``/``Subscription`` instance
* Support usage in other REPLs
* You tell me!


Credits
-------

* 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

Autobahn Python Repl

A REPL environment that uses Autobahn-Python to enable interactive interaction with a WAMP router

Autobahn Python Repl Info

⭐ Stars 10
🔗 Source Code github.com
🕒 Last Update 2 years ago
🕒 Created 4 years ago
🐞 Open Issues 2
➗ Star-Issue Ratio 5
😎 Author opn-oss