https://github.com/NYUCCL/psiTurk
Raw File
Tip revision: a8345d1ad039f11d43ea8b13129c5f2cb8e8f28d authored by Dave Eargle on 15 February 2017, 15:23:26 UTC
bumping version
Tip revision: a8345d1
psiturk_org_services.py
# -*- coding: utf-8 -*-
""" This module """

import os
import urllib2
import json
import requests
from psiturk.version import version_number
import git
import subprocess
import signal
import struct
from sys import platform as _platform
from psiturk.psiturk_config import PsiturkConfig
import psutil


class PsiturkOrgServices(object):
    """
        PsiturkOrgServices this class provides an interface to the API provided
        by the psiturk_org website. The two main features of this API are
        registering secure ads and providing tunnel access see:
        https://github.com/NYUCCL/api-psiturk-org
    """
    def __init__(self, key, secret):

        # 'https://api.psiturk.org' # by default for now
        self.api_server = 'https://api.psiturk.org'
        self.ad_server = 'https://ad.psiturk.org'
        self.sandbox_ad_server = 'https://sandbox.ad.psiturk.org'
        self.update_credentials(key, secret)
        if not self.check_credentials():
            print 'WARNING *****************************'
            print 'Sorry, psiTurk Credentials invalid.\nYou will only be able '\
                + 'to test experiments locally until you enter\nvalid '\
                + 'credentials in the psiTurk Access section of ' \
                + '~/.psiturkconfig.\n  Get your credentials at '\
                + 'https://www.psiturk.org/login.\n'

    def check_credentials(self):
        ''' Check credentials '''
        req = requests.get(self.api_server + '/api/ad',
                           auth=(self.access_key, self.secret_key))
        # Not sure 500 server error should be included here
        if req.status_code in [401, 403, 500]:
            return False
        else:
            return True

    def update_credentials(self, key, secret):
        ''' Update credentials '''
        self.access_key = key
        self.secret_key = secret

    def connect(self, server):
        """
            connect:
                "connects to server"  since the is a fairly
            basic API, just allows overriding of which Ad server
            you are talking to
        """
        self.api_server = server

    def get_system_status(self):
        """
            get_system_status:
        """
        try:
            api_server_status_link = self.api_server + '/status_msg?version=' +\
                version_number
            response = urllib2.urlopen(api_server_status_link, timeout=1)
            status_msg = json.load(response)['status']
        except urllib2.HTTPError:
            status_msg = "Sorry, can't connect to psiturk.org, please check\
                your internet connection.\nYou will not be able to create new\
                hits, but testing locally should work.\n"
        return status_msg

    @classmethod
    def get_my_ip(cls):
        """
            Asks and external server what your ip appears to be (useful is
            running from behind a NAT/wifi router).  Of course, incoming port
            to the router must be forwarded correctly.
        """
        if 'OPENSHIFT_SECRET_TOKEN' in os.environ:
            my_ip = os.environ['OPENSHIFT_APP_DNS']
        else:
            my_ip = json.load(urllib2.urlopen(
                'http://httpbin.org/ip'
            ))['origin']
        return my_ip

    def create_record(self, name, content, username, password):
        ''' Create record '''
        #headers = {'key': username, 'secret': password}
        req = requests.post(self.api_server + '/api/' + name,
                            data=json.dumps(content), auth=(username, password))
        return req

    def update_record(self, name, recordid, content, username, password):
        ''' Update record '''
        # headers = {'key': username, 'secret': password}
        req = requests.put(self.api_server + '/api/' + name + '/' +
                           str(recordid), data=json.dumps(content),
                           auth=(username, password))
        return req

    def delete_record(self, name, recordid, username, password):
        ''' Delete record '''
        #headers = {'key': username, 'secret': password}
        req = requests.delete(self.api_server + '/api/' + name + '/' +
                              str(recordid), auth=(username, password))
        return req

    def query_records(self, name, username, password, query=''):
        ''' Query records '''
        #headers = {'key': username, 'secret': password}
        req = requests.get(self.api_server + '/api/' + name + "/" + query,
                           auth=(username, password))
        return req

    def get_ad_url(self, ad_id, sandbox):
        """
            get_ad_url:
            gets ad server thing
        """
        if sandbox:
            return self.sandbox_ad_server + '/view/' + str(ad_id)
        else:
            return self.ad_server + '/view/' + str(ad_id)

    def set_ad_hitid(self, ad_id, hit_id, sandbox):
        """
            get_ad_hitid:
            updates the ad with the corresponding hitid
        """
        if sandbox:
            req = self.update_record('sandboxad', ad_id, {'amt_hit_id':hit_id},
                                     self.access_key, self.secret_key)
        else:
            req = self.update_record('ad', ad_id, {'amt_hit_id':hit_id},
                                     self.access_key, self.secret_key)
        if req.status_code == 201:
            return True
        else:
            return False

    def create_ad(self, ad_content):
        """
            create_ad:
        """
        if not 'is_sandbox' in ad_content:
            return False
        else:
            if ad_content['is_sandbox']:
                req = self.create_record(
                    'sandboxad', ad_content, self.access_key, self.secret_key
                )
            else:
                req = self.create_record(
                    'ad', ad_content, self.access_key, self.secret_key
                )
            if req.status_code == 201:
                return req.json()['ad_id']
            else:
                return False

    def download_experiment(self, experiment_id):
        """
            download_experiment:
        """
        req = self.query_records('experiment', self.access_key,
                                 self.secret_key,
                                 query='download/'+experiment_id)
        print req.text
        return


