A complete simple taskΒΆ

Having done pip install whisker, you should be able to do this:

# demo_config.yaml

# The URL is of the SQLAlchemy type.
# http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls
# Here we'll use a SQLite file. For bigger purposes, consider MySQL etc.

database_url: sqlite:///demo_task_results.db

server: localhost
# server: wombatvmxp

# Let's include a comma to check the CSV output
subject: Mr Potato, Head
session: 5
#!/usr/bin/env python
# demo_simple_whisker_client.py

# =============================================================================
# Imports, and configure logging
# =============================================================================

import logging
from attrdict import AttrDict
from datetime import datetime
from twisted.internet import reactor
from whisker.logging import configure_logger_for_colour
from whisker.constants import DEFAULT_PORT
from whisker.convenience import (load_config_or_die,
                                 connect_to_db_using_attrdict,
                                 insert_and_set_id,
                                 ask_user,
                                 save_data)
from whisker.twistedclient import WhiskerTwistedTask

log = logging.getLogger(__name__)
configure_logger_for_colour(log)

log.setLevel(logging.DEBUG)  # debug-level logging for this file...
logging.getLogger("whisker").setLevel(logging.DEBUG)  # ... and for Whisker

# =============================================================================
# Constants
# =============================================================================

TASKNAME_SHORT = "countpings"  # no spaces; we'll use it in a filename
TASKNAME_LONG = "Ping Counting Task"

# Our tables. They will be autocreated. (NOTE: do not store separate copies of
# table objects, as they can get out of sync as new columns area created.)
SESSION_TABLE = 'session'
TRIAL_TABLE = 'trial'
SUMMARY_TABLE = 'summary'


# =============================================================================
# The task itself
# =============================================================================

class MyWhiskerTwistedTask(WhiskerTwistedTask):
    def __init__(self, config, db, session):
        """Here, we initialize the task, and store any relevant variables."""
        super().__init__()  # call base class init
        self.config = config
        self.db = db
        self.session = session
        self.trial_num = 0

    def fully_connected(self):
        """At this point, we are fully connected to the Whisker server."""
        print("Task running.")
        period_ms = 1000
        self.whisker.timestamps(True)
        self.whisker.report_name(TASKNAME_LONG)
        self.whisker.get_network_latency_ms()
        self.whisker.timer_set_event("TimerFired", period_ms,
                                     self.session.num_pings - 1)
        self.whisker.timer_set_event("EndOfTask",
                                     period_ms * (self.session.num_pings + 1))

    def incoming_event(self, event, timestamp=None):
        """An event has arrived from the Whisker server."""
        # timestamp is the Whisker server's clock time in ms; we want real time
        now = datetime.utcnow()
        print("Event: {e} (timestamp {t}, real time {n})".format(
            e=event, t=timestamp, n=now.isoformat()))
        # We could do lots of things at this point. But let's keep it simple:
        if event == "EndOfTask":
            # noinspection PyUnresolvedReferences
            reactor.stop()  # stops Twisted and thus network processing
        else:
            trial = AttrDict(
                session_id=self.session.id,  # important foreign key
                trial_num=self.trial_num,
                event="Ping!",
                received=True,  # now we're just making things up...
                when=now,
            )
            insert_and_set_id(self.db[TRIAL_TABLE], trial)  # save to database
            self.trial_num += 1
            log.info("{} pings received so far".format(self.trial_num))


# =============================================================================
# Main execution sequence
# =============================================================================

def main():
    # -------------------------------------------------------------------------
    # Load config; establish database connection; ask the user for anything else
    # -------------------------------------------------------------------------

    log.info("Asking user for config filename")
    config = load_config_or_die(
        mandatory=['database_url'],
        defaults=dict(server='localhost', port=DEFAULT_PORT),
        log_config=True  # send to console (beware security of database URLs)
    )
    db = connect_to_db_using_attrdict(config.database_url)

    # Any additional user input required?
    num_pings = ask_user("Number of pings", default=10, type=int, min=1)
    ask_user("Irrelevant: Heads or tails", default='H', options=['H', 'T'])

    # -------------------------------------------------------------------------
    # Set up task and go
    # -------------------------------------------------------------------------

    session = AttrDict(start=datetime.now(),
                       subject=config.subject,
                       session=config.session,
                       num_pings=num_pings)
    insert_and_set_id(db[SESSION_TABLE], session)  # save to database
    log.info("Off we go...")
    task = MyWhiskerTwistedTask(config, db, session)
    task.connect(config.server, config.port)
    # noinspection PyUnresolvedReferences
    reactor.run()  # starts Twisted and thus network processing
    log.info("Finished.")

    # -------------------------------------------------------------------------
    # Done. Calculate summaries. Save data from this session to new CSV files.
    # -------------------------------------------------------------------------

    # Retrieve all our trials. (There may also be many others in the database.)
    # NOTE that find() returns an iterator (you get to iterate through it ONCE).
    # Since we want to use this more than once (below), use a list.
    trials = list(db[TRIAL_TABLE].find(session_id=session.id))

    # Calculate some summary measures
    summary = AttrDict(
        session_id=session.id,  # foreign key
        n_pings_received=sum(t.received for t in trials)
    )
    insert_and_set_id(db[SUMMARY_TABLE], summary)  # save to database

    # Save data. (Since the session and summary objects are single objects, we
    # encapsulate them in a list.)
    save_data("session", [session], timestamp=session.start,
              taskname=TASKNAME_SHORT)
    save_data("trial", trials, timestamp=session.start,
              taskname=TASKNAME_SHORT)
    save_data("summary", [summary], timestamp=session.start,
              taskname=TASKNAME_SHORT)


if __name__ == '__main__':
    main()