source: orange-bioinformatics/obiGeneAtlas.py @ 1613:c3560c335bf9

Revision 1613:c3560c335bf9, 23.7 KB checked in by Ales Erjavec <ales.erjavec@…>, 2 years ago (diff)

Changed results from the GeneAtlas REST api. Added cache versioning.

Line 
1"""
2========================================
3Gene Expression Atlas (``obiGeneAtlas``)
4========================================
5
6Interface to Gene Expression Atlas.
7
8`Gene Expression Atlas <http://www.ebi.ac.uk/gxa/>`_ is a curated subset of
9gene expression experiments in Array Express Archive.
10
11.. autofunction:: gene_expression_atlas
12
13.. autofunction:: default_gene_matcher
14
15.. autofunction:: to_taxid
16
17"""
18
19import os, sys
20from collections import defaultdict, namedtuple
21
22from Orange.utils import serverfiles
23
24GeneResults = namedtuple("GeneResults", "id name synonyms expressions")
25ExpressionResults = namedtuple("ExpressionResults", "ef efv up down experiments")
26ExperimentExpression = namedtuple("ExperimentExpression", "accession expression pvalue")
27
28##
29GeneAtlasResult = GeneResults
30AtlasExpressions = ExpressionResults
31AtlasExperiment = ExperimentExpression
32##
33import shelve
34import orngServerFiles
35import obiGene
36from contextlib import closing
37
38CACHE_VERSION = 1
39
40
41def _cache(name="AtlasGeneResult.shelve"):
42    """ Return a open cache instance (a shelve object).
43    """
44    if not os.path.exists(orngServerFiles.localpath("GeneAtlas")):
45        try:
46            os.makedirs(orngServerFiles.localpath("GeneAtlas"))
47        except OSError:
48            pass
49    cache = shelve.open(orngServerFiles.localpath("GeneAtlas", name))
50    if cache.get(name + "__CACHE_VERSION__", None) == CACHE_VERSION:
51        return cache
52    else:
53        cache = shelve.open(orngServerFiles.localpath("GeneAtlas", name), "n")
54        cache[name + "__CACHE_VERSION__"] = CACHE_VERSION
55        return cache
56
57
58SLEEP_TIME_MULTIPLIER = 3.0
59
60def gene_expression_atlas(genes, progress_callback=None):
61    """ Return GeneResults instances for genes (genes must be valid ensembl ids).
62    """
63    import time
64    genes = list(genes)
65    result_dict = {}
66    genes_not_cached = []
67    # See which genes are already cached
68    with closing(_cache()) as cache:
69        for gene in genes:
70            if str(gene) in cache:
71                result_dict[gene] = cache[str(gene)]
72            else:
73                genes_not_cached.append(gene)
74   
75    batch_size = 10
76    start = 0
77    res = []
78    while start < len(genes_not_cached):
79        batch = genes_not_cached[start: start + batch_size]
80        start += batch_size
81        start_time = time.time()
82        batch_res = batch_gene_atlas_expression(batch)
83        # Cache the new results.
84        # TODO: handle genes without any results.
85        genes_with_no_results = set(batch) - set(r.id for r in batch_res) 
86        with closing(_cache()) as cache:
87            for atlas_res in batch_res:
88                cache[str(atlas_res.id)] = atlas_res
89                result_dict[atlas_res.id] = atlas_res
90            for g in genes_with_no_results:
91                cache[str(g)] = None
92        res.extend(batch_res)
93        # Sleep
94        if start % (batch_size * 10) == 0:
95            # every 10 batches wait one minute before continuing.
96            time.sleep(60)
97        else:
98            time.sleep(min(20.0, SLEEP_TIME_MULTIPLIER*(time.time() - start_time)))
99           
100        if progress_callback:
101            progress_callback(100.0 * start / len(genes_not_cached))
102   
103    return [result_dict.get(g, None) for g in genes]
104
105   
106def batch_gene_atlas_expression(genes):
107    cond = GenePropertyCondition("Ensgene", "Is", genes)
108    res = run_query(cond, format="json")
109    results = res["results"]
110    results_genes = []
111    for one_result in results:
112        gene = one_result["gene"]
113        id = gene["id"]
114        name = gene["name"]
115        synonyms = gene.get("synonyms", [])
116        expressions = one_result["expressions"]
117        result_expressions = []
118        for expression in expressions:
119            ef = expression["ef"]
120            efv = expression["efv"]
121            up = expression["upExperiments"]
122            down = expression["downExperiments"]
123            experiments = expression["experiments"]
124            result_experiment = []
125            for exp in experiments:
126                if "accession" in exp:
127                    exp_accession = exp["accession"]
128                elif "experimentAccession" in exp:
129                    exp_accession = exp["experimentAccession"]
130                else:
131                    raise KeyError()
132                if "expression" in exp:
133                    updown = exp["expression"]
134                elif "updn" in exp:
135                    updown = exp["updn"]
136                else:
137                    raise KeyError
138                pval = exp["pvalue"]
139                result_experiment.append(ExperimentExpression(exp_accession, updown, pval))
140            result_expressions.append(ExpressionResults(ef, efv, up, down, result_experiment))
141        results_genes.append(GeneResults(id, name, synonyms, result_expressions))
142    return results_genes
143
144
145def default_gene_matcher(organism):
146    """ Return a default gene matcher for organism
147    (targeting Ensembl gene ids).
148   
149    """
150    taxid = to_taxid(organism)
151    matcher = obiGene.matcher([obiGene.GMEnsembl(taxid),
152                               obiGene.GMNCBI(taxid)])
153    matcher.set_targets(obiGene.EnsembleGeneInfo(taxid).keys())
154    return matcher
155
156from Orange.utils import lru_cache
157
158@lru_cache(maxsize=3)
159def _cached_default_gene_matcher(organism): 
160    return default_gene_matcher(organism)
161   
162
163def get_atlas_summary(genes, organism, gene_matcher=None,
164                      progress_callback=None):
165    """ Return 3 dictionaries containing a summary of atlas information
166    about three experimental factors:
167   
168        - Organism Part (OP)
169        - Disease State (DS)
170        - Cell type (CT)
171   
172    Each dictionary contains query genes as keys. Values are dictionaries
173    mapping factor values to a 2-tuple containig the count of up regulated
174    and down regulated experiments.
175   
176    Example ::
177   
178        >>> get_atlas_summary(["RUNX1"], "Homo sapiens")
179        ({u'RUNX1': ...
180       
181    """
182    if gene_matcher is None:
183        gene_matcher = _cached_default_gene_matcher(organism)
184       
185    matched, unmatched = [], []
186    for gene, match in zip(genes, map(gene_matcher.umatch, genes)):
187        if match:
188            matched.append(match)
189        else:
190            unmatched.append(gene)
191    if unmatched:
192        import warnings
193        warnings.warn("Unmatched genes " + "," .join(["%r" % g for g in unmatched]))
194   
195    results = gene_expression_atlas(matched, progress_callback=progress_callback)
196   
197    def collect_ef_summary(result, ef, summary):
198        for exp in result.expressions:
199            if exp.ef == ef:
200                if any([exp.up, exp.down]):
201                    summary[result.name][exp.efv] = (exp.up, exp.down)
202       
203           
204    op, ds, ct = defaultdict(dict), defaultdict(dict), defaultdict(dict)
205    for res in results:
206        if res:
207            collect_ef_summary(res, "organism_part", op)
208            collect_ef_summary(res, "disease_state", ds)
209            collect_ef_summary(res, "cell_type", ct)
210       
211    return dict(op), dict(ds), dict(ct)
212
213def drop_none(iter):
214    """ Drop all ``None`` from the iterator.
215    """
216    for e in iter:
217        if e is not None:
218            yield e
219           
220def construct_atlas_gene_sets(genes, organism, factors=["organism_part",
221                                    "disease_state", "cell_type"],
222                              max_pvalue=1e-5):
223    """ Construct gene sets for atlas experimental factor values in
224    ``factors``.
225    """
226    results = gene_expression_atlas(genes)
227    sets = defaultdict(list)
228   
229    for res in drop_none(results):
230        for exp in res.expressions:
231            if exp.ef not in factors:
232                continue
233            diff_exp = [e for e in exp.experiments \
234                        if e.pvalue <= max_pvalue]
235            if diff_exp:
236                sets[exp.ef, exp.efv].append(res.id)
237
238    organism = "+".join(organism.lower().split())
239    from obiGeneSets import GeneSets, GeneSet
240   
241    def display_string(name):
242        return name.capitalize().replace("_", " ")
243   
244    gene_sets = []
245    for (ef, efv), genes in sets.items():
246        ef_display = display_string(ef)
247        gs = GeneSet(genes, "Diff. expressed in %s=%s." % (ef_display, efv), id=ef + ":" + efv,
248                     description="Diff. expressed in %s=%s" % (ef_display, efv),
249                     link="http://www.ebi.ac.uk/gxa/qrs?specie_0={organism}&gprop_0=&gnot_0=&gval_0=&fact_1=&fexp_1=UPDOWN&fmex_1=&fval_1=%22{efv}%22+&view=hm".format( \
250                            organism=organism, efv="+".join(efv.lower().split())),
251                     hierarchy=("Tissues", ef_display))
252        gene_sets.append(gs)
253    return GeneSets(gene_sets)
254
255
256# Mapping for common taxids from obiTaxonomy
257TAXID_TO_ORG = {"": "Anopheles gambiae",
258                "3702": "Arabidopsis thaliana",
259                "9913": "Bos taurus",
260                "6239": "Caenorhabditis elegans",
261                "7955": "Danio rerio",
262                "7227": "Drosophila melanogaster",
263                "": "Epstein barr virus",
264                "": "Gallus gallus",
265                "9606": "Homo sapiens",
266                "": "Human cytomegalovirus",
267                "": "Kaposi sarcoma-associated herpesvirus",
268                "10090": "Mus musculus",
269                "10116": "Rattus norvegicus",
270                "4932": "Saccharomyces cerevisiae",
271                "4896": "Schizosaccharomyces pombe",
272                "8355": "Xenopus laevis"
273     }
274
275def to_taxid(name):
276    dd = dict((v, k) for k, v in TAXID_TO_ORG.items())
277    if name in dd:
278        return dd[name]
279    else:
280        import obiTaxonomy as tax
281        ids = tax.to_taxid(name, mapTo=TAXID_TO_ORG.keys())
282        if ids:
283            return ids.pop()
284        else:
285            raise ValueError("Unknown organism.")
286
287
288__doc__ += """\
289Low level REST query interface
290------------------------------
291
292Use `query_atlas_simple` for simple querys.
293
294Example (query human genes for experiments in which they are up regulated) ::
295
296    >>> run_simple_query(genes=["SORL1", "PSIP1", "CDKN1C"], regulation="up", organism="Homo sapiens")
297    {u'...
298   
299Or use the `AtlasCondition` subclasses in this module to construct a more
300advanced query and use the `run_query` function.
301
302Example (query human genes annotated to the GO term 'transporter activity'
303that are up regulated in the liver in at least three experiments) ::
304
305    >>> go_cond = GenePropertyCondition("Goterm", "Is", "transporter activity")
306    >>> liver_cond = ExperimentalFactorCondition("Organism_part", "up", 3, "liver")
307    >>> org_cond = OrganismCondition("Homo sapiens")
308    >>> cond_list = ConditionList([go_cond, liver_cond, org_cond])
309    >>> run_query(cond_list)
310    {u'...
311
312"""
313
314import urllib2
315from  StringIO import StringIO
316import json
317from xml.etree.ElementTree import ElementTree
318
319parse_json = json.load
320
321
322def parse_xml(stream):
323    """ Parse an xml stream into an instance of xml.etree.ElementTree.ElementTree.
324    """
325    return ElementTree(file=stream)
326
327
328class GeneExpressionAtlasConenction(object):
329    """ A connection to Gene Expression Atlas database.
330    """
331    DEFAULT_ADDRESS = "http://www.ebi.ac.uk:80/gxa/"
332    DEFAULT_CACHE = orngServerFiles.localpath("GeneAtlas", "GeneAtlasConnectionCache.shelve")
333    def __init__(self, address=None, timeout=30, cache=None):
334        """ Initialize the conenction.
335       
336        :param address: Address of the server.
337        :param timeout: Socket timeout.
338        :param cache : A dict like object to use as a cache.
339       
340        """
341        self.address = address if address is not None else self.DEFAULT_ADDRESS
342        self.timeout = timeout
343        self.cache = cache if cache is not None else self.DEFAULT_CACHE
344   
345    def query(self, condition, format="json", start=None, rows=None, indent=False):
346        url = self.address + "api/vx?" + condition.rest()
347        if start is not None and rows is not None:
348            url += "&start={0}&rows={1}".format(start, rows)
349        url += "&format={0}".format(format)
350        if indent:
351            url += "&indent"
352#        print url
353        if self.cache is not None:
354            return self._query_cached(url, format)
355        else:
356            return urllib2.urlopen(url)
357        return response
358   
359    def _query_cached(self, url, format):
360        if self.cache is not None:
361            with self.open_cache() as cache:
362                cached = url in cache
363           
364            if not cached:
365                response = urllib2.urlopen(url)
366                contents = response.read()
367                # Test if the contents is a valid json or xml string (some
368                # times the stream just stops in the middle, so this makes
369                # sure we don't cache an invalid response
370                # TODO: what about errors (e.g. 'cannot handle the
371                # query in a timely fashion'
372                if format == "json":
373                    parse_json(StringIO(contents))
374                else:
375                    parse_xml(StringIO(contents))
376                   
377                with self.open_cache() as cache:
378                    cache[url] = contents
379            else:
380                with self.open_cache() as cache:
381                    contents = cache[url]
382            return StringIO(contents)
383        else:
384            return urllib2.urlopen(url)
385       
386    def open_cache(self):
387        if isinstance(self.cache, basestring):
388            return closing(shelve.open(self.cache))
389        elif hasattr(self.cache, "close"):
390            return closing(self.cache)
391        elif self.cache is None:
392            return fake_closing({})
393        else:
394            return fake_closing(self.cache)
395       
396       
397from contextlib import contextmanager
398@contextmanager
399def fake_closing(obj):
400    yield obj
401   
402   
403# Names of all Gene Property filter names
404GENE_FILTERS = \
405    ["Name", # Gene name
406     "Goterm", #Gene Ontology Term
407     "Interproterm", #InterPro Term
408     "Disease", #Gene-Disease Assocation
409     "Keyword", #Gene Keyword
410     "Protein", #Protein
411
412     "Dbxref", #Other Database Cross-Refs
413     "Embl", #EMBL-Bank ID
414     "Ensfamily", #Ensembl Family
415     "Ensgene", #Ensembl Gene ID
416
417     "Ensprotein", #Ensembl Protein ID
418     "Enstranscript", #Ensembl Transcript ID
419     "Goid", #Gene Ontology ID
420     "Image", #IMAGE ID
421     "Interproid", #InterPro ID
422     "Locuslink", #Entrez Gene ID
423
424     "Omimid", #OMIM ID
425     "Orf", #ORF
426     "Refseq", #RefSeq ID
427     "Unigene", #UniGene ID
428     "Uniprot", #UniProt Accession
429
430     "Hmdb", #HMDB ID
431     "Chebi", #ChEBI ID
432     "Cas", #CAS
433     "Uniprotmetenz", #Uniprotmetenz
434     "Gene", #Gene Name or Identifier
435     "Synonym", #Gene Synonym
436     ]
437   
438# Valid Gene Property filter qualifiers
439GENE_FILTER_QUALIFIERS =\
440    ["Is",
441     "IsNot"
442     ]
443
444# Organisms in the Atlas
445ATLAS_ORGANISMS = \
446    ["Anopheles gambiae",
447     "Arabidopsis thaliana",
448     "Bos taurus",
449     "Caenorhabditis elegans",
450     "Danio rerio",
451     "Drosophila melanogaster",
452     "Epstein barr virus",
453     "Gallus gallus",
454     "Homo sapiens",
455     "Human cytomegalovirus",
456     "Kaposi sarcoma-associated herpesvirus",
457     "Mus musculus",
458     "Rattus norvegicus",
459     "Saccharomyces cerevisiae",
460     "Schizosaccharomyces pombe",
461#     "Unknown",
462     "Xenopus laevis"
463     ]
464   
465def ef_ontology():
466    """ Return the `EF <http://www.ebi.ac.uk/efo/>`_ (Experimental Factor) ontology
467    """
468    import obiOntology
469    import orngServerFiles
470    # Should this be in the OBOFoundry (Ontology) domain
471    try:
472        file = open(orngServerFiles.localpath_download("ArrayExpress", "efo.obo"), "rb")
473    except urllib2.HTTPError:
474        file = urllib2.urlopen("http://efo.svn.sourceforge.net/svnroot/efo/trunk/src/efoinobo/efo.obo")
475    return obiOntology.OBOOntology(file)
476
477
478class Condition(object):
479    """ Base class for Gene Expression Atlas query condition
480    """
481    def validate(self):
482        """ Validate condition in a subclass.
483        """
484        raise NotImplementedError
485   
486    def rest(self):
487        """ Return a REST query part in a subclass.
488        """
489        raise NotImplementedError
490   
491   
492class ConditionList(list, Condition):
493    """ A list of AtlasCondition instances.
494    """ 
495    def validate(self):
496        for item in self:
497            item.validate()
498       
499    def rest(self):
500        return "&".join(cond.rest() for cond in self)
501
502
503class GenePropertyCondition(Condition):
504    """ An atlas gene filter condition.
505   
506    :param property: Property of the gene. If None or "" all properties
507        will be searched.
508    :param qualifier: Qualifier can be 'Is' or 'IsNot'
509    :param value: The value to search for.
510   
511    Example ::
512   
513        >>> # Condition on a gene name
514        >>> condition = GenePropertyCondition("Name", "Is", "AS3MT")
515        >>> # Condition on genes from a GO Term
516        >>> condition = GenePropertyCondition("Goterm", "Is", "p53 binding")
517        >>> # Condition on disease association
518        >>> condition = GenePropertyCondition("Disease", "Is", "cancer")
519       
520    """
521    def __init__(self, property, qualifier, value):
522        self.property = property or ""
523        self.qualifier = qualifier
524        if isinstance(value, basestring):
525            self.value = value.replace(" ", "+")
526        elif isinstance(value, list):
527            self.value = "+".join(value)
528        else:
529            raise ValueError(value)
530       
531        self.validate()
532       
533    def validate(self):
534        assert(self.property in GENE_FILTERS + [""])
535        assert(self.qualifier in GENE_FILTER_QUALIFIERS + [""])
536       
537    def rest(self):
538        return "gene{property}{qualifier}={value}".format(**self.__dict__)
539       
540       
541class ExperimentalFactorCondition(Condition):
542    """ An atlas experimental factor filter condition.
543   
544    :param factor: EFO experiamntal factor
545    :param regulation: "up", "down", "updown", "any" or "none"
546    :param n: Minimum number of of experimants with this condition
547    :param value: Experimantal factor value
548   
549    Example ::
550   
551        >>> # Any genes up regulated in at least 3 experiments involving cancer.
552        >>> condition = ExperimentalFactorCondition("", "up", 3, "cancer")
553        >>> # Only genes which are up/down regulated in the heart in at least one experiment.
554        >>> condition = ExperimentalFactorCondition("Organism_part", "updown", 1, "heart")
555       
556    """
557    def __init__(self, factor, regulation, n, value):
558        self.factor = factor
559        self.regulation = regulation
560        self.n = n
561        self.value = value
562        self.validate()
563       
564    def validate(self):
565        # TODO: validate the factor and value
566#        assert(self.factor in ef_ontology())
567        assert(self.regulation in ["up", "down", "updown"])
568       
569    def rest(self):
570        return "{regulation}{n}In{factor}={value}".format(**self.__dict__)
571       
572       
573class OrganismCondition(Condition):
574    """ Condition on organism.
575    """
576    def __init__(self, organism):
577        self.organism = organism
578        self.validate()
579       
580    def validate(self):
581        assert(self.organism in ATLAS_ORGANISMS)
582       
583    def rest(self):
584        return "species={0}".format(self.organism.replace(" ", "+").lower())
585       
586       
587class ExperimentCondition(Condition):
588    """ Condition on experiement
589   
590    :param property: Property of the experiment. If None or "" all properties
591        will be searched.
592    :param qualifier: Qualifier can be 'Has' or 'HasNot'
593    :param value: The value to search for.
594   
595    Example ::
596   
597        >>> # Condition on a experiemnt acession
598        >>> condition = ExperimentCondition("", "", "E-GEOD-24283")
599        >>> # Condition on experiments involving lung
600        >>> condition = ExperimentCondition("Organism_part", "Has", "lung")
601       
602    """
603    EXPERIMENT_FILTER_QUALIFIERS = [
604                "Has",
605                "HasNot"]
606   
607    def __init__(self, property, qualifier, value):
608        self.property = property
609        self.qualifier = qualifier
610        if isinstance(value, basestring):
611            self.value = value.replace(" ", "+")
612        elif isinstance(value, list):
613            self.value = "+".join(value)
614        else:
615            raise ValueError(value)
616       
617        self.validate()
618       
619    def validate(self):
620        # TODO: check to EFO factors
621#        assert(self.property in EXPERIMENT_FILTERS + [""])
622        assert(self.qualifier in self.EXPERIMENT_FILTER_QUALIFIERS + [""])
623       
624    def rest(self):
625        return "experiment{property}{qualifier}={value}".format(**self.__dict__)
626       
627       
628class GeneExpressionAtlasError(Exception):
629    """ An error response from the Atlas server.
630    """
631    pass
632   
633   
634def __check_atlas_error_json(response):
635    if "error" in response:
636        raise GeneExpressionAtlasError(response["error"])
637    return response
638 
639     
640def __check_atlas_error_xml(response):
641    error = response.find("error")
642    if error is not None:
643        raise GeneExpressionAtlasError(error.text)
644    return response
645   
646       
647def run_simple_query(genes=None, regulation=None, organism=None,
648                     condition=None, format="json", start=None,
649                     rows=None):
650    """ A simple Gene Atlas query.
651   
652    :param genes: A list of gene names to search for.
653    :param regulation: Search for experiments in which `genes` are "up",
654        "down", "updown" or "none" regulated. If None all experiments
655        are searched.
656    :param organism: Search experiments for organism. If None all experiments
657        are searched.
658    :param condition: An EFO factor value (e.g. "brain")
659   
660    Example ::
661       
662        >>> run_simple_query(genes=['Pou5f1', 'Dppa3'], organism="Mus musculus")
663        {u'...
664       
665        >>> run_simple_query(genes=['Pou5f1', 'Dppa3'], regulation="up", organism="Mus musculus")
666        {u'...
667       
668        >>> run_simple_query(genes=['Pou5f1', 'Dppa3'], regulation="up", condition="liver", organism="Mus musculus")
669        {u'...
670       
671    """
672    conditions = ConditionList()
673    if genes:
674        conditions.append(GenePropertyCondition("Gene", "Is", genes))
675    if regulation or condition:
676        regulation = "any" if regulation is None else regulation
677        condition = "" if condition is None else condition
678        conditions.append(ExperimentalFactorCondition("", regulation, 1, condition))
679    if organism:
680        conditions.append(OrganismCondition(organism))
681       
682    connection = GeneExpressionAtlasConenction()
683    results = connection.query(conditions, format=format, start=start,
684                               rows=rows)
685    if format == "json":
686        return parse_json(results)
687    else:
688        return parse_xml(results)
689
690"""\
691.. todo:: can this be implemented query_atlas(organism="...", Locuslink="...", Chebi="...", up3InCompound="..." downInEFO="...")
692      Need a full list of accepted factors
693"""
694
695def run_query(condition, format="json", start=None, rows=None, indent=False, connection=None):
696    """ Query Atlas based on a `condition` (instance of :class:`Condition`)
697   
698    Example ::
699       
700        >>> condition1 = GenePropertyCondition("Goterm", "Is", "p53 binding")
701        >>> condition2 = ExperimentalFactorCondition("Organism_part", "up", 3, "heart")
702        >>> condition = ConditionList([condition1, condition2])
703        >>> run_query(condition)
704        {u'...
705       
706    """
707    if connection is None:
708        connection = GeneExpressionAtlasConenction()
709    results = connection.query(condition, format=format, start=start,
710                               rows=rows, indent=indent)
711    if format == "json":
712        response = parse_json(results)
713        return __check_atlas_error_json(response)
714    else:
715        response = parse_xml(results)
716        return __check_atlas_error_xml(response)
717   
718def test():
719    from pprint import pprint   
720    pprint(get_atlas_summary(['Pou5f1', 'Dppa3'], 'Mus musculus'))
721       
722    pprint(get_atlas_summary(['PDLIM5', 'FGFR2' ], 'Homo sapiens'))
723    import doctest 
724    doctest.testmod(optionflags=doctest.ELLIPSIS)
725   
726if __name__ == "__main__":
727    test()
Note: See TracBrowser for help on using the repository browser.