changeset 7:f9a6e151b3b4 draft

planemo upload for repository https://github.com/TAMU-CPT/galaxy-webapollo commit 52b9e5bf6a6efb09a5cb845ee48703651c644174
author eric-rasche
date Tue, 27 Jun 2017 04:05:17 -0400
parents 8f76685cdfc8
children df7a90763b3c
files create_account.py create_features_from_gff3.py create_features_from_gff3.xml create_or_update_organism.py create_or_update_organism.xml delete_features.py delete_features.xml delete_organism.py delete_organism.xml export.py fetch_organism_jbrowse.py fetch_organism_jbrowse.xml json2iframe.py list_organisms.py macros.xml webapollo.py
diffstat 16 files changed, 647 insertions(+), 128 deletions(-) [+]
line wrap: on
line diff
--- a/create_account.py	Sat Mar 04 18:00:52 2017 -0500
+++ b/create_account.py	Tue Jun 27 04:05:17 2017 -0400
@@ -1,4 +1,7 @@
 #!/usr/bin/env python
+from __future__ import print_function
+from builtins import str
+from builtins import range
 import random
 import argparse
 import time
@@ -9,6 +12,7 @@
     chars = list('qwrtpsdfghjklzxcvbnm')
     return ''.join(random.choice(chars) for _ in range(length))
 
+
 if __name__ == '__main__':
     parser = argparse.ArgumentParser(description='Sample script to add an account via web services')
     WAAuth(parser)
@@ -30,9 +34,9 @@
         # Update name, regen password if the user ran it again
         userObj = user[0]
         returnData = wa.users.updateUser(userObj, args.email, args.first, args.last, password)
-        print 'Updated User\nUsername: %s\nPassword: %s' % (args.email, password)
+        print('Updated User\nUsername: %s\nPassword: %s' % (args.email, password))
     else:
         returnData = wa.users.createUser(args.email, args.first, args.last, password, role='user')
-        print 'Created User\nUsername: %s\nPassword: %s' % (args.email, password)
+        print('Created User\nUsername: %s\nPassword: %s' % (args.email, password))
 
-    print "Return data: " + str(returnData)
+    print("Return data: " + str(returnData))
--- a/create_features_from_gff3.py	Sat Mar 04 18:00:52 2017 -0500
+++ b/create_features_from_gff3.py	Tue Jun 27 04:05:17 2017 -0400
@@ -1,10 +1,10 @@
 #!/usr/bin/env python
+from builtins import str
 import sys
-import json
 import time
 import argparse
 from webapollo import WebApolloInstance, featuresToFeatureSchema
-from webapollo import WAAuth, OrgOrGuess, GuessOrg, AssertUser
+from webapollo import WAAuth, OrgOrGuess, GuessOrg, AssertUser, retry
 from BCBio import GFF
 import logging
 logging.basicConfig(level=logging.INFO)
@@ -15,6 +15,7 @@
     parser = argparse.ArgumentParser(description='Sample script to add an attribute to a feature via web services')
     WAAuth(parser)
     parser.add_argument('email', help='User Email')
+    parser.add_argument('--source', help='URL where the input dataset can be found.')
     OrgOrGuess(parser)
 
     parser.add_argument('gff3', type=argparse.FileType('r'), help='GFF3 file')
@@ -37,63 +38,147 @@
     sys.stdout.write('# ')
     sys.stdout.write('\t'.join(['Feature ID', 'Apollo ID', 'Success', 'Messages']))
     sys.stdout.write('\n')
-
     # print(wa.annotations.getFeatures())
     for rec in GFF.parse(args.gff3):
         wa.annotations.setSequence(rec.id, org['id'])
         for feature in rec.features:
             # We can only handle genes right now
-            if feature.type != 'gene':
+            if feature.type not in ('gene', 'terminator'):
                 continue
             # Convert the feature into a presentation that Apollo will accept
             featureData = featuresToFeatureSchema([feature])
+            if 'children' in featureData[0] and any([child['type']['name'] == 'tRNA' for child in featureData[0]['children']]):
+                # We're experiencing a (transient?) problem where gene_001 to
+                # gene_025 will be rejected. Thus, hardcode to a known working
+                # gene name and update later.
 
-            try:
+                featureData[0]['name'] = 'tRNA_000'
+                tRNA_sf = [child for child in feature.sub_features if child.type == 'tRNA'][0]
+                tRNA_type = 'tRNA-' + tRNA_sf.qualifiers.get('Codon', ["Unk"])[0]
+
+                if 'Name' in feature.qualifiers:
+                    if feature.qualifiers['Name'][0].startswith('tRNA-'):
+                        tRNA_type = feature.qualifiers['Name'][0]
+
+                newfeature = wa.annotations.addFeature(featureData, trustme=True)
+
+                def func0():
+                    wa.annotations.setName(
+                        newfeature['features'][0]['uniquename'],
+                        tRNA_type,
+                    )
+                retry(func0)
+
+                if args.source:
+                    gene_id = newfeature['features'][0]['parent_id']
+
+                    def setSource():
+                        wa.annotations.addAttributes(gene_id, {'DatasetSource': [args.source]})
+                    retry(setSource)
+
+                sys.stdout.write('\t'.join([
+                    feature.id,
+                    newfeature['features'][0]['uniquename'],
+                    'success',
+                ]))
+            elif featureData[0]['type']['name'] == 'terminator':
                 # We're experiencing a (transient?) problem where gene_001 to
                 # gene_025 will be rejected. Thus, hardcode to a known working
                 # gene name and update later.
