Merge branch 'dev' into remote_chart_plugins

This commit is contained in:
guerler
2016-10-14 16:24:39 -04:00
123 changed files with 2319 additions and 653 deletions
+2 -2
View File
@@ -1,8 +1,8 @@
.venv/
.tox/
eggs/
client/
client/node_modules/
database/
doc/patch.py
doc/source/conf.py
eggs/
lib/galaxy/util/jstree.py
+34 -7
View File
@@ -1,8 +1,10 @@
client/galaxy/style/source_material/circle.py
contrib/
cron/add_manual_builds.py
cron/cleanup_datasets.py
cron/parse_builds_3_sites.py
cron/parse_builds.py
doc/parse_gx_xsd.py
lib/galaxy/actions/
lib/galaxy/auth/__init__.py
lib/galaxy/auth/providers/__init__.py
@@ -93,6 +95,7 @@ lib/galaxy/main.py
lib/galaxy/managers/annotatable.py
lib/galaxy/managers/api_keys.py
lib/galaxy/managers/base.py
lib/galaxy/managers/collections_util.py
lib/galaxy/managers/context.py
lib/galaxy/managers/deletable.py
lib/galaxy/managers/__init__.py
@@ -101,7 +104,9 @@ lib/galaxy/managers/libraries.py
lib/galaxy/managers/secured.py
lib/galaxy/managers/taggable.py
lib/galaxy/model/__init__.py
lib/galaxy/model/item_attrs.py
lib/galaxy/model/mapping.py
lib/galaxy/model/metadata.py
lib/galaxy/model/migrate/__init__.py
lib/galaxy/model/migrate/versions/0001_initial_tables.py
lib/galaxy/model/migrate/versions/0002_metadata_file_table.py
@@ -244,9 +249,11 @@ lib/galaxy/sample_tracking/sample.py
lib/galaxy/security/validate_user_input.py
lib/galaxy/tags/
lib/galaxy/tools/actions/metadata.py
lib/galaxy/tools/cwl
lib/galaxy/tools/cwl/
lib/galaxy/tools/data_manager/__init__.py
lib/galaxy/tools/deps
lib/galaxy/tools/deps/
lib/galaxy/tools/exception_handling.py
lib/galaxy/tools/execute.py
lib/galaxy/tools/filters/
lib/galaxy/tools/imp_exp/export_history.py
lib/galaxy/tools/imp_exp/__init__.py
@@ -254,11 +261,15 @@ lib/galaxy/tools/linters/
lib/galaxy/tools/lint.py
lib/galaxy/tools/lint_util.py
lib/galaxy/tools/loader_directory.py
lib/galaxy/tools/loader.py
lib/galaxy/tools/parameters/dataset_matcher.py
lib/galaxy/tools/parameters/history_query.py
lib/galaxy/tools/parameters/__init__.py
lib/galaxy/tools/parameters/input_translation.py
lib/galaxy/tools/parameters/sanitize.py
lib/galaxy/tools/parameters/validation.py
lib/galaxy/tools/parameters/wrapped_json.py
lib/galaxy/tools/parameters/wrapped.py
lib/galaxy/tools/parser/
lib/galaxy/tools/special_tools.py
lib/galaxy/tools/test.py
@@ -345,9 +356,11 @@ lib/galaxy/web/framework/middleware/statsd.py
lib/galaxy/web/framework/middleware/translogger.py
lib/galaxy/web/framework/middleware/xforwardedhost.py
lib/galaxy/web/params.py
lib/galaxy/workflow/extract.py
lib/galaxy/workflow/__init__.py
lib/galaxy/workflow/render.py
lib/galaxy/workflow/schedulers/__init__.py
lib/galaxy/workflow/run.py
lib/galaxy/workflow/schedulers/
lib/galaxy/workflow/steps.py
lib/galaxy/work/__init__.py
lib/mimeparse.py
@@ -440,6 +453,7 @@ scripts/tool_shed/migrate_tools_to_repositories.py
scripts/tool_shed/show_tool_dependency_installation_dir_contents.py
scripts/tools/re_escape_output.py
test/api/__init__.py
test/api/test_dataset_collections.py
test/api/test_datasets.py
test/api/test_datatypes.py
test/api/test_framework.py
@@ -447,27 +461,39 @@ test/api/test_histories.py
test/api/test_history_contents_provenance.py
test/api/test_jobs.py
test/api/test_libraries.py
test/api/test_page_revisions.py
test/api/test_search.py
test/api/test_tool_data.py
test/api/test_tools.py
test/api/test_tours.py
test/api/test_workflow_extraction.py
test/api/test_workflows_from_yaml.py
test/api/workflows_format_2/interface.py
test/api/workflows_format_2/main.py
test/api/workflows_format_2/
test/base/api_asserts.py
test/base/api.py
test/base/api_util.py
test/base/__init__.py
test/base/instrument.py
test/base/integration_util.py
test/base/interactor.py
test/base/nose_util.py
test/base/test_logging.py
test/base/tool_shed_util.py
test/base/twilltestcase.py
test/casperjs/
test/functional/database_contexts.py
test/functional/__init__.py
test/functional/test_data_managers.py
test/functional/test_toolbox.py
test/functional/tool-data/
test/functional/tools/
test/integration/__init__.py
test/integration/test_pulsar_embedded.py
test/functional/workflow.py
test/integration/
test/manual/__init__.py
test/shed_functional/base/common.py
test/shed_functional/base/__init__.py
test/shed_functional/functional/
test/shed_functional/functional_tests.py
test/shed_functional/__init__.py
test/unit/dataset_collections/__init__.py
test/unit/datatypes/dataproviders/__init__.py
@@ -502,6 +528,7 @@ test/unit/tools/test_tool_deps.py
test/unit/tools/test_tool_loader.py
test/unit/tools/test_toolbox_filters.py
test/unit/tools/test_watcher.py
test/unit/tools_support.py
test/unit/unittest_utils/__init__.py
test/unit/unittest_utils/utility.py
test/unit/visualizations/__init__.py
+14
View File
@@ -1,5 +1,7 @@
client/galaxy/style/source_material/circle.py
contrib/
cron/
doc/parse_gx_xsd.py
lib/galaxy/actions/
lib/galaxy/auth/
lib/galaxy/config.py
@@ -26,8 +28,11 @@ lib/galaxy/forms/
lib/galaxy/jobs/
lib/galaxy/managers/
lib/galaxy/model/__init__.py
lib/galaxy/model/item_attrs.py
lib/galaxy/model/mapping.py
lib/galaxy/model/metadata.py
lib/galaxy/model/migrate/
lib/galaxy/model/orm/now.py
lib/galaxy/objectstore/
lib/galaxy/openid/
lib/galaxy/quota/
@@ -36,17 +41,26 @@ lib/galaxy/security/
lib/galaxy/tags/
lib/galaxy/tools/cwl/
lib/galaxy/tools/deps/
lib/galaxy/tools/exception_handling.py
lib/galaxy/tools/execute.py
lib/galaxy/tools/lint.py
lib/galaxy/tools/lint_util.py
lib/galaxy/tools/linters/
lib/galaxy/tools/loader.py
lib/galaxy/tools/loader_directory.py
lib/galaxy/tools/parameters/dataset_matcher.py
lib/galaxy/tools/parameters/__init__.py
lib/galaxy/tools/parameters/wrapped_json.py
lib/galaxy/tools/parameters/wrapped.py
lib/galaxy/tools/parser/
lib/galaxy/tools/toolbox/
lib/galaxy/tours/
lib/galaxy/util/
lib/galaxy/visualization/
lib/galaxy/work/
lib/galaxy/workflow/extract.py
lib/galaxy/workflow/run.py
lib/galaxy/workflow/schedulers/core.py
lib/galaxy_ext/
lib/galaxy_utils/
lib/log_tempfile.py
-3
View File
@@ -24,9 +24,6 @@ matrix:
allow_failures:
- env: TOX_ENV=py27-lint-imports
before_install:
- if [ `uname` == "Darwin" ]; then bash -c "brew update && brew install python"; fi
install:
- pip install tox
- if [ "$TOX_ENV" == "validate-test-tools" ]; then sudo apt-get install libxml2-utils; fi
+6
View File
@@ -633,6 +633,12 @@ var ToolSearchView = Backbone.View.extend({
if (!this.model.is_visible()) {
this.$el.hide();
}
// Adjust top for issue 2907 depending on whether the messagebox is visible.
if ($("#messagebox").is(":visible")) {
this.$el.css("top","95px");
}
this.$el.find('[title]').tooltip();
return this;
},
@@ -1,15 +1,14 @@
#!/usr/bin/env python
"""
usage: %prog width height bg_color hatch_color [color alpha stop_pos] +
"""
from __future__ import division
import sys
import cairo
from math import pi
import cairo
assert sys.version_info[:2] >= ( 2, 4 )
+27 -3
View File
@@ -203,7 +203,7 @@ paste.app_factory = galaxy.web.buildapp:app_factory
# Pass debug flag to conda commands.
#conda_debug = False
# conda channels to enable by default (http://conda.pydata.org/docs/custom-channels.html)
#conda_ensure_channels = r,bioconda,iuc
#conda_ensure_channels = conda-forge,r,bioconda,iuc
# Set to True to instruct Galaxy to look for and install missing tool
# dependencies before each job runs.
#conda_auto_install = False
@@ -228,6 +228,31 @@ paste.app_factory = galaxy.web.buildapp:app_factory
# than the watchdog default.
#watch_tools = False
# Enable Galaxy to fetch Docker containers registered with quay.io generated
# from tool requirements resolved through conda. These containers (when
# available) have been generated using mulled - https://github.com/mulled.
# These containers are highly beta and availablity will vary by tool.
# This option will additionally only be used for job destinations with
# Docker enabled.
#enable_beta_mulled_containers = False
# Container resolvers configuration (beta). Setup a file describing container
# resolvers to use when discovering containers for Galaxy. If this is set to
# None, the default containers loaded is determined by
# enable_beta_mulled_containers.
#containers_resolvers_config_file = None
# involucro is a tool used to build Docker containers for tools from Conda
# dependencies referenced in tools as `requirement`s. The following path is
# the location of involucro on the Galaxy host. This is ignored if the relevant
# container resolver isn't enabled, and will install on demand unless
# involucro_auto_init is set to False.
#involucro_path = database/dependencies/involucro
# Install involucro as needed to build Docker containers for tools. Ignored if
# relevant container resolver is not used.
#involucro_auto_init = True
# Enable automatic polling of relative tool sheds to see if any updates
# are available for installed repositories. Ideally only one Galaxy
# server process should be able to check for repository updates. The
@@ -861,8 +886,7 @@ use_interactive = True
# Galaxy encodes various internal values when these values will be output in
# some format (for example, in a URL or cookie). You should set a key to be
# used by the algorithm that encodes and decodes these values. It can be any
# string. If left unchanged, anyone could construct a cookie that would grant
# them access to others' sessions.
# string.
# One simple way to generate a value for this is with the shell command:
# python -c 'import time; print time.time()' | md5sum | cut -f 1 -d ' '
#id_secret = USING THE DEFAULT IS NOT SECURE!
+6
View File
@@ -362,6 +362,12 @@
a fallback. -->
<!-- <param id="docker_default_container_id">busybox:ubuntu-14.04</param> -->
<!-- If the destination should be secured to only allow containerized jobs
the following parameter may be set for the job destination. Not all,
or even most, tools available in Galaxy core or in the Tool Shed
support Docker yet so this option may require a lot of extra work for
the deployer. -->
<!-- <param id="require_container">true</param> -->
</destination>
<destination id="pbs" runner="pbs" tags="mycluster"/>
<destination id="pbs_longjobs" runner="pbs" tags="mycluster,longjobs">
+2 -4
View File
@@ -2,14 +2,12 @@
# TODO: Add examples, tables and best practice links to command
# TODO: Examples of truevalue, falsevalue
# TODO: Test param extra_file
from __future__ import print_function
# Things dropped from TOC (still documented inside schema).
# - request_parameter_translation
from __future__ import print_function
from lxml import etree
from StringIO import StringIO
from six import StringIO
with open("doc/schema_template.md", "r") as f:
MARKDOWN_TEMPLATE = f.read()
+1 -1
View File
@@ -182,7 +182,7 @@ debug
ensure_channels
conda channels to enable by default. See
http://conda.pydata.org/docs/custom-channels.html for more
information about channels. (default: r,bioconda).
information about channels. (default: conda-forge,r,bioconda,iuc).
auto_install
Set to True to instruct Galaxy to look for and install missing tool
+2
View File
@@ -11,6 +11,8 @@ documentation. These resources should be used together.
dependency_resolvers.rst
conda_faq.rst
mulled_containers.rst
interactive_environments.rst
+128
View File
@@ -0,0 +1,128 @@
=================================
Containers for Tool Dependencies
=================================
Galaxy tools (also called wrappers) are able to use Conda packages
(see more information in our `Galaxy Conda documentation`_) and Docker containers as dependency resolvers.
The IUC_ recommends to use Conda packages as primary dependency resolver, mainly because Docker is not
available on every (HPC-) system. Conda on the other hand can be installed by Galaxy and maintained
entirely in user-space. Nevertheless, Docker (Containers in general) has some unique features and
there are many use-cases in the Galaxy community which makes containerized systems very appealing.
Since 2014 Galaxy supports running tools in Docker containers via a special `container annotation`_ inside of the
requirement field.
.. code-block:: xml
<requirements>
<!-- Container based dependency handling -->
<container type="docker">busybox:ubuntu-14.04</container>
<!-- Conda based dependency handling -->
<requirement type="package" version="8.22">gnu_coreutils</requirement>
</requirements>
This approach has shown two limitations that slowed down the adoption by tool developers.
First, every tool needs to be annotated with a container name (as shown above) and this container needs
to be created beforehand, usually manually. The second reason is that a Galaxy tool aims to be deployed everywhere,
independet of the underlying system, meaning if Docker is not available Galaxy should use Conda packages.
This puts an additional burden on tool developers who need to take care of two dependency resolvers. This setup can cause
different tool results depending on the resolver, because both the Conda package and the Docker container are
usually not created out of the same recipe and maybe were compiled in a different way, use different sources etc.
Not an ideal solution and something we wanted to solve.
Here we demonstrate a solution that can create Containers out of Conda packages automatically.
This can be either used to support communities like BioContainers_ to create Containers
before deploying a Galaxy tool, or this can be used by Galaxy to create Containers on-demand and on-the-fly if one
is not available already.
Automatic build of Linux containers
-----------------------------------
We utilize [mulled](https://github.com/mulled/mulled) with [involucro](https://github.com/involucro/involucro)
in an automatic way. This is for example used to convert all packages in bioconda_ into Linux Containers
(Docker and rkt at the moment) and made available at the `BioContainers Quay.io account`_.
We have developed small utilities around this technology stack which is currently included in galaxy-lib_.
Here is a short introduction:
Search for containers
^^^^^^^^^^^^^^^^^^^^^
This will search for containers in the biocontainers organisation.
.. code-block:: bash
$ mulled-search -s vsearch -o biocontainers
Build all packages from bioconda from the last 24h
^^^^^^^^^^^^^^^^^^^^^
The BioConda community is building a container for every package they create with a command similar to this.
.. code-block:: bash
$ mulled-build-channel --channel bioconda --namespace biocontainers \
--involucro-path ./involucro --recipes-dir ./bioconda-recipes --diff-hours 25 build
Building Docker containers for local Conda packages
^^^^^^^^^^^^^^^^^^^^^
Conda packages can be tested with creating a busybox based container for this particular package in the following way.
This also demonstrates how you can build a container locally and on-the-fly.
> we modified the samtools package to version 3.0 to make clear we are using a local version
1) build your recipe
.. code-block:: bash
$ conda build recipes/samtools
2) index your local builds
.. code-block:: bash
$ conda index /home/bag/miniconda2/conda-bld/linux-64/
3) build a container for your local package
.. code-block:: bash
$ mulled-build build-and-test 'samtools=3.0--0' \
--extra-channel file://home/bag/miniconda2/conda-bld/ --test 'samtools --help'
The ``--0`` indicates the build version of the conda package. It is recommended to specify this number otherwise
you will override already existing images. For Python Conda packages this extension might look like this ``--py35_1``.
Build, test and push a conda-forge package to biocontainers
^^^^^^^^^^^^^^^^^^^^^
> You need to have write access to the biocontainers repository
You can build packages from other Conda channels as well, not only from BioConda. ``pandoc`` is available from the
conda-forge channel and conda-forge is also enabled by default in Galaxy. To build ``pandoc`` and push it to biocontainrs
you could do something along these lines.
.. code-block:: bash
$ mulled-build build-and-test 'pandoc=1.17.2--0' --test 'pandoc --help' -n biocontainers
.. code-block:: bash
$ mulled-build push 'pandoc=1.17.2--0' --test 'pandoc --help' -n biocontainers
.. _Galaxy Conda documentation: ./conda_faq.rst
.. _IUC: https://wiki.galaxyproject.org/IUC
.. _container annotation: https://github.com/galaxyproject/galaxy/blob/dev/test/functional/tools/catDocker.xml#L4
.. _BioContainers: https://github.com/biocontainers
.. _bioconda: https://github.com/bioconda/bioconda-recipes
.. _BioContainers Quay.io account: https://quay.io/organization/biocontainers
.. _galaxy-lib: https://github.com/galaxyproject/galaxy-lib
+19 -1
View File
@@ -226,6 +226,7 @@ class Configuration( object ):
self.local_task_queue_workers = int(kwargs.get("local_task_queue_workers", 2))
self.tool_submission_burst_threads = int( kwargs.get( 'tool_submission_burst_threads', '1' ) )
self.tool_submission_burst_at = int( kwargs.get( 'tool_submission_burst_at', '10' ) )
# Enable new interface for API installations from TS.
# Admin menu will list both if enabled.
self.enable_beta_ts_api_install = string_as_bool( kwargs.get( 'enable_beta_ts_api_install', 'False' ) )
@@ -323,6 +324,19 @@ class Configuration( object ):
else:
self.tool_dependency_dir = None
self.use_tool_dependencies = os.path.exists(self.dependency_resolvers_config_file)
self.enable_beta_mulled_containers = string_as_bool( kwargs.get( 'enable_beta_mulled_containers', 'False' ) )
containers_resolvers_config_file = kwargs.get( 'containers_resolvers_config_file', None )
if containers_resolvers_config_file:
containers_resolvers_config_file = resolve_path(containers_resolvers_config_file, self.root)
self.containers_resolvers_config_file = containers_resolvers_config_file
involucro_path = kwargs.get('involucro_path', None)
if involucro_path is None:
involucro_path = os.path.join(tool_dependency_dir, "involucro")
self.involucro_path = resolve_path(involucro_path, self.root)
self.involucro_auto_init = string_as_bool(kwargs.get( 'involucro_auto_init', True))
# Configuration options for taking advantage of nginx features
self.upstream_gzip = string_as_bool( kwargs.get( 'upstream_gzip', False ) )
self.apache_xsendfile = string_as_bool( kwargs.get( 'apache_xsendfile', False ) )
@@ -846,7 +860,11 @@ class ConfiguresGalaxyMixin:
default_file_path=file_path,
outputs_to_working_directory=self.config.outputs_to_working_directory,
container_image_cache_path=self.config.container_image_cache_path,
library_import_dir=self.config.library_import_dir
library_import_dir=self.config.library_import_dir,
enable_beta_mulled_containers=self.config.enable_beta_mulled_containers,
containers_resolvers_config_file=self.config.containers_resolvers_config_file,
involucro_path=self.config.involucro_path,
involucro_auto_init=self.config.involucro_auto_init,
)
self.container_finder = containers.ContainerFinder(app_info)
+1 -9
View File
@@ -8,18 +8,10 @@ from six.moves.urllib.parse import quote_plus
from galaxy.datatypes.binary import Binary
from galaxy.datatypes.sniff import get_headers
from galaxy.datatypes.text import Html as HtmlFromText
from galaxy.datatypes.util.image_util import check_image_type
from galaxy.util import nice_size
from galaxy.util.image_util import check_image_type
from . import data
try:
import Image as PIL
except ImportError:
try:
from PIL import Image as PIL
except:
PIL = None
log = logging.getLogger(__name__)
# TODO: Uploading image files of various types is supported in Galaxy, but on
+6 -15
View File
@@ -434,27 +434,18 @@ class Registry( object ):
Return the datatype class where the datatype's `type` attribute
(as defined in the datatype_conf.xml file) contains `name`.
"""
# TODO: too roundabout - would be better to generate this once as a map and store in this object
# TODO: obviously not ideal but some of these base classes that are useful for testing datatypes
# aren't loaded into the datatypes registry, so we'd need to test for them here
if name == 'images.Image':
return images.Image
# TODO: too inefficient - would be better to generate this once as a map and store in this object
for ext, datatype_obj in self.datatypes_by_extension.items():
datatype_obj_class = datatype_obj.__class__
datatype_obj_class_str = str( datatype_obj_class )
if name in datatype_obj_class_str:
return datatype_obj_class
return None
# these seem to be connected to the dynamic classes being generated in this file, lines 157-158
# they appear when a one of the three are used in inheritance with subclass="True"
# TODO: a possible solution is to def a fn in datatypes __init__ for creating the dynamic classes
# remap = {
# 'galaxy.datatypes.registry.Tabular' : galaxy.datatypes.tabular.Tabular,
# 'galaxy.datatypes.registry.Text' : galaxy.datatypes.data.Text,
# 'galaxy.datatypes.registry.Binary' : galaxy.datatypes.binary.Binary
# }
# datatype_str = str( datatype )
# if datatype_str in remap:
# datatype = remap[ datatype_str ]
#
# return datatype
def get_available_tracks( self ):
return self.available_tracks
+1 -1
View File
@@ -15,9 +15,9 @@ from six import PY3
from galaxy import util
from galaxy.datatypes import metadata
from galaxy.util.checkers import is_gzip
from galaxy.util.image_util import check_image_type
from galaxy.datatypes.metadata import MetadataElement
from galaxy.datatypes.sniff import get_headers
from galaxy.datatypes.util.image_util import check_image_type
from galaxy.util import nice_size
from . import data
-78
View File
@@ -1,78 +0,0 @@
"""Provides utilities for working with image files."""
import logging
import imghdr
try:
import Image as PIL
except ImportError:
try:
from PIL import Image as PIL
except:
PIL = None
log = logging.getLogger(__name__)
def image_type( filename, image=None ):
format = ''
if PIL is not None:
if image is not None:
format = image.format
else:
try:
im = PIL.open( filename )
format = im.format
im.close()
except:
return False
else:
format = imghdr.what( filename )
if format is not None:
format = format.upper()
else:
return False
return format
def check_image_type( filename, types, image=None ):
format = image_type( filename, image )
# First check if we can use PIL
if format in types:
return True
return False
def get_image_ext( file_path, image ):
# determine ext
format = image_type( file_path, image )
if format in [ 'JPG', 'JPEG' ]:
return 'jpg'
if format == 'PNG':
return 'png'
if format == 'TIFF':
return 'tiff'
if format == 'BMP':
return 'bmp'
if format == 'GIF':
return 'gif'
if format == 'IM':
return 'im'
if format == 'PCD':
return 'pcd'
if format == 'PCX':
return 'pcx'
if format == 'PPM':
return 'ppm'
if format == 'PSD':
return 'psd'
if format == 'XBM':
return 'xbm'
if format == 'XPM':
return 'xpm'
if format == 'RGB':
return 'rgb'
if format == 'PBM':
return 'pbm'
if format == 'PGM':
return 'pgm'
return None
+4
View File
@@ -803,6 +803,10 @@ class JobWrapper( object ):
"""
return self.get_destination_configuration("cleanup_job", DEFAULT_CLEANUP_JOB)
@property
def requires_containerization(self):
return util.asbool(self.get_destination_configuration("require_container", "False"))
def can_split( self ):
# Should the job handler split this job up?
return self.app.config.use_tasked_jobs and self.tool.parallelism
+2
View File
@@ -198,6 +198,8 @@ class BaseJobRunner( object ):
def build_command_line( self, job_wrapper, include_metadata=False, include_work_dir_outputs=True,
modify_command_for_container=True ):
container = self._find_container( job_wrapper )
if not container and job_wrapper.requires_containerization:
raise Exception("Failed to find a container when required, contact Galaxy admin.")
return build_command(
self,
job_wrapper,
+6 -7
View File
@@ -1,8 +1,7 @@
from galaxy import exceptions
from galaxy import web
from galaxy import model
import logging
from galaxy import exceptions, model, web
log = logging.getLogger( __name__ )
ERROR_MESSAGE_UNKNOWN_SRC = "Unknown dataset source (src) %s."
@@ -77,7 +76,7 @@ def dictify_dataset_collection_instance( dataset_collection_instance, parent, se
dict_value[ 'url' ] = web.url_for( 'library_content', library_id=encoded_library_id, id=encoded_id, folder_id=encoded_folder_id )
if view == "element":
collection = dataset_collection_instance.collection
dict_value[ 'elements' ] = map( dictify_element, collection.elements )
dict_value[ 'elements' ] = [ dictify_element(_) for _ in collection.elements ]
dict_value[ 'populated' ] = collection.populated
security.encode_all_ids( dict_value, recursive=True ) # TODO: Use Kyle's recursive formulation of this.
return dict_value
@@ -89,10 +88,10 @@ def dictify_element( element ):
if element.child_collection:
# Recursively yield elements for each nested collection...
child_collection = element.child_collection
object_detials[ "elements" ] = map( dictify_element, child_collection.elements )
object_detials[ "elements" ] = [ dictify_element(_) for _ in child_collection.elements ]
object_detials[ "populated" ] = child_collection.populated
dictified[ "object" ] = object_detials
return dictified
__all__ = [ api_payload_to_create_params, dictify_dataset_collection_instance ]
__all__ = ( 'api_payload_to_create_params', 'dictify_dataset_collection_instance' )
+5 -3
View File
@@ -1,7 +1,9 @@
import logging
from sqlalchemy.sql.expression import func
# Cannot import galaxy.model b/c it creates a circular import graph.
import galaxy
import logging
log = logging.getLogger( __name__ )
@@ -162,8 +164,8 @@ class UsesAnnotations:
return getattr( galaxy.model, class_name, None )
__all__ = [
__all__ = (
'UsesAnnotations',
'UsesItemRatings',
'RuntimeException',
]
)
+12 -11
View File
@@ -4,7 +4,6 @@ Galaxy Metadata
"""
import copy
import cPickle
import json
import logging
import os
@@ -15,6 +14,7 @@ import weakref
from os.path import abspath
from six import string_types
from six.moves import cPickle
from sqlalchemy.orm import object_session
import galaxy.model
@@ -93,13 +93,14 @@ class MetadataCollection( object ):
return default
def items(self):
return iter( [ ( k, self.get( k ) ) for k in self.spec.iterkeys() ] )
return iter( [ ( k, self.get( k ) ) for k in self.spec.keys() ] )
def __str__(self):
return dict( self.items() ).__str__()
def __nonzero__( self ):
def __bool__( self ):
return bool( self.parent._metadata )
__nonzero__ = __bool__
def __getattr__( self, name ):
if name in self.spec:
@@ -205,7 +206,7 @@ class MetadataSpecCollection( odict ):
self[item.name] = item
def iter( self ):
return self.itervalues()
return iter(self.values())
def __getattr__( self, name ):
return self.get( name )
@@ -452,7 +453,7 @@ class RangeParameter( SelectParameter ):
other_values = other_values or {}
if values is None:
values = zip( range( self.min, self.max, self.step ), range( self.min, self.max, self.step ))
values = list(zip( range( self.min, self.max, self.step ), range( self.min, self.max, self.step ) ))
return SelectParameter.get_html_field( self, value=value, context=context, other_values=other_values, values=values, **kwd )
def get_html( self, value, context=None, other_values=None, values=None, **kwd ):
@@ -460,7 +461,7 @@ class RangeParameter( SelectParameter ):
other_values = other_values or {}
if values is None:
values = zip( range( self.min, self.max, self.step ), range( self.min, self.max, self.step ))
values = list(zip( range( self.min, self.max, self.step ), range( self.min, self.max, self.step ) ))
return SelectParameter.get_html( self, value, context=context, other_values=other_values, values=values, **kwd )
@classmethod
@@ -478,7 +479,7 @@ class ColumnParameter( RangeParameter ):
if values is None and context:
column_range = range( 1, ( context.columns or 0 ) + 1, 1 )
values = zip( column_range, column_range )
values = list(zip( column_range, column_range ))
return RangeParameter.get_html_field( self, value=value, context=context, other_values=other_values, values=values, **kwd )
def get_html( self, value, context=None, other_values=None, values=None, **kwd ):
@@ -487,7 +488,7 @@ class ColumnParameter( RangeParameter ):
if values is None and context:
column_range = range( 1, ( context.columns or 0 ) + 1, 1 )
values = zip( column_range, column_range )
values = list(zip( column_range, column_range ))
return RangeParameter.get_html( self, value, context=context, other_values=other_values, values=values, **kwd )
@@ -815,7 +816,7 @@ class JobExternalOutputMetadataWrapper( object ):
metadata_files.filename_override_metadata = abspath( tempfile.NamedTemporaryFile( dir=tmp_dir, prefix="metadata_override_%s_" % key ).name )
open( metadata_files.filename_override_metadata, 'wb+' ) # create the file on disk, so it cannot be reused by tempfile (unlikely, but possible)
override_metadata = []
for meta_key, spec_value in dataset.metadata.spec.iteritems():
for meta_key, spec_value in dataset.metadata.spec.items():
if isinstance( spec_value.param, FileParameter ) and dataset.metadata.get( meta_key, None ) is not None:
metadata_temp = MetadataTempFile()
shutil.copy( dataset.metadata.get( meta_key, None ).file_name, metadata_temp.file_name )
@@ -871,7 +872,7 @@ class JobExternalOutputMetadataWrapper( object ):
sa_session.add( metadata_files )
sa_session.flush()
__all__ = [
__all__ = (
"Statement",
"MetadataElement",
"MetadataCollection",
@@ -889,4 +890,4 @@ __all__ = [
"FileParameter",
"MetadataTempFile",
"JobExternalOutputMetadataWrapper",
]
)
+1 -1
View File
@@ -10,4 +10,4 @@ from datetime import datetime
# Return the current time in UTC without any timezone information
now = datetime.utcnow
__all__ = [now]
__all__ = ('now', )
+3 -2
View File
@@ -1538,9 +1538,10 @@ class Tool( object, Dictifiable ):
# Add input file tuples to the list.
for input in test.inputs:
for input_value in test.inputs[ input ]:
input_path = os.path.abspath( os.path.join( 'test-data', input_value ) )
input_filename = str( input_value )
input_path = os.path.abspath( os.path.join( 'test-data', input_filename ) )
if os.path.exists( input_path ):
td_tup = ( input_path, os.path.join( 'test-data', input_value ) )
td_tup = ( input_path, os.path.join( 'test-data', input_filename ) )
tarball_files.append( td_tup )
# And add output file tuples to the list.
for label, filename, _ in test.outputs:
+3 -4
View File
@@ -2,13 +2,12 @@ from .cwltool_deps import (
needs_shell_quoting,
shellescape,
)
from .parser import tool_proxy
from .parser import workflow_proxy
from .parser import tool_proxy, workflow_proxy
from .representation import to_cwl_job, to_galaxy_parameters
from .runtime_actions import handle_outputs
__all__ = [
__all__ = (
'tool_proxy',
'workflow_proxy',
'handle_outputs',
@@ -16,4 +15,4 @@ __all__ = [
'to_galaxy_parameters',
'needs_shell_quoting',
'shellescape',
]
)
+3 -4
View File
@@ -4,6 +4,7 @@ Use this as the import interface for cwltool and just call
:func:`ensure_cwltool_available` before using any of the imported
functionality at runtime.
"""
import re
try:
import requests
@@ -40,8 +41,6 @@ except (ImportError, SyntaxError):
# Drop SyntaxError once schema_salad supports Python 3
schema_salad = None
import re
needs_shell_quoting = re.compile(r"""(^$|[\s|&;()<>\'"$@])""").search
@@ -65,7 +64,7 @@ def ensure_cwltool_available():
raise ImportError(message)
__all__ = [
__all__ = (
'main',
'load_tool',
'workflow',
@@ -74,4 +73,4 @@ __all__ = [
'schema_salad',
'shellescape',
'needs_shell_quoting',
]
)
+9 -7
View File
@@ -10,6 +10,8 @@ import logging
import os
from abc import ABCMeta, abstractmethod
import six
from galaxy.util import safe_makedirs
from galaxy.util.bunch import Bunch
from galaxy.util.odict import odict
@@ -119,8 +121,8 @@ def check_requirements(rec, tool=True):
check_requirements(d, tool=tool)
@six.add_metaclass(ABCMeta)
class ToolProxy( object ):
__metaclass__ = ABCMeta
def __init__(self, tool, tool_path):
self._tool = tool
@@ -176,7 +178,7 @@ class CommandLineToolProxy(ToolProxy):
schema = self._tool.schemaDefs[schema_type]
if schema["type"] == "record":
return map(_simple_field_to_input, schema["fields"])
return [_simple_field_to_input(_) for _ in schema["fields"]]
def output_instances(self):
outputs_schema = self._tool.outputs_record_schema
@@ -235,12 +237,12 @@ class JobProxy(object):
def _ensure_cwl_job_initialized(self):
if self._cwl_job is None:
self._cwl_job = self._tool_proxy._tool.job(
self._cwl_job = next(self._tool_proxy._tool.job(
self._input_dict,
self._output_callback,
basedir=self._job_directory,
use_container=False
).next()
))
self._is_command_line_job = hasattr(self._cwl_job, "command_line")
@property
@@ -536,7 +538,7 @@ class ConditionalInstance(object):
when=odict(),
)
for value, block in self.whens:
as_dict["when"][value] = map(lambda i: i.to_dict(), block)
as_dict["when"][value] = [i.to_dict() for i in block]
return as_dict
@@ -616,7 +618,7 @@ class OutputInstance(object):
self.path = path
__all__ = [
__all__ = (
'tool_proxy',
'load_job_proxy',
]
)
+3 -3
View File
@@ -20,7 +20,7 @@ def handle_outputs(job_directory=None):
job_proxy = load_job_proxy(job_directory)
tool_working_directory = os.path.join(job_directory, "working")
outputs = job_proxy.collect_outputs(tool_working_directory)
for output_name, output in outputs.iteritems():
for output_name, output in outputs.items():
target_path = job_proxy.output_path( output_name )
if isinstance(output, dict) and "path" in output:
output_path = output["path"]
@@ -48,6 +48,6 @@ def handle_outputs(job_directory=None):
with open(target_path, "w") as f:
f.write(json.dumps(output))
__all__ = [
__all__ = (
'handle_outputs',
]
)
+5 -3
View File
@@ -5,10 +5,12 @@ Dependency management for tools.
import logging
import os.path
from collections import OrderedDict
from galaxy.util import plugin_config
from .resolvers import NullDependency
from .resolvers.conda import CondaDependencyResolver
from .resolvers.conda import CondaDependencyResolver, DEFAULT_ENSURE_CHANNELS
from .resolvers.galaxy_packages import GalaxyPackageDependencyResolver
from .resolvers.tool_shed_packages import ToolShedPackageDependencyResolver
@@ -21,7 +23,7 @@ EXTRA_CONFIG_KWDS = {
'conda_prefix': None,
'conda_exec': None,
'conda_debug': None,
'conda_ensure_channels': 'r,bioconda,iuc',
'conda_ensure_channels': DEFAULT_ENSURE_CHANNELS,
'conda_auto_install': False,
'conda_auto_init': False,
'conda_copy_dependencies': False,
@@ -97,7 +99,7 @@ class DependencyManager( object ):
Takes a list of requirements and returns a dictionary
with requirements as key and dependencies as value.
"""
requirement_to_dependency = dict()
requirement_to_dependency = OrderedDict()
for requirement in requirements:
if requirement.type in [ 'package', 'set_environment' ]:
dependency = self.find_dep( name=requirement.name,
+6 -10
View File
@@ -3,15 +3,12 @@ import os
import subprocess
import sys as _sys
from six.moves import shlex_quote
from galaxy.util import which
STDOUT_INDICATOR = "-"
try:
from shlex import quote as shell_quote
except ImportError:
from pipes import quote as shell_quote
def redirecting_io(sys=_sys):
"""Predicate to determine if we are redicting I/O in process."""
@@ -83,11 +80,11 @@ def argv_to_str(command_argv, quote=True):
If None appears in the command list it is simply excluded.
Arguments are quoted with shlex.quote. That said, this method is not meant to be
Arguments are quoted with shlex_quote. That said, this method is not meant to be
used in security critical paths of code and should not be used to sanitize
code.
"""
map_func = shell_quote if quote else lambda x: x
map_func = shlex_quote if quote else lambda x: x
return " ".join([map_func(c) for c in command_argv if c is not None])
@@ -144,7 +141,7 @@ class CommandLineException(Exception):
return self.message
__all__ = [
__all__ = (
'argv_to_str',
'CommandLineException',
'download_command',
@@ -153,6 +150,5 @@ __all__ = [
'redirecting_io',
'shell',
'shell_process',
'shell_quote',
'which',
]
)
+112
View File
@@ -0,0 +1,112 @@
"""Compat. layer with conda_build/verify if Galaxy/galaxy-lib not installed through conda.
In general there are utilities available for Conda building and parsing that are high-quality
and should be utilized when available but that are only available in conda channels and not in
PyPI. This module serves as a PyPI capable interface to these utilities.
"""
import collections
import os
import yaml
try:
from conda_build.metadata import MetaData
except ImportError:
MetaData = None
try:
from anaconda_verify.recipe import parse, render_jinja2
except ImportError:
render_jinja2 = None
parse = None
class _Memoized(object):
def __init__(self, func):
self.func = func
self.cache = {}
def __call__(self, *args):
if not isinstance(args, collections.Hashable):
# uncacheable. a list, for instance.
# better to not cache than blow up.
return self.func(*args)
if args in self.cache:
return self.cache[args]
else:
value = self.func(*args)
self.cache[args] = value
return value
def _parse(data, cfg):
"""Parse metadata YAML."""
assert cfg is None, "Conda utilities for evaluating cfg are not available."
return dict(yamlize(data))
def _render_jinja2(recipe_dir):
"""Evaluate Conda recipe as a jinja template."""
try:
import jinja2
except ImportError:
raise Exception("Failed to import jinja2 for evaluating Conda recipe templates.")
loaders = [jinja2.FileSystemLoader(recipe_dir)]
env = jinja2.Environment(loader=jinja2.ChoiceLoader(loaders))
template = env.get_or_select_template('meta.yaml')
return template.render(environment=env)
@_Memoized
def yamlize(data):
res = yaml.load(data)
# ensure the result is a dict
if res is None:
res = {}
return res
if render_jinja2 is None:
render_jinja2 = _render_jinja2
if parse is None:
parse = _parse
def raw_metadata(recipe_dir):
"""Evaluate Conda template if needed and return raw metadata for supplied recipe directory."""
meta_path = os.path.join(recipe_dir, 'meta.yaml')
with open(meta_path, 'rb') as fi:
data = fi.read()
if b'{{' in data:
data = render_jinja2(recipe_dir)
meta = parse(data, None)
return meta
class _MetaData(object):
def __init__(self, input_dir):
self.meta = raw_metadata(input_dir)
def get_value(self, field, default=None):
"""Get nested field value or supplied default is not present."""
section, key = field.split('/')
submeta = self.meta.get(section)
if submeta is None:
submeta = {}
res = submeta.get(key)
if res is None:
res = default
return res
if MetaData is None:
MetaData = _MetaData
__all__ = (
"MetaData",
"raw_metadata",
)
+75 -17
View File
@@ -7,12 +7,14 @@ import re
import shutil
import tempfile
from distutils.version import LooseVersion
from sys import platform as _platform
import six
import yaml
from ..deps import commands
from ..deps import installable
log = logging.getLogger(__name__)
@@ -47,7 +49,8 @@ def find_conda_prefix(conda_prefix=None):
return conda_prefix
class CondaContext(object):
class CondaContext(installable.InstallableContext):
installable_description = "Conda"
def __init__(self, conda_prefix=None, conda_exec=None,
shell_exec=None, debug=False, ensure_channels='',
@@ -230,6 +233,16 @@ class CondaContext(object):
def activate(self):
return self._bin("activate")
def is_installed(self):
return self.is_conda_installed()
def can_install(self):
return self.can_install_conda()
@property
def parent_path(self):
return os.path.dirname(os.path.abspath(self.conda_prefix))
def _bin(self, name):
return os.path.join(self.conda_prefix, "bin", name)
@@ -273,6 +286,8 @@ class CondaTarget(object):
return "CondaTarget[%s]" % attributes
__repr__ = __str__
@property
def package_specifier(self):
""" Return a package specifier as consumed by conda install/create.
@@ -293,6 +308,17 @@ class CondaTarget(object):
else:
return "__%s@_uv_" % (self.package)
def __hash__(self):
return hash((self.package, self.version, self.channel))
def __eq__(self, other):
if isinstance(other, self.__class__):
return (self.package, self.version, self.channel) == (other.package, other.version, other.channel)
return False
def __ne__(self, other):
return not(self == other)
def hash_conda_packages(conda_packages, conda_target=None):
""" Produce a unique hash on supplied packages.
@@ -339,23 +365,55 @@ def cleanup_failed_install(conda_target, conda_context=None):
conda_context.exec_remove([conda_target.install_environment])
def is_target_available(conda_target, conda_context=None):
""" Checks if a specified target is available for installation.
If the package name exists return "True". If in addition the version matches exactly return "exact".
Otherwise return False.
def best_search_result(conda_target, conda_context=None, channels_override=None):
"""Find best "conda search" result for specified target.
Return ``None`` if no results match.
"""
conda_context = _ensure_conda_context(conda_context)
conda_context.ensure_channels_configured()
search_cmd = [conda_context.conda_exec, "search", "--full-name", "--json", conda_target.package]
if not channels_override:
conda_context.ensure_channels_configured()
search_cmd = [conda_context.conda_exec, "search", "--full-name", "--json"]
if channels_override:
search_cmd.append("--override-channels")
for channel in channels_override:
search_cmd.extend(["--channel", channel])
search_cmd.append(conda_target.package)
res = commands.execute(search_cmd)
hits = json.loads(res).get(conda_target.package, [])
hits = sorted(hits, key=lambda hit: LooseVersion(hit['version']), reverse=True)
if len(hits) > 0:
if conda_target.version:
for hit in hits:
if hit['version'] == conda_target.version:
return 'exact'
return True
if len(hits) == 0:
return (None, None)
best_result = (hits[0], False)
for hit in hits:
if is_search_hit_exact(conda_target, hit):
best_result = (hit, True)
break
return best_result
def is_search_hit_exact(conda_target, search_hit):
target_version = conda_target.version
# It'd be nice to make request verson of 1.0 match available
# version of 1.0.3 or something like that.
return not target_version or search_hit['version'] == target_version
def is_target_available(conda_target, conda_context=None, channels_override=None):
"""Check if a specified target is available for installation.
If the package name exists return ``True`` (the ``bool``). If in addition
the version matches exactly return "exact" (a string). Otherwise return
``False``.
"""
(best_hit, exact) = best_search_result(conda_target, conda_context, channels_override)
if best_hit:
return 'exact' if exact else True
else:
return False
@@ -387,7 +445,7 @@ def filter_installed_targets(conda_targets, conda_context=None, verbose_install_
installed = functools.partial(is_conda_target_installed,
conda_context=conda_context,
verbose_install_check=verbose_install_check)
return filter(installed, conda_targets)
return list(filter(installed, conda_targets))
def build_isolated_environment(
@@ -452,7 +510,7 @@ def requirement_to_conda_targets(requirement, conda_context=None):
def requirements_to_conda_targets(requirements, conda_context=None):
r_to_ct = functools.partial(requirement_to_conda_targets,
conda_context=conda_context)
conda_targets = map(r_to_ct, requirements)
conda_targets = (r_to_ct(_) for _ in requirements)
return [c for c in conda_targets if c is not None]
@@ -462,10 +520,10 @@ def _ensure_conda_context(conda_context):
return conda_context
__all__ = [
__all__ = (
'CondaContext',
'CondaTarget',
'install_conda',
'install_conda_target',
'requirements_to_conda_targets',
]
)
@@ -0,0 +1,50 @@
"""The module defines the abstract interface for resolving container images for tool execution."""
from abc import (
ABCMeta,
abstractmethod,
abstractproperty,
)
from galaxy.util.dictifiable import Dictifiable
class ContainerResolver(Dictifiable, object):
"""Description of a technique for resolving container images for tool execution."""
# Keys for dictification.
dict_collection_visible_keys = ['resolver_type']
__metaclass__ = ABCMeta
def __init__(self, app_info=None, **kwds):
"""Default initializer for ``ContainerResolver`` subclasses."""
self.app_info = app_info
self.resolver_kwds = kwds
def _get_config_option(self, key, default=None, config_prefix=None, **kwds):
"""Look in resolver-specific settings for option and then fallback to
global settings.
"""
global_key = "%s_%s" % (config_prefix, key)
if key in kwds:
return kwds.get(key)
elif self.app_info and hasattr(self.app_info, global_key):
return getattr(self.app_info, global_key)
else:
return default
@abstractmethod
def resolve(self, tool_info):
"""Find a container matching all supplied requirements for tool.
The supplied argument is a :class:`galaxy.tools.deps.containers.ToolInfo` description
of the tool and its requirements.
"""
@abstractproperty
def resolver_type(self):
"""Short label for the type of container resolution."""
def _container_type_enabled(self, container_description, enabled_container_types):
"""Return a boolean indicating if the specified container type is enabled."""
return container_description.type in enabled_container_types
@@ -0,0 +1,26 @@
"""This module describes the :class:`ExplicitContainerResolver` ContainerResolver plugin."""
import logging
from ..container_resolvers import (
ContainerResolver,
)
log = logging.getLogger(__name__)
class ExplicitContainerResolver(ContainerResolver):
"""Find explicit containers referenced in the tool description (e.g. tool XML file) if present."""
resolver_type = "explicit"
def resolve(self, enabled_container_types, tool_info):
"""Find a container explicitly mentioned in tool description.
This ignores the tool requirements and assumes the tool author crafted
a correct container.
"""
for container_description in tool_info.container_descriptions:
if self._container_type_enabled(container_description):
return True
return False
@@ -0,0 +1,207 @@
"""This module describes the :class:`MulledContainerResolver` ContainerResolver plugin."""
import collections
import logging
from ..container_resolvers import (
ContainerResolver,
)
from ..docker_util import build_docker_images_command
from ..mulled.mulled_build import (
check_output,
DEFAULT_CHANNELS,
ensure_installed,
InvolucroContext,
mull_targets,
)
from ..mulled.mulled_build_tool import requirements_to_mulled_targets
from ..mulled.util import (
image_name,
mulled_tags_for,
split_tag,
)
from ..requirements import ContainerDescription
log = logging.getLogger(__name__)
CachedMulledImageSingleTarget = collections.namedtuple("CachedMulledImageSingleTarget", ["package_name", "version", "build", "image_identifier"])
CachedMulledImageMultiTarget = collections.namedtuple("CachedMulledImageMultiTarget", ["hash", "image_identifier"])
CachedMulledImageSingleTarget.multi_target = False
CachedMulledImageMultiTarget.multi_target = True
def list_cached_mulled_images(namespace=None):
command = build_docker_images_command(truncate=True, sudo_docker=False)
command = "%s | tail -n +2 | tr -s ' ' | cut -d' ' -f1,2" % command
images_and_versions = check_output(command)
name_filter = get_filter(namespace)
def output_line_to_image(line):
image_name, version = line.split(" ", 1)
identifier = "%s:%s" % (image_name, version)
url, namespace, package_description = image_name.split("/")
if package_description.startswith("mulled-v1-"):
hash = package_description
image = CachedMulledImageMultiTarget(hash, identifier)
else:
build = None
if not version or version == "latest":
version = None
if version and "--" in version:
version, build = split_tag(version)
image = CachedMulledImageSingleTarget(image_name, version, build, identifier)
return image
return [output_line_to_image(_) for _ in filter(name_filter, images_and_versions.splitlines())]
def get_filter(namespace):
prefix = "quay.io/" if namespace is None else "quay.io/%s" % namespace
return lambda name: name.startswith(prefix) and name.count("/") == 2
def cached_container_description(targets, namespace):
if len(targets) == 0:
return None
cached_images = list_cached_mulled_images(namespace)
image = None
if len(targets) == 1:
target = targets[0]
for cached_image in cached_images:
if cached_image.multi_target:
continue
if not cached_image.package_name == target.package_name:
continue
if not target.version or target.version == cached_image.version:
image = cached_image
break
else:
name = image_name(targets)
for cached_image in cached_images:
if not cached_image.multi_target:
continue
if name == cached_image.hash:
image = cached_image
break
container = None
if image:
container = ContainerDescription(
image.image_identifier,
type="docker",
)
return container
class CachedMulledContainerResolver(ContainerResolver):
resolver_type = "cached_mulled"
def __init__(self, app_info=None, namespace=None):
super(CachedMulledContainerResolver, self).__init__(app_info)
self.namespace = namespace
def resolve(self, enabled_container_types, tool_info):
targets = mulled_targets(tool_info)
return cached_container_description(targets, self.namespace)
class MulledContainerResolver(ContainerResolver):
"""Look for mulled images matching tool dependencies."""
resolver_type = "mulled"
def __init__(self, app_info=None, namespace="mulled"):
super(MulledContainerResolver, self).__init__(app_info)
self.namespace = namespace
def resolve(self, enabled_container_types, tool_info):
targets = mulled_targets(tool_info)
if len(targets) == 0:
return None
name = None
if len(targets) == 1:
target = targets[0]
target_version = target.version
tags = mulled_tags_for(self.namespace, target.package_name)
if not tags:
return None
if target_version:
for tag in tags:
version, build = split_tag(tag)
if version == target_version:
name = "%s:%s--%s" % (target.package_name, version, build)
break
else:
version, build = split_tag(tags[0])
name = "%s:%s--%s" % (target.package_name, version, build)
else:
base_image_name = image_name(targets)
tags = mulled_tags_for(self.namespace, base_image_name)
if tags:
name = "%s:%s" % (base_image_name, tags[0])
if name:
return ContainerDescription(
"quay.io/%s/%s" % (self.namespace, name),
type="docker",
)
class BuildMulledContainerResolver(ContainerResolver):
"""Look for mulled images matching tool dependencies."""
resolver_type = "build_mulled"
def __init__(self, app_info=None, namespace="local", **kwds):
super(BuildMulledContainerResolver, self).__init__(app_info)
self._involucro_context_kwds = {
'involucro_bin': self._get_config_option("involucro_path", None)
}
self.namespace = namespace
self._mulled_kwds = {
'namespace': namespace,
'channels': self._get_config_option("channels", DEFAULT_CHANNELS, prefix="mulled"),
}
self.auto_init = self._get_config_option("auto_init", DEFAULT_CHANNELS, prefix="involucro")
def resolve(self, enabled_container_types, tool_info):
targets = mulled_targets(tool_info)
if len(targets) == 0:
return None
mull_targets(
targets,
involucro_context=self._get_involucro_context(),
**self._mulled_kwds
)
return cached_container_description(targets, self.namespace)
def _get_involucro_context(self):
involucro_context = InvolucroContext(**self._involucro_context_kwds)
self.enabled = ensure_installed(involucro_context, self.auto_init)
return involucro_context
def mulled_targets(tool_info):
return requirements_to_mulled_targets(tool_info.requirements)
__all__ = (
"CachedMulledContainerResolver",
"MulledContainerResolver",
"BuildMulledContainerResolver",
)
+80 -26
View File
@@ -10,14 +10,23 @@ from abc import (
import six
from galaxy.util import asbool
from galaxy.util import plugin_config
from .container_resolvers.explicit import ExplicitContainerResolver
from .container_resolvers.mulled import (
BuildMulledContainerResolver,
CachedMulledContainerResolver,
MulledContainerResolver,
)
from .requirements import ContainerDescription
from .requirements import DEFAULT_CONTAINER_RESOLVE_DEPENDENCIES, DEFAULT_CONTAINER_SHELL
from ..deps import docker_util
log = logging.getLogger(__name__)
DEFAULT_CONTAINER_TYPE = "docker"
DOCKER_CONTAINER_TYPE = "docker"
DEFAULT_CONTAINER_TYPE = DOCKER_CONTAINER_TYPE
ALL_CONTAINER_TYPES = [DOCKER_CONTAINER_TYPE]
LOAD_CACHED_IMAGE_COMMAND_TEMPLATE = '''
python << EOF
@@ -47,9 +56,18 @@ class ContainerFinder(object):
def __init__(self, app_info):
self.app_info = app_info
self.container_registry = ContainerRegistry()
self.container_registry = ContainerRegistry(app_info)
def __enabled_container_types(self, destination_info):
return [t for t in ALL_CONTAINER_TYPES if self.__container_type_enabled(t, destination_info)]
def find_container(self, tool_info, destination_info, job_info):
enabled_container_types = self.__enabled_container_types(destination_info)
# Short-cut everything else and just skip checks if no container type is enabled.
if not enabled_container_types:
return NULL_CONTAINER
def __destination_container(container_description=None, container_id=None, container_type=None):
if container_description:
container_id = container_description.identifier
@@ -82,23 +100,10 @@ class ContainerFinder(object):
return container
# Otherwise lets see if we can find container for the tool.
# Exact matches first from explicitly listed containers in tools...
for container_description in tool_info.container_descriptions:
container = __destination_container(container_description)
if container:
return container
# Implement vague concept of looping through all containers
# matching requirements. Exact details need to be worked through
# but hopefully the idea that it sits below find_container somewhere
# external components to this module don't need to worry about it
# is good enough.
container_descriptions = self.container_registry.container_descriptions_for_requirements(tool_info.requirements)
for container_description in container_descriptions:
container = __destination_container(container_description)
if container:
return container
container_description = self.container_registry.find_best_container_description(enabled_container_types, tool_info)
container = __destination_container(container_description)
if container:
return container
# If we still don't have a container, check to see if any container
# types define a default container id and use that.
@@ -175,14 +180,55 @@ class NullContainerFinder(object):
return []
class ContainerRegistry():
class ContainerRegistry(object):
"""Loop through enabled ContainerResolver plugins and find first match."""
def __init__(self):
pass
def __init__(self, app_info):
self.resolver_classes = self.__resolvers_dict()
self.enable_beta_mulled_containers = app_info.enable_beta_mulled_containers
self.app_info = app_info
self.container_resolvers = self.__build_container_resolvers(app_info)
def container_descriptions_for_requirements(self, requirements):
# Return lists of containers that would match requirements...
return []
def __build_container_resolvers( self, app_info ):
conf_file = getattr(app_info, 'containers_resolvers_config_file', None)
if not conf_file:
return self.__default_containers_resolvers()
if not os.path.exists( conf_file ):
log.debug( "Unable to find config file '%s'", conf_file)
return self.__default_containers_resolvers()
plugin_source = plugin_config.plugin_source_from_path( conf_file )
return self.__parse_resolver_conf_xml( plugin_source )
def __parse_resolver_conf_xml(self, plugin_source):
extra_kwds = {}
return plugin_config.load_plugins(self.resolver_classes, plugin_source, extra_kwds)
def __default_containers_resolvers(self):
default_resolvers = [
ExplicitContainerResolver(self.app_info),
]
if self.enable_beta_mulled_containers:
default_resolvers.extend([
CachedMulledContainerResolver(self.app_info),
MulledContainerResolver(self.app_info, namespace="mulled"),
BuildMulledContainerResolver(self.app_info),
])
return default_resolvers
def __resolvers_dict( self ):
import galaxy.tools.deps.container_resolvers
return plugin_config.plugins_dict( galaxy.tools.deps.container_resolvers, 'resolver_type' )
def find_best_container_description(self, enabled_container_types, tool_info):
"""Yield best container description of supplied types matching tool info."""
for container_resolver in self.container_resolvers:
container_description = container_resolver.resolve(enabled_container_types, tool_info)
log.info("Checking with container resolver [%s] found description [%s]" % (container_resolver, container_description))
if container_description:
assert container_description.type in enabled_container_types
return container_description
return None
class AppInfo(object):
@@ -193,7 +239,11 @@ class AppInfo(object):
default_file_path=None,
outputs_to_working_directory=False,
container_image_cache_path=None,
library_import_dir=None
library_import_dir=None,
enable_beta_mulled_containers=False,
containers_resolvers_config_file=None,
involucro_path=None,
involucro_auto_init=True,
):
self.galaxy_root_dir = galaxy_root_dir
self.default_file_path = default_file_path
@@ -201,6 +251,10 @@ class AppInfo(object):
self.outputs_to_working_directory = outputs_to_working_directory
self.container_image_cache_path = container_image_cache_path
self.library_import_dir = library_import_dir
self.enable_beta_mulled_containers = enable_beta_mulled_containers
self.containers_resolvers_config_file = containers_resolvers_config_file
self.involucro_path = involucro_path
self.involucro_auto_init = involucro_auto_init
class ToolInfo(object):
+13 -10
View File
@@ -3,7 +3,10 @@
...using common defaults and configuration mechanisms.
"""
import os
from .commands import argv_to_str, shell_quote
from six.moves import shlex_quote
from .commands import argv_to_str
DEFAULT_DOCKER_COMMAND = "docker"
DEFAULT_SUDO = True
@@ -33,7 +36,7 @@ class DockerVolume(object):
if not volumes_as_str:
return []
volume_strs = [v.strip() for v in volumes_as_str.split(",")]
return map(DockerVolume.volume_from_str, volume_strs)
return [DockerVolume.volume_from_str(_) for _ in volume_strs]
@staticmethod
def volume_from_str(as_str):
@@ -152,19 +155,19 @@ def build_docker_run_command(
if terminal:
command_parts.append("-t")
for env_directive in env_directives:
command_parts.extend(["-e", shell_quote(env_directive)])
command_parts.extend(["-e", shlex_quote(env_directive)])
for volume in volumes:
command_parts.extend(["-v", shell_quote(str(volume))])
command_parts.extend(["-v", shlex_quote(str(volume))])
if volumes_from:
command_parts.extend(["--volumes-from", shell_quote(str(volumes_from))])
command_parts.extend(["--volumes-from", shlex_quote(str(volumes_from))])
if memory:
command_parts.extend(["-m", shell_quote(memory)])
command_parts.extend(["-m", shlex_quote(memory)])
if name:
command_parts.extend(["--name", shell_quote(name)])
command_parts.extend(["--name", shlex_quote(name)])
if working_directory:
command_parts.extend(["-w", shell_quote(working_directory)])
command_parts.extend(["-w", shlex_quote(working_directory)])
if net:
command_parts.extend(["--net", shell_quote(net)])
command_parts.extend(["--net", shlex_quote(net)])
if auto_rm:
command_parts.append("--rm")
if run_extra_arguments:
@@ -177,7 +180,7 @@ def build_docker_run_command(
full_image = image
if tag:
full_image = "%s:%s" % (full_image, tag)
command_parts.append(shell_quote(full_image))
command_parts.append(shlex_quote(full_image))
command_parts.append(container_command)
return " ".join(command_parts)
+77
View File
@@ -0,0 +1,77 @@
"""Abstractions for installing local software managed and required by Galaxy/galaxy-lib."""
import logging
import os
from abc import (
ABCMeta,
abstractmethod,
abstractproperty,
)
from galaxy.util.filelock import (
FileLock,
FileLockException
)
log = logging.getLogger(__name__)
class InstallableContext(object):
"""Represent a directory/configuration of something that can be installed."""
__metaclass__ = ABCMeta
@abstractmethod
def is_installed(self):
"""Return bool indicating if the configured software is installed."""
@abstractmethod
def can_install(self):
"""Check preconditions for installation."""
@abstractproperty
def installable_description(self):
"""Short description of thing being installed for log statements."""
@abstractproperty
def parent_path(self):
"""Return parent path of the location the installable will be created within."""
def ensure_installed(installable_context, install_func, auto_init):
"""Make sure target is installed - handle multiple processes potentially attempting installation."""
parent_path = installable_context.parent_path
desc = installable_context.installable_description
def _check():
if not installable_context.is_installed():
if auto_init:
if installable_context.can_install():
if install_func(installable_context):
installed = False
log.warning("%s installation requested and failed." % desc)
else:
installed = installable_context.is_installed()
if not installed:
log.warning("%s installation requested, seemed to succeed, but not found." % desc)
else:
installed = False
else:
installed = False
log.warning("%s not installed and auto-installation disabled.", desc)
else:
installed = True
return installed
if not os.path.exists(parent_path):
os.mkdir(parent_path)
try:
if auto_init and os.access(parent_path, os.W_OK):
with FileLock(os.path.join(parent_path, desc.lower())):
return _check()
else:
return _check()
except FileLockException:
return ensure_installed(installable_context, auto_init)
+19
View File
@@ -0,0 +1,19 @@
"""CLI helpers for mulled command-line tools."""
import argparse
def arg_parser(argv, globals):
"""Build an argparser for this CLI tool."""
doc = globals["__doc__"]
description, epilog = doc.split("\n", 1)
parser = argparse.ArgumentParser(
description=description,
epilog=epilog,
formatter_class=argparse.RawTextHelpFormatter,
)
return parser
__all__ = (
"arg_parser",
)
+67
View File
@@ -0,0 +1,67 @@
-- http://stackoverflow.com/questions/19262761/lua-need-to-split-at-comma/19263313#19263313
function string:split( inSplitPattern, outResults )
if not outResults then
outResults = { }
end
local theStart = 1
local theSplitStart, theSplitEnd = string.find( self, inSplitPattern, theStart )
while theSplitStart do
table.insert( outResults, string.sub( self, theStart, theSplitStart-1 ) )
theStart = theSplitEnd + 1
theSplitStart, theSplitEnd = string.find( self, inSplitPattern, theStart )
end
table.insert( outResults, string.sub( self, theStart ) )
return outResults
end
local repo = VAR.REPO
local channel_args = ''
local channels = VAR.CHANNELS:split(",")
for i = 1, #channels do
channel_args = channel_args .. " -c " .. channels[i]
end
local target_args = ''
local targets = VAR.TARGETS:split(",")
for i = 1, #targets do
target_args = target_args .. " " .. targets[i]
end
local bind_args = {}
local binds_table = VAR.BINDS:split(",")
for i = 1, #binds_table do
table.insert(bind_args, binds_table[i])
end
inv.task('build')
.using('continuumio/miniconda:latest')
.withHostConfig({binds = {"build:/data"}})
.run('rm', '-rf', '/data/dist')
.using('continuumio/miniconda:latest')
.withHostConfig({binds = bind_args})
.run('/bin/sh', '-c', 'conda install '
.. channel_args .. ' '
.. target_args
.. ' -p /usr/local --copy --yes')
.wrap('build/dist')
.at('/usr/local')
.inImage('bgruening/busybox-bash:0.1')
.as(repo)
inv.task('test')
.using(repo)
.withConfig({entrypoint = {'/bin/sh', '-c'}})
.run(VAR.TEST)
inv.task('push')
.push(repo)
inv.task('build-and-test')
.runTask('build')
.runTask('test')
inv.task('all')
.runTask('build')
.runTask('test')
.runTask('push')
@@ -0,0 +1,286 @@
#!/usr/bin/env python
"""Build a mulled image for specified conda targets.
Examples
Build a mulled image with:
mulled-build build 'samtools=1.3.1--4,bedtools=2.22'
"""
from __future__ import print_function
import json
import os
import string
import subprocess
from sys import platform as _platform
try:
import yaml
except ImportError:
yaml = None
from galaxy.tools.deps import commands, installable
from ._cli import arg_parser
from .util import build_target, conda_build_target_str, image_name
from ..conda_compat import MetaData
DIRNAME = os.path.dirname(__file__)
DEFAULT_CHANNEL = "bioconda"
DEFAULT_EXTRA_CHANNELS = ["conda-forge", "r"]
DEFAULT_CHANNELS = [DEFAULT_CHANNEL] + DEFAULT_EXTRA_CHANNELS
DEFAULT_REPOSITORY_TEMPLATE = "quay.io/${namespace}/${image}"
DEFAULT_BINDS = ["build/dist:/usr/local/"]
IS_OS_X = _platform == "darwin"
INVOLUCRO_VERSION = "1.1.2"
def involucro_link():
if IS_OS_X:
url = "https://github.com/involucro/involucro/releases/download/v%s/involucro.darwin" % INVOLUCRO_VERSION
else:
url = "https://github.com/involucro/involucro/releases/download/v%s/involucro" % INVOLUCRO_VERSION
return url
def get_tests(args, pkg_path):
"""Extract test cases given a recipe's meta.yaml file."""
recipes_dir = args.recipes_dir
tests = ""
input_dir = os.path.dirname(os.path.join(recipes_dir, pkg_path))
recipe_meta = MetaData(input_dir)
tests_commands = recipe_meta.get_value('test/commands')
tests_imports = recipe_meta.get_value('test/imports')
requirements = recipe_meta.get_value('requirements/run')
if tests_imports or tests_commands:
if tests_commands:
tests = ' && '.join(tests_commands)
elif tests_imports and 'python' in requirements:
tests = ' && '.join('python -c "import %s"' % imp for imp in tests_imports)
elif tests_imports and ('perl' in requirements or 'perl-threaded' in requirements):
tests = ' && '.join('''perl -e "use %s;"''' % imp for imp in tests_imports)
tests = tests.replace('$R ', 'Rscript ')
else:
pass
return tests
def get_pkg_name(args, pkg_path):
"""Extract the package name from a given meta.yaml file."""
recipes_dir = args.recipes_dir
input_dir = os.path.dirname(os.path.join(recipes_dir, pkg_path))
recipe_meta = MetaData(input_dir)
return recipe_meta.get_value('package/name')
def get_affected_packages(args):
"""Return a list of all meta.yaml file that where modified/created recently.
Length of time to check for indicated by the ``hours`` parameter.
"""
recipes_dir = args.recipes_dir
hours = args.diff_hours
cmd = """cd '%s' && git log --diff-filter=ACMRTUXB --name-only --pretty="" --since="%s hours ago" | grep -E '^recipes/.*/meta.yaml' | sort | uniq""" % (recipes_dir, hours)
pkg_list = check_output(cmd, shell=True)
ret = list()
for pkg in pkg_list.strip().split('\n'):
if pkg and os.path.exists(os.path.join( recipes_dir, pkg )):
ret.append( (get_pkg_name(args, pkg), get_tests(args, pkg)) )
return ret
def check_output(cmd, shell=True):
return subprocess.check_output(cmd, shell=shell)
def conda_versions(pkg_name, file_name):
"""Return all conda version strings for a specified package name."""
j = json.load(open(file_name))
ret = list()
for pkg in j['packages'].values():
if pkg['name'] == pkg_name:
ret.append('%s--%s' % (pkg['version'], pkg['build']))
return ret
def mull_targets(
targets, involucro_context=None,
command="build", channels=DEFAULT_CHANNELS, namespace="mulled",
test='true', image_build=None, name_override=None,
repository_template=DEFAULT_REPOSITORY_TEMPLATE, dry_run=False,
binds=DEFAULT_BINDS
):
targets = list(targets)
if involucro_context is None:
involucro_context = InvolucroContext()
repo_template_kwds = {
"namespace": namespace,
"image": image_name(targets, image_build=image_build, name_override=name_override)
}
repo = string.Template(repository_template).safe_substitute(repo_template_kwds)
for channel in channels:
if channel.startswith('file://'):
bind_path = channel.lstrip('file://')
binds.append('/%s:/%s' % (bind_path, bind_path))
channels = ",".join(channels)
target_str = ",".join(map(conda_build_target_str, targets))
bind_str = ",".join(binds)
involucro_args = [
'-f', '%s/invfile.lua' % DIRNAME,
'-set', "CHANNELS='%s'" % channels,
'-set', "TEST='%s'" % test,
'-set', "TARGETS='%s'" % target_str,
'-set', "REPO='%s'" % repo,
'-set', "BINDS='%s'" % bind_str,
command,
]
print(" ".join(involucro_context.build_command(involucro_args)))
if not dry_run:
ensure_installed(involucro_context, True)
involucro_context.exec_command(involucro_args)
def context_from_args(args):
return InvolucroContext(involucro_bin=args.involucro_path)
class InvolucroContext(installable.InstallableContext):
installable_description = "Involucro"
def __init__(self, involucro_bin=None, shell_exec=None, verbose="3"):
if involucro_bin is None:
if os.path.exists("./involucro"):
self.involucro_bin = "./involucro"
else:
self.involucro_bin = "involucro"
else:
self.involucro_bin = involucro_bin
self.shell_exec = shell_exec or commands.shell
self.verbose = verbose
def build_command(self, involucro_args):
return [self.involucro_bin, "-v=%s" % self.verbose] + involucro_args
def exec_command(self, involucro_args):
cmd = self.build_command(involucro_args)
return self.shell_exec(" ".join(cmd))
def is_installed(self):
return os.path.exists(self.involucro_bin)
def can_install(self):
return True
@property
def parent_path(self):
return os.path.dirname(os.path.abspath(self.involucro_bin))
def ensure_installed(involucro_context, auto_init):
return installable.ensure_installed(involucro_context, install_involucro, auto_init)
def install_involucro(involucro_context=None, to_path=None):
to_path = involucro_context.involucro_bin
download_cmd = " ".join(commands.download_command(involucro_link(), to=to_path, quote_url=True))
full_cmd = "%s && chmod +x %s" % (download_cmd, to_path)
return involucro_context.shell_exec(full_cmd)
def add_build_arguments(parser):
"""Base arguments describing how to 'mull'."""
parser.add_argument('--involucro-path', dest="involucro_path", default=None,
help="Path to involucro (if not set will look in working directory and on PATH).")
parser.add_argument('--force-rebuild', dest="force_rebuild", action="store_true",
help="Rebuild package even if already published.")
parser.add_argument('--dry-run', dest='dry_run', action="store_true",
help='Just print commands instead of executing them.')
parser.add_argument('-n', '--namespace', dest='namespace', default="mulled",
help='quay.io namespace.')
parser.add_argument('-r', '--repository_template', dest='repository_template', default=DEFAULT_REPOSITORY_TEMPLATE,
help='Docker repository target for publication (only quay.io or compat. API is currently supported).')
parser.add_argument('-c', '--channel', dest='channel', default=DEFAULT_CHANNEL,
help='Target conda channel')
parser.add_argument('--extra-channels', dest='extra_channels', default=",".join(DEFAULT_EXTRA_CHANNELS),
help='Dependent conda channels.')
def add_single_image_arguments(parser):
parser.add_argument("--name-override", dest="name_override", default=None,
help="Override mulled image name - this is not recommended since metadata will not be detectable from the name of resulting images")
parser.add_argument("--image-build", dest="image_build", default=None,
help="Build a versioned variant of this image.")
def target_str_to_targets(targets_raw):
def parse_target(target_str):
if "=" in target_str:
package_name, version = target_str.split("=", 1)
build = None
if "--" in version:
version, build = version.split('--')
target = build_target(package_name, version, build)
else:
target = build_target(target_str)
return target
targets = [parse_target(_) for _ in targets_raw.split(",")]
return targets
def args_to_mull_targets_kwds(args):
kwds = {}
if hasattr(args, "image_build"):
kwds["image_build"] = args.image_build
if hasattr(args, "name_override"):
kwds["name_override"] = args.name_override
if hasattr(args, "namespace"):
kwds["namespace"] = args.namespace
if hasattr(args, "dry_run"):
kwds["dry_run"] = args.dry_run
if hasattr(args, "test"):
kwds["test"] = args.test
if hasattr(args, "channel"):
channels = [args.channel]
if hasattr(args, "extra_channels"):
channels += args.extra_channels.split(",")
kwds["channels"] = channels
if hasattr(args, "command"):
kwds["command"] = args.command
if hasattr(args, "repository_template"):
kwds["repository_template"] = args.repository_template
kwds["involucro_context"] = context_from_args(args)
return kwds
def main(argv=None):
"""Main entry-point for the CLI tool."""
parser = arg_parser(argv, globals())
add_build_arguments(parser)
add_single_image_arguments(parser)
parser.add_argument('command', metavar='COMMAND', help='Command (build-and-test, build, all)')
parser.add_argument('targets', metavar="TARGETS", default=None, help="Build a single container with specific package(s).")
parser.add_argument('--repository-name', dest="repository_name", default=None, help="Name of mulled container (leave blank to auto-generate based on packages - recommended).")
parser.add_argument('--test', help='Provide a test command for the container.')
args = parser.parse_args()
targets = target_str_to_targets(args.targets)
mull_targets(targets, **args_to_mull_targets_kwds(args))
__all__ = ("main", )
if __name__ == '__main__':
main()
@@ -0,0 +1,109 @@
#!/usr/bin/env python
"""Build a mulled images for all recent conda recipe updates that don't have existing images.
Examples:
Build mulled images for recent bioconda changes with:
mulled-build-channel build
Build, test, and publish images with the follow command:
mulled-build-channel all
See recent changes that would be built with:
mulled-build-channel list
"""
from __future__ import print_function
import os
import time
from ._cli import arg_parser
from .mulled_build import (
add_build_arguments,
args_to_mull_targets_kwds,
build_target,
check_output,
conda_versions,
get_affected_packages,
mull_targets,
)
from .util import quay_versions, version_sorted
def _fetch_repo_data(args):
repo_data = args.repo_data
channel = args.channel
if repo_data is None:
repo_data = "%s-repodata.json" % channel
if not os.path.exists(repo_data):
check_output("wget --quiet https://conda.anaconda.org/%s/linux-64/repodata.json.bz2 -O '%s.bz2' && bzip2 -d '%s.bz2'" % (channel, repo_data, repo_data))
return repo_data
def _new_versions(quay, conda):
"""Calculate the versions that are in conda but not on quay.io."""
sconda = set(conda)
squay = set(quay) if quay else set()
return sconda - squay # sconda.symmetric_difference(squay)
def run_channel(args, build_last_n_versions=1):
"""Build list of involucro commands (as shell snippet) to run."""
pkgs = get_affected_packages(args)
for pkg_name, pkg_tests in pkgs:
repo_data = _fetch_repo_data(args)
c = conda_versions(pkg_name, repo_data)
# only package the most recent N versions
c = version_sorted(c)[:build_last_n_versions]
if not args.force_rebuild:
time.sleep(1)
q = quay_versions(args.namespace, pkg_name)
versions = _new_versions(q, c)
else:
versions = c
for tag in versions:
target = build_target(pkg_name, tag=tag)
targets = [target]
mull_targets(targets, test=pkg_tests, **args_to_mull_targets_kwds(args))
def get_pkg_names(args):
"""Print package names that would be affected."""
print('\n'.join(pkg_name for pkg_name, pkg_tests in get_affected_packages(args)))
def add_channel_arguments(parser):
"""Add arguments only used if running mulled over a whole conda channel."""
parser.add_argument('--repo-data', dest='repo_data', default=None,
help='Published repository data (will be fetched from --channel if not available and written). Defaults to [channel_name]-repodata.json.')
parser.add_argument('--diff-hours', dest='diff_hours', default="25",
help='If finding all recently changed recipes, use this number of hours.')
parser.add_argument('--recipes-dir', dest="recipes_dir", default="./bioconda-recipes")
def main(argv=None):
"""Main entry-point for the CLI tool."""
parser = arg_parser(argv, globals())
add_channel_arguments(parser)
add_build_arguments(parser)
parser.add_argument('command', metavar='COMMAND', help='Command (list, build-and-test, build, all)')
parser.add_argument('--targets', dest="targets", default=None, help="Build a single container with specific package(s).")
parser.add_argument('--repository-name', dest="repository_name", default=None, help="Name of a single container (leave blank to auto-generate based on packages).")
args = parser.parse_args()
if args.command == "list":
get_pkg_names(args)
else:
run_channel(args)
__all__ = ("main", )
if __name__ == '__main__':
main()
@@ -0,0 +1,79 @@
"""Build all composite mulled recipes discovered in TSV files.
Use mulled-build-channel to build images for single recipes for a whole conda
channel. This script instead builds images for combinations of recipes. This
script can be given a single TSV file or a directory of TSV files to process.
Examples:
Build all recipes discovered in tsv files in a single directory.
mulled-build-files build
"""
import collections
import glob
import os
from ._cli import arg_parser
from .mulled_build import (
add_build_arguments,
args_to_mull_targets_kwds,
mull_targets,
target_str_to_targets,
)
def main(argv=None):
"""Main entry-point for the CLI tool."""
parser = arg_parser(argv, globals())
add_build_arguments(parser)
parser.add_argument('command', metavar='COMMAND', help='Command (build-and-test, build, all)')
parser.add_argument('files', metavar="FILES", default=".",
help="Path to directory (or single file) of TSV files describing composite recipes.")
args = parser.parse_args()
for targets in generate_targets(args.files):
mull_targets(targets, **args_to_mull_targets_kwds(args))
def generate_targets(target_source):
"""Generate all targets from TSV files in specified file or directory."""
target_source = os.path.abspath(target_source)
if os.path.isdir(target_source):
target_source_files = glob.glob(target_source + "/*.tsv")
else:
target_source_files = [target_source]
for target_source_file in target_source_files:
with open(target_source_file, "r") as f:
for line in f.readlines():
if line:
line = line.strip()
if not line or line.startswith("#"):
continue
yield line_to_targets(line)
def line_to_targets(line_str):
line = _parse_line(line_str)
return target_str_to_targets(line)
_Line = collections.namedtuple("_Line", ["targets", "image_build", "name_override"])
def _parse_line(line_str):
line_parts = line_str.split(" ")
assert len(line_parts) < 3, "Too many fields in line [%s], expect at most 3 - targets, image build number, and name override." % line_str
line_parts += [None] * (3 - len(line_parts))
return _Line(*line_parts)
__all__ = ("main", )
if __name__ == '__main__':
main()
@@ -0,0 +1,52 @@
#!/usr/bin/env python
"""Build a mulled images for a tool source (Galaxy or CWL tool).
Examples:
Build mulled images for requirements defined in a tool:
mulled-build-tool build path/to/tool_file.xml
"""
from galaxy.tools.parser import get_tool_source
from ._cli import arg_parser
from .mulled_build import (
add_build_arguments,
add_single_image_arguments,
args_to_mull_targets_kwds,
build_target,
mull_targets,
)
def main(argv=None):
"""Main entry-point for the CLI tool."""
parser = arg_parser(argv, globals())
add_build_arguments(parser)
add_single_image_arguments(parser)
parser.add_argument('command', metavar='COMMAND', help='Command (build-and-test, build, all)')
parser.add_argument('tool', metavar="TOOL", default=None, help="Path to tool to build mulled image for.")
args = parser.parse_args()
tool_source = get_tool_source(args.tool)
requirements, _ = tool_source.parse_requirements_and_containers()
targets = requirements_to_mulled_targets(requirements)
mull_targets(targets, **args_to_mull_targets_kwds(args))
def requirements_to_mulled_targets(requirements):
"""Convert Galaxy's representation of requirements into mulled Target objects.
Only package requirements are retained.
"""
package_requirements = [r for r in requirements if r.type == "package"]
targets = [build_target(r.name, r.version) for r in package_requirements]
return targets
__all__ = ("main", "requirements_to_mulled_targets")
if __name__ == '__main__':
main()
@@ -0,0 +1,131 @@
#!/usr/bin/env python
import argparse
import json
import sys
import tempfile
try:
import requests
except ImportError:
requests = None
try:
from whoosh.fields import Schema
from whoosh.fields import TEXT
from whoosh.fields import STORED
from whoosh.index import create_in
from whoosh.qparser import QueryParser
except ImportError:
Schema = TEXT = STORED = create_in = QueryParser = None
QUAY_API_URL = 'https://quay.io/api/v1/repository'
class QuaySearch():
"""
Tool to search within a quay organization for a given software name.
"""
def __init__(self, organization):
self.index = None
self.organization = organization
def build_index(self):
"""
Create an index to quickly examine the repositories of a given quay.io organization.
"""
# download all information about the repositories from the
# given organization in self.organization
parameters = {'public': 'true', 'namespace': self.organization}
r = requests.get(QUAY_API_URL, headers={'Accept-encoding': 'gzip'}, params=parameters,
timeout=12)
tmp_dir = tempfile.mkdtemp()
schema = Schema(title=TEXT(stored=True), content=STORED)
self.index = create_in(tmp_dir, schema)
json_decoder = json.JSONDecoder()
decoded_request = json_decoder.decode(r.text)
writer = self.index.writer()
for repository in decoded_request['repositories']:
writer.add_document(title=repository['name'], content=repository['description'])
writer.commit()
def search_repository(self, search_string, non_strict):
"""
Search Docker containers on quay.io.
Results are displayed with all available versions,
including the complete image name.
"""
# with statement closes searcher after usage.
with self.index.searcher() as searcher:
search_string = "*%s*" % search_string
query = QueryParser("title", self.index.schema).parse(search_string)
results = searcher.search(query)
if non_strict:
# look for spelling errors and use suggestions as a search term too
corrector = searcher.corrector("title")
suggestions = corrector.suggest(search_string, limit=2)
# get all repositories with suggested keywords
for suggestion in suggestions:
search_string = "*%s*" % suggestion
query = QueryParser("title", self.index.schema).parse(search_string)
results_tmp = searcher.search(query)
results.extend(results_tmp)
sys.stdout.write("The query \033[1m %s \033[0m resulted in %s result(s).\n" % (search_string, len(results)))
if non_strict:
sys.stdout.write('The search was relaxed and the following search terms were searched: ')
sys.stdout.write('\033[1m %s \033[0m\n' % ', '.join(suggestions))
out = list()
for result in results:
title = result['title']
for version in self.get_additional_repository_information(title):
row = [title]
row.append(version)
out.append(row)
if out:
col_width = max(len(word) for row in out for word in row) + 2 # padding
for row in out:
name = row[0]
version = row[1]
sys.stdout.write("".join(word.ljust(col_width) for word in row) + "docker pull quay.io/%s/%s:%s\n" % (self.organization, name, version))
else:
sys.stdout.write("No results found for %s in quay.io/%s.\n" % (search_string, self.organization))
def get_additional_repository_information(self, repository_string):
"""
Function downloads additional information from quay.io to
get the tag-field which includes the version number.
"""
url = "%s/%s/%s" % (QUAY_API_URL, self.organization, repository_string)
r = requests.get(url, headers={'Accept-encoding': 'gzip'}, timeout=12)
json_decoder = json.JSONDecoder()
decoded_request = json_decoder.decode(r.text)
return decoded_request['tags']
def main(argv=None):
parser = argparse.ArgumentParser(description='Searches in a given quay organization for a repository')
parser.add_argument('-o', '--organization', dest='organization_string', default="mulled",
help='Change organization. Default is mulled.')
parser.add_argument('--non-strict', dest='non_strict', action="store_true",
help='Autocorrection of typos activated. Lists more results but can be confusing.\
For too many queries quay.io blocks the request and the results can be incomplete.')
parser.add_argument('-s', '--search', required=True,
help='The name of the tool you want to search for.')
args = parser.parse_args()
quay = QuaySearch(args.organization_string)
quay.build_index()
quay.search_repository(args.search, args.non_strict)
if __name__ == "__main__":
main()
+117
View File
@@ -0,0 +1,117 @@
"""Utilities for working with mulled abstractions outside the mulled package."""
from __future__ import print_function
import collections
import hashlib
from distutils.version import LooseVersion
try:
import requests
except ImportError:
requests = None
def quay_versions(namespace, pkg_name):
"""Get all version tags for a Docker image stored on quay.io for supplied package name."""
if requests is None:
raise Exception("requets library is unavailable, functionality not available.")
assert namespace is not None
assert pkg_name is not None
url = 'https://quay.io/api/v1/repository/%s/%s' % (namespace, pkg_name)
response = requests.get(url, timeout=None)
data = response.json()
if 'error_type' in data and data['error_type'] == "invalid_token":
return []
if 'tags' not in data:
raise Exception("Unexpected response from quay.io - not tags description found [%s]" % data)
return [tag for tag in data['tags'] if tag != 'latest']
def mulled_tags_for(namespace, image):
"""Fetch remote tags available for supplied image name.
The result will be sorted so newest tags are first.
"""
tags = quay_versions(namespace, image)
tags = version_sorted(tags)
return tags
def split_tag(tag):
"""Split mulled image name into conda version and conda build."""
version = tag.split('--', 1)[0]
build = tag.split('--', 1)[1]
return version, build
def version_sorted(elements):
"""Sort iterable based on loose description of "version" from newest to oldest."""
return sorted(elements, key=LooseVersion, reverse=True)
Target = collections.namedtuple("Target", ["package_name", "version", "build"])
def build_target(package_name, version=None, build=None, tag=None):
"""Use supplied arguments to build a :class:`Target` object."""
if tag is not None:
assert version is None
assert build is None
version, build = split_tag(tag)
return Target(package_name, version, build)
def conda_build_target_str(target):
rval = target.package_name
if target.version:
rval += "=%s" % target.version
if target.build:
rval += "=%s" % target.build
return rval
def image_name(targets, image_build=None, name_override=None):
if name_override is not None:
print("WARNING: Overriding mulled image name, auto-detection of 'mulled' package attributes will fail to detect result.")
return name_override
targets = list(targets)
if len(targets) == 1:
target = targets[0]
suffix = ""
if target.version is not None:
if image_build is not None:
print("WARNING: Hard-coding image build instead of using Conda build - this is not recommended.")
suffix = image_build
else:
suffix += ":%s" % target.version
build = target.build
if build is not None:
suffix += "--%s" % build
return "%s%s" % (target.package_name, suffix)
else:
targets_order = sorted(targets, key=lambda t: t.package_name)
requirements_buffer = "\n".join(map(conda_build_target_str, targets_order))
m = hashlib.sha1()
m.update(requirements_buffer.encode())
suffix = "" if not image_build else ":%s" % image_build
return "mulled-v1-%s%s" % (m.hexdigest(), suffix)
__all__ = (
"build_target",
"conda_build_target_str",
"image_name",
"mulled_tags_for",
"quay_versions",
"split_tag",
"Target",
"version_sorted",
)
+12 -7
View File
@@ -1,3 +1,4 @@
"""The module defines the abstract interface for dealing tool dependency resolution plugins."""
from abc import (
ABCMeta,
abstractmethod,
@@ -10,6 +11,8 @@ from ..requirements import ToolRequirement
class DependencyResolver(Dictifiable, object):
"""Abstract description of a technique for resolving container images for tool execution."""
# Keys for dictification.
dict_collection_visible_keys = ['resolver_type', 'resolves_simple_dependencies']
# A "simple" dependency is one that does not depend on the the tool
@@ -23,13 +26,15 @@ class DependencyResolver(Dictifiable, object):
@abstractmethod
def resolve( self, name, version, type, **kwds ):
"""
Given inputs describing dependency in the abstract, yield tuple of
(script, bin, version). Here script is the env.sh file to source
before running a job, if that is not found the bin directory will be
appended to the path (if it is not None). Finally, version is the
resolved tool dependency version (which may differ from requested
version for instance if the request version is 'default'.)
"""Given inputs describing dependency in the abstract yield a Dependency object.
The Dependency object describes various attributes (script, bin,
version) used to build scripts with the dependency availble. Here
script is the env.sh file to source before running a job, if that is
not found the bin directory will be appended to the path (if it is
not ``None``). Finally, version is the resolved tool dependency
version (which may differ from requested version for instance if the
request version is 'default'.)
"""
def _get_config_option(self, key, dependency_resolver, default=None, config_prefix=None, **kwds):
@@ -5,7 +5,6 @@ via shed2tap (e.g. https://github.com/jmchilton/homebrew-toolshed).
"""
import logging
import os
from xml.etree import ElementTree as ET
from .resolver_mixins import (
@@ -147,4 +146,4 @@ def build_recipe_name(package_name, package_version, repository_owner, repositor
return base
__all__ = ['HomebrewToolShedDependencyResolver']
__all__ = ('HomebrewToolShedDependencyResolver', )
+5 -40
View File
@@ -6,10 +6,8 @@ incompatible changes coming.
import logging
import os
from galaxy.util.filelock import (
FileLock,
FileLockException
)
import galaxy.tools.deps.installable
from ..conda_util import (
build_isolated_environment,
cleanup_failed_install,
@@ -32,7 +30,7 @@ from ..resolvers import (
DEFAULT_BASE_PATH_DIRECTORY = "_conda"
DEFAULT_CONDARC_OVERRIDE = "_condarc"
DEFAULT_ENSURE_CHANNELS = "r,bioconda,iuc"
DEFAULT_ENSURE_CHANNELS = "conda-forge,r,bioconda,iuc"
log = logging.getLogger(__name__)
@@ -95,44 +93,11 @@ class CondaDependencyResolver(DependencyResolver, ListableDependencyResolver, In
copy_dependencies = _string_as_bool(get_option("copy_dependencies"))
self.auto_init = _string_as_bool(get_option("auto_init"))
self.conda_context = conda_context
self.ensure_conda_installed()
self.disabled = not galaxy.tools.deps.installable.ensure_installed(conda_context, install_conda, self.auto_init)
self.auto_install = auto_install
self.copy_dependencies = copy_dependencies
self.verbose_install_check = verbose_install_check
def ensure_conda_installed(self):
"""
Make sure that conda is installed, and if conda can't be installed, mark resolver as disabled.
We acquire a lock, so that multiple handlers do not attempt to install conda simultaneously.
"""
target_path = self.conda_prefix_parent
def _check():
if not self.conda_context.is_conda_installed():
if self.auto_init:
if self.conda_context.can_install_conda():
if install_conda(self.conda_context):
self.disabled = True
log.warning("Conda installation requested and failed.")
else:
self.disabled = True
else:
self.disabled = True
log.warning("Conda not installed and auto-installation disabled.")
else:
self.disabled = False
if not os.path.exists(target_path):
os.mkdir(target_path)
try:
if self.auto_init and os.access(target_path, os.W_OK):
with FileLock(os.path.join(target_path, 'conda')):
_check()
else:
_check()
except FileLockException:
self.ensure_conda_installed()
def resolve(self, name, version, type, **kwds):
# Check for conda just not being there, this way we can enable
# conda by default and just do nothing in not configured.
@@ -279,4 +244,4 @@ def _string_as_bool( value ):
return str( value ).lower() == "true"
__all__ = ['CondaDependencyResolver']
__all__ = ('CondaDependencyResolver', )
@@ -122,4 +122,8 @@ def _is_dependency_directory(directory):
return exists(join(directory, 'env.sh')) or exists(join(directory, 'bin'))
__all__ = ['GalaxyPackageDependencyResolver', 'GalaxyPackageDependency', 'ToolShedDependency']
__all__ = (
'GalaxyPackageDependency',
'GalaxyPackageDependencyResolver',
'ToolShedDependency'
)
+1 -1
View File
@@ -53,4 +53,4 @@ def _string_as_bool( value ):
return str( value ).lower() == "true"
__all__ = ['HomebrewDependencyResolver']
__all__ = ('HomebrewDependencyResolver', )
+1 -2
View File
@@ -7,7 +7,6 @@ it, hence support for it will be minimal. The Galaxy team eagerly welcomes
community contribution and maintenance however.
"""
import logging
from os import environ, pathsep
from os.path import exists, isdir, join
from subprocess import PIPE, Popen
@@ -180,4 +179,4 @@ class ModuleDependency(Dependency):
def _string_as_bool( value ):
return str( value ).lower() == "true"
__all__ = ['ModuleDependencyResolver']
__all__ = ('ModuleDependencyResolver', )
@@ -2,7 +2,6 @@ from os.path import abspath, exists, join
from .galaxy_packages import BaseGalaxyPackageDependencyResolver, ToolShedDependency
from .resolver_mixins import UsesInstalledRepositoriesMixin
from ..resolvers import NullDependency
@@ -64,4 +63,4 @@ class ToolShedPackageDependencyResolver(BaseGalaxyPackageDependencyResolver, Use
return NullDependency(version=None, name=name)
__all__ = ['ToolShedPackageDependencyResolver']
__all__ = ('ToolShedPackageDependencyResolver', )
@@ -19,7 +19,6 @@ See bottom for instructions on how to add this resolver.
"""
import logging
from os import listdir
from os.path import exists, getmtime, join
@@ -153,7 +152,7 @@ class CandidateDependency(Dependency):
return self.dependency.shell_commands( requirement )
__all__ = ['UnlinkedToolShedPackageDependencyResolver']
__all__ = ('UnlinkedToolShedPackageDependencyResolver', )
"""
At the time of writing July 3 2015 this resolver has to be plugged in.
+2 -2
View File
@@ -1,6 +1,6 @@
# We put a tool that references this package into the tool shed
# so we have to provide this legacy location for import indefinitely
# it seems.
from galaxy.util.ucsc import UCSCOutWrapper, UCSCLimitException
from galaxy.util.ucsc import UCSCLimitException, UCSCOutWrapper
__all__ = ['UCSCOutWrapper', 'UCSCLimitException']
__all__ = ('UCSCOutWrapper', 'UCSCLimitException')
+10 -9
View File
@@ -4,13 +4,15 @@ from various states, tracking results, and building implicit dataset
collections from matched collections.
"""
import collections
import logging
from threading import Thread
from six.moves.queue import Queue
from galaxy.tools.actions import on_text_for_names, ToolExecutionCache
from galaxy.tools.parser import ToolOutputCollectionPart
from galaxy.util import ExecutionTimer
from galaxy.tools.actions import on_text_for_names, ToolExecutionCache
from threading import Thread
from Queue import Queue
import logging
log = logging.getLogger( __name__ )
EXECUTION_SUCCESS_MESSAGE = "Tool [%s] created job [%s] %s"
@@ -135,7 +137,6 @@ class ToolExecutionTracker( object ):
return []
structure = self.collection_info.structure
collections = self.collection_info.collections.values()
# params is just one sample tool param execution with parallelized
# collection replaced with a specific dataset. Need to replace this
@@ -143,13 +144,13 @@ class ToolExecutionTracker( object ):
# label.
params.update( self.collection_info.collections ) # Replace datasets with source collections for labelling outputs.
collection_names = map( lambda c: "collection %d" % c.hid, collections )
collection_names = ["collection %d" % c.hid for c in self.collection_info.collections.values()]
on_text = on_text_for_names( collection_names )
collections = {}
implicit_inputs = list(self.collection_info.collections.iteritems())
for output_name, outputs in self.outputs_by_output_name.iteritems():
implicit_inputs = list(self.collection_info.collections.items())
for output_name, outputs in self.outputs_by_output_name.items():
if not len( structure ) == len( outputs ):
# Output does not have the same structure, if all jobs were
# successfully submitted this shouldn't have happened.
@@ -202,4 +203,4 @@ class ToolExecutionTracker( object ):
trans.sa_session.flush()
self.implicit_collections = collections
__all__ = [ execute ]
__all__ = ( 'execute', )
+1 -1
View File
@@ -7,4 +7,4 @@ from galaxy.util.xml_macros import (
load_tool = load
__all__ = ["load_tool", "raw_tool_xml_tree", "imported_macro_paths", "template_macro_params"]
__all__ = ("load_tool", "raw_tool_xml_tree", "imported_macro_paths", "template_macro_params")
+4 -4
View File
@@ -241,11 +241,11 @@ def _find_tool_files(path, recursive, enable_beta_formats):
files = glob.glob(path + "/*.xml")
else:
files = _find_files(path, "*.xml")
return map(os.path.abspath, files)
return [os.path.abspath(_) for _ in files]
def _has_extension(path, extensions):
return any(map(lambda e: path.endswith(e), extensions))
return any(path.endswith(e) for e in extensions)
def _find_files(directory, pattern='*'):
@@ -266,7 +266,7 @@ BETA_TOOL_CHECKERS = {
'cwl': looks_like_a_tool_cwl,
}
__all__ = [
__all__ = (
"find_possible_tools_from_path",
"is_a_yaml_with_class",
"is_tool_load_error",
@@ -276,4 +276,4 @@ __all__ = [
"looks_like_a_tool_cwl",
"looks_like_a_tool_xml",
"looks_like_a_tool_yaml",
]
)
+9 -8
View File
@@ -4,15 +4,16 @@ Classes encapsulating Galaxy tool parameters.
import re
from json import dumps, loads
from basic import RuntimeValue, DataCollectionToolParameter, DataToolParameter, SelectToolParameter
from grouping import Conditional, Repeat, Section, UploadDataset
from galaxy.util.expressions import ExpressionContext
from galaxy.util.json import json_fix
from .basic import DataCollectionToolParameter, DataToolParameter, RuntimeValue, SelectToolParameter
from .grouping import Conditional, Repeat, Section, UploadDataset
REPLACE_ON_TRUTHY = object()
# Some tools use the code tag and access the code base, expecting certain tool parameters to be available here.
__all__ = [ DataCollectionToolParameter, DataToolParameter, SelectToolParameter ]
__all__ = ( 'DataCollectionToolParameter', 'DataToolParameter', 'SelectToolParameter' )
def visit_input_values( inputs, input_values, callback, name_prefix='', label_prefix='', parent_prefix='', context=None, no_replacement_value=REPLACE_ON_TRUTHY ):
@@ -82,7 +83,7 @@ def visit_input_values( inputs, input_values, callback, name_prefix='', label_pr
context = ExpressionContext( input_values, context )
payload = { 'context': context, 'no_replacement_value': no_replacement_value }
for input in inputs.itervalues():
for input in inputs.values():
if isinstance( input, Repeat ) or isinstance( input, UploadDataset ):
values = input_values[ input.name ] = input_values.get( input.name, [] )
for i, d in enumerate( values ):
@@ -143,7 +144,7 @@ def params_to_strings( params, param_values, app ):
such).
"""
rval = dict()
for key, value in param_values.iteritems():
for key, value in param_values.items():
if key in params:
value = params[ key ].value_to_basic( value, app )
rval[ key ] = str( dumps( value ) )
@@ -158,7 +159,7 @@ def params_from_strings( params, param_values, app, ignore_errors=False ):
preferred form).
"""
rval = dict()
for key, value in param_values.iteritems():
for key, value in param_values.items():
value = json_fix( loads( value ) )
if key in params:
value = params[ key ].value_from_basic( value, app, ignore_errors )
@@ -173,7 +174,7 @@ def params_to_incoming( incoming, inputs, input_values, app, name_prefix="" ):
Useful for e.g. the rerun function.
"""
for input in inputs.itervalues():
for input in inputs.values():
if isinstance( input, Repeat ) or isinstance( input, UploadDataset ):
for d in input_values[ input.name ]:
index = d[ '__index__' ]
@@ -201,7 +202,7 @@ def update_param( prefixed_name, input_values, new_value ):
"""
for key in input_values:
match = re.match( '^' + key + '_(\d+)\|(.+)', prefixed_name )
if match:
if match and not key.endswith( "|__identifier__" ):
index = int( match.group( 1 ) )
if isinstance( input_values[ key ], list ) and len( input_values[ key ] ) > index:
update_param( match.group( 2 ), input_values[ key ][ index ], new_value )
+1 -1
View File
@@ -151,7 +151,7 @@ class ToolParameter( object, Dictifiable ):
Convert a value to a text representation suitable for displaying to
the user
"""
if value:
if value is not None:
return unicodify( value )
return "Not available."
@@ -1,6 +1,7 @@
from logging import getLogger
import galaxy.model
from logging import getLogger
log = getLogger( __name__ )
ROLES_UNSET = object()
@@ -179,4 +180,4 @@ class DatasetCollectionMatcher( object ):
break
return valid
__all__ = [ DatasetMatcher, DatasetCollectionMatcher ]
__all__ = ( 'DatasetMatcher', 'DatasetCollectionMatcher' )
+12 -11
View File
@@ -1,20 +1,21 @@
from galaxy.tools.parameters.basic import (
DataToolParameter,
DataCollectionToolParameter,
DataToolParameter,
SelectToolParameter
)
from galaxy.tools.wrappers import (
InputValueWrapper,
SelectToolParameterWrapper,
DatasetFilenameWrapper,
DatasetListWrapper,
DatasetCollectionWrapper
)
from galaxy.tools.parameters.grouping import (
Repeat,
Conditional,
Repeat,
Section
)
from galaxy.tools.wrappers import (
DatasetCollectionWrapper,
DatasetFilenameWrapper,
DatasetListWrapper,
InputValueWrapper,
SelectToolParameterWrapper
)
PARAMS_UNWRAPPED = object()
@@ -40,7 +41,7 @@ class WrappedParameters( object ):
incoming = self.incoming
# Wrap tool inputs as necessary
for input in inputs.itervalues():
for input in inputs.values():
if input.name not in input_values and skip_missing_values:
continue
value = input_values[ input.name ]
@@ -112,4 +113,4 @@ def make_list_copy( from_list ):
return new_list
__all__ = [ 'WrappedParameters', 'make_dict_copy' ]
__all__ = ( 'WrappedParameters', 'make_dict_copy' )
+4 -4
View File
@@ -1,6 +1,6 @@
import logging
log = logging.getLogger(__name__)
log = logging.getLogger(__name__)
SKIP_INPUT = object()
@@ -9,7 +9,7 @@ def json_wrap(inputs, input_values, as_dict=None, handle_files="SKIP"):
if as_dict is None:
as_dict = {}
for input in inputs.itervalues():
for input in inputs.values():
input_name = input.name
value = input_values[input_name]
json_value = _json_wrap_input(input, value, handle_files=handle_files)
@@ -65,7 +65,7 @@ def _json_wrap_input(input, value, handle_files="SKIP"):
json_value = _cast_if_not_none(value, bool)
elif input_type == "data_column":
# value is a SelectToolParameterWrapper()
json_value = map(int, _cast_if_not_none(value.value, list))
json_value = [int(_) for _ in _cast_if_not_none(value.value, list)]
else:
raise NotImplementedError("input_type [%s] not implemented" % input_type)
@@ -80,4 +80,4 @@ def _cast_if_not_none(value, cast_to, empty_to_none=False):
return cast_to(value)
__all__ = ['json_wrap']
__all__ = ('json_wrap', )
+3 -4
View File
@@ -1,15 +1,14 @@
""" Package responsible for parsing tools from files/abstract tool sources.
"""
from .factory import get_input_source
from .factory import get_tool_source
from .factory import get_input_source, get_tool_source
from .interface import ToolSource
from .output_objects import (
ToolOutputCollectionPart,
)
__all__ = [
__all__ = (
"get_input_source",
"get_tool_source",
"ToolOutputCollectionPart",
"ToolSource",
]
)
+2 -3
View File
@@ -10,8 +10,7 @@ from galaxy.util.odict import odict
from .cwl import CwlToolSource
from .interface import InputSource
from .xml import XmlInputSource
from .xml import XmlToolSource
from .xml import XmlInputSource, XmlToolSource
from .yaml import YamlToolSource
log = logging.getLogger(__name__)
@@ -72,4 +71,4 @@ def get_input_source(content):
return content
__all__ = ["get_tool_source", "get_input_source"]
__all__ = ("get_tool_source", "get_input_source")
+4 -7
View File
@@ -1,16 +1,13 @@
"""API for this module containing functionality related to the toolbox."""
from .base import AbstractToolBox
from .base import BaseGalaxyToolBox
from .base import AbstractToolBox, BaseGalaxyToolBox
from .panel import panel_item_types
from .panel import ToolSection
from .panel import ToolSectionLabel
from .panel import panel_item_types, ToolSection, ToolSectionLabel
__all__ = [
__all__ = (
"AbstractToolBox",
"BaseGalaxyToolBox",
"panel_item_types",
"ToolSection",
"ToolSectionLabel",
]
)
+8 -3
View File
@@ -520,15 +520,20 @@ class AbstractToolBox( Dictifiable, ManagesIntegratedToolPanelMixin, object ):
tool = self.load_tool_from_cache(os.path.join(tool_path, path))
from_cache = tool
if from_cache:
log.debug("Loading tool %s from cache", str(tool.id))
elif guid: # tool was not in cache and is a tool shed tool
if guid and tool.id != guid:
# In rare cases a tool shed tool is loaded into the cache without guid.
# In that case recreating the tool will correct the cached version.
from_cache = False
else:
log.debug("Loading tool %s from cache", str(tool.id))
if guid and not from_cache: # tool was not in cache and is a tool shed tool
tool_shed_repository = self.get_tool_repository_from_xml_item(item, path)
if tool_shed_repository:
# Only load tools if the repository is not deactivated or uninstalled.
can_load_into_panel_dict = not tool_shed_repository.deleted
repository_id = self.app.security.encode_id(tool_shed_repository.id)
tool = self.load_tool(os.path.join( tool_path, path ), guid=guid, repository_id=repository_id, use_cached=False)
else: # tool was not in cache and is not a tool shed tool.
if not tool: # tool was not in cache and is not a tool shed tool.
tool = self.load_tool(os.path.join(tool_path, path), use_cached=False)
if string_as_bool(item.get( 'hidden', False )):
tool.hidden = True
@@ -3,4 +3,4 @@ from .interface import ToolLineage
from .tool_shed import ToolVersionCache
__all__ = ["LineageMap", "ToolLineage", "ToolVersionCache"]
__all__ = ("LineageMap", "ToolLineage", "ToolVersionCache")
+1 -1
View File
@@ -46,4 +46,4 @@ class LineageMap(object):
return self.lineage_map.get(versionless_tool_id, None)
__all__ = ["LineageMap"]
__all__ = ("LineageMap", )
@@ -1,5 +1,4 @@
from .interface import ToolLineage
from .interface import ToolLineageVersion
from .interface import ToolLineage, ToolLineageVersion
try:
from galaxy.model.tool_shed_install import ToolVersion
@@ -78,13 +77,13 @@ class ToolShedLineage(ToolLineage):
return result
def get_versions( self, reverse=False ):
return map( ToolLineageVersion.from_guid, self.get_version_ids( reverse=reverse ) )
return [ ToolLineageVersion.from_guid(_) for _ in self.get_version_ids( reverse=reverse ) ]
def to_dict(self):
tool_shed_repository = self._tool_shed_repository
rval = dict(
tool_version_id=self.tool_version_id,
tool_versions=map(lambda v: v.to_dict(), self.get_versions()),
tool_versions=[v.to_dict() for v in self.get_versions()],
tool_shed_repository=tool_shed_repository if tool_shed_repository is not None else None,
lineage_type='tool_shed',
)
@@ -94,4 +93,4 @@ class ToolShedLineage(ToolLineage):
def get_installed_tool_version( app, tool_id ):
return app.tool_version_cache.tool_version_by_tool_id.get(tool_id, None)
__all__ = [ "ToolShedLineage" ]
__all__ = ( "ToolShedLineage", )
+10 -11
View File
@@ -3,9 +3,9 @@
These files define tool lists, sections, labels, etc... the elements of the
Galaxy tool panel.
"""
from abc import ABCMeta
from abc import abstractmethod
from abc import ABCMeta, abstractmethod
import six
import yaml
from galaxy.util import parse_xml, string_as_bool
@@ -13,11 +13,10 @@ from galaxy.util import parse_xml, string_as_bool
DEFAULT_MONITOR = False
@six.add_metaclass(ABCMeta)
class ToolConfSource(object):
"""Interface represents a container of tool references."""
__metaclass__ = ABCMeta
@abstractmethod
def parse_items(self):
"""Return a list of ToolConfItem describing source."""
@@ -45,7 +44,7 @@ class XmlToolConfSource(ToolConfSource):
return self.root.get('tool_path')
def parse_items(self):
return map(ensure_tool_conf_item, self.root.getchildren())
return [ensure_tool_conf_item(_) for _ in self.root.getchildren()]
def is_shed_tool_conf(self):
has_tool_path = self.parse_tool_path() is not None
@@ -67,7 +66,7 @@ class YamlToolConfSource(ToolConfSource):
return self.as_dict.get('tool_path')
def parse_items(self):
return map(ToolConfItem.from_dict, self.as_dict.get('items'))
return [ToolConfItem.from_dict(_) for _ in self.as_dict.get('items')]
def parse_monitor(self):
return self.as_dict.get('monitor', DEFAULT_MONITOR)
@@ -94,7 +93,7 @@ class ToolConfItem(object):
del as_dict['type']
attributes = as_dict
if type == 'section':
items = map(cls.from_dict, as_dict['items'])
items = [cls.from_dict(_) for _ in as_dict['items']]
del as_dict['items']
item = ToolConfSection(attributes, items)
else:
@@ -141,18 +140,18 @@ def ensure_tool_conf_item(xml_or_item):
if type != "section":
return ToolConfItem(type, attributes, elem)
else:
items = map(ensure_tool_conf_item, elem.getchildren())
items = [ensure_tool_conf_item(_) for _ in elem.getchildren()]
return ToolConfSection(attributes, items, elem=elem)
def get_toolbox_parser(config_filename):
is_yaml = any(map(lambda e: config_filename.endswith(e), [".yml", ".yaml", ".json"]))
is_yaml = any(config_filename.endswith(e) for e in [".yml", ".yaml", ".json"])
if is_yaml:
return YamlToolConfSource(config_filename)
else:
return XmlToolConfSource(config_filename)
__all__ = [
__all__ = (
"get_toolbox_parser",
"ensure_tool_conf_item",
]
)
+28 -1
View File
@@ -1990,6 +1990,33 @@ multiple formats.
help="The set of aligned reads." />
```
Perhaps counter-intuitively, a ``multiple="true"`` data parameter requires at least one
data input. If ``optional="true"`` is specified, this condition is relaxed and the user
is allowed to select 0 datasets. Unfortunately, if 0 datasets are selected the resulting
value for the parameter during Cheetah templating (such as in a ``command`` block) will
effectively be a list with one ``None``-like entity in it.
The following idiom can be used to iterate over such a list and build a hypothetical ``-B``
parameter for each file - the ``if`` block is used to handle the case where a ``None``-like
entity appears in the list because no files were selected:
```
#for $input in $input1
#if $input
-B "$input"
#end if
#end for
```
Some example tools using ``multiple="true"`` data parameters include:
- [multi_data_param.xml](https://github.com/galaxyproject/galaxy/blob/dev/test/functional/tools/multi_data_param.xml)
- [multi_data_optional.xml](https://github.com/galaxyproject/galaxy/blob/dev/test/functional/tools/multi_data_optional.xml)
Additionally, a detailed discussion of handling multiple homogenous files can be found in the
the [Planemo Documentation](http://planemo.readthedocs.io/en/latest/writing_advanced.html#consuming-collections)
on this topic.
$attribute_list:format,multiple
#### ``select``
@@ -3661,7 +3688,7 @@ conditionals are accessed using a hash named after the conditional.
</inputs>
<outputs>
<data format="txt" label="Alignment report on ${on_string}" name="output_txt" />
<data format="vcf" label="Variant summary on ${on_string}" name="output_vcf>
<data format="vcf" label="Variant summary on ${on_string}" name="output_vcf">
<filter>options['selection_mode'] == 'advanced' and options['vcf_output']</filter>
</data>
</outputs>
+8 -24
View File
@@ -1,38 +1,15 @@
import bz2
import gzip
import imghdr
import re
import zipfile
from six import StringIO
from galaxy import util
from galaxy.util.image_util import image_type
HTML_CHECK_LINES = 100
try:
import Image as PIL
except ImportError:
try:
from PIL import Image as PIL
except:
PIL = None
def check_image( file_path ):
if PIL is not None:
try:
im = PIL.open( file_path )
except:
return False
if im:
return im
return False
else:
if imghdr.what( file_path ) is not None:
return True
return False
def check_html( file_path, chunk=None ):
if chunk is None:
@@ -143,6 +120,13 @@ def is_gzip( file_path ):
return is_gzipped
def check_image( file_path ):
""" Simple wrapper around image_type to yield a True/False verdict """
if image_type( file_path ):
return True
return False
__all__ = [
'check_binary',
'check_bz2',
+75
View File
@@ -0,0 +1,75 @@
"""Provides utilities for working with image files."""
import imghdr
import logging
try:
import Image as PIL
except ImportError:
try:
from PIL import Image as PIL
except:
PIL = None
log = logging.getLogger(__name__)
def image_type( filename ):
fmt = None
if PIL is not None:
try:
im = PIL.open( filename )
fmt = im.format
im.close()
except:
# We continue to try with imghdr, so this is a rare case of an
# exception we expect to happen frequently, so we're not logging
pass
if not fmt:
fmt = imghdr.what( filename )
if fmt:
return fmt.upper()
else:
return False
def check_image_type( filename, types ):
fmt = image_type( filename )
if fmt in types:
return True
return False
def get_image_ext( file_path ):
# determine ext
fmt = image_type( file_path )
if fmt in [ 'JPG', 'JPEG' ]:
return 'jpg'
if fmt == 'PNG':
return 'png'
if fmt == 'TIFF':
return 'tiff'
if fmt == 'BMP':
return 'bmp'
if fmt == 'GIF':
return 'gif'
if fmt == 'IM':
return 'im'
if fmt == 'PCD':
return 'pcd'
if fmt == 'PCX':
return 'pcx'
if fmt == 'PPM':
return 'ppm'
if fmt == 'PSD':
return 'psd'
if fmt == 'XBM':
return 'xbm'
if fmt == 'XPM':
return 'xpm'
if fmt == 'RGB':
return 'rgb'
if fmt == 'PBM':
return 'pbm'
if fmt == 'PGM':
return 'pgm'
return None
+51 -49
View File
@@ -1,25 +1,26 @@
""" This module contains functionality to aid in extracting workflows from
histories.
"""
from galaxy.util.odict import odict
from galaxy import exceptions
from galaxy import model
import logging
from galaxy import exceptions, model
from galaxy.tools.parameters.basic import (
DataToolParameter,
DataCollectionToolParameter
DataCollectionToolParameter,
DataToolParameter
)
from galaxy.tools.parser import ToolOutputCollectionPart
from galaxy.tools.parameters.grouping import (
Conditional,
Repeat,
Section
)
from galaxy.tools.parser import ToolOutputCollectionPart
from galaxy.util.odict import odict
from .steps import (
attach_ordered_steps,
order_workflow_steps_with_levels
)
import logging
log = logging.getLogger( __name__ )
WARNING_SOME_DATASETS_NOT_READY = "Some datasets still queued or running were ignored"
@@ -67,16 +68,14 @@ def extract_steps( trans, history=None, job_ids=None, dataset_ids=None, dataset_
elif type( dataset_collection_ids) is not list:
dataset_collection_ids = [ dataset_collection_ids ]
# Convert both sets of ids to integers
job_ids = [ int( id ) for id in job_ids ]
dataset_ids = [ int( id ) for id in dataset_ids ]
dataset_collection_ids = [ int( id ) for id in dataset_collection_ids ]
# Find each job, for security we (implicately) check that they are
# associated witha job in the current history.
job_ids = [ int( _ ) for _ in job_ids ]
dataset_ids = [ int( _ ) for _ in dataset_ids ]
dataset_collection_ids = [ int( _ ) for _ in dataset_collection_ids ]
# Find each job, for security we (implicitly) check that they are
# associated with a job in the current history.
summary = WorkflowSummary( trans, history )
jobs = summary.jobs
jobs_by_id = dict( ( job.id, job ) for job in jobs.keys() )
steps = []
steps_by_job_id = {}
hid_to_output_pair = {}
# Input dataset steps
for i, hid in enumerate( dataset_ids ):
@@ -104,10 +103,10 @@ def extract_steps( trans, history=None, job_ids=None, dataset_ids=None, dataset_
steps.append( step )
# Tool steps
for job_id in job_ids:
if job_id not in jobs_by_id:
log.warning( "job_id %s not found in jobs_by_id %s" % ( job_id, jobs_by_id ) )
if job_id not in summary.job_id2representative_job:
log.warning( "job_id %s not found in job_id2representative_job %s" % ( job_id, summary.job_id2representative_job ) )
raise AssertionError( "Attempt to create workflow with job not connected to current history" )
job = jobs_by_id[ job_id ]
job = summary.job_id2representative_job[job_id]
tool_inputs, associations = step_inputs( trans, job )
step = model.WorkflowStep()
step.type = 'tool'
@@ -134,7 +133,6 @@ def extract_steps( trans, history=None, job_ids=None, dataset_ids=None, dataset_
conn.output_step = other_step
conn.output_name = other_name
steps.append( step )
steps_by_job_id[ job_id ] = step
# Store created dataset hids
for assoc in (job.output_datasets + job.output_dataset_collection_instances):
assoc_name = assoc.name
@@ -147,8 +145,8 @@ def extract_steps( trans, history=None, job_ids=None, dataset_ids=None, dataset_
if query_assoc_name == assoc_name:
hid = dataset_collection.hid
if hid is None:
template = "Failed to find matching implicit job - job is %s, jobs are %s, assoc_name is %s."
message = template % ( job.id, jobs, assoc.name )
template = "Failed to find matching implicit job - job id is %s, implicit pairs are %s, assoc_name is %s."
message = template % ( job.id, jobs[job], assoc_name )
log.warning( message )
raise Exception( "Failed to extract job." )
else:
@@ -202,6 +200,7 @@ class WorkflowSummary( object ):
self.history = history
self.warnings = set()
self.jobs = odict()
self.job_id2representative_job = {} # map a non-fake job id to its representative job
self.implicit_map_jobs = []
self.collection_types = {}
@@ -223,25 +222,27 @@ class WorkflowSummary( object ):
else:
self.__summarize_dataset( content )
def __summarize_dataset_collection( self, content ):
content = self.__original_hdca( content )
dataset_collection = content
hid = content.hid
self.collection_types[ hid ] = content.collection.collection_type
cja = content.creating_job_associations
def __summarize_dataset_collection( self, dataset_collection ):
dataset_collection = self.__original_hdca( dataset_collection )
hid = dataset_collection.hid
self.collection_types[ hid ] = dataset_collection.collection.collection_type
cja = dataset_collection.creating_job_associations
if cja:
# Use the first job to represent all mapped jobs.
representive_job_assoc = content.creating_job_associations[0]
job = representive_job_assoc.job
if job not in self.jobs or self.jobs[ job ][ 0 ][ 1 ].history_content_type == "dataset":
self.jobs[ job ] = [ ( representive_job_assoc.name, dataset_collection ) ]
if content.implicit_output_name:
self.implicit_map_jobs.append( job )
# Use the "first" job to represent all mapped jobs.
representative_assoc = cja[0]
representative_job = representative_assoc.job
if representative_job not in self.jobs or self.jobs[ representative_job ][ 0 ][ 1 ].history_content_type == "dataset":
self.jobs[ representative_job ] = [ ( representative_assoc.name, dataset_collection ) ]
if dataset_collection.implicit_output_name:
self.implicit_map_jobs.append( representative_job )
else:
self.jobs[ job ].append( ( representive_job_assoc.name, dataset_collection ) )
self.jobs[ representative_job ].append( ( representative_assoc.name, dataset_collection ) )
for assoc in cja:
job = assoc.job
self.job_id2representative_job[job.id] = representative_job
# This whole elif condition may no longer be needed do to additional
# tracking with creating_job_associations. Will delete at some point.
elif content.implicit_output_name:
elif dataset_collection.implicit_output_name:
# TODO: Optimize db call
dataset_instance = dataset_collection.collection.dataset_instances[ 0 ]
if not self.__check_state( dataset_instance ):
@@ -249,38 +250,40 @@ class WorkflowSummary( object ):
# makes me wonder if even need this check at all?
return
job_hda = self.__original_hda( dataset_instance )
if not job_hda.creating_job_associations:
original_hda = self.__original_hda( dataset_instance )
if not original_hda.creating_job_associations:
log.warning( "An implicitly create output dataset collection doesn't have a creating_job_association, should not happen!" )
job = DatasetCollectionCreationJob( dataset_collection )
self.jobs[ job ] = [ ( None, dataset_collection ) ]
for assoc in job_hda.creating_job_associations:
for assoc in original_hda.creating_job_associations:
job = assoc.job
if job not in self.jobs or self.jobs[ job ][ 0 ][ 1 ].history_content_type == "dataset":
self.jobs[ job ] = [ ( assoc.name, dataset_collection ) ]
self.job_id2representative_job[job.id] = job
self.implicit_map_jobs.append( job )
else:
self.jobs[ job ].append( ( assoc.name, dataset_collection ) )
else:
job = DatasetCollectionCreationJob( content )
self.jobs[ job ] = [ ( None, content ) ]
job = DatasetCollectionCreationJob( dataset_collection )
self.jobs[ job ] = [ ( None, dataset_collection ) ]
def __summarize_dataset( self, dataset ):
if not self.__check_state( dataset ):
return
job_hda = self.__original_hda( dataset )
original_hda = self.__original_hda( dataset )
if not job_hda.creating_job_associations:
if not original_hda.creating_job_associations:
self.jobs[ FakeJob( dataset ) ] = [ ( None, dataset ) ]
for assoc in job_hda.creating_job_associations:
for assoc in original_hda.creating_job_associations:
job = assoc.job
if job in self.jobs:
self.jobs[ job ].append( ( assoc.name, dataset ) )
else:
self.jobs[ job ] = [ ( assoc.name, dataset ) ]
self.job_id2representative_job[job.id] = job
def __original_hdca( self, hdca ):
while hdca.copied_from_history_dataset_collection_association:
@@ -288,11 +291,10 @@ class WorkflowSummary( object ):
return hdca
def __original_hda( self, hda ):
# if this hda was copied from another, we need to find the job that created the origial hda
job_hda = hda
while job_hda.copied_from_history_dataset_association:
job_hda = job_hda.copied_from_history_dataset_association
return job_hda
# if this hda was copied from another, we need to find the job that created the original hda
while hda.copied_from_history_dataset_association:
hda = hda.copied_from_history_dataset_association
return hda
def __check_state( self, hda ):
# FIXME: Create "Dataset.is_finished"
@@ -369,4 +371,4 @@ def __cleanup_param_values( inputs, values ):
cleanup( "", inputs, values )
return associations
__all__ = [ summarize, extract_workflow ]
__all__ = ( 'summarize', 'extract_workflow' )
+7 -9
View File
@@ -1,15 +1,13 @@
import logging
import uuid
from galaxy import model
from galaxy import util
from galaxy.util.odict import odict
from galaxy import model, util
from galaxy.util import ExecutionTimer
from galaxy.util.odict import odict
from galaxy.workflow import modules
from galaxy.workflow.run_request import WorkflowRunConfig
from galaxy.workflow.run_request import workflow_run_config_to_request
from galaxy.workflow.run_request import (workflow_run_config_to_request,
WorkflowRunConfig)
import logging
log = logging.getLogger( __name__ )
@@ -253,7 +251,7 @@ class WorkflowProgress( object ):
self.module_injector.inject( step )
if step_id not in step_states:
template = "Workflow invocation [%s] has no step state for step id [%s]. States ids are %s."
message = template % (self.workflow_invocation.id, step_id, step_states.keys())
message = template % (self.workflow_invocation.id, step_id, list(step_states.keys()))
raise Exception(message)
runtime_state = step_states[ step_id ].value
step.state = step.module.recover_runtime_state( runtime_state )
@@ -399,4 +397,4 @@ class WorkflowProgress( object ):
except modules.DelayedWorkflowEvaluation:
self.mark_step_outputs_delayed( step )
__all__ = [ invoke, WorkflowRunConfig ]
__all__ = ( 'invoke', 'WorkflowRunConfig' )
+4 -5
View File
@@ -1,14 +1,13 @@
""" The class defines the stock Galaxy workflow scheduling plugin - currently
it simply schedules the whole workflow up front when offered.
"""
from ..schedulers import ActiveWorkflowSchedulingPlugin
import logging
from galaxy.work import context
from galaxy.workflow import run, run_request
from galaxy.workflow import run
from galaxy.workflow import run_request
from ..schedulers import ActiveWorkflowSchedulingPlugin
import logging
log = logging.getLogger( __name__ )
@@ -43,4 +42,4 @@ class CoreWorkflowSchedulingPlugin( ActiveWorkflowSchedulingPlugin ):
workflow_invocation=workflow_invocation,
)
__all__ = [ 'CoreWorkflowSchedulingPlugin' ]
__all__ = ( 'CoreWorkflowSchedulingPlugin', )
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+5 -4
View File
@@ -1,10 +1,11 @@
from base import api_asserts
import json
import time
from operator import itemgetter
import time
import json
from six import StringIO
from pkg_resources import resource_string
from six import StringIO
from base import api_asserts
# Simple workflow that takes an input and call cat wrapper on it.
workflow_str = resource_string( __name__, "test_workflow_1.ga" )
+3 -1
View File
@@ -1,7 +1,9 @@
import json
import pprint
import logging
import pprint
from requests import post
from base import api
log = logging.getLogger( "functional_tests.py" )
+1 -1
View File
@@ -1,8 +1,8 @@
import base64
from requests import get
from base import api
from base import api
TEST_USER_EMAIL = "auth_user_test@bx.psu.edu"
TEST_USER_PASSWORD = "testpassword1"
+4 -3
View File
@@ -1,7 +1,8 @@
from base import api
import json
from .helpers import DatasetPopulator
from .helpers import DatasetCollectionPopulator
from base import api
from .helpers import DatasetCollectionPopulator, DatasetPopulator
class DatasetCollectionApiTestCase( api.ApiTestCase ):
+5 -6
View File
@@ -1,11 +1,10 @@
from base import api
import json
from .helpers import TestsDatasets
from .helpers import LibraryPopulator
from .helpers import DatasetCollectionPopulator
from requests import delete
from requests import put
from requests import delete, put
from base import api
from .helpers import DatasetCollectionPopulator, LibraryPopulator, TestsDatasets
# TODO: Test anonymous access.
+2 -1
View File
@@ -1,5 +1,6 @@
from galaxy.exceptions import error_codes
from api.test_pages import BasePageApiTestCase
from .test_pages import BasePageApiTestCase
class PageRevisionsApiTestCase( BasePageApiTestCase ):
+3 -4
View File
@@ -1,8 +1,7 @@
from galaxy.exceptions import error_codes
from base import api
from requests import delete
from operator import itemgetter
from base import api
from galaxy.exceptions import error_codes
class BasePageApiTestCase( api.ApiTestCase ):
@@ -102,4 +101,4 @@ class PageApiTestCase( BasePageApiTestCase ):
index_response = self._get( "pages" )
self._assert_status_code_is( index_response, 200 )
pages = index_response.json()
return id in map( itemgetter( "id" ), pages )
return id in ( _["id"] for _ in pages )
+3 -1
View File
@@ -1,9 +1,11 @@
""" Tests for the tool data API.
"""
from __future__ import print_function
from base import api
import operator
from base import api
class ToolDataApiTestCase( api.ApiTestCase ):
+10 -11
View File
@@ -1,13 +1,12 @@
# Test tools API.
from base import api
import json
from operator import itemgetter
from .helpers import DatasetPopulator
from .helpers import DatasetCollectionPopulator
from .helpers import LibraryPopulator
from .helpers import skip_without_tool
from base import api
from galaxy.tools.verify.test_data import TestDataResolver
from .helpers import (DatasetCollectionPopulator, DatasetPopulator,
LibraryPopulator, skip_without_tool)
class ToolsTestCase( api.ApiTestCase ):
@@ -25,7 +24,7 @@ class ToolsTestCase( api.ApiTestCase ):
tools_index = index.json()
# No need to flatten out sections, with in_panel=False, only tools are
# returned.
tool_ids = map( itemgetter( "id" ), tools_index )
tool_ids = [_["id"] for _ in tools_index]
assert "upload1" in tool_ids
@skip_without_tool( "cat1" )
@@ -195,7 +194,7 @@ class ToolsTestCase( api.ApiTestCase ):
def get_state(dce):
return dce["object"]["state"]
mixed_states = map(get_state, mixed_hdca["elements"])
mixed_states = [get_state(_) for _ in mixed_hdca["elements"]]
assert mixed_states == [u"ok", u"error", u"ok", u"error"], mixed_states
inputs = {
"input": { "src": "hdca", "id": mixed_hdca["id"] },
@@ -206,7 +205,7 @@ class ToolsTestCase( api.ApiTestCase ):
self.assertEquals( len(filter_output_collections), 1 )
filtered_hid = filter_output_collections[0]["hid"]
filtered_hdca = self.dataset_populator.get_history_collection_details(history_id, hid=filtered_hid, wait=False)
filtered_states = map(get_state, filtered_hdca["elements"])
filtered_states = [get_state(_) for _ in filtered_hdca["elements"]]
assert filtered_states == [u"ok", u"ok"], filtered_states
@skip_without_tool( "multi_select" )
@@ -525,7 +524,7 @@ class ToolsTestCase( api.ApiTestCase ):
# Assert we have three outputs with 1, 2, and 3 lines respectively.
assert len( outputs ) == 3
outputs_contents = [ self.dataset_populator.get_history_dataset_content( history_id, dataset=o ).strip() for o in outputs ]
assert sorted( map( lambda c: len( c.split( "\n" ) ), outputs_contents ) ) == [ 1, 2, 3 ]
assert sorted( len( c.split( "\n" ) ) for c in outputs_contents ) == [ 1, 2, 3 ]
@skip_without_tool( "cat1" )
def test_multirun_in_repeat( self ):
@@ -1230,7 +1229,7 @@ class ToolsTestCase( api.ApiTestCase ):
else:
tools.append( tool_or_section )
tool_ids = map( itemgetter( "id" ), tools )
tool_ids = [_["id"] for _ in tools]
return tool_ids
def __build_nested_list( self, history_id ):
+2
View File
@@ -1,5 +1,7 @@
import json
from requests import put
from base import api
TEST_USER_EMAIL = "user_for_users_index_test@bx.psu.edu"
+5 -4
View File
@@ -1,8 +1,9 @@
from __future__ import print_function
from collections import namedtuple
import functools
from json import dumps, loads
import operator
from collections import namedtuple
from json import dumps, loads
from .helpers import skip_without_tool
from .test_workflows import BaseWorkflowsApiTestCase
@@ -215,7 +216,7 @@ test_data:
text_input2: "samp1\t30.0\nsamp2\t40.0\n"
""")
tool_ids = [ "cat1", "collection_split_on_column", "cat_list" ]
job_ids = list(map( functools.partial(self._job_id_for_tool, jobs_summary.jobs ), tool_ids ))
job_ids = [ functools.partial(self._job_id_for_tool, jobs_summary.jobs )(_) for _ in tool_ids ]
downloaded_workflow = self._extract_and_download_workflow(
dataset_ids=[ "1", "2" ],
job_ids=job_ids,
@@ -265,7 +266,7 @@ test_data:
content: "samp1\t30.0\nsamp2\t40.0\n"
""")
tool_ids = [ "cat1", "collection_creates_pair", "cat_collection", "cat_list" ]
job_ids = list(map( functools.partial(self._job_id_for_tool, jobs_summary.jobs ), tool_ids ))
job_ids = [ functools.partial(self._job_id_for_tool, jobs_summary.jobs )(_) for _ in tool_ids ]
downloaded_workflow = self._extract_and_download_workflow(
dataset_collection_ids=[ "3" ],
job_ids=job_ids,
+12 -14
View File
@@ -1,26 +1,24 @@
from __future__ import print_function
import time
import yaml
from json import dumps
from collections import namedtuple
from json import dumps
from uuid import uuid4
import yaml
from requests import delete, put
from base import api
from galaxy.tools.verify.test_data import TestDataResolver
from galaxy.exceptions import error_codes
from .helpers import WorkflowPopulator
from .helpers import DatasetPopulator
from .helpers import DatasetCollectionPopulator
from .helpers import skip_without_tool
from galaxy.tools.verify.test_data import TestDataResolver
from .helpers import (DatasetCollectionPopulator, DatasetPopulator,
skip_without_tool, WorkflowPopulator)
from .workflows_format_2 import (
convert_and_import_workflow,
ImporterGalaxyInterface,
)
from requests import delete
from requests import put
SIMPLE_NESTED_WORKFLOW_YAML = """
class: GalaxyWorkflow
inputs:
@@ -277,7 +275,7 @@ class WorkflowsApiTestCase( BaseWorkflowsApiTestCase ):
workflow = show_response.json()
self._assert_looks_like_instance_workflow_representation( workflow )
assert len(workflow["steps"]) == 3
self.assertEqual(sorted([step["id"] for step in workflow["steps"].values()]), [0, 1, 2])
self.assertEqual(sorted(step["id"] for step in workflow["steps"].values()), [0, 1, 2])
show_response = self._get( "workflows/%s" % workflow_id, {"legacy": True} )
workflow = show_response.json()
@@ -285,7 +283,7 @@ class WorkflowsApiTestCase( BaseWorkflowsApiTestCase ):
assert len(workflow["steps"]) == 3
# Can't reay say what the legacy IDs are but must be greater than 3 because dummy
# workflow was created first in this instance.
self.assertNotEqual(sorted([step["id"] for step in workflow["steps"].values()]), [0, 1, 2])
self.assertNotEqual(sorted(step["id"] for step in workflow["steps"].values()), [0, 1, 2])
def test_show_invalid_key_is_400( self ):
show_response = self._get( "workflows/%s" % self._random_key() )
@@ -457,7 +455,7 @@ class WorkflowsApiTestCase( BaseWorkflowsApiTestCase ):
def get_subworkflow_content_id(workflow_id):
workflow_contents = self._download_workflow(workflow_id, style="editor")
steps = workflow_contents['steps']
subworkflow_step = filter(lambda s: s["type"] == "subworkflow", steps.values())[0]
subworkflow_step = next(s for s in steps.values() if s["type"] == "subworkflow")
return subworkflow_step['content_id']
workflow_id = self._upload_yaml_workflow(SIMPLE_NESTED_WORKFLOW_YAML, publish=True)
@@ -1636,7 +1634,7 @@ steps:
contents_url = "histories/%s/contents" % history
history_contents_response = self._get( contents_url )
self._assert_status_code_is( history_contents_response, 200 )
hda_summary = filter( lambda hc: hc[ "hid" ] == hid, history_contents_response.json() )[ 0 ]
hda_summary = next(hc for hc in history_contents_response.json() if hc[ "hid" ] == hid)
hda_info_response = self._get( "%s/%s" % ( contents_url, hda_summary[ "id" ] ) )
self._assert_status_code_is( hda_info_response, 200 )
self.assertEqual( hda_info_response.json()[ "metadata_data_lines" ], lines )
+3 -3
View File
@@ -1,11 +1,11 @@
""" This module defines the public interface or entry point for the
Format 2 workflow code.
"""
from .main import convert_and_import_workflow
from .interface import ImporterGalaxyInterface
from .main import convert_and_import_workflow
__all__ = [
__all__ = (
'convert_and_import_workflow',
'ImporterGalaxyInterface',
]
)
+8 -8
View File
@@ -1,11 +1,11 @@
"""Functionality for converting a Format 2 workflow into a standard Galaxy workflow."""
from __future__ import print_function
from collections import OrderedDict
import json
import os
import sys
import uuid
from collections import OrderedDict
import yaml
@@ -96,7 +96,7 @@ def _python_to_workflow(as_python, conversion_context):
as_python["steps"] = steps_as_dict
steps = steps_as_dict
for step in steps.itervalues():
for step in steps.values():
step_type = step.get("type", None)
if "run" in step:
if step_type is not None:
@@ -118,7 +118,7 @@ def _python_to_workflow(as_python, conversion_context):
run_to_step_function(conversion_context, step, run_action)
del step["run"]
for step in steps.itervalues():
for step in steps.values():
step_type = step.get("type", "tool")
step_type = STEP_TYPE_ALIASES.get(step_type, step_type)
if step_type not in STEP_TYPES:
@@ -326,7 +326,7 @@ def transform_tool(context, step):
return {"__class__": "RuntimeValue"}
if isinstance(value, dict):
new_values = {}
for k, v in value.iteritems():
for k, v in value.items():
new_key = _join_prefix(key, k)
new_values[k] = replace_links(v, new_key)
return new_values
@@ -349,7 +349,7 @@ def transform_tool(context, step):
step_state = step["state"]
step_state = replace_links(step_state)
for key, value in step_state.iteritems():
for key, value in step_state.items():
tool_state[key] = json.dumps(value)
del step["state"]
@@ -470,7 +470,7 @@ def _populate_input_connections(context, step, connect):
input_connections = step["input_connections"]
is_subworkflow_step = step.get("type") == "subworkflow"
for key, values in connect.iteritems():
for key, values in connect.items():
input_connection_value = []
if not isinstance(values, list):
values = [values]
@@ -512,7 +512,7 @@ def main(argv):
if __name__ == "__main__":
main(sys.argv)
__all__ = [
__all__ = (
'yaml_to_workflow',
'python_to_workflow',
]
)
+2 -2
View File
@@ -36,6 +36,6 @@ def convert_and_import_workflow(has_workflow, **kwds):
import_kwds["publish"] = True
return galaxy_interface.import_workflow(workflow, **import_kwds)
__all__ = [
__all__ = (
'convert_and_import_workflow',
]
)
+10 -14
View File
@@ -1,23 +1,19 @@
from contextlib import contextmanager
from six.moves.urllib.parse import urlencode
from .api_asserts import (
assert_error_code_is,
assert_has_keys,
assert_not_has_keys,
assert_status_code_is,
)
from .api_util import get_master_api_key, get_user_api_key
from .interactor import GalaxyInteractorApi as BaseInteractor
# TODO: We don't need all of TwillTestCase, strip down to a common super class
# shared by API and Twill test cases.
from .twilltestcase import TwillTestCase
from base.interactor import GalaxyInteractorApi as BaseInteractor
from .api_util import get_master_api_key
from .api_util import get_user_api_key
from .api_asserts import (
assert_status_code_is,
assert_has_keys,
assert_not_has_keys,
assert_error_code_is,
)
from urllib import urlencode
TEST_USER = "user@bx.psu.edu"
DEFAULT_OTHER_USER = "otheruser@bx.psu.edu" # A second user for API testing.
+10 -13
View File
@@ -17,24 +17,21 @@ import nose.config
import nose.core
import nose.loader
import nose.plugins.manager
from paste import httpserver
import requests
from .api_util import get_master_api_key, get_user_api_key
from .tool_shed_util import parse_tool_panel_config
from .nose_util import run
from .instrument import StructuredTestDataPlugin
from paste import httpserver
from functional import database_contexts
from galaxy.app import UniverseApplication as GalaxyUniverseApplication
from galaxy.web import buildapp
from galaxy.webapps.tool_shed.app import UniverseApplication as ToolshedUniverseApplication
from galaxy.util import asbool
from galaxy.util.properties import load_app_properties
from galaxy.web import buildapp
from galaxy.webapps.tool_shed.app import UniverseApplication as ToolshedUniverseApplication
from base.test_logging import logging_config_file
from .api_util import get_master_api_key, get_user_api_key
from .instrument import StructuredTestDataPlugin
from .nose_util import run
from .test_logging import logging_config_file
from .tool_shed_util import parse_tool_panel_config
galaxy_root = os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir, os.path.pardir))
DEFAULT_WEB_HOST = "localhost"
@@ -695,7 +692,7 @@ def drive_test(test_driver_class):
sys.exit(test_driver_class().run())
__all__ = [
__all__ = (
"copy_database_template",
"build_logger",
"drive_test",
@@ -708,4 +705,4 @@ __all__ = [
"setup_galaxy_config",
"TestDriver",
"wait_for_http_server",
]
)

Some files were not shown because too many files have changed in this diff Show More