shellbot package

Module contents

class shellbot.Bus(context)[source]

Bases: object

Represents an information bus between publishers and subscribers

In the context of shellbot, channels are channel identifiers, and messages are python objects serializable with json.

A first pattern is the synchronization of direct channels from the group channel:

  • every direct channel is a subscriber, and filters messages sent to their own channel identifier
  • group channel is a publisher, and broadcast instructions to the list of direct channel identifiers it knows about

A second pattern is the observation by a group channel of what is happening in related direct channels:

  • every direct channel is a publisher, and the channel used is their own channel identifier
  • group channel is a subscriber, and observed messages received from all direct channels it knows about

For example, a distributed voting system can be built by combining the two patterns. The vote itself can be triggered simultaneously to direct channels on due time, so that every participants are involved more or less at the same time. And data that is collected in direct channels can ce centralised back to the group channel where results are communicated.

DEFAULT_ADDRESS = 'tcp://127.0.0.1:5555'
check()[source]

Checks configuration settings

This function reads key bus and below, and update the context accordingly. It handles following parameters:

  • bus.address - focal point of bus exchanges on the network. The default value is tcp://*:5555 which means ‘use TCP port 5555 on local machine’.
publish()[source]

Publishes messages

Returns:Publisher

Example:

# get a publisher for subsequent broadcasts
publisher = bus.publish()

# start the publishing process
publisher.start()

...

# broadcast information_message
publisher.put(channel, message)
subscribe(channels)[source]

Subcribes to some channels

Parameters:channels (str or list of str) – one or multiple channels
Returns:Subscriber

Example:

# subscribe from all direct channels related to this group channel
subscriber = bus.subscribe(bot.direct_channels)

...

# get next message from these channels
message = subscriber.get()
class shellbot.Channel(attributes=None)[source]

Bases: object

Represents a chat channel managed by a space

A channel is a group of interactions within a chat space. It features a unique identifier, and a title. It also have participants and content.

This class is a general abstraction of a communication channel, that can easily been adapted to various chat systems. It has been designed as a dictionary wrapper, with minimum exposure to shellbot, while enabling the transmission of rich information through serialization.

Instances of Channel are created within a Space object, and consumed by it as well.

For example, to create a channel with a given title, you could write:

channel = space.create(title='A new channel')
bot.say(u"I am happy to join {}".format(channel.title))

And to change the title of the channel:

channel.title = 'An interesting place'
space.update(channel)

Direct channels support one-to-one interactions between the bot and one person. The creation of a direct channel can only be indirect, by sending an invitation to the target person. For example:

bot.say(person='foo.bar@acme.com',
        text='Do you want to deal with me?')

If the person receives and accepts the invitation, the engine will receive a join event and load a new bot devoted to the direct channel. So, at the end of the day, when multiple persons interact with a shellbot, this involve both group and direct channels.

For example, if you create a shellbot named shelly, that interacts with Alice and with Bob, then shelly will overlook multiple bots and channels:

  • shelly main channel (bot + channel + store + state machine)
  • direct channel with Alice (bot + channel + store + state machine)
  • direct channel with Bob (bot + channel + store + state machine)
get(key, default=None)[source]

Returns the value of one attribute :param key: name of the attribute :type key: str

Parameters:default (str or other serializable object) – default value of the attribute
Returns:value of the attribute
Return type:str or other serializable object or None

The use case for this function is when you adapt a channel that does not feature an attribute that is expected by shellbot. More specifically, call this function on optional attributes so as to avoid AttributeError

id

Returns channel unique id

Return type:str
is_direct

Indicates if this channel is only for one person and the bot

Return type:str

A channel is deemed direct when it is reserved to one-to-one interactions. Else it is considered a group channel, with potentially many participants.

is_moderated

Indicates if this channel is moderated

Return type:str

A channel is moderated when some participants have specific powers that others do not have. Else all participants are condidered the same and peer with each others.

title

Returns channel title

Return type:str
class shellbot.Command(engine=None, **kwargs)[source]

Bases: object

Implements one command

execute(bot, arguments=None, **kwargs)[source]

Executes this command

Parameters:
  • bot (Shellbot) – The bot for this execution
  • arguments (str or None) – The arguments for this command

The function is invoked with a variable number of arguments. Therefore the need for **kwargs, so that your code is safe in all cases.

The recommended signature for commands that handle textual arguments is the following:

``` def execute(self, bot, arguments=None, **kwargs):

... if arguments:

...

```

In this situation, arguments contains all text typed after the verb itself. For example, when the command magic is invoked with the string:

magic rub the lamp

then the related command instance is called like this:

magic = shell.command('magic')
magic.execute(bot, arguments='rub the lamp')

For commands that can handle file attachments, you could use following approach:

def execute(self,
            bot,
            arguments=None,
            attachment=None,
            url=None,
            **kwargs):
    ...
    if url:  # a document has been uploaded with this command
        content = bot.space.download_attachment(url)
        ...