-                featureData[0]['name'] = 'gene_000'
-                # Extract CDS feature from the feature data, this will be used
-                # to set the CDS location correctly (apollo currently screwing
-                # this up (2.0.6))
-                CDS = featureData[0]['children'][0]['children']
-                CDS = [x for x in CDS if x['type']['name'] == 'CDS'][0]['location']
-                # Create the new feature
+                featureData[0]['name'] = 'terminator_000'
                 newfeature = wa.annotations.addFeature(featureData, trustme=True)
-                # Extract the UUIDs that apollo returns to us
-                mrna_id = newfeature['features'][0]['uniquename']
-                gene_id = newfeature['features'][0]['parent_id']
-                # Sleep to give it time to actually persist the feature. Apollo
-                # is terrible about writing + immediately reading back written
-                # data.
-                time.sleep(1)
-                # Correct the translation start, but with strand specific log
-                if CDS['strand'] == 1:
-                    wa.annotations.setTranslationStart(mrna_id, min(CDS['fmin'], CDS['fmax']))
-                else:
-                    wa.annotations.setTranslationStart(mrna_id, max(CDS['fmin'], CDS['fmax']) - 1)
+
+                def func0():
+                    wa.annotations.setName(
+                        newfeature['features'][0]['uniquename'],
+                        'terminator'
+                    )
 
-                # Finally we set the name, this should be correct.
-                wa.annotations.setName(mrna_id, feature.qualifiers.get('product', ["Unknown"])[0])
-                wa.annotations.setName(gene_id, feature.qualifiers.get('product', ["Unknown"])[0])
+                retry(func0)
 
-                for (k, v) in feature.qualifiers.items():
-                    if k not in bad_quals:
-                        # set qualifier
-                        pass
+                if args.source:
+                    gene_id = newfeature['features'][0]['parent_id']
+
+                    def setSource():
+                        wa.annotations.addAttributes(gene_id, {'DatasetSource': [args.source]})
+                    retry(setSource)
 
                 sys.stdout.write('\t'.join([
                     feature.id,
-                    gene_id,
+                    newfeature['features'][0]['uniquename'],
                     'success',
-                    "Dropped qualifiers: %s" % (json.dumps({k: v for (k, v) in feature.qualifiers.items() if k not in bad_quals})),
                 ]))
-            except Exception as e:
-                sys.stdout.write('\t'.join([
-                    feature.id,
-                    '',
-                    'ERROR',
-                    str(e)
-                ]))
+            else:
+                try:
+                    # We're experiencing a (transient?) problem where gene_001 to
+                    # gene_025 will be rejected. Thus, hardcode to a known working
+                    # gene name and update later.
+                    featureData[0]['name'] = 'gene_000'
+                    # Extract CDS feature from the feature data, this will be used
+                    # to set the CDS location correctly (apollo currently screwing
+                    # this up (2.0.6))
+                    CDS = featureData[0]['children'][0]['children']
+                    CDS = [x for x in CDS if x['type']['name'] == 'CDS'][0]['location']
+                    # Create the new feature
+                    newfeature = wa.annotations.addFeature(featureData, trustme=True)
+                    # Extract the UUIDs that apollo returns to us
+                    mrna_id = newfeature['features'][0]['uniquename']
+                    gene_id = newfeature['features'][0]['parent_id']
+                    # Sleep to give it time to actually persist the feature. Apollo
+                    # is terrible about writing + immediately reading back written
+                    # data.
+                    time.sleep(1)
+                    # Correct the translation start, but with strand specific log
+                    if CDS['strand'] == 1:
+                        wa.annotations.setTranslationStart(mrna_id, min(CDS['fmin'], CDS['fmax']))
+                    else:
+                        wa.annotations.setTranslationStart(mrna_id, max(CDS['fmin'], CDS['fmax']) - 1)
+
+                    # Finally we set the name, this should be correct.
+                    time.sleep(0.5)
+                    wa.annotations.setName(mrna_id, feature.qualifiers.get('product', feature.qualifiers.get('Name', ["Unknown"]))[0])
+                    time.sleep(0.5)
+
+                    def func():
+                        wa.annotations.setName(gene_id, feature.qualifiers.get('product', feature.qualifiers.get('Name', ["Unknown"]))[0])
+                    retry(func)
 
+                    if args.source:
+                        gene_id = newfeature['features'][0]['parent_id']
+
+                        def setSource():
+                            wa.annotations.addAttributes(gene_id, {'DatasetSource': [args.source]})
+                        retry(setSource)
+                    extra_attr = {}
+                    for (key, values) in feature.qualifiers.items():
+                        if key in bad_quals:
+                            continue
+
+                        if key == 'Note':
+                            def func2():
+                                wa.annotations.addComments(gene_id, values)
+                            retry(func2)
+                        else:
+                            extra_attr[key] = values
+
+                    def func3():
+                        wa.annotations.addAttributes(gene_id, extra_attr)
+                    retry(func3)
+
+                    sys.stdout.write('\t'.join([
+                        feature.id,
+                        gene_id,
+                        'success',
+                    ]))
+                except Exception as e:
+                    msg = str(e)
+                    if '\n' in msg:
+                        msg = msg[0:msg.index('\n')]
+                    sys.stdout.write('\t'.join([
+                        feature.id,
+                        '',
+                        'ERROR',
+                        msg
+                    ]))
             sys.stdout.write('\n')
