!/usr/bin/env python import argparse import glob from collections import defaultdict import os import re import subprocess import time import yaml DRY_RUN = False

def discover_trainings(topics_dir):

"""Auto-discover all topic metadata files."""
for training_dir in glob.glob(os.path.join(topics_dir, '*')):
    metadata_file = os.path.join(training_dir, 'metadata.yaml')
    if not os.path.exists(metadata_file):
        continue

    with open(metadata_file, 'r') as handle:
        training_data = yaml.safe_load(handle)

    training = {
        'title': training_data['title'],
        'trainings': {},
    }

    for material in glob.glob(os.path.join(training_dir, 'tutorials', '*', 'tutorial.md')) + glob.glob(os.path.join(training_dir, 'tutorials', '*', 'slides.html')):
        with open(material, 'r') as handle:
            material_data = yaml.safe_load_all(handle)
            material_data = next(material_data)

        name = material.split('/')[-2]
        training['trainings'][name] = material_data['title']

    training['count'] = len(training['trainings'].keys())

    yield training_data['name'], training

def safe_name(server, dashes=True):

"""Make human strings 'safe' for usage in paths."""
safe_name = re.sub('\s', '_', server)
if dashes:
    safe_name = re.sub('[^A-Za-z0-9_-]', '_', safe_name)
else:
    safe_name = re.sub('[^A-Za-z0-9_]', '_', safe_name)

return server

def get_badge_path(label, value, color):

"""Return a string representing the expected badge filename. Returns something like 'Training Name|Supported' or 'Training Name|Unsupported'."""
safe_label = label.replace('@', '%40').replace(' ', '%20').replace('-', '--').replace('/', '%2F')
safe_value = value.replace('@', '%40').replace(' ', '%20').replace('-', '--').replace('/', '%2F')
return '%s-%s-%s.svg' % (safe_label, safe_value, color)

def realise_badge(badge, badge_cache_dir):

"""Download the badge to the badge_cache_dir (if needed) and return this real path to the user."""
if not os.path.exists(os.path.join(badge_cache_dir, badge)):
    # Download the missing image
    cmd = [
        'wget', 'https://img.shields.io/badge/%s' % badge,
        '--quiet', '-O', os.path.join(badge_cache_dir, badge)
    ]
    if not DRY_RUN:
        try:
            subprocess.check_call(cmd)
        except subprocess.CalledProcessError:
            print('unable to retrieve badges, please try again later')
        time.sleep(1)
    else:
        print(' '.join(cmd))
    # Be nice to their servers
return os.path.join(badge_cache_dir, badge)

def badge_it(label, value, color, CACHE_DIR, identifier_parts, output_dir):

# Get a path to a (cached) badge file.
real_badge_path = realise_badge(get_badge_path(
    label, value, color
), CACHE_DIR)
# Deteremine the per-instance output name
output_filedir = os.path.join(args.output, *map(safe_name, identifier_parts[0:-1]))
if not os.path.exists(output_filedir):
    os.makedirs(output_filedir)

output_filename = safe_name(identifier_parts[-1]) + '.svg'
# Ensure dir exists
output_filepath = os.path.join(output_filedir, output_filename)

# Copy the badge to a per-instance named .svg file.
up = ['..'] * (len(identifier_parts) - 1)
total = up + [real_badge_path[len('badges/'):]]
symlink_source = os.path.join(*total)
if not DRY_RUN:
    # Remove it if it exists, since this is easier than testing for
    # equality.
    if os.path.exists(output_filepath):
        os.unlink(output_filepath)

    # Now (re-)create the symlink
    os.symlink(symlink_source, output_filepath)
else:
    print(' '.join(['ln -s ', symlink_source, output_filepath]))
return output_filename

if __name__ == ‘__main__’:

parser = argparse.ArgumentParser(description='Build the badge directory for instances to use.')
parser.add_argument('--public-server-list', help='Url to access the public galaxy server list at',
                    default='https://raw.githubusercontent.com/martenson/public-galaxy-servers/master/servers.csv')
parser.add_argument('--topics-directory', help='Path to the topics directory', default='./topics/')
parser.add_argument('--instances', help='File containing the instances and their supported trainings', default='metadata/instances.yaml')

parser.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')
args = parser.parse_args()

# Validate training dir argument
if not os.path.exists(args.topics_directory) and os.path.is_dir(args.topics_directory):
    raise Exception("Invalid topics directory")
all_trainings = {k: v for (k, v) in discover_trainings(args.topics_directory)}

# Create output directory if not existing.
if not os.path.exists(args.output):
    os.makedirs(args.output)

# Also check/create the badge cache directory.
CACHE_DIR = os.path.join(args.output, 'cache')
if not os.path.exists(CACHE_DIR):
    os.makedirs(CACHE_DIR)

# Load the validated list of instances which support trainings
with open(args.instances, 'r') as handle:
    data = yaml.safe_load(handle)

# Collect a list of instances seen
instances = []
for topic in data:
    for training in data[topic]['tutorials']:
        for instance in data[topic]['tutorials'][training]['instances']:
            data[topic]['tutorials'][training]['instances'][instance]['supported'] = True
            instances.append(instance)
# All of these instances support at least one training.
instances = sorted(set(instances))

# Mark the unsupported ones as such for easier processing later.
for topic in data:
    for training in data[topic]['tutorials']:
        for instance in instances:
            # Not in one of the existing supported ones
            if instance not in data[topic]['tutorials'][training]['instances']:
                data[topic]['tutorials'][training]['instances'][instance]['supported'] = False

# Map of instance -> badges
instance_badges = {}

# Count of tutorials in each topic.
for topic in data:
    # All trainings, not just those available
    for training in sorted(data[topic]['tutorials']):
        for instance in data[topic]['tutorials'][training]['instances']:
            if instance not in instance_badges:
                instance_badges[instance] = {}

            if topic not in instance_badges[instance]:
                instance_badges[instance][topic] = []

            # If available, green badge
            is_supported = data[topic]['tutorials'][training]['instances'][instance]['supported']

            # We'll only place the badge in the HTML if the training is
            # supported (but the unavailable badge will still be available
            # in case they ever go out of compliance.)

            label = all_trainings[topic]['trainings'][training]
            if is_supported:
                output_filename = badge_it(
                    label,
                    'Supported', 'green',
                    CACHE_DIR, (instance, topic, training), args.output
                )
                instance_badges[instance][topic].append(output_filename)
            else:
                badge_it(
                    label,
                    'Unsupported', 'lightgrey',
                    CACHE_DIR, (instance, topic, training), args.output
                )

# All instances, not just checked
for instance in sorted(instance_badges):
    total = sum([len(instance_badges[instance][topic]) for topic in instance_badges[instance]])

    if total == 0:
        continue

    for topic in instance_badges[instance]:
        # Get the number of badges in this topic.
        count = len(instance_badges[instance][topic])

        if float(count) / all_trainings[topic]['count'] > 0.90:
            color = 'green'
        elif float(count) / all_trainings[topic]['count'] > 0.25:
            color = 'orange'
        else:
            color = 'red'

        output_filename = badge_it(
            all_trainings[topic]['title'], '%s%%2f%s' % (count, all_trainings[topic]['count']), color,
            CACHE_DIR, (instance, topic), args.output
        )