Reference information on parameters provided by the shell:

  • bot - This is the bot instance for which the command is executed. From this you can update the chat with bot.say(), or access data attached to the bot in bot.store. The engine and all global items can be access with bot.engine.
  • arguments - This is a string that contains everything after the command verb. When hello How are you doing? is submitted to the shell, hello is the verb, and How are you doing? are the arguments. This is the regular case. If there is no command hello then the command *default is used instead, and arguments provided are the full line hello How are you doing?.
  • attachment - When a file has been uploaded, this attribute provides its external name, e.g., picture024.png. This can be used in the executed command, if you keep in mind that the same name can be used multiple times in a conversation.
  • url - When a file has been uploaded, this is the handle by which actual content can be retrieved. Usually, ask the underlying space to get a local copy of the document.

This function should report on progress by sending messages with one or multiple bot.say("Whatever response").

in_direct = True
in_group = True
information_message = None
is_hidden = False
keyword = None
on_init()[source]

Handles extended initialisation

This function should be expanded in sub-class, where necessary.

Example:

def on_init(self):
    self.engine.register('stop', self)
usage_message = None
class shellbot.Context(settings=None, filter=None)[source]

Bases: object

Stores settings across multiple independent processing units

This is a key-value store, that supports concurrency across multiple processes.

apply(settings={})[source]

Applies multiple settings at once

Parameters:settings (dict) – variables to be added to this context
check(key, default=None, is_mandatory=False, validate=None, filter=False)[source]

Checks some settings

Parameters:
  • key – the key that has to be checked
  • default (str) – the default value if no statement can be found
  • is_mandatory (bool) – raise an exception if keys are not found
  • validate (callable) – a function called to validate values before the import
  • filter (bool) – look at the content, and change it eventually

Example:

context = Context({
    'spark': {
        'room': 'My preferred room',
        'participants':
            ['alan.droit@azerty.org', 'bob.nard@support.tv'],
        'team': 'Anchor team',
        'token': 'hkNWEtMJNkODk3ZDZLOGQ0OVGlZWU1NmYtyY>',
        'webhook': "http://73a1e282.ngrok.io",
        'weird_token', '$WEIRD_TOKEN',
    }
})

context.check('spark.room', is_mandatory=True)
context.check('spark.team')
context.check('spark.weird_token', filter=True)

When a default value is provided, it is used to initialize properly a missing key:

context.check('general.switch', 'on')

Another usage is to ensure that a key has been set:

context.check('spark.room', is_mandatory=True)

Additional control can be added with the validation function:

context.check('general.switch',
              validate=lambda x: x in ('on', 'off'))

When filter is True, if the value is a string starting with ‘$’, then a variable with the same name is loaded from the environment:

>>>token=context.check('spark.weird_token', filter=True)
>>>assert token == os.environ.get('WEIRD_TOKEN')
True

The default filter can be changed at the creation of a context:

>>>context=Context(filter=lambda x : x + '...')

This function raises KeyError if a mandatory key is absent. If a validation function is provided, then a ValueError can be raised as well in some situations.

clear()[source]

Clears content of a context

decrement(key, delta=1)[source]

Decrements a value

get(key, default=None)[source]

Retrieves the value of one configurationkey

Parameters:
  • key (str) – name of the value
  • default (any serializable type is accepted) – default value
Returns:

the actual value, or the default value, or None

Example:

message = context.get('bot.on_start')

This function is safe on multiprocessing and multithreading.

has(prefix)[source]

Checks the presence of some prefix

Parameters:prefix (str) – key prefix to be checked
Returns:True if one or more key start with the prefix, else False

This function looks at keys actually used in this context, and return True if prefix is found. Else it returns False.

Example:

context = Context(settings={'space': {'title', 'a title'}})

>>>context.has('space')
True

>>>context.has('space.title')
True

>>>context.has('spark')
False
increment(key, delta=1)[source]

Increments a value

is_empty

Does the context store something?

Returns:True if there at least one value, False otherwise
set(key, value)[source]

Changes the value of one configuration key

Parameters:
  • key (str) – name of the value
  • value (any serializable type is accepted) – new value

Example:

context.set('bot.on_start', 'hello world')

This function is safe on multiprocessing and multithreading.

classmethod set_logger(level=10)[source]

Configure logging

Parameters:level – expected level of verbosity

This utility function should probably be put elsewhere

class shellbot.Engine(context=None, settings={}, configure=False, mouth=None, ears=None, fan=None, space=None, type=None, server=None, store=None, command=None, commands=None, driver=<class 'shellbot.bot.ShellBot'>, machine_factory=None, updater_factory=None, preload=0)[source]

Bases: object

Powers multiple bots

The engine manages the infrastructure that is used accross multiple bots acting in multiple spaces. It is made of an extensible set of components that share the same context, that is, configuration settings.

Shellbot allows the creation of bots with a given set of commands. Each bot instance is bonded to a single chat space. The chat space can be either created by the bot itself, or the bot can join an existing space.

The first use case is adapted when a collaboration space is created for semi-automated interactions between human and machines. In the example below, the bot controls the entire life cycle of the chat space. A chat space is created when the program is launched. And it is deleted when the program is stopped.

Example of programmatic chat space creation:

from shellbot import Engine, ShellBot, Context, Command
Context.set_logger()

# create a bot and load command
#
class Hello(Command):
    keyword = 'hello'
    information_message = u"Hello, World!"

engine = Engine(command=Hello(), type='spark')

# load configuration
#
engine.configure()

# create a chat space, or connect to an existing one
# settings of the chat space are provided
# in the engine configuration itself
#
engine.bond(reset=True)

