Path: blob/main/bin/add_galaxy_instance_badges.py
1677 views
#!/usr/bin/env python1import argparse2import glob3from collections import defaultdict4import os5import re6import subprocess7import time8import yaml9DRY_RUN = False101112def discover_trainings(topics_dir):13"""Auto-discover all topic metadata files."""14for training_dir in glob.glob(os.path.join(topics_dir, '*')):15metadata_file = os.path.join(training_dir, 'metadata.yaml')16if not os.path.exists(metadata_file):17continue1819with open(metadata_file, 'r') as handle:20training_data = yaml.safe_load(handle)2122training = {23'title': training_data['title'],24'trainings': {},25}2627for material in glob.glob(os.path.join(training_dir, 'tutorials', '*', 'tutorial.md')) + glob.glob(os.path.join(training_dir, 'tutorials', '*', 'slides.html')):28with open(material, 'r') as handle:29material_data = yaml.safe_load_all(handle)30material_data = next(material_data)3132name = material.split('/')[-2]33training['trainings'][name] = material_data['title']3435training['count'] = len(training['trainings'].keys())3637yield training_data['name'], training383940def safe_name(server, dashes=True):41"""Make human strings 'safe' for usage in paths."""42safe_name = re.sub('\s', '_', server)43if dashes:44safe_name = re.sub('[^A-Za-z0-9_-]', '_', safe_name)45else:46safe_name = re.sub('[^A-Za-z0-9_]', '_', safe_name)4748return server495051def get_badge_path(label, value, color):52"""Return a string representing the expected badge filename. Returns something like 'Training Name|Supported' or 'Training Name|Unsupported'."""53safe_label = label.replace('@', '%40').replace(' ', '%20').replace('-', '--').replace('/', '%2F')54safe_value = value.replace('@', '%40').replace(' ', '%20').replace('-', '--').replace('/', '%2F')55return '%s-%s-%s.svg' % (safe_label, safe_value, color)565758def realise_badge(badge, badge_cache_dir):59"""Download the badge to the badge_cache_dir (if needed) and return this real path to the user."""60if not os.path.exists(os.path.join(badge_cache_dir, badge)):61# Download the missing image62cmd = [63'wget', 'https://img.shields.io/badge/%s' % badge,64'--quiet', '-O', os.path.join(badge_cache_dir, badge)65]66if not DRY_RUN:67try:68subprocess.check_call(cmd)69except subprocess.CalledProcessError:70print('unable to retrieve badges, please try again later')71time.sleep(1)72else:73print(' '.join(cmd))74# Be nice to their servers75return os.path.join(badge_cache_dir, badge)767778def badge_it(label, value, color, CACHE_DIR, identifier_parts, output_dir):79# Get a path to a (cached) badge file.80real_badge_path = realise_badge(get_badge_path(81label, value, color82), CACHE_DIR)83# Deteremine the per-instance output name84output_filedir = os.path.join(args.output, *map(safe_name, identifier_parts[0:-1]))85if not os.path.exists(output_filedir):86os.makedirs(output_filedir)8788output_filename = safe_name(identifier_parts[-1]) + '.svg'89# Ensure dir exists90output_filepath = os.path.join(output_filedir, output_filename)9192# Copy the badge to a per-instance named .svg file.93up = ['..'] * (len(identifier_parts) - 1)94total = up + [real_badge_path[len('badges/'):]]95symlink_source = os.path.join(*total)96if not DRY_RUN:97# Remove it if it exists, since this is easier than testing for98# equality.99if os.path.exists(output_filepath):100os.unlink(output_filepath)101102# Now (re-)create the symlink103os.symlink(symlink_source, output_filepath)104else:105print(' '.join(['ln -s ', symlink_source, output_filepath]))106return output_filename107108109if __name__ == '__main__':110parser = argparse.ArgumentParser(description='Build the badge directory for instances to use.')111parser.add_argument('--public-server-list', help='Url to access the public galaxy server list at',112default='https://raw.githubusercontent.com/martenson/public-galaxy-servers/master/servers.csv')113parser.add_argument('--topics-directory', help='Path to the topics directory', default='./topics/')114parser.add_argument('--instances', help='File containing the instances and their supported trainings', default='metadata/instances.yaml')115116parser.add_argument('--output', help='Path to the the directory where the badges should be stored. The directory will be created if it does not exist.', default='badges')117args = parser.parse_args()118119# Validate training dir argument120if not os.path.exists(args.topics_directory) and os.path.is_dir(args.topics_directory):121raise Exception("Invalid topics directory")122all_trainings = {k: v for (k, v) in discover_trainings(args.topics_directory)}123124# Create output directory if not existing.125if not os.path.exists(args.output):126os.makedirs(args.output)127128# Also check/create the badge cache directory.129CACHE_DIR = os.path.join(args.output, 'cache')130if not os.path.exists(CACHE_DIR):131os.makedirs(CACHE_DIR)132133# Load the validated list of instances which support trainings134with open(args.instances, 'r') as handle:135data = yaml.safe_load(handle)136137# Collect a list of instances seen138instances = []139for topic in data:140for training in data[topic]['tutorials']:141for instance in data[topic]['tutorials'][training]['instances']:142data[topic]['tutorials'][training]['instances'][instance]['supported'] = True143instances.append(instance)144# All of these instances support at least one training.145instances = sorted(set(instances))146147# Mark the unsupported ones as such for easier processing later.148for topic in data:149for training in data[topic]['tutorials']:150for instance in instances:151# Not in one of the existing supported ones152if instance not in data[topic]['tutorials'][training]['instances']:153data[topic]['tutorials'][training]['instances'][instance]['supported'] = False154155# Map of instance -> badges156instance_badges = {}157158# Count of tutorials in each topic.159for topic in data:160# All trainings, not just those available161for training in sorted(data[topic]['tutorials']):162for instance in data[topic]['tutorials'][training]['instances']:163if instance not in instance_badges:164instance_badges[instance] = {}165166if topic not in instance_badges[instance]:167instance_badges[instance][topic] = []168169# If available, green badge170is_supported = data[topic]['tutorials'][training]['instances'][instance]['supported']171172# We'll only place the badge in the HTML if the training is173# supported (but the unavailable badge will still be available174# in case they ever go out of compliance.)175176label = all_trainings[topic]['trainings'][training]177if is_supported:178output_filename = badge_it(179label,180'Supported', 'green',181CACHE_DIR, (instance, topic, training), args.output182)183instance_badges[instance][topic].append(output_filename)184else:185badge_it(186label,187'Unsupported', 'lightgrey',188CACHE_DIR, (instance, topic, training), args.output189)190191# All instances, not just checked192for instance in sorted(instance_badges):193total = sum([len(instance_badges[instance][topic]) for topic in instance_badges[instance]])194195if total == 0:196continue197198for topic in instance_badges[instance]:199# Get the number of badges in this topic.200count = len(instance_badges[instance][topic])201202if float(count) / all_trainings[topic]['count'] > 0.90:203color = 'green'204elif float(count) / all_trainings[topic]['count'] > 0.25:205color = 'orange'206else:207color = 'red'208209output_filename = badge_it(210all_trainings[topic]['title'], '%s%%2f%s' % (count, all_trainings[topic]['count']), color,211CACHE_DIR, (instance, topic), args.output212)213214215