+            sys.stdout.flush()
--- a/create_features_from_gff3.xml	Sat Mar 04 18:00:52 2017 -0500
+++ b/create_features_from_gff3.xml	Tue Jun 27 04:05:17 2017 -0400
@@ -13,6 +13,8 @@
 
 "$__user_email__"
 $gff3_data
+--source "${__app__.config.galaxy_infrastructure_url}history/view/${__app__.security.encode_id($input.history_id)}"
+
 > $output]]></command>
   <inputs>
     <expand macro="org_or_guess" />
@@ -35,4 +37,3 @@
 @REFERENCES@
 ]]></help>
 </tool>
-
--- a/create_or_update_organism.py	Sat Mar 04 18:00:52 2017 -0500
+++ b/create_or_update_organism.py	Tue Jun 27 04:05:17 2017 -0400
@@ -1,4 +1,5 @@
 #!/usr/bin/env python
+from __future__ import print_function
 import sys
 import json
 import argparse
@@ -85,8 +86,8 @@
         if args.group:
             group = wa.groups.loadGroupByName(name=args.group)
             res = wa.groups.updateOrganismPermission(group, org_cn,
-                                             administrate=False, write=True, read=True,
-                                             export=True)
+                                                     administrate=False, write=True, read=True,
+                                                     export=True)
 
     data = [o for o in data if o['commonName'] == org_cn]
     print(json.dumps(data, indent=2))
--- a/create_or_update_organism.xml	Sat Mar 04 18:00:52 2017 -0500
+++ b/create_or_update_organism.xml	Tue Jun 27 04:05:17 2017 -0400
@@ -15,7 +15,7 @@
 
 --genus "$genus"
 --species "$species"
-#if str(${group}) != "None":
+#if str($group) != "None":
 --group '${group}'
 #end if
 $public
--- a/delete_features.py	Sat Mar 04 18:00:52 2017 -0500
+++ b/delete_features.py	Tue Jun 27 04:05:17 2017 -0400
@@ -1,7 +1,9 @@
 #!/usr/bin/env python
+from __future__ import print_function
 import argparse
+import random
 from webapollo import WebApolloInstance
-from webapollo import WAAuth, OrgOrGuess, GuessOrg, AssertUser
+from webapollo import WAAuth, OrgOrGuess, GuessOrg, AssertUser, retry
 import logging
 logging.basicConfig(level=logging.INFO)
 log = logging.getLogger(__name__)
@@ -11,6 +13,7 @@
     parser = argparse.ArgumentParser(description='Sample script to delete all features from an organism')
     WAAuth(parser)
     parser.add_argument('email', help='User Email')
+    parser.add_argument('--type', help='Feature type filter')
     OrgOrGuess(parser)
 
     args = parser.parse_args()
@@ -27,13 +30,36 @@
     # TODO: Check user perms on org.
     org = wa.organisms.findOrganismByCn(org_cn)
 
-    # Call setSequence to tell apollo which organism we're working with
-    wa.annotations.setSequence(org['commonName'], org['id'])
-    # Then get a list of features.
-    features = wa.annotations.getFeatures()
-    # For each feature in the features
-    for feature in features['features']:
-        # We see that deleteFeatures wants a uniqueName, and so we pass
-        # is the uniquename field in the feature.
-        print(wa.annotations.deleteFeatures([feature['uniquename']]))
+    sequences = wa.organisms.getSequencesForOrganism(org['id'])
+    for sequence in sequences['sequences']:
+        log.info("Processing %s %s", org['commonName'], sequence['name'])
+        # Call setSequence to tell apollo which organism we're working with
+        wa.annotations.setSequence(sequence['name'], org['id'])
+        # Then get a list of features.
+        features = wa.annotations.getFeatures()
+        # For each feature in the features
+        for feature in sorted(features['features'], key=lambda x: random.random()):
+            if args.type:
+                if args.type == 'tRNA':
+                    if feature['type']['name'] != 'tRNA':
+                        continue
 
+                elif args.type == 'terminator':
+                    if feature['type']['name'] != 'terminator':
+                        continue
+
+                elif args.type == 'mRNA':
+                    if feature['type']['name'] != 'mRNA':
+                        continue
+
+                else:
+                    raise Exception("Unknown type")
+
+            # We see that deleteFeatures wants a uniqueName, and so we pass
+            # is the uniquename field in the feature.
+            def fn():
+                wa.annotations.deleteFeatures([feature['uniquename']])
+                print('Deleted %s [type=%s]' % (feature['uniquename'], feature['type']['name']))
+
+            if not retry(fn, limit=3):
+                print('Error %s' % feature['uniquename'])
--- a/delete_features.xml	Sat Mar 04 18:00:52 2017 -0500
+++ b/delete_features.xml	Tue Jun 27 04:05:17 2017 -0400
@@ -1,5 +1,5 @@
 <?xml version="1.0"?>