# run the engine
#
engine.run()

# delete the chat channel when the engine is stopped
#
engine.dispose()

A second interesting use case is when a bot is invited to an existing chat space. On such an event, a new bot instance can be created and bonded to the chat space.

Example of invitation to a chat space:

def on_enter(self, channel_id):
    bot = engine.get_bot(channel_id=channel_id)

The engine is configured by setting values in the context that is attached to it. This is commonly done by loading the context with a dict before the creation of the engine itself, as in the following example:

context = Context({

    'bot': {
        'on_enter': 'You can now chat with Batman',
        'on_exit': 'Batman is now quitting the channel, bye',
    },

    'server': {
        'url': 'http://d9b62df9.ngrok.io',
        'hook': '/hook',
    },

})

engine = Engine(context=context)

engine.configure()

Please note that the configuration is checked and actually used on the call engine.configure(), rather on the initialisation itself.

When configuration statements have been stored in a separate text file in YAML format, then the engine can be initialised with an empty context, and configuration is loaded afterwards.

Example:

engine = Engine()
engine.configure_from_path('/opt/shellbot/my_bot.yaml')

When no configuration is provided to the engine, then default settings are considered for the engine itself, and for various components.

For example, for a basic engine interacting in a Cisco Spark channel:

engine = Engine(type='spark')
engine.configure()

When no indication is provided at all, the engine loads a space of type ‘local’.

So, in other terms:

engine = Engine()
engine.configure()

is strictly equivalent to:

engine = Engine('local')
engine.configure()

In principle, the configuration of the engine is set once for the full life of the instance. This being said, some settings can be changed globally with the member function set(). For example:

engine.set('bot.on_banner': 'Hello, I am here to help')
DEFAULT_SETTINGS = {'bot': {'banner.content': '$BOT_BANNER_CONTENT', 'on_enter': '$BOT_ON_ENTER', 'on_exit': '$BOT_ON_EXIT', 'banner.text': '$BOT_BANNER_TEXT', 'banner.file': '$BOT_BANNER_FILE'}}
bond(title=None, reset=False, participants=None, **kwargs)[source]

Bonds to a channel

Parameters:
  • title – title of the target channel
  • reset (bool) – if True, delete previous channel and re-create one
  • participants (list of str) – the list of initial participants (optional)
Type:

title: str

Returns:

Channel or None

This function creates a channel, or connect to an existing one. If no title is provided, then the generic title configured for the underlying space is used instead.

For example:

channel = engine.bond('My crazy channel')
if channel:
    ...

Note: this function asks the listener to load a new bot in its cache on successful channel creation or lookup. In other terms, this function can be called safely from any process for the creation of a channel.

build_bot(id=None, driver=<class 'shellbot.bot.ShellBot'>)[source]

Builds a new bot

Parameters:id (str) – The unique id of the target space
Returns:a ShellBot instance, or None

This function receives the id of a chat space, and returns the related bot.

build_machine(bot)[source]

Builds a state machine for this bot

Parameters:bot (ShellBot) – The target bot
Returns:a Machine instance, or None

This function receives a bot, and returns a state machine bound to it.

build_store(channel_id=None)[source]

Builds a store for this bot

Parameters:channel_id (str) – Identifier of the target chat space
Returns:a Store instance, or None

This function receives an identifier, and returns a store bound to it.

build_updater(id)[source]

Builds an updater for this channel

Parameters:id (str) – The identifier of an audited channel
Returns:an Updater instance, or None

This function receives a bot, and returns a state machine bound to it.

check()[source]

Checks settings of the engine

Parameters:settings (dict) – a dictionary with some statements for this instance

This function reads key bot and below, and update the context accordingly.

Example:

context = Context({

    'bot': {
        'on_enter': 'You can now chat with Batman',
        'on_exit': 'Batman is now quitting the channel, bye',
    },

    'server': {
        'url': 'http://d9b62df9.ngrok.io',
        'hook': '/hook',
    },

})
engine = Engine(context=context)
engine.check()
configure(settings={})[source]

Checks settings

Parameters:settings (dict) – configuration information

If no settings is provided, and the context is empty, then self.DEFAULT_SETTINGS and self.space.DEFAULT_SETTINGS are used instead.

configure_from_file(stream)[source]

Reads configuration information

Parameters:stream (file) – the handle that contains configuration information

The function loads configuration from the file and from the environment. Port number can be set from the command line.

configure_from_path(path='settings.yaml')[source]

Reads configuration information

Parameters:path (str) – path to the configuration file

The function loads configuration from the file and from the environment. Port number can be set from the command line.

dispatch(event, **kwargs)[source]

Triggers objects that have registered to some event

Parameters:event (str) – label of the event

Example:

def on_bond(self):
    self.dispatch('bond', bot=this_bot)

For each registered object, the function will look for a related member function and call it. For example for the event ‘bond’ it will look for the member function ‘on_bond’, etc.

Dispatch uses weakref so that it affords the unattended deletion of registered objects.

dispose(title=None, **kwargs)[source]

Destroys a named channel

Parameters:title – title of the target channel
Type:title: str
enumerate_bots()[source]

Enumerates all bots

get(key, default=None)[source]

Retrieves the value of one configuration key