class ExperimentExchangeServices(object):
    """
        ExperimentExchangeServices
        this class provides a non-authenticated interface to the API provided
        by the psiturk_org website.  the feature is interfacing with the
        experiment exchange see: https://github.com/NYUCCL/api-psiturk-org
    """
    def __init__(self):
        # 'https://api.psiturk.org' # by default for now
        self.api_server = 'https://api.psiturk.org'

    def query_records_no_auth(self, name, query=''):
        ''' Query records without authorization '''
        #headers = {'key': username, 'secret': password}
        req = requests.get(self.api_server + '/api/' + name + "/" + query)
        return req

    def download_experiment(self, experiment_id):
        """
            download_experiment:
        """
        req = self.query_records_no_auth('experiment',
                                         query='download/'+experiment_id)
        if req.status_code == 404:
            print "Sorry, no experiment matching id # " + experiment_id
            print "Please double check the code you obtained on the\
                http://psiturk.org/ee"
        else:
            # Check if folder with same name already exists.
            expinfo = req.json()
            gitr = requests.get(expinfo['git_url']).json()
            if os.path.exists('./'+gitr['name']):
                print "*"*20
                print "Sorry, you already have a file or folder named\
                    "+gitr['name']+". Please rename or delete it before trying\
                    to download this experiment.  You can do this by typing `rm\
                    -rf " + gitr['name'] + "`"
                print "*"*20
                return
            if "clone_url" in gitr:
                git.Git().clone(gitr["clone_url"])
                print "="*20
                print "Downloading..."
                print "Name: " + expinfo['name']
                print "Downloads: " + str(expinfo['downloads'])
                print "Keywords: " + expinfo['keywords']
                print "psiTurk Version: " +\
                    str(expinfo['psiturk_version_string'])
                print "URL: http://psiturk.org/ee/"+experiment_id
                print "\n"
                print "Experiment downloaded into the `" + gitr['name'] + "`\
                    folder of the current directory"
                print "Type 'cd " + gitr['name'] + "` then run the `psiturk`\
                    command."
                print "="*20
            else:
                print "Sorry, experiment not located on github.  You might\
                    contact the author of this experiment.  Experiment NOT\
                    downloaded."
            return


class TunnelServices(object):
    ''' Allow psiTurk to puncture firewalls using reverse tunnelling.'''

    def __init__(self):
        config = PsiturkConfig()
        config.load_config()
        self.access_key = config.get('psiTurk Access', 'psiturk_access_key_id')
        self.secret_key = config.get('psiTurk Access', 'psiturk_secret_access_id')
        self.local_port = config.getint('Server Parameters', 'port')
        self.is_open = False
        self.tunnel_port = 8000  # Set by tunnel server
        self.tunnel_host = 'tunnel.psiturk.org'
        self.tunnel_server = os.path.join(os.path.dirname(__file__),
                                          "tunnel/ngrok")
        self.tunnel_config = os.path.join(os.path.dirname(__file__),
                                          "tunnel/ngrok-config")

    @classmethod
    def is_compatible(cls):
        ''' Check OS '''
        is_64bit = struct.calcsize('P')*8 == 64
        if (_platform == "darwin" and is_64bit):
            return True
        else:
            print("Linux tunnels are currenlty unsupported.  Please notify "\
                  "authors@psiturk.org\nif you'd like to see this feature.")
            return False

    def get_tunnel_ad_url(self):
        ''' Get tunnel hostname from psiturk.org  '''
        req = requests.get('https://api.psiturk.org/api/tunnel',
                           auth=(self.access_key, self.secret_key))
        if req.status_code in [401, 403, 500]:
            print(req.content)
            return False
        else:
            return req.json()['tunnel_hostname']

    def change_tunnel_ad_url(self):
        ''' Change tunnel ad url. '''
        if self.is_open:
            self.close()
        req = requests.delete('https://api.psiturk.org/api/tunnel/',
                              auth=(self.access_key, self.secret_key))
        # the request content here actually will include the tunnel_hostname
        # if needed or wanted.
        if req.status_code in [401, 403, 500]:
            print(req.content)
            return False

    def open(self):
        ''' Open tunnel '''
        if self.is_compatible():
            tunnel_ad_url = self.get_tunnel_ad_url()
            if not tunnel_ad_url:
                return("Tunnel server appears to be down.")
            cmd = '%s -subdomain=%s -config=%s -log=stdout %s 2>&1 > server.log' \
                      %(self.tunnel_server, tunnel_ad_url, self.tunnel_config,
                        self.local_port)
            self.tunnel = subprocess.Popen(cmd, shell=True)
            self.url = '%s.%s' %(tunnel_ad_url, self.tunnel_host)
            self.full_url = 'http://%s.%s:%s' %(tunnel_ad_url, self.tunnel_host,
                                                self.tunnel_port)
            self.is_open = True
            print "Tunnel URL: %s" % self.full_url
            print "Hint: In OSX, you can open a terminal link using cmd + click"

    def close(self):
        ''' Close tunnel '''
        parent_pid = psutil.Process(self.tunnel.pid)
        child_pid = parent_pid.get_children(recursive=True)
        for pid in child_pid:
            pid.send_signal(signal.SIGTERM)
        self.is_open = False

back to top