-<tool id="edu.tamu.cpt2.webapollo.delete_features" name="Delete all annotations from an Apollo record" version="1.2">
+<tool id="edu.tamu.cpt2.webapollo.delete_features" name="Delete all annotations from an Apollo record" version="1.5" profile="16.04">
   <description></description>
   <macros>
     <import>macros.xml</import>
@@ -8,32 +8,36 @@
   <expand macro="requirements"/>
   <command detect_errors="aggressive"><![CDATA[
 #if str($ask_one) == "yes":
-    #if str($ask_two) == "yes":
-        ## Nope, still don't trust them to not be dumb (or malicious), so we backup first.
-        python $__tool_directory__/export.py
-        @ADMIN_AUTH@
-        @ORG_OR_GUESS@
-        --gff "$gff_out"
-        --fasta "$fasta_out"
-        --json "$json_out";
+	## Nope, still don't trust them to not be dumb (or malicious), so we backup first.
+	python $__tool_directory__/export.py
+	@ADMIN_AUTH@
+	@ORG_OR_GUESS@
+	--gff "$gff_out"
+	--fasta "$fasta_out"
+	--json "$json_out";
 
-        ## Now we delete
-        python $__tool_directory__/delete_features.py
-        @ADMIN_AUTH@
-        @ORG_OR_GUESS@
-        "$__user_email__"
-        > $output;
-    #else
-        echo "Nothing to do" > $output;
-    #end if
+	## Now we delete
+	python $__tool_directory__/delete_features.py
+	@ADMIN_AUTH@
+	@ORG_OR_GUESS@
+	"$__user_email__"
+	#if str($filter) != "all"
+		--type $filter
+	#end if
+	> $output;
 #else
     echo "Nothing to do" > $output;
 #end if
     ]]></command>
   <inputs>
     <expand macro="org_or_guess" />
+    <param name="filter" type="select" label="Feature Type Filter">
+        <option value="all">All</option>
+        <option value="mRNA">Genes</option>
+        <option value="terminator">Terminators</option>
+        <option value="tRNA">tRNAs</option>
+    </param>
     <param name="ask_one" type="boolean" truevalue="yes" falsevalue="" label="Are you SURE you want to do this?" help="It will PERMANENTLY delete all of the features on this organism."/>
-    <param name="ask_two" type="boolean" truevalue="yes" falsevalue="" label="Are you really, really SURE you want to do this?" help="There's NO coming back from this."/>
   </inputs>
   <outputs>
     <data format="tabular" name="output" label="Process and Error Log"/>
@@ -57,4 +61,3 @@
 @REFERENCES@
 ]]></help>
 </tool>
-
--- a/delete_organism.py	Sat Mar 04 18:00:52 2017 -0500
+++ b/delete_organism.py	Tue Jun 27 04:05:17 2017 -0400
@@ -1,4 +1,5 @@
 #!/usr/bin/env python
+from __future__ import print_function
 import argparse
 from webapollo import WebApolloInstance
 from webapollo import WAAuth, OrgOrGuess, GuessOrg, AssertUser
@@ -36,4 +37,3 @@
         # We see that deleteFeatures wants a uniqueName, and so we pass
         # is the uniquename field in the feature.
         print(wa.annotations.deleteFeatures([feature['uniquename']]))
-
--- a/delete_organism.xml	Sat Mar 04 18:00:52 2017 -0500
+++ b/delete_organism.xml	Tue Jun 27 04:05:17 2017 -0400
@@ -1,5 +1,5 @@
 <?xml version="1.0"?>
-<tool id="edu.tamu.cpt2.webapollo.delete_organism" name="Delete an Apollo record" version="1.0">
+<tool id="edu.tamu.cpt2.webapollo.delete_organism" name="Delete an Apollo record" version="1.0" profile="16.04">
   <description></description>
   <macros>
     <import>macros.xml</import>
@@ -57,4 +57,3 @@
 @REFERENCES@
 ]]></help>
 </tool>
-
--- a/export.py	Sat Mar 04 18:00:52 2017 -0500
+++ b/export.py	Tue Jun 27 04:05:17 2017 -0400
@@ -1,16 +1,18 @@
 #!/usr/bin/env python
+from __future__ import print_function
+import argparse
+import json
 import sys
+from Bio import SeqIO
+from BCBio import GFF
+from webapollo import WAAuth, WebApolloInstance, CnOrGuess, GuessCn
+from future import standard_library
+standard_library.install_aliases()
 try:
     import StringIO as io
 except ImportError:
     import io
 
-import json
-import argparse
-from Bio import SeqIO
-from BCBio import GFF
-from webapollo import WAAuth, WebApolloInstance, CnOrGuess, GuessCn
-
 
 def export(org_cn, seqs):
     org_data = wa.organisms.findOrganismByCn(org_cn)
--- a/fetch_organism_jbrowse.py	Sat Mar 04 18:00:52 2017 -0500
+++ b/fetch_organism_jbrowse.py	Tue Jun 27 04:05:17 2017 -0400
@@ -1,13 +1,54 @@
 #!/usr/bin/env python
+from __future__ import print_function
 import os
+import sys
+import time
 import argparse
-from webapollo import WAAuth, WebApolloInstance, GuessOrg, OrgOrGuess
+import filecmp
+import os.path
 import logging
 import subprocess
+from webapollo import WAAuth, WebApolloInstance, GuessOrg, OrgOrGuess
 logging.basicConfig(level=logging.INFO)
 log = logging.getLogger(__name__)
 
 