Parameters:
  • key (str) – name of the value
  • default (any serializable type is accepted) – default value
Returns:

the actual value, or the default value, or None

Example:

message = engine.get('bot.on_start')

This function is safe on multiprocessing and multithreading.

get_bot(channel_id=None, **kwargs)[source]

Gets a bot by id

Parameters:channel_id (str) – The unique id of the target chat space
Returns:a bot instance, or None

This function receives the id of a chat space, and returns the related bot.

If no id is provided, then the underlying space is asked to provide with a default channel, as set in overall configuration.

Note: this function should not be called from multiple processes, because this would create one bot per process. Use the function engine.bond() for the creation of a new channel.

get_hook()[source]

Provides the hooking function to receive messages from Cisco Spark

hook(server=None)[source]

Connects this engine with back-end API

Parameters:server (Server) – web server to be used

This function adds a route to the provided server, and asks the back-end service to send messages there.

initialize_store(bot)[source]

Copies engine settings to the bot store

load_command(*args, **kwargs)[source]

Loads one commands for this bot

This function is a convenient proxy for the underlying shell.

load_commands(*args, **kwargs)[source]

Loads commands for this bot

This function is a convenient proxy for the underlying shell.

name

Retrieves the dynamic name of this bot

Returns:The value of bot.name key in current context
Return type:str
on_build(bot)[source]

Extends the building of a new bot instance

Parameters:bot (ShellBot) – a new bot instance

Provide your own implementation in a sub-class where required.

Example:

on_build(self, bot):
    bot.secondary_machine = Input(...)
on_enter(join)[source]

Bot has been invited to a chat space

Parameters:join (Join) – The join event received from the chat space

Provide your own implementation in a sub-class where required.

Example:

on_enter(self, join):
    mailer.post(u"Invited to {}".format(join.space_title))
on_exit(leave)[source]

Bot has been kicked off from a chat space

Parameters:leave (Leave) – The leave event received from the chat space

Provide your own implementation in a sub-class where required.

Example:

on_exit(self, leave):
    mailer.post(u"Kicked off from {}".format(leave.space_title))
on_start()[source]

Does additional stuff when the engine is started

Provide your own implementation in a sub-class where required.

on_stop()[source]

Does additional stuff when the engine is stopped

Provide your own implementation in a sub-class where required.

Note that some processes may have been killed at the moment of this function call. This is likely to happen when end-user hits Ctl-C on the keyboard for example.

register(event, instance)[source]

Registers an object to process an event

Parameters:
  • event (str) – label, such as ‘start’ or ‘bond’
  • instance (object) – an object that will handle the event

This function is used to propagate events to any module that may need it via callbacks.

On each event, the engine will look for a related member function in the target instance and call it. For example for the event ‘start’ it will look for the member function ‘on_start’, etc.

Following standard events can be registered:

  • ‘bond’ - when the bot has connected to a chat channel
  • ‘dispose’ - when resources, including chat space, will be destroyed
  • ‘start’ - when the engine is started
  • ‘stop’ - when the engine is stopped
  • ‘join’ - when a person is joining a space
  • ‘leave’ - when a person is leaving a space

Example:

def on_init(self):
    self.engine.register('bond', self)  # call self.on_bond()
    self.engine.register('dispose', self) # call self.on_dispose()

If the function is called with an unknown label, then a new list of registered callbacks will be created for this event. Therefore the engine can be used for the dispatching of any custom event.

Example:

self.engine.register('input', processor)  # for processor.on_input()
...
received = 'a line of text'
self.engine.dispatch('input', received)

Registration uses weakref so that it affords the unattended deletion of registered objects.

run(server=None)[source]

Runs the engine

Parameters:server (Server) – a web server

If a server is provided, it is ran in the background. A server could also have been provided during initialisation, or loaded during configuration check.

If no server instance is available, a loop is started to fetch messages in the background.

In both cases, this function does not return, except on interrupt.

set(key, value)[source]

Changes the value of one configuration key

Parameters:
  • key (str) – name of the value
  • value (any serializable type is accepted) – new value

Example:

engine.set('bot.on_start', 'hello world')

This function is safe on multiprocessing and multithreading.

start()[source]

Starts the engine

start_processes()[source]

Starts the engine processes

This function starts a separate process for each main component of the architecture: listener, speaker, etc.

stop()[source]

Stops the engine

This function changes in the context a specific key that is monitored by bot components.

version

Retrieves the version of this bot

Returns:The value of bot.version key in current context
Return type:str
class shellbot.Listener(engine=None, filter=None)[source]

Bases: multiprocessing.process.Process

Handles messages received from chat space

DEFER_DURATION = 2.0
EMPTY_DELAY = 0.005
FRESH_DURATION = 0.5
idle()[source]

Finds something smart to do

on_inbound(received)[source]

Another event has been received

Parameters:received (Event or derivative) – the event received

Received information is transmitted to registered callbacks on the inbound at the engine level.

on_join(received)[source]

A person, or the bot, has joined a space

Parameters:received (Join) – the event received

Received information is transmitted to registered callbacks on the join at the engine level.

In the special case where the bot itself is joining a channel by invitation, then the event enter is dispatched instead.

on_leave(received)[source]

A person, or the bot, has left a space

Parameters:received (Leave) – the event received

