shellbot package¶
Subpackages¶
- shellbot.commands package
- Submodules
- shellbot.commands.audit module
- shellbot.commands.base module
- shellbot.commands.close module
- shellbot.commands.default module
- shellbot.commands.echo module
- shellbot.commands.empty module
- shellbot.commands.help module
- shellbot.commands.input module
- shellbot.commands.noop module
- shellbot.commands.sleep module
- shellbot.commands.start module
- shellbot.commands.step module
- shellbot.commands.update module
- shellbot.commands.upload module
- shellbot.commands.version module
- Module contents
- Submodules
- shellbot.lists package
- shellbot.machines package
- shellbot.routes package
- shellbot.spaces package
- shellbot.stores package
- shellbot.updaters package
Submodules¶
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 istcp://*: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 commandmagic
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 withbot.say()
, or access data attached to the bot inbot.store
. The engine and all global items can be access withbot.engine
.arguments
- This is a string that contains everything after the command verb. Whenhello How are you doing?
is submitted to the shell,hello
is the verb, andHow are you doing?
are the arguments. This is the regular case. If there is no commandhello
then the command*default
is used instead, and arguments provided are the full linehello 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¶
-
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 aValueError
can be raised as well in some situations.
-
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
-
is_empty
¶ Does the context store something?
Returns: True if there at least one value, False otherwise
-
-
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
andself.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
-
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.
-
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.
-
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 contextReturn 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_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 contextReturn 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¶
-
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 oftime.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 thebot.fan
queue. Else message is just thrown away.- Check the
-
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 theon_message()
function.join
– This is when a person or the bot joins a space. The functionon_join()
is called, providing details on the person or the bot who joinedleave
– This is when a person or the bot leaves a space. The functionon_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.
-
notification
= None¶
-
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
-
route
= None¶
-
-
class
shellbot.
Server
(context=None, httpd=None, route=None, routes=None, check=False)[source]¶ Bases:
bottle.Bottle
Serves web requests
-
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})
-
routes
¶ Lists all routes
Returns: a list of routes, or [] Example:
>>>server.get_routes() ['/hello', '/world']
-
-
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 inshellbot.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 inshellbot.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 inshellbot.commands.upload
.Following parameters are used for the execution of a command:
bot
- A bot instance is retrieved from the channel id mentioned inreceived
, and provided to the command.arguments
- This is a string that contains everything after the command verb. Whenhello How are you doing?
is submitted to the shell,hello
is the verb, andHow are you doing?
are the arguments. This is the regular case. If there is no commandhello
then the command*default
is used instead, and arguments provided are the full linehello 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
inshellbot.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
inshellbot.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
inshellbot.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
inshellbot.commands
.Example:
>>>from shellbot.commands.version import Version >>>version = Version() >>>from shellbot.commands.help import Help >>>help = Help() >>>shell.load_commands([version, help])
-
-
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:
A bot is commonly created from the engine, or directly:
bot = ShellBot(engine, channel_id='123')
The space is connected to some back-end API:
space.connect()
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.
Messages can be posted:
space.post_message(id, 'Hello, World!')
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 valueFalse
.
-
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_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
Sends banner to the channel
This function uses following settings from the context:
bot.banner.text
orbot.on_enter
- a textual messagebot.banner.content
- some rich content, e.g., Markdown or HTMLbot.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'>}¶
-
classmethod
-
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¶
-
route
= None¶
-