def get_selection(available, selection, base='/scif/apps'): '''we compare the basename (the exp_id) of the selection and available, regardless of parent directories''' if isinstance(selection, str): selection = selection.split(',') available = [os.path.basename(x) for x in available] selection = [os.path.basename(x) for x in selection] finalset = [x for x in selection if x in available] if len(finalset) == 0: bot.warning("No user experiments selected, providing all %s" %(len(available))) finalset = available return ["%s/%s" %(base,x) for x in finalset]
def get_selection(available, selection, base="/scif/apps"): """we compare the basename (the exp_id) of the selection and available, regardless of parent directories""" if isinstance(selection, str): selection = selection.split(",") available = [os.path.basename(x) for x in available] selection = [os.path.basename(x) for x in selection] finalset = [x for x in selection if x in available] if len(finalset) == 0: bot.warning("No user experiments selected, providing all %s" % (len(available))) finalset = available return ["%s/%s" % (base, x) for x in finalset]
def main(args, parser, subparser=None): """this is the main entrypoint for a container based web server, with most of the variables coming from the environment. See the Dockerfile template for how this function is executed. """ # First priority to args.base base = args.base if base is None: base = os.environ.get("EXPFACTORY_BASE") # Does the base folder exist? if base is None: bot.error("You must set a base of experiments with --base" % base) sys.exit(1) if not os.path.exists(base): bot.error("Base folder %s does not exist." % base) sys.exit(1) # Export environment variables for the client experiments = args.experiments if experiments is None: experiments = " ".join(glob("%s/*" % base)) os.environ["EXPFACTORY_EXPERIMENTS"] = experiments # If defined and file exists, set runtime variables if args.vars is not None: if os.path.exists(args.vars): os.environ["EXPFACTORY_RUNTIME_VARS"] = args.vars os.environ["EXPFACTORY_RUNTIME_DELIM"] = args.delim else: bot.warning("Variables file %s not found." % args.vars) subid = os.environ.get("EXPFACTORY_STUDY_ID") if args.subid is not None: subid = args.subid os.environ["EXPFACTORY_SUBID"] = subid os.environ["EXPFACTORY_RANDOM"] = str(args.disable_randomize) os.environ["EXPFACTORY_BASE"] = base from expfactory.server import start start(port=5000)
def main(args,parser,subparser=None): '''this is the main entrypoint for a container based web server, with most of the variables coming from the environment. See the Dockerfile template for how this function is executed. ''' # First priority to args.base base = args.base if base is None: base = os.environ.get('EXPFACTORY_BASE') # Does the base folder exist? if base is None: bot.error("You must set a base of experiments with --base" % base) sys.exit(1) if not os.path.exists(base): bot.error("Base folder %s does not exist." % base) sys.exit(1) # Export environment variables for the client experiments = args.experiments if experiments is None: experiments = " ".join(glob("%s/*" % base)) os.environ['EXPFACTORY_EXPERIMENTS'] = experiments # If defined and file exists, set runtime variables if args.vars is not None: if os.path.exists(args.vars): os.environ['EXPFACTORY_RUNTIME_VARS'] = args.vars os.environ['EXPFACTORY_RUNTIME_DELIM'] = args.delim else: bot.warning('Variables file %s not found.' %args.vars) subid = os.environ.get('EXPFACTORY_STUDY_ID') if args.subid is not None: subid = args.subid os.environ['EXPFACTORY_SUBID'] = subid os.environ['EXPFACTORY_RANDOM'] = str(args.disable_randomize) os.environ['EXPFACTORY_BASE'] = base from expfactory.server import start start(port=5000)
def validate(self, folder): '''validate is the first entrypoint function for running an experiment or survey robot. It ensures that the content is valid, and then calls _validate (should be defined in subclass)''' validator = ExperimentValidator() valid = validator.validate(folder) if valid is True: # IF missing favicon, add self._check_favicon(folder) valid = self._validate(folder) bot.log("[done] stopping web server...") self.httpd.server_close() else: bot.warning('%s is not valid, skipping robot testing.' %folder)
def initdb(self): '''initdb will check for writability of the data folder, meaning that it is bound to the local machine. If the folder isn't bound, expfactory runs in demo mode (not saving data) ''' self.database = EXPFACTORY_DATABASE bot.info("DATABASE: %s" %self.database) # Supported database options valid = ('sqlite', 'postgres', 'mysql', 'filesystem') if not self.database.startswith(valid): bot.warning('%s is not yet a supported type, saving to filesystem.' % self.database) self.database = 'filesystem' # Add functions specific to database type self.init_db() # uses url in self.database bot.log("Data base: %s" % self.database)
def _validate_preview(self, url): bot.test("Experiment url: %s" % url) org, repo = url.split("/")[-2:] if repo.endswith(".git"): repo = repo.replace(".git", "") github_pages = "https://%s.github.io/%s" % (org, repo) bot.test("Github Pages url: %s" % github_pages) response = requests.get(github_pages) if response.status_code == 404: bot.error("""Preview not found at %s. You must publish a static preview from the master branch of your repository to add to the library.""" % github_pages) return False index = response.text tmpdir = tempfile.mkdtemp() repo_master = clone(url, tmpdir) contenders = glob("%s/*" % repo_master) license = False found = False for test in contenders: if os.path.isdir(test): continue print("...%s" % test) if "LICENSE" in os.path.basename(test): license = True if os.path.basename(test) == "index.html": bot.test("Found index file in repository.") found = True break if license is False: bot.warning( "LICENSE file not found. This will be required for future experiments!" ) self._print_valid(found) return found
def _validate_preview(self, url): bot.test('Experiment url: %s' %url) org,repo = url.split('/')[-2:] if repo.endswith('.git'): repo = repo.replace('.git','') github_pages = "https://%s.github.io/%s" %(org,repo) bot.test('Github Pages url: %s' %github_pages) response = requests.get(github_pages) if response.status_code == 404: bot.error('''Preview not found at %s. You must publish a static preview from the master branch of your repository to add to the library.''' % github_pages) return False index = response.text tmpdir = tempfile.mkdtemp() repo_master = clone(url, tmpdir) contenders = glob('%s/*' %repo_master) license = False found = False for test in contenders: if os.path.isdir(test): continue print('...%s' %test) if "LICENSE" in os.path.basename(test): license = True if os.path.basename(test) == "index.html": bot.test('Found index file in repository.') found = True break if license is False: bot.warning("LICENSE file not found. This will be required for future experiments!") self._print_valid(found) return found
def save_data(self, session, exp_id, content): '''save data will obtain the current subid from the session, and save it depending on the database type. Currently we just support flat files''' subid = session.get('subid') # We only attempt save if there is a subject id, set at start data_file = None if subid is not None: if os.path.exists(self.data_base): # /scif/data data_base = "%s/%s" % (self.data_base, subid) # expfactory/00001 if not os.path.exists(data_base): os.mkdir(data_base) data_file = "%s/%s/%s-results.json" % (self.data_base, subid, exp_id) if os.path.exists(data_file): bot.warning('%s exists, and is being overwritten.' % data_file) write_json(content, data_file) return data_file
def _validate_row(row, sep=',', required_length=None): '''validate_row will ensure that a row has the proper length, and is not empty and cleaned of extra spaces. Parameters ========== row: a single row, not yet parsed. Returns a valid row, or None if not valid ''' if not isinstance(row, list): row = _parse_row(row, sep) if required_length: length = len(row) if length != required_length: bot.warning('Row should have length %s (not %s)' %(required_length, length)) bot.warning(row) row = None return row
def _validate_row(row, sep=",", required_length=None): """validate_row will ensure that a row has the proper length, and is not empty and cleaned of extra spaces. Parameters ========== row: a single row, not yet parsed. Returns a valid row, or None if not valid """ if not isinstance(row, list): row = _parse_row(row, sep) if required_length: length = len(row) if length != required_length: bot.warning("Row should have length %s (not %s)" % (required_length, length)) bot.warning(row) row = None return row
def main(args, parser, subparser): # List of experiments is required template = get_template('build/docker/Dockerfile.template') # For now, only one database provided database = args.database studyid = args.studyid experiments = args.experiments template = sub_template(template, "{{studyid}}", studyid) template = sub_template(template, "{{database}}", database) library = get_library(key='name') apps = "\n" # Add local experiments to library, first preference local_installs = 0 for experiment in experiments: if os.path.exists(experiment): bot.info('local experiment %s found, validating...' % experiment) # Is the experiment valid? cli = ExperimentValidator() valid = cli.validate(experiment, cleanup=False) if valid is True: local_installs += 1 config = load_experiment(experiment) exp_id = config['exp_id'] # If we aren't building in the experiment directory, we need to copy there output_dir = "%s/%s" % (os.path.abspath( os.path.dirname(args.output)), exp_id) experiment_dir = os.path.abspath(experiment) if output_dir != experiment_dir: copy_directory(experiment_dir, output_dir) config['local'] = os.path.abspath(experiment) library[exp_id] = config # Warn the user that local installs are not reproducible (from recipe) if local_installs > 0: bot.warning( "%s local installs detected: build is not reproducible without experiment folders" % local_installs) # Build Image with Experiments for experiment in experiments: exp_id = os.path.basename(experiment) if exp_id in library: config = library[exp_id] app = "LABEL EXPERIMENT_%s /scif/apps/%s\n" % (exp_id, exp_id) # Here add custom build routine, should be list of lines if "install" in config: commands = "\n".join( ["RUN %s " % s for x in config['install']]).strip('\n') app = "%s%s\n" % (app, commands) # The final installation step, either from Github (url) or local folder if "local" in config: app = "%sADD %s /scif/apps/%s\n" % (app, exp_id, exp_id) app = "%sWORKDIR /scif/apps\nRUN expfactory install %s\n" % ( app, exp_id) else: app = "%sWORKDIR /scif/apps\nRUN expfactory install %s\n" % ( app, config['github']) apps = "%s%s\n" % (apps, app) else: bot.warning('%s not in library, check spelling & punctuation.' % exp_id) if apps == "\n": bot.error('No valid experiments found, cancelling recipe build.') sys.exit(1) template = sub_template(template, "{{experiments}}", apps) outfile = write_file(args.output, template) bot.log("Recipe written to %s" % outfile)
def generate_runtime_vars(variable_file=None, sep=","): """generate a lookup data structure from a delimited file. We typically obtain the file name and delimiter from the environment by way of EXPFACTORY_RUNTIME_VARS, and EXPFACTORY_RUNTIME_DELIM, respectively, but the user can also parse from a custom variable file by way of specifying it to the function (preference is given here). The file should be csv, with the only required first header field as "token" and second as "exp_id" to distinguish the participant ID and experiment id. The subsequent columns should correspond to experiment variable names. No special parsing of either is done. Parameters ========== variable_file: full path to the tabular file with token, exp_id, etc. sep: the default delimiter to use, if not set in enironment. Returns ======= varset: a dictionary lookup by exp_id and then participant ID. { 'test-parse-url': { '123': { 'color': 'red', 'globalname': 'globalvalue', 'words': 'at the thing' }, '456': {'color': 'blue', 'globalname': 'globalvalue', 'words': 'omg tacos'} } } """ # First preference goes to runtime, then environment, then unset if variable_file is None: if EXPFACTORY_RUNTIME_VARS is not None: variable_file = EXPFACTORY_RUNTIME_VARS if variable_file is not None: if not os.path.exists(variable_file): bot.warning("%s is set, but not found" % variable_file) return variable_file # If still None, no file if variable_file is None: return variable_file # If we get here, we have a variable file that exists delim = sep if EXPFACTORY_RUNTIME_DELIM is not None: delim = EXPFACTORY_RUNTIME_DELIM bot.debug("Delim for variables file set to %s" % sep) # Read in the file, generate config varset = dict() rows = _read_runtime_vars(variable_file) if len(rows) > 0: # When we get here, we are sure to have # 'exp_id', 'var_name', 'var_value', 'token' for row in rows: exp_id = row[0].lower() # exp-id must be lowercase var_name = row[1] var_value = row[2] token = row[3] # Level 1: Experiment ID if exp_id not in varset: varset[exp_id] = {} # Level 2: Participant ID if token not in varset[exp_id]: varset[exp_id][token] = {} # If found global setting, courtesy debug message if token == "*": bot.debug("Found global variable %s" % var_name) # Level 3: is the variable, issue warning if already defined if var_name in varset[exp_id][token]: bot.warning("%s defined twice %s:%s" % (var_name, exp_id, token)) varset[exp_id][token][var_name] = var_value return varset
def generate_runtime_vars(variable_file=None, sep=','): '''generate a lookup data structure from a delimited file. We typically obtain the file name and delimiter from the environment by way of EXPFACTORY_RUNTIME_VARS, and EXPFACTORY_RUNTIME_DELIM, respectively, but the user can also parse from a custom variable file by way of specifying it to the function (preference is given here). The file should be csv, with the only required first header field as "token" and second as "exp_id" to distinguish the participant ID and experiment id. The subsequent columns should correspond to experiment variable names. No special parsing of either is done. Parameters ========== variable_file: full path to the tabular file with token, exp_id, etc. sep: the default delimiter to use, if not set in enironment. Returns ======= varset: a dictionary lookup by exp_id and then participant ID. { 'test-parse-url': { '123': { 'color': 'red', 'globalname': 'globalvalue', 'words': 'at the thing' }, '456': {'color': 'blue', 'globalname': 'globalvalue', 'words': 'omg tacos'} } } ''' # First preference goes to runtime, then environment, then unset if variable_file is None: if EXPFACTORY_RUNTIME_VARS is not None: variable_file = EXPFACTORY_RUNTIME_VARS if variable_file is not None: if not os.path.exists(variable_file): bot.warning('%s is set, but not found' %variable_file) return variable_file # If still None, no file if variable_file is None: return variable_file # If we get here, we have a variable file that exists delim = sep if EXPFACTORY_RUNTIME_DELIM is not None: delim = EXPFACTORY_RUNTIME_DELIM bot.debug('Delim for variables file set to %s' %sep) # Read in the file, generate config varset = dict() rows = _read_runtime_vars(variable_file) if len(rows) > 0: # When we get here, we are sure to have # 'exp_id', 'var_name', 'var_value', 'token' for row in rows: exp_id = row[0].lower() # exp-id must be lowercase var_name = row[1] var_value = row[2] token = row[3] # Level 1: Experiment ID if exp_id not in varset: varset[exp_id] = {} # Level 2: Participant ID if token not in varset[exp_id]: varset[exp_id][token] = {} # If found global setting, courtesy debug message if token == "*": bot.debug('Found global variable %s' %var_name) # Level 3: is the variable, issue warning if already defined if var_name in varset[exp_id][token]: bot.warning('%s defined twice %s:%s' %(var_name, exp_id, token)) varset[exp_id][token][var_name] = var_value return varset
def main(args, parser, subparser): template = "build/docker/Dockerfile.template" # Full path to template is required if provided via input if args.input is not None: template = args.input template = get_template(template) # For now, only one database provided database = args.database studyid = args.studyid experiments = args.experiments branch = "-b %s" % os.environ.get("EXPFACTORY_BRANCH", "master") headless = "false" if args.headless is True: headless = "true" template = sub_template(template, "{{studyid}}", studyid) template = sub_template(template, "{{database}}", database) template = sub_template(template, "{{headless}}", headless) template = sub_template(template, "{{branch}}", branch) if args.headless is True: bot.info( "Headless build detected, you will need to generate tokens for application entry with expfactory users --new" ) library = get_library(key="name") apps = "\n" # Add local experiments to library, first preference local_installs = 0 for experiment in experiments: if os.path.exists(experiment): bot.info("local experiment %s found, validating..." % experiment) # Is the experiment valid? cli = ExperimentValidator() valid = cli.validate(experiment, cleanup=False) if valid is True: local_installs += 1 config = load_experiment(experiment) exp_id = config["exp_id"] # If we aren't building in the experiment directory, we need to copy there output_dir = "%s/%s" % ( os.path.abspath(os.path.dirname(args.output)), exp_id, ) experiment_dir = os.path.abspath(experiment) if output_dir != experiment_dir: copy_directory(experiment_dir, output_dir) config["local"] = os.path.abspath(experiment) library[exp_id] = config # Warn the user that local installs are not reproducible (from recipe) if local_installs > 0: bot.warning( "%s local installs detected: build is not reproducible without experiment folders" % local_installs ) # Build Image with Experiments for experiment in experiments: exp_id = os.path.basename(experiment) if exp_id in library: config = library[exp_id] app = "LABEL EXPERIMENT_%s /scif/apps/%s\n" % (exp_id, exp_id) # Here add custom build routine, should be list of lines if "install" in config: commands = "\n".join(["RUN %s " % s for x in config["install"]]).strip( "\n" ) app = "%s%s\n" % (app, commands) # The final installation step, either from Github (url) or local folder if "local" in config: app = "%sADD %s /scif/apps/%s\n" % (app, exp_id, exp_id) app = "%sWORKDIR /scif/apps\nRUN expfactory install %s\n" % ( app, exp_id, ) else: app = "%sWORKDIR /scif/apps\nRUN expfactory install %s\n" % ( app, config["github"], ) apps = "%s%s\n" % (apps, app) else: bot.warning("%s not in library, check spelling & punctuation." % exp_id) if apps == "\n": bot.error("No valid experiments found, cancelling recipe build.") sys.exit(1) template = sub_template(template, "{{experiments}}", apps) outfile = write_file(args.output, template) bot.log("Recipe written to %s" % outfile)
def main(args,parser,subparser): template = 'build/docker/Dockerfile.template' # Full path to template is required if provided via input if args.input is not None: template = args.input template = get_template(template) # For now, only one database provided database = args.database studyid = args.studyid experiments = args.experiments branch = "-b %s" %os.environ.get('EXPFACTORY_BRANCH','master') headless = "false" if args.headless is True: headless = "true" template = sub_template(template,"{{studyid}}",studyid) template = sub_template(template,"{{database}}",database) template = sub_template(template,"{{headless}}",headless) template = sub_template(template,"{{branch}}",branch) if args.headless is True: bot.info("Headless build detected, you will need to generate tokens for application entry with expfactory users --new") library = get_library(key='name') apps = "\n" # Add local experiments to library, first preference local_installs = 0 for experiment in experiments: if os.path.exists(experiment): bot.info('local experiment %s found, validating...' %experiment) # Is the experiment valid? cli = ExperimentValidator() valid = cli.validate(experiment, cleanup=False) if valid is True: local_installs +=1 config = load_experiment(experiment) exp_id = config['exp_id'] # If we aren't building in the experiment directory, we need to copy there output_dir = "%s/%s" %(os.path.abspath(os.path.dirname(args.output)), exp_id) experiment_dir = os.path.abspath(experiment) if output_dir != experiment_dir: copy_directory(experiment_dir, output_dir) config['local'] = os.path.abspath(experiment) library[exp_id] = config # Warn the user that local installs are not reproducible (from recipe) if local_installs > 0: bot.warning("%s local installs detected: build is not reproducible without experiment folders" %local_installs) # Build Image with Experiments for experiment in experiments: exp_id = os.path.basename(experiment) if exp_id in library: config = library[exp_id] app = "LABEL EXPERIMENT_%s /scif/apps/%s\n" %(exp_id, exp_id) # Here add custom build routine, should be list of lines if "install" in config: commands = "\n".join(["RUN %s "%s for x in config['install']]).strip('\n') app = "%s%s\n" %(app, commands) # The final installation step, either from Github (url) or local folder if "local" in config: app = "%sADD %s /scif/apps/%s\n" %(app, exp_id, exp_id) app = "%sWORKDIR /scif/apps\nRUN expfactory install %s\n" %(app, exp_id) else: app = "%sWORKDIR /scif/apps\nRUN expfactory install %s\n" %(app, config['github']) apps = "%s%s\n" %(apps,app) else: bot.warning('%s not in library, check spelling & punctuation.' %exp_id) if apps == "\n": bot.error('No valid experiments found, cancelling recipe build.') sys.exit(1) template = sub_template(template,"{{experiments}}",apps) outfile = write_file(args.output,template) bot.log("Recipe written to %s" %outfile)