Received information is transmitted to registered callbacks on the leave at the engine level.

In the special case where the bot itself has been kicked off from a channel, then the event exit is dispatched instead.

on_message(received)[source]

A message has been received

Parameters:received (Message) – the message received

Received information is transmitted to registered callbacks on the message event at the engine level.

When a message is directed to the bot it is submitted directly to the shell. This is handled as a command, that can be executed immediately, or pushed to the inbox and processed by the worker when possible.

All other input is thrown away, except if there is some downwards listeners. In that situation the input is pushed to a queue so that some process can pick it up and process it.

The protocol for downwards listeners works like this:

  • Check the bot.fan queue frequently
  • On each check, update the string fan.<channel_id> in the context with the value of time.time(). This will say that you are around.

The value of fan.<channel_id> is checked on every message that is not for the bot itself. If this is fresh enough, then data is put to the bot.fan queue. Else message is just thrown away.

process(item)[source]

Processes items received from the chat space

Parameters:item (dict or json-encoded string) – the item received

This function dispatches items based on their type. The type is a key of the provided dict.

Following types are handled:

  • message – This is a textual message, maybe with a file attached. The message is given to the on_message() function.
  • join – This is when a person or the bot joins a space. The function on_join() is called, providing details on the person or the bot who joined
  • leave – This is when a person or the bot leaves a space. The function on_leave() is called with details on the leaving person or bot.
  • load_bot – This is a special event to load the cache in the process that is running the listener. The identifier of the channel to load is provided as well.
  • on any other case, the function on_inbound() is called.
run()[source]

Continuously receives updates

This function is looping on items received from the queue, and is handling them one by one in the background.

Processing should be handled in a separate background process, like in the following example:

listener = Listener(engine=my_engine)
process = listener.start()

The recommended way for stopping the process is to change the parameter general.switch in the context. For example:

engine.set('general.switch', 'off')

Alternatively, the loop is also broken when a poison pill is pushed to the queue. For example:

engine.ears.put(None)
class shellbot.MachineFactory(module=None, name=None, **kwargs)[source]

Bases: object

Provides new state machines

In simple situations, you can rely on standard machines, and provide any parameters by these. For example:

factory = MachineFactory(module='shellbot.machines.input'
                         question="What's Up, Doc?")

...

machine = factory.get_machine()

When you provide different state machines for direct channels and for group channels, overlay member functions as in this example:

class GreatMachineForDirectChannel(Machine):
    ...

class MachineOnlyForGroup(Machine):
    ...

class MyFactory(MachineFactory):

    def get_machine_for_direct_channel(self, bot):
        return GreatMachineForDirectChannel( ... )

    def get_machine_for_group_channel(self, bot):
        return MachineOnlyForGroup( ... )
get_default_machine(bot)[source]

Gets a new state machine

Parameters:bot (ShellBot) – The bot associated with this state machine

Example:

my_machine = factory.get_default_machine(bot=my_bot)
my_machine.start()

This function can be overlaid in a subclass for adapting the production of state machines for default case.

get_machine(bot=None)[source]

Gets a new state machine

Parameters:bot (ShellBot) – The bot associated with this state machine

Example:

my_machine = factory.get_machine(bot=my_bot)
my_machine.start()

This function detects the kind of channel that is associated with this bot, and provides a suitable state machine.

get_machine_for_direct_channel(bot)[source]

Gets a new state machine for a direct channel

Parameters:bot (ShellBot) – The bot associated with this state machine

Example:

my_machine = factory.get_machine_for_direct_channel(bot=my_bot)
my_machine.start()

This function can be overlaid in a subclass for adapting the production of state machines for direct channels.

get_machine_for_group_channel(bot)[source]

Gets a new state machine for a group channel

Parameters:bot (ShellBot) – The bot associated with this state machine

Example:

my_machine = factory.get_machine_for_group_channel(bot=my_bot)
my_machine.start()

This function can be overlaid in a subclass for adapting the production of state machines for group channels.

get_machine_from_class(bot, module, name, **kwargs)[source]

Gets a new state machine from a module

Parameters:
  • bot (ShellBot) – The bot associated with this state machine
  • module (str) – The python module to import
  • name (str) – The class name to instantiate (optional)

Example:

machine = factory.get_machine_from_class(my_bot,
                                         'shellbot.machines.base',
                                         'Machine')
class shellbot.Notifier(context=None, **kwargs)[source]

Bases: shellbot.routes.base.Route

Notifies a queue on web request

>>>queue = Queue() >>>route = Notifier(route=’/notify’, queue=queue, notification=’hello’)

When the route is requested over the web, the notification is pushed to the queue.

>>>queue.get() ‘hello’

Notification is triggered on GET, POST, PUT and DELETE verbs.

delete()[source]
get(**kwargs)[source]
notification = None
notify()[source]
post()[source]
put()[source]
queue = <shellbot.routes.notifier.NoQueue object>
route = '/notify'
class shellbot.Publisher(context)[source]

Bases: multiprocessing.process.Process

Publishes asynchronous messages

For example, from a group channel, you may send instructions to every direct channels:

# get a publisher
publisher = bus.publish()

# send instruction to direct channels
publisher.put(bot.direct_channels, instruction)

From within a direct channel, you may reflect your state to observers:

# get a publisher
publish = bus.publish()

# share new state
publisher.put(bot.id, bit_of_information_here)
DEFER_DURATION = 0.3
EMPTY_DELAY = 0.005
process(item)[source]

Processes items received from the queue

Parameters:item (str) – the item received

Note that the item should result from serialization of (channel, message) tuple done previously.

put(channels, message)[source]

Broadcasts a message

Parameters:
  • channels (str or list of str) – one or multiple channels
  • message (dict or other json-serializable object) – the message to send

Example:

message = { ... }
publisher.put(bot.id, message)

This function actually put the message in a global queue that is handled asynchronously. Therefore, when the function returns there is no guarantee that message has been transmitted nor received.

run()[source]

Continuously broadcasts messages

This function is looping on items received from the queue, and is handling them one by one in the background.

Processing should be handled in a separate background process, like in the following example:

publisher = Publisher(address)
process = publisher.start()

The recommended way for stopping the process is to change the parameter general.switch in the context. For example:

engine.set('general.switch', 'off')

Alternatively, the loop is also broken when a poison pill is pushed to the queue. For example:

publisher.fan.put(None)
class shellbot.Route(context=None, **kwargs)[source]

Bases: object

Implements one route

delete()[source]
get(**kwargs)[source]
post()[source]
put()[source]
route = None
class shellbot.Server(context=None, httpd=None, route=None, routes=None, check=False)[source]

Bases: bottle.Bottle

Serves web requests

add_route(item)[source]

Adds one web route

Parameters:route (Route) – one additional route
add_routes(items)[source]

Adds web routes

Parameters:routes (list of routes) – a list of additional routes
configure(settings={})[source]

Checks settings of the server

Parameters:settings (dict) – a dictionary with some statements for this instance

This function reads key server and below, and update the context accordingly:

>>>server.configure({'server': {
       'binding': '10.4.2.5',
       'port': 5000,
       'debug': True,
       }})

This can also be written in a more compact form:

>>>server.configure({'server.port': 5000})
route(route)[source]

Gets one route by path

Returns:the related route, or None
routes

Lists all routes

Returns:a list of routes, or []

Example:

>>>server.get_routes()
['/hello', '/world']
run()[source]

Serves requests

class shellbot.Shell(engine)[source]

Bases: object

Parses input and reacts accordingly

command(keyword)[source]

Get one command

Parameters:keyword (str) – the keyword for this command
Returns:the instance for this command
Return type:command or None

Lists available commands and related usage information.

Example:

>>>print(shell.command('help').information_message)
commands

Lists available commands

Returns:a list of verbs
Return type:list of str

This function provides with a dynamic inventory of all capabilities of this shell.

Example:

>>>print(shell.commands)
['*default', '*empty', 'help']
configure(settings={})[source]

Checks settings of the shell

Parameters:settings (dict) – a dictionary with some statements for this instance

This function reads key shell and below, and update the context accordingly:

>>>shell.configure({'shell': {
       'commands':
          ['examples.exception.state', 'examples.exception.next']
       }})

This can also be written in a more compact form:

>>>shell.configure({'shell.commands':
       ['examples.exception.state', 'examples.exception.next']
       })

Note that this function does preserve commands that could have been loaded previously.

do(line, received=None)[source]

Handles one line of text

Parameters:
  • line (str) – a line of text to parse and to handle
  • received (Message) – the message that contains the command

This function uses the first token as a verb, and looks for a command of the same name in the shell.

If the command does not exist, the command *default is used instead. Default behavior is implemented in shellbot.commands.default yet you can load a different command for customization.

If an empty line is provided, the command *empty is triggered. Default implementation is provided in shellbot.commands.empty.

When a file has been uploaded, the information is given to the command that is executed. If no message is provided with the file, the command *upload is triggered instad of *empty. Default implementation is provided in shellbot.commands.upload.

Following parameters are used for the execution of a command:

  • bot - A bot instance is retrieved from the channel id mentioned in received, and provided to the command.
  • arguments - This is a string that contains everything after the command verb. When hello How are you doing? is submitted to the shell, hello is the verb, and How are you doing? are the arguments. This is the regular case. If there is no command hello then the command *default is used instead, and arguments provided are the full line hello How are you doing?.
  • attachment - When a file has been uploaded, this attribute provides its external name, e.g., picture024.png. This can be used in the executed command, if you keep in mind that the same name can be used multiple times in a conversation.
  • url - When a file has been uploaded, this is the handle by which actual content can be retrieved. Usually, ask the underlying space to get a local copy of the document.
load_command(command)[source]

Loads one command for this shell

Parameters:command (str or command) – A command to load

If a string is provided, it should reference a python module that can be used as a command. Check base.py in shellbot.commands for a clear view of what it means to be a vaid command for this shell.

Example:

>>>shell.load_command('shellbot.commands.help')

If an object is provided, it should duck type the command defined in base.py in shellbot.commands.

Example:

>>>from shellbot.commands.version import Version
>>>command = Version()
>>>shell.load_command(command)
load_commands(commands=[])[source]

Loads commands for this shell

Parameters:commands (List of labels or list of commands) – A list of commands to load

Example:

>>>commands = ['shellbot.commands.help']
>>>shell.load_commands(commands)

