class Object

Constants

APPROVED_VOICES
ARI_MAP
BASE_REF
CONTRIBUTORS

Full Contributors Data

CONTRIBUTORS_SCHEMA
CONTRIBUTORS_SCHEMA_UNSAFE

Any error messages

END_OF_SENTENCE_DURATION
END_OF_SLIDE_DURATION
FASTIMAGE_AVAILABLE

The fastimage gem may not be installed, and that is OK, we will fall back to another method

FUNDERS
FUNDERS_SCHEMA
FUNDERS_SCHEMA_UNSAFE
GALAXIES
GTN_CACHE
NOW

modifiedfiles = `git diff –cached –name-only –ignore-all-space –diff-filter=M #{options}`.split(“n”)

ORGANISATIONS
ORGANISATIONS_SCHEMA
ORGANISATIONS_SCHEMA_UNSAFE
PUNCTUATION
TEST_PHRASES
TITLE_TOPIC
TUTO_ID
WORD_MAP

Public Instance Methods

automagic_loading(f) click to toggle source
# File bin/gtn.rb, line 7
def automagic_loading(f)
  # Remove our documentation
  f.reject! { |k, v| k == 'description' and v.is_a?(String) }
  f.reject! { |k| k == 'examples' }

  # Auto-replace CONTRIBUTORS in enums.
  f.each do |k, v|
    if v.is_a?(Hash)
      automagic_loading(v)
    elsif v.is_a?(Array)
      if k == 'enum'
        repl = []
        # If one of the elements in this array is CONTRIBUTORS, replace it with the same named variable
        repl << CONTRIBUTORS.keys if v.find { |x| x == 'CONTRIBUTORS' }
        repl << FUNDERS.keys if v.find { |x| x == 'FUNDERS' }
        repl << ORGANISATIONS.keys if v.find { |x| x == 'ORGANISATIONS' }
        v.replace repl.flatten if repl.length.positive?
      end
      v.flatten.each { |x| automagic_loading(x) if x.is_a?(Hash) }
    end
  end
  f