+def are_dir_trees_equal(dir1, dir2):
+    """
+    Compare two directories recursively. Files in each directory are
+    assumed to be equal if their names and contents are equal.
+
+    @param dir1: First directory path
+    @param dir2: Second directory path
+
+    @return: True if the directory trees are the same and
+        there were no errors while accessing the directories or files,
+        False otherwise.
+
+    # http://stackoverflow.com/questions/4187564/recursive-dircmp-compare-two-directories-to-ensure-they-have-the-same-files-and/6681395#6681395
+    """
+
+    dirs_cmp = filecmp.dircmp(dir1, dir2)
+    if len(dirs_cmp.left_only) > 0 or len(dirs_cmp.right_only) > 0 or \
+            len(dirs_cmp.funny_files) > 0:
+        print(('LEFT', dirs_cmp.left_only))
+        print(('RIGHT', dirs_cmp.right_only))
+        print(('FUNNY', dirs_cmp.funny_files))
+        return False
+    (_, mismatch, errors) = filecmp.cmpfiles(
+        dir1, dir2, dirs_cmp.common_files, shallow=False)
+    if len(mismatch) > 0 or len(errors) > 0:
+        print(mismatch)
+        print(errors)
+        return False
+    for common_dir in dirs_cmp.common_dirs:
+        new_dir1 = os.path.join(dir1, common_dir)
+        new_dir2 = os.path.join(dir2, common_dir)
+        if not are_dir_trees_equal(new_dir1, new_dir2):
+            return False
+    return True
+
+
 if __name__ == '__main__':
     parser = argparse.ArgumentParser(description='Sample script to add an attribute to a feature via web services')
     WAAuth(parser)
@@ -26,9 +67,46 @@
     if not os.path.exists(args.target_dir):
         os.makedirs(args.target_dir)
 
+    if not os.path.exists(os.path.join(org['directory'], 'seq')):
+        sys.stderr.write("Missing seq directory BEFORE copy")
+        sys.exit(1)
+
     cmd = [
-        'cp', '-R',
-        org['directory'],
+        'rsync', '-avr',
+        org['directory'].rstrip('/') + '/',
+        os.path.join(args.target_dir, 'data', '')
+    ]
+    # We run this OBSESSIVELY because my org had a hiccup where the origin
+    # (silent) cp -R failed at one point. This caused MANY HEADACHES.
+    #
+    # Our response is to run this 3 times (in case the issue is temporary),
+    # with delays in between. And ensure that we have the correct number of
+    # files / folders before and after.
+    sys.stderr.write(' '.join(cmd))
+    sys.stderr.write('\n')
+    sys.stderr.write(subprocess.check_output(cmd))
+    if not are_dir_trees_equal(
+        os.path.join(org['directory'].rstrip('/')),
         os.path.join(args.target_dir, 'data')
-    ]
-    subprocess.check_call(cmd)
+    ):
+        # Not good
+        time.sleep(5)
+        sys.stderr.write('\n')
+        sys.stderr.write(' '.join(cmd))
+        sys.stderr.write('\n')
+        sys.stderr.write(subprocess.check_output(cmd))
+        if not are_dir_trees_equal(
+            os.path.join(org['directory'].rstrip('/'), 'data'),
+            os.path.join(args.target_dir, 'data')
+        ):
+            time.sleep(5)
+            sys.stderr.write('\n')
+            sys.stderr.write(' '.join(cmd))
+            sys.stderr.write('\n')
+            sys.stderr.write(subprocess.check_output(cmd))
+            if not are_dir_trees_equal(
+                os.path.join(org['directory'].rstrip('/'), 'data'),
+                os.path.join(args.target_dir, 'data')
+            ):
+                sys.stderr.write('FAILED THREE TIMES TO COPY. SOMETHING IS WRONG WRONG WRONG.')
+                sys.exit(2)
--- a/fetch_organism_jbrowse.xml	Sat Mar 04 18:00:52 2017 -0500
+++ b/fetch_organism_jbrowse.xml	Tue Jun 27 04:05:17 2017 -0400
@@ -1,5 +1,5 @@
 <?xml version="1.0"?>
-<tool id="edu.tamu.cpt2.webapollo.fetch_jbrowse" name="Retrieve JBrowse" version="3.0">
+<tool id="edu.tamu.cpt2.webapollo.fetch_jbrowse" name="Retrieve JBrowse" version="3.2" profile="16.04">
   <description>for an organism, from Apollo</description>
   <macros>
     <import>macros.xml</import>
@@ -13,9 +13,11 @@
 @ADMIN_AUTH@
 
 @ORG_OR_GUESS@
-$jbrowse.files_path/;
+$jbrowse.files_path/ &&
 
-cp $dummyIndex $jbrowse;
+cp $dummyIndex $jbrowse &&
+find $jbrowse.files_path -type f -printf '<li><a href="%P">%P</a></li>\n' | sort >> $jbrowse &&
+echo '</ul></body></html>' >> $jbrowse;
 
 ]]></command>
   <configfiles>
@@ -40,8 +42,11 @@
                 Convert to Standalone" tool in Galaxy to "upgrade" to a full
                 JBrowse instance.
               </p>