Each label should reference a python module that can be used as a command. Check base.py in shellbot.commands for a clear view of what it means to be a vaid command for this shell.

If objects are provided, they should duck type the command defined in base.py in shellbot.commands.

Example:

>>>from shellbot.commands.version import Version
>>>version = Version()
>>>from shellbot.commands.help import Help
>>>help = Help()
>>>shell.load_commands([version, help])
load_default_commands()[source]

Loads default commands for this shell

Example:

>>>shell.load_default_commands()
class shellbot.ShellBot(engine, channel_id=None, space=None, store=None, fan=None, machine=None)[source]

Bases: object

Manages interactions with one space, one store, one state machine

A bot consists of multiple components devoted to one chat channel: - a space - a store - a state machine - ... other optional components that may prove useful

It is designated by a unique id, that is also the unique id of the channel itself.

A bot relies on an underlying engine instance for actual access to the infrastructure, including configuration settings.

The life cycle of a bot can be described as follows:

  1. A bot is commonly created from the engine, or directly:

    bot = ShellBot(engine, channel_id='123')
    
  2. The space is connected to some back-end API:

    space.connect()
    
  3. Multiple channels can be handled by a single space:

    channel = space.create(title)
    
    channel = space.get_by_title(title)
    channel = space.get_by_id(id)
    
    channel.title = 'A new title'
    space.update(channel)
    
    space.delete(id)
    

    Channels feature common attributes, yet can be extended to convey specificities of some platforms.

  4. Messages can be posted:

    space.post_message(id, 'Hello, World!')
    
  5. The interface allows for the addition or removal of channel participants:

    space.add_participants(id, persons)
    space.add_participant(id, person, is_moderator)
    space.remove_participants(id, persons)
    space.remove_participant(id, person)
    
add_participant(person, is_moderator=False)[source]

Adds one participant

Parameters:person (str) – e-mail addresses of person to add

The underlying platform may, or not, take the optional parameter is_moderator into account. The default bahaviour is to discard it, as if the parameter had the value False.

add_participants(persons=[])[source]

Adds multiple participants

Parameters:persons (list of str) – e-mail addresses of persons to add
append(key, item)[source]

Appends an item to a list

Parameters:
  • key (str) – name of the list
  • item (any serializable type is accepted) – a new item to append

Example:

>>>bot.append('names', 'Alice')
>>>bot.append('names', 'Bob')
>>>bot.recall('names')
['Alice', 'Bob']
bond()[source]

Bonds to a channel

This function is called either after the creation of a new channel, or when the bot has been invited to an existing channel. In such situations the banner should be displayed as well.

There are also situations where the engine has been completely restarted. The bot bonds to a channel where it has been before. In that case the banner should be avoided.

dispose(**kwargs)[source]

Disposes all resources

This function deletes the underlying channel in the cloud and resets this instance. It is useful to restart a clean environment.

>>>bot.bond(title=”Working Space”) ... >>>bot.dispose()

After a call to this function, bond() has to be invoked to return to normal mode of operation.

forget(key=None)[source]

Forgets a value or all values

Parameters:key (str) – name of the value to forget, or None

To clear only one value, provides the name of it. For example:

bot.forget('variable_123')

To clear all values in the store, just call the function without a value. For example:

bot.forget()
id

Gets unique id of the related chat channel

Returns:the id of the underlying channel, or None
is_ready

Checks if this bot is ready for interactions

Returns:True or False
on_bond()[source]

Adds processing to channel bonding

This function should be changed in sub-class, where necessary.

Example:

def on_bond(self):
    do_something_important_on_bond()
on_enter()[source]

Enters a channel

on_exit()[source]

Exits a channel

on_init()[source]

Adds to bot initialization

It can be overlaid in subclass, where needed

on_reset()[source]

Adds processing to space reset

This function should be expanded in sub-class, where necessary.

Example:

def on_reset(self):
    self._last_message_id = 0
recall(key, default=None)[source]

Recalls a value

Parameters:
  • key (str) – name of the value
  • default (any serializable type is accepted) – default value
Returns:

the actual value, or the default value, or None

Example:

value = bot.recall('variable_123')
remember(key, value)[source]

Remembers a value

Parameters:
  • key (str) – name of the value
  • value (any serializable type is accepted) – new value

This functions stores or updates a value in the back-end storage system.

Example:

bot.remember('variable_123', 'George')
remove_participant(person)[source]

Removes one participant

Parameters:person (str) – e-mail addresses of person to add
remove_participants(persons=[])[source]

Removes multiple participants

Parameters:persons (list of str) – e-mail addresses of persons to remove
reset()[source]

Resets a space

After a call to this function, bond() has to be invoked to return to normal mode of operation.

say(text=None, content=None, file=None, person=None)[source]

Sends a message to the chat space

Parameters:
  • text (str or None) – Plain text message
  • content (str or None) – Rich content such as Markdown or HTML
  • file (str or None) – path or URL to a file to attach
  • person (str) – for direct message to someone
say_banner()[source]

Sends banner to the channel

This function uses following settings from the context:

  • bot.banner.text or bot.on_enter - a textual message
  • bot.banner.content - some rich content, e.g., Markdown or HTML
  • bot.banner.file - a document to be uploaded

The quickest setup is to change bot.on_enter in settings, or the environment variable $BOT_ON_ENTER.