end
build_news(data, filter: nil, updates: true, only_news: false) click to toggle source
# File bin/news.rb, line 159
def build_news(data, filter: nil, updates: true, only_news: false)
  infix = filter.nil? ? '' : titleize(filter)
  output = "# GTN #{infix} News for #{NOW.strftime('%b %d')}"
  newsworthy = false

  if filter.nil?
    output += format_news(data[:added][:news])
    newsworthy |= format_news(data[:added][:news]).length.positive?
  end

  if only_news
    return [output, newsworthy]
  end


  o = format_tutorials(
    data[:added][:tutorials].select { |n| filter.nil? || n[:path] =~ %r{topics/#{filter}} },
    data[:modified][:tutorials].select { |n| filter.nil? || n[:path] =~ %r{topics/#{filter}} },
    updates: updates
  )

  output += o
  newsworthy |= o.length.positive?

  o = format_tutorials(
    data[:added][:slides].select { |n| filter.nil? || n[:path] =~ %r{topics/#{filter}} },
    data[:modified][:slides].select { |n| filter.nil? || n[:path] =~ %r{topics/#{filter}} },
    kind: 'slides',
    updates: updates
  )
  output += o
  newsworthy |= o.length.positive?

  if filter.nil? && data[:contributors].length.positive?
    newsworthy = true
    output += "\n\n## #{data[:contributors].length} new contributors!\n\n"
    output += data[:contributors].map { |c| linkify("@#{c}", "hall-of-fame/#{c}") }.join("\n").gsub(/^/, '- ')
  end

  if filter.nil? && data[:organisations].length.positive?
    newsworthy = true
    output += "\n\n## #{data[:organisations].length} new organisations!\n\n"
    output += data[:organisations].map { |c| linkify("@#{c}", "hall-of-fame/#{c}") }.join("\n").gsub(/^/, '- ')
  end

  if filter.nil? && data[:funders].length.positive?
    newsworthy = true
    output += "\n\n## #{data[:funders].length} new funders!\n\n"
    output += data[:funders].map { |c| linkify("@#{c}", "hall-of-fame/#{c}") }.join("\n").gsub(/^/, '- ')
  end

  [output, newsworthy]
end
call_engine(engine, line, mp3, voice, lang, neural) click to toggle source
# File bin/ari-synthesize.rb, line 60
def call_engine(engine, line, mp3, voice, lang, neural)
  if engine == 'aws'
    awseng = if neural
               'neural'
             else
               'standard'
             end

    # Synthesize
    args = ['aws', 'polly', 'synthesize-speech', '--engine', awseng, '--language-code', lang, '--voice-id', voice,
            '--output-format', 'mp3', '--text', line, mp3]
    _, stderr, err = Open3.capture3(*args)
    if err.exited? && err.exitstatus.positive?
      puts "ERROR: #{stderr}"
      puts "ERROR: #{err}"
      exit 1
    end
  elsif engine == 'mozilla'
    raw = Tempfile.new('synth-raw')
    _, stderr, err = Open3.capture3('curl', '--silent', '-G', '--output', raw.path,
                                    "http://localhost:5002/api/tts?text=#{CGI.escape(line)}")
    if err.exited? && err.exitstatus.positive?
      puts "ERROR: #{stderr}"
      exit 1
    end

    _, stderr, err = Open3.capture3('ffmpeg', '-loglevel', 'error', '-i', raw.path, '-y', mp3)
    if err.exited? && err.exitstatus.positive?
      puts "ERROR: #{stderr}"
      exit 1
    end
  end
end
check_indent(file) click to toggle source
# File bin/check-indent.rb, line 5
def check_indent(file)
  doc = Nokogiri::HTML(File.open(file))
  # Find all <pre> tags
  # <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
  ec = false
  doc.css('div.language-plaintext.highlighter-rouge div.highlight pre.highlight code').each do |pre|
    # Get the text content of the <pre> tag
    content = pre.text
    # Split the content by newlines
    lines = content.split("\n")

    # If all lines look like URLs:
    if lines.all? { |line| line =~ %r{://} }
      # If any are space indented
      lines.each do |line|
        if line =~ /^\s+/
          puts "#{file}: Indentation error: #{line}"
          ec = true
        end
      end
    end
  end
  ec
end
correct(uncorrected_line) click to toggle source
# File bin/ari-synthesize.rb, line 48
def correct(uncorrected_line)
  # First we try and catch the things we can directly replace (esp usegalaxy.*)
  line = uncorrected_line.strip.split.map do |w|
    translate(w)
  end.join(' ')

  # Now we do more fancy replacements
  line.strip.split(/([ ‘’,'".:;!`()])/).reject(&:empty?).compact.map do |w|
    translate(w)
  end.join
end
data_library_for_tutorial(path) click to toggle source
# File bin/update-data-library, line 139
def data_library_for_tutorial(path)
  File.join(File.dirname(path), 'data-library.yaml')
end
fetch_toolcats(server) click to toggle source

Get the list of toolcats

# File bin/fetch-categories.rb, line 8
def fetch_toolcats(server)
  uri = URI.parse(server.to_s)
  request = Net::HTTP::Get.new(uri)
  req_options = {
    use_ssl: uri.scheme == 'https',
  }
  response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
    http.request(request)
  end

  begin
    JSON.parse(response.body) do |w|
      w
    end
  rescue StandardError
    {}
  end
end
fetch_workflowhub() click to toggle source
# File bin/workflows-fetch.rb, line 39
def fetch_workflowhub
  projects = JSON.parse(request('https://workflowhub.eu/projects').body)
  project_mapping = projects['data'].to_h { |p| [p['id'], p['attributes']['title']] }

  response = request('https://workflowhub.eu/workflows?filter[workflow_type]=galaxy')
  data = JSON.parse(response.body)
  if !data['links']['next'].nil?
    puts 'ERROR: Cannot yet handle multiple pages'
    exit 42
  end
  puts "INFO: Fetching #{data['data'].length} workflows from WorkflowHub"
  data['data'].map.with_index do |w, _i|
    # {"id"=>"14", "type"=>"workflows", "attributes"=>{"title"=>"Cheminformatics - Docking"}, "links"=>{"self"=>"/workflows/14"}}
    wf_info = JSON.parse(request("https://workflowhub.eu#{w['links']['self']}").body)
    creator_list = []

    creator0 = wf_info['data']['attributes']['creators'][0]
    if creator0.nil?
      # Other creators
      other = wf_info['data']['attributes']['other_creators']
      if !other.nil? && other.length.positive?
        creator_list.push(wf_info['data']['attributes']['other_creators'].split(',').map(&:strip))
      end
    else
      # Primary
      creator_list.push("#{creator0['given_name']} #{creator0['family_name']}")
    end
    # Projects
    wf_info['data']['relationships']['projects']['data'].each do |p|
      creator_list.push(project_mapping[p['id']])
    end

    creator_list = creator_list.flatten.compact.uniq

    begin
      r = {
        'name' => wf_info['data']['attributes']['title'],
        'owner' => creator_list.join(', '),
        'number_of_steps' => wf_info['data']['attributes']['internals']['steps'].length,
        'server' => 'https://workflowhub.eu',
        'id' => wf_info['data']['id'],
        'tags' => wf_info['data']['attributes']['tags'].map { |t| t.gsub(/^name:/, '') },
        'update_time' => wf_info['data']['attributes']['updated_at'],
      }
    rescue StandardError
      r = nil
    end
    r
  end.compact
end
fetch_workflows(server) click to toggle source

Get the list of workflows

# File bin/workflows-fetch.rb, line 21
def fetch_workflows(server)
  begin
    response = request("#{server}/api/workflows/")
  rescue StandardError
    puts "ERROR: Failed to fetch workflows from #{server}"
    return []
  end

  begin
    JSON.parse(response.body).map do |w|
      w['server'] = server
      w
    end
  rescue StandardError
    []
  end
end
filterSlides(x) click to toggle source
# File bin/news.rb, line 68
def filterSlides(x)
  x =~ %r{topics/.*/tutorials/.*/slides.*\.html}
end
filterTutorials(x) click to toggle source

new news new slidevideos new contributors Done new tutorials Done new slides Done

# File bin/news.rb, line 64
def filterTutorials(x)
  x =~ %r{topics/.*/tutorials/.*/tutorial.*\.md}
end
find_duration(mp3) click to toggle source
# File bin/ari-synthesize.rb, line 94
def find_duration(mp3)
  stdout, = Open3.capture2('ffprobe', '-loglevel', 'error', '-show_format', '-show_streams', '-print_format', 'json',
                           '-i', mp3)
  data = JSON.parse(stdout)
  data['format']['duration'].to_f
end
fixNews(n) click to toggle source
# File bin/news.rb, line 93
def fixNews(n)
  # news/_posts/2021-11-10-api.html => news/2021/11/10/api.html
  n[:md].gsub(%r{news/_posts/(....)-(..)-(..)-(.*.html)}, 'news/\1/\2/\3/\4')
end
format_news(news) click to toggle source
# File bin/news.rb, line 132
def format_news(news)
  output = ''
  if news.length.positive?
    output += "\n\n## Big News!\n\n"
    output += news.join("\n").gsub(/^/, '- ')
  end
  output
end
format_tutorials(added, modified, kind: 'tutorials', updates: true) click to toggle source
# File bin/news.rb, line 141
def format_tutorials(added, modified, kind: 'tutorials', updates: true)
  output = ''
  count = added.length
  count += modified.length if updates
  output += "\n\n## #{count} #{kind}!" if count.positive?

  if added.length.positive?
    output += "\n\nNew #{kind}:\n\n"
    output += added.map { |n| n[:md] }.join("\n").gsub(/^/, '- ')
  end

  if updates && modified.length.positive?
    output += "\n\nUpdated #{kind}:\n\n"
    output += modified.map { |n| n[:md] }.join("\n").gsub(/^/, '- ')
  end
  output
end
generate_topic_feeds(site) click to toggle source
# File _plugins/feeds.rb, line 6
def generate_topic_feeds(site)
  TopicFilter.list_topics(site).each do |topic|
    feed_path = File.join(site.dest, 'topics', topic, 'feed.xml')
    Jekyll.logger.debug "Generating feed for #{topic} => #{feed_path}"

    topic_pages = site.pages
                      .select { |x| x.path =~ %r{^\.?/?topics/#{topic}} }
                      .select { |x| x.path =~ %r{(tutorial.md|slides.html|faqs/.*.md)} }
                      .reject { |x| x.path =~ /index.md/ }
                      .reject { |x| x.data.fetch('draft', '').to_s == 'true' }
                      .reject { |x| x.url =~ /slides-plain.html/ }
                      .reject { |x| File.symlink?(x.path) } # Remove symlinks to other faqs/tutorials
                      .uniq(&:path)
                      .sort_by { |page| Gtn::PublicationTimes.obtain_time(page.path) }
                      .reverse

    if topic_pages.empty?
      Jekyll.logger.warn "No pages for #{topic}"
      next
    else
      Jekyll.logger.debug "Found #{topic_pages.length} pages for #{topic}"
    end

    builder = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
      # Set stylesheet
      xml.feed(xmlns: 'http://www.w3.org/2005/Atom') do
        # Set generator also needs a URI attribute
        xml.generator('Jekyll', uri: 'https://jekyllrb.com/')
        xml.link(href: "#{site.config['url']}#{site.baseurl}/topics/#{topic}/feed.xml", rel: 'self')
        xml.updated(Gtn::ModificationTimes.obtain_time(topic_pages.first.path).to_datetime.rfc3339)
        xml.id("#{site.config['url']}#{site.baseurl}/topics/#{topic}/feed.xml")
        topic_title = site.data[topic]['title']
        xml.title("Galaxy Training Network - #{topic_title}")
        xml.subtitle("Recently added tutorials, slides, and FAQs in the #{topic} topic")

        topic_pages.each do |page|
          page_type = if page.path =~ %r{faqs/.*.md}
                        'faq'
                      else
                        page.path.split('/').last.split('.').first
                      end

          xml.entry do
            xml.title(page.data['title'])
            link = "#{site.config['url']}#{site.baseurl}#{page.url}"
            xml.link(href: link)
            # Our links are stable
            xml.id(link)

            # This is a feed of only NEW tutorials, so we only include publication times.
            # xml.published(Gtn::PublicationTimes.obtain_time(page.path).to_datetime.rfc3339)
            xml.updated(Gtn::PublicationTimes.obtain_time(page.path).to_datetime.rfc3339)

            # xml.path(page.path)
            xml.category(term: "new #{page_type}")
            # xml.content(page.content, type: "html")
            xml.summary(page.content.strip.split("\n").first, type: 'html')

            Gtn::Contributors.get_authors(page.data).each do |c|
              xml.author do
                xml.name(Gtn::Contributors.fetch_name(site, c))
                xml.uri("#{site.config['url']}#{site.baseurl}/hall-of-fame/#{c}/")
              end
            end

            Gtn::Contributors.get_non_authors(page.data).each do |c|
              xml.contributor do
                xml.name(Gtn::Contributors.fetch_name(site, c))
                xml.uri("#{site.config['url']}#{site.baseurl}/hall-of-fame/#{c}/")
              end
            end
          end
        end
      end
    end

    # The builder won't let you add a processing instruction, so we have to
    # serialise it to a string and then parse it again. Ridiculous.
    finalised = Nokogiri::XML builder.to_xml
    pi = Nokogiri::XML::ProcessingInstruction.new(
      finalised, 'xml-stylesheet',
      %(type="text/xml" href="#{site.config['url']}#{site.baseurl}/feed.xslt.xml")
    )
    finalised.root.add_previous_sibling pi
    File.write(feed_path, finalised.to_xml)
  end

  nil
end
json_boxify(h, page) click to toggle source
# File _plugins/notebook-jupyter.rb, line 6
def json_boxify(h, page)
  h['cells'].each do |cell|
    # If it's a list, loop
    if cell['source'].is_a? Array
      cell['source'].each do |line|
        # rubocop:disable Layout/LineLength
        line.gsub!(%r{<(?<boxclass>#{Gtn::Boxify.box_classes})-title( ?(?<noprefix>noprefix))>(?<title>.*?)</\s*\k<boxclass>-title\s*>}) do
          # rubocop:enable Layout/LineLength
          m = Regexp.last_match
          box_type = m[:boxclass]
          title = m[:title]
          noprefix = m[:noprefix]
          _, box = Gtn::Boxify.generate_title(box_type, title, lang, page.path, noprefix: noprefix)
          box
        end
      end
    else
      # rubocop:disable Layout/LineLength
      cell['source'].gsub!(%r{<(?<boxclass>#{Gtn::Boxify.box_classes})-title(?<noprefix>\s+noprefix)?>(?<title>.*?)</\s*\k<boxclass>-title\s*>}) do
        # rubocop:enable Layout/LineLength
        m = Regexp.last_match
        box_type = m[:boxclass]
        title = m[:title]
        noprefix = m[:noprefix]
        _, box = Gtn::Boxify.generate_title(box_type, title, 'en', page.path, noprefix: noprefix)
        box
      end
    end
  end
  h
end
jupyter_post_write(site) click to toggle source
# File _plugins/notebook-jupyter.rb, line 120
def jupyter_post_write(site)
  site.config['__rendered_notebook_cache'].each do |_path, info|
    # Create if missing
    FileUtils.mkdir_p(info['dir'])
    # Write it out!
    File.write(info['path1'], info['content1'])
    File.write(info['path2'], info['content2'])
  end
end
jupyter_pre_render(site) click to toggle source
# File _plugins/notebook-jupyter.rb, line 38
def jupyter_pre_render(site)
  Jekyll.logger.info '[GTN/Notebooks] Rendering'

  site.config['__rendered_notebook_cache'] = {}

  # For every tutorial with the 'notebook' key in the page data
  site.pages.select { |page| GTNNotebooks.notebook_filter(page.data) }.each do |page|
    # We get the path to the tutorial source
    dir = File.dirname(File.join('.', page.url))
    fn = File.join('.', page.url).sub(/html$/, 'md')
    notebook_language = page.data['notebook'].fetch('language', 'python')

    # Tag our source page
    page.data['tags'] = page.data['tags'] || []
    page.data['tags'].push('jupyter-notebook')

    Jekyll.logger.info "[GTN/Notebooks] Rendering #{notebook_language} #{fn}"
    last_modified = Gtn::ModificationTimes.obtain_time(page.path)
    notebook = GTNNotebooks.render_jupyter_notebook(page.data, page.content, page.url, last_modified,
                                                    notebook_language, site, dir)

    topic_id = dir.split('/')[-3]
    tutorial_id = dir.split('/')[-1]
    with_solutions = notebook.clone

    with_solutions['cells'] = with_solutions['cells'].map do |cell|
      if cell.fetch('cell_type') == 'markdown' && (cell['source'].is_a? String)
        m = cell['source'].match(/<blockquote class="solution"[^>]*>/)
        if m
          cell['source'].gsub!(/<blockquote class="solution"[^>]*>/,
                               '<br/><details style="border: 2px solid #B8C3EA; margin: 1em 0.2em;' \
                               'padding: 0.5em; cursor: pointer;"><summary>👁 View solution</summary>')

          idx = m.begin(0)
          q = cell['source'][0..idx]
          w = cell['source'][idx + 1..]
          e = w.index('</blockquote>')
          r = "#{w[0..e - 1]}</details>#{w[e + 13..]}"

          cell['source'] = q + r
        end
      end
      cell
    end

    # Write it out!
    ipynb_dir = File.join(site.dest, dir)
    ipynb_path = File.join(ipynb_dir, "#{topic_id}-#{tutorial_id}.ipynb")
    # page2 = PageWithoutAFile.new(site, '', dir, "#{topic_id}-#{tutorial_id}.ipynb")
    # page2.content = JSON.pretty_generate(with_solutions)
    # page2.data['layout'] = nil
    # page2.data['citation_target'] = 'jupyter'
    # site.pages << page2

    # Create a no-solutions version:
    no_solutions = notebook.clone

    no_solutions['cells'] = no_solutions['cells'].map do |cell|
      if cell.fetch('cell_type') == 'markdown' && (cell['source'].is_a? String)
        cell['source'].gsub!(/<blockquote class="solution"[^>]*>/,
                             '<blockquote class="solution" style="display:none">')
      end
      cell
    end

    ipynb_path2 = File.join(ipynb_dir, "#{topic_id}-#{tutorial_id}-course.ipynb")
    # page2 = PageWithoutAFile.new(site, '', dir, "#{topic_id}-#{tutorial_id}-course.ipynb")
    # page2.content = JSON.pretty_generate(no_solutions)
    # page2.data['layout'] = nil
    # page2.data['citation_target'] = 'jupyter'
    # site.pages << page2

    site.config['__rendered_notebook_cache'][page.path] = {
      'dir' => ipynb_dir,
      'path1' => ipynb_path,
      'content1' => JSON.pretty_generate(json_boxify(with_solutions, page)),
      'path2' => ipynb_path2,
      'content2' => JSON.pretty_generate(json_boxify(no_solutions, page)),
    }
  end
end
linkify(text, path) click to toggle source
# File bin/news.rb, line 83
def linkify(text, path)
  "[#{text.gsub('|', '-')}](https://training.galaxyproject.org/training-material/#{path}?utm_source=matrix&utm_medium=newsbot&utm_campaign=matrix-news)"
end
lookup_topic(topic_id) click to toggle source
# File bin/prepare_feedback.rb, line 54
def lookup_topic(topic_id)
  @cache ||= {}
  @cache.fetch(topic_id) do |key|
    file = "metadata/#{topic_id}.yaml"
    return nil unless File.exist? file

    data = YAML.load_file(file)
    @cache[key] = data['title']
  end
end
lookup_tuto(topic_id, tuto_id) click to toggle source
# File bin/prepare_feedback.rb, line 33
def lookup_tuto(topic_id, tuto_id)
  @cache ||= {}
  @cache.fetch("#{topic_id}/#{tuto_id}") do |key|
    @cache[key] = nil

    file = "topics/#{topic_id}/tutorials/#{tuto_id}/tutorial.md"
    if File.exist? file
      data = YAML.load_file(file)
      @cache[key] = data['title']
    else
      file = "topics/#{topic_id}/tutorials/#{tuto_id}/slides.html"
      if File.exist? file
        data = YAML.load_file(file)
        @cache[key] = data['title']
      else
        puts "No file for #{topic_id}/#{tuto_id}"
      end
    end
  end
end
onlyEnabled(x) click to toggle source
# File bin/news.rb, line 72
def onlyEnabled(x)
  tutorial_meta = YAML.load_file(x)
  tutorial_enabled = tutorial_meta.fetch('enable', true)

  topic = x.split('/')[1]
  topic_meta = YAML.load_file("metadata/#{topic}.yaml")
  topic_enabled = topic_meta.fetch('enable', true)

  tutorial_enabled and topic_enabled
end
parseOptions() click to toggle source
# File bin/ari-synthesize.rb, line 157
def parseOptions
  options = {}
  OptionParser.new do |opts|
    opts.banner = 'Usage: ari-synthesize.rb [options]'

    options[:neural] = true
    options[:voice] = 'Amy'
    options[:lang] = 'en-GB'

    opts.on('--aws', 'Use AWS Polly') do |v|
      options[:aws] = v
    end

    opts.on('--mozilla', 'Use MozillaTTS') do |v|
      options[:mozilla] = v
    end

    opts.on('--non-neural', '[AWS] Non-neural voice') do |_v|
      options[:neural] = false
    end

    opts.on('--voice=VOICE', '[AWS] Voice ID') do |n|
      options[:voice] = n
    end

    opts.on('--lang=LANG', '[AWS] Language code') do |n|
      options[:lang] = n
    end

    opts.on('-fFILE', '--file=FILE', 'File containing line of text to speak') do |n|
      options[:file] = n
    end

    opts.on('-oFILE', '--output=FILE', 'Location to save the file in (defaults to auto-generated location)') do |n|
      options[:output] = n
    end

    opts.on('-v', '--[no-]verbose', 'Run verbosely') do |v|
      options[:verbose] = v
    end
  end.parse!

  if !(options[:aws] || options[:mozilla])
    puts 'ERROR: You must use aws or mozilla'
    exit 1
  end

  if !(options[:file])
    puts 'ERROR: You must provide a file with a single sentence to speak'
    exit 1
  end

  sentence = File.read(options[:file]).chomp
  if options[:aws]
    engine = 'aws'
  elsif options[:mozilla]
    engine = 'mozilla'
  end

  [sentence, engine, options]
end
parse_metadata(path) click to toggle source
# File bin/update-data-library, line 149
def parse_metadata(path)
  parts = path.to_s.split('/')
  topic_id = parts[1]
  topic_metadata = YAML.load_file(File.join('metadata', "#{topic_id}.yaml"))
  tutorial_metadata = YAML.load_file(path)
  topic = { 'name' => topic_metadata['title'], 'description' => topic_metadata['summary'] }
  tutorial = { 'name' => tutorial_metadata['title'] }
  [topic, tutorial]
end
parse_zenodo_id_formats(link) click to toggle source
# File bin/update-data-library, line 30
def parse_zenodo_id_formats(link)
  # https://zenodo.org/record/1234567
  # https://zenodo.org/record/1234567#.X0X0X0X0X0X
  # doi:10.5281/zenodo.1234567
  # doi:10.5281/zenodo.1234567#.X0X0X0X0X0X
  # 10.5281/zenodo.1234567
  # 10.5281/zenodo.1234567#.X0X0X0X0X0X
  # https://doi.org/10.5281/zenodo.3732358
  # https://doi.org/10.5281/zenodo.3732358#.X0X0X0X0X0X
  #
  link = link.split('#')[0]
  if link.match(/doi:/) || link.match(/^10.5281/) || link.match(/doi.org/)
    link.split('.')[-1]
  else
    link.split('/')[-1]
  end
end
printableMaterial(path) click to toggle source
# File bin/news.rb, line 87
def printableMaterial(path)
  d = YAML.load_file(path)
  { md: linkify(d['title'], path.gsub(/.md/, '.html')),
    path: path }
end
request(url) click to toggle source
# File bin/update-data-library, line 17
def request(url)
  uri = URI.parse(url)
  request = Net::HTTP::Get.new(uri)
  request['Accept'] = 'application/json'
  req_options = {
    use_ssl: uri.scheme == 'https',
  }
  Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
    json_s = http.request(request).body
    JSON.parse(json_s)
  end
end
send_news(output, options, channel: 'default') click to toggle source
# File bin/news.rb, line 213
def send_news(output, options, channel: 'default')
  if options[:postToMatrix]
    # rubocop:disable Style/GlobalVars
    homeserver = $rooms[channel]
    # rubocop:enable Style/GlobalVars
    pp homeserver

    data = {
      'msgtype' => 'm.notice',
      'body' => output,
      'format' => 'org.matrix.custom.html',
      'formatted_body' => Kramdown::Document.new(output).to_html,
    }

    headers = {
      'Authorization' => "Bearer #{ENV.fetch('MATRIX_ACCESS_TOKEN', nil)}",
      'Content-type' => 'application/json',
    }

    uri_send_message = URI("#{homeserver[:server]}/_matrix/client/r0/rooms/#{homeserver[:room]}/send/m.room.message")
    req = Net::HTTP.post(uri_send_message, JSON.generate(data), headers)
    # Parse response
    resp = JSON.parse(req.body)
    puts resp

    if resp['errcode'] == 'M_FORBIDDEN' && (resp['error'] =~ /not in room/)
      puts 'Not in room, attempting to join'
      # Join room
      #  POST /_matrix/client/v3/join/{roomIdOrAlias}
      uri_join = URI("#{homeserver[:server]}/_matrix/client/v3/join/#{homeserver[:room]}")
      req = Net::HTTP.post(uri_join, JSON.generate({}), headers)
      # Parse response
      resp = JSON.parse(req.body)

      # Now we're safe to re-try
      if resp.key?('room_id')
        req = Net::HTTP.post(uri_send_message, JSON.generate(data), headers)
        # Parse response
        resp = JSON.parse(req.body)
        puts resp
      end
    end
  else
    puts '=============='
    puts output
    puts '=============='
  end
end
show_errors(file, errs) click to toggle source
# File bin/validate-contributors.rb, line 29
def show_errors(file, errs)
  # If we had no errors, validated successfully
  if errs.empty?
    puts "\e[38;5;40m#{file} validated succesfully\e[m"
    0
  else
    # Otherwise, print errors and exit non-zero
    puts "\e[48;5;09m#{file}  has errors\e[m"
    errs.each { |x| puts "  #{x}" }
    1
  end
end
split_sentence(sentence, timing) click to toggle source
# File bin/ari-prep-script.rb, line 56
def split_sentence(sentence, timing)
  res = sentence.split
  chunk_size = (res.length.to_f / (res.length.to_f / 20).ceil).ceil
  chunks = res.each_slice(chunk_size).to_a.length
  res.each_slice(chunk_size).with_index.map do |chunk, idx|
    t0 = timing * (idx / chunks.to_f)
    tf = timing * ((1 + idx) / chunks.to_f)
    [chunk, t0, tf]
  end
end
synthesize(uncorrected_line, engine, voice: 'Amy', lang: 'en-GB', neural: true, output: nil) click to toggle source
# File bin/ari-synthesize.rb, line 101
def synthesize(uncorrected_line, engine, voice: 'Amy', lang: 'en-GB', neural: true, output: nil)
  line = correct(uncorrected_line)
  digest = Digest::MD5.hexdigest line
  if output.nil?
    mp3 = File.join(GTN_CACHE, "#{engine}-#{digest}-#{voice}.mp3")
    json = File.join(GTN_CACHE, "#{engine}-#{digest}-#{voice}.json")
    if File.file?(mp3)
      duration = JSON.parse(File.read(json))['end']
      return mp3, json, duration.to_f
    end
  else
    mp3 = output
    json = "#{output}.json"
    if File.file?(output)
      return mp3, json, 0.0 # Todo
    end
  end

  # Call our engine
  call_engine(engine, line, mp3, voice, lang, neural)
  duration = find_duration(mp3)

  if line.length < 200 && duration > 27
    # Helena managed to find a specific bad string which, when fed to Mozilla's
    # TTS would generate
    #
    # In: Some important terms you should know.
    # Out Some important terms you should know know know know know know know know know know know know know know ...
    #
    # So we put in a check that the duration hasn't done something crazy, and
    # if it is add something to the end which seems to short-circuit that
    # error.
    #
    # I've reported this upstream but the response was not useful, apparently
    # this is an "expected failure mode".
    #
    # https://github.com/synesthesiam/docker-mozillatts/issues/9
    # https://discourse.mozilla.org/t/sentences-which-trigger-an-endless-loop/72261/8
    warn 'Strange: line was too long'
    call_engine(engine, "#{line}.", mp3)
    duration = find_duration(mp3)
  end

  if line.length < 200 && duration > 27
    # Or maybe they just wrote a super long sentence. Or maybe we need to update the cutoff time.
    warn "ERROR: #{duration} of line is bad: #{line}"
  end

  # Now collect metadata for JSON
  json_handle = File.open(json, 'w')
  json_handle.write(JSON.generate({ time: 0, type: 'sentence', start: 0, end: duration, value: line }))
  json_handle.close

  [mp3, json, duration]
end
test_workflow(workflow_file, galaxy_id) click to toggle source
# File bin/workflow-test.rb, line 16
def test_workflow(workflow_file, galaxy_id)
  directory = File.dirname(workflow_file)
  workflow_base = File.basename(workflow_file, '.ga')
  workflow_output_json = File.join(directory, "#{workflow_base}.#{galaxy_id}.json")
  galaxy_url = GALAXIES[galaxy_id][:url]
  galaxy_user_key = GALAXIES[galaxy_id][:key]
  cmd = [
    'planemo', '--verbose', 'test',
    '--galaxy_url', galaxy_url,
    '--galaxy_user_key', galaxy_user_key,
    '--no_shed_install',
    '--engine', 'external_galaxy',
    '--polling_backoff', '1',
    '--simultaneous_uploads',
    '--test_output_json', workflow_output_json,
    workflow_file
  ]
  p cmd.join(' ')

  Open3.popen3(*cmd) do |_stdin, stdout, stderr, wait_thr|
    exit_status = wait_thr.value # Process::Status object returned
    File.write("#{directory}/#{workflow_base}.#{galaxy_id}.log", stdout.read)
    File.write("#{directory}/#{workflow_base}.#{galaxy_id}.err", stderr.read)
    puts "#{workflow_file} => #{exit_status} (#{stderr})"
  end
end
timefmt(t, fmt) click to toggle source

Intro slides. Fast: editly –json editly.json5 126,23s user 5,62s system 126% cpu 1:44,08 total Slow: editly –json editly.json5 902,71s user 69,27s system 326% cpu 4:57,54 total

# File bin/ari-prep-script.rb, line 39
def timefmt(t, fmt)
  seconds = t % (24 * 3600)
  hours = seconds.to_i / 3600
  seconds = seconds % 3600
  minutes = seconds.to_i / 60
  seconds = seconds % 60
  (seconds, ms) = seconds.divmod(1)
  # seconds = seconds
  ms = 1000 * ms

  if fmt == 'vtt'
    format('%<h>02d:%<m>02d:%<s>02d.%<ms>03d', { h: hours, m: minutes, s: seconds, ms: ms })
  else
    format('%<h>02d:%<m>02d:%<s>02d,%<ms>03d', { h: hours, m: minutes, s: seconds, ms: ms })
  end
end
titleize(t) click to toggle source
# File bin/news.rb, line 128
def titleize(t)
  t.gsub('-', ' ').gsub(/\w+/, &:capitalize)
end
translate(word) click to toggle source
# File bin/ari-synthesize.rb, line 24
def translate(word)
  return word if /^\s+$/.match(word)

  return word if PUNCTUATION.find_index(word)

  return WORD_MAP[word] if WORD_MAP.key?(word)

  m = /([^A-Za-z0-9]*)([A-Za-z0-9]+)([^A-Za-z0-9]*)(.*)/.match(word)

  if !m
    puts "Error: #{word}"
    return word
  end

  fixed = if m[2]
            WORD_MAP.fetch(m[2].downcase, m[2])
          else
            m[2]
          end

  # puts "#{m} ⇒ #{m[1] + fixed + m[3]}"
  m[1] + fixed + m[3] + m[4]
end
update_data_library(path, topic, tutorial, zenodo_record) click to toggle source
# File bin/update-data-library, line 55
def update_data_library(path, topic, tutorial, zenodo_record)
  zenodo_id = zenodo_record['id'].to_s
  zenodo_files = zenodo_record.fetch('files', []).map do |f|
    official_extension = f['type']

    link = f['links']['self'].sub(%r{/content$}, '')
    unofficial_extension = link.split('.')[-2..].join('.')
    ext = @SHARED_DATATYPES.fetch(unofficial_extension, nil) || @SHARED_DATATYPES.fetch(official_extension, nil)

    # Example:
    # https://zenodo.org/api/records/10870107/files/elem_s2_r1.fq.gz/content
    # Needs to be
    # https://zenodo.org/record/10870107/files/elem_s2_r1.fq.gz
    real_link = f['links']['self'].sub(%r{/content$}, '').sub('/api/records/', '/record/')
    # puts "Processing file: #{f['type']} #{f['links']['self']} => #{ext}"
    # puts "#{unofficial_extension} => #{@SHARED_DATATYPES.fetch(unofficial_extension, nil)}"
    # puts "#{official_extension} => #{@SHARED_DATATYPES.fetch(official_extension, nil)}"
    warn "Unknown file type: #{f['type']}. Consider adding this to shared/datatypes.yaml" if ext.nil?

    {
      'url' => real_link,
      'src' => 'url',
      'ext' => ext || f['type'],
      'info' => "https://doi.org/10.5281/zenodo.#{zenodo_id}",
      # 'checksum' => f['checksum'],
      # 'key' => f['key'],
    }
  end

  library = {
    'destination' => {
      'type' => 'library',
      'name' => 'GTN - Material',
      'description' => 'Galaxy Training Network Material',
      'synopsis' => 'Galaxy Training Network Material. See https://training.galaxyproject.org',
    },
    'items' => [
      'name' => topic['name'],
      'description' => topic['description'],
      'items' => [
        'name' => tutorial['name'],
        'items' => [
          'name' => "DOI: 10.5281/zenodo.#{zenodo_id}",
          'description' => 'latest',
          'items' => zenodo_files
        ]
      ]
    ]
  }
  data_library_path = data_library_for_tutorial(path)
  puts "Writing data library to #{data_library_path}"
  File.write(data_library_path, library.to_yaml)
end
update_tutorial(path, zenodo_id) click to toggle source
# File bin/update-data-library, line 48
def update_tutorial(path, zenodo_id)
  # Edit the yaml header of the markdown file to update the ID
  contents = File.read(path)
  contents.gsub!(/^zenodo_link:.*/, "zenodo_link: 'https://zenodo.org/record/#{zenodo_id}'")
  File.write(path, contents)
end
validate_document(document, validator) click to toggle source
# File bin/validate-contributors.rb, line 22
def validate_document(document, validator)
  errors = validator.validate(document)
  return errors if errors && !errors.empty?

  []
end
write_data_library(path, topic, tutorial, tutorial_zenodo_id, force) click to toggle source
# File bin/update-data-library, line 109
def write_data_library(path, topic, tutorial, tutorial_zenodo_id, force)
  # Fetch the zenodo record
  zenodo_record = request("https://zenodo.org/api/records/#{tutorial_zenodo_id}")
  new_zenodo_id = zenodo_record['id'].to_s

  # If it's redirected we'll get a different ID here
  puts "Discovered zenodo link: #{new_zenodo_id}"

  # So load the data library, and check what's written there
  datalibrary_zenodo_id = if File.exist?(data_library_for_tutorial(path))
                            YAML.load_file(data_library_for_tutorial(path))['zenodo_id'].to_s
                          end

  # If the ID has changed we should update the tutorial as well:
  if new_zenodo_id == tutorial_zenodo_id && !force
    warn 'Tutorial is up to date'
  else
    warn "Zenodo ID has changed from #{tutorial_zenodo_id} to #{new_zenodo_id}, updating the tutorial"
    update_tutorial(path, new_zenodo_id)
  end

  # If the new ID doesn't match the data library, then we should update it.
  if new_zenodo_id == datalibrary_zenodo_id && !force
    warn 'Data library is up to date'
  else
    warn "Zenodo ID has changed from #{datalibrary_zenodo_id} to #{new_zenodo_id}, updating the data library"
    update_data_library(path, topic, tutorial, zenodo_record)
  end
end