Welcome to SILF Experiment API’s documentation!¶
Contents:
Api of the device¶
device
Package¶
It packages api for single device.
_const
Module¶
-
silf.backend.commons.device._const.
DEVICE_STATES
= ('off', 'stand-by', 'ready', 'running', 'cleaned_up')¶ Touple containing all allowable device states.
_device
Module¶
This is a api for a device.
-
class
silf.backend.commons.device._device.
Device
(device_id='default', config_file=None)¶ Bases:
object
Defines plugin for a particular device in the experiment.
All methods are blocking, that is should block the current thread until finished.
Note
Instances of this object don’t need to use any synchronization, they will always be called from single thread, This instance will be constructed used and destroyed on single process.
Warning
All methods should exit relatively fast.
Warning
Both method parameters and responses should be pickleable, these will be travelling between process boundaries.
-
MAIN_LOOP_INTERVAL
= 0.1¶ Interval between invocations of main loop. Represents number of seconds as
float
.
-
apply_settings
(settings)¶ Applies some set of settings to this device.
Parameters: settings (dict) – Settings to be applied, it is already validated by the
IDeviceManager
.Raises: - InvalidStateException – If device is in invalid state (that is
not
STAND_BY
orRUNNING
) - DeviceRuntimeException – If any exception occours
Returns: None
Return type: - InvalidStateException – If device is in invalid state (that is
not
-
logger
¶ Returns: Logger instance attached to this device. Utility method, you may use whatsoever logger you want
-
loop_iteration
()¶ Perform an iteration of main experiment loop. Should terminate quickly,
Raises: DeviceRuntimeException – If any exception occours Returns: If returned value is False or None next iteration of this method will be scheduled after MAIN_LOOP_INTERVAL
seconds, it result is true it will be sheduled earlier (after at most one command from controller was performed);
-
perform_diagnostics
(diagnostics_level='short')¶ Performs diagnostics on the device. Can be ran if this device is
OFF
. orSTAND_BY
.Parameters: diagnostics_level (str) – Whether diagnostisc should be thororough or not, must be in
DEVICE_STATES
Raises: - DiagnosticsException – If there is error in diagnostics.
- InvalidStateException – If device is in invalid state (that is not
OFF
) - DeviceRuntimeException – If any exception occours.
-
pop_results
()¶ This method returns list of recently acquired points, it should clear this list so next calls won’t return the same result points.
Raises: - InvalidStateException – If device is in invalid state (that is
not
RUNNING
- InvalidStateException – If device is in invalid state (that is
not
READY
- DeviceRuntimeException – If any exception occours
Returns: Returns results for (possibly) many points.
Return type: list
(or any other iterable) of dict.- InvalidStateException – If device is in invalid state (that is
not
-
post_power_up_diagnostics
(diagnostics_level='short')¶
-
power_down
()¶ Call to this method moves this class to
OFF
state.Raises: - InvalidStateException – If device is in invalid state (that is
not
STAND_BY
- DeviceRuntimeException – If any exception occours
- InvalidStateException – If device is in invalid state (that is
not
-
power_up
()¶ Call to this method enables consecutive
apply_settings()
.It also should power up the device (if this action makes any sense for this particular device see also: Power management).
Raises: - InvalidStateException – If device is in invalid state (that is
not
OFF
- DeviceRuntimeException – If any exception occours
- InvalidStateException – If device is in invalid state (that is
not
-
pre_power_up_diagnostics
(diagnostics_level='short')¶
-
start
()¶ Starts the acquisituon on the device (that is starts the measurements).
Blocks until this device is stared.
Raises: InvalidStateException – If device is in invalid state (that is not READY
Returns: None Return type: None
-
state
= None¶ State of this device should be in
DEVICE_STATES
, full state chart is avilable in: Device state chart.
-
stop
()¶ Stops the acquisituon on the device (that is stops the measurements).
Blocks until this device is stared.
Raises: - InvalidStateException – If device is in invalid state (that is not
RUNNING
) - DeviceRuntimeException – If any exception occours
Returns: None
Return type: - InvalidStateException – If device is in invalid state (that is not
-
tearDown
()¶
-
tear_down
()¶ Called when current process is being disabled.
This method can be called multiple times.
Note
do not override this method, override
_tear_down()
.Raises: DeviceRuntimeException – If any exception occours Returns: None Return type: None
-
API fine print¶
Device state chart
Power management¶
Note
If your device does not need to power itself up or down, please just
ignore power_up()
and power_down()
methods.
Devices should be powered up when we start call power_up()
, but
needn`t do so, they must be powered up when after we exit from
start()
. So there are three methods in which devices should
power up:
power_up()
, this method is called relatively early in during the experiment, and should allow plenty of time to initialize everyhingapply_settings()
, use this method if your device powers up quickly.start()
, if your device is volatile and you want to minimize the time it is powered up use this.
You can power down the device when following methods are called:
Threading considerations¶
Devices are accessed from single thread. All methods sould exit relatively fast, you should not use loops that are infinite (or can be infinite — for example if hardware will not respond).
Change device state¶
It is quite important to change state of your device after appropriate method calls.
How to test the devices according to the API¶
There are two ways in which you can test it: start ipython interpreter create device and manage it by hand:
Use DeviceWorkerWrapper
from interpreter¶
Import classes:
>>> from silf.backend.commons_test.device.test_device import *
>>> from silf.backend.commons.device_manager import start_worker_interactive
Start the device:
>>> work = start_worker_interactive('foo', MockDevice,
... configure_logging=False, auto_pull_results=False)
>>> work.state
'off'
>>> work.power_up()
UUID(...)
Let’s setup the device:
>>> work.apply_settings({"foo": 3, "bar": 2})
UUID(...)
>>> work.start()
UUID(...)
This device will perform own acquisition in separete process, well wait for results to be acquired:
>>> time.sleep(1.2)
First pop_results()
will return stale data, and schedule acquisition
of new data:
>>> work.state
'running'
>>> work.pop_results() == []
True
Wait for results to get processed (will be faster on server!)
>>> time.sleep(0.5)
>>> results = work.pop_results()
>>> results == [{'foo_result': 3, 'bar_result': 2}]
True
Kill it without waiting;
>>> work.kill(wait_time=None)
Auto result pooling¶
You can configure this to auto poll for results:
>>> work = start_worker_interactive('foo', MockDevice,
... configure_logging=False, auto_pull_results=True)
>>> work.power_up()
UUID(...)
As in last test:
>>> work.apply_settings({"foo": 3, "bar": 2})
UUID(...)
>>> work.start()
UUID(...)
Wait for results to be gathered
>>> time.sleep(2)
Notice that results are avilable at once (no need to query)
>>> results = work.pop_results()
>>> results == [{'foo_result': 3, 'bar_result': 2}]
True
>>> work.kill(wait_time=None)
>>> results == [{'foo_result': 3, 'bar_result': 2}]
True
>>> work.kill(wait_time=None)
Device Api examples¶
This is pseudocode
Engine driver¶
This imaginary device implements an engine. This is not actual experiment code, sxperiment will not be doing any waiting!
engine = ImaginaryDriver()
assert engine.state == 'off'
engine.power_up() # Powers up the device
assert engine.state == 'stand-by'
engine.apply_settings({"position" : 512})
assert engine.state == 'ready'
engine.start() # Start the engine
assert engine.state == 'acquiring'
# Silnik ruszył i teraz jest w stanie `acquiring`
# .. wait
while engine.state != 'ready':
time.sleep(0.1)
# Silnik doszedł do końca i jest w stanie `ready`
# Następny pukt
engine.apply_settings({"position" : 1024})
engine.start() # Start the engine
Engine driver¶
Imaginary voltimeter
volt = ImaginaryVoltimeter()
assert volt.state == 'off'
volt.power_up() # Powers up the device
assert volt.state == 'stand-by'
volt.apply_settings({'range' : 15})
assert volt.state == 'ready'
volt.start() # Start the volt
assert volt.state == 'acquiring'
while volt.state != 'ready':
time.sleep(0.1)
assert volt.pop_results() == [{'voltage' : 243.11}]
Engine and voltimeter connected¶
It works that so voltimeter measures single point after position is set by the engine.
engine = ImaginaryDriver()
volt = ImaginaryVoltimeter()
engine.power_up() # Powers up the device
volt.power_up() # Powers up the device
engine.apply_settings({"position" : 512})
engine.start();
while engine.state != 'ready':
time.sleep(0.1)
volt.apply_settings({'range' : 15})
volt.start() # Start the volt
while volt.state != 'ready':
time.sleep(0.1)
assert volt.pop_results() == [{'voltage' : 243.11}]
engine.apply_settings({"position" : 1024})
engine.start();
while engine.state != 'ready':
time.sleep(0.1)
while volt.state != 'ready':
time.sleep(0.1)
assert volt.pop_results() == [{'voltage' : 123.123}]
Howto create experiment (in four easy steps!)¶
Create device drivers for all needed devices¶
See device_api.rst
Create device managers for all the devices¶
Single mode devices¶
Device managers are wrappers around devices that have following repsonsibilities:
- Provide metadata about the device it holds
- Validate input
- Provide any neccessary per-experiment customisation
For example you need to create a DeviceManager
for HantekPPS2116ADevice
that can be found in this repository:
https://bitbucket.org/silf/silf-backend-driver-power-hantek. This is a simple
programmable power source. This manager will also allow user to directly
set temperature of light-bulb that is beinbg controlled by this power source,
so we will need to convert both user settinga as well as experiment responses.
We’ll start with the following:
class HantekManager(SingleModeDeviceManager):
DEVICE_ID = "hantek"
DEVICE_CONSTRUCTOR = HantekPPS2116ADevice
CONTROLS = ControlSuite(
NumberControl("temperature", "Temperatura włókna żarówki", default_value=2700, min_value=300, max_value=2800)
)
OUTPUT_FIELDS = OutputFieldSuite(
blackbody_temperature = OutputField(
"integer-indicator", "temperature", label="Aktualna temperatura włókna żarówki"
)
)
RESULT_CREATORS = [
ReturnLastElementResultCreator("temperature")
]
Following points are important:
DEVICE_ID
is programmer-readable unique for experiment name of the device, it is not visible to the end-user.DEVICE_CONSTRUCTOR
is a callable that creates the Device. Mostly it will be just a subtype ofDevice
. It should accept the same arguments as meth:Device.__init__()
CONTROLS
Controls define controls for user for this device. For more information about how controls work and how they are represented read: Input fields and :mod:`silf.backend.commons.- Result creators allows to customize how results are sent to user (there will be more about it in this document)
Converting settings¶
For now the problem is that device needs voltage and current, and student is
providing the temperature. To convert settings you need to override
silf.backend.commons.device_manager.DefaultDeviceManager._convert_settings_to_device_format()
.
class HantekManager(SingleModeDeviceManager):
...
def __get_hantek_settings_from_temperature(self, temperature):
return {'voltage' : ( 1.55155e-6 *temperature**2 - 0.00045359 ) } #Temperature [K] approx. conv. to volt.
def _convert_settings_to_device_format(self, converted_settings):
return self.__get_hantek_settings_from_temperature(converted_settings['temperature'])
Converting results¶
Also resuls need to be converted:
class HantekManager(SingleModeDeviceManager):
...
def __get_temperature_from_hantek_results(self, voltage, current):
return {"temperature" : (math.sqrt( 684732 * voltage) + 86.5226)} #Voltage approx. conv. to temp. K
def _convert_result(self, results):
return self.__get_temperature_from_hantek_results(
results['voltage'], results['current']
)
Results creators¶
Devices produce results asynchroneusly, that is while DeviceManager
was doing
something else Device
could procudce many results (each of these results
being a dictionary holding many values) or none at all. RESULT_CREATORS
are designed to convert that unknown number of results to something that
can be sent to users.
If experiment produced N result dictionaries, first we convert each of them using _convert_results then we fire result creators.
Create the experiment manager¶
Complicated experiment manager¶
class BlackbodyExperiment(EventExperimentManager):
LOOP_TIMEOUT = 1
DEVICE_MANAGERS = {
'hantek': HantekManager,
'strawberry': StrawberryManager,
'voltimeter': RigolDM3000UsbtmcManager
}
Configuration:
LOOP_TIMEOUT
- As everywhere — will controls wait after each iteration of main loop
DEVICE_MANAGERS
- Dictionary mapping name to
IDeviceManager
.
Logic of this experiment is as follows:
- For each point:
- Move detector to the desired position
- Set parameters for the light source
- Capture voltage
We need to implement this logic using EventExperimentManager
. In
this class each lifecycle method launches an event, rest of the logic may be
added via custom events.
All lifecycle events will be handled by default handlers, with the exception of ‘start’ which will invoke new logic.
These default handlers have following meaning
power_up
- Powers up all the devices
stop
- Stops all the devices
power_down
- Powers down all the devices
apply_settings
- Applies settings to all the devices
tick
- Calls
Device.loop_iteration()
pop_results
- If device is running gets the results from all the devices and sends them to client.
Here are custom handlers for this device:
start
- Clears internal state, starts engine driver and powersource, then and rises
next_point
event next_point
Moves engine to the next point and schedules
check_position
to be fired after one second. If this is the last point firesstop
event.def _on_next_point(self, event): try: self['strawberry'].move_to_next_series_point() # Moves the engine to the next series point self.schedule_event('check_position') # Schedule event for self.LOOP_TIMEOUT except OnFinalSeriesPoint: # This is raised if we are at last series point self.experiment_callback.send_series_done() # Send users that series is finished self.stop() # Stop all devices
check_position
If engine arrived on target position starts voltimeter and fires
check_voltage
if not schedulescheck_position
to be fired after one second.def _on_check_position(self, event): # This is how we detect that device finished working -- it should # switch it's state if self['strawberry'].device_state == DEVICE_STATES[READY]: # Purge all cached results self['voltimeter'].clear_current_results() # Start the voltimeter self['voltimeter'].start() # Schedule next event for immediate execution self.schedule_event('check_voltage', 0) else: # If engines are working schedule check position once more self.schedule_event('check_position')
check_voltage
If voltimeter finished the measurement fires
next_point
event if notcheck_voltage
to be fired after one second.Also if voltimeter has finished it sends next point to the user.
def _on_check_voltage(self, event): voltimeter = self['voltimeter'] # If volrimter has results let's send them! if voltimeter.has_result('voltage'): voltage = voltimeter.current_result_map['voltage'] wavelength = self['strawberry'].current_wavelength results = {'voltage': voltage, 'wavelength': wavelength} self.chart_generator.aggregate_results(results) chart_item = self.chart_generator.pop_results() self.experiment_callback.send_results(ResultSuite( **{self.chart_generator.result_name: chart_item} )) self.schedule_event('next_point', 0) else: # Wait for results self.schedule_event('check_voltage')
Then you need to hook events to this instance:
def initialize(self, experiment_callback):
super().initialize(experiment_callback)
# Load default events
self.install_default_event_managers_for('power_up')
self.install_default_event_managers_for('stop')
self.install_default_event_managers_for('power_down')
self.install_default_event_managers_for('apply_settings')
self.install_default_event_managers_for('tick')
self.install_default_event_managers_for('pop_results')
#Load additional events
self.register_listener('start', self._on_series_started)
self.register_listener('next_point', self._on_next_point)
self.register_listener('check_position', self._on_check_position)
self.register_listener('check_voltage', self._on_check_voltage)
# Clear results for chart
self.register_listener('stop', lambda evt: self.chart_generator.clear())
class BlackbodyExperiment(EventExperimentManager):
def initialize(self, experiment_callback):
super().initialize(experiment_callback)
self.install_default_event_managers_for('power_up')
self.install_default_event_managers_for('stop')
self.install_default_event_managers_for('power_down')
self.install_default_event_managers_for('apply_settings')
self.install_default_event_managers_for('tick')
self.install_default_event_managers_for('pop_results')
self.register_listener('start', self._on_series_started)
self.register_listener('next_point', self._on_next_point)
self.register_listener('check_position', self._on_check_position)
self.register_listener('check_voltage', self._on_check_voltage)
self.register_listener('stop', lambda evt: self.chart_generator.clear())
def _on_series_started(self, event):
self['hantek'].start() # Starts the power source
self['strawberry'].start() # Starts the engine
self._on_next_point(event)
def _on_next_point(self, event):
try:
self['strawberry'].move_to_next_series_point() # Moves the engine to the next series point
self.schedule_event('check_position')
except OnFinalSeriesPoint:
self.experiment_callback.send_series_done()
self.stop()
def _on_check_position(self, event):
if self['strawberry'].device_state == DEVICE_STATES[READY]:
self['voltimeter'].clear_current_results()
self['voltimeter'].start()
self.schedule_event('check_voltage', 0)
else:
self.schedule_event('check_position')
def _on_check_voltage(self, event):
voltimeter = self['voltimeter']
if voltimeter.has_result('voltage'):
voltage = voltimeter.current_result_map['voltage']
wavelength = self['strawberry'].current_wavelength
results = {'voltage': voltage, 'wavelength': wavelength}
self.chart_generator.aggregate_results(results)
chart_item = self.chart_generator.pop_results()
self.experiment_callback.send_results(ResultSuite(
**{self.chart_generator.result_name: chart_item}
))
self.schedule_event('next_point', 0)
else:
self.schedule_event('check_voltage')
Configure the experiment¶
With experiment manager you need to create configuration file, please see Experiment configuration file.
[Experiment]
#Experiment name
ExperimentName=MockExperiment
#Name of the experiment manager (one created in last step)
ExperimentManagerClass=silf.backend.commons_test.experiment.mock_experiment_manager.MockExperimentManager #Ścieżka do klasy zarządzającej eksperymentem
#Client class
ClientClass=silf.backend.client.mock_client.Client #Klasa klienta
# How long this experiment will wait before it shuts itself down after last series did end
shutdown_experiment_timeout=3600
# How long this experiment will wait before it kills current session
stop_series_timeout=600
[XMPPClient]
#Configutation for XMPP client
nick = experiment
jid = geiger@pip.ilf.edu.pl/experiment
password = NepjotmirkOdofruebcajigIaheHuSka
room = test-geiger@muc.pip.ilf.edu.pl
port = 5222
host = pip.ilf.edu.pl
[Logging]
config_type=file
Experiment configuration file¶
This file contains experiment description.
[Experiment] Section¶
Contains configuration for the whole experiment:
ExperimentName
- Name of the experiment
ExperimentManagerClass
- Class that manages the experiment (see: Complicated experiment manager and
BaseExperimentManager
). ClientClass
- XMPPClient to use.
Other entries in this section are optional, and are dependent ExperimentManagerClass
.
LoopTimeout
- Timeout of main experiment loop [in seconds]
series_pattern
Experiment series will get label according to this pattern (unless
ExperimentManagerClass
overries it ;).This pattern can contain names of any settings sent to the experiment in
str.format()
format.stop_series_timeout
- Timeout (in seconds) after which we stop series that was not stopped by the user or the experiment (and it does not send any results)
shutdown_experiment_timeout
- Timeout (in seconds) after which we stop experiment if there are no series active.
AdditionalConfigFiles
- See Multi File Config
Multi File Config¶
You might want to split experiment configuration into several config files, valid examples for such setup are, we just wanted to store config files inside VCS, and have passwords stored elsewhere.
Config entry: AdditionalConfigFiles
in section [Config]
contains semicolon separated
list of paths to config files, these config files will be loaded
during experiment startup and will override any entries in original
config file.
If any of specified files is missing exception will be raised.
[XMPPClient] Section¶
Contains configuration for the XMPP client.
It is mostly obvious:
jid
- JID (part before
@
symbol) nick
- nick in XMPP groupchat, any string
password
- password for XMPP account
room
- XMPP groupchat room name (part before
@
symbol) server_config_section
- Name of section that contains XMPP server configuration,
by default it is
[Server]
[Server] Section¶
These properties govern connection to tigase server
host
- server address Due to some Sleek bug you should provide IP rather than a hostname
port
- server port
These properties are added used to create JID
domain
- Domain (added to JID after
@
symbol) groupchat_domain
- Domain (added to room name
@
symbol)
[Logging] Section¶
Configures logging for this experiment.
config_type
- Either
file
orbasic
.basic
callslogging.basicConfig(DEBUG
,file
allows useini
based logging configuration. logging_file
- Usable for
config_type=file
. Specifies the logging configuration to use. Defaults to prepared config file.
In both configs a we send e-mails with errors to experiment administrators, to configure this behaviour in your logging config you might use logging section that looks like:
[handler_exception]
class: silf.backend.commons.util.mail.ExceptionHandler
formatter: detailed
args: []
And then add handler configured in this way to root logger.
[SMTP] Section¶
This section is optional. It configures functionality of sending emails to administratiors of the experiment.
smtp_enabled
Boolean value isfalse
or missing altogether thhis functionality is disabled.
test_smtp
Boolean value iftrue
will use mock smtp (no emails will be sent).
smtp_server
, smtp_port
Host to which we connect via SMTP
smtp_user
, smtp_password
Login credentials
smtp_client_class
SMTP client class to use. This entry contains path to module and type to use, for example:smtplib.SMTP_SSL
. This class has to have similar api tosmtplib.SMTP_SSL
.
from_email
FROM address for emails.
admin_list
List of users to which we send error emails.
Other sections¶
This file may also contain other sections that define particular devices.
Device uses configuratuion defined in section named by it’s deviceId
.
While device can use any property in it’s own section this sections also contains:
single_task_timeout
Float value, maximal time for each operation on the remote device in seconds. If operation takes longer time error is raised.
XMPPClient api¶
For functions descriptions refer to silf.backend.client Package
Client is low-level component used by experiment that encapsulates XMPP communication, it’s based on SleekXMPP library with minor modifications.
Client allows sending and receiving custom XMPP substanzas (labdata) embedded in basic Message stanzas.
Functionality¶
Client can send XMPP groupchat messages to two separate XMPP rooms. One room is used for data transfer between parts of experiment, second one is chat room for users.
Messages sent to main room must contain labdata substanza. Client provides callbacks fired when specific labdata namespace or type arrives.
Config options:
- nick - Nick in XMPP room used by client
- jid - first part of jid (login) on XMPP server
- password - password
- room - XMPP groupchat room to use as main room (for data transfer)
- chatroom - XMPP groupchat room to use as chat room (for user messages)
- server_config_section - Name of config file section that contains XMPP server config
Server config options:
- host - Host used for connection (or IP address)
- domain - Second part of JIDs of users (domain configured in XMPP server)
- groupchat_domain - Domain configured in XMPP server used in room JIDs
- port - Port used for connection
Example config:
[Client]
nick = Echo
jid = echo
password = jhfjkkdutgouhitg
room = testroom
chatroom = testchatroom
server_config_section = Server
[Server]
host = pip.ilf.edu.pl
domain = pip.ilf.edu.pl
groupchat_domain = muc.pip.ilf.edu.pl
port = 5222
You can specify parsed config file section by passing ‘section’ parameter to client constructor ([XMPPClient] in default experiment implementation)
Basic usage in experiment¶
You do not have to touch anything inside client when writing standard experiment, specify client class in experiment config file (in [Experiment] section)
[Experiment]
(...)
ClientClass=silf.backend.client.client.Client
(...)
Usage as standalone class¶
c = Client(config='default.ini', section='Client')
c.initialize() # make XMPP connection
c.send_chatmessage('Hello, XMPP client here') # send message to chatroom
l = format_labdata(...)
c.send_labdata(l) # Send message with labdata substanza to main room
c.register_callback(callback_func, namespace=None, type=None) # register callback function for ALL labdata messages
c.register_callback(callback_get, namespace='silf:mode:get', type=None) # register callback for all labdata messages in 'silf:mode:get' namespace
c.register_callback(callback_set_query, namespace='silf:mode:set', type='query') # register callback for labdata messages of type query in namespace 'silf:mode:set'
c.disconnect() # terminate XMPP connection
Refer to protocol documentation for list of available namespaces and labdata types
Module index¶
setup Module¶
silf.backend.client Package¶
silf.backend.client
Package¶
api
Module¶
-
exception
silf.backend.client.api.
ClientException
¶ Bases:
Exception
Base class for all Client related exceptions
-
exception
silf.backend.client.api.
ClientNoErrorNamespaceException
¶ Bases:
silf.backend.client.api.ClientException
Thrown when send_error is called in a way that does not provide namespace (all args None)
-
class
silf.backend.client.api.
IClient
(config='config/default.ini', section='Client', block=False)¶ Bases:
object
-
Message
¶
-
copy
() → silf.backend.client.api.IClient¶ Creates a copy of this client
-
create_random_id
()¶
-
disconnect
(wait)¶
-
initialize
()¶
-
make_labdata_message
(namespace=None, type=None, suite=None, labdata=None)¶ Create XMPP message stanza with labdata substanza using suppliend parameters
Parameters: - namespace – Namespace of labdata substanza
- type – Type of labdata substanza
- suite – Data suite that will be inserted into labdata substanza
- labdata – Labdata substanza
Note
if labdata parameter is None, new labdata substanza is created using namespace, type and suite parameters, otherwise they are ignored.
-
make_message
(mto, labdata, mbody=None, msubject=None, mtype=None, mhtml=None, mfrom=None, mnick=None)¶
-
random_resource
(jid)¶ Add random resource to JID
-
register_callback
(callback, namespace=None, type=None)¶
-
register_error_callback
(func)¶
-
send
(namespace, suite, ltype='query', id=None, incoming_stanza=None)¶ Send labdata stanza with body containing JSON from suite, take namespace and id from state if serializing/sending labdata fails for some reason send error labdata instead
Parameters: - state (IncomingStanza) – IncomingStanza object (see IncomingStanza class description)
- suite – Data to be sent (JSON serializable object - suite/stanza)
- ltype (str) – Labtata type to use (string)
Returns: None
-
send_error
(suite, incoming_stanza=None, lnamespace=None)¶ Send error labdata
Parameters: - suite (ErrorSuite) – ErrorSuite object (contains info about errors - seraializable to JSON)
- incoming_stanza – IncomingStanza object
- lnamespace – Labdata namespace to use
Returns: None
Note
labdata namespace is taken from state object, if state is None, namespace is taken from lnamespace parameter, if both are None exception is thrown.
-
send_labdata
(labdata)¶
-
send_results
(suite)¶ Send experiment results in ‘data’ labdata stanza :param suite: DataSuite object (serializable to JSON)
-
client
Module¶
-
class
silf.backend.client.client.
CallbackWrapper
(name, matcher, pointer, client, **kwargs)¶ Bases:
sleekxmpp.xmlstream.handler.callback.Callback
SleekXMPP does not allow passing additional arguments to _callbacks CallbackWrapper adds one layer between sleekxmpp and actual callback function.
-
run
(payload, instream=False)¶
-
-
class
silf.backend.client.client.
Client
(config='config/default.ini', section='Client', block=False)¶ Bases:
silf.backend.client.api.IClient
XMPP client
Initiates connection to XMPP server, joins multi user chat.
After initialization use register_*_callback functions to add handlers for labdata messages in specific namespaces.
-
Message
¶
-
copy
() → silf.backend.client.client.Client¶ Creates a copy of this client
-
disconnect
(wait=None)¶ Terminate connection to XMPP server. Both clients will be disconnected
-
initialize
(time_to_wait_for_connection=0)¶ Connect to XMPP server, register necessary plugins
Create SleekXMPP client, for sending labdata stanzas. Use unencrypted plain authentication, because Tigase has problem with SSL. Insert self.error_handler into modified SleekXMPP clinets which will be called when XML perser error occurs.
-
make_message
(mto, labdata, mbody=None, msubject=None, mtype=None, mhtml=None, mfrom=None, mnick=None)¶ Create XMPP message stanza and insert labdata into it
Parameters: - labdata – Labdata substanza which will be inserted into message
- mbody – Body of message (text)
- msubject – Message subject
- mtype – Message type (None for user to user messages, ‘groupchat’ for muc messages)
- mhtml – HTML body content (optional)
- mfrom – JID of sender (some servers require full client JID)
- mnick – Nick of sender (optional)
-
register_error_callback
(func)¶ Register callback function executed before client reset when XML parser error occurs
-
send_labdata
(stanza)¶ Send message with labdata substanza to groupchat Add dummy <body> tag, XMPP server (openfire) does not pass groupchat messages without it :param stanza: received message.
-
start
(event)¶ Fired automatically after successful connection to XMPP server Join groupchat.
-
wait_until_online
(timeout=15)¶ Wait until client gets online :param timeout: max time to wait
-
-
exception
silf.backend.client.client.
DisconnectedException
¶ Bases:
Exception
Raised when Client couldn’t connect during startup.
We treat all connection errors as fatal errors, that force experiments to be terminated and then resurrected by systemd.
-
class
silf.backend.client.client.
SleekErrorHandler
¶ Bases:
object
SleekXMPP sometimes receives XMPP conflict, XML interpreter (expat?) throws SyntaxError exception, and tries to reconnect (XML stream is stuck in some unmanageable condition, and reconnect never succeeds). This triggers endless stream of reconnect attempts, experinment server becomes unusable. Resetting XML stream from inside of SleekXMPP (abort and then connect) does not work (triggers SocketError). XML processing thread must be killed (we must do this from Client context)
-
add_callable
(c)¶ Add callback function (called on stream error from main experiment thread) avoid using this function directly, use Client.register_error_handler
-
check_flag
()¶
-
fire
()¶ Called when error is caught, calls all stored callback functions, flag marks client as ‘broken’ and can be read by other threads
-
const
Module¶
echo
Module¶
-
class
silf.backend.client.echo.
Echo
(config, section='Echo')¶ Bases:
silf.backend.client.listener.Listener
Test XMPP echo bot - bounces back all messages Used for labdata/Client debugging
-
muc_message
(msg)¶
-
labdata
Module¶
Labdata XMPP stanza (substanza of Message)
-
exception
silf.backend.client.labdata.
InvalidLabdataContentException
¶ Bases:
silf.backend.client.labdata.LabdataException
Thrown when we find labdata with invalid content.
-
class
silf.backend.client.labdata.
LabdataBase
(xml=None, parent=None, type=None, suite=None, id=None)¶ Bases:
sleekxmpp.xmlstream.stanzabase.ElementBase
-
body_as_dict
¶
-
content
¶
-
get_body
()¶
-
has_body
¶
-
insert_into_message
(msg)¶ Insert Labdata substanza into message
-
interfaces
= {'id', 'type', 'body'}¶
-
name
= 'labdata'¶
-
namespace
= 'silf'¶
-
plugin_attrib
= 'labdata'¶
-
set_body
(value)¶ Set body of labdata element in XML: <labdata …> HERE </labdata>
-
set_type
(value)¶ Set type of Labdata substanza, ‘result’, ‘ack’ and ‘done’ need ‘id’ attribute set before type
-
sub_interfaces
= set()¶
-
suite
¶
-
suite_type
¶
-
type
¶
-
-
exception
silf.backend.client.labdata.
LabdataException
¶ Bases:
Exception
Base class for all Labdata stanza related exceptions
-
class
silf.backend.client.labdata.
LabdataExperimentStop
(xml=None, parent=None, type=None, suite=None, id=None)¶ Bases:
silf.backend.client.labdata.LabdataBase
-
namespace
= 'silf:experiment:stop'¶
-
plugin_attrib
= 'silf:experiment:stop'¶
-
-
class
silf.backend.client.labdata.
LabdataLangSet
(xml=None, parent=None, type=None, suite=None, id=None)¶ Bases:
silf.backend.client.labdata.LabdataBase
-
namespace
= 'silf:lang:set'¶
-
plugin_attrib
= 'silf:lang:set'¶
-
-
class
silf.backend.client.labdata.
LabdataMatcher
(lnamespace=None, ltype=None)¶ Bases:
sleekxmpp.xmlstream.matcher.base.MatcherBase
-
match
(stanza)¶ Test if message stanza contains proper labdata substanza, check if labdata satisfies conditions set in __init__
-
-
class
silf.backend.client.labdata.
LabdataMiscError
(xml=None, parent=None, type=None, suite=None, id=None)¶ Bases:
silf.backend.client.labdata.LabdataBase
-
namespace
= 'silf:misc:error'¶
-
plugin_attrib
= 'silf:misc:error'¶
-
-
class
silf.backend.client.labdata.
LabdataModeGet
(xml=None, parent=None, type=None, suite=None, id=None)¶ Bases:
silf.backend.client.labdata.LabdataBase
-
namespace
= 'silf:mode:get'¶
-
plugin_attrib
= 'silf:mode:get'¶
-
-
class
silf.backend.client.labdata.
LabdataModeSet
(xml=None, parent=None, type=None, suite=None, id=None)¶ Bases:
silf.backend.client.labdata.LabdataBase
-
namespace
= 'silf:mode:set'¶
-
plugin_attrib
= 'silf:mode:set'¶
-
-
class
silf.backend.client.labdata.
LabdataResults
(xml=None, parent=None, type=None, suite=None, id=None)¶ Bases:
silf.backend.client.labdata.LabdataBase
-
namespace
= 'silf:results'¶
-
plugin_attrib
= 'silf:results'¶
-
-
class
silf.backend.client.labdata.
LabdataSeriesStart
(xml=None, parent=None, type=None, suite=None, id=None)¶ Bases:
silf.backend.client.labdata.LabdataBase
-
namespace
= 'silf:series:start'¶
-
plugin_attrib
= 'silf:series:start'¶
-
-
class
silf.backend.client.labdata.
LabdataSeriesStop
(xml=None, parent=None, type=None, suite=None, id=None)¶ Bases:
silf.backend.client.labdata.LabdataBase
-
namespace
= 'silf:series:stop'¶
-
plugin_attrib
= 'silf:series:stop'¶
-
-
class
silf.backend.client.labdata.
LabdataSettingsCheck
(xml=None, parent=None, type=None, suite=None, id=None)¶ Bases:
silf.backend.client.labdata.LabdataBase
-
namespace
= 'silf:settings:check'¶
-
plugin_attrib
= 'silf:settings:check'¶
-
-
class
silf.backend.client.labdata.
LabdataSettingsUpdate
(xml=None, parent=None, type=None, suite=None, id=None)¶ Bases:
silf.backend.client.labdata.LabdataBase
-
namespace
= 'silf:settings:update'¶
-
plugin_attrib
= 'silf:settings:update'¶
-
-
class
silf.backend.client.labdata.
LabdataState
(xml=None, parent=None, type=None, suite=None, id=None)¶ Bases:
silf.backend.client.labdata.LabdataBase
-
namespace
= 'silf:state'¶
-
plugin_attrib
= 'silf:state'¶
-
-
class
silf.backend.client.labdata.
LabdataVersionGet
(xml=None, parent=None, type=None, suite=None, id=None)¶ Bases:
silf.backend.client.labdata.LabdataBase
-
namespace
= 'silf:protocol:version:get'¶
-
plugin_attrib
= 'silf:protocol:version:get'¶
-
-
class
silf.backend.client.labdata.
LabdataVersionSet
(xml=None, parent=None, type=None, suite=None, id=None)¶ Bases:
silf.backend.client.labdata.LabdataBase
-
namespace
= 'silf:protocol:version:set'¶
-
plugin_attrib
= 'silf:protocol:version:set'¶
-
-
exception
silf.backend.client.labdata.
NoLabdataException
¶ Bases:
silf.backend.client.labdata.LabdataException
Thrown when
reply_labdata()
is called on message without labdata substanza.
-
exception
silf.backend.client.labdata.
NoLabdataIdException
¶ Bases:
silf.backend.client.labdata.LabdataException
Thrown when user tries to set labdata type to ack, result or done wtithout setting
id
first
-
exception
silf.backend.client.labdata.
NoLabdataNamespaceException
¶ Bases:
silf.backend.client.labdata.LabdataException
Thrown when matcher encounters labdata stanza without namespace
-
exception
silf.backend.client.labdata.
UnknownLabdataNamespaceException
¶ Bases:
silf.backend.client.labdata.LabdataException
Thrown when user tries to format_labdata with namespace not in
LABDATA_NAMESPACES
-
exception
silf.backend.client.labdata.
UnknownLabdataTypeException
¶ Bases:
silf.backend.client.labdata.LabdataException
Thrown when user tries to create labdata of unknown type (not in
LABDATA_TYPES
array)
-
silf.backend.client.labdata.
check_labdata
(stanza)¶ Check if stanza is valid labdata
-
silf.backend.client.labdata.
check_message_labdata
(stanza)¶ Check if message contains valid labdata
-
silf.backend.client.labdata.
format_labdata
(lnamespace, ltype='ack', lid=None, lbody=None, suite=None)¶ Prepare labdata stanza using supplied data
-
silf.backend.client.labdata.
labdata_subclass
¶ alias of
LabdataSettingsUpdate
-
silf.backend.client.labdata.
reply_labdata
(labdata, ltype='ack', lbody=None)¶ Reply to labdata stanza (create another labdata)
labdata_sender
Module¶
-
class
silf.backend.client.labdata_sender.
LabdataSender
(args, config='config/default.ini', section='Sender')¶ Bases:
object
Connects to XMPP server, joins groupchat, sends single labdata stanza with parameters specified by command line args and disconnects. Used only for labdata/Client debugging
-
initialize
(block=True)¶
-
muc_message
(msg)¶
-
send_labdata
(stanza)¶
-
start
(event)¶
-
listener
Module¶
mock_client
Module¶
-
class
silf.backend.client.mock_client.
Client
(config='config/default.ini', section='Client', block=False, log_file=None)¶ Bases:
silf.backend.client.api.IClient
>>> mock = Client()
>>> mock.sent_stanzas {}
We are registering callback for all incoming stanzas:
>>> mock.register_callback(lambda x: print("Reclieved {}".format(x))) >>> mock.fire_synthetic_event( "silf:mode:get", "query") Reclieved <IncomingStanza type='query' namespace='silf:mode:get' contents=<<empty>> >
Sent stanzas contains stanzas sent by this client so it is empty:
>>> mock.sent_stanzas {}
>>> mock.register_send_callback(lambda x: print("Sent {}".format(x)))
>>> mock.send_standalone_stanza('silf:experiment:stop') Sent <IncomingStanza type='data' namespace='silf:experiment:stop' contents=<<empty>> >
>>> mock.sent_stanzas {'silf:experiment:stop': [<labdata data silf:experiment:stop None>]}
-
assert_and_get_stanza
(namespace, types=None)¶ Returns stanza with proper
namespace
andtype
if it was sent by the remote side (experiment)Parameters: Raises: AssertionError – If stanza was not found
Returns: Specified labdata
Return type:
-
assert_no_errors
()¶
-
clear_client
()¶
-
copy
() → silf.backend.client.mock_client.Client¶
-
disconnect
(wait=None)¶
-
fire_synthetic_event
(namespace, type, contents=None, suite=None)¶ Parameters: incoming_stanza (LabdataState) – Event to be fired. Returns:
-
get_and_clear_errors
()¶
-
get_stanza
(namespace, types=None)¶
-
initialize
()¶
-
register_error_callback
(func)¶
-
register_send_callback
(callback, *, type=None, namespace=None)¶
-
send_labdata
(labdata)¶
-
sent_errors
¶
-
sent_stanzas
¶
-
-
class
silf.backend.client.mock_client.
TestClient
(config='config/default.ini', section='Client', block=False, log_file=None)¶ Bases:
silf.backend.client.mock_client.Client
-
send_set_mode
(mode)¶
-
send_set_mode_and_wait
(mode, wait=2)¶
-
send_start_series
(**kwargs)¶
-
send_start_series_and_wait
(settings, wait_time=3)¶
-
send_stop_experiment
()¶
-
send_stop_experiment_and_wait
(wait_time=3)¶
-
send_stop_series
()¶
-
send_stop_series_and_wait
(wait_time=5)¶
-
watch_for_results
(max_time, on_result_callback=None, do_print=False)¶
-
api Package¶
api
Package¶
const
Module¶
exceptions
Module¶
-
exception
silf.backend.commons.api.exceptions.
ConfigurationException
¶
-
exception
silf.backend.commons.api.exceptions.
DeviceException
(message, fixable_by_user=False, **kwargs)¶ Bases:
silf.backend.commons.api.exceptions.SilfException
Exception raised by the device.
-
fixable_by_user
= None¶ bool
. True if this erroc can be fixed by user.
-
-
exception
silf.backend.commons.api.exceptions.
DeviceRuntimeException
¶ Bases:
silf.backend.commons.api.exceptions.SilfException
Wrapper for any exception raised by the device.
-
exception
silf.backend.commons.api.exceptions.
DiagnosticsException
¶ Bases:
silf.backend.commons.api.exceptions.DeviceRuntimeException
Thrown if intialization of this device encounters an error.
-
exception
silf.backend.commons.api.exceptions.
ExperimentBackendUnresponsive
¶ Bases:
silf.backend.commons.api.exceptions.SILFProtocolError
-
exception
silf.backend.commons.api.exceptions.
ExperimentCreationError
¶ Bases:
silf.backend.commons.api.exceptions.ConfigurationException
Raised when we can’t create an experiment
-
exception
silf.backend.commons.api.exceptions.
InvalidModeException
¶ Bases:
silf.backend.commons.api.exceptions.SilfException
Raised when experiment or device wrapper is in invalid mode.
-
exception
silf.backend.commons.api.exceptions.
InvalidStateException
(message, fixable_by_user=False, **kwargs)¶ Bases:
silf.backend.commons.api.exceptions.DeviceException
Raised when device is in invalid state
-
exception
silf.backend.commons.api.exceptions.
MissingRequiredControlException
(errors)¶
-
exception
silf.backend.commons.api.exceptions.
SILFProtocolError
(errors)¶ Bases:
silf.backend.commons.api.exceptions.SilfException
Exception that should be propagated to the enduser, it should be caught by the experiment class and trensformed to error stanza.
-
classmethod
from_args
(severity, error_type, message, field=None, **kwargs)¶ >>> raise SILFProtocolError.from_args("error", "system", "Test error") Traceback (most recent call last): exceptions.SILFProtocolError: Test error
Constructs
SILFProtocolError
fromError
constructed from args and kwargs. :rtype:SILFProtocolError
-
classmethod
join
(*args)¶ Constructs
SILFProtocolError
from otherSILFProtocolError
newly created exception will contain all errors from exception it was created from. :param args: Iterable ofSILFProtocolError
:rtype:SILFProtocolError
-
classmethod
-
exception
silf.backend.commons.api.exceptions.
SerializationException
¶ Bases:
silf.backend.commons.api.exceptions.SilfException
Exception raised during serialization to or from json. It normally signifies programming error.
-
exception
silf.backend.commons.api.exceptions.
SettingNonLiveControlException
(errors)¶
-
exception
silf.backend.commons.api.exceptions.
ValidationError
(errors)¶ Bases:
silf.backend.commons.api.exceptions.SILFProtocolError
Subpackages¶
stanza_content Package¶
stanza_content
Package¶
_error
Module¶
-
class
silf.backend.commons.api.stanza_content._error.
Error
(severity, error_type, message, field=None, **kwargs)¶ Bases:
silf.backend.commons.api.stanza_content._json_mapper.JsonMapped
Represents error message.
>>> err = Error("warning", "device", "foo") >>> err <Error severity='warning' error_type='device' metadata='[('message', 'foo')]'> >>> err.field = "Foo" >>> err <Error severity='warning' error_type='device' metadata='[('field_name', 'Foo'), ('message', 'foo')]'> >>> err.__getstate__() == {'metadata': {'field_name': 'Foo', 'message': 'foo'}, 'severity': 'warning', 'error_type': 'device'} True
>>> Error.from_dict(err.__getstate__()) == err True
-
DEFAULTS
= {'metadata': {}}¶
-
DICT_CONTENTS
= {'metadata', 'severity', 'error_type'}¶
-
field
¶ Field to which this error is attached
-
message
¶
-
-
class
silf.backend.commons.api.stanza_content._error.
ErrorSuite
(errors=None)¶ Bases:
silf.backend.commons.api.stanza_content._json_mapper.JsonMapped
>>> err = ErrorSuite(errors=[ ... Error("warning", "device", "foo"), Error("warning", "device", "bar") ... ])
>>> err.__getstate__() == {'errors': [{'metadata': {'message': 'foo'}, 'severity': 'warning', 'error_type': 'device'}, {'metadata': {'message': 'bar'}, 'severity': 'warning', 'error_type': 'device'}]} True >>> err.setstate({'errors': []}) >>> err.errors [] >>> err.__setstate__({'errors': [{'metadata': {'message': 'foo'}, 'severity': 'warning', 'error_type': 'device'}, {'metadata': {'message': 'bar'}, 'severity': 'warning', 'error_type': 'device'}]}) >>> err.errors [<Error severity='warning' error_type='device' metadata='[('message', 'foo')]'>, <Error severity='warning' error_type='device' metadata='[('message', 'bar')]'>]
>>> ErrorSuite.from_dict(err.__getstate__()) == err True
-
DICT_CONTENTS
= {'errors'}¶
-
getstate
()¶
-
classmethod
join
(*args)¶ Returns
ErrorSuite
constructed from all errors in*args
.>>> error1 = Error("warning", "device", "foo") >>> error2 = Error("warning", "device", "bar") >>> error3 = Error("warning", "device", "baz") >>> ErrorSuite.join(ErrorSuite(errors=[error1, error2]), ErrorSuite(errors=[error3])) <ErrorSuite errors=[<Error severity='warning' error_type='device' metadata='[('message', 'foo')]'>, <Error severity='warning' error_type='device' metadata='[('message', 'bar')]'>, <Error severity='warning' error_type='device' metadata='[('message', 'baz')]'>] >
Parameters: args – List of ErrorSuite
.Return type: ErrorSuite
-
setstate
(state)¶
-
_json_mapper
Module¶
Does serialization to json.
Serialization example
>>> from silf.backend.commons.api import *
>>> import json
>>> selection = ModeSelection(mode = "foo")
>>> json.dumps(selection.__getstate__())
'{"mode": "foo"}'
Deserialization example
>>> ModeSelection.from_dict(json.loads('{"mode": "foo"}'))
<ModeSelection mode=foo>
-
exception
silf.backend.commons.api.stanza_content._json_mapper.
SerializationException
¶ Bases:
silf.backend.commons.api.exceptions.SilfException
Exception raised during serialization to or from json. It normally signifies programming error.
-
class
silf.backend.commons.api.stanza_content._json_mapper.
FromStateMixin
¶ Bases:
object
-
classmethod
from_dict
(state)¶ Creates instance of this class using provided state dictionatry :param state: state from which to create instance :return: Created instance :rtype:cls
-
classmethod
-
class
silf.backend.commons.api.stanza_content._json_mapper.
JsonMapped
(**kwargs)¶ Bases:
silf.backend.commons.api.stanza_content._json_mapper.FromStateMixin
Json class that has well-known set of properties. Each property has known type (that can optionally be checked).
This class should be subclassed like that:
>>> class TmpTestJsonMapped(JsonMapped): ... ... DICT_CONTENTS = {'foo'} ... ... def __init__(self, foo): ... super().__init__() ... self.foo = foo >>> foo = TmpTestJsonMapped(foo="bar") >>> foo.__getstate__() {'foo': 'bar'}
This class can automagically set atributes in __init__ method
>>> class TmpTestJsonMapped(JsonMapped): ... ... DICT_CONTENTS = {'foo'} ... ... def __init__(self, **kwargs): ... super().__init__(**kwargs) >>> foo = TmpTestJsonMapped(foo="bar") >>> foo.__getstate__() {'foo': 'bar'} >>> foo = TmpTestJsonMapped(bar="bar") Traceback (most recent call last): ValueError: Invalid __init__ parameter bar
-
DEFAULTS
= {}¶ Dictionaty containing default values for some of the properties.
-
DICT_CONTENTS
= set()¶ Defines set of properties that need to be in state dicts (for both serialization and deserialization)
-
getstate
()¶ Iternal function that should be overriden, it returns state dictionary, that will be transformed to json:
>>> foo = TestJsonMapped(foo="bar") >>> foo.getstate() == {'foo': 'bar'} True
Default implementation just filters the __dict__ property, so any additional items are removed:
>>> foo.bar = 'baz' >>> foo.getstate() == {'foo': 'bar'} True
-
setstate
(state)¶ Iternal function that should be overriden by subclasses.
Note
This function should not be called directly, rather use
__setstate__()
that checks state dicitonary for invalid keys.Default implementation just does:
for k, v in state.items(): setattr(self, k, v)
>>> foo = TestJsonMapped("foo")
This function performs no checks whatsoever!
>>> foo.setstate({'foo': 'bar', 'bar': 'baz'}) >>> foo.bar 'baz'
-
-
class
silf.backend.commons.api.stanza_content._json_mapper.
FlatDictJsonMapped
(**kwargs)¶ Bases:
silf.backend.commons.api.stanza_content._json_mapper.FromStateMixin
Represents type that serializes to json object that can contain any property, but all properties are of the same type.
>>> class TmpTestFlatDictJsonMapped(FlatDictJsonMapped): ... INTERNAL_DICT_NAME = "foos" ... INTERNAL_VALUE_CLASS = TestJsonMapped
Init method can take optional keyword named as
INTERNAL_DICT_NAME
>>> flat = TmpTestFlatDictJsonMapped( … foo = TestJsonMapped(foo=’bar’), … bar = TestJsonMapped(foo=’baz’)) >>> sorted(flat.foos.items()) # doctest: +ELLIPSIS [(‘bar’, <Foo foo=baz>), (‘foo’, <Foo foo=bar>)]It implements get state:
>>> flat.__getstate__() == {'foo': {'foo': 'bar'}, 'bar': {'foo': 'baz'}} True >>> flat.foos = {} >>> flat.foos {}
And set state:
>>> flat.__setstate__({'foo': {'foo': 'bar'}, 'bar': {'foo': 'baz'}}) >>> sorted(flat.foos.items()) [('bar', <Foo foo=baz>), ('foo', <Foo foo=bar>)]
-
INTERNAL_DICT_NAME
= None¶ Name of property that contains internal dictionary that will hold mapping.
-
INTERNAL_VALUE_CLASS
= None¶ Class to which all properties will be deserialized
-
deserialize_internal_item
(item)¶ Deserializes :attr:INTERNAL_VALUE_CLASS. by default it calls item.from_dict.
-
get
(*args, **kwargs)¶
-
internal_dict
¶
-
classmethod
join
(*args)¶ Joins series of instances of this class into single instance, checking for eventual duplicate keys;
>>> from silf.backend.commons.api import ResultSuite, Result
>>> a = ResultSuite(foo=Result(value=[5]), foobar=Result(value=[1, 2, 3])) >>> b = ResultSuite(bar=Result(value=[10]), barbar=Result(value=[1, 2, 3]))
>>> ResultSuite.join(a, b) <ResultSuite bar=<Result pragma=append value=[10] > barbar=<Result pragma=append value=[1, 2, 3] > foo=<Result pragma=append value=[5] > foobar=<Result pragma=append value=[1, 2, 3] > >
>>> ResultSuite.join(a, b, a) Traceback (most recent call last): ValueError: We are joining instances of <class 'silf.backend.commons.api.misc.ResultSuite'>, these instances should not contain duplicate data, however they did. Duplicate keys were ['foo', 'foobar']. Debug data is: [{"foo": {"pragma": "append", "value": [5]}, "foobar": {"pragma": "append", "value": [1, 2, 3]}}, {"bar": {"pragma": "append", "value": [10]}, "barbar": {"pragma": "append", "value": [1, 2, 3]}}, {"foo": {"pragma": "append", "value": [5]}, "foobar": {"pragma": "append", "value": [1, 2, 3]}}].
-
serialize_internal_item
(item)¶ Serializes :attr:INTERNAL_VALUE_CLASS. by default it calls item.__getstate__.
-
_misc
Module¶
-
class
silf.backend.commons.api.stanza_content._misc.
Setting
(value=None, current=False)¶ Bases:
silf.backend.commons.api.stanza_content._json_mapper.JsonMapped
Single setting object. It serializes to following JSON:
>>> setting = Setting(value=15.4, current=True)
Serialization format
>>> setting.__getstate__() == {'current': True, 'value': 15.4} True >>> setting.__setstate__({'current': True, 'value': 15.4}) >>> setting.current True >>> setting.value 15.4
It can deserialize from dictionary without current item: >>> setting.__setstate__({‘value’: 15.4}) >>> setting.current False >>> setting.value 15.4
Check equality:
>>> setting2 = Setting.from_dict(setting.__getstate__()) >>> setting2 == setting True
Setting incorrect type for current raises an exception >>> setting = Setting(value=15.4, current=”dupa”) Traceback (most recent call last): silf.backend.commons.api.exceptions.SerializationException: Current property must be either True or False >>> setting.__setstate__({“value” :5, ‘current’:”dupa”}) Traceback (most recent call last): silf.backend.commons.api.exceptions.SerializationException: Current property must be either True or False
-
DEFAULTS
= {'current': False}¶
-
DICT_CONTENTS
= {'current', 'value'}¶
-
-
class
silf.backend.commons.api.stanza_content._misc.
SettingSuite
(**kwargs)¶ Bases:
silf.backend.commons.api.stanza_content._json_mapper.FlatDictJsonMapped
Contains a set of settings.
Serializes to following JSON:
>>> settings_set = SettingSuite( ... foo = Setting(value = 13), ... bar = Setting(value= "Kotek!", current=True) ... )
<SettingSuite bar=<Setting current=True value=Kotek! > foo=<Setting current=False value=13 > >
Serialization format>>> settings_set.__getstate__() == {'foo': {'current': False, 'value': 13}, 'bar': {'current': True, 'value': 'Kotek!'}} True
Test equality:
>>> setting_set2 = SettingSuite.from_dict(settings_set.__getstate__()) >>> setting_set2 == settings_set True
-
INTERNAL_DICT_NAME
= 'settings'¶
-
settings
= {}¶
-
-
class
silf.backend.commons.api.stanza_content._misc.
InputFieldSuite
(controls=(), **kwargs)¶ Bases:
silf.backend.commons.api.stanza_content._json_mapper.FlatDictJsonMapped
Contains a set of controls.
>>> from silf.backend.commons.api import *
>>> control_set = InputFieldSuite( ... controls = [NumberControl("foo", "Insert foo"), ... NumberControl("bar", "Insert bar")] ... ) >>> control_set.__getstate__() == { ... 'foo': {'validations': {}, 'live': False, 'type': 'number', 'name': 'foo', 'metadata': {'label': 'Insert foo'}, "order": 0}, ... 'bar': {'live': False, 'type': 'number', 'name': 'bar', 'metadata': {'label': 'Insert bar'}, 'validations': {}, "order": 1} ... } True
Test equality:
>>> InputFieldSuite.from_dict(control_set.__getstate__()) == control_set True
>>> control_set.__setstate__({"foo" : 15}) Traceback (most recent call last): ValueError: Can deserialize only dict, got 15 (type: <class 'int'>)
-
INTERNAL_DICT_NAME
= 'controls'¶
-
INTERNAL_VALUE_CLASS
= None¶
-
controls
= {}¶
-
deserialize_internal_item
(item)¶
-
serialize_internal_item
(item)¶
-
-
class
silf.backend.commons.api.stanza_content._misc.
OutputField
(html_class, name, label=None, **kwargs)¶ Bases:
silf.backend.commons.api.stanza_content._json_mapper.JsonMapped
Represents a Result field.
Default values are applied when constructing, and while using __setstate__
>>> result = OutputField("chart", "betaArray") >>> result <Result class=chart name=['betaArray'] type=array metadata={} settings={}>
>>> result.__setstate__({"class": "chart", "name": ["betaArray"]}) >>> result <Result class=chart name=['betaArray'] type=array metadata={} settings={}> >>> result.settings {}
Some metadata properties can ve accessed as properties:
>>> result.label = "foo" >>> result <Result class=chart name=['betaArray'] type=array metadata={'label': 'foo'} settings={}>
Json property named class is avilable as html_class property
>>> result.html_class = "integer-indicator"
Serialization format
>>> result.__getstate__() == {'metadata': {'label': 'foo'}, 'class': 'integer-indicator', 'settings': {}, 'type': 'array', 'name': ['betaArray']} True >>> result.html_class 'integer-indicator'
Check equality:
>>> OutputField.from_dict(result.__getstate__()) == result True
>>> result = OutputField("chart", "betaArray") >>> result.__getstate__() == {'class': 'chart', 'name': ['betaArray'], 'metadata': {}, 'type': 'array', 'settings': {}} True >>> result.setstate( {'class': 'chart', 'name': ['gammaArray'], 'metadata': {}, 'type': 'array', 'settings': {}}) >>> result <Result class=chart name=['gammaArray'] type=array metadata={} settings={}>
-
DEFAULTS
= {'type': 'array', 'metadata': {}, 'settings': {}}¶
-
DICT_CONTENTS
= {'type', 'metadata', 'class', 'name', 'settings'}¶
-
getstate
()¶ Override parent implementation to dynamically translate labels inside of metadata dict :return:
-
html_class
¶
-
label
¶
-
-
class
silf.backend.commons.api.stanza_content._misc.
ChartField
(html_class, name, label=None, axis_x_label=None, axis_y_label=None, **kwargs)¶ Bases:
silf.backend.commons.api.stanza_content._misc.OutputField
-
axis_x_label
¶
-
axis_y_label
¶
-
getstate
()¶ Override parent implementation to dynamically translate labels inside of metadata dict :return:
-
-
class
silf.backend.commons.api.stanza_content._misc.
OutputFieldSuite
(**kwargs)¶ Bases:
silf.backend.commons.api.stanza_content._json_mapper.FlatDictJsonMapped
Holds suite of output results:
>>> suite = OutputFieldSuite(foo = OutputField("foo", ["foo"])) >>> suite.output_fields {'foo': <Result class=foo name=['foo'] type=array metadata={} settings={}>} >>> suite.__getstate__() == {'foo': {'class': 'foo', 'metadata': {}, 'name': ['foo'], 'settings': {}, 'type': 'array'}} True
Check equality:
>>> suite == OutputFieldSuite.from_dict(suite.__getstate__()) True
>>> suite.__setstate__({'bar': {'class': 'foo', 'metadata': {}, 'name': ['bar'], 'settings': {}, 'type': 'array'}}) >>> suite.output_fields {'bar': <Result class=foo name=['bar'] type=array metadata={} settings={}>}
TEST ERRORS
>>> suite = OutputFieldSuite(foo = OutputField("foo", ["foo", "baz"])) Traceback (most recent call last): silf.backend.commons.api.exceptions.ConfigurationException: Following errors encountered: ["Error --- control foo takes input from more than one result (that is: ['foo', 'baz']) this is disallowed"]
-
INTERNAL_DICT_NAME
= 'output_fields'¶
-
INTERNAL_VALUE_CLASS
¶ alias of
OutputField
-
output_fields
= None¶
-
-
class
silf.backend.commons.api.stanza_content._misc.
Mode
(label, description=None, order=None, **kwargs)¶ Bases:
silf.backend.commons.api.stanza_content._json_mapper.JsonMapped
Encapsulates single mode.
>>> mode = Mode("foo", description="Foo mode", order=1) >>> mode.__getstate__() == {'description': 'Foo mode', 'label': 'foo', 'order': 1} True >>> mode.setstate({'description': 'Bar mode', 'label': 'bar'}) >>> mode <Mode 'bar' description='Bar mode'>
Check Equality:
>>> mode == Mode.from_dict(mode.__getstate__()) True
-
DEFAULTS
= {'order': None, 'description': None}¶
-
DICT_CONTENTS
= {'order', 'label', 'description'}¶
-
getstate
()¶
-
-
class
silf.backend.commons.api.stanza_content._misc.
ModeSuite
(**kwargs)¶ Bases:
silf.backend.commons.api.stanza_content._json_mapper.FlatDictJsonMapped
Encapsulates suite of modes.
>>> suite = ModeSuite(foo= Mode("Foo Mode", "descr"), bar = Mode("Bar Mode", "descr")) >>> suite.__getstate__() == { ... 'foo': {'label': 'Foo Mode', 'description': 'descr', 'order': None}, ... 'bar': {'label': 'Bar Mode', 'description': 'descr', 'order': None} ... } True >>> suite.__setstate__({'bar': {'label': 'Bar Mode', 'description': 'descr'}}) >>> suite.modes {'bar': <Mode 'Bar Mode' description='descr'>}
Check equality:
>>> suite == ModeSuite.from_dict(suite.__getstate__()) True
Nice example:
>>> suite = ModeSuite(time = Mode("Pomiar czasu", "Mierz zliczenia, i kończ pomiar po upływie ustalonego czasu.", order=0), counts = Mode("Pomiar zliczeń", "Mierz czas potrzebny do uzyskania ustalonej liczby zliczeń", order=1)) >>> suite.__getstate__() == { ... 'counts': { ... 'description': 'Mierz czas potrzebny do uzyskania ustalonej liczby zliczeń', ... 'label': 'Pomiar zliczeń', ... 'order' : 1 ... }, ... 'time': { ... 'description': 'Mierz zliczenia, i kończ pomiar po upływie ustalonego czasu.', ... 'label': 'Pomiar czasu', ... 'order': 0 ... } ... } True
-
INTERNAL_DICT_NAME
= 'modes'¶
-
modes
= {}¶
-
-
class
silf.backend.commons.api.stanza_content._misc.
ModeSelection
(**kwargs)¶ Bases:
silf.backend.commons.api.stanza_content._json_mapper.JsonMapped
Encapsulates mode selection:
>>> mode = ModeSelection(mode = "foo") >>> mode.__getstate__() {'mode': 'foo'} >>> mode.setstate( {'mode': 'bar'}) >>> mode.mode 'bar'
Check Equality:
>>> mode == ModeSelection.from_dict(mode.__getstate__()) True
-
DICT_CONTENTS
= {'mode'}¶
-
-
class
silf.backend.commons.api.stanza_content._misc.
ModeSelected
(mode, experimentId=None, settings=None, resultDescription=None, **kwargs)¶ Bases:
silf.backend.commons.api.stanza_content._json_mapper.JsonMapped
Response after user selects mode:
>>> from silf.backend.commons.api import ControlSuite, NumberControl >>> from silf.backend.commons.api.stanza_content._misc import *
>>> c = ControlSuite( ... NumberControl("foo", "Insert foo", max_value=10), ... NumberControl("bar", "Insert bar") ... )
>>> resp = ModeSelected("foo", 'dfae1382-1746-4b5e-851e-93dff62b01ba', ... settings=c.to_input_field_suite(), ... resultDescription=OutputFieldSuite(betaArray = OutputField("array", name=["betaArray"])) ... )
>>> resp.resultDescription <OutputFieldSuite betaArray=<Result class=array name=['betaArray'] type=array metadata={} settings={}> >
>>> resp.__getstate__() == { ... 'settings': { ... 'foo': {'validations': {'max_value': 10}, 'live': False, 'metadata': {'label': 'Insert foo'}, 'name': 'foo', 'type': 'number', "order": 0}, ... 'bar': {'live': False, 'metadata': {'label': 'Insert bar'}, 'name': 'bar', 'type': 'number', 'validations': {}, "order": 1} ... }, ... 'experimentId': 'dfae1382-1746-4b5e-851e-93dff62b01ba', ... 'mode': 'foo', ... 'resultDescription': { ... 'betaArray': {'metadata': {}, 'class': 'array', 'settings': {}, 'name': ['betaArray'], 'type': 'array'}}} True
Check Equality:
>>> resp == ModeSelected.from_dict(resp.__getstate__()) True
>>> resp = ModeSelected("foo", 'dfae1382-1746-4b5e-851e-93dff62b01ba', ... settings=c, ... resultDescription=OutputFieldSuite(betaArray = OutputField("array", name=["betaArray"])) ... )
-
DICT_CONTENTS
= {'resultDescription', 'experimentId', 'mode', 'settings'}¶
-
getstate
()¶
-
setstate
(state)¶
-
-
class
silf.backend.commons.api.stanza_content._misc.
SeriesStartedStanza
(**kwargs)¶ Bases:
silf.backend.commons.api.stanza_content._json_mapper.JsonMapped
Message sent when we start the single measurement session..
>>> settings_set = SettingSuite( ... foo = Setting(value = 13), ... bar = Setting(value= "Kotek!", current=True) ... ) >>> series_started = SeriesStartedStanza(seriesId = "fac96014-e88f-4bab-af2c-32fd0cf14d20", initialSettings=settings_set, label="Seria z foo=12") >>> series_started <SeriesStartedStanza initialSettings=<SettingSuite bar=<Setting current=True value=Kotek! > foo=<Setting current=False value=13 > > metadata={'label': 'Seria z foo=12'} seriesId=fac96014-e88f-4bab-af2c-32fd0cf14d20 > >>> series_started.__getstate__() == {'initialSettings': {'foo': {'current': False, 'value': 13}, 'bar': {'current': True, 'value': 'Kotek!'}}, 'metadata': {'label': 'Seria z foo=12'}, 'seriesId': 'fac96014-e88f-4bab-af2c-32fd0cf14d20'} True
Test equality:
>>> series_started == SeriesStartedStanza.from_dict(series_started.__getstate__()) True
-
DEFAULTS
= {'metadata': {}}¶
-
DICT_CONTENTS
= {'initialSettings', 'metadata', 'seriesId'}¶
-
getstate
()¶
-
initialSettings
= <SettingSuite >¶
-
label
¶
-
seriesId
= ''¶
-
setstate
(state)¶
-
-
class
silf.backend.commons.api.stanza_content._misc.
Result
(**kwargs)¶ Bases:
silf.backend.commons.api.stanza_content._json_mapper.JsonMapped
Single result type.
>>> res = Result(value = [1, 2, 3, 4], pragma = "transient") >>> res.__getstate__() == {'pragma': 'transient', 'value': [1, 2, 3, 4]} True >>> res.__setstate__({"value" : "foobar"}) >>> res <Result pragma=append value=foobar > >>> res.__getstate__() == {'pragma': 'append', 'value': 'foobar'} True >>> res = Result(value = [[1, 1], [2, 2]], pragma="transient") >>> res.__getstate__() == {'pragma': 'transient', 'value': [[1, 1], [2, 2]]} True
-
DEFAULTS
= {'pragma': 'append'}¶
-
DEFAULT_PRAGMA
= 'append'¶
-
DICT_CONTENTS
= {'pragma', 'value'}¶
-
-
class
silf.backend.commons.api.stanza_content._misc.
ResultSuite
(**kwargs)¶ Bases:
silf.backend.commons.api.stanza_content._json_mapper.FlatDictJsonMapped
Set of results.
>>> suite = ResultSuite(foo = Result(value = [1, 2, 3, 4]), bar = Result(value = True)) >>> suite.__getstate__() == {'foo': {'value': [1, 2, 3, 4], 'pragma': 'append'}, 'bar': {'value': True, 'pragma': 'append'}} True
-
INTERNAL_DICT_NAME
= 'results'¶
-
-
class
silf.backend.commons.api.stanza_content._misc.
ResultsStanza
(**kwargs)¶ Bases:
silf.backend.commons.api.stanza_content._json_mapper.JsonMapped
Message containing a set of results sent to user.
>>> results_stanza = ResultsStanza(seriesId = "96beeb7e-3209-400a-892a-bf3201aa4658", results = ResultSuite(foo = Result(value = [1, 2, 3, 4]), bar = Result(value = True))) >>> results_stanza.__getstate__() == {'seriesId': '96beeb7e-3209-400a-892a-bf3201aa4658', 'results': {'foo': {'value': [1, 2, 3, 4], 'pragma': 'append'}, 'bar': {'value': True, 'pragma': 'append'}}} True >>> results_stanza2 = ResultsStanza.from_dict({'seriesId': '96beeb7e-3209-400a-892a-bf3201aa4658', 'results': {'foo': {'value': [1, 2, 3, 4], 'pragma': 'append'}, 'bar': {'value': True, 'pragma': 'append'}}}) >>> results_stanza == results_stanza2 True
-
DICT_CONTENTS
= {'seriesId', 'results'}¶
-
getstate
()¶
-
setstate
(state)¶
-
-
class
silf.backend.commons.api.stanza_content._misc.
CheckStateStanza
(**kwargs)¶ Bases:
silf.backend.commons.api.stanza_content._json_mapper.JsonMapped
Response for checking of experiment state.
>>> state = CheckStateStanza(state = "off") >>> state.__getstate__() == {'seriesId': None, 'state': 'off', 'experimentId': None} True >>> state = CheckStateStanza(state = "off", experimentId = 'f5d1762e-f528-4ca1-bee4-97512c22ae6a', seriesId = 'eeb23307-69dc-4745-83da-db22f3dfa557') >>> state.__getstate__() == {'seriesId': 'eeb23307-69dc-4745-83da-db22f3dfa557', 'state': 'off', 'experimentId': 'f5d1762e-f528-4ca1-bee4-97512c22ae6a'} True
Check equality:
>>> state == CheckStateStanza.from_dict(state.__getstate__()) True
-
DEFAULTS
= {'seriesId': None, 'experimentId': None}¶
-
DICT_CONTENTS
= {'experimentId', 'seriesId', 'state'}¶
-
-
class
silf.backend.commons.api.stanza_content._misc.
SeriesStopped
(**kwargs)¶ Bases:
silf.backend.commons.api.stanza_content._json_mapper.JsonMapped
Sent when someone stops series
>>> state = SeriesStopped(seriesId = "f5d1762e-f528-4ca1-bee4-97512c22ae6a") >>> state.__getstate__() == {'seriesId': 'f5d1762e-f528-4ca1-bee4-97512c22ae6a'} True
-
DEFAULTS
= {'seriesId': None}¶
-
DICT_CONTENTS
= {'seriesId'}¶
-
-
class
silf.backend.commons.api.stanza_content._misc.
VideoElement
(video_type=None, camera_url=None, label=None, **kwargs)¶ Bases:
silf.backend.commons.api.stanza_content._misc.OutputField
>>> video = VideoElement("image", "http://someserver/path") >>> video.__getstate__() == {'name': [], 'type': 'array', 'metadata': {}, 'settings': {'camera_type': 'image', 'camera_url': 'http://someserver/path'}, 'class': 'camera'} True
-
camera_type
¶
-
camera_url
¶
-
-
class
silf.backend.commons.api.stanza_content._misc.
ProtocolVersions
(version_list)¶ Bases:
silf.backend.commons.api.stanza_content._json_mapper.JsonMapped
-
DEFAULTS
= {'versions': None}¶
-
DICT_CONTENTS
= {'versions'}¶
-
-
class
silf.backend.commons.api.stanza_content._misc.
ProtocolVersion
(**kwargs)¶ Bases:
silf.backend.commons.api.stanza_content._json_mapper.JsonMapped
-
DEFAULTS
= {'version': None}¶
-
DICT_CONTENTS
= {'version'}¶
-
-
class
silf.backend.commons.api.stanza_content._misc.
Language
(lang)¶ Bases:
silf.backend.commons.api.stanza_content._json_mapper.JsonMapped
Represents single language chosen by experiment in which user data is translated
-
DICT_CONTENTS
= {'lang'}¶
-
-
class
silf.backend.commons.api.stanza_content._misc.
Languages
(langs)¶ Bases:
silf.backend.commons.api.stanza_content._json_mapper.JsonMapped
Represents stanza with list of possible languages used by user
-
DICT_CONTENTS
= {'langs'}¶
-
control Package¶
control
Package¶
_const
Module¶
_control_api
Module¶
-
class
silf.backend.commons.api.stanza_content.control._control_api.
Control
(name, label, type, style=None, default_value=None, live=False, validators=None, required=True, description=None, order=None)¶ Bases:
object
Class representing a input field in WEB UI.
>>> c = Control("foo", "Foo label", "Foo type") >>> c <Control name=foo type=Foo type live=False label='Foo label'> >>> c.to_json_dict() == {'type': 'Foo type', 'metadata': {'label': 'Foo label'}, 'live': False, 'name': 'foo', 'order': None} True >>> c.default_value=3 >>> c.to_json_dict() == {'default_value': 3, 'name': 'foo', 'live': False, 'metadata': {'label': 'Foo label'}, 'type': 'Foo type', 'order': None} True >>> c.style = "indicator" >>> c.description = "Foo Bar" >>> c.to_json_dict() == {'name': 'foo', 'default_value': 3, 'type': 'Foo type', 'live': False, 'metadata': {'style': 'indicator', 'label': 'Foo label', 'description': 'Foo Bar'}, 'order': None} True
-
convert_json_to_python
(value)¶ Converts python value to json representation of this value.
Parameters: value (object) – Unparsed value reclieved from json schema. Note
Do not override this method, rather override
_json_to_python()
as this method performs additional checks,
-
convert_python_to_json
(value, omit_validation=False)¶ Converts json value to python object.
Note
Do not override this method, rather override
_python_to_json()
as this method performs additional checks,
-
get_value_type
()¶
-
html_class
¶
-
id
= None¶ Html id of the field. Defaults to
"id_{}".format(self.name)
-
live
= None¶ Value of data-live attribute. This value governs when settings from this field are sent. Defaults to
False
-
name
¶ Html name of the field. Required.
-
post_construct_validate
()¶ Overriden to perform any post-construction validation.
-
style
= None¶ HTML class of the field. Defaults to
None
.
-
to_json_dict
()¶
-
validators
= None¶ List of validators attached to this field. Validators are python callables that should raise
silf.backend.commons.api.error.SILFProtocolError
.
-
value_is_empty
(raw_value)¶ Checks whether
raw_value
shoule be considered empty. :param object raw_value: Unparsed value reclieved from json schema. :return:
-
_controls
Module¶
-
class
silf.backend.commons.api.stanza_content.control._controls.
IntegerControl
(name, label, type='number', style=None, default_value=None, live=False, validators=None, required=True, description=None, min_value=None, max_value=None, step=None)¶ Bases:
silf.backend.commons.api.stanza_content.control._controls.NumberControl
-
class
silf.backend.commons.api.stanza_content.control._controls.
NumberControl
(name, label, type='number', style=None, default_value=None, live=False, validators=None, required=True, description=None, min_value=None, max_value=None, step=None)¶ Bases:
silf.backend.commons.api.stanza_content.control._control_api.Control
Control that take integer input, rendered as “number” field.
>>> control = NumberControl("foo", "Enter a number") >>> control.to_json_dict() == {'type': 'number', 'name': 'foo', 'metadata': {'label': 'Enter a number'}, 'live': False, 'validations': {}, 'order': None} True >>> control.min_value = 1 >>> control.max_value = 10 >>> control.description = "Foo" >>> control.style = "gauge"
>>> control.to_json_dict() == { ... 'type': 'number', ... 'validations': {'min_value': 1, 'max_value': 10}, ... 'live': False, ... 'metadata': {'label': 'Enter a number', 'description': 'Foo', 'style': 'gauge'}, ... 'name': 'foo', ... 'order': None} True
>>> control.convert_json_to_python("5") 5.0
>>> control.convert_json_to_python("5.5") 5.5
>>> control.convert_json_to_python("aaa") Traceback (most recent call last): silf.backend.commons.api.exceptions.ValidationError: Pole "Enter a number" powinno przyjmować wartość liczbową. Nie udało się skonwerować: aaa do wartości liczbowej.
>>> control.convert_json_to_python(5) 5.0 >>> control.convert_json_to_python(11) Traceback (most recent call last): silf.backend.commons.api.exceptions.ValidationError: Wartość w polu "Enter a number" powinna być mniejsza niż 10 a wynosi 11.0.
>>> control.convert_json_to_python(-1) Traceback (most recent call last): silf.backend.commons.api.exceptions.ValidationError: Wartość w polu "Enter a number" powinna być większa niż 1 a wynosi -1.0.
-
get_value_type
()¶
-
to_json_dict
()¶
-
-
class
silf.backend.commons.api.stanza_content.control._controls.
TimeControlMinSec
(name, label, type='interval', style=None, default_value=None, live=False, validators=None, required=True, description=None, max_time=None, min_time=datetime.timedelta(0))¶ Bases:
silf.backend.commons.api.stanza_content.control._control_api.Control
Control that renders to silf-proprietary input field that allows input time deltas in format MM:SS.
>>> control = TimeControlMinSec("foo", "Insert time", ... min_time=datetime.timedelta(seconds=30), ... max_time=datetime.timedelta(days=60))
>>> control.to_json_dict() == {'type': 'interval', 'validations': {'max_value': 5184000.0, 'min_value': 30.0}, 'name': 'foo', 'metadata': {'label': 'Insert time'}, 'live': False, 'order': None} True
>>> control.convert_json_to_python(75) datetime.timedelta(0, 75)
>>> control.convert_python_to_json(datetime.timedelta(days = 10)) 864000.0
>>> control.convert_json_to_python("bar") Traceback (most recent call last): silf.backend.commons.api.exceptions.ValidationError: Podaj ilość sekund jako "Insert time". Nie udało się przekształcić tej wartości: "bar" na stałą ilczbową.
>>> control.convert_json_to_python(0) Traceback (most recent call last): silf.backend.commons.api.exceptions.ValidationError: Minimalny czas w polu "Insert time" wynosi 0:00:30. Ustawiono mniejszą wartość: 0:00:00.
>>> control.convert_python_to_json(datetime.timedelta(days = 60, seconds=1)) Traceback (most recent call last): silf.backend.commons.api.exceptions.ValidationError: Maksymalny czas w polu "Insert time" wynosi 60 days, 0:00:00. Ustawiono większą wartość: 60 days, 0:00:01.
-
get_value_type
()¶
-
to_json_dict
()¶
-
-
class
silf.backend.commons.api.stanza_content.control._controls.
BooleanControl
(name, label, type='boolean', style=None, default_value=None, live=False, validators=None, required=True, description=None)¶ Bases:
silf.backend.commons.api.stanza_content.control._control_api.Control
-
get_value_type
()¶
-
-
class
silf.backend.commons.api.stanza_content.control._controls.
ComboBoxControl
(name, label, item_type=<class 'str'>, choices=None, **kwargs)¶ Bases:
silf.backend.commons.api.stanza_content.control._control_api.Control
-
choices
¶
-
post_construct_validate
()¶
-
to_json_dict
()¶
-
_suite
Module¶
-
class
silf.backend.commons.api.stanza_content.control._suite.
ControlSuite
(*args)¶ Bases:
object
-
check_and_convert_live_settings
(setting_suite: silf.backend.commons.api.stanza_content._misc.SettingSuite, old_settings: dict) → dict¶ - Checks settings and returns parsed values as a dictionaty.
>>> from silf.backend.commons.api import SettingSuite, Setting
>>> c = ControlSuite( ... NumberControl("foo", "Insert foo", max_value=10, live=True), ... NumberControl("bar", "Insert bar", required=False) ... )
>>> old_settings = {'foo': 1.0, "bar":2.0}
>>> converted = c.check_and_convert_live_settings(SettingSuite(foo=Setting(5, False)), old_settings)
>>> converted == {'foo': 5.0, 'bar': 2.0} True
>>> c.check_and_convert_live_settings(SettingSuite(bar=Setting(1, False)), old_settings) Traceback (most recent call last): silf.backend.commons.api.exceptions.ValidationError: Pole Insert bar zostało ustawione przez settings:update, ale nie zostało oznaczone jako live.
Parameters: - setting_suite –
- old_settings –
Returns:
-
check_and_convert_settings
(setting_suite)¶ Checks settings and returns parsed values as a dictionaty.
>>> from silf.backend.commons.api import SettingSuite, Setting
>>> c = ControlSuite( ... NumberControl("foo", "Insert foo", max_value=10), ... NumberControl("bar", "Insert bar", required=False) ... )
>>> i = SettingSuite( ... foo = Setting(1, False), ... bar = Setting(2, False) ... )
>>> c.check_and_convert_settings(i) == {'bar': 2, 'foo': 1} True
>>> i = SettingSuite( ... foo = Setting(11, False), ... bar = Setting(2, False) ... ) >>> c.check_and_convert_settings(i) Traceback (most recent call last): silf.backend.commons.api.exceptions.ValidationError: Wartość w polu "Insert foo" powinna być mniejsza niż 10 a wynosi 11.0.
>>> i = SettingSuite( ... bar = Setting(9, True) ... ) >>> c.check_settings(i)
-
check_settings
(setting_suite, ignore_unset_settings=True) → dict¶ Checks settings received from json and converts them to python domain.
>>> c = ControlSuite( ... NumberControl("foo", "Insert foo", max_value=10), ... NumberControl("bar", "Insert bar") ... )
>>> from silf.backend.commons.api import SettingSuite, Setting
>>> i = SettingSuite( ... foo = Setting(1, False), ... bar = Setting(2, False) ... )
>>> c.check_settings(i)
>>> i = SettingSuite( ... foo = Setting(11, True), ... bar = Setting(2, False) ... ) >>> c.check_settings(i) Traceback (most recent call last): silf.backend.commons.api.exceptions.ValidationError: Wartość w polu "Insert foo" powinna być mniejsza niż 10 a wynosi 11.0.
>>> i = SettingSuite( ... foo = Setting(9, True), ... ) >>> c.check_settings(i)
Parameters: - setting_suite (SettingSuite) – Settings read from json.
- ignore_current_parameter (bool) – Whether this is called before applying
settings (
check_only``=False) or only to check them and display errors to user. If ``check_only
is true this method might omit specific validations for fields that are not currently edited.
Raises: ValidationError – If there is a validation error.
Returns: None
-
controls
= []¶
-
classmethod
join
(*args)¶ Parameters: args – Returns:
-
to_input_field_suite
()¶ Converts this instance to
misc.InputFieldSuite
.
-
device_manager Package¶
High level overview¶
Conctete subclasses of IDeviceManager()
represent a
silf.backend.commons.device._device.Device
to
the ExperimentManager()
, it has following responsibilities:
- Manage modes of the Device
- Store and return metadata that enables construction of experiment GUI
- Aggregate and modify resuts from the Device to and return modivied results to the user.
- Optionally separate Device inside its own subprocess.
Mode management¶
DeviceManager is aware of current mode as it gets it as a
parameter for IDeviceManager.power_up()
method.
Moreover methods that return metadata, also get mode as a parameter, in this case it means: “Get metadata for appripriate mode”.
Metadata management¶
Each DeviceManager should describe input fields it needs for particular mode.
These input fields are returned from IDeviceManager.get_input_fields()
.
Output fielfs are retuned from IDeviceManager.get_output_fields()
.
Default DeviceManager implementations allow to define metadata declaratively:
see: DefaultDeviceManager
and SingleModeDeviceManager
.
Result aggregation¶
All DeviceManager concrete implementation derive from ResultMixin
that provides sensible way to aggregate results.
Basic device manager classes¶
-
class
silf.backend.commons.device_manager._device_manager.
IDeviceManager
(experiment_name, config)¶ Bases:
object
Instances of this class serve as proxy and translator between main experiment
Experiment
andDevice
.TODO: add link do the experiment
It translated unfiltered input from user to input recognizable to device, it also contains metadata needed by the experiment.
-
applicable_modes
¶ List of modes this device can be in. It is used purely for validation purposes, if we try to
power_up()
this device in a mode that is outside ofapplicable_modes
exception should be raised.If value returned is
None
it means that this device is mode agnostic,Returns: Modes applicable for this device Return type: list of strings or None.
-
apply_settings
(settings)¶ Applies settings to the device.
Parameters: settings (dict) – settings to be applied. Can contain settings that are not interesting for this device, these should be filtered out. It contains raw input from json parser, values might need to be converted to python objects. Raises: error.SILFProtocolError – if there are any errors in settings, or if there is any error during setting the equipement up.
-
check_settings
(mode, settings)¶ Checks settings for a particular mode. This method is implemented, using native validation of class:.ControlSuite, using
get_input_fields()
.Parameters: - mode (str) – Mode for which settings are checked.
- settings (
SettingSuite
) – Settings to be checked.
Raises: SILFProtocolError – When settings are invalid
-
config_section
= None¶ Section that customizes this device in config file
-
current_mode
¶ Current mode this device is in. :return: Current mode :rtype: str
-
current_result_map
¶ Current results, that is: all results produces in current series. This method returns a plain dictionary.
Returns: Current results Return type: immutable dict
-
current_results
¶ Current results, that is: all results produces in current series. This returns class that is serializable and ready to be sent. For developer friendly version use
current_result_map()
.This property is resetted during
stop()
method.Returns: Current results Return type: ResultSuite
-
current_settings
¶ Currenty applied settings.
This property is resetted during
power_down()
method.Returns: Currently applied settings. Mapping from setting name to setting value. Return type: dict
-
device_name
= 'default'¶ Name of the device. Should be unique in the experiment.
-
device_state
¶ State of device managed by this manager.
-
get_input_fields
(mode)¶ Returns _control suite for mode :param str mode: mode for which we get controls :rtype:
ControlSuite
-
get_output_fields
(mode)¶ Returns output fields for mode :param str mode: mode for which we get output fields
Returns: Returns output fields for mode Return type: OutputFieldSuite
-
has_result
(name)¶ Returns: Returns true if this instance has result named name in it’s current results. Result is considered valid if it’s value resolves to True
(not None, if collection is must be not empty, etc).Return type: bool
-
pop_results
()¶ Return new results that could be sent to user. This method mutates the state of this instance, immidiately after a call to
pop_results()
next call will return emptyResultSuite
.Returns: Recently acquired results Return type: ResultSuite
-
power_down
(suspend=False)¶ Powers down the devie, if suspend is false it also cleans up the remote device kills and remote proces.
Parameters: suspend (bool) – If False
will kill the remote process.
-
power_up
(mode)¶ Powers up the device and sets it’s mode (if applicable).
Parameters: mode (str) – Mode to initialize device in.
-
query
()¶ This method will be called periodically it should perform any neccesary communication with the device to refresh state of this instane.
It should exit in in less than 50ms.
-
running
¶ If this device is running it means:
- That
start()
was called - That calls to
perform_diagnostics()
,power_up()
,apply_settings()
,start()
should fail. - Results will be updated periodically.
If this device is not running it means:
- Results are None
Returns: Whether this device is running. Return type: bool
- That
-
start
(callback=None)¶ Starts data acquisition for this device.
Parameters: callback – Optional callable to be called when devie is started.
-
state
¶ State of this manager, may different from the
device_state
Returns: State of this manager Return type: str
-
stop
()¶ Stops data acquisition for the device.
-
tearDown
()¶ Cleans up this instance and releases all attached resources.
-
tear_down
()¶ Cleans up this instance and releases all attached resources.
-
-
class
silf.backend.commons.device_manager._device_manager.
DefaultDeviceManager
(*args, **kwargs)¶ Bases:
silf.backend.commons.device_manager._device_manager.DeclarativeDeviceMixin
,silf.backend.commons.device_manager._device_manager.DeviceWrapperMixin
To create instance of this class you need to specify following attributes:
>>> from silf.backend.commons_test.device.mock_voltimeter import MockVoltimeter >>> class TestDeviceManager(DefaultDeviceManager): ... DEVICE_CONSTRUCTOR = MockVoltimeter ... DEVICE_ID = "voltimeter1" ... CONTROL_MAP = { ... "mode1" : ControlSuite(NumberControl("foo", "Foo Control")), ... "mode2" : ControlSuite(NumberControl("foo", "Foo Control")) ... } ... OUTPUT_FIELD_MAP = { ... "mode2" : OutputFieldSuite(), ... "mode1" : OutputFieldSuite(bar = OutputField("foo", "bar")) ... } >>> mngr = TestDeviceManager("FooExperiment", None) >>> sorted(mngr.applicable_modes) ['mode1', 'mode2'] >>> mngr.get_output_fields('mode1') <OutputFieldSuite bar=<Result class=foo name=['bar'] type=array metadata={} settings={}> > >>> mngr.get_input_fields('mode2') <ControlSuite <Control name=foo type=number live=False label='Foo Control'>>
-
class
silf.backend.commons.device_manager._device_manager.
SingleModeDeviceManager
(*args, **kwargs)¶ Bases:
silf.backend.commons.device_manager._device_manager.SingleModeDeviceMixin
,silf.backend.commons.device_manager._device_manager.DeviceWrapperMixin
To create instance of this class you need to specify following attributes:
>>> from silf.backend.commons_test.device.mock_voltimeter import MockVoltimeter >>> class TestDeviceManager(SingleModeDeviceManager): ... DEVICE_CONSTRUCTOR = MockVoltimeter ... DEVICE_ID = "voltimeter1" ... CONTROLS = ControlSuite(NumberControl("foo", "Foo Control")) ... OUTPUT_FIELDS = OutputFieldSuite(bar = OutputField("foo", "bar")) >>> mngr = TestDeviceManager("FooExperiment", None) >>> mngr.applicable_modes >>> mngr.get_output_fields('any-mode') <OutputFieldSuite bar=<Result class=foo name=['bar'] type=array metadata={} settings={}> > >>> mngr.get_input_fields('any-mode') <ControlSuite <Control name=foo type=number live=False label='Foo Control'>>
-
class
silf.backend.commons.device_manager._device_manager.
ResultMixin
(*args, **kwargs)¶ Bases:
silf.backend.commons.device_manager._device_manager.IDeviceManager
This mixin handles getting results from the experiment,
Each entry in
RESULT_CREATORS
define single result that will be sent to the client. So class in example will send only two results: foo and bar, even if device itself will prowide other results.Contents of particular result fields (foo and bar) are totally dependent on result creators.
Additionaly you can override
ResultMixin._convert_result()
that allows for for more extennsive costumistaion.>>> from silf.backend.commons.device_manager import * >>> from silf.backend.commons.device_manager._result_creator import * >>> from silf.backend.commons.util.abc_utils import patch_abc
>>> p = patch_abc(ResultMixin)
>>> class TestResultMixin(ResultMixin): ... RESULT_CREATORS = [ ... AppendResultCreator("foo"), ... OverrideResultsCreator("bar") ... ] ... ... RESULTS = [] ... ... def _pop_results_internal(self): ... try: ... return self.RESULTS ... finally: ... self.RESULTS = []
>>> test = TestResultMixin(None, None)
>>> test.RESULTS = [{"foo": [1, 2, 3], "bar": 1.15}, {"foo": [4, 5], "bar" : 3.12}]
>>> test.current_results <ResultSuite bar=<Result pragma=replace value=3.12 > foo=<Result pragma=append value=[1, 2, 3, 4, 5] > >
>>> test.current_results <ResultSuite bar=<Result pragma=replace value=3.12 > foo=<Result pragma=append value=[1, 2, 3, 4, 5] > >
>>> test.pop_results() <ResultSuite bar=<Result pragma=replace value=3.12 > foo=<Result pragma=append value=[1, 2, 3, 4, 5] > >
>>> test.pop_results() <ResultSuite bar=<Result pragma=replace value=3.12 > >
>>> test.clear_current_results()
>>> test.pop_results() <ResultSuite >
>>> test.current_results <ResultSuite >
Clean up: >>> p.stop()
-
RESULT_CREATORS
= []¶ A list that maps str (name of the result recieved from the device) to instance of :class:’result_appender.ResultAggregator`.
-
_convert_result
(dict)¶ Enables to translate between device and experiment results.
-
_pop_results_internal
()¶ Returns raw results from the device. :return: list of dicts.
-
clear_current_results
()¶ Clears current results (that is after this call it is guaranteed that both
current_results
andpop_results
will be empty. :return:
-
current_results
¶ Return type: misc.ResultSuite
-
pop_results
()¶ Returns:
-
-
class
silf.backend.commons.device_manager._device_manager.
SingleModeDeviceMixin
(*args, **kwargs)¶ Bases:
silf.backend.commons.device_manager._device_manager.IDeviceManager
Mixin for single current_mode device.
-
get_input_fields
(mode)¶ Returns _control suite for current_mode :param str mode: current_mode for which we get controls (Ignored) :return: :rtype: misc.ControlSuite
-
get_output_fields
(mode)¶ Returns output fields for current_mode :param str mode: current_mode for which we get output fields (Ignored) :return: :rtype:
_control.OutputFieldSuite
-
-
class
silf.backend.commons.device_manager._device_manager.
DeclarativeDeviceMixin
(*args, **kwargs)¶ Bases:
silf.backend.commons.device_manager._device_manager.IDeviceManager
>>> from silf.backend.commons.api import * >>> from silf.backend.commons.util.abc_utils import patch_abc
>>> foo_mode = ControlSuite( ... NumberControl("foo", "Insert foo", max_value=10), ... NumberControl("bar", "Insert bar") ... )
>>> bar_mode = ControlSuite( ... NumberControl("foo", "Insert foo", max_value=10), ... NumberControl("bar", "Insert bar") ... )
>>> output_fields = _misc.OutputFieldSuite( ... bar=_misc.OutputField("foo", "bar") ... )
Enable instantiating DeclarativeDeviceMixin (it is an ABC so you shouldn’t be able to do this normallu >>> p = patch_abc(DeclarativeDeviceMixin)
>>> class DeclarativeDeviceMixinTest(DeclarativeDeviceMixin): ... CONTROL_MAP = {'foo' : foo_mode, 'bar' : bar_mode} ... OUTPUT_FIELD_MAP = { ... 'foo' : output_fields, ... 'bar' : output_fields, ... }
>>> manager = DeclarativeDeviceMixinTest(None, None)
Basic operatoons
>>> manager.get_input_fields('foo') <ControlSuite <Control name=bar type=number live=False label='Insert bar'> <Control name=foo type=number live=False label='Insert foo'>>
>>> manager.get_input_fields('bar') <ControlSuite <Control name=bar type=number live=False label='Insert bar'> <Control name=foo type=number live=False label='Insert foo'>>
>>> manager.get_input_fields("baz") Traceback (most recent call last): KeyError: 'baz'
>>> manager.get_output_fields("foo") <OutputFieldSuite bar=<Result class=foo name=['bar'] type=array metadata={} settings={}> >
Clean up: >>> p.stop()
-
get_input_fields
(mode)¶ Returns _control suite for current_mode :param str mode: current_mode for which we get controls :return: :rtype: misc.ControlSuite
-
get_output_fields
(mode)¶ Returns output fields for current_mode :param str mode: current_mode for which we get output fields :return: :rtype:
_control.OutputFieldSuite
-
-
class
silf.backend.commons.device_manager._device_manager.
DeviceWrapperMixin
(experiment_name, config)¶ Bases:
silf.backend.commons.device_manager._device_manager.ResultMixin
Device manager that wraps a device.
-
AUTOMATICALLY_POOL_RESULTS
= True¶ Boolean value. If true device will automatically pool results on each iteration.
-
BIND_STATE_TO_DEVICE_STATE
= True¶ If true state of this device will be synchronized with state of underlying device. State is updated on every call to
query()
.
-
DEVICE_CONSTRUCTOR
= None¶ Type of the device, or callable that takes single argument: the device id.
-
DEVICE_ID
= ''¶ Id of the device
-
USE_WORKER
= True¶ If this is set to true this instance will spawn device in dofferent process, and all methods will be executed asynchroneusly. If set to False methods will be executed in current thread.
Warning
This is sometimes usefull when debugging stuff, but may break stuff in weird ways. Never use it in production.
-
_apply_settings_to_device
(converted_settings)¶ Applies settings to the device. :param dict converted_settings: Result of settings conversion. :return:
-
_construct_device
()¶ Protected method that is used to construct the internal device. :return: WorkerWrapper
-
_convert_settings_to_device_format
(converted_settings)¶ Converts settings from format readable to user, to format readable to device. :param dict converted_settings: Parsed, validated, settings from the user. :return: Converted settings :rtype:
dict
-
static
_exception_translator
()¶ Wrapper that catches all
DeviceException
and raises them again asSilfProtocolError
.>>> with exception_translator(): ... raise DeviceException("Bar") Traceback (most recent call last): silf.backend.commons.api.exceptions.SILFProtocolError: Bar
>>> with exception_translator(): ... raise TypeError("Foo") Traceback (most recent call last): TypeError: Foo
Rtype None:
-
_load_settings_without_applying_to_device
(settings, live)¶ Loads settings (updates self.current_settings) :param SetingsSuite settings: :return:
-
apply_settings
(settings)¶
-
perform_diagnostics
(diagnostics_level='short')¶ See
IDeviceManager.perform_diagnostics()
.
-
power_down
(suspend=False, wait_time=10)¶ Powers down the device, and optionally kills it.
-
power_up
(mode)¶ Creates and powers up the device.
-
running
¶
-
start
(callback=None)¶
-
stop
()¶
-
tear_down
(wait_time=2)¶ Kills the device if device is present.
-
Contains classes that cope with merging results taken from the device prior to sending them to the client.
-
class
silf.backend.commons.device_manager._result_creator.
ResultCreator
(result_name, pragma='append', *, read_from=None)¶ Bases:
object
Instances of this class get results from the device and produce appropriate
Result
instances.-
aggregate_results
(results)¶ Adds results to results cache. :param dict results: dictionary containing many results values.
-
clear
()¶ Clears the results cache.
-
has_results
¶
-
pop_results
() → silf.backend.commons.api.stanza_content._misc.Result¶ Returns current set of results and possibly removes them from results cache.
Returns: Experiment results or RESULTS_UNSET if there are no new results to return. Return type: Result
orNone
-
result_name
= None¶ Name of interesting results
-
-
class
silf.backend.commons.device_manager._result_creator.
AppendResultCreator
(result_name, pragma='append', **kwargs)¶ Bases:
silf.backend.commons.device_manager._result_creator.ResultCreator
Class that aggregates results from source, and empties cache on each call to
pop_results()
.It is useful if device provides series of points, each of these points is unique, and should not be resent to client. >>> agg = AppendResultCreator(“foo”) >>> agg.has_results False >>> agg.pop_results() <Result pragma=append value=[] > >>> agg.aggregate_results({“foo” : [1, 2], “bar”: [3, 4]}) >>> agg.pop_results() <Result pragma=append value=[1, 2] > >>> agg.pop_results() <Result pragma=append value=[] > >>> agg.aggregate_results({“foo” : [1, 2], “bar”: [3, 4]}) >>> agg.aggregate_results({“foo” : [2, 3], “bar”: [3, 4]}) >>> agg.has_results True >>> agg.pop_results() <Result pragma=append value=[1, 2, 2, 3] > >>> agg.pop_results() <Result pragma=append value=[] > >>> agg.has_results False
-
aggregate_results
(results)¶
-
clear_results
(results_to_clear=0)¶
-
has_results
¶
-
pop_results
()¶
-
-
class
silf.backend.commons.device_manager._result_creator.
OverrideResultsCreator
(result_name, pragma='replace', *, read_from=None)¶ Bases:
silf.backend.commons.device_manager._result_creator.ResultCreator
Class that always returns last result. Useful for returning device state to user, and when single result compromises whole result set.
>>> agg = OverrideResultsCreator("foo") >>> agg.has_results False >>> agg.pop_results() <Result pragma=replace value=[] > >>> agg.aggregate_results({"foo": [1, 2, 3, 4, 5, 6], "bar": "ignored"}) >>> agg.has_results True >>> agg.pop_results() <Result pragma=replace value=[1, 2, 3, 4, 5, 6] > >>> agg.pop_results() <Result pragma=replace value=[1, 2, 3, 4, 5, 6] > >>> agg.aggregate_results({"foo": [7, 8, 9, 10, 11, 12], "bar": "ignored"}) >>> agg.pop_results() <Result pragma=replace value=[7, 8, 9, 10, 11, 12] > >>> agg.aggregate_results({"bar": "ignored"}) >>> agg.pop_results() <Result pragma=replace value=[7, 8, 9, 10, 11, 12] > >>> agg = OverrideResultsCreator("bar", read_from="foo") >>> agg.has_results False >>> agg.pop_results() <Result pragma=replace value=[] > >>> agg.aggregate_results({"foo": [1]}) >>> agg.pop_results() <Result pragma=replace value=[1] >
-
aggregate_results
(results)¶
-
clear
()¶
-
has_results
¶
-
pop_results
()¶
-
-
class
silf.backend.commons.device_manager._result_creator.
XYChartResultCreator
(result_name, pragma='replace', *, read_from=())¶ Bases:
silf.backend.commons.device_manager._result_creator.ResultCreator
Create this result creator
>>> creator = XYChartResultCreator(result_name="baz", read_from=['foo', 'bar'])
At the beginning it is empty
>>> creator.pop_results() <Result pragma=replace value=[] > >>> creator.has_results False
Now let’s append some results
>>> creator.aggregate_results({"foo": [1, 2, 3], "bar": [-1, -2, -3]}) >>> creator.has_results True >>> creator.pop_results() <Result pragma=replace value=[[1, -1], [2, -2], [3, -3]] >
Results are stored
>>> creator.has_results True >>> creator.pop_results() <Result pragma=replace value=[[1, -1], [2, -2], [3, -3]] >
Results are cleared after call to clear:
>>> creator.clear() >>> creator.has_results False >>> creator.pop_results() <Result pragma=replace value=[] >
After appending only one coordinate we don’t produce results
>>> creator.aggregate_results({"foo": [1, 2, 3]}) >>> creator.has_results False >>> creator.pop_results() <Result pragma=replace value=[] >
After adding second coordinate we produce them:
>>> creator.aggregate_results({"bar": [1]}) >>> creator.has_results True >>> creator.pop_results() <Result pragma=replace value=[[1, 1]] >
>>> creator = XYChartResultCreator(result_name="baz", read_from=['foo', 'bar']) >>> creator.aggregate_results({"foo": [1, 1, 1], "bar": [-1, -2, -3]}) >>> creator.pop_results() <Result pragma=replace value=[[1, -3]] >
-
aggregate_results
(results)¶
-
clear
()¶
-
has_results
¶
-
pop_results
()¶
-
-
class
silf.backend.commons.device_manager._result_creator.
ReturnLastElementResultCreator
(result_name, pragma='append', *, read_from=None)¶ Bases:
silf.backend.commons.device_manager._result_creator.ResultCreator
>>> agg = ReturnLastElementResultCreator("foo") >>> agg.has_results() False >>> agg.pop_results() <Result pragma=append value=None > >>> agg.aggregate_results({'foo' : [1, 2, 3]}) >>> agg.pop_results() <Result pragma=append value=3 > >>> agg.pop_results() <Result pragma=append value=3 >
-
aggregate_results
(results)¶
-
clear
()¶
-
has_results
()¶
-
pop_results
()¶
-
Series manager¶
This package contains contains device managers for one use-case, we want student to capture series of points, and device is able to capture only single point at a time.
-
class
silf.backend.commons.device_manager._series_manager.
ISeriesManager
(experiment_name, config)¶ Bases:
silf.backend.commons.device_manager._series_manager.AbstractSeriesManager
-
apply_settings
(settings)¶
-
get_number_of_points_in_series
(settings)¶ Calculates number of points in series
Parameters: settings (dict) – Mapping from setting name to setting value. Returns: Number of points in series. Return type: int
-
is_series_finished
¶
-
-
class
silf.backend.commons.device_manager._series_manager.
SingleModeSeriesManager
(experiment_name, config)¶ Bases:
silf.backend.commons.device_manager._series_manager.ISeriesManager
,silf.backend.commons.device_manager._device_manager.SingleModeDeviceMixin
-
class
silf.backend.commons.device_manager._series_manager.
MultiModeSeriesManager
(experiment_name, config)¶ Bases:
silf.backend.commons.device_manager._series_manager.ISeriesManager
,silf.backend.commons.device_manager._device_manager.DeclarativeDeviceMixin
Device Manger worlker module¶
This module contains plumbing that allows
Here we have defined a wrapper that creates device on separate process and allows to all methods of this device
-
class
silf.backend.commons.device_manager._worker.
DeviceWorkerWrapper
(device_id, device_constructor, multiprocess=<object object>, config_file=None)¶ Bases:
object
This a wrapper for the device it launched it works that way:
- Creates a process on this machine
- Constructs the device inside this process
- Enables more-or-less transparent operations on the remote device.
- Enables us to kill the remote process when really needed
Support for remote process should be transparent, with one important exception: all mehods are asynchroneous, for example when making a
get_results()
call you actually end up doing:- Sheduling a request to refresh results from remote side
- Reading all messages from input queue
- Returning last used results
-
append_results
(results)¶
-
auto_pull_results
= None¶ If true the experiment will automatically pull results.
-
cleanup
(callback=None)¶ Schedules a task to turn off the device kill the remote thread. :return: task_id
-
config_file
= None¶ Congig parser instance for this experiment.
-
device_constructor
= None¶ Remote process.
-
execute_task
(task, callback=None, sync=False)¶ Schedules task execution on remote process.
Parameters: - task – Task to be executed
- callback – A callable that will be called upon completion of this
task. It should accept single argument: instance of
TaskResponse
that contains the results for this task.
Returns: Task id
-
in_queue
= None¶ Queue that contains responses for tasks from remote process. Contains instances of
TaskResponse
.
-
kill
(wait_time=2)¶ Kills remote process, optionally waiting. This method will block until remote process is killed.
Parameters: wait_time (float) – Wait time in seconds. If wait_time is None, then terminate process without additional waiting. Returns: None
-
logger
¶
-
loop_iteration
(max_read=None)¶ Reads all messages from the input queue, updating self.state and self.results.
Returns: None
-
out_queue
= None¶ Queue that contains tasks to be executed on remote process, Should contain instances of
QueueTask
, though any callable with proper signature will do.
-
ping_results
(callback=None, sync=False)¶ Schedules getting of new results/ :param callback: See
execute_task()
-
pop_results
(sync=False)¶ Schedules getting of new results and then returns cached results. :return:
-
process_alive
¶ Checks if remote process is alive.
Returns: True
if remote process is alive,False
otherwiseReturn type: bool
-
purge_queue
(queue)¶ Removes all pending operations from a queue. :return: None`
-
start
(callback=None, sync=False)¶
-
start_subprocess
()¶ Starts subprocess attached to this instance.
-
state
¶ Returns most recent state of remote process. :return: State :rtype: str
-
stop
(callback=None, sync=True)¶ Schedules a task to stop the device. :param callback: See
execute_task()
:return: Returns task id
-
wait_for_response
(task_id)¶ Gets responses from queue until response for task with id equal to task_id param.
Note
This method will block until we get response for task_id
Warning
It may introduce deadlock if response for task_id has already been processed. To be sure it will not cause deadlock please execute this function just after sheduling the task. Much safer solution is to attach a callback for particular task, please see
execute_task()
.Parameters: task_id – Task id we are looking for. Returns: Response for task with id equal to task_id.
-
silf.backend.commons.device_manager._worker.
set_multiprocessing
(do_multiprocess)¶
-
silf.backend.commons.device_manager._worker.
reset_multiprocessing
()¶
-
silf.backend.commons.device_manager._worker.
start_worker_interactive
(device_id, device_class, auto_pull_results=True, configure_logging=True, config_file=None)¶
io Package¶
i2c
Module¶
-
class
silf.backend.commons.io.i2c.
DeclarativeDriver
¶ Bases:
silf.backend.commons.io.i2c.I2CDriver
-
ADDR
= None¶
-
REGISTER_MAPPER
= {}¶
-
USER_READABLE_NAME
= ''¶
-
-
class
silf.backend.commons.io.i2c.
I2CDriver
(addr)¶ Bases:
object
-
perform_diagnostics
(diagnostics_level=None)¶ Performs diangostics for this device.
This method checks whether
i2cdetect
detects address of this device.Note
Should call superclass method.
-
read_bool
(register)¶
-
read_byte
(register, min_value=None, max_value=None)¶
-
read_int
(register, num_bytes=4, fmt='<I', min_value=None, max_value=None)¶
-
read_raw
(register, num_bytes=1)¶
-
wait_until_byte
(register, desired_value, max_time=3, iter_time=0.1)¶
-
write_byte
(register, value)¶
-
-
silf.backend.commons.io.i2c.
long_transaction
(tries=5, timeout=0.5, caught_exceptions=(<class 'OSError'>, <class 'silf.backend.commons.io.i2c.I2CError'>))¶ Executes decorated function. If this function raises OSError with errno == 5 (raised by quick2wire when i2c error occours) it will wait timeout and retry. It will retry tries time. :param int tries: Maximal number of retrues :param float timeout: Timeout between retries in seconds. :return:
silf.backend.commons Package¶
silf.backend.commons
Package¶
version
Module¶
-
class
silf.backend.commons.version.
ProtocolVersionUtil
¶ Bases:
object
-
classmethod
is_version_in_list
(ver, version_list)¶ >>> ProtocolVersionUtil.is_version_in_list("1.0.0", []) False >>> ProtocolVersionUtil.is_version_in_list("1.0.0", ["0.0.1"]) False >>> ProtocolVersionUtil.is_version_in_list("1.0.0", ["0.0.1","0.5.0-0.9.0"]) False >>> ProtocolVersionUtil.is_version_in_list("1.0.0", ["1.0.0","0.5.0-0.9.0"]) True >>> ProtocolVersionUtil.is_version_in_list("1.0.0", ["0.7.0","0.9.0-1.9.0"]) True
-
classmethod
validate_protocol_version
(ver)¶ >>> ProtocolVersionUtil.validate_protocol_version("0.0.0") True
>>> ProtocolVersionUtil.validate_protocol_version("2.1.0") True
>>> ProtocolVersionUtil.validate_protocol_version("2.1.0-2.2.0") True
>>> ProtocolVersionUtil.validate_protocol_version("1.0") Traceback (most recent call last): ValueError: Wrong syntax for protocol version: 1.0 >>> ProtocolVersionUtil.validate_protocol_version("1.0.a") Traceback (most recent call last): ValueError: Wrong syntax for protocol version: 1.0.a >>> ProtocolVersionUtil.validate_protocol_version("ala") Traceback (most recent call last): ValueError: Wrong syntax for protocol version: ala >>> ProtocolVersionUtil.validate_protocol_version("2.a.0-2.2.0") Traceback (most recent call last): ValueError: Wrong syntax for protocol range: 2.a.0-2.2.0 >>> ProtocolVersionUtil.validate_protocol_version("2.1.0-2.2.bb") Traceback (most recent call last): ValueError: Wrong syntax for protocol range: 2.1.0-2.2.bb
-
classmethod
validate_protocol_versions
(version_list)¶
-
version_pattern
= re.compile('\\d+\\.\\d+\\.\\d+')¶
-
version_range_pattern
= re.compile('\\d+\\.\\d+\\.\\d+-\\d+\\.\\d+\\.\\d+')¶
-
classmethod
Subpackages¶
silf.backend.commons.util package¶
Subpackages¶
Submodules¶
silf.backend.commons.util.abc_utils module¶
>>> import abc
>>> class A(metaclass = abc.ABCMeta):
... @abc.abstractmethod
... def foo(self): pass
>>> A()
Traceback (most recent call last):
TypeError: Can't instantiate abstract class A with abstract methods foo
>>> A.__abstractmethods__=set()
>>> A()
<....A object at 0x...>
>>> class B(object): pass
>>> B()
<....B object at 0x...>
>>> B.__abstractmethods__={"foo"}
>>> B()
Traceback (most recent call last):
TypeError: Can't instantiate abstract class B with abstract methods foo
>>> class A(metaclass = abc.ABCMeta):
... @abc.abstractmethod
... def foo(self): pass
>>> from unittest.mock import patch
>>> p = patch.multiple(A, __abstractmethods__=set())
>>> p.start()
{}
>>> A()
<....A object at 0x...>
>>> p.stop()
>>> A()
Traceback (most recent call last):
TypeError: Can't instantiate abstract class A with abstract methods foo
-
silf.backend.commons.util.abc_utils.
patch_abc
(to_patch)¶
silf.backend.commons.util.config module¶
-
silf.backend.commons.util.config.
prepend_current_dir_to_config_file
(file_name, caller_frame_offset=1)¶ Todo
Use proper packaging tool, and get rid of assumption that files are laying on the fs directories.
Prepends directory name of caller file_name
Parameters: Returns: Absolute path to file
-
silf.backend.commons.util.config.
open_configfile
(*config_files)¶ Opens configfile.
Parameters: config_file (str) – Absolute path to open Returns: :class:SilfConfigParser
-
class
silf.backend.commons.util.config.
SilfConfigParser
(*args, **kwargs)¶ Bases:
configparser.ConfigParser
-
validate_mandatory_config_keys
(section, required_cofig_keys)¶
-
-
exception
silf.backend.commons.util.config.
ConfigValidationError
(msg='')¶ Bases:
configparser.Error
silf.backend.commons.util.mail module¶
-
class
silf.backend.commons.util.mail.
BorgSender
¶ Bases:
object
This is a borg class containing e-mail sender for currenly tunning expeirment we have to have global state containing e-mail sender because without it confguiring logging by ini file would be hard.
-
borg
= {}¶
-
set_sender
(sender)¶
-
-
class
silf.backend.commons.util.mail.
EmailSender
(config)¶ Bases:
object
Class that sends e-mails with errors to experiment admins.
-
email_throttling_dict
= None¶ A dictionary that maps tracebak to time when exception with this traceback was sent last time.
-
install_error_function
()¶
-
send_current_exception_to_admins
(message=None, subject=None, excinfo=None)¶
-
send_email_to_admins
(body, subject=None)¶
-
should_send_exception
(traceback)¶
-
-
class
silf.backend.commons.util.mail.
ExceptionHandler
(level=40)¶ Bases:
logging.Handler
Sends e-mail with logging messahes
-
emit
(record)¶
-
-
silf.backend.commons.util.mail.
RESEND_EXCEPTION_TIMEOUT_SEC
= 300.0¶ How long we weit between sending emails with exceptions with the same stacktrace.
silf.backend.commons.util.reflect_utils module¶
-
silf.backend.commons.util.reflect_utils.
load_item_from_module
(path)¶
-
silf.backend.commons.util.reflect_utils.
pythonpath_entries
(pythonpath_string)¶
silf.backend.commons.util.uniqueify module¶
-
silf.backend.commons.util.uniqueify.
uniquefy_last
(seq, idfun=None)¶
-
silf.backend.commons.util.uniqueify.
uniqueify
(seq, idfun=None)¶ >>> uniqueify([1, 2, 3, 1, 1, 4, 3 , 5]) [1, 2, 3, 4, 5]
Pasted from: www.peterbe.com/plog/uniqifiers-benchmark
Parameters: - seq (iterable) –
- idfun –
Returns:
Module contents¶
device Package¶
device
Package¶
device_tests
Module¶
mock_engine
Module¶
mock_voltimeter
Module¶
test_device
Module¶
device_manager Package¶
device_manager
Package¶
device_manager_tests
Module¶
mock_engine_manager
Module¶
mock_engine_tests
Module¶
mock_voltimeter_manager
Module¶
mock_voltimeter_tests
Module¶
worker_tests
Module¶
experiment Package¶
experiment
Package¶
experiment_manager_tests
Module¶
mock_experiment_manager
Module¶
mock_experiment_manager_tests
Module¶
silf.backend.experiment Package¶
experiment
Module¶
-
class
silf.backend.experiment.experiment.
ClientResetToken
¶ Bases:
object
Object of this class is put into the queue if client encounters fatal error and must be reset
-
class
silf.backend.experiment.experiment.
Experiment
(cp, ManagerClass, client: silf.backend.client.client.Client)¶ Bases:
object
-
LOOP_TIMEOUT
= 0.5¶
-
add_exception_callback
(callback)¶
-
configure
()¶
-
experiment_loop
()¶
-
initialize
()¶
-
logger
¶
-
schedule_client_reset
()¶
-
sender
= None¶ Sends emails to admins of this experiment if any error happens
-
tear_down
()¶
-
-
class
silf.backend.experiment.experiment.
ExperimentManagerCallback
(client, experiment)¶ Bases:
silf.backend.commons.experiment._experiment_manager.ExperimentCallback
-
send_experiment_done
(message=None)¶
-
send_results
(results)¶
-
send_series_done
(message=None)¶
-
-
silf.backend.experiment.experiment.
load_experiment
(cp)¶
expmain
Module¶
Utility that launches experiment.
- Usage:
- expmain.py <configfile> –test-smtp-sending expmain.py <configfile> [–no-multiprocessing] [–no-default-configfiles] [–additional-cf=<cf>]… expmain.py <configfile> [–no-multiprocessing] [–no-default-configfiles] [–additional-cf=<cf>]… debug <host> <port>
-
silf.backend.experiment.expmain.
configure_email_sender
(cp)¶
-
silf.backend.experiment.expmain.
configure_logging
(cp)¶
-
silf.backend.experiment.expmain.
main
(commands, run_loop=True)¶
-
silf.backend.experiment.expmain.
run_new
(configfile, client, ExperimentManagerClass)¶
-
silf.backend.experiment.expmain.
run_old
(cp, commands, client, ExperimentManagerClass)¶
Authors:¶
- Jacek Bzdak
<jbzdak[at]gmail.com>
– did
most of the work and maintains this. Blame this guy.
* Daniel Kowalski <kowal256[at]gmail.com>
– created XMPP
client
* Mikołaj Kubicki ....
– some fixes
Release notes¶
Version 1.4.2¶
Enchancements
You can now split experiment ini file into several files. when calling
expmain.py experiment.ini
, Multi File Config.You can now configure experiments to send e-mails with every uncaught exception, and every logging message with
error
orexception
level (latter ones contain stacktrace). See [SMTP] Section.Warning
Please beware that if you use not default logging you’ll need to add additional configuration: Experiment configuration file .
You should upgrade requirements (as SleekXMPP was changed)
Please change
host
in[Server]
section to use IP (workaround for SleekXMPP bug).
Version 1.4.1¶
Incompatible changes
If any task on the device takes longer than 30 seconds it is considered an error. This can be configured using
single_task_timeout
parameter in a device section in config file.Default is to wait for 30 seconds, so I don’t expect anyting will be broken.
Version 1.4.0¶
Incompatible changes
- Renamed all packages from
silf_backend_*
tosilf.backend.*
, you need to change- Imports of these packages
- Experiment
ini
files (if they contain references to changed classes).
IntegerControl
class is now deprecated, please useNumberControl
instead.- Diagnostics api has changed (and is at least! actually used).
perform_diagnostics
method is now deprecated. Please use:pre_power_up_diagnostics
andpost_power_up_diagnostics
.
Enchancements
- Experiment now uses randomly assigned XMPP resources.
- You can now sent chatroom messages
- We can reconnect when XMPP server fails.
Version 1.2.0¶
Incompatible changes
- You need to change client config, to reflect changes in how we configure XMPP client
Sending e-mails with errors¶
SILF now has ability to automatically send emails with:
- Uncaught exceptions
- Logged messages with error level
Configuration is described in: Experiment configuration file.
Warning
To log exceptions you must also configure logging properly.
Protocol (current previous):
Conversation in protocol v0.5¶
In this document we will define what stanzas are exchanged during the experiment.
- Operator sends silf:mode:get
- Experiment responds with message containing possible modes.
- Operator sends silf:mode:set
- Experiment sets the mode, and then sends description of the interface.
- Operator may send silf:settings:check (possibly many times), this message
contains settings set by the user. This settings will be validated
- Experiments validates the settings and sends response containing validation results
- Operator sends silf:series:start. It contains settings set by the user.
- Experiment response varies on the validity of settings:
- If settings are valid measurement series is stated.
- If settings are invalid user get’s an error and must send them via silf:series:start again
- Experiment response varies on the validity of settings:
- Experiment sends silf:results until the session ends.
- During experiment session user may change settings that are
live
(see Input fields). It should use: silf:settings:update. - Data series ends when experiment sends silf:series:stop, it may end it
in following circumstances:
- After series is finished (in this case experiment sends: silf:series:stop on its own)
- After operator requests it by sending silf:series:stop
- When experiment detects series that take to long, and drops it (shouldn’t occour).
- When experiment is ending.
- After end of series user may either:
- Start a next one
- Change mode
- End experiment
- Experiment ends when experiment server sends: silf:series:stop.
- Experiment is idle for too long time.
- Operator requests it by sending silf:series:stop.
- Bot that is operating the experiment detects end of reservation, and requests stop by sending: silf:series:stop.
SILF Protocol¶
Version note¶
This document describes protocol version 0.5 which will be implemented in silf laboratory up to version 1.0.0.
Version 1.0 of this protocol will be significantly simpler.
Namespaces¶
silf:mode:get
¶
This namespace is used query modes from the experiment.
First client sends:
>>> write(LabdataModeGet(id="query1", type="query"))
<ns0:labdata xmlns:ns0="silf:mode:get" id="query1" type="query" />
Then server responds:
>>> write(LabdataModeGet(id="query1", type="result", suite=ModeSuite(default=Mode("Default mode", "This is the default mode description"))))
<ns0:labdata xmlns:ns0="silf:mode:get" id="query1" type="result">{"default": {"label": "Default mode", "description": "This is the default mode description"}}</ns0:labdata>
Stanza content¶
Contents of silf:mode:get
in type result
are formatted as follows:
{
"mode-keyname": {
"label": "Mode label", "description": "Mode description",
"order" : "1"
},
"mode-keyname-2": {
"label": "Mode label", "description": "Mode description",
"order": "1"
}
}
Each property in this object contains description of one mode, and name of this property is name of the mode.
Modes should be sorded ascending with order
attribute, if order
is equal
sort them alphabetically.
Warning
Before we upgrade experiment software, client should not expect
order
attribute to be defined. If it is undefined sort
it so all input fields with undefined
should be below those
with defined order
.
Backend behaviour
In current implementation experiment does not change state when reclieving this message
Client behaviour
Client allows user to choose mode.
silf:mode:set
¶
This namespace is used to set mode of the experiment, and to return experiment metadata to the users.
This namespace is used query modes from the experiment.
Client sends¶
>>> write(LabdataModeSet(id="query1", type="query", suite=ModeSelection(mode="default")))
<ns0:labdata xmlns:ns0="silf:mode:set" id="query1" type="query">{"mode": "default"}</ns0:labdata>
Code sent by client contains Json object contains single property “mode” and it’s value must be “mode-keyname” (that is name of property under which particular mode is found in silf:mode:get).
Server responds¶
Server responds with metadata containing description of selected mode.
Object will have following properties:
- mode
- name of selected mode — usefull for visitor users
- experimentId
- Globally unique identifier of experiment session
- settings
- Description of user controllable input fields, described in Input fields
- resultDescription
- Description of fields that will present results to the user Output fields
Example response:
{
"experimentId": "urn:uuid:a12e6562-5feb-4046-b98f-d8c40ca1609c",
"mode": "aluminium",
"settings": {
"acquisition_time": {
"live": false, "metadata": {
"label": "Czas pomiaru dla jednej grubo\u015bci materia\u0142u"},
"name": "acquisition_time", "validations": {"min_value": 5.0, "max_value": 600.0}, "type": "interval",
"sort" : "1"},
"light": {"live": false, "metadata": {"label": "O\u015bwietlenie", "style": "light_control"}, "name": "light", "type": "boolean", "sort" : "1"}
},
"resultDescription": {
"time_left": {"name": ["tile_left"], "type": "array", "class": "interval-indicator", "settings": {}, "metadata": {"label": "Czas pozosta\u0142y do ko\u0144ca bierz\u0105cego punktu"}},
"chart": {"name": ["chart"], "type": "array", "class": "chart", "settings": {}, "metadata": {"chart.axis.y.label": "Liczba zlicze\u0144 zarejestrowana przez licznik", "chart.axis.x.label": "Grubo\u015b\u0107 przes\u0142ony [mm]"}
}
}
silf:settings:check
¶
Namespace: silf:settings:check
issed to check settings for validity,
prior to submiting them.
Client sends:
{
"acquisition_time": {"value": 150, "current": false},
"light": {"value": false, "current": false}
}
Property names in this object correspond to appropriate property names
from settings
proprerty in silf:mode:set.
Server responds either with empty tag of type result
or with an error
message (see: Errors).
When user finishes edition of a control (for example when focus is lost)
Client should send values of all controls that were touched by user
in current experiment run, and set current
to true for
control that triggered rsult sending, and to false for all others.
Settings¶
Settings themselves have following properties
value
Value of the settings. Type of the setting varies and is based on type send in silf:mode:set
current
Whether this setting was most recently updated by user. This helps to customise displayed erros and validation, in case of controls that are dependend on each other.
silf:series:start
¶
Sets all the parameters and starts the measurement series, contents sent by the client are the same as for silf:settings:check.
Note
Currently current
property in this stanza is ignored by the server.
But I think it is easier to have the same format (despite
ignored information).
Server response
Server responds either with an error: Errors, or with following structure:
{
"metadata": {"label": "Czas pomiaru 90s."},
"initialSettings": {
"acquisition_time": {"current": false, "value": 90},
"light": {"current": false, "value": false}
},
"seriesId": "urn:uuid:6bd8a024-5a8b-4f04-be84-76dcad89d89f"
}
Where properties have following meaning:
- seriesId
- Unique id of current series
- metadata
- Dictionary containing additional metadata of the series. For now only key in
this dictionary is series
label
(ie. non-unique human-readable name of the series) - initialSettings
- Structure containing settings for the series (as it was set by the user)
silf:settings:update
¶
Note
It’s a planned feaure and details might change.
During experiment session user my update input fields which are labeled as
live
(see: Input fields). In this case client should send
silf:settings:update
containing only the changed settings.
For example if only light
was changed user should send:
{
"light": {"value": false, "current": true}
}
If settings validate server should respond with
silf:settings:update
containting changed settings. If there are
erros in settings, server should respod with proper error.
silf:results
¶
Results are send as an object, where each result is sent as a distinct property, name of this property is also name of associated result.
So in following example:
{
'foo' : {value: [1, 2, 3], pragma: "append"}.
'bar' : {value: [1], pragma: "transient"}.
}
two results are sent, one named foo
other named bar
.
Single result¶
Result type has following properties:
pragma
- Controls method in which result series is reconstructed.
value
- Result value
Result series reconstruction¶
Experiment user wants to see whole result series, but in most cases we don’t want to send whole series each time (in some cases we need to!). We also don’t want to send new result stanza for each point if it is unnecessary.
Pragma property defines how series is reconstructed from series of result
messages.
We support following pragmas:
append
Initially experiment series is empty, after each result contents of
value
property is appended to series.For example if we sent following results:
{ "value": [1, 2, 3], "pragma": "append"} { "value": [4, 5], "pragma": "append"}
Series will be reconstructed as: {“value”: [1, 2, 3, 4, 5], “pragma”: “append”}
replace
- Initially experiment series is empty, after reclieving of each result we replace series contents with this result.
transient
- As
append
but will not be stored. Usefull when sending status variables.
silf:series:stop
¶
Stops the series.
User sends following object:
{
"seriesId" : "urn:uuid:6bd8a024-5a8b-4f04-be84-76dcad89d89f"
}
seriesId
property signified series to stop.
Client can omit this seriesId
property.
Experiment responds with the same object:
{
"seriesId" : "urn:uuid:6bd8a024-5a8b-4f04-be84-76dcad89d89f"
}
silf:experiment:stop
¶
Stops the experiment. User sends empty tag to the server.
Structures used in many stanzas¶
Input fields¶
Control fields contain at least following fields:
name
- name of the control, it is not visible to user, but used to in both: silf:settings:check, and silf:series:stop.
type
- Type of data this controls sends.
live
- Boolean value. Currently unused.
metatadata
- A dictionary that contains data
visble to users:
label
Label of conntrol. validations
- Dictionary containing validations ot be done at client side.
default_value
- Default value. It has the same type.
order
Used to sort input fields before they are rendered. Input fields with highter
order
should be presented on top of the input field list. In case of equalorder
fields should be sorted alphabetically onname
attribute.Warning
Before we upgrade experiment software, client should not expect
order
attribute to be defined. If it is undefined sort it so all input fields withundefined
should be below those with definedorder
.
Currently we following types of input fields:
Number control¶
It allows user to submit an integer.
It has following possible validations:
min_value
- minimal value present in the field
max_value
- maximal value present in the field
step
- increment in which to go from
min_value
tomax_value
Value rendered to JSON Format¶
Client should create values that are json number objects, or dscimal strings.
{
"number_control": {"value": 150, "current": false},
"number_control_2": {"value": "36", "current": false}
}
Example of serialized control:
>>> control = NumberControl("foo", "Enter a number")
>>> control.to_json_dict() == {
... 'type': 'number', 'name': 'foo',
... 'metadata': {'label': 'Enter a number'},
... 'live': False}
True
>>> control.min_value = 1
>>> control.max_value = 10
>>> control.to_json_dict() == {
... 'type': 'number',
... 'validations': {'min_value': 1, 'max_value': 10},
... 'live': False,
... 'metadata': {'label': 'Enter a number'}, 'name': 'foo'}
True
Boolean control¶
It allows user to submit an boolean value.
Value rendered to JSON Format¶
Client should create values that are json boolean objects (any other objects
will be passed to bool
function.
{
"number_control": {"value": 150, "current": false},
"number_control_2": {"value": "36", "current": false}
}
Time interval control¶
Allows student to submit amount of time.
{
"timedelta_control": {"value": 150, "current": false}
}
It has following validations:
min_value
- minimal amout of time student could send. In the same format as it is send from client as settings.
max_value
- maximal amount of time student can send.
Value rendered to JSON Format¶
Client should send to server amount of seconds (possibly as a float value).
ComboBox control¶
Renders a combo box.
It has no additional properties, but metadata
property must contain
choices
property containing object defining the combo box.
Keys in the choices object define values that may will be sent from this controls, and values are user readable labels.
Example:
{
'type': 'combo-box',
'live': False,
'metadata': {
'label': 'Select material',
'choices': {
'lead': 'Use lead aperture',
'cu': 'Use cooper aperture'
}
},
'name': 'material',
'default_value': 'cu'
}
This control would be rendered as a ComboBox with two choices:
Use lead aperture
and Use cooper aperture
. And if user would choose
Use cooper aperture
, this control would send:
{
...
'material: 'cu',
...
}
Output fields¶
Set of output fields is a plain object, each property denotes one output field, name of the property.
This block defines two output fields named foo
and bar
.
{
'foo': {'name': ['some_result'], ... },
'bar': {'name': ['other_result'], ... }
}
Format of object desceibing single output fields is defined in the next section.
Definitionamen of single output field¶
Output fields have following attributes:
name
names of result fields this output consumes. It is an array of strings.
Warning
Warning this is unimplemented in GUI,
names
is ignored, and follwing logic is used:If experiment reclieves:
{ 'foo': {'name': ['bar'], ... }, 'baz': {'name': ['foobar'], ... } }
Control that is under property
foo
will display results from propertyfoo
(and notbar
asname
would indicate).This will be fixed sometime!
Until then Python API disallows sending
name
that is different than control name. So currently writen code is future proof.type
- Type of the resul field. This attribute defines how this field works. A string.
class
- HTML class of result. This defines how does this field look. A string.
metadata
- Dictionary od properties of this field that are visible
settings
- Dictionary od properties of this field that are not visible to user
Applicable classes, and their behaviour¶
For now following classes are understood by the client.
Indicators, that display only the newest measurement in the series:
interval-indicator
- Renders incoming data as a time interval, signifying (for example) time left end of current measurement point. It requires for
integer-indicator
- Renders a number, for example number of registered counts on geiger counter.
Other classes:
chart
- A chart.
Indicator example¶
Example of serialized indicator:
{
'class': 'integer-indicator',
'metadata': {'label': 'Current voltage [V]'},
'name': ['current_voltage'],
'settings': {},
'type': 'array'
}
Chart example¶
Example of serialized chart:
{
'class': 'chart',
'metadata': {'chart.axis.x.label': 'Voltage between GM electrodes',
'chart.axis.y.label': 'Counts in given time interval',
'label': 'GM Counter characteristics'},
'name': ['chart'],
'settings': {},
'type': 'array'}
How are results send and displayed¶
Each output field defines from which result field (or fields) it pulls the
results, this defined in the name
property.
Errors¶
Results are send as an object with single property errors
that contains
a list of error objects.
Each error object has following properties:
severity
- A string describing how severe is this error. Following values are
acceptable:
error
,warning
,info
. error_type
- A string. Following values are accetable:
device
,user
. If value isuser
it means that user generated this error, and (so) can be fixed,device
means that there is some error in the device. metadata
Object with other error info:
message
- Message for user
field
- If it is present it is the name of an input field that is cause of this error (used when validating input fields).
Example:
{'errors':[
{
'severity':'error',
'error_type': 'user',
'metadata':{
'message': 'Invalid value in field foo',
'field': 'foo',
},{
'severity':'error',
'error_type': 'user',
'metadata':{
'message': 'Invalid value in field bar',
'field': 'bar',
}
])
Protocol (current version):
High level overviev of protocol v. 1.1 DRAFT¶
Warning
This document describes protocol after establishing version. Part of protocol for establishing version is described in Establishing protocol version.
Whole conversation is done inside labdata
tag, see: Protocol labdata stanza.
Experiment protocol¶
- Operator sends silf:lang:set along with list of preferred languages for GUI
- Experiment responds with language code which determines language tha will be used for GUI texts in following stanzas
- Operator sends silf:mode:get
- Experiment responds with message containing possible modes.
- Operator sends silf:mode:set
- Experiment sets the mode, and then sends description of the interface.
- Operator may send silf:settings:check (possibly many times), this message
contains settings set by the user. This settings will be validated
- Experiments validates the settings and sends response containing validation results
- Operator sends silf:series:start. It contains settings set by the user.
- Experiment response varies on the validity of settings:
- If settings are valid measurement series is stated.
- If settings are invalid user get’s an error and must send them via silf:series:start again
- Experiment response varies on the validity of settings:
- Experiment sends silf:results until the session ends.
- During experiment session user may change settings that are
live
(see Input fields). It should use: silf:settings:update. - Data series ends when experiment sends silf:series:stop, it may end it
in following circumstances:
- After series is finished (in this case experiment sends: silf:series:stop on its own)
- After operator requests it by sending silf:series:stop
- When experiment detects series that take to long, and drops it (shouldn’t occour).
- When experiment is ending.
- After end of series user may either:
- Start a next one
- Change mode
- End experiment
- Experiment ends when experiment server sends: silf:series:stop.
- Experiment is idle for too long time.
- Operator requests it by sending silf:series:stop.
- Bot that is operating the experiment detects end of reservation, and requests stop by sending: silf:series:stop.
Joining groupchat late¶
Note
This is implemented in protocol version 1.1.0 and above.
When an user (irrevelant whether he is operator or not) joins groupchat he is updated with current experiment state.
First thing he gets information about protocol version used, see: version_selection. Then Athena bot updates client’s state in the following manner:
- First Athena sends current series using following namespaces:
These messages have the same content and metadata as messages that experiment sent during normal (i.e. not joining late) operation.
If any of these messages were not sent during current session they are not sent now either.
See also: Result compression
After sending all above stanzas (that is syncing with current experiment state) Athena should give voice (allow him to write to the room) to user if he is an operator.
Then athena sends results from finished series belonging to current session. Each session is sent by sending three stanzas:
- silf:mode:historical this one sends controls and result field description
- silf:series:historical this one sends settings
- silf:results:historical this one sends results for the series.
All three stanzas should be send consecutively, however client can join them by comparing
experimentId
andseriesId
.See also: Result compression
Note
Results compression There is no requirement to send archival results in single stanza, but athena should compress these results so data is send in concise way.
SILF Protocol version 1.1 DRAFT¶
Version note¶
This document describes protocol version 1.1.0
Namespaces¶
silf:lang:set
¶
This namespace is used to set preferred language for the user interface texts send by experiment server. So content contains ordered list of preferred languages by user.
First client sends:
<ns0:labdata xmlns:ns0="silf:lang:set" id="query1" type="query">
{
"langs": ["en", "pl"]
}
</ns0:labdata>
Then server responds:
<ns0:labdata xmlns:ns0="silf:lang:set" id="query1" type="result">
{
"lang": "en"
}
</ns0:labdata>
Response contains single language code. This language will be used in further communication in texts which are provided by experiment server for GUI.
silf:mode:get
¶
This namespace is used query modes from the experiment.
First client sends:
>>> write(LabdataModeGet(id="query1", type="query"))
<ns0:labdata xmlns:ns0="silf:mode:get" id="query1" type="query" />
Then server responds:
>>> write(LabdataModeGet(id="query1", type="result", suite=ModeSuite(default=Mode("Default mode", "This is the default mode description"))))
<ns0:labdata xmlns:ns0="silf:mode:get" id="query1" type="result">{"default": {"label": "Default mode", "description": "This is the default mode description"}}</ns0:labdata>
Stanza content¶
Contents of silf:mode:get
in type result
are formatted as follows:
{
"mode-keyname": {
"label": "Mode label", "description": "Mode description",
"order" : "1"
},
"mode-keyname-2": {
"label": "Mode label", "description": "Mode description",
"order": "1"
}
}
Each property in this object contains description of one mode, and name of this property is name of the mode.
Modes should be sorded ascending with order
attribute, if order
is equal
sort them alphabetically.
Warning
Before we upgrade experiment software, client should not expect
order
attribute to be defined. If it is undefined sort
it so all input fields with undefined
should be below those
with defined order
.
Backend behaviour
In current implementation experiment does not change state when reclieving this message
Client behaviour
Client allows user to choose mode.
silf:mode:set
¶
This namespace is used to set mode of the experiment, and to return experiment metadata to the users.
This namespace is used query modes from the experiment.
Client sends¶
>>> write(LabdataModeSet(id="query1", type="query", suite=ModeSelection(mode="default")))
<ns0:labdata xmlns:ns0="silf:mode:set" id="query1" type="query">{"mode": "default"}</ns0:labdata>
Code sent by client contains Json object contains single property “mode” and it’s value must be “mode-keyname” (that is name of property under which particular mode is found in silf:mode:get).
Server responds¶
Server responds with metadata containing description of selected mode.
Object will have following properties:
- mode
- name of selected mode — usefull for visitor users
- experimentId
- Globally unique identifier of experiment session
- settings
- Description of user controllable input fields, described in Input fields
- resultDescription
- Description of fields that will present results to the user Output fields
Example response:
{
"experimentId": "urn:uuid:a12e6562-5feb-4046-b98f-d8c40ca1609c",
"mode": "aluminium",
"settings": {
"acquisition_time": {
"live": false, "metadata": {
"label": "Czas pomiaru dla jednej grubo\u015bci materia\u0142u"},
"name": "acquisition_time", "validations": {"min_value": 5.0, "max_value": 600.0}, "type": "interval",
"sort" : "1"},
"light": {"live": false, "metadata": {"label": "O\u015bwietlenie", "style": "light_control"}, "name": "light", "type": "boolean", "sort" : "1"}
},
"resultDescription": {
"time_left": {"name": ["tile_left"], "type": "array", "class": "interval-indicator", "settings": {}, "metadata": {"label": "Czas pozosta\u0142y do ko\u0144ca bierz\u0105cego punktu"}},
"chart": {"name": ["chart"], "type": "array", "class": "chart", "settings": {}, "metadata": {"chart.axis.y.label": "Liczba zlicze\u0144 zarejestrowana przez licznik", "chart.axis.x.label": "Grubo\u015b\u0107 przes\u0142ony [mm]"}
}
}
silf:mode:historical
¶
Has the same contents as silf:mode:set, but sends interface details for historical series.
silf:settings:check
¶
Namespace: silf:settings:check
issed to check settings for validity,
prior to submiting them.
Client sends:
{
"acquisition_time": {"value": 150, "current": false},
"light": {"value": false, "current": false}
}
Property names in this object correspond to appropriate property names
from settings
proprerty in silf:mode:set.
Server responds either with empty tag of type result
or with an error
message (see: Errors).
When user finishes edition of a control (for example when focus is lost)
Client should send values of all controls that were touched by user
in current experiment run, and set current
to true for
control that triggered rsult sending, and to false for all others.
Settings¶
Settings themselves have following properties
value
Value of the settings. Type of the setting varies and is based on type send in silf:mode:set
current
Whether this setting was most recently updated by user. This helps to customise displayed erros and validation, in case of controls that are dependend on each other.
silf:series:start
¶
Sets all the parameters and starts the measurement series, contents sent by the client are the same as for silf:settings:check.
Note
Currently current
property in this stanza is ignored by the server.
But I think it is easier to have the same format (despite
ignored information).
Server response
Server responds either with an error: Errors, or with following structure:
{
"metadata": {"label": "Czas pomiaru 90s."},
"initialSettings": {
"acquisition_time": {"current": false, "value": 90},
"light": {"current": false, "value": false}
},
"seriesId": "urn:uuid:6bd8a024-5a8b-4f04-be84-76dcad89d89f",
"experimentId": "urn:uuid:a12e6562-5feb-4046-b98f-d8c40ca1609c"
}
Where properties have following meaning:
seriesId
- Unique id of current series
experimentId
- Unique id of experiment session to which this series belongs.
metadata
- Dictionary containing additional metadata of the series. For now only key in
this dictionary is series
label
(ie. non-unique human-readable name of the series) initialSettings
- Structure containing settings for the series (as it was set by the user)
silf:series:historical
¶
Has the same contents as silf:series:start but is used by the athena bot to send historical results to the client.
silf:settings:update
¶
Note
It’s a planned feaure and details might change.
During experiment session user my update input fields which are labeled as
live
(see: Input fields). In this case client should send
silf:settings:update
containing only the changed settings.
For example if only light
was changed user should send:
{
"light": {"value": false, "current": true}
}
If settings validate server should respond with
silf:settings:update
containting changed settings. If there are
erros in settings, server should respod with proper error.
silf:results
¶
Results are send as an object, where each result is sent as a distinct property, name of this property is also name of associated result.
So in following example:
{
'sessionId': "urn:uuid:6bd8a024-5a8b-4f04-be84-76dcad89d89f"
'foo' : {value: [1, 2, 3], pragma: "append"}.
'bar' : {value: [1], pragma: "transient"}.
}
two results are sent, one named foo
other named bar
.
Additionaly we send sessionId
property which signifies to which
series this result series belong
Single result¶
Result type has following properties:
pragma
- Controls method in which result series is reconstructed.
value
- Result value
Result series reconstruction¶
Experiment user wants to see whole result series, but in most cases we don’t want to send whole series each time (in some cases we need to!). We also don’t want to send new result stanza for each point if it is unnecessary.
Pragma property defines how series is reconstructed from series of result
messages.
We support following pragmas:
append
Initially experiment series is empty, after each result contents of
value
property is appended to series.For example if we sent following results:
{ "value": [1, 2, 3], "pragma": "append"} { "value": [4, 5], "pragma": "append"}
Series will be reconstructed as: {“value”: [1, 2, 3, 4, 5], “pragma”: “append”}
replace
- Initially experiment series is empty, after reclieving of each result we replace series contents with this result.
transient
- As
append
but will not be stored. Usefull when sending status variables.
silf:results:historical
¶
Has the same contents as as silf:results, but ut is used by the athena bot to send historical results to the client.
silf:series:stop
¶
Stops the series.
User sends following object:
{
"seriesId" : "urn:uuid:6bd8a024-5a8b-4f04-be84-76dcad89d89f"
}
seriesId
property signified series to stop.
Client can omit this seriesId
property.
Experiment responds with the same object:
{
"seriesId" : "urn:uuid:6bd8a024-5a8b-4f04-be84-76dcad89d89f"
}
silf:experiment:stop
¶
Stops the experiment. User sends empty tag to the server.
Structures used in many stanzas¶
Input fields¶
Control fields contain at least following fields:
name
- name of the control, it is not visible to user, but used to in both: silf:settings:check, and silf:series:stop.
type
- Type of data this controls sends.
live
- Boolean value. Currently unused.
metatadata
- A dictionary that contains data
visble to users:
label
Label of conntrol. validations
- Dictionary containing validations ot be done at client side.
default_value
- Default value. It has the same type.
order
Used to sort input fields before they are rendered. Input fields with highter
order
should be presented on top of the input field list. In case of equalorder
fields should be sorted alphabetically onname
attribute.Warning
Before we upgrade experiment software, client should not expect
order
attribute to be defined. If it is undefined sort it so all input fields withundefined
should be below those with definedorder
.
Currently we following types of input fields:
Number control¶
It allows user to submit an integer.
It has following possible validations:
min_value
- minimal value present in the field
max_value
- maximal value present in the field
step
- increment in which to go from
min_value
tomax_value
Value rendered to JSON Format¶
Client should create values that are json number objects, or dscimal strings.
{
"number_control": {"value": 150, "current": false},
"number_control_2": {"value": "36", "current": false}
}
Example of serialized control:
>>> control = NumberControl("foo", "Enter a number")
>>> control.to_json_dict() == {
... 'type': 'number', 'name': 'foo',
... 'metadata': {'label': 'Enter a number'},
... 'live': False}
True
>>> control.min_value = 1
>>> control.max_value = 10
>>> control.to_json_dict() == {
... 'type': 'number',
... 'validations': {'min_value': 1, 'max_value': 10},
... 'live': False,
... 'metadata': {'label': 'Enter a number'}, 'name': 'foo'}
True
Boolean control¶
It allows user to submit an boolean value.
Value rendered to JSON Format¶
Client should create values that are json boolean objects (any other objects
will be passed to bool
function.
{
"number_control": {"value": 150, "current": false},
"number_control_2": {"value": "36", "current": false}
}
Time interval control¶
Allows student to submit amount of time.
{
"timedelta_control": {"value": 150, "current": false}
}
It has following validations:
min_value
- minimal amout of time student could send. In the same format as it is send from client as settings.
max_value
- maximal amount of time student can send.
Value rendered to JSON Format¶
Client should send to server amount of seconds (possibly as a float value).
ComboBox control¶
Renders a combo box.
It has no additional properties, but metadata
property must contain
choices
property containing object defining the combo box.
Keys in the choices object define values that may will be sent from this controls, and values are user readable labels.
Example:
{
'type': 'combo-box',
'live': False,
'metadata': {
'label': 'Select material',
'choices': {
'lead': 'Use lead aperture',
'cu': 'Use cooper aperture'
}
},
'name': 'material',
'default_value': 'cu'
}
This control would be rendered as a ComboBox with two choices:
Use lead aperture
and Use cooper aperture
. And if user would choose
Use cooper aperture
, this control would send:
{
...
'material: 'cu',
...
}
Output fields¶
Set of output fields is a plain object, each property denotes one output field, name of the property.
This block defines two output fields named foo
and bar
.
{
'foo': {'name': ['some_result'], ... },
'bar': {'name': ['other_result'], ... }
}
Format of object desceibing single output fields is defined in the next section.
Definitionamen of single output field¶
Output fields have following attributes:
name
names of result fields this output consumes. It is an array of strings.
Warning
Warning this is unimplemented in GUI,
names
is ignored, and follwing logic is used:If experiment reclieves:
{ 'foo': {'name': ['bar'], ... }, 'baz': {'name': ['foobar'], ... } }
Control that is under property
foo
will display results from propertyfoo
(and notbar
asname
would indicate).This will be fixed sometime!
Until then Python API disallows sending
name
that is different than control name. So currently writen code is future proof.type
- Type of the resul field. This attribute defines how this field works. A string.
class
- HTML class of result. This defines how does this field look. A string.
metadata
- Dictionary od properties of this field that are visible
settings
- Dictionary od properties of this field that are not visible to user
Applicable classes, and their behaviour¶
For now following classes are understood by the client.
Indicators, that display only the newest measurement in the series:
interval-indicator
- Renders incoming data as a time interval, signifying (for example) time left end of current measurement point. It requires for
integer-indicator
- Renders a number, for example number of registered counts on geiger counter.
Other classes:
chart
- A chart.
Indicator example¶
Example of serialized indicator:
{
'class': 'integer-indicator',
'metadata': {'label': 'Current voltage [V]'},
'name': ['current_voltage'],
'settings': {},
'type': 'array'
}
Chart example¶
Example of serialized chart:
{
'class': 'chart',
'metadata': {'chart.axis.x.label': 'Voltage between GM electrodes',
'chart.axis.y.label': 'Counts in given time interval',
'label': 'GM Counter characteristics'},
'name': ['chart'],
'settings': {},
'type': 'array'}
How are results send and displayed¶
Each output field defines from which result field (or fields) it pulls the
results, this defined in the name
property.
Errors¶
Results are send as an object with single property errors
that contains
a list of error objects.
Each error object has following properties:
severity
- A string describing how severe is this error. Following values are
acceptable:
error
,warning
,info
. error_type
- A string. Following values are accetable:
device
,user
. If value isuser
it means that user generated this error, and (so) can be fixed,device
means that there is some error in the device. metadata
Object with other error info:
message
- Message for user
field
- If it is present it is the name of an input field that is cause of this error (used when validating input fields).
Example:
{'errors':[
{
'severity':'error',
'error_type': 'user',
'metadata':{
'message': 'Invalid value in field foo',
'field': 'foo',
},{
'severity':'error',
'error_type': 'user',
'metadata':{
'message': 'Invalid value in field bar',
'field': 'bar',
}
])
Establishing protocol version¶
Version is negotiated between experiment and experiment operator. Version is
established from when experiment sends silf:protocol:version:set
until
experiment is finished.
Note
Currently experiment is finished when experiment sends
silf:experiment:stop
with type done
. But precise moment may depend
on protocol version.
Version values¶
We use semantic versioning v. 2.0.0 to version the protocol.
Experiment may specify that it supports version ranges that have following syntax:
1.1.1-2.0.0
which specifies versions from (including) 1.1.1
up to
(excluding) 2.0.0
.
Conversation¶
In this document we will define what stanzas are exchanged during the
protocol negotiation. All communication is done inside labdata
messages, :see:`proto-1.0-labdata`.
Happy path
- Operator sends
labdata
:silf:protocol:version:get
- Server responds with :
silf:protocol:version:get
that contains list of avilable versions.
Operetor sends:
silf:protocol:version:set
, this stanza contains selected version.Server responds with:
silf:protocol:version:set
with version selected by client.From now on version is set.
Other proper paths
Operator does not need to check avilable versions from the server, it can
just send silf:protocol:version:set
and hope for the best, if he sent
version that can’t be handled by the experiment, experiment will send
and error
stanza.
Error conditions¶
Folowing error conditions are possible:
- Client sends version that is not acceptable for the experiment (was not in
avilable version list). In this case experiment sends
silf:protocol:version:set
with type:error
. - Client can’t handle any version sent by the experiment. In this case
client does not send
silf:protocol:version:set
and just disconects from the experiment room. - Client sends any other stanza not specified by the protocol. In this case
experiment sends appropriate
error
stanza, or just ignores the message.
Joining experiment after version is established¶
If observer joins the room after session negotiation he is informed about protocol version by the athena bot before he joins the room.
If experiment operator joins the room after session negotiation has taken place Athena bot should send experiment version before operator is given voice in the room. If the operator is given voice in the room, and he didn’t get this information he can start version negotiation.
In both cases athena bot sends silf:protocol:version:set
with type
result
and the same content that experiment sent earlier.
Stanza content¶
silf:protocol:version:get
¶
Client sends empty labdata
with type query
.
Server responds with a message of type result
, and
list of protocol versions this experiment supports,
in the following list:
{
"versions": ["1.0", "1.0.2", "0.0.0-0.6.0"]
}
silf:protocol:version:set
¶
Client sends labdata
with type query
and sends selected version:
{
"version": "0.5.9"
}
Server responds with labdata
with type result
and the same content.
From now on up to the moment when experiment closes the experiment session protocol version siginified in before mentioned stanza is used.
If client sends improper version (one not present in list of versions), experiment responds with error.
Note
Server responds by confirming selected version just so all elements of the system can know version just by checking single stanza.
Example conversation¶
<message from='exp@muc.ilf/operator' to='exp@muc.ilf' type='groupchat'>
<labdata xmlns="silf:protocol:version:get" type="query" id="q1"/>
</message>
<message from='exp@muc.ilf/experiment' to='exp@muc.ilf' type='groupchat'>
<labdata xmlns="silf:protocol:version:get" type="result" id="q1">
{
"versions": ["1.0", "1.0.2", "0.5"]
}
</labdata>
</message>
<message from='exp@muc.ilf/operator' to='exp@muc.ilf' type='groupchat'>
<labdata xmlns="silf:protocol:version:set" type="query" id="q2">
{
"version": "1.0"
}
</labdata>
</message>
<message from='exp@muc.ilf/experiment' to='exp@muc.ilf' type='groupchat'>
<labdata xmlns="silf:protocol:version:set" type="result" id="q2">
{
"version": "1.0"
}
</labdata>
</message>