Example:

os.environ['BOT_ON_ENTER'] = 'You can now chat with Batman'
engine.configure()

Then there are situtations where you want a lot more flexibility, and rely on a smart banner. For example you could do the following:

settings = {
    'bot': {
        'banner': {
            'text': u"Type '@{} help' for more information",
            'content': u"Type ``@{} help`` for more information",
            'file': "http://on.line.doc/guide.pdf"
        }
    }
}

engine.configure(settings)

When bonding to a channel, the bot will send an update similar to the following one, with a nice looking message and image:

Type '@Shelly help' for more information

Default settings for the banner rely on the environment, so it is easy to inject strings from the outside. Use following variables:

  • $BOT_BANNER_TEXT or $BOT.ON_ENTER - the textual message
  • $BOT_BANNER_CONTENT - some rich content, e.g., Markdown or HTML
  • $BOT_BANNER_FILE - a document to be uploaded
title

Gets title of the related chat channel

Returns:the title of the underlying channel, or None
update(key, label, item)[source]

Updates a dict

Parameters:
  • key (str) – name of the dict
  • label (str) – named entry in the dict
  • item (any serializable type is accepted) – new value of this entry

Example:

>>>bot.update('input', 'PO Number', '1234A')
>>>bot.update('input', 'description', 'some description')
>>>bot.recall('input')
{'PO Number': '1234A',
 'description': 'some description'}
class shellbot.SpaceFactory[source]

Bases: object

Builds a space from configuration

Example:

my_context = Context(settings={
    'space': {
        'type': "spark",
        'room': 'My preferred room',
        'participants':
            ['alan.droit@azerty.org', 'bob.nard@support.tv'],
        'team': 'Anchor team',
        'token': 'hkNWEtMJNkODk3ZDZLOGQ0OVGlZWU1NmYtyY',
        'fuzzy_token': '$MY_FUZZY_SPARK_TOKEN',
    }
})

space = SpaceFactory.build(context=my_context)
classmethod build(context, **kwargs)[source]

Builds an instance based on provided configuration

Parameters:context (Context) – configuration to be used
Returns:a ready-to-use space
Return type:Space

This function “senses” for a type in the context itself, then provides with an instantiated object of this type.

A ValueError is raised when no type can be identified.

classmethod get(type, **kwargs)[source]

Loads a space by type

Parameters:type (str) – the required space
Returns:a space instance

This function seeks for a suitable space class in the library, and returns an instance of it.

Example:

space = SpaceFactory.get('spark', ex_token='123')

A ValueError is raised if the type is unknown.

classmethod sense(context)[source]

Detects type from configuration

Parameters:context (Context) – configuration to be analyzed
Returns:a guessed type
Return type:str

Example:

type = SpaceFactory.sense(context)

A ValueError is raised if no type could be identified.

types = {'spark': <class 'shellbot.spaces.ciscospark.SparkSpace'>, 'local': <class 'shellbot.spaces.local.LocalSpace'>, 'space': <class 'shellbot.spaces.base.Space'>}
class shellbot.Speaker(engine=None)[source]

Bases: multiprocessing.process.Process

Sends updates to a business messaging space

EMPTY_DELAY = 0.005
process(item)[source]

Sends one update to a business messaging space

Parameters:item (str or object) – the update to be transmitted
run()[source]

Continuously send updates

This function is looping on items received from the queue, and is handling them one by one in the background.

Processing should be handled in a separate background process, like in the following example:

speaker = Speaker(engine=my_engine)
speaker.start()

The recommended way for stopping the process is to change the parameter general.switch in the context. For example:

engine.set('general.switch', 'off')

Alternatively, the loop is also broken when an exception is pushed to the queue. For example:

engine.mouth.put(None)
class shellbot.Subscriber(context, channels)[source]

Bases: object

Subscribes to asynchronous messages

For example, from a group channel, you may subscribe from direct channels of all participants:

# subscribe from all direct channels related to this group channel
subscriber = bus.subscribe(bot.direct_channels)

# get messages from direct channels
while True:
    message = subscriber.get()
    ...

From within a direct channel, you may receive instructions sent by the group channel:

# subscribe for messages sent to me
subscriber = bus.subscribe(bot.id)

# get and process instructions one at a time
while True:
    instruction = subscriber.get()
    ...
get(block=False)[source]

Gets next message

Returns:dict or other serializable message or None

This function returns next message that has been made available, or None if no message has arrived yet.

Example:

message = subscriber.get()  # immedaite return
if message:
    ...

Change the parameter block if you prefer to wait until next message arrives.

Example:

message = subscriber.get(block=True)  # wait until available

Note that this function does not preserve the enveloppe of the message. In other terms, the channel used for the communication is lost in translation. Therefore the need to put within messages all information that may be relevant for the receiver.

class shellbot.Wrapper(context=None, **kwargs)[source]

Bases: shellbot.routes.base.Route

Calls a function on web request

When the route is requested over the web, the wrapped function is called.

Example:

def my_callable(**kwargs):
    ...

route = Wrapper(callable=my_callable, route='/hook')

Wrapping is triggered on GET, POST, PUT and DELETE verbs.

callable = None
delete()[source]
get(**kwargs)[source]
post()[source]
put()[source]
route = None