-          </body>
-      </html>
+              <p>
+                  The following list is provided for your convenience / debugging.
+              </p>
+              <h2>Contained Files</h2>
+              <ul>
           ]]>
       </configfile>
   </configfiles>
--- a/json2iframe.py	Sat Mar 04 18:00:52 2017 -0500
+++ b/json2iframe.py	Tue Jun 27 04:05:17 2017 -0400
@@ -1,7 +1,8 @@
 #!/usr/bin/env python
+from __future__ import print_function
+import argparse
 import json
-import base64
-import argparse
+
 
 if __name__ == '__main__':
     parser = argparse.ArgumentParser(description='Sample script to add an attribute to a feature via web services')
@@ -26,6 +27,5 @@
             </body>
         </html>
     """
-    # HTML_TPL = base64.b64decode(HTML_TPL.replace('\n', ''))
 
-    print HTML_TPL.format(base_url=args.external_apollo_url, chrom="", orgId=data[0]['id'])
+    print(HTML_TPL.format(base_url=args.external_apollo_url, chrom="", orgId=data[0]['id']))
--- a/list_organisms.py	Sat Mar 04 18:00:52 2017 -0500
+++ b/list_organisms.py	Tue Jun 27 04:05:17 2017 -0400
@@ -1,4 +1,5 @@
 #!/usr/bin/env python
+from __future__ import print_function
 import json
 import argparse
 from webapollo import WAAuth, WebApolloInstance, AssertUser, accessible_organisms
--- a/macros.xml	Sat Mar 04 18:00:52 2017 -0500
+++ b/macros.xml	Tue Jun 27 04:05:17 2017 -0400
@@ -116,7 +116,7 @@
 **Citation**
 
 If you use this tool in Galaxy, please cite:
-Eric Rasche (2016), `Galaxy wrapper <https://github.com/TAMU-CPT/galaxy-webapollo>`_
+Eric Rasche (2016), `Galaxy Apollo Tools <https://github.com/galaxy-genome-annotation/galaxy-tools/tree/master/tools/apollo>`_
 ]]>
     </token>
 </macros>
--- a/webapollo.py	Sat Mar 04 18:00:52 2017 -0500
+++ b/webapollo.py	Tue Jun 27 04:05:17 2017 -0400
@@ -1,23 +1,29 @@
-import requests
-import json
-import os
+from __future__ import print_function
+import argparse
 import collections
-try:
-    import StringIO as io
-except:
-    import io
+import json
 import logging
+import os
+import requests
 import time
-import argparse
+from future import standard_library
+from builtins import next
+from builtins import str
+from builtins import object
 from abc import abstractmethod
 from BCBio import GFF
 from Bio import SeqIO
+standard_library.install_aliases()
+try:
+    import StringIO as io
+except BaseException:
+    import io
 logging.getLogger("requests").setLevel(logging.CRITICAL)
 log = logging.getLogger()
 
 
 #############################################
-###### BEGIN IMPORT OF CACHING LIBRARY ######
+#      BEGIN IMPORT OF CACHING LIBRARY      #
 #############################################
 # This code is licensed under the MIT       #
 # License and is a copy of code publicly    #
@@ -25,6 +31,7 @@
 # e27332bc82f4e327aedaec17c9b656ae719322ed  #
 # of https://github.com/tkem/cachetools/    #
 #############################################
+
 class DefaultMapping(collections.MutableMapping):
 
     __slots__ = ()
@@ -65,6 +72,7 @@
             self[key] = value = default
         return value
 
+
 DefaultMapping.register(dict)
 
 
@@ -381,21 +389,24 @@
 
 
 #############################################
-######  END IMPORT OF CACHING LIBRARY  ######
+#       END IMPORT OF CACHING LIBRARY       #
 #############################################
 
+
 cache = TTLCache(
-    100, # Up to 100 items
-    5 * 60 # 5 minute cache life
+    100,  # Up to 100 items
+    5 * 60  # 5 minute cache life
 )
 userCache = TTLCache(
-    2, # Up to 2 items
-    60 # 1 minute cache life
+    2,  # Up to 2 items
+    60  # 1 minute cache life
 )
 
+
 class UnknownUserException(Exception):
     pass
 
+
 def WAAuth(parser):
     parser.add_argument('apollo', help='Complete Apollo URL')
     parser.add_argument('username', help='WA Username')
@@ -468,8 +479,6 @@
         self.apollo_url = url
         self.username = username
         self.password = password
-        # TODO: Remove after apollo 2.0.6.
-        self.clientToken = time.time()
 
         self.annotations = AnnotationsClient(self)
         self.groups = GroupsClient(self)
@@ -478,6 +487,10 @@
         self.users = UsersClient(self)
         self.metrics = MetricsClient(self)
         self.bio = RemoteRecord(self)
+        self.status = StatusClient(self)
+        self.canned_comments = CannedCommentsClient(self)
+        self.canned_keys = CannedKeysClient(self)
+        self.canned_values = CannedValuesClient(self)
 
     def __str__(self):
         return '<WebApolloInstance at %s>' % self.apollo_url
@@ -539,6 +552,12 @@
             data[prop] = getattr(self, prop)
         return data
 
+    def orgPerms(self):
+        for orgPer in self.organismPermissions:
+            if len(orgPer['permissions']) > 2:
+                orgPer['permissions'] = json.loads(orgPer['permissions'])
+                yield orgPer
+
     def __str__(self):
         return '<User %s: %s %s <%s>>' % (self.userId, self.firstName,
                                           self.lastName, self.username)
@@ -565,7 +584,6 @@
         data.update({
             'username': self._wa.username,
             'password': self._wa.password,
-            'clientToken': self._wa.clientToken,
         })
 
         r = requests.post(url, data=json.dumps(data), headers=headers,
@@ -677,22 +695,49 @@
         data = self._update_data(data)
         return self.request('getComments', data)
 
-    def addComments(self, feature_id, comment):
-        #TODO: This is probably not great and will delete comments, if I had to guess...
+    def addComments(self, feature_id, comments):
+        # TODO: This is probably not great and will delete comments, if I had to guess...
         data = {
             'features': [
                 {
                     'uniquename': feature_id,
-                    'comments': [comment]
+                    'comments': comments
                 }
             ],
         }
         data = self._update_data(data)
-        return self.request('getComments', data)
+        return self.request('addComments', data)
+
+    def addAttributes(self, feature_id, attributes):
+        nrps = []
+        for (key, values) in attributes.items():
+            for value in values:
+                nrps.append({
+                    'tag': key,
+                    'value': value
+                })
 
-    def addAttribute(self, features):
         data = {
-            'features': features,
+            'features': [
+                {
+                    'uniquename': feature_id,
+                    'non_reserved_properties': nrps
+                }
+            ]
+        }
+        data = self._update_data(data)
+        return self.request('addAttribute', data)
+
+    def deleteAttribute(self, feature_id, key, value):
+        data = {
+            'features': [
+                {
+                    'uniquename': feature_id,
+                    'non_reserved_properties': [
+                        {'tag': key, 'value': value}
+                    ]
+                }
+            ]
         }
         data = self._update_data(data)
         return self.request('addAttribute', data)
@@ -991,6 +1036,198 @@
         return self.request('write', data)
 
 
+class StatusClient(Client):
+    CLIENT_BASE = '/availableStatus/'
+
+    def addStatus(self, value):
+        data = {
+            'value': value
+        }
+
+        return self.request('createStatus', data)
+
+    def findAllStatuses(self):
+        return self.request('showStatus', {})
+
+    def findStatusByValue(self, value):
+        statuses = self.findAllStatuses()
+        statuses = [x for x in statuses if x['value'] == value]
+        if len(statuses) == 0:
+            raise Exception("Unknown status value")
+        else:
+            return statuses[0]
+
+    def findStatusById(self, id_number):
+        statuses = self.findAllStatuses()
+        statuses = [x for x in statuses if str(x['id']) == str(id_number)]
+        if len(statuses) == 0:
+            raise Exception("Unknown ID")
+        else:
+            return statuses[0]
+
+    def updateStatus(self, id_number, new_value):
+        data = {
+            'id': id_number,
+            'new_value': new_value
+        }
+
+        return self.request('updateStatus', data)
+
+    def deleteStatus(self, id_number):
+        data = {
+            'id': id_number
+        }
+
+        return self.request('deleteStatus', data)
+
+
+class CannedCommentsClient(Client):
+    CLIENT_BASE = '/cannedComment/'
+
+    def addComment(self, comment, metadata=""):
+        data = {
+            'comment': comment,
+            'metadata': metadata
+        }
+
+        return self.request('createComment', data)
+
+    def findAllComments(self):
+        return self.request('showComment', {})
+
+    def findCommentByValue(self, value):
+        comments = self.findAllComments()
+        comments = [x for x in comments if x['comment'] == value]
+        if len(comments) == 0:
+            raise Exception("Unknown comment")
+        else:
+            return comments[0]
+
+    def findCommentById(self, id_number):
+        comments = self.findAllComments()
+        comments = [x for x in comments if str(x['id']) == str(id_number)]
+        if len(comments) == 0:
+            raise Exception("Unknown ID")
+        else:
+            return comments[0]
+
+    def updateComment(self, id_number, new_value, metadata=None):
+        data = {
+            'id': id_number,
+            'new_comment': new_value
+        }
+
+        if metadata is not None:
+            data['metadata'] = metadata
+
+        return self.request('updateComment', data)
+
+    def deleteComment(self, id_number):
+        data = {
+            'id': id_number
+        }
+
+        return self.request('deleteComment', data)
+
+
+class CannedKeysClient(Client):
+    CLIENT_BASE = '/cannedKey/'
+
+    def addKey(self, key, metadata=""):
+        data = {
+            'key': key,
+            'metadata': metadata
+        }
+
+        return self.request('createKey', data)
+
+    def findAllKeys(self):
+        return self.request('showKey', {})
+
+    def findKeyByValue(self, value):
+        keys = self.findAllKeys()
+        keys = [x for x in keys if x['label'] == value]
+        if len(keys) == 0:
+            raise Exception("Unknown key")
+        else:
+            return keys[0]
+
+    def findKeyById(self, id_number):
+        keys = self.findAllKeys()
+        keys = [x for x in keys if str(x['id']) == str(id_number)]
+        if len(keys) == 0:
+            raise Exception("Unknown ID")
+        else:
+            return keys[0]
+
+    def updateKey(self, id_number, new_key, metadata=None):
+        data = {
+            'id': id_number,
+            'new_key': new_key
+        }
+
+        if metadata is not None:
+            data['metadata'] = metadata
+
+        return self.request('updateKey', data)
+
+    def deleteKey(self, id_number):
+        data = {
+            'id': id_number
+        }
+
+        return self.request('deleteKey', data)
+
+
+class CannedValuesClient(Client):
+    CLIENT_BASE = '/cannedValue/'
+
+    def addValue(self, value, metadata=""):
+        data = {
+            'value': value,
+            'metadata': metadata
+        }
+
+        return self.request('createValue', data)
+
+    def findAllValues(self):
+        return self.request('showValue', {})
+
+    def findValueByValue(self, value):
+        values = self.findAllValues()
+        values = [x for x in values if x['label'] == value]
+        if len(values) == 0:
+            raise Exception("Unknown value")
+        else:
+            return values[0]
+
+    def findValueById(self, id_number):
+        values = self.findAllValues()
+        values = [x for x in values if str(x['id']) == str(id_number)]
+        if len(values) == 0:
+            raise Exception("Unknown ID")
+        else:
+            return values[0]
+
+    def updateValue(self, id_number, new_value, metadata=None):
+        data = {
+            'id': id_number,
+            'new_value': new_value
+        }
+
+        if metadata is not None:
+            data['metadata'] = metadata
+
+        return self.request('updateValue', data)
+
+    def deleteValue(self, id_number):
+        data = {
+            'id': id_number
+        }
+
+        return self.request('deleteValue', data)
+
+
 class OrganismsClient(Client):
     CLIENT_BASE = '/organism/'
 
@@ -1222,7 +1459,7 @@
 
 
 def _tnType(feature):
-    if feature.type in ('gene', 'mRNA', 'exon', 'CDS'):
+    if feature.type in ('gene', 'mRNA', 'exon', 'CDS', 'terminator', 'tRNA'):
         return feature.type
     else:
         return 'exon'
@@ -1365,8 +1602,55 @@
     # Return org list
     return orgs
 
-## This is all for implementing the command line interface for testing.
+
+def galaxy_list_users(trans, *args, **kwargs):
+    email = trans.get_user().email
+    wa = WebApolloInstance(
+        os.environ['GALAXY_WEBAPOLLO_URL'],
+        os.environ['GALAXY_WEBAPOLLO_USER'],
+        os.environ['GALAXY_WEBAPOLLO_PASSWORD']
+    )
+    # Assert that the email exists in apollo
+    try:
+        gx_user = wa.requireUser(email)
+    except UnknownUserException:
+        return []
 
+    # Key for cached data
+    cacheKey = 'users-' + email
+    # We don't want to trust "if key in cache" because between asking and fetch
+    # it might through key error.
+    if cacheKey not in cache:
+        # However if it ISN'T there, we know we're safe to fetch + put in
+        # there.
+        data = _galaxy_list_users(wa, gx_user, *args, **kwargs)
+        cache[cacheKey] = data
+        return data
+    try:
+        # The cache key may or may not be in the cache at this point, it
+        # /likely/ is. However we take no chances that it wasn't evicted between
+        # when we checked above and now, so we reference the object from the
+        # cache in preparation to return.
+        data = cache[cacheKey]
+        return data
+    except KeyError:
+        # If access fails due to eviction, we will fail over and can ensure that
+        # data is inserted.
+        data = _galaxy_list_users(wa, gx_user, *args, **kwargs)
+        cache[cacheKey] = data
+        return data
+
+
+def _galaxy_list_users(wa, gx_user, *args, **kwargs):
+    # Fetch the users.
+    user_data = []
+    for user in wa.users.loadUsers():
+        # Reformat
+        user_data.append((user.username, user.username, False))
+    return user_data
+
+
+# This is all for implementing the command line interface for testing.
 class obj(object):
     pass
 
@@ -1381,16 +1665,46 @@
         o.email = self.un
         return o
 
+
+def retry(closure, sleep=1, limit=5):
+    """
+    Apollo has the bad habit of returning 500 errors if you call APIs
+    too quickly, largely because of the unholy things that happen in
+    grails.
+
+    To deal with the fact that we cannot send an addComments call too
+    quickly after a createFeature call, we have this function that will
+    keep calling a closure until it works.
+    """
+    count = 0
+    while True:
+        count += 1
+
+        if count >= limit:
+            return False
+        try:
+            # Try calling it
+            closure()
+            # If successful, exit
+            return True
+        except Exception as e:
+            log.info(str(e)[0:100])
+            time.sleep(sleep)
+
+
 if __name__ == '__main__':
     parser = argparse.ArgumentParser(description='Test access to apollo server')
     parser.add_argument('email', help='Email of user to test')
-    parser.add_argument('--action', choices=['org', 'group'], default='org', help='Data set to test, fetch a list of groups or users known to the requesting user.')
+    parser.add_argument('--action', choices=['org', 'group', 'users'], default='org', help='Data set to test, fetch a list of groups or users known to the requesting user.')
     args = parser.parse_args()
 
     trans = fakeTrans(args.email)
     if args.action == 'org':
         for f in galaxy_list_orgs(trans):
             print(f)
-    else:
+    elif args.action == 'group':
         for f in galaxy_list_groups(trans):
             print(f)
+    else:
+        for f in galaxy_list_users(trans):
+            print(f)