pax_global_header00006660000000000000000000000064144476170430014524gustar00rootroot0000000000000052 comment=dec0e91b252b92904f91bf86d522ef40a8b8365b bioblend-1.2.0/000077500000000000000000000000001444761704300133025ustar00rootroot00000000000000bioblend-1.2.0/.git-blame-ignore-revs000066400000000000000000000001231444761704300173760ustar00rootroot00000000000000# Format Python code with black and isort 7bcd07db8392ac790d1b0b92f4a377945197e43d bioblend-1.2.0/.github/000077500000000000000000000000001444761704300146425ustar00rootroot00000000000000bioblend-1.2.0/.github/workflows/000077500000000000000000000000001444761704300166775ustar00rootroot00000000000000bioblend-1.2.0/.github/workflows/deploy.yaml000066400000000000000000000014371444761704300210640ustar00rootroot00000000000000name: Deploy on: [push, pull_request] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: python-version: '3.11' - name: Install dependencies run: | python3 -m pip install --upgrade pip setuptools python3 -m pip install --upgrade build twine - name: Create and check sdist and wheel packages run: | python3 -m build twine check dist/* - name: Publish to PyPI if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && github.repository_owner == 'galaxyproject' uses: pypa/gh-action-pypi-publish@release/v1 with: user: __token__ password: ${{ secrets.PYPI_PASSWORD }} bioblend-1.2.0/.github/workflows/lint.yaml000066400000000000000000000007271444761704300205370ustar00rootroot00000000000000name: Lint on: [push, pull_request] concurrency: group: lint-${{ github.ref }} cancel-in-progress: true jobs: lint: runs-on: ubuntu-latest strategy: matrix: python-version: ['3.7', '3.11'] steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install tox run: python -m pip install 'tox>=1.8.0' - name: Lint run: tox -e lint bioblend-1.2.0/.github/workflows/test.yaml000066400000000000000000000112771444761704300205520ustar00rootroot00000000000000name: Tests on: push: pull_request: schedule: # Run at midnight UTC every Tuesday - cron: '0 0 * * 2' concurrency: group: test-${{ github.ref }} cancel-in-progress: true jobs: test: if: github.event_name != 'schedule' || github.repository_owner == 'galaxyproject' runs-on: ${{ matrix.os }} services: postgres: image: postgres # Provide the password for postgres env: POSTGRES_PASSWORD: postgres # Set health checks to wait until postgres has started options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 ports: - 5432:5432 strategy: fail-fast: false matrix: os: [ubuntu-latest] tox_env: [py37] galaxy_version: - dev - release_23.1 - release_23.0 - release_22.05 - release_22.01 - release_21.09 - release_21.05 - release_21.01 - release_20.09 - release_20.05 - release_20.01 - release_19.09 - release_19.05 include: - os: ubuntu-latest tox_env: py311 galaxy_version: dev # Cannot test on macOS because service containers are not supported # yet: https://github.community/t/github-actions-services-available-on-others-vms/16916 # - os: macos-latest # tox_env: py37 # galaxy_version: dev steps: - uses: actions/checkout@v3 - name: Cache pip dir uses: actions/cache@v3 with: path: ~/.cache/pip key: pip-cache-${{ matrix.tox_env }}-${{ matrix.galaxy_version }} - name: Calculate Python version for BioBlend from tox_env id: get_bioblend_python_version run: echo "bioblend_python_version=$(echo "${{ matrix.tox_env }}" | sed -e 's/^py\([3-9]\)\([0-9]\+\)/\1.\2/')" >> $GITHUB_OUTPUT - name: Set up Python for BioBlend uses: actions/setup-python@v4 with: python-version: ${{ steps.get_bioblend_python_version.outputs.bioblend_python_version }} - name: Install tox run: | python3 -m pip install --upgrade pip setuptools python3 -m pip install 'tox>=1.8.0' 'virtualenv>=20.0.14' - name: Determine Python version for Galaxy id: get_galaxy_python_version run: | case ${{ matrix.galaxy_version }} in release_19.05 | release_19.09 | release_20.0* ) # The minimum Python version supported by the 19.05 and 19.09 # releases is 2.7, but virtualenv dropped support for creating # Python <3.7 environments in v20.22.0 . # The minimum Python version supported by the 20.0* releases is # 3.5, but the setup-python GitHub action dropped support for # Python 3.5 and 3.6 on Ubuntu 22.04, see # https://github.com/actions/setup-python/issues/544#issuecomment-1332535877 galaxy_python_version=3.7 ;; release_21.0* ) # The minimum Python supported version by these releases is 3.6, # but same as above. galaxy_python_version=3.7 ;; release_22.0* | release_23.* | dev ) galaxy_python_version=3.7 esac echo "galaxy_python_version=$galaxy_python_version" >> $GITHUB_OUTPUT - name: Set up Python for Galaxy uses: actions/setup-python@v4 with: python-version: ${{ steps.get_galaxy_python_version.outputs.galaxy_python_version }} - name: Run tests env: PGPASSWORD: postgres PGPORT: 5432 PGHOST: localhost run: | # Create a PostgreSQL database for Galaxy. The default SQLite3 database makes test fail randomly because of "database locked" error. createdb -U postgres galaxy # Run ToolShed tests only once per Python version if [ "${{ matrix.galaxy_version }}" = 'dev' ]; then export BIOBLEND_TOOLSHED_URL=https://testtoolshed.g2.bx.psu.edu/ fi # Install Galaxy GALAXY_DIR=galaxy-${{ matrix.galaxy_version }} git clone --depth=1 -b ${{ matrix.galaxy_version }} https://github.com/galaxyproject/galaxy $GALAXY_DIR export DATABASE_CONNECTION=postgresql://postgres:@localhost/galaxy ./run_bioblend_tests.sh -g $GALAXY_DIR -v python${{ steps.get_galaxy_python_version.outputs.galaxy_python_version }} -e ${{ matrix.tox_env }} - name: The job has failed if: ${{ failure() }} run: | cat galaxy-${{ matrix.galaxy_version }}/*.log bioblend-1.2.0/.gitignore000066400000000000000000000005671444761704300153020ustar00rootroot00000000000000*.py[co] *~ # Packages *.egg *.egg-info dist build eggs parts bin var sdist develop-eggs .installed.cfg .eggs # Installer logs pip-log.txt # Unit test / coverage reports .coverage .tox #Translations *.mo #Mr Developer .mr.developer.cfg #Vim *.swp #Code coverage cover #eclipse/pydev .project .pydevproject .idea # compiled docs docs/_build # Python virtualenv .venv bioblend-1.2.0/.isort.cfg000066400000000000000000000004341444761704300152020ustar00rootroot00000000000000[settings] force_alphabetical_sort_within_sections=true # Override force_grid_wrap value from profile=black, but black is still happy force_grid_wrap=2 # Same line length as for black line_length=120 no_lines_before=LOCALFOLDER profile=black reverse_relative=true skip_gitignore=true bioblend-1.2.0/.readthedocs.yaml000066400000000000000000000013021444761704300165250ustar00rootroot00000000000000# .readthedocs.yaml # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 # Set the OS, Python version and other tools you might need build: os: ubuntu-22.04 tools: python: "3.11" # Build documentation in the docs/ directory with Sphinx sphinx: configuration: docs/conf.py # Optionally build your docs in additional formats such as PDF and ePub formats: - pdf # Optional but recommended, declare the Python requirements required # to build your documentation # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html python: install: - path: . - requirements: docs/requirements.txt bioblend-1.2.0/ABOUT.rst000066400000000000000000000034111444761704300147050ustar00rootroot00000000000000`BioBlend `_ is a Python library for interacting with the `Galaxy`_ API. BioBlend is supported and tested on: - Python 3.7 - 3.11 - Galaxy release 19.05 and later. BioBlend's goal is to make it easier to script and automate the running of Galaxy analyses and administering of a Galaxy server. In practice, it makes it possible to do things like this: - Interact with Galaxy via a straightforward API:: from bioblend.galaxy import GalaxyInstance gi = GalaxyInstance('', key='your API key') libs = gi.libraries.get_libraries() gi.workflows.show_workflow('workflow ID') wf_invocation = gi.workflows.invoke_workflow('workflow ID', inputs) - Interact with Galaxy via an object-oriented API:: from bioblend.galaxy.objects import GalaxyInstance gi = GalaxyInstance("URL", "API_KEY") wf = gi.workflows.list()[0] hist = gi.histories.list()[0] inputs = hist.get_datasets()[:2] input_map = dict(zip(wf.input_labels, inputs)) params = {"Paste1": {"delimiter": "U"}} wf_invocation = wf.invoke(input_map, params=params) About the library name ~~~~~~~~~~~~~~~~~~~~~~ The library was originally called just ``Blend`` but we `renamed it `_ to reflect more of its domain and a make it bit more unique so it can be easier to find. The name was intended to be short and easily pronounceable. In its original implementation, the goal was to provide a lot more support for `CloudMan`_ and other integration capabilities, allowing them to be *blended* together via code. ``BioBlend`` fitted the bill. .. References/hyperlinks used above .. _CloudMan: https://galaxyproject.org/cloudman/ .. _Galaxy: https://galaxyproject.org/ bioblend-1.2.0/CHANGELOG.md000066400000000000000000000762701444761704300151270ustar00rootroot00000000000000### Bioblend v1.2.0 - 2023-06-30 * Dropped support for Galaxy releases 17.09-19.01. Added support for Galaxy release 23.1. * Added a new ``container_resolution`` attribute to ``GalaxyInstance`` objects, which is an instance of the new ``ContainerResolutionClient``. This new module can be used to list container resolvers, and to resolve (and install) tool requirements against specified container resolvers (thanks to [cat-bro](https://github.com/cat-bro) and [Matthias Bernt](https://github.com/bernt-matthias)). * Added ``reload_toolbox()`` method to ``ConfigClient`` (thanks to [Anthony Bretaudeau](https://github.com/abretaud)). * Added ``delete_unused_dependency_paths()`` and ``unused_dependency_paths()`` methods to ``ToolDependenciesClient`` (thanks to [Matthias Bernt](https://github.com/bernt-matthias)). * Added ``data_manager_mode`` parameter to ``ToolClient.run_tool()`` method (thanks to [Marius van den Beek](https://github.com/mvdbeek)). * Added ``user_data`` parameter to ``UserClient.update_user()`` method (thanks to [Uwe Winter](https://github.com/uwwint)). * Fixed bug in ``DatasetClient.download_dataset()`` and BioBlend.objects ``HistoryDatasetAssociation.get_stream()`` where the wrong download URL was generated if the Galaxy instance is served at a subdirectory (reported by [Anil Thanki](https://github.com/anilthanki)). * Improvements to tests and documentation (thanks to [kxk302](https://github.com/kxk302) and [Simon Bray](https://github.com/simonbray)). ### BioBlend v1.1.1 - 2023-02-21 * Same as BioBlend v1.1.0, briefly released with wrong version number 1.0.1 on GitHub, PyPI and Bioconda. * Added support for Python 3.11. Added support for Galaxy release 23.0. * Using the deprecated ``folder_id`` parameter of the ``LibraryClient.get_folders()`` method now raises a ``ValueError`` exception. * Using the deprecated ``library_id`` parameter of the ``LibraryClient.get_libraries()`` method now raises a ``ValueError`` exception. * Using the deprecated ``tool_id`` parameter of the ``ToolClient.get_tools()`` method now raises a ``ValueError`` exception. * Using the deprecated ``workflow_id`` parameter of the ``WorkflowClient.get_workflows()`` method now raises a ``ValueError`` exception. * Modified ``delete_workflow()`` method of ``WorkflowClient`` to return ``None`` instead of a string. * Added ``py.typed`` marker file to distributed packages (as per PEP 561) to declare type checking support. * Improvements to tests and documentation. ### BioBlend v1.0.0 - 2022-10-13 * Dropped support for deprecated CloudMan, see https://galaxyproject.org/blog/2021-10-sunsetting-cloudlaunch/ * Added dependency on ``typing-extensions`` package, removed dependencies on ``boto`` and ``pyyaml``. * Deprecated ``max_get_retries()``, ``set_max_get_retries()``, ``get_retry_delay()`` and ``set_get_retry_delay()`` methods of ``Client``. * Moved ``max_get_attempts`` and ``get_retry_delay`` properties from ``GalaxyInstance`` to ``GalaxyClient``, so they are also available in ``ToolshedInstance``. * Added ``get_or_create_user_apikey()`` method to ``UserClient``. * Added ``all`` parameter to ``HistoryClient.get_histories()`` method (thanks to [Paprikant](https://github.com/Paprikant)). * Added ``require_exact_tool_versions`` parameter to ``WorkflowClient.invoke_workflow()`` method (thanks to [cat-bro](https://github.com/cat-bro)). * Added ``name`` and ``owner`` parameters to ``ToolShedRepositoryClient.get_repositories()``. * Remove unused methods from ``bioblend.config.Config``. If needed, use the methods inherited from `configparser.ConfigParser` instead. * Allowed any 2XX HTTP response status code in ``Client._delete()`` to correctly support history purging via Celery (thanks to [Nolan Woods](https://github.com/innovate-invent)). * Fixed bug in ``FormsClient.create_form()`` where the ``form_xml_text`` argument was not passed correctly to the Galaxy API. * Fixed bug in ``HistoryClient.show_dataset_provenance()`` where the ``follow`` argument was not passed to the Galaxy API. * BioBlend.objects: Added ``delete()`` abstract method to ``DatasetContainer`` class. * Added Code of Conduct for the project. * Finished the full type annotation of the library (thanks to [cat-bro](https://github.com/cat-bro), [Fabio Cumbo](https://github.com/cumbof), [Jayadev Joshi](https://github.com/jaidevjoshi83), [thepineapplepirate](https://github.com/thepineapplepirate)). * Improvements to tests and documentation. ### BioBlend v0.18.0 - 2022-07-07 * Added support for Galaxy release 22.05. * Added tus support to ``ToolClient.upload_file()`` (thanks to [Nate Coraor](https://github.com/natefoo)). * Formatted Python code with black and isort. * Improvements to type annotations, tests and documentation. ### BioBlend v0.17.0 - 2022-05-09 * Dropped support for Python 3.6. Added support for Python 3.10. Added support for Galaxy releases 21.09 and 22.01. * Removed deprecated ``run_workflow()`` method of ``WorkflowClient``. * Using the deprecated ``history_id`` parameter of the ``HistoryClient.get_histories()`` method now raises a ``ValueError`` exception. * Made ``tool_inputs_update`` parameter of ``JobsClient.rerun_job()`` more flexible. * Added ``whoami()`` method to ``ConfigClient`` (thanks to [cat-bro](https://github.com/cat-bro)). * Added ``get_extra_files()`` method to ``HistoryClient``. * Added ``build()`` and ``reload()`` methods to ``ToolClient`` (thanks to [Jayadev Joshi](https://github.com/jaidevjoshi83) and [cat-bro](https://github.com/cat-bro) respectively). * Added ``get_repositories()`` method to ``ToolShedCategoryClient`` (thanks to [cat-bro](https://github.com/cat-bro)). * Added ``update_repository_metadata()`` method to ``ToolShedRepositoryClient``. * Added ``order_by`` parameter to ``JobsClient.get_jobs()`` method. * BioBlend.objects: Removed deprecated ``run()`` method of ``Workflow``. * BioBlend.objects: Fail if multiple libraries/histories/workflows match when deleting by name, instead of deleting them all. * BioBlend.objects: in ``HistoryDatasetAssociation.get_stream()``, wait for the dataset to be ready. * BioBlend.objects: in ``Workflow.invoke()``, check that the workflow is mapped and runnable before invoking, allow the ``inputs`` parameter to be an instance of a ``Dataset`` subclass, and allow the ``history`` parameter to be the name of a new history. * BioBlend.objects: Added new ``datasets`` and ``dataset_collections`` attributes to ``GalaxyInstance`` objects, which are instances of the new ``ObjDatasetClient`` and ``ObjDatasetCollectionClient`` respectively. * BioBlend.objects: Added ``refresh()``, ``get_outputs()`` and ``get_output_collections()`` methods to ``InvocationStep``. * Fixed [error](https://github.com/galaxyproject/bioblend/issues/398) when instantiating ``GalaxyInstance`` with ``email`` and ``password`` (reported by [Peter Briggs](https://github.com/pjbriggs)). * Fixed parameter validation errors for POST requests with attached files on upcoming Galaxy 22.05. * Code cleanups (thanks to [Martmists](https://github.com/Martmists-GH)). * Improvements to type annotations, tests and documentation. ### BioBlend v0.16.0 - 2021-06-13 * Added support for Galaxy release 21.05. * Replaced the ``job_info`` parameter with separate ``tool_id``, ``inputs`` and ``state`` parameters in ``JobsClient.search_jobs()`` (thanks to [rikeshi](https://github.com/rikeshi)). * Pass the API key for all requests as the ``x-api-key`` header instead of as a parameter (thanks to [rikeshi](https://github.com/rikeshi)). * Try prepending "https://" and "http://" if the scheme is missing in the ``url`` argument passed to ``GalaxyClient``, i.e. when initialising a Galaxy or ToolShed instance. * Added a new ``dataset_collections`` attribute to ``GalaxyInstance`` objects, which is an instance of the new ``DatasetCollectionClient``. This new module can be used to get details of a dataset collection, wait until elements of a dataset collection are in a terminal state, and download a history dataset collection as an archive (thanks to [rikeshi](https://github.com/rikeshi)). * Added a new ``tool_dependencies`` attribute to ``GalaxyInstance`` objects, which is an instance of the new ``ToolDependenciesClient``. This new module can be used to summarize requirements across toolbox (thanks to [cat-bro](https://github.com/cat-bro)). * Added ``publish_dataset()`` ``update_permissions()`` and ``wait_for_dataset()`` methods to ``DatasetClient``. * Added ``get_invocation_biocompute_object()``, ``get_invocation_report_pdf()``, ``get_invocation_step_jobs_summary()``, ``rerun_invocation()`` and ``wait_for_invocation()`` methods to ``InvocationClient`` (thanks to [rikeshi](https://github.com/rikeshi)). * Added ``cancel_job()``, ``get_common_problems()``, ``get_destination_params()``, ``get_inputs()``, ``get_outputs()``, ``resume_job()``, ``show_job_lock()``, ``update_job_lock()`` and ``wait_for_job()`` methods to ``JobsClient`` (thanks to [Andrew Mcgregor](https://github.com/Mcgregor381) and [rikeshi](https://github.com/rikeshi)). * Added ``get_citations()`` and ``uninstall_dependencies()`` methods to ``ToolClient`` (thanks to [rikeshi](https://github.com/rikeshi)). * Added ``extract_workflow_from_history()``, ``refactor_workflow()`` and ``show_versions()`` methods to ``WorkflowClient`` (thanks to [rikeshi](https://github.com/rikeshi)). * Added several parameters to ``DatasetClient.get_datasets()`` method (thanks to [rikeshi](https://github.com/rikeshi)). * Added several parameters to ``InvocationClient.get_invocations()`` method (thanks to [Nolan Woods](https://github.com/innovate-invent) and [rikeshi](https://github.com/rikeshi)). * Added several parameters to ``JobsClient.get_jobs()`` method (thanks to [rikeshi](https://github.com/rikeshi)). * Added ``parameters_normalized`` parameter to ``WorkflowClient.invoke_workflow()`` method (thanks to [rikeshi](https://github.com/rikeshi)). * Deprecated ``folder_id`` parameter of ``LibraryClient.get_folders()`` method. * Deprecated ``library_id`` parameter of ``LibraryClient.get_libraries()`` method. * Deprecated ``tool_id`` parameter of ``ToolClient.get_tools()`` method. * Deprecated ``workflow_id`` parameter of ``WorkflowClient.get_workflows()`` method. * BioBlend.objects: Removed deprecated ``container_id`` property of ``Dataset`` and ``Folder`` objects. * BioBlend.objects: Removed ``Preview`` abstract class. * BioBlend.objects: Added ``invoke()`` method to ``Workflow``. Added ``ObjInvocationClient``, and ``Invocation`` and ``InvocationPreview`` wrappers (thanks to [rikeshi](https://github.com/rikeshi)). * BioBlend.objects: Added ``latest_workflow_uuid`` property to ``Workflow`` objects. Added ``deleted``, ``latest_workflow_uuid``, ``number_of_steps``, ``owner`` and ``show_in_tool_panel`` properties to ``WorkflowPreview`` (thanks to [Nolan Woods](https://github.com/innovate-invent)). * BioBlend.objects: Deprecated ``run()`` method of ``Workflow``. * Added ``use_ssl``, ``verify`` and ``authuser`` parameters to ``CloudManInstance.__init__()`` (thanks to [Nathan Edwards](https://github.com/edwardsnj)). * Improvements to type annotations, tests and documentation (thanks to [rikeshi](https://github.com/rikeshi)). ### BioBlend v0.15.0 - 2021-02-10 * Dropped support for Python 3.5. Added support for Python 3.9. Added support for Galaxy releases 20.09 and 21.01. * Changed the return value of ``RolesClient.create_role()`` method from a 1-element list containing a dict to a dict. * Removed deprecated ``download_dataset()`` and ``get_current_history`` methods of ``HistoryClient``. * Removed deprecated ``export_workflow_json()`` and ``import_workflow_json`` methods of ``WorkflowClient``. * Added ``copy_content()``, ``get_published_histories()`` and ``open_history()`` methods to ``HistoryClient``. * Added ``rerun_job()`` method to ``JobsClient``. * Added ``requirements()`` method to ``ToolClient`` (thanks to [cat-bro](https://github.com/cat-bro)). * Added ``published`` and ``slug`` parameters to ``HistoryClient.get_histories()``. * Added ``require_state_ok`` parameter to ``DatasetClient.download_dataset()``. * Added ``input_format`` parameter to ``ToolClient.run_tool()``. * Deprecated ``history_id`` parameter of ``HistoryClient.get_histories()`` method. * BioBlend.objects: Added ``owner`` property to ``Workflow`` objects. Added ``annotation``, ``published`` and ``purged`` properties to ``HistoryPreview`` objects. * Fixed issue where specifying the Galaxy URL with "http://" instead of "https://" when creating a ``GalaxyInstance`` made the subsequent non-GET requests to silently fail. * Moved the Continuous Integration (CI) from TravisCI to GitHub workflows (thanks to [Oleg Zharkov](https://github.com/OlegZharkov)). * Improvements to tests and documentation (thanks to [Gianmauro Cuccuru](https://github.com/gmauro)). ### BioBlend v0.14.0 - 2020-07-04 * Dropped support for Python 2.7. Dropped support for Galaxy releases 14.10-17.05. Added support for Python 3.8. Added support for Galaxy releases 19.09, 20.01 and 20.05. * Added a new ``invocations`` attribute to ``GalaxyInstance`` objects, which is an instance of the new ``InvocationClient`` class. This new module can be used to get all workflow invocations, show or cancel an invocation, show or pause an invocation step, get a summary or a report for an invocation (thanks to [Simon Bray](https://github.com/simonbray)). * Added ``get_datasets()`` method to ``DatasetClient`` (thanks to [Simon Bray](https://github.com/simonbray)). * Added ``import_history()`` method to ``HistoryClient`` (thanks to [David Christiany](https://github.com/davidchristiany) and [Marius van den Beek](https://github.com/mvdbeek)). * Added ``copy_dataset()`` method to ``HistoryClient`` (thanks to [Simon Bray](https://github.com/simonbray)). * Added ``get_metrics()`` method to ``JobsClient`` (thanks to [Andreas Skorczyk](https://github.com/AndreasSko)). * Added ``report_error()`` method to ``JobsClient`` (thanks to [Peter Selten](https://github.com/selten)). * Added ``get_dataset_permissions()`` and ``set_dataset_permissions()`` methods to ``LibraryClient`` (thanks to [Frederic Sapet](https://github.com/FredericBGA)). * Added ``update_user()`` method to ``UserClient`` (thanks to [Anthony Bretaudeau](https://github.com/abretaud)). * Added ``update_workflow()`` method to ``WorkflowClient``. * Added ``tags`` parameter to ``upload_file_from_url()``, ``upload_file_contents()``, ``upload_file_from_local_path()``, ``upload_file_from_server()`` and ``upload_from_galaxy_filesystem()`` methods of ``LibraryClient`` (thanks to [Anthony Bretaudeau](https://github.com/abretaud)). * Changed the default for the ``tag_using_filenames`` parameter of ``upload_file_from_server()`` and ``upload_from_galaxy_filesystem()`` methods of ``LibraryClient`` from ``True`` to ``False`` (thanks to [Anthony Bretaudeau](https://github.com/abretaud)). * Added ``version`` parameter to ``show_workflow()`` and ``export_workflow_dict()`` methods of ``WorkflowClient``. * Added ``inputs_by`` option to ``invoke_workflow()`` method of ``WorkflowClient`` (thanks to [Marius van den Beek](https://github.com/mvdbeek)). * Removed deprecated ``show_stderr()`` and ``show_stdout`` methods of ``DatasetClient``. * BioBlend.objects: Allowed workflow steps of type ``parameter_input`` and ``subworkflow``. Added ``parameter_input_ids`` property to ``Workflow`` objects (reported by [Nolan Woods](https://github.com/innovate-invent)). * Fixed ``HistoryClient.export_history(..., wait=False, maxwait=None)`` (reported by [David Christiany](https://github.com/davidchristiany)). * Moved internal ``_make_url()`` method from ``GalaxyClient`` to ``Client`` class. ### BioBlend v0.13.0 - 2019-08-09 * Dropped support for Python 3.4. Added support for Galaxy releases 19.01 and 19.05. * Updated ``requests-toolbelt`` requirement to ``>=0.5.1`` to prevent failing of uploads to Galaxy (reported by [m93](https://github.com/mmeier93)). * Added ``toolshed`` attribute to ``GalaxyInstance`` and made ``toolShed`` an alias to it (reported by [Miriam PayĆ”](https://github.com/mpaya)). * Added ``uninstall_repository_revision()`` method to ``ToolShedClient`` (thanks to [Helena Rasche](https://github.com/erasche), reported by [Alexander Lenail](https://github.com/alexlenail)). * Added ``maxwait`` parameter to ``HistoryClient.export_history()`` and ``History.export()`` methods. * Fixed handling of ``type`` argument in ``HistoryClient.show_history()`` (thanks to [Marius van den Beek](https://github.com/mvdbeek)). * Fixed handling of ``deleted`` argument in ``LibraryClient.get_libraries()`` (thanks to [Luke Sargent](https://github.com/luke-c-sargent), reported by [Katie](https://github.com/emartchenko)). * Fixed ``LibraryClient.wait_for_dataset()`` when ``maxwait`` or ``interval`` arguments are of type ``float``. * Unify JSON-encoding of non-file parameters of POST requests inside ``GalaxyClient.make_post_request()``. * Improvements to tests and documentation (thanks to [Helena Rasche](https://github.com/erasche), [Peter Selten](https://github.com/selten) and [Pablo Moreno](https://github.com/pcm32)). ### BioBlend v0.12.0 - 2018-12-17 * Added support for Python 3.7. Added support for Galaxy releases 18.05 and 18.09. * Added ``update_library_dataset()`` method to ``LibraryClient`` (thanks to [Anthony Bretaudeau](https://github.com/abretaud)). * Added ``preserve_dirs`` and ``tag_using_filenames`` parameters to ``upload_file_from_server()`` and ``upload_from_galaxy_filesystem()`` methods of ``LibraryClient`` (thanks to [Anthony Bretaudeau](https://github.com/abretaud)). * Removed deprecated ``wait_for_completion`` parameter of ``DatasetClient.download_dataset()`` method. * BioBlend.objects: added ``genome_build`` and ``misc_info`` attributes to ``Dataset`` objects. Moved ``deleted`` attribute from ``Dataset`` to ``HistoryDatasetAssociation`` and ``LibraryDatasetDatasetAssociation`` objects. Moved ``purged`` attribute from ``Dataset`` to ``HistoryDatasetAssociation`` objects. * BioBlend.objects: added ``update()`` method to ``LibraryDataset`` (thanks to [Anthony Bretaudeau](https://github.com/abretaud)). * Run tests with pytest instead of nose. ### BioBlend v0.11.0 - 2018-04-18 * Dropped support for Python 3.3. Added support for Galaxy release 18.01. * Always wait for terminal state when downloading a dataset. * Deprecated ``wait_for_completion`` parameter of ``DatasetClient.download_dataset()`` method. * Fixed downloading of datasets receiving a HTTP 500 status code (thanks to [Helena Rasche](https://github.com/erasche)). * Added ``wait_for_dataset()`` method to ``LibraryClient``. * Added ``verify`` parameter to ``GalaxyInstance.__init__()`` method (thanks to Devon Ryan). * Improvements to tests and documentation. ### BioBlend v0.10.0 - 2017-09-26 * Dropped support for Python 2.6. Added support for Galaxy release 17.09. * Added ``contents`` parameter to ``FoldersClient.show_folder()`` method (thanks to [Helena Rasche](https://github.com/erasche)). * Exposed the `verify` attribute of `GalaxyInstance` and `ToolShedInstance` objects as `__init__()` parameter. * Added ``create_role()`` method to ``RolesClient`` (thanks to Ashok Varadharajan). * Added ``timeout`` parameter to ``GalaxyClient.__init__()`` method. * Added ``publish`` parameter to ``import_workflow_dict()`` and ``import_workflow_from_local_path()`` methods of ``WorkflowClient`` (thanks to Marco Enrico Piras). * BioBlend.objects: added ``publish`` parameter to ``ObjWorkflowClient.import_new()`` method (thanks to Marco Enrico Piras). * Do not check for mismatching content size when streaming a dataset to file (reported by Jorrit Boekel). * Fixed delete requests when Galaxy uses external authentication (thanks to [Helena Rasche](https://github.com/erasche)). * Fixed retrieval of the API key when a ``GalaxyClient`` object is initialised with email and password on Python 3 (thanks to [Marius van den Beek](https://github.com/mvdbeek)). * Documentation improvements. ### BioBlend v0.9.0 - 2017-05-25 * Dropped support for Galaxy releases 14.02, 14.04, 14.06 and 14.08. Added support for Python 3.5 and 3.6, and Galaxy releases 16.07, 16.10, 17.01 and 17.05. * Deprecated ``import_workflow_json()`` and ``export_workflow_json()`` methods of ``WorkflowClient`` in favor of ``import_workflow_dict()`` and ``export_workflow_dict()`` (reported by @manabuishii). * Deprecated ``show_stderr()`` and ``show_stdout()`` methods of ``DatasetClient`` in favour of ``JobsClient.show_job()`` with ``full_details=True``. * Added ``install_dependencies()`` method to ``ToolClient`` (thanks to [Marius van den Beek](https://github.com/mvdbeek)). * Added ``reload_data_table()`` method to ``ToolDataClient`` (thanks to [Marius van den Beek](https://github.com/mvdbeek)). * Added ``create_folder()``, ``update_folder()``, ``get_permissions()``, ``set_permissions()`` methods to ``FoldersClient`` (thanks to [Helena Rasche](https://github.com/erasche)). * Added ``get_version()`` method to ``ConfigClient`` (thanks to [Helena Rasche](https://github.com/erasche)). * Added ``get_user_apikey()`` method to ``UserClient`` (thanks to [Helena Rasche](https://github.com/erasche)). * Added ``create_quota()``, ``update_quota()``, ``delete_quota()`` and ``undelete_quota()`` methods to ``QuotaClient`` (thanks to [Helena Rasche](https://github.com/erasche)). * Added ``purge`` parameter to ``HistoryClient.delete_dataset()`` method. * Added ``f_email``, ``f_name``, and ``f_any`` parameters to ``UserClient.get_users()`` method (thanks to [Helena Rasche](https://github.com/erasche)). * Updated ``WorkflowClient.import_shared_workflow()`` method to use the newer Galaxy API request (thanks to @DamCorreia). * Fixed ``HistoryClient.update_history()`` and ``History.update()`` methods when ``name`` argument is not specified. * Added warning if content size differs from content-length header in ``DatasetClient.download_dataset()``. * BioBlend.objects: added ``purge`` parameter to ``HistoryDatasetAssociation.delete()`` method. * BioBlend.objects: added ``purged`` attribute to ``Dataset`` objects. * BioBlend.objects: added ``published`` attribute to ``History`` objects. * Code refactoring, added tests and documentation improvements. ### BioBlend v0.8.0 - 2016-08-11 * Removed deprecated ``create_user()`` method of ``UserClient``. * Deprecated ``HistoryClient.download_dataset()`` in favor of ``DatasetClient.download_dataset()``. * Modified ``update_dataset()``, ``update_dataset_collection()`` and ``update_history()`` methods of ``HistoryClient`` to return the details instead of the status code. * Modified ``update_dataset()``, ``update_dataset_collection()`` and ``update_history()`` methods of ``HistoryClient`` to return the details instead of the status code. * Modified ``GalaxyClient.make_put_request()`` to return the decoded response content. * Added ``install_resolver_dependencies`` parameter to ``ToolShedClient.install_repository_revision()``, applicable for Galaxy release 16.07 and later (thanks to [Marius van den Beek](https://github.com/mvdbeek)). * Improve ``DatasetClient.download_dataset()`` by downloading the dataset in chunks when saving to file (thanks to Jorrit Boekel). * Added ``bioblend.toolshed.categories.ToolShedCategoryClient``; renamed ``bioblend.toolshed.repositories.ToolShedClient`` class to ``bioblend.toolshed.repositories.ToolShedRepositoryClient``; renamed ``bioblend.toolshed.tools.ToolShedClient`` class to ``bioblend.toolshed.tools.ToolShedToolClient``. * Added ``delete_user()`` method to ``UserClient``. * BioBlend.objects: added ``update()`` method to ``HistoryDatasetAssociation``. * BioBlend.objects: added ``annotation`` and ``genome_build`` attributes to ``HistoryDatasetAssociation`` objects. * BioBlend.objects: added ``update()`` method to ``HistoryDatasetAssociation``. * BioBlend.objects: added ability to create and delete dataset collections (thanks to Alex MacLean). * BioBlend.objects: added dataset collections to the outputs of ``Workflow.run()``. * Added ability to launch Galaxy CloudMan instances into AWS VPC. * A number of testing tweaks, documentation improvements and minor fixes. ### BioBlend v0.7.0 - 2015-11-02 * BioBlend.objects: enabled import of workflows containing dataset collection inputs. * Implemented APIs for a modern Galaxy workflow APIs (i.e. delayed scheduling). * Implemented APIs to search Tool Shed repositories and tools. * Added support for uploading (importing) from FTP (thanks to [Helena Rasche](https://github.com/erasche)). * Added ``to_posix_lines`` and ``space_to_tab`` params to ``upload_file()``, ``upload_from_ftp()`` and ``paste_content()`` methods of ``ToolClient``. * BioBlend.objects: added ``upload_from_ftp()`` method to ``History``. * Updated the testing framework to work with Galaxy wheels; use TravisCI's container infrastructure; test Galaxy release 15.07. * Updated CloudmanLauncher's ``launch`` method to accept ``subnet_id`` parameter, for VPC support (thanks to Matthew Ralston). * Properly pass extra arguments to cloud instance userdata. * Updated placement finding methods and `get_clusters_pd` method to return a dict vs. lists so error messages can be included. * A numer of documentation improvements and minor updates/fixes (see individual commits). ### BioBlend v0.6.1 - 2015-07-27 * BioBlend.objects: renamed ``ObjDatasetClient`` abstract class to ``ObjDatasetContainerClient``. * BioBlend.objects: added ``ABCMeta`` metaclass and ``list()`` method to ``ObjClient``. * BioBlend.objects: added ``io_details`` and ``link_details`` parameters to ``ObjToolClient.get()`` method. * Open port 8800 when launching cloud instances for use by NodeJS proxy for Galaxy IPython Interactive Environments. * When launching cloud instances, propagate error messages back to the called. The return types for methods ``create_cm_security_group``, ``create_key_pair`` in ``CloudManLauncher`` class have changed as a result of this. ### BioBlend v0.6.0 - 2015-06-30 * Added support for Python >= 3.3. * Added ``get_library_permissions()`` method to ``LibraryClient``. * Added ``update_group()``, ``get_group_users()``, ``get_group_roles()``, ``add_group_user()``, ``add_group_role()``, ``delete_group_user()`` and ``delete_group_role()`` methods to ``GroupsClient``. * Added ``full_details`` parameter to ``JobsClient.show_job()`` (thanks to Rossano Atzeni). * BioBlend.objects: added ``ObjJobClient`` and ``Job`` wrapper (thanks to Rossano Atzeni). * BioBlend.objects: added check to verify that all tools in a workflow are installed on the Galaxy instance (thanks to [Gianmauro Cuccuru](https://github.com/gmauro)). * Removed several deprecated parameters: see commits [19e168f](https://github.com/galaxyproject/bioblend/commit/19e168f5342f4c791d37694d7039a85f2669df71) and [442ae98](https://github.com/galaxyproject/bioblend/commit/442ae98037be7455d57be15542553dc848d99431). * Verify SSL certificates by default. * Added documentation about the Tool Shed and properly link all the docs on ReadTheDocs. * Solidified automated testing by using [tox](https://tox.readthedocs.org/) and [flake8](https://gitlab.com/pycqa/flake8). ### BioBlend v0.5.3 - 2015-03-18 * Project source moved to new URL - https://github.com/galaxyproject/bioblend * Huge improvements to automated testing, tests now run against Galaxy release 14.02 and all later versions to ensure backward compatibility (see `.travis.yml` for details). * Many documentation improvements (thanks to [Helena Rasche](https://github.com/erasche)). * Added Galaxy clients for the tool data tables, the roles, and library folders (thanks to [Anthony Bretaudeau](https://github.com/abretaud)). * Added method to get the standard error and standard output for the job corresponding to a Galaxy dataset (thanks to [Anthony Bretaudeau](https://github.com/abretaud)). * Added ``get_state()`` method to ``JobsClient``. * Added ``copy_from_dataset()`` method to ``LibraryClient``. * Added ``create_repository()`` method to ``ToolShedRepositoryClient`` (thanks to [Helena Rasche](https://github.com/erasche)). * Fixed ``DatasetClient.download_dataset()`` for certain proxied Galaxy deployments. * Made ``LibraryClient._get_root_folder_id()`` method safer and faster for Galaxy release 13.06 and later. * Deprecate and ignore invalid ``deleted`` parameter to ``WorkflowClient.get_workflows()``. * CloudMan: added method to fetch instance types. * CloudMan: updated cluster options to reflect change to SLURM. * BioBlend.objects: deprecate and ignore invalid ``deleted`` parameter to ``ObjWorkflowClient.list()``. * BioBlend.objects: added ``paste_content()`` method to ``History`` objects. * BioBlend.objects: added ``copy_from_dataset()`` method and ``root_folder`` property to ``Library`` objects. * BioBlend.objects: added ``container`` and ``deleted`` attributes to ``Folder`` objects. * BioBlend.objects: the ``parent`` property of a ``Folder`` object is now set to its parent folder object (thanks to John M. Eppley). * BioBlend.objects: added ``deleted`` parameter to ``list()`` method of libraries and histories. * BioBlend.objects: added ``state`` and ``state_details`` attributes to ``History`` objects (thanks to [Gianmauro Cuccuru](https://github.com/gmauro)). * BioBlend.objects: renamed ``upload_dataset()`` method to ``upload_file()`` for ``History`` objects. * BioBlend.objects: renamed ``input_ids`` and ``output_ids`` attributes of ``Workflow`` objects to ``source_ids`` and ``sink_ids`` respectively. * Add ``run_bioblend_tests.sh`` script (useful for Continuous Integration testing). ### BioBlend v0.5.2 - 2014-10-17 * BioBlend.objects: enabled email&password auth * Enabled Tool Shed tar ball uploads * BioBlend.objects: implemented deletion of history and library datasets * BioBlend.objects: fixed library dataset downloads * Fixed the Tool Shed tool installation method * Added 'deleted' attribute to DatasetContainer * Handle `data_type` changes in the Oct 2014 Galaxy release * Renamed `get_current_history()` to `get_most_recently_used_history()` * A number of documentation improvements and other small fixes (see the commit messages for more details) ### BioBlend v0.5.1 - 2014-08-19 * Fixed url joining problem described in issue #82 * Enabled Travis Continuous Inetgration testing * Added script to create a user and get its API key * Deprecated ``create_user()`` method in favor of clearer ``create_remote_user()``. Added ``create_local_user()``. * Skip instead of fail tests when ``BIOBLEND_GALAXY_URL`` and ``BIOBLEND_GALAXY_API_KEY`` environment variables are not defined. * Added export and download to objects API * Added export/download history * GalaxyClient: changed ``make_put_request()`` to return whole ``requests`` response object * Added Tool wrapper to *BioBlend.objects* plus methods to list tools and get one. * Added ``show_tool()`` method to ``ToolClient`` class * Added ``name``, ``in_panel`` and ``trackster`` filters to ``get_tools()`` * Added ``upload_dataset()`` method to ``History`` class. * Removed ``DataInput`` and ``Tool`` classes for workflow steps. ``Tool`` is to be used for running single tools. bioblend-1.2.0/CITATION000066400000000000000000000026711444761704300144450ustar00rootroot00000000000000If you use BioBlend in your published work, please cite the following article: - Clare Sloggett, Nuwan Goonasekera, Enis Afgan "BioBlend: automating pipeline analyses within Galaxy and CloudMan" Bioinformatics (2013) 29(13):1685-1686 doi:10.1093/bioinformatics/btt199 BibTeX format: @article{10.1093/bioinformatics/btt199, author = {Sloggett, Clare and Goonasekera, Nuwan and Afgan, Enis}, doi = {10.1093/bioinformatics/btt199}, journal = {Bioinformatics}, number = {13}, pages = {1685-1686}, title = {{BioBlend: automating pipeline analyses within Galaxy and CloudMan}}, url = {https://doi.org/10.1093/bioinformatics/btt199}, volume = {29}, year = {2013}, } If you use BioBlend.objects in your published work, please cite the following article: - Simone Leo, Luca Pireddu, Gianmauro Cuccuru, Luca Lianas, Nicola Soranzo, Enis Afgan, Gianluigi Zanetti "BioBlend.objects: metacomputing with Galaxy" Bioinformatics (2014) 30(19):2816-2817 doi:10.1093/bioinformatics/btu386 BibTeX format: @article{10.1093/bioinformatics/btu386, author = {Leo, Simone and Pireddu, Luca and Cuccuru, Gianmauro and Lianas, Luca and Soranzo, Nicola and Afgan, Enis and Zanetti, Gianluigi}, doi = {10.1093/bioinformatics/btu386}, journal = {Bioinformatics}, number = {19}, pages = {2816-2817}, title = {{BioBlend.objects: metacomputing with Galaxy}}, url = {https://doi.org/10.1093/bioinformatics/btu386}, volume = {30}, year = {2014}, } bioblend-1.2.0/CODE_OF_CONDUCT.md000066400000000000000000000004451444761704300161040ustar00rootroot00000000000000Code of Conduct =============== As part of the Galaxy Community, this project is committed to providing a welcoming and harassment-free experience for everyone. We therefore expect participants to abide by our Code of Conduct, which can be found at: https://galaxyproject.org/community/coc/ bioblend-1.2.0/CONTRIBUTING.md000066400000000000000000000030151444761704300155320ustar00rootroot00000000000000Making a new release -------------------- 1. For a new major release, remove stuff (e.g. parameters, methods) deprecated in the previous cycle. 2. Update the `__version__` string in `bioblend/__init__.py` . 3. Update `CHANGELOG.md` . 4. Commit the changes above, push to GitHub, and wait for Continuous Integration (CI) tests to pass. 5. Make a new release through the GitHub interface. A CI job will automatically upload the packages to PyPI. 7. Check and merge the automatic pull request to update the [Bioconda package](https://github.com/bioconda/bioconda-recipes/blob/master/recipes/bioblend/meta.yaml). How to run BioBlend tests ------------------------- 1. Clone Galaxy to a directory outside of BioBlend source directory via `git clone https://github.com/galaxyproject/galaxy.git` 2. Change directory to your BioBlend source and run the tests via `./run_bioblend_tests.sh -g GALAXY_PATH [-r GALAXY_REV] [-e TOX_ENV]` where `GALAXY_PATH` is the directory where the galaxy repository was cloned, `GALAXY_REV` is the branch or commit of Galaxy that you would like to test against (if different from the current state of your galaxy clone), and `TOX_ENV` is used to specify the Python version to use for BioBlend, e.g. `py37` for Python 3.7. You can also add `2>&1 | tee log.txt` to the command above to contemporarily view the test output and save it to the `log.txt` file. 3. If needed, you can temporarily increase the Galaxy job timeout used by BioBlend tests with e.g. `export BIOBLEND_TEST_JOB_TIMEOUT=100`, and re-run the tests. bioblend-1.2.0/LICENSE000066400000000000000000000020641444761704300143110ustar00rootroot00000000000000MIT License Copyright (c) 2012-2023 Galaxy Project Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. bioblend-1.2.0/MANIFEST.in000066400000000000000000000002231444761704300150350ustar00rootroot00000000000000# Add non-Python files graft bioblend/_tests # Add documentation graft docs global-exclude *.swp *.pyc .gitignore include *.rst CITATION LICENSE bioblend-1.2.0/Makefile000066400000000000000000000013701444761704300147430ustar00rootroot00000000000000IN_VENV=. .venv/bin/activate .PHONY: clean release venv all: @echo "This makefile is used for the release process. A sensible all target is not implemented." clean: rm -rf bioblend.egg-info/ build/ dist/ make -C docs/ clean venv: # Create and activate a virtual environment [ -f .venv/bin/activate ] || virtualenv -p python3 .venv ( $(IN_VENV) && \ # Install latest versions of pip and setuptools \ python3 -m pip install --upgrade pip setuptools && \ # Install latest versions of other needed packages in the virtualenv \ python3 -m pip install --upgrade twine wheel \ ) release: clean venv ( $(IN_VENV) && \ # Create files in dist/ \ python3 setup.py sdist bdist_wheel && \ twine check dist/* && \ twine upload dist/* ) bioblend-1.2.0/README.rst000066400000000000000000000016261444761704300147760ustar00rootroot00000000000000.. image:: https://img.shields.io/pypi/v/bioblend.svg :target: https://pypi.org/project/bioblend/ :alt: latest version available on PyPI .. image:: https://readthedocs.org/projects/bioblend/badge/ :alt: Documentation Status :target: https://bioblend.readthedocs.io/ .. image:: https://badges.gitter.im/galaxyproject/bioblend.svg :alt: Join the chat at https://gitter.im/galaxyproject/bioblend :target: https://gitter.im/galaxyproject/bioblend?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge BioBlend is a Python library for interacting with the `Galaxy`_ API. BioBlend is supported and tested on: - Python 3.7 - 3.11 - Galaxy release 19.05 and later. Full docs are available at https://bioblend.readthedocs.io/ with a quick library overview also available in `ABOUT.rst <./ABOUT.rst>`_. .. References/hyperlinks used above .. _Galaxy: https://galaxyproject.org/ bioblend-1.2.0/bioblend/000077500000000000000000000000001444761704300150605ustar00rootroot00000000000000bioblend-1.2.0/bioblend/__init__.py000066400000000000000000000061651444761704300172010ustar00rootroot00000000000000import contextlib import logging import logging.config import os from typing import ( Optional, Union, ) from bioblend.config import ( BioBlendConfigLocations, Config, ) # Current version of the library __version__ = "1.2.0" # default chunk size (in bytes) for reading remote data try: import resource CHUNK_SIZE = resource.getpagesize() except Exception: CHUNK_SIZE = 4096 config = Config() def get_version() -> str: """ Returns a string with the current version of the library (e.g., "0.2.0") """ return __version__ def init_logging() -> None: """ Initialize BioBlend's logging from a configuration file. """ for config_file in BioBlendConfigLocations: with contextlib.suppress(Exception): logging.config.fileConfig(os.path.expanduser(config_file)) class NullHandler(logging.Handler): def emit(self, record: logging.LogRecord) -> None: pass # By default, do not force any logging by the library. If you want to see the # log messages in your scripts, add the following to the top of your script: # import logging # logging.basicConfig(filename="bioblend.log", level=logging.DEBUG) default_format_string = "%(asctime)s %(name)s [%(levelname)s]: %(message)s" log = logging.getLogger("bioblend") log.addHandler(NullHandler()) init_logging() # Convenience functions to set logging to a particular file or stream # To enable either of these, simply add the following at the top of a # bioblend module: # import bioblend # bioblend.set_stream_logger(__name__) def set_file_logger( name: str, filepath: str, level: Union[int, str] = logging.INFO, format_string: Optional[str] = None ) -> None: global log if not format_string: format_string = default_format_string logger = logging.getLogger(name) logger.setLevel(level) fh = logging.FileHandler(filepath) fh.setLevel(level) formatter = logging.Formatter(format_string) fh.setFormatter(formatter) logger.addHandler(fh) log = logger def set_stream_logger(name: str, level: Union[int, str] = logging.DEBUG, format_string: Optional[str] = None) -> None: global log if not format_string: format_string = default_format_string logger = logging.getLogger(name) logger.setLevel(level) fh = logging.StreamHandler() fh.setLevel(level) formatter = logging.Formatter(format_string) fh.setFormatter(formatter) logger.addHandler(fh) log = logger class ConnectionError(Exception): """ An exception class that is raised when unexpected HTTP responses come back. Should make it easier to debug when strange HTTP things happen such as a proxy server getting in the way of the request etc. @see: body attribute to see the content of the http response """ def __init__( self, message: str, body: Optional[Union[bytes, str]] = None, status_code: Optional[int] = None ) -> None: super().__init__(message) self.body = body self.status_code = status_code def __str__(self) -> str: return f"{self.args[0]}: {self.body!s}" class TimeoutException(Exception): pass bioblend-1.2.0/bioblend/_tests/000077500000000000000000000000001444761704300163615ustar00rootroot00000000000000bioblend-1.2.0/bioblend/_tests/GalaxyTestBase.py000066400000000000000000000035541444761704300216220ustar00rootroot00000000000000import os import unittest from typing import ( Any, Dict, ) from typing_extensions import Literal import bioblend from bioblend.galaxy import GalaxyInstance from . import test_util bioblend.set_stream_logger("test", level="INFO") BIOBLEND_TEST_JOB_TIMEOUT = int(os.environ.get("BIOBLEND_TEST_JOB_TIMEOUT", "60")) @test_util.skip_unless_galaxy() class GalaxyTestBase(unittest.TestCase): gi: GalaxyInstance @classmethod def setUpClass(cls): galaxy_key = os.environ["BIOBLEND_GALAXY_API_KEY"] galaxy_url = os.environ["BIOBLEND_GALAXY_URL"] cls.gi = GalaxyInstance(url=galaxy_url, key=galaxy_key) def _test_dataset(self, history_id: str, contents: str = "1\t2\t3", **kwargs: Any) -> str: tool_output = self.gi.tools.paste_content(contents, history_id, **kwargs) return tool_output["outputs"][0]["id"] def _wait_and_verify_dataset( self, dataset_id: str, expected_contents: bytes, timeout_seconds: float = BIOBLEND_TEST_JOB_TIMEOUT ) -> None: dataset_contents = self.gi.datasets.download_dataset(dataset_id, maxwait=timeout_seconds) assert dataset_contents == expected_contents def _run_random_lines1( self, history_id: str, dataset_id: str, input_format: Literal["21.01", "legacy"] = "legacy" ) -> Dict[str, Any]: tool_inputs = { "num_lines": "1", "input": {"src": "hda", "id": dataset_id}, } if input_format == "21.01": tool_inputs.update({"seed_source": {"seed_source_selector": "set_seed", "seed": "asdf"}}) else: # legacy format tool_inputs.update({"seed_source|seed_source_selector": "set_seed", "seed_source|seed": "asdf"}) return self.gi.tools.run_tool( history_id=history_id, tool_id="random_lines1", tool_inputs=tool_inputs, input_format=input_format ) bioblend-1.2.0/bioblend/_tests/README.TXT000066400000000000000000000013121444761704300177140ustar00rootroot00000000000000To run Galaxy tests, the following environment variables must be set: BIOBLEND_GALAXY_API_KEY = BIOBLEND_GALAXY_URL = To run ToolShed tests, the following environment variable must be set: BIOBLEND_TOOLSHED_URL = If you wish to run the entire suite, set all of the above. The integration tests can subsequently be run by invoking `pytest` from the command line. pytest should be invoked from the project root folder, and not the tests child folder, since the test data is resolved relative to the bioblend folder. bioblend-1.2.0/bioblend/_tests/TestGalaxyConfig.py000066400000000000000000000012721444761704300221500ustar00rootroot00000000000000from . import GalaxyTestBase class TestGalaxyConfig(GalaxyTestBase.GalaxyTestBase): def test_get_config(self): response = self.gi.config.get_config() assert isinstance(response, dict) assert "brand" in response.keys() def test_get_version(self): response = self.gi.config.get_version() assert isinstance(response, dict) assert "version_major" in response.keys() def test_whoami(self): response = self.gi.config.whoami() assert isinstance(response, dict) assert "username" in response.keys() def test_reload_toolbox(self): response = self.gi.config.reload_toolbox() assert response is None bioblend-1.2.0/bioblend/_tests/TestGalaxyDatasetCollections.py000066400000000000000000000233521444761704300245320ustar00rootroot00000000000000import os import tarfile import tempfile from inspect import signature from typing import ( Any, Dict, Union, ) from zipfile import ZipFile from bioblend.galaxy import dataset_collections from . import GalaxyTestBase class TestGalaxyDatasetCollections(GalaxyTestBase.GalaxyTestBase): def test_create_list_in_history(self): history_id = self.gi.histories.create_history(name="TestDSListCreate")["id"] dataset1_id = self._test_dataset(history_id) dataset2_id = self._test_dataset(history_id) dataset3_id = self._test_dataset(history_id) collection_response = self.gi.histories.create_dataset_collection( history_id=history_id, collection_description=dataset_collections.CollectionDescription( name="MyDatasetList", elements=[ dataset_collections.HistoryDatasetElement(name="sample1", id=dataset1_id), dataset_collections.HistoryDatasetElement(name="sample2", id=dataset2_id), dataset_collections.HistoryDatasetElement(name="sample3", id=dataset3_id), ], ), ) assert collection_response["name"] == "MyDatasetList" assert collection_response["collection_type"] == "list" elements = collection_response["elements"] assert len(elements) == 3 assert elements[0]["element_index"] == 0 assert elements[0]["object"]["id"] == dataset1_id assert elements[1]["object"]["id"] == dataset2_id assert elements[2]["object"]["id"] == dataset3_id assert elements[2]["element_identifier"] == "sample3" def test_create_list_of_paired_datasets_in_history(self): history_id = self.gi.histories.create_history(name="TestDSListCreate")["id"] dataset1_id = self._test_dataset(history_id) dataset2_id = self._test_dataset(history_id) dataset3_id = self._test_dataset(history_id) dataset4_id = self._test_dataset(history_id) collection_response = self.gi.histories.create_dataset_collection( history_id=history_id, collection_description=dataset_collections.CollectionDescription( name="MyListOfPairedDatasets", type="list:paired", elements=[ dataset_collections.CollectionElement( name="sample1", type="paired", elements=[ dataset_collections.HistoryDatasetElement(name="forward", id=dataset1_id), dataset_collections.HistoryDatasetElement(name="reverse", id=dataset2_id), ], ), dataset_collections.CollectionElement( name="sample2", type="paired", elements=[ dataset_collections.HistoryDatasetElement(name="forward", id=dataset3_id), dataset_collections.HistoryDatasetElement(name="reverse", id=dataset4_id), ], ), ], ), ) assert collection_response["name"] == "MyListOfPairedDatasets" assert collection_response["collection_type"] == "list:paired" elements = collection_response["elements"] assert len(elements) == 2 assert elements[0]["element_index"] == 0 created_pair1 = elements[0]["object"] assert created_pair1["collection_type"] == "paired" assert len(created_pair1["elements"]) == 2 forward_element1 = created_pair1["elements"][0] assert forward_element1["element_identifier"] == "forward" assert forward_element1["element_index"] == 0 forward_dataset1 = forward_element1["object"] assert forward_dataset1["id"] == dataset1_id assert elements[1]["element_index"] == 1 created_pair2 = elements[1]["object"] assert created_pair2["collection_type"] == "paired" assert len(created_pair2["elements"]) == 2 reverse_element2 = created_pair2["elements"][1] reverse_dataset2 = reverse_element2["object"] assert reverse_element2["element_identifier"] == "reverse" assert reverse_element2["element_index"] == 1 assert reverse_dataset2["id"] == dataset4_id def test_collections_in_history_index(self): history_id = self.gi.histories.create_history(name="TestHistoryDSIndex")["id"] history_dataset_collection = self._create_pair_in_history(history_id) contents = self.gi.histories.show_history(history_id, contents=True) assert len(contents) == 3 assert contents[2]["id"] == history_dataset_collection["id"] assert contents[2]["name"] == "MyTestPair" assert contents[2]["collection_type"] == "paired" def test_show_history_dataset_collection(self): history_id = self.gi.histories.create_history(name="TestHistoryDSIndexShow")["id"] history_dataset_collection = self._create_pair_in_history(history_id) show_response = self.gi.histories.show_dataset_collection(history_id, history_dataset_collection["id"]) for key in ["collection_type", "elements", "name", "deleted", "visible"]: assert key in show_response assert not show_response["deleted"] assert show_response["visible"] def test_delete_history_dataset_collection(self): history_id = self.gi.histories.create_history(name="TestHistoryDSDelete")["id"] history_dataset_collection = self._create_pair_in_history(history_id) self.gi.histories.delete_dataset_collection(history_id, history_dataset_collection["id"]) show_response = self.gi.histories.show_dataset_collection(history_id, history_dataset_collection["id"]) assert show_response["deleted"] def test_update_history_dataset_collection(self): history_id = self.gi.histories.create_history(name="TestHistoryDSDelete")["id"] history_dataset_collection = self._create_pair_in_history(history_id) self.gi.histories.update_dataset_collection(history_id, history_dataset_collection["id"], visible=False) show_response = self.gi.histories.show_dataset_collection(history_id, history_dataset_collection["id"]) assert not show_response["visible"] def test_show_dataset_collection(self): history_id = self.gi.histories.create_history(name="TestDatasetCollectionShow")["id"] dataset_collection1 = self._create_pair_in_history(history_id) dataset_collection2 = self.gi.dataset_collections.show_dataset_collection(dataset_collection1["id"]) for key in ( "collection_type", "deleted", "id", "hid", "history_content_type", "history_id", "name", "url", "visible", ): assert dataset_collection1[key] == dataset_collection2[key] for element1, element2 in zip(dataset_collection1["elements"], dataset_collection2["elements"]): assert element1["id"] == element2["id"] assert element1.keys() == element2.keys() for key in element1["object"].keys(): assert key in element2["object"].keys() def test_download_dataset_collection(self): history_id = self.gi.histories.create_history(name="TestDatasetCollectionDownload")["id"] dataset_collection_id = self._create_pair_in_history(history_id)["id"] self.gi.dataset_collections.wait_for_dataset_collection(dataset_collection_id) tempdir = tempfile.mkdtemp(prefix="bioblend_test_dataset_collection_download_") archive_path = os.path.join(tempdir, "dataset_collection") archive_type = self.gi.dataset_collections.download_dataset_collection( dataset_collection_id, file_path=archive_path )["archive_type"] expected_contents = signature(self._test_dataset).parameters["contents"].default + "\n" extract_dir_path = os.path.join(tempdir, "extracted_files") os.mkdir(extract_dir_path) if archive_type == "zip": archive: Union[ZipFile, tarfile.TarFile] = ZipFile(archive_path) elif archive_type == "tgz": archive = tarfile.open(archive_path) archive.extractall(extract_dir_path) for fname in os.listdir(extract_dir_path): dataset_dir_path = os.path.join(extract_dir_path, fname) file_path = os.path.join(dataset_dir_path, os.listdir(dataset_dir_path)[0]) with open(file_path) as f: assert expected_contents == f.read() archive.close() def test_wait_for_dataset_collection(self): history_id = self.gi.histories.create_history(name="TestDatasetCollectionWait")["id"] dataset_collection_id = self._create_pair_in_history(history_id)["id"] dataset_collection = self.gi.dataset_collections.wait_for_dataset_collection(dataset_collection_id) for element in dataset_collection["elements"]: assert element["object"]["state"] == "ok" def _create_pair_in_history(self, history_id: str) -> Dict[str, Any]: dataset1_id = self._test_dataset(history_id) dataset2_id = self._test_dataset(history_id) collection_response = self.gi.histories.create_dataset_collection( history_id=history_id, collection_description=dataset_collections.CollectionDescription( name="MyTestPair", type="paired", elements=[ dataset_collections.HistoryDatasetElement(name="forward", id=dataset1_id), dataset_collections.HistoryDatasetElement(name="reverse", id=dataset2_id), ], ), ) return collection_response bioblend-1.2.0/bioblend/_tests/TestGalaxyDatasets.py000066400000000000000000000247001444761704300225140ustar00rootroot00000000000000import shutil import tempfile import pytest from bioblend import ( ConnectionError, galaxy, ) from . import ( GalaxyTestBase, test_util, ) class TestGalaxyDatasets(GalaxyTestBase.GalaxyTestBase): def setUp(self): super().setUp() self.history_id = self.gi.histories.create_history(name="TestDataset")["id"] self.dataset_contents = "line 1\nline 2\rline 3\r\nline 4" self.dataset_id = self._test_dataset(self.history_id, contents=self.dataset_contents) self.gi.datasets.wait_for_dataset(self.dataset_id) def tearDown(self): self.gi.histories.delete_history(self.history_id, purge=True) @test_util.skip_unless_galaxy("release_19.05") def test_show_nonexistent_dataset(self): with pytest.raises(ConnectionError): self.gi.datasets.show_dataset("nonexistent_id") def test_show_dataset(self): self.gi.datasets.show_dataset(self.dataset_id) def test_download_dataset(self): with pytest.raises((TypeError, ConnectionError)): self.gi.datasets.download_dataset(None) # type: ignore[call-overload] expected_contents = ("\n".join(self.dataset_contents.splitlines()) + "\n").encode() # download_dataset() with file_path=None is already tested in TestGalaxyTools.test_paste_content() # self._wait_and_verify_dataset(self.dataset_id, expected_contents) tempdir = tempfile.mkdtemp(prefix="bioblend_test_") try: downloaded_dataset = self.gi.datasets.download_dataset( self.dataset_id, file_path=tempdir, maxwait=GalaxyTestBase.BIOBLEND_TEST_JOB_TIMEOUT * 2 ) assert downloaded_dataset.startswith(tempdir) with open(downloaded_dataset, "rb") as f: assert f.read() == expected_contents finally: shutil.rmtree(tempdir) with tempfile.NamedTemporaryFile(prefix="bioblend_test_") as f: download_filename = self.gi.datasets.download_dataset( self.dataset_id, file_path=f.name, use_default_filename=False, maxwait=GalaxyTestBase.BIOBLEND_TEST_JOB_TIMEOUT, ) assert download_filename == f.name f.flush() assert f.read() == expected_contents @test_util.skip_unless_galaxy("release_19.05") def test_get_datasets(self): datasets = self.gi.datasets.get_datasets() dataset_ids = [dataset["id"] for dataset in datasets] assert self.dataset_id in dataset_ids @test_util.skip_unless_galaxy("release_19.05") def test_get_datasets_history(self): datasets = self.gi.datasets.get_datasets(history_id=self.history_id) assert len(datasets) == 1 @test_util.skip_unless_galaxy("release_19.05") def test_get_datasets_limit_offset(self): datasets = self.gi.datasets.get_datasets(limit=1) assert len(datasets) == 1 datasets = self.gi.datasets.get_datasets(history_id=self.history_id, offset=1) assert datasets == [] @test_util.skip_unless_galaxy("release_19.05") def test_get_datasets_name(self): datasets = self.gi.datasets.get_datasets(history_id=self.history_id, name="Pasted Entry") assert len(datasets) == 1 datasets = self.gi.datasets.get_datasets(history_id=self.history_id, name="Wrong Name") assert datasets == [] @test_util.skip_unless_galaxy("release_20.05") def test_get_datasets_time(self): dataset = self.gi.datasets.show_dataset(self.dataset_id) ct = dataset["create_time"] datasets = self.gi.datasets.get_datasets(history_id=self.history_id, create_time_min=ct) assert len(datasets) == 1 datasets = self.gi.datasets.get_datasets(history_id=self.history_id, create_time_max=ct) assert len(datasets) == 1 datasets = self.gi.datasets.get_datasets(history_id=self.history_id, create_time_min="2100-01-01T00:00:00") assert datasets == [] datasets = self.gi.datasets.get_datasets(history_id=self.history_id, create_time_max="2000-01-01T00:00:00") assert datasets == [] ut = dataset["update_time"] datasets = self.gi.datasets.get_datasets(history_id=self.history_id, update_time_min=ut) assert len(datasets) == 1 datasets = self.gi.datasets.get_datasets(history_id=self.history_id, update_time_max=ut) assert len(datasets) == 1 datasets = self.gi.datasets.get_datasets(history_id=self.history_id, update_time_min="2100-01-01T00:00:00") assert datasets == [] datasets = self.gi.datasets.get_datasets(history_id=self.history_id, update_time_max="2000-01-01T00:00:00") assert datasets == [] @test_util.skip_unless_galaxy("release_20.05") def test_get_datasets_extension(self): datasets = self.gi.datasets.get_datasets(history_id=self.history_id, extension="txt") assert len(datasets) == 1 datasets = self.gi.datasets.get_datasets(history_id=self.history_id, extension="bam") assert datasets == [] @test_util.skip_unless_galaxy("release_22.01") def test_get_datasets_extension_list(self): datasets = self.gi.datasets.get_datasets(history_id=self.history_id, extension=["bam", "txt"]) assert len(datasets) == 1 @test_util.skip_unless_galaxy("release_20.05") def test_get_datasets_state(self): datasets = self.gi.datasets.get_datasets(history_id=self.history_id, state="ok") assert len(datasets) == 1 datasets = self.gi.datasets.get_datasets(history_id=self.history_id, state="queued") assert datasets == [] with pytest.raises(ConnectionError): self.gi.datasets.get_datasets(history_id=self.history_id, state="nonexistent_state") datasets = self.gi.datasets.get_datasets(history_id=self.history_id, state=["ok", "queued"]) assert len(datasets) == 1 @test_util.skip_unless_galaxy("release_20.05") def test_get_datasets_visible(self): datasets = self.gi.datasets.get_datasets(history_id=self.history_id, visible=True) assert len(datasets) == 1 datasets = self.gi.datasets.get_datasets(history_id=self.history_id, visible=False) assert len(datasets) == 0 @test_util.skip_unless_galaxy("release_19.05") def test_get_datasets_ordering(self): self.dataset_id2 = self._test_dataset(self.history_id, contents=self.dataset_contents) self.gi.datasets.wait_for_dataset(self.dataset_id2) datasets = self.gi.datasets.get_datasets(history_id=self.history_id, order="create_time-dsc") assert datasets[0]["id"] == self.dataset_id2 datasets = self.gi.datasets.get_datasets(history_id=self.history_id, order="create_time-asc") assert datasets[0]["id"] == self.dataset_id datasets = self.gi.datasets.get_datasets(history_id=self.history_id, order="hid-dsc") assert datasets[0]["id"] == self.dataset_id2 datasets = self.gi.datasets.get_datasets(history_id=self.history_id, order="hid-asc") assert datasets[0]["id"] == self.dataset_id @test_util.skip_unless_galaxy("release_19.05") def test_get_datasets_deleted(self): deleted_datasets = self.gi.datasets.get_datasets(history_id=self.history_id, deleted=True) assert deleted_datasets == [] self.gi.histories.delete_dataset(self.history_id, self.dataset_id) deleted_datasets = self.gi.datasets.get_datasets(history_id=self.history_id, deleted=True) assert len(deleted_datasets) == 1 purged_datasets = self.gi.datasets.get_datasets(history_id=self.history_id, purged=True) assert purged_datasets == [] self.gi.histories.delete_dataset(self.history_id, self.dataset_id, purge=True) purged_datasets = self.gi.datasets.get_datasets(history_id=self.history_id, purged=True) assert len(purged_datasets) == 1 @test_util.skip_unless_galaxy("release_19.05") def test_get_datasets_tool_id_and_tag(self): cat1_datasets = self.gi.datasets.get_datasets(history_id=self.history_id, tool_id="cat1") assert cat1_datasets == [] upload1_datasets = self.gi.datasets.get_datasets(history_id=self.history_id, tool_id="upload1") assert len(upload1_datasets) == 1 self.gi.histories.update_dataset(self.history_id, self.dataset_id, tags=["test"]) tagged_datasets = self.gi.datasets.get_datasets(history_id=self.history_id, tag="test") assert len(tagged_datasets) == 1 def test_wait_for_dataset(self): history_id = self.gi.histories.create_history(name="TestWaitForDataset")["id"] dataset_contents = "line 1\nline 2\rline 3\r\nline 4" dataset_id = self._test_dataset(history_id, contents=dataset_contents) dataset = self.gi.datasets.wait_for_dataset(dataset_id) assert dataset["state"] == "ok" self.gi.histories.delete_history(history_id, purge=True) @test_util.skip_unless_galaxy("release_19.05") def test_dataset_permissions(self): admin_user_id = self.gi.users.get_current_user()["id"] username = test_util.random_string() user_id = self.gi.users.create_local_user(username, f"{username}@example.org", test_util.random_string(20))[ "id" ] user_api_key = self.gi.users.create_user_apikey(user_id) anonymous_gi = galaxy.GalaxyInstance(url=self.gi.base_url, key=None) user_gi = galaxy.GalaxyInstance(url=self.gi.base_url, key=user_api_key) sharing_role = self.gi.roles.create_role("sharing_role", "sharing_role", [user_id, admin_user_id])["id"] self.gi.datasets.publish_dataset(self.dataset_id, published=False) with pytest.raises(ConnectionError): anonymous_gi.datasets.show_dataset(self.dataset_id) self.gi.datasets.publish_dataset(self.dataset_id, published=True) # now dataset is public, i.e. accessible to anonymous users assert anonymous_gi.datasets.show_dataset(self.dataset_id)["id"] == self.dataset_id self.gi.datasets.publish_dataset(self.dataset_id, published=False) with pytest.raises(ConnectionError): user_gi.datasets.show_dataset(self.dataset_id) self.gi.datasets.update_permissions(self.dataset_id, access_ids=[sharing_role], manage_ids=[sharing_role]) assert user_gi.datasets.show_dataset(self.dataset_id)["id"] == self.dataset_id # anonymous access now fails because sharing is only with the shared user role with pytest.raises(ConnectionError): anonymous_gi.datasets.show_dataset(self.dataset_id) bioblend-1.2.0/bioblend/_tests/TestGalaxyFolders.py000066400000000000000000000045741444761704300223510ustar00rootroot00000000000000from typing import ( Dict, List, ) from . import GalaxyTestBase FOO_DATA = "foo\nbar\n" class TestGalaxyFolders(GalaxyTestBase.GalaxyTestBase): def setUp(self): super().setUp() self.name = "automated test folder" self.library = self.gi.libraries.create_library( self.name, description="automated test", synopsis="automated test synopsis" ) self.folder = self.gi.folders.create_folder( self.library["root_folder_id"], self.name, description="automatically created folder" ) def tearDown(self): self.gi.libraries.delete_library(self.library["id"]) def test_create_folder(self): assert self.folder["name"] == self.name assert self.folder["description"] == "automatically created folder" def test_show_folder(self): f2 = self.gi.folders.show_folder(self.folder["id"]) assert f2["id"] == self.folder["id"] def test_show_folder_contents(self): f2 = self.gi.folders.show_folder(self.folder["id"], contents=True) assert "folder_contents" in f2 assert "metadata" in f2 assert self.name == f2["metadata"]["folder_name"] def test_delete_folder(self): self.sub_folder = self.gi.folders.create_folder(self.folder["id"], self.name) self.gi.folders.delete_folder(self.sub_folder["id"]) def test_update_folder(self): self.folder = self.gi.folders.update_folder(self.folder["id"], "new-name", "new-description") assert self.folder["name"] == "new-name" assert self.folder["description"] == "new-description" def test_get_set_permissions(self): empty_permission: Dict[str, List] = { "add_library_item_role_list": [], "modify_folder_role_list": [], "manage_folder_role_list": [], } # They should be empty to start with assert self.gi.folders.get_permissions(self.folder["id"], scope="current") == empty_permission assert self.gi.folders.get_permissions(self.folder["id"], scope="available") == empty_permission # Then we'll add a role role = self.gi.roles.get_roles()[0] self.gi.folders.set_permissions(self.folder["id"], add_ids=[role["id"]]) assert ( role["id"] in self.gi.folders.get_permissions(self.folder["id"], scope="available")["add_library_item_role_list"][0] ) bioblend-1.2.0/bioblend/_tests/TestGalaxyGroups.py000066400000000000000000000047041444761704300222250ustar00rootroot00000000000000""" WARNING: only admins can operate on groups! """ import uuid from . import GalaxyTestBase class TestGalaxyGroups(GalaxyTestBase.GalaxyTestBase): def setUp(self): super().setUp() self.name = f"test_{uuid.uuid4().hex}" self.group = self.gi.groups.create_group(self.name)[0] def tearDown(self): # As of 2015/04/13, deleting a group is not possible through the API pass def test_create_group(self): assert self.group["name"] == self.name assert self.group["id"] is not None def test_get_groups(self): groups = self.gi.groups.get_groups() for group in groups: assert group["id"] is not None assert group["name"] is not None def test_show_group(self): group_data = self.gi.groups.show_group(self.group["id"]) assert self.group["id"] == group_data["id"] assert self.group["name"] == group_data["name"] def test_get_group_users(self): group_users = self.gi.groups.get_group_users(self.group["id"]) assert group_users == [] def test_get_group_roles(self): group_roles = self.gi.groups.get_group_roles(self.group["id"]) assert group_roles == [] def test_update_group(self): new_name = f"test_{uuid.uuid4().hex}" new_users = [self.gi.users.get_current_user()["id"]] self.gi.groups.update_group(self.group["id"], new_name, user_ids=new_users) updated_group = self.gi.groups.show_group(self.group["id"]) assert self.group["id"] == updated_group["id"] assert updated_group["name"] == new_name updated_group_users = [_["id"] for _ in self.gi.groups.get_group_users(self.group["id"])] assert set(updated_group_users) == set(new_users) updated_group_roles = [_["id"] for _ in self.gi.groups.get_group_roles(self.group["id"])] assert set(updated_group_roles) == set() def test_add_delete_group_user(self): new_user = self.gi.users.get_current_user()["id"] ret = self.gi.groups.add_group_user(self.group["id"], new_user) assert ret["id"] == new_user updated_group_users = [_["id"] for _ in self.gi.groups.get_group_users(self.group["id"])] assert new_user in updated_group_users self.gi.groups.delete_group_user(self.group["id"], new_user) updated_group_users = [_["id"] for _ in self.gi.groups.get_group_users(self.group["id"])] assert new_user not in updated_group_users bioblend-1.2.0/bioblend/_tests/TestGalaxyHistories.py000066400000000000000000000305261444761704300227200ustar00rootroot00000000000000""" """ import os import shutil import tarfile import tempfile import pytest from bioblend import ( ConnectionError, galaxy, ) from . import ( GalaxyTestBase, test_util, ) class TestGalaxyHistories(GalaxyTestBase.GalaxyTestBase): def setUp(self): super().setUp() self.default_history_name = "buildbot - automated test" self.history = self.gi.histories.create_history(name=self.default_history_name) def test_create_history(self): history_name = "another buildbot - automated test" new_history = self.gi.histories.create_history(name=history_name) assert new_history["id"] is not None assert new_history["name"] == history_name assert new_history["url"] is not None def test_update_history(self): new_name = "buildbot - automated test renamed" new_annotation = f"Annotation for {new_name}" new_tags = ["tag1", "tag2"] updated_hist = self.gi.histories.update_history( self.history["id"], name=new_name, annotation=new_annotation, tags=new_tags ) if "id" not in updated_hist: updated_hist = self.gi.histories.show_history(self.history["id"]) assert self.history["id"] == updated_hist["id"] assert updated_hist["name"] == new_name assert updated_hist["annotation"] == new_annotation assert updated_hist["tags"] == new_tags def test_publish_history(self): # Verify that searching for published histories does not return the test history published_histories = self.gi.histories.get_histories(published=True) assert not any(h["id"] == self.history["id"] for h in published_histories) updated_hist = self.gi.histories.update_history(self.history["id"], published=True) if "id" not in updated_hist: updated_hist = self.gi.histories.show_history(self.history["id"]) assert self.history["id"] == updated_hist["id"] assert updated_hist["published"] # Verify that searching for published histories now returns the test history published_histories = self.gi.histories.get_histories(published=True) assert any(h["id"] == self.history["id"] for h in published_histories) # Verify that get_published_histories as an anonymous user also returns the test history anonymous_gi = galaxy.GalaxyInstance(url=self.gi.base_url, key=None) published_histories = anonymous_gi.histories.get_published_histories() assert any(h["id"] == self.history["id"] for h in published_histories) history_from_slug = anonymous_gi.histories.get_published_histories(slug=updated_hist["slug"]) assert len(history_from_slug) == 1 assert self.history["id"] == history_from_slug[0]["id"] def test_get_histories(self): # Make sure there's at least one value - the one we created all_histories = self.gi.histories.get_histories() assert len(all_histories) > 0 # Check whether id is present, when searched by name histories = self.gi.histories.get_histories(name=self.default_history_name) assert len([h for h in histories if h["id"] == self.history["id"]]) == 1 # TODO: check whether deleted history is returned correctly # At the moment, get_histories() returns only not-deleted histories # and get_histories(deleted=True) returns only deleted histories, # so they are not comparable. # In the future, according to https://trello.com/c/MoilsmVv/1673-api-incoherent-and-buggy-indexing-of-deleted-entities , # get_histories() will return both not-deleted and deleted histories # and we can uncomment the following test. # deleted_history = self.gi.histories.get_histories(deleted=True) # assert len(all_histories) >= len(deleted_history) @test_util.skip_unless_galaxy("release_20.01") def test_other_users_histories(self): username = test_util.random_string() user_id = self.gi.users.create_local_user(username, f"{username}@example.org", test_util.random_string(20))[ "id" ] user_api_key = self.gi.users.create_user_apikey(user_id) user_gi = galaxy.GalaxyInstance(url=self.gi.base_url, key=user_api_key) # Normal users cannot use the `all` parameter with pytest.raises(ConnectionError): other_user_histories = user_gi.histories.get_histories(all=True) user_history_id = user_gi.histories.create_history(name=f"History for {username}")["id"] # Get all users' histories from an admin account other_user_histories = self.gi.histories.get_histories(all=True) assert user_history_id in [h["id"] for h in other_user_histories] def test_show_history(self): history_data = self.gi.histories.show_history(self.history["id"]) assert self.history["id"] == history_data["id"] assert self.history["name"] == history_data["name"] assert "new" == history_data["state"] def test_show_history_with_contents(self): history_id = self.history["id"] contents = self.gi.histories.show_history(history_id, contents=True) # Empty history has no datasets, content length should be 0 assert len(contents) == 0 self._test_dataset(history_id) contents = self.gi.histories.show_history(history_id, contents=True) # history has 1 dataset, content length should be 1 assert len(contents) == 1 contents = self.gi.histories.show_history(history_id, contents=True, types=["dataset"]) # filtering for dataset, content length should still be 1 assert len(contents) == 1 contents = self.gi.histories.show_history(history_id, contents=True, types=["dataset_collection"]) # filtering for dataset collection but there's no collection in the history assert len(contents) == 0 contents = self.gi.histories.show_history(history_id, contents=True, types=["dataset", "dataset_collection"]) assert len(contents) == 1 def test_create_history_tag(self): new_tag = "tag1" self.gi.histories.create_history_tag(self.history["id"], new_tag) updated_hist = self.gi.histories.show_history(self.history["id"]) assert self.history["id"] == updated_hist["id"] assert new_tag in updated_hist["tags"] def test_show_dataset(self): history_id = self.history["id"] dataset1_id = self._test_dataset(history_id) dataset = self.gi.histories.show_dataset(history_id, dataset1_id) for key in ["name", "hid", "id", "deleted", "history_id", "visible"]: assert key in dataset assert dataset["history_id"] == history_id assert dataset["hid"] == 1 assert dataset["id"] == dataset1_id assert not dataset["deleted"] assert dataset["visible"] @test_util.skip_unless_galaxy("release_22.01") def test_show_dataset_provenance(self) -> None: MINIMAL_PROV_KEYS = ("id", "uuid") OTHER_PROV_KEYS = ("job_id", "parameters", "stderr", "stdout", "tool_id") ALL_PROV_KEYS = MINIMAL_PROV_KEYS + OTHER_PROV_KEYS history_id = self.history["id"] dataset1_id = self._test_dataset(history_id) dataset2_id = self._run_random_lines1(history_id, dataset1_id)["outputs"][0]["id"] prov = self.gi.histories.show_dataset_provenance(history_id, dataset2_id) for key in ALL_PROV_KEYS: assert key in prov for key in MINIMAL_PROV_KEYS: assert key in prov["parameters"]["input"] for key in OTHER_PROV_KEYS: assert key not in prov["parameters"]["input"] recursive_prov = self.gi.histories.show_dataset_provenance(history_id, dataset2_id, follow=True) for key in ALL_PROV_KEYS: assert key in recursive_prov for key in ALL_PROV_KEYS: assert key in recursive_prov["parameters"]["input"] def test_delete_dataset(self): history_id = self.history["id"] dataset1_id = self._test_dataset(history_id) self.gi.histories.delete_dataset(history_id, dataset1_id) dataset = self.gi.histories.show_dataset(history_id, dataset1_id) assert dataset["deleted"] assert not dataset["purged"] def test_purge_dataset(self): history_id = self.history["id"] dataset1_id = self._test_dataset(history_id) self.gi.histories.delete_dataset(history_id, dataset1_id, purge=True) dataset = self.gi.histories.show_dataset(history_id, dataset1_id) assert dataset["deleted"] assert dataset["purged"] def test_update_dataset(self): history_id = self.history["id"] dataset1_id = self._test_dataset(history_id) updated_dataset = self.gi.histories.update_dataset(history_id, dataset1_id, visible=False) if "id" not in updated_dataset: updated_dataset = self.gi.histories.show_dataset(history_id, dataset1_id) assert not updated_dataset["visible"] def test_upload_dataset_from_library(self): pass # download_dataset() is already tested in TestGalaxyDatasets def test_delete_history(self): result = self.gi.histories.delete_history(self.history["id"]) assert result["deleted"] all_histories = self.gi.histories.get_histories() assert not any(d["id"] == self.history["id"] for d in all_histories) def test_undelete_history(self): self.gi.histories.delete_history(self.history["id"]) self.gi.histories.undelete_history(self.history["id"]) all_histories = self.gi.histories.get_histories() assert any(d["id"] == self.history["id"] for d in all_histories) def test_get_status(self): state = self.gi.histories.get_status(self.history["id"]) assert "new" == state["state"] def test_get_most_recently_used_history(self): most_recently_used_history = self.gi.histories.get_most_recently_used_history() # if the user has been created via the API, it does not have # a session, therefore no history if most_recently_used_history is not None: assert most_recently_used_history["id"] is not None assert most_recently_used_history["name"] is not None assert most_recently_used_history["state"] is not None def test_download_history(self): jeha_id = self.gi.histories.export_history(self.history["id"], wait=True, maxwait=60) assert jeha_id tempdir = tempfile.mkdtemp(prefix="bioblend_test_") temp_fn = os.path.join(tempdir, "export.tar.gz") try: with open(temp_fn, "wb") as fo: self.gi.histories.download_history(self.history["id"], jeha_id, fo) assert tarfile.is_tarfile(temp_fn) finally: shutil.rmtree(tempdir) def test_import_history(self): path = test_util.get_abspath(os.path.join("data", "Galaxy-History-Test-history-for-export.tar.gz")) self.gi.histories.import_history(file_path=path) def test_copy_dataset(self): history_id = self.history["id"] contents = "1\t2\t3" dataset1_id = self._test_dataset(history_id, contents=contents) self.history_id2 = self.gi.histories.create_history("TestCopyDataset")["id"] copied_dataset = self.gi.histories.copy_dataset(self.history_id2, dataset1_id) expected_contents = ("\n".join(contents.splitlines()) + "\n").encode() self._wait_and_verify_dataset(copied_dataset["id"], expected_contents) self.gi.histories.delete_history(self.history_id2, purge=True) @test_util.skip_unless_galaxy("release_20.09") def test_update_dataset_datatype(self): history_id = self.history["id"] dataset1_id = self._test_dataset(history_id) self._wait_and_verify_dataset(dataset1_id, b"1\t2\t3\n") original_hda = self.gi.datasets.show_dataset(dataset1_id) assert original_hda["extension"] == "bed" self.gi.histories.update_dataset(history_id, dataset1_id, datatype="tabular") updated_hda = self.gi.datasets.show_dataset(dataset1_id) assert updated_hda["extension"] == "tabular" def test_get_extra_files(self): history_id = self.history["id"] dataset_id = self._test_dataset(history_id) extra_files = self.gi.histories.get_extra_files(history_id, dataset_id) assert extra_files == [] def tearDown(self): self.gi.histories.delete_history(self.history["id"], purge=True) bioblend-1.2.0/bioblend/_tests/TestGalaxyInstance.py000066400000000000000000000040741444761704300225120ustar00rootroot00000000000000""" Tests on the GalaxyInstance object itself. """ import os import time import unittest import pytest from bioblend import ConnectionError from bioblend.galaxy import GalaxyInstance from . import test_util class TestGalaxyInstance(unittest.TestCase): def setUp(self): # "connect" to a fake Galaxy instance self.gi = GalaxyInstance("http://localhost:56789", key="whatever") def test_url_attribute(self): assert self.gi.base_url == "http://localhost:56789" assert self.gi.url == "http://localhost:56789/api" # Test instance served at a subdirectory gi = GalaxyInstance("http://localhost:56789/galaxy/", key="whatever") assert gi.base_url == "http://localhost:56789/galaxy" assert gi.url == "http://localhost:56789/galaxy/api" def test_set_max_get_attempts(self): self.gi.max_get_attempts = 3 assert 3 == self.gi.max_get_attempts def test_set_retry_delay(self): self.gi.get_retry_delay = 5.0 assert 5.0 == self.gi.get_retry_delay def test_get_retry(self): # We set the client to try twice, with a delay of 5 seconds between # attempts. So, we expect the call to take at least 5 seconds before # failing. self.gi.max_get_attempts = 3 self.gi.get_retry_delay = 2 start = time.time() with pytest.raises(ConnectionError): self.gi.libraries.get_libraries() end = time.time() duration = end - start assert duration > self.gi.get_retry_delay * (self.gi.max_get_attempts - 1), "Didn't seem to retry long enough" def test_missing_scheme_fake_url(self): with pytest.raises(ValueError): GalaxyInstance("localhost:56789", key="whatever") @test_util.skip_unless_galaxy() def test_missing_scheme_real_url(self): galaxy_url = os.environ["BIOBLEND_GALAXY_URL"] # Strip the scheme from galaxy_url scheme_sep = "://" if scheme_sep in galaxy_url: galaxy_url = galaxy_url.partition(scheme_sep)[2] GalaxyInstance(url=galaxy_url) bioblend-1.2.0/bioblend/_tests/TestGalaxyInvocations.py000066400000000000000000000153261444761704300232440ustar00rootroot00000000000000import contextlib import os import time from typing import ( Any, Dict, ) from . import ( GalaxyTestBase, test_util, ) class TestGalaxyInvocations(GalaxyTestBase.GalaxyTestBase): def setUp(self): super().setUp() path = test_util.get_abspath(os.path.join("data", "paste_columns.ga")) self.workflow_id = self.gi.workflows.import_workflow_from_local_path(path)["id"] self.history_id = self.gi.histories.create_history(name="TestGalaxyInvocations")["id"] self.dataset_id = self._test_dataset(self.history_id) def tearDown(self): self.gi.histories.delete_history(self.history_id, purge=True) @test_util.skip_unless_galaxy("release_19.09") def test_cancel_invocation(self): invocation = self._invoke_workflow() invocation_id = invocation["id"] invocations = self.gi.invocations.get_invocations() assert len(invocations) == 1 assert invocations[0]["id"] == invocation_id self.gi.invocations.cancel_invocation(invocation_id) invocation = self.gi.invocations.show_invocation(invocation_id) assert invocation["state"] == "cancelled" @test_util.skip_unless_galaxy("release_20.01") def test_get_invocations(self): invoc1 = self._invoke_workflow() # Run the first workflow on another history dataset = {"src": "hda", "id": self.dataset_id} hist2_id = self.gi.histories.create_history("hist2")["id"] invoc2 = self.gi.workflows.invoke_workflow( self.workflow_id, history_id=hist2_id, inputs={"Input 1": dataset, "Input 2": dataset}, inputs_by="name" ) # Run another workflow on the 2nd history path = test_util.get_abspath(os.path.join("data", "paste_columns.ga")) workflow2_id = self.gi.workflows.import_workflow_from_local_path(path)["id"] invoc3 = self.gi.workflows.invoke_workflow( workflow2_id, history_id=hist2_id, inputs={"Input 1": dataset, "Input 2": dataset}, inputs_by="name" ) for invoc in (invoc1, invoc2, invoc3): self.gi.invocations.wait_for_invocation(invoc["id"]) # Test filtering by workflow ID for wf_id, expected_invoc_num in {self.workflow_id: 2, workflow2_id: 1}.items(): invocs = self.gi.invocations.get_invocations(workflow_id=wf_id) assert len(invocs) == expected_invoc_num for invoc in invocs: assert invoc["workflow_id"] == wf_id # Test filtering by history ID for hist_id, expected_invoc_num in {self.history_id: 1, hist2_id: 2}.items(): invocs = self.gi.invocations.get_invocations(history_id=hist_id) assert len(invocs) == expected_invoc_num for invoc in invocs: assert invoc["history_id"] == hist_id # Test limiting limit_invocs = self.gi.invocations.get_invocations(limit=2) assert len(limit_invocs) == 2 self.gi.histories.delete_history(hist2_id, purge=True) @test_util.skip_unless_galaxy("release_19.09") def test_get_invocation_report(self): invocation = self._invoke_workflow() invocation_id = invocation["id"] workflow_id = invocation["workflow_id"] report = self.gi.invocations.get_invocation_report(invocation_id) assert report["workflows"] == {workflow_id: {"name": "paste_columns"}} with contextlib.suppress(Exception): # This can fail if dependencies as weasyprint are not installed on the Galaxy server ret = self.gi.invocations.get_invocation_report_pdf(invocation_id, "report.pdf") assert ret is None @test_util.skip_unless_galaxy("release_20.09") def test_get_invocation_biocompute_object(self): invocation = self._invoke_workflow() self.gi.invocations.wait_for_invocation(invocation["id"]) biocompute_object = self.gi.invocations.get_invocation_biocompute_object(invocation["id"]) assert len(biocompute_object["description_domain"]["pipeline_steps"]) == 1 @test_util.skip_unless_galaxy("release_19.09") def test_get_invocation_jobs_summary(self): invocation = self._invoke_workflow() self.gi.invocations.wait_for_invocation(invocation["id"]) jobs_summary = self.gi.invocations.get_invocation_summary(invocation["id"]) assert jobs_summary["populated_state"] == "ok" step_jobs_summary = self.gi.invocations.get_invocation_step_jobs_summary(invocation["id"]) assert len(step_jobs_summary) == 1 assert step_jobs_summary[0]["populated_state"] == "ok" @test_util.skip_unless_galaxy("release_19.09") @test_util.skip_unless_tool("cat1") @test_util.skip_unless_tool("cat") def test_workflow_scheduling(self): path = test_util.get_abspath(os.path.join("data", "test_workflow_pause.ga")) workflow = self.gi.workflows.import_workflow_from_local_path(path) invocation = self.gi.workflows.invoke_workflow( workflow["id"], inputs={"0": {"src": "hda", "id": self.dataset_id}}, history_id=self.history_id, ) invocation_id = invocation["id"] def invocation_steps_by_order_index() -> Dict[int, Dict[str, Any]]: invocation = self.gi.invocations.show_invocation(invocation_id) return {s["order_index"]: s for s in invocation["steps"]} for _ in range(20): if 2 in invocation_steps_by_order_index(): break time.sleep(0.5) steps = invocation_steps_by_order_index() pause_step = steps[2] assert self.gi.invocations.show_invocation_step(invocation_id, pause_step["id"])["action"] is None self.gi.invocations.run_invocation_step_action(invocation_id, pause_step["id"], action=True) assert self.gi.invocations.show_invocation_step(invocation_id, pause_step["id"])["action"] self.gi.invocations.wait_for_invocation(invocation["id"]) @test_util.skip_unless_galaxy("release_21.01") def test_rerun_invocation(self): invocation = self._invoke_workflow() self.gi.invocations.wait_for_invocation(invocation["id"]) rerun_invocation = self.gi.invocations.rerun_invocation(invocation["id"], import_inputs_to_history=True) self.gi.invocations.wait_for_invocation(rerun_invocation["id"]) history = self.gi.histories.show_history(rerun_invocation["history_id"], contents=True) assert len(history) == 3 def _invoke_workflow(self) -> Dict[str, Any]: dataset = {"src": "hda", "id": self.dataset_id} return self.gi.workflows.invoke_workflow( self.workflow_id, inputs={"Input 1": dataset, "Input 2": dataset}, history_id=self.history_id, inputs_by="name", ) bioblend-1.2.0/bioblend/_tests/TestGalaxyJobs.py000066400000000000000000000241241444761704300216410ustar00rootroot00000000000000import os from datetime import ( datetime, timedelta, ) from operator import itemgetter from typing_extensions import Literal from bioblend.galaxy.tools.inputs import ( dataset, inputs, ) from . import ( GalaxyTestBase, test_util, ) class TestGalaxyJobs(GalaxyTestBase.GalaxyTestBase): def setUp(self): super().setUp() self.history_id = self.gi.histories.create_history(name="TestGalaxyJobs")["id"] self.dataset_contents = "line 1\nline 2\rline 3\r\nline 4" self.dataset_id = self._test_dataset(self.history_id, contents=self.dataset_contents) def tearDown(self): self.gi.histories.delete_history(self.history_id, purge=True) @test_util.skip_unless_tool("cat1") def test_wait_for_job(self): tool_inputs = inputs().set("input1", dataset(self.dataset_id)) tool_output = self.gi.tools.run_tool(history_id=self.history_id, tool_id="cat1", tool_inputs=tool_inputs) job_id = tool_output["jobs"][0]["id"] job = self.gi.jobs.wait_for_job(job_id) assert job["state"] == "ok" @test_util.skip_unless_tool("random_lines1") def test_get_jobs(self): self._run_tool() self._run_tool() jobs = self.gi.jobs.get_jobs(tool_id="random_lines1", history_id=self.history_id) assert len(jobs) == 2 jobs = self.gi.jobs.get_jobs(history_id=self.history_id, state="failed") assert len(jobs) == 0 yesterday = datetime.today() - timedelta(days=1) jobs = self.gi.jobs.get_jobs(date_range_max=yesterday.strftime("%Y-%m-%d"), history_id=self.history_id) assert len(jobs) == 0 tomorrow = datetime.today() + timedelta(days=1) jobs = self.gi.jobs.get_jobs(date_range_min=tomorrow.strftime("%Y-%m-%d")) assert len(jobs) == 0 jobs = self.gi.jobs.get_jobs(date_range_min=datetime.today().strftime("%Y-%m-%d"), history_id=self.history_id) assert len(jobs) == 3 @test_util.skip_unless_galaxy("release_21.05") def test_get_jobs_with_filtering(self): path = test_util.get_abspath(os.path.join("data", "paste_columns.ga")) workflow_id = self.gi.workflows.import_workflow_from_local_path(path)["id"] dataset = {"src": "hda", "id": self.dataset_id} invocation1 = self.gi.workflows.invoke_workflow( workflow_id, inputs={"Input 1": dataset, "Input 2": dataset}, history_id=self.history_id, inputs_by="name", ) invocation2 = self.gi.workflows.invoke_workflow( workflow_id, inputs={"Input 1": dataset, "Input 2": dataset}, history_id=self.history_id, inputs_by="name", ) self.gi.invocations.wait_for_invocation(invocation1["id"]) self.gi.invocations.wait_for_invocation(invocation2["id"]) all_jobs = self.gi.jobs.get_jobs(history_id=self.history_id, order_by="create_time") assert len(all_jobs) == 3 job1_id = all_jobs[1]["id"] jobs = self.gi.jobs.get_jobs(history_id=self.history_id, limit=1, offset=1, order_by="create_time") assert len(jobs) == 1 assert jobs[0]["id"] == job1_id jobs = self.gi.jobs.get_jobs(invocation_id=invocation1["id"]) assert len(jobs) == 1 job_id_inv = jobs[0]["id"] jobs = self.gi.jobs.get_jobs(workflow_id=workflow_id) assert len(jobs) == 2 assert job_id_inv in [job["id"] for job in jobs] @test_util.skip_unless_galaxy("release_21.01") @test_util.skip_unless_tool("random_lines1") def test_run_and_rerun_random_lines(self): original_output = self._run_tool(input_format="21.01") original_job_id = original_output["jobs"][0]["id"] rerun_output = self.gi.jobs.rerun_job(original_job_id) original_output_content = self.gi.datasets.download_dataset(original_output["outputs"][0]["id"]) rerun_output_content = self.gi.datasets.download_dataset(rerun_output["outputs"][0]["id"]) assert rerun_output_content == original_output_content @test_util.skip_unless_galaxy("release_21.01") @test_util.skip_unless_tool("Show beginning1") def test_rerun_and_remap(self): path = test_util.get_abspath(os.path.join("data", "select_first.ga")) wf = self.gi.workflows.import_workflow_from_local_path(path) wf_inputs = { "0": {"src": "hda", "id": self.dataset_id}, "1": "-1", } invocation_id = self.gi.workflows.invoke_workflow(wf["id"], inputs=wf_inputs, history_id=self.history_id)["id"] invocation = self.gi.invocations.wait_for_invocation(invocation_id) job_steps = [step for step in invocation["steps"] if step["job_id"]] job_steps.sort(key=itemgetter("order_index")) try: self.gi.jobs.wait_for_job(job_steps[0]["job_id"]) except Exception: pass # indicates the job failed as expected else: raise Exception("The job should have failed") history_contents = self.gi.histories.show_history(self.history_id, contents=True) assert len(history_contents) == 3 assert history_contents[1]["state"] == "error" assert history_contents[2]["state"] == "paused" # resume the paused step job resumed_outputs = self.gi.jobs.resume_job(job_steps[-1]["job_id"]) assert resumed_outputs[0]["name"] == "out_file1" # the following does not pass stably - the job goes back to paused too quickly # history_contents_resumed = self.gi.histories.show_history(self.history_id, contents=True) # assert history_contents_resumed[2]["state"] != "paused" # now rerun and remap with correct input param failed_job_id = self.gi.datasets.show_dataset(history_contents[1]["id"])["creating_job"] tool_inputs_update = {"lineNum": "1"} rerun_job = self.gi.jobs.rerun_job(failed_job_id, remap=True, tool_inputs_update=tool_inputs_update) new_job_id = rerun_job["jobs"][0]["id"] # Wait for the last dataset in the history to be unpaused and complete last_dataset = self.gi.histories.show_history(self.history_id, contents=True)[-1] last_job_id = self.gi.datasets.show_dataset(last_dataset["id"])["creating_job"] self.gi.jobs.wait_for_job(new_job_id) self.gi.jobs.resume_job(last_job_id) # last_job can get stuck on paused - resume it in case self.gi.jobs.wait_for_job(last_job_id) assert last_dataset["hid"] == 3 assert last_dataset["id"] == history_contents[2]["id"] self._wait_and_verify_dataset(last_dataset["id"], b"line 1\tline 1\n") @test_util.skip_unless_galaxy("release_19.05") @test_util.skip_unless_tool("random_lines1") def test_get_common_problems(self): job_id = self._run_tool()["jobs"][0]["id"] response = self.gi.jobs.get_common_problems(job_id) assert response == {"has_duplicate_inputs": False, "has_empty_inputs": True} @test_util.skip_unless_tool("random_lines1") def test_get_inputs(self): job_id = self._run_tool()["jobs"][0]["id"] response = self.gi.jobs.get_inputs(job_id) assert response == [{"name": "input", "dataset": {"src": "hda", "id": self.dataset_id}}] @test_util.skip_unless_tool("random_lines1") def test_get_outputs(self): output = self._run_tool() job_id, output_id = output["jobs"][0]["id"], output["outputs"][0]["id"] response = self.gi.jobs.get_outputs(job_id) assert response == [{"name": "out_file1", "dataset": {"src": "hda", "id": output_id}}] @test_util.skip_unless_galaxy("release_20.05") @test_util.skip_unless_tool("random_lines1") def test_get_destination_params(self): job_id = self._run_tool()["jobs"][0]["id"] # In Galaxy 20.05 and 20.09 we need to wait for the job, otherwise # `get_destination_params()` receives a 500 error code. Fixed upstream # in https://github.com/galaxyproject/galaxy/commit/3e7f03cd1f229b8c9421ade02002728a33e131d8 self.gi.jobs.wait_for_job(job_id) response = self.gi.jobs.get_destination_params(job_id) assert "Runner" in response assert "Runner Job ID" in response assert "Handler" in response @test_util.skip_unless_tool("random_lines1") def test_search_jobs(self): job_id = self._run_tool()["jobs"][0]["id"] inputs = { "num_lines": "1", "input": {"src": "hda", "id": self.dataset_id}, "seed_source|seed_source_selector": "set_seed", "seed_source|seed": "asdf", } response = self.gi.jobs.search_jobs("random_lines1", inputs) assert job_id in [job["id"] for job in response] @test_util.skip_unless_galaxy("release_20.01") @test_util.skip_unless_tool("random_lines1") def test_report_error(self): output = self._run_tool() job_id, output_id = output["jobs"][0]["id"], output["outputs"][0]["id"] response = self.gi.jobs.report_error(job_id, output_id, "Test error") # expected response when the Galaxy server does not have mail configured assert response == { "messages": [ [ "An error occurred sending the report by email: Mail is not configured for this Galaxy instance", "danger", ] ] } @test_util.skip_unless_galaxy("release_20.05") def test_show_job_lock(self): status = self.gi.jobs.show_job_lock() assert not status @test_util.skip_unless_galaxy("release_20.05") def test_update_job_lock(self): status = self.gi.jobs.update_job_lock(active=True) assert status status = self.gi.jobs.update_job_lock(active=False) assert not status def test_cancel_job(self): job_id = self._run_tool()["jobs"][0]["id"] self.gi.jobs.cancel_job(job_id) job = self.gi.jobs.wait_for_job(job_id, check=False) assert job["state"] in ("deleted", "deleting") def _run_tool(self, input_format: Literal["21.01", "legacy"] = "legacy") -> dict: return super()._run_random_lines1(self.history_id, self.dataset_id, input_format=input_format) bioblend-1.2.0/bioblend/_tests/TestGalaxyLibraries.py000066400000000000000000000177211444761704300226650ustar00rootroot00000000000000import os import tempfile from . import ( GalaxyTestBase, test_util, ) FOO_DATA = "foo\nbar\n" class TestGalaxyLibraries(GalaxyTestBase.GalaxyTestBase): def setUp(self): super().setUp() self.name = "automated test library" self.library = self.gi.libraries.create_library( self.name, description="automated test", synopsis="automated test synopsis" ) def tearDown(self): self.gi.libraries.delete_library(self.library["id"]) def test_create_library(self): assert self.library["name"] == self.name assert self.library["id"] is not None def test_get_libraries(self): libraries_with_name = self.gi.libraries.get_libraries(name=self.name) assert len([lib for lib in libraries_with_name if lib["id"] == self.library["id"]]) == 1 deleted_name = "deleted test library" deleted_library_id = self.gi.libraries.create_library( deleted_name, description="a deleted library", synopsis="automated test synopsis" )["id"] self.gi.libraries.delete_library(deleted_library_id) deleted_libraries_with_name = self.gi.libraries.get_libraries(name=deleted_name, deleted=True) assert len([lib for lib in deleted_libraries_with_name if lib["id"] == deleted_library_id]) == 1 all_non_deleted_libraries = self.gi.libraries.get_libraries(deleted=False) assert len([lib for lib in all_non_deleted_libraries if lib["id"] == self.library["id"]]) == 1 assert [lib for lib in all_non_deleted_libraries if lib["id"] == deleted_library_id] == [] all_deleted_libraries = self.gi.libraries.get_libraries(deleted=True) assert [lib for lib in all_deleted_libraries if lib["id"] == self.library["id"]] == [] assert len([lib for lib in all_deleted_libraries if lib["id"] == deleted_library_id]) == 1 all_libraries = self.gi.libraries.get_libraries(deleted=None) assert len([lib for lib in all_libraries if lib["id"] == self.library["id"]]) == 1 assert len([lib for lib in all_libraries if lib["id"] == deleted_library_id]) == 1 def test_show_library(self): library_data = self.gi.libraries.show_library(self.library["id"]) assert self.library["id"] == library_data["id"] assert self.library["name"] == library_data["name"] def test_upload_file_from_url(self): url = "https://zenodo.org/record/582600/files/wildtype.fna?download=1" ret = self.gi.libraries.upload_file_from_url(self.library["id"], url) assert len(ret) == 1 ldda_dict = ret[0] assert ldda_dict["name"] == url def test_upload_file_contents(self): ret = self.gi.libraries.upload_file_contents(self.library["id"], FOO_DATA) assert len(ret) == 1 ldda_dict = ret[0] assert ldda_dict["name"] == "Pasted Entry" def test_upload_file_from_local_path(self): with tempfile.NamedTemporaryFile(mode="w", prefix="bioblend_test_") as f: f.write(FOO_DATA) f.flush() filename = f.name ret = self.gi.libraries.upload_file_from_local_path(self.library["id"], filename) assert len(ret) == 1 ldda_dict = ret[0] assert ldda_dict["name"] == os.path.basename(filename) # def test_upload_file_from_server(self): # pass def test_upload_from_galaxy_filesystem(self): bnames = [f"f{i}.txt" for i in range(2)] with tempfile.TemporaryDirectory() as tempdir: fnames = [os.path.join(tempdir, _) for _ in bnames] for fn in fnames: with open(fn, "w") as f: f.write(FOO_DATA) filesystem_paths = "\n".join(fnames) ret = self.gi.libraries.upload_from_galaxy_filesystem(self.library["id"], filesystem_paths) for fn, dataset_dict in zip(fnames, ret): dataset = self.gi.libraries.wait_for_dataset(self.library["id"], dataset_dict["id"]) assert dataset["state"] == "ok" assert dataset["name"] == os.path.basename(fn) ret = self.gi.libraries.upload_from_galaxy_filesystem( self.library["id"], filesystem_paths, link_data_only="link_to_files" ) for fn, dataset_dict in zip(fnames, ret): dataset = self.gi.libraries.wait_for_dataset(self.library["id"], dataset_dict["id"]) assert dataset["state"] == "ok" assert dataset["name"] == os.path.basename(fn) def test_copy_from_dataset(self): history = self.gi.histories.create_history() dataset_id = self._test_dataset(history["id"]) self.gi.libraries.copy_from_dataset(self.library["id"], dataset_id, message="Copied from dataset") def test_update_dataset(self): library_id = self.library["id"] dataset1 = self.gi.libraries.upload_file_contents(library_id, FOO_DATA) updated_dataset = self.gi.libraries.update_library_dataset( dataset1[0]["id"], name="Modified name", misc_info="Modified the name succesfully" ) assert updated_dataset["name"] == "Modified name" assert updated_dataset["misc_info"] == "Modified the name succesfully" def test_library_permissions(self): current_user = self.gi.users.get_current_user() user_id_list_new = [current_user["id"]] self.gi.libraries.set_library_permissions( self.library["id"], access_in=user_id_list_new, modify_in=user_id_list_new, add_in=user_id_list_new, manage_in=user_id_list_new, ) ret = self.gi.libraries.get_library_permissions(self.library["id"]) assert {_[1] for _ in ret["access_library_role_list"]} == set(user_id_list_new) assert {_[1] for _ in ret["modify_library_role_list"]} == set(user_id_list_new) assert {_[1] for _ in ret["add_library_item_role_list"]} == set(user_id_list_new) assert {_[1] for _ in ret["manage_library_role_list"]} == set(user_id_list_new) def test_dataset_permissions(self): current_user = self.gi.users.get_current_user() user_id_list_new = [current_user["id"]] library_id = self.library["id"] dataset1 = self.gi.libraries.upload_file_contents(library_id, FOO_DATA) ret = self.gi.libraries.set_dataset_permissions( dataset1[0]["id"], access_in=user_id_list_new, modify_in=user_id_list_new, manage_in=user_id_list_new ) assert {_[1] for _ in ret["access_dataset_roles"]} == set(user_id_list_new) assert {_[1] for _ in ret["modify_item_roles"]} == set(user_id_list_new) assert {_[1] for _ in ret["manage_dataset_roles"]} == set(user_id_list_new) # test get_dataset_permissions ret_get = self.gi.libraries.get_dataset_permissions(dataset1[0]["id"]) assert {_[1] for _ in ret_get["access_dataset_roles"]} == set(user_id_list_new) assert {_[1] for _ in ret_get["modify_item_roles"]} == set(user_id_list_new) assert {_[1] for _ in ret_get["manage_dataset_roles"]} == set(user_id_list_new) @test_util.skip_unless_galaxy("release_19.09") def test_upload_file_contents_with_tags(self): datasets = self.gi.libraries.upload_file_contents(self.library["id"], FOO_DATA, tags=["name:foobar", "barfoo"]) dataset_show = self.gi.libraries.show_dataset(self.library["id"], datasets[0]["id"]) assert dataset_show["tags"] == "name:foobar, barfoo" @test_util.skip_unless_galaxy("release_19.09") def test_update_dataset_tags(self): datasets = self.gi.libraries.upload_file_contents(self.library["id"], FOO_DATA) dataset_show = self.gi.libraries.show_dataset(self.library["id"], datasets[0]["id"]) assert dataset_show["tags"] == "" updated_dataset = self.gi.libraries.update_library_dataset(datasets[0]["id"], tags=["name:foobar", "barfoo"]) dataset_show = self.gi.libraries.show_dataset(self.library["id"], updated_dataset["id"]) assert dataset_show["tags"] == "name:foobar, barfoo" bioblend-1.2.0/bioblend/_tests/TestGalaxyObjects.py000066400000000000000000001157401444761704300223420ustar00rootroot00000000000000# pylint: disable=C0103,E1101 import json import os import shutil import socket import sys import tarfile import tempfile import unittest import uuid from ssl import SSLError from typing import ( Any, Callable, Collection, Dict, Iterable, List, Set, Tuple, Union, ) from urllib.error import URLError from urllib.request import urlopen import pytest from typing_extensions import Literal import bioblend from bioblend.galaxy import dataset_collections from bioblend.galaxy.objects import ( galaxy_instance, wrappers, ) from . import test_util bioblend.set_stream_logger("test", level="INFO") socket.setdefaulttimeout(10.0) SAMPLE_FN = test_util.get_abspath(os.path.join("data", "paste_columns.ga")) SAMPLE_WF_COLL_FN = test_util.get_abspath(os.path.join("data", "paste_columns_collections.ga")) SAMPLE_WF_PARAMETER_INPUT_FN = test_util.get_abspath(os.path.join("data", "workflow_with_parameter_input.ga")) FOO_DATA = "foo\nbar\n" FOO_DATA_2 = "foo2\nbar2\n" SAMPLE_WF_DICT = { "deleted": False, "id": "9005c5112febe774", "inputs": { "571": {"label": "Input Dataset", "value": ""}, "572": {"label": "Input Dataset", "value": ""}, }, "model_class": "StoredWorkflow", "name": "paste_columns", "owner": "user_foo", "published": False, "steps": { "571": { "id": 571, "input_steps": {}, "tool_id": None, "tool_inputs": {"name": "Input Dataset"}, "tool_version": None, "type": "data_input", }, "572": { "id": 572, "input_steps": {}, "tool_id": None, "tool_inputs": {"name": "Input Dataset"}, "tool_version": None, "type": "data_input", }, "573": { "id": 573, "input_steps": { "input1": {"source_step": 571, "step_output": "output"}, "input2": {"source_step": 572, "step_output": "output"}, }, "tool_id": "Paste1", "tool_inputs": { "delimiter": '"T"', "input1": "null", "input2": "null", }, "tool_version": "1.0.0", "type": "tool", }, }, "tags": [], "url": "/api/workflows/9005c5112febe774", } SAMPLE_INV_DICT: Dict[str, Any] = { "history_id": "2f94e8ae9edff68a", "id": "df7a1f0c02a5b08e", "inputs": {"0": {"id": "a7db2fac67043c7e", "src": "hda", "uuid": "7932ffe0-2340-4952-8857-dbaa50f1f46a"}}, "model_class": "WorkflowInvocation", "state": "ready", "steps": [ { "action": None, "id": "d413a19dec13d11e", "job_id": None, "model_class": "WorkflowInvocationStep", "order_index": 0, "state": None, "update_time": "2015-10-31T22:00:26", "workflow_step_id": "cbbbf59e8f08c98c", "workflow_step_label": None, "workflow_step_uuid": "b81250fd-3278-4e6a-b269-56a1f01ef485", }, { "action": None, "id": "2f94e8ae9edff68a", "job_id": "e89067bb68bee7a0", "model_class": "WorkflowInvocationStep", "order_index": 1, "state": "new", "update_time": "2015-10-31T22:00:26", "workflow_step_id": "964b37715ec9bd22", "workflow_step_label": None, "workflow_step_uuid": "e62440b8-e911-408b-b124-e05435d3125e", }, ], "update_time": "2015-10-31T22:00:26", "uuid": "c8aa2b1c-801a-11e5-a9e5-8ca98228593c", "workflow_id": "03501d7626bd192f", } def is_reachable(url: str) -> bool: res = None try: res = urlopen(url, timeout=5) except (SSLError, URLError, socket.timeout): return False if res is not None: res.close() return True def upload_from_fs( lib: wrappers.Library, bnames: Iterable[str], **kwargs: Any ) -> Tuple[List[wrappers.LibraryDataset], List[str]]: tempdir = tempfile.mkdtemp(prefix="bioblend_test_") try: fnames = [os.path.join(tempdir, _) for _ in bnames] for fn in fnames: with open(fn, "w") as f: f.write(FOO_DATA) dss = lib.upload_from_galaxy_fs(fnames, **kwargs) finally: shutil.rmtree(tempdir) return dss, fnames class MockWrapper(wrappers.Wrapper): BASE_ATTRS = ("a", "b") a: int b: List[int] def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) class TestWrapper(unittest.TestCase): def setUp(self): self.d: Dict[str, Any] = {"a": 1, "b": [2, 3], "c": {"x": 4}} with pytest.raises(TypeError): wrappers.Wrapper(self.d) self.w = MockWrapper(self.d) def test_initialize(self): for k in MockWrapper.BASE_ATTRS: assert getattr(self.w, k) == self.d[k] self.w.a = 222 self.w.b[0] = 222 assert self.w.a == 222 assert self.w.b[0] == 222 assert self.d["a"] == 1 assert self.d["b"][0] == 2 with pytest.raises(AttributeError): _ = self.w.foo # type: ignore[attr-defined] with pytest.raises(AttributeError): self.w.foo = 0 # type: ignore[assignment] def test_taint(self): assert not self.w.is_modified self.w.a = 111 # pylint: disable=W0201 assert self.w.is_modified def test_serialize(self): w = MockWrapper.from_json(self.w.to_json()) assert w.wrapped == self.w.wrapped def test_clone(self): w = self.w.clone() assert w.wrapped == self.w.wrapped w.b[0] = 111 assert self.w.b[0] == 2 def test_kwargs(self): parent = MockWrapper({"a": 10}) w = MockWrapper(self.d, parent=parent) assert w.parent is parent with pytest.raises(AttributeError): w.parent = 0 # type: ignore[assignment,misc] @test_util.skip_unless_galaxy() class GalaxyObjectsTestBase(unittest.TestCase): gi: galaxy_instance.GalaxyInstance @classmethod def setUpClass(cls) -> None: galaxy_key = os.environ["BIOBLEND_GALAXY_API_KEY"] galaxy_url = os.environ["BIOBLEND_GALAXY_URL"] cls.gi = galaxy_instance.GalaxyInstance(galaxy_url, galaxy_key) class TestWorkflow(GalaxyObjectsTestBase): def setUp(self): self.wf = wrappers.Workflow(SAMPLE_WF_DICT) def test_initialize(self): assert self.wf.id == "9005c5112febe774" assert self.wf.name == "paste_columns" assert not self.wf.deleted assert self.wf.owner == "user_foo" assert not self.wf.published assert self.wf.tags == [] assert self.wf.input_labels_to_ids == {"Input Dataset": {"571", "572"}} assert self.wf.tool_labels_to_ids == {"Paste1": {"573"}} assert self.wf.data_input_ids == {"571", "572"} assert self.wf.source_ids == {"571", "572"} assert self.wf.sink_ids == {"573"} def test_dag(self): inv_dag: Dict[str, Set[str]] = {} for h, tails in self.wf.dag.items(): for t in tails: inv_dag.setdefault(str(t), set()).add(h) assert self.wf.inv_dag == inv_dag heads = set(self.wf.dag) assert heads == set.union(*self.wf.inv_dag.values()) tails = set(self.wf.inv_dag) assert tails == set.union(*self.wf.dag.values()) ids = self.wf.sorted_step_ids() assert set(ids) == heads | tails for h, tails in self.wf.dag.items(): for t in tails: assert ids.index(h) < ids.index(t) def test_steps(self): steps = SAMPLE_WF_DICT["steps"] assert isinstance(steps, dict) for sid, s in self.wf.steps.items(): assert isinstance(s, wrappers.Step) assert s.id == sid assert sid in steps assert s.parent is self.wf assert self.wf.data_input_ids == {"571", "572"} assert self.wf.tool_ids == {"573"} def test_taint(self): assert not self.wf.is_modified self.wf.steps["571"].tool_id = "foo" assert self.wf.is_modified def test_input_map(self): history = wrappers.History({}, gi=self.gi) library = wrappers.Library({}, gi=self.gi) hda = wrappers.HistoryDatasetAssociation({"id": "hda_id"}, container=history, gi=self.gi) ldda = wrappers.LibraryDatasetDatasetAssociation({"id": "ldda_id"}, container=library, gi=self.gi) input_map = self.wf._convert_input_map({"0": hda, "1": ldda, "2": {"id": "hda2_id", "src": "hda"}}) assert input_map == { "0": {"id": "hda_id", "src": "hda"}, "1": {"id": "ldda_id", "src": "ldda"}, "2": {"id": "hda2_id", "src": "hda"}, } @test_util.skip_unless_galaxy("release_19.09") class TestInvocation(GalaxyObjectsTestBase): dataset: wrappers.HistoryDatasetAssociation history: wrappers.History inv: wrappers.Invocation workflow: wrappers.Workflow workflow_pause: wrappers.Workflow @classmethod def setUpClass(cls) -> None: super().setUpClass() cls.inv = wrappers.Invocation(SAMPLE_INV_DICT, gi=cls.gi) with open(SAMPLE_FN) as f: cls.workflow = cls.gi.workflows.import_new(f.read()) path_pause = test_util.get_abspath(os.path.join("data", "test_workflow_pause.ga")) with open(path_pause) as f: cls.workflow_pause = cls.gi.workflows.import_new(f.read()) cls.history = cls.gi.histories.create(name="TestInvocation") cls.dataset = cls.history.paste_content("1\t2\t3") @classmethod def tearDownClass(cls): cls.history.delete(purge=True) def test_initialize(self): assert self.inv.workflow_id == "03501d7626bd192f" assert self.inv.history_id == "2f94e8ae9edff68a" assert self.inv.id == "df7a1f0c02a5b08e" assert self.inv.state == "ready" assert self.inv.update_time == "2015-10-31T22:00:26" assert self.inv.uuid == "c8aa2b1c-801a-11e5-a9e5-8ca98228593c" def test_initialize_steps(self): for step, step_dict in zip(self.inv.steps, SAMPLE_INV_DICT["steps"]): assert isinstance(step_dict, dict) assert isinstance(step, wrappers.InvocationStep) assert step.parent is self.inv assert step.id == step_dict["id"] assert step.job_id == step_dict["job_id"] assert step.order_index == step_dict["order_index"] assert step.state == step_dict["state"] assert step.update_time == step_dict["update_time"] assert step.workflow_step_id == step_dict["workflow_step_id"] assert step.workflow_step_label == step_dict["workflow_step_label"] assert step.workflow_step_uuid == step_dict["workflow_step_uuid"] def test_initialize_inputs(self): for i, input in enumerate(self.inv.inputs): assert input == {**SAMPLE_INV_DICT["inputs"][str(i)], "label": str(i)} def test_sorted_step_ids(self): assert self.inv.sorted_step_ids() == ["d413a19dec13d11e", "2f94e8ae9edff68a"] def test_step_states(self): assert self.inv.step_states() == {None, "new"} def test_number_of_steps(self): assert self.inv.number_of_steps() == 2 def test_sorted_steps_by(self): assert len(self.inv.sorted_steps_by()) == 2 steps = self.inv.sorted_steps_by(step_ids={"2f94e8ae9edff68a"}) assert len(steps) == 1 assert steps[0].id == "2f94e8ae9edff68a" assert self.inv.sorted_steps_by(step_ids={"unmatched_id"}) == [] steps = self.inv.sorted_steps_by(states={"new"}) assert len(steps) == 1 assert steps[0].state == "new" assert self.inv.sorted_steps_by(states={"unmatched_state"}) == [] steps = self.inv.sorted_steps_by(indices={0}, states={None, "new"}) assert len(steps) == 1 assert steps[0].order_index == 0 assert self.inv.sorted_steps_by(indices={2}) == [] def test_cancel(self): inv = self._obj_invoke_workflow() inv.cancel() assert inv.state == "cancelled" def test_wait(self): inv = self._obj_invoke_workflow() inv.wait() assert inv.state == "scheduled" def test_refresh(self): inv = self._obj_invoke_workflow() inv.state = "placeholder" # use wait_for_invocation() directly, because inv.wait() will update inv automatically self.gi.gi.invocations.wait_for_invocation(inv.id) inv.refresh() assert inv.state == "scheduled" def test_run_step_actions(self): inv = self.workflow_pause.invoke( inputs={"0": self.dataset}, history=self.history, ) for _ in range(20): with pytest.raises(bioblend.TimeoutException): inv.wait(maxwait=0.5, interval=0.5) inv.refresh() if len(inv.steps) >= 3: break assert inv.steps[2].action is None inv.run_step_actions([inv.steps[2]], [True]) assert inv.steps[2].action is True def test_summary(self): inv = self._obj_invoke_workflow() inv.wait() summary = inv.summary() assert summary["populated_state"] == "ok" def test_step_jobs_summary(self): inv = self._obj_invoke_workflow() inv.wait() step_jobs_summary = inv.step_jobs_summary() assert len(step_jobs_summary) == 1 assert step_jobs_summary[0]["populated_state"] == "ok" def test_report(self): inv = self._obj_invoke_workflow() report = inv.report() assert report["workflows"] == {self.workflow.id: {"name": "paste_columns"}} @test_util.skip_unless_galaxy("release_20.09") def test_biocompute_object(self): inv = self._obj_invoke_workflow() inv.wait() biocompute_object = inv.biocompute_object() assert len(biocompute_object["description_domain"]["pipeline_steps"]) == 1 def _obj_invoke_workflow(self) -> wrappers.Invocation: return self.workflow.invoke( inputs={"Input 1": self.dataset, "Input 2": self.dataset}, history=self.history, inputs_by="name", ) @test_util.skip_unless_galaxy("release_19.09") class TestObjInvocationClient(GalaxyObjectsTestBase): history: wrappers.History inv: wrappers.Invocation workflow: wrappers.Workflow @classmethod def setUpClass(cls) -> None: super().setUpClass() with open(SAMPLE_FN) as f: cls.workflow = cls.gi.workflows.import_new(f.read()) cls.history = cls.gi.histories.create(name="TestGalaxyObjInvocationClient") dataset = cls.history.paste_content("1\t2\t3") cls.inv = cls.workflow.invoke( inputs={"Input 1": dataset, "Input 2": dataset}, history=cls.history, inputs_by="name", ) cls.inv.wait() @classmethod def tearDownClass(cls): cls.history.delete(purge=True) def test_get(self): inv = self.gi.invocations.get(self.inv.id) assert inv.id == self.inv.id assert inv.workflow_id == self.workflow.id assert inv.history_id == self.history.id assert inv.state == "scheduled" assert inv.update_time == self.inv.update_time assert inv.uuid == self.inv.uuid def test_get_previews(self): previews = self.gi.invocations.get_previews() assert {type(preview) for preview in previews} == {wrappers.InvocationPreview} inv_preview = next(p for p in previews if p.id == self.inv.id) assert inv_preview.id == self.inv.id assert inv_preview.workflow_id == self.workflow.id assert inv_preview.history_id == self.history.id assert inv_preview.state == "scheduled" assert inv_preview.update_time == self.inv.update_time assert inv_preview.uuid == self.inv.uuid def test_list(self): invs = self.gi.invocations.list() inv = next(i for i in invs if i.id == self.inv.id) assert inv.id == self.inv.id assert inv.workflow_id == self.workflow.id assert inv.history_id == self.history.id assert inv.state == "scheduled" assert inv.update_time == self.inv.update_time assert inv.uuid == self.inv.uuid assert len(self.inv.steps) > 0 history = self.gi.histories.create(name="TestGalaxyObjInvocationClientList") assert self.gi.invocations.list(history=history) == [] history.delete(purge=True) class TestGalaxyInstance(GalaxyObjectsTestBase): def test_library(self): name = f"test_{uuid.uuid4().hex}" description, synopsis = "D", "S" lib = self.gi.libraries.create(name, description=description, synopsis=synopsis) assert lib.name == name assert lib.description == description assert lib.synopsis == synopsis assert len(lib.content_infos) == 1 # root folder assert len(lib.folder_ids) == 1 assert len(lib.dataset_ids) == 0 assert lib.id in [_.id for _ in self.gi.libraries.list()] lib.delete() assert not lib.is_mapped def test_workflow_from_str(self): with open(SAMPLE_FN) as f: wf = self.gi.workflows.import_new(f.read()) self._check_and_del_workflow(wf) def test_workflow_collections_from_str(self): with open(SAMPLE_WF_COLL_FN) as f: wf = self.gi.workflows.import_new(f.read()) self._check_and_del_workflow(wf) def test_workflow_parameter_input(self): with open(SAMPLE_WF_PARAMETER_INPUT_FN) as f: self.gi.workflows.import_new(f.read()) def test_workflow_from_dict(self): with open(SAMPLE_FN) as f: wf = self.gi.workflows.import_new(json.load(f)) self._check_and_del_workflow(wf) def test_workflow_publish_from_dict(self): with open(SAMPLE_FN) as f: wf = self.gi.workflows.import_new(json.load(f), publish=True) self._check_and_del_workflow(wf, check_is_public=True) def test_workflow_missing_tools(self): with open(SAMPLE_FN) as f: wf_dump = json.load(f) wf_info = self.gi.gi.workflows.import_workflow_dict(wf_dump) wf_dict = self.gi.gi.workflows.show_workflow(wf_info["id"]) for id_, step in wf_dict["steps"].items(): if step["type"] == "tool": for k in "tool_inputs", "tool_version": wf_dict["steps"][id_][k] = None wf = wrappers.Workflow(wf_dict, gi=self.gi) assert not wf.is_runnable with pytest.raises(RuntimeError): wf.invoke() wf.delete() def test_workflow_export(self): with open(SAMPLE_FN) as f: wf1 = self.gi.workflows.import_new(f.read()) wf2 = self.gi.workflows.import_new(wf1.export()) assert wf1.id != wf2.id for wf in wf1, wf2: self._check_and_del_workflow(wf) def _check_and_del_workflow(self, wf: wrappers.Workflow, check_is_public: bool = False) -> None: # Galaxy appends additional text to imported workflow names assert wf.name.startswith("paste_columns") assert len(wf.steps) == 3 for step_id, step in wf.steps.items(): assert isinstance(step, wrappers.Step) assert step_id == step.id assert isinstance(step.tool_inputs, dict) if step.type == "tool": assert step.tool_id is not None assert step.tool_version is not None assert isinstance(step.input_steps, dict) elif step.type in ("data_collection_input", "data_input"): assert step.tool_id is None assert step.tool_version is None assert step.input_steps == {} wf_ids = {_.id for _ in self.gi.workflows.list()} assert wf.id in wf_ids if check_is_public: assert wf.published wf.delete() # not very accurate: # * we can't publish a wf from the API # * we can't directly get another user's wf def test_workflow_from_shared(self): all_prevs = {_.id: _ for _ in self.gi.workflows.get_previews(published=True)} pub_only_ids = set(all_prevs).difference(_.id for _ in self.gi.workflows.get_previews()) if pub_only_ids: wf_id = pub_only_ids.pop() imported = self.gi.workflows.import_shared(wf_id) assert isinstance(imported, wrappers.Workflow) imported.delete() else: self.skipTest("no published workflows, manually publish a workflow to run this test") def test_get_libraries(self): self._test_multi_get("libraries") def test_get_histories(self): self._test_multi_get("histories") def test_get_workflows(self): self._test_multi_get("workflows") def _normalized_functions( self, obj_type: Literal["histories", "libraries", "workflows"] ) -> Tuple[Callable, Dict[str, Any]]: if obj_type == "libraries": create: Callable = self.gi.libraries.create del_kwargs = {} elif obj_type == "histories": create = self.gi.histories.create del_kwargs = {"purge": True} elif obj_type == "workflows": def create(name): with open(SAMPLE_FN) as f: d = json.load(f) d["name"] = name return self.gi.workflows.import_new(d) del_kwargs = {} return create, del_kwargs def _test_multi_get(self, obj_type: Literal["histories", "libraries", "workflows"]) -> None: obj_gi_client = getattr(self.gi, obj_type) create, del_kwargs = self._normalized_functions(obj_type) def ids(seq: Iterable[wrappers.Wrapper]) -> Set[str]: return {_.id for _ in seq} names = [f"test_{uuid.uuid4().hex}" for _ in range(2)] objs = [] try: objs = [create(_) for _ in names] assert ids(objs) <= ids(obj_gi_client.list()) if obj_type != "workflows": filtered = obj_gi_client.list(name=names[0]) assert len(filtered) == 1 assert filtered[0].id == objs[0].id del_id = objs[-1].id objs.pop().delete(**del_kwargs) assert del_id in ids(obj_gi_client.get_previews(deleted=True)) else: # Galaxy appends info strings to imported workflow names prev = obj_gi_client.get_previews()[0] filtered = obj_gi_client.list(name=prev.name) assert len(filtered) == 1 assert filtered[0].id == prev.id finally: for o in objs: o.delete(**del_kwargs) def test_delete_libraries_by_name(self): self._test_delete_by_name("libraries") self._test_delete_by_ambiguous_name("libraries") def test_delete_histories_by_name(self): self._test_delete_by_name("histories") self._test_delete_by_ambiguous_name("histories") def test_delete_workflows_by_name(self): self._test_delete_by_name("workflows") self._test_delete_by_ambiguous_name("workflows") def _test_delete_by_name(self, obj_type: Literal["histories", "libraries", "workflows"]) -> None: obj_gi_client = getattr(self.gi, obj_type) create, del_kwargs = self._normalized_functions(obj_type) name = f"test_{uuid.uuid4().hex}" create(name) prevs = [_ for _ in obj_gi_client.get_previews(name=name) if not _.deleted] assert len(prevs) == 1 del_kwargs["name"] = name obj_gi_client.delete(**del_kwargs) prevs = [_ for _ in obj_gi_client.get_previews(name=name) if not _.deleted] assert len(prevs) == 0 def _test_delete_by_ambiguous_name(self, obj_type: Literal["histories", "libraries", "workflows"]) -> None: obj_gi_client = getattr(self.gi, obj_type) create, del_kwargs = self._normalized_functions(obj_type) name = f"test_{uuid.uuid4().hex}" objs = [create(name) for _ in range(2)] prevs = [_ for _ in obj_gi_client.get_previews(name=name) if not _.deleted] assert len(prevs) == len(objs) del_kwargs["name"] = name with pytest.raises(ValueError): obj_gi_client.delete(**del_kwargs) # Cleanup del del_kwargs["name"] for prev in prevs: del_kwargs["id_"] = prev.id obj_gi_client.delete(**del_kwargs) class TestLibrary(GalaxyObjectsTestBase): # just something that can be expected to be always up DS_URL = "https://tools.ietf.org/rfc/rfc1866.txt" def setUp(self): super().setUp() self.lib = self.gi.libraries.create(f"test_{uuid.uuid4().hex}") def tearDown(self): self.lib.delete() def test_root_folder(self): r = self.lib.root_folder assert r.parent is None def test_folder(self): name = f"test_{uuid.uuid4().hex}" desc = "D" folder = self.lib.create_folder(name, description=desc) assert folder.name == name assert folder.description == desc assert folder.container is self.lib assert folder.parent is not None assert folder.parent.id == self.lib.root_folder.id assert len(self.lib.content_infos) == 2 assert len(self.lib.folder_ids) == 2 assert folder.id in self.lib.folder_ids retrieved = self.lib.get_folder(folder.id) assert folder.id == retrieved.id def _check_datasets(self, dss: Collection[wrappers.LibraryDataset]) -> None: assert len(dss) == len(self.lib.dataset_ids) assert {_.id for _ in dss} == set(self.lib.dataset_ids) for ds in dss: assert ds.container is self.lib def test_dataset(self): folder = self.lib.create_folder(f"test_{uuid.uuid4().hex}") ds = self.lib.upload_data(FOO_DATA, folder=folder) assert len(self.lib.content_infos) == 3 assert len(self.lib.folder_ids) == 2 self._check_datasets([ds]) def test_dataset_from_url(self): if is_reachable(self.DS_URL): ds = self.lib.upload_from_url(self.DS_URL) self._check_datasets([ds]) else: self.skipTest(f"{self.DS_URL} not reachable") def test_dataset_from_local(self): with tempfile.NamedTemporaryFile(mode="w", prefix="bioblend_test_") as f: f.write(FOO_DATA) f.flush() ds = self.lib.upload_from_local(f.name) self._check_datasets([ds]) def test_datasets_from_fs(self): bnames = [f"f{i}.txt" for i in range(2)] dss, fnames = upload_from_fs(self.lib, bnames) self._check_datasets(dss) dss, fnames = upload_from_fs(self.lib, bnames, link_data_only="link_to_files") for ds, fn in zip(dss, fnames): assert ds.file_name == fn def test_copy_from_dataset(self): hist = self.gi.histories.create(f"test_{uuid.uuid4().hex}") try: hda = hist.paste_content(FOO_DATA) ds = self.lib.copy_from_dataset(hda) finally: hist.delete(purge=True) self._check_datasets([ds]) def test_get_dataset(self): ds = self.lib.upload_data(FOO_DATA) retrieved = self.lib.get_dataset(ds.id) assert ds.id == retrieved.id def test_get_datasets(self): bnames = [f"f{i}.txt" for i in range(2)] dss, _ = upload_from_fs(self.lib, bnames) retrieved = self.lib.get_datasets() assert len(dss) == len(retrieved) assert {_.id for _ in dss} == {_.id for _ in retrieved} name = f"/{bnames[0]}" selected = self.lib.get_datasets(name=name) assert len(selected) == 1 assert selected[0].name == bnames[0] class TestLDContents(GalaxyObjectsTestBase): def setUp(self): super().setUp() self.lib = self.gi.libraries.create(f"test_{uuid.uuid4().hex}") self.ds = self.lib.upload_data(FOO_DATA) self.ds.wait() def tearDown(self): self.lib.delete() def test_dataset_get_stream(self): for idx, c in enumerate(self.ds.get_stream(chunk_size=1)): assert FOO_DATA[idx].encode() == c def test_dataset_peek(self): fetched_data = self.ds.peek(chunk_size=4) assert FOO_DATA[0:4].encode() == fetched_data def test_dataset_download(self): with tempfile.TemporaryFile() as f: self.ds.download(f) f.seek(0) assert FOO_DATA.encode() == f.read() def test_dataset_get_contents(self): assert FOO_DATA.encode() == self.ds.get_contents() def test_dataset_delete(self): self.ds.delete() # Cannot test this yet because the 'deleted' attribute is not exported # by the API at the moment # assert self.ds.deleted def test_dataset_update(self): new_name = f"test_{uuid.uuid4().hex}" new_misc_info = f"Annotation for {new_name}" new_genome_build = "hg19" updated_ldda = self.ds.update(name=new_name, misc_info=new_misc_info, genome_build=new_genome_build) assert self.ds.id == updated_ldda.id assert self.ds.name == new_name assert self.ds.misc_info == new_misc_info assert self.ds.genome_build == new_genome_build class TestHistory(GalaxyObjectsTestBase): def setUp(self): super().setUp() self.hist = self.gi.histories.create(f"test_{uuid.uuid4().hex}") def tearDown(self): self.hist.delete(purge=True) def test_create_delete(self): name = f"test_{uuid.uuid4().hex}" hist = self.gi.histories.create(name) assert hist.name == name hist_id = hist.id assert hist_id in [_.id for _ in self.gi.histories.list()] hist.delete(purge=True) assert not hist.is_mapped h = self.gi.histories.get(hist_id) assert h.deleted def _check_dataset(self, hda: wrappers.HistoryDatasetAssociation) -> None: assert isinstance(hda, wrappers.HistoryDatasetAssociation) assert hda.container is self.hist assert len(self.hist.dataset_ids) == 1 assert self.hist.dataset_ids[0] == hda.id def test_import_dataset(self): lib = self.gi.libraries.create(f"test_{uuid.uuid4().hex}") lds = lib.upload_data(FOO_DATA) assert len(self.hist.dataset_ids) == 0 hda = self.hist.import_dataset(lds) lib.delete() self._check_dataset(hda) def test_upload_file(self): with tempfile.NamedTemporaryFile(mode="w", prefix="bioblend_test_") as f: f.write(FOO_DATA) f.flush() hda = self.hist.upload_file(f.name) self._check_dataset(hda) def test_paste_content(self): hda = self.hist.paste_content(FOO_DATA) self._check_dataset(hda) def test_get_dataset(self): hda = self.hist.paste_content(FOO_DATA) retrieved = self.hist.get_dataset(hda.id) assert hda.id == retrieved.id def test_get_datasets(self): bnames = [f"f{i}.txt" for i in range(2)] lib = self.gi.libraries.create(f"test_{uuid.uuid4().hex}") lds = upload_from_fs(lib, bnames)[0] hdas = [self.hist.import_dataset(_) for _ in lds] lib.delete() retrieved = self.hist.get_datasets() assert len(hdas) == len(retrieved) assert {_.id for _ in hdas} == {_.id for _ in retrieved} selected = self.hist.get_datasets(name=bnames[0]) assert len(selected) == 1 assert selected[0].name == bnames[0] def test_export_and_download(self): jeha_id = self.hist.export(wait=True, maxwait=60) assert jeha_id tempdir = tempfile.mkdtemp(prefix="bioblend_test_") temp_fn = os.path.join(tempdir, "export.tar.gz") try: with open(temp_fn, "wb") as fo: self.hist.download(jeha_id, fo) assert tarfile.is_tarfile(temp_fn) finally: shutil.rmtree(tempdir) def test_update(self): new_name = f"test_{uuid.uuid4().hex}" new_annotation = f"Annotation for {new_name}" new_tags = ["tag1", "tag2"] updated_hist = self.hist.update(name=new_name, annotation=new_annotation, tags=new_tags) assert self.hist.id == updated_hist.id assert self.hist.name == new_name assert self.hist.annotation == new_annotation assert self.hist.tags == new_tags updated_hist = self.hist.update(published=True) assert self.hist.id == updated_hist.id assert self.hist.published def test_create_dataset_collection(self): self._create_collection_description() hdca = self.hist.create_dataset_collection(self.collection_description) assert isinstance(hdca, wrappers.HistoryDatasetCollectionAssociation) assert hdca.collection_type == "list" assert hdca.container is self.hist assert len(hdca.elements) == 2 assert self.dataset1.id == hdca.elements[0]["object"]["id"] assert self.dataset2.id == hdca.elements[1]["object"]["id"] def test_delete_dataset_collection(self): self._create_collection_description() hdca = self.hist.create_dataset_collection(self.collection_description) hdca.delete() assert hdca.deleted def _create_collection_description(self) -> None: self.dataset1 = self.hist.paste_content(FOO_DATA) self.dataset2 = self.hist.paste_content(FOO_DATA_2) self.collection_description = dataset_collections.CollectionDescription( name="MyDatasetList", elements=[ dataset_collections.HistoryDatasetElement(name="sample1", id=self.dataset1.id), dataset_collections.HistoryDatasetElement(name="sample2", id=self.dataset2.id), ], ) class TestHDAContents(GalaxyObjectsTestBase): def setUp(self): super().setUp() self.hist = self.gi.histories.create(f"test_{uuid.uuid4().hex}") self.ds = self.hist.paste_content(FOO_DATA) def tearDown(self): self.hist.delete(purge=True) def test_dataset_get_stream(self): for idx, c in enumerate(self.ds.get_stream(chunk_size=1)): assert FOO_DATA[idx].encode() == c def test_dataset_peek(self): fetched_data = self.ds.peek(chunk_size=4) assert FOO_DATA[0:4].encode() == fetched_data def test_dataset_download(self): with tempfile.TemporaryFile() as f: self.ds.download(f) f.seek(0) assert FOO_DATA.encode() == f.read() def test_dataset_get_contents(self): assert FOO_DATA.encode() == self.ds.get_contents() def test_dataset_update(self): new_name = f"test_{uuid.uuid4().hex}" new_annotation = f"Annotation for {new_name}" new_genome_build = "hg19" updated_hda = self.ds.update(name=new_name, annotation=new_annotation, genome_build=new_genome_build) assert self.ds.id == updated_hda.id assert self.ds.name == new_name assert self.ds.annotation == new_annotation assert self.ds.genome_build == new_genome_build def test_dataset_delete(self): self.ds.delete() assert self.ds.deleted assert not self.ds.purged def test_dataset_purge(self): self.ds.delete(purge=True) assert self.ds.deleted assert self.ds.purged @test_util.skip_unless_galaxy("release_19.09") class TestRunWorkflow(GalaxyObjectsTestBase): def setUp(self): super().setUp() self.lib = self.gi.libraries.create(f"test_{uuid.uuid4().hex}") with open(SAMPLE_FN) as f: self.wf = self.gi.workflows.import_new(f.read()) self.contents = ["one\ntwo\n", "1\n2\n"] self.inputs = [self.lib.upload_data(_) for _ in self.contents] def tearDown(self): self.wf.delete() self.lib.delete() def _test(self, existing_hist: bool = False, pass_params: bool = False) -> None: hist_name = f"test_{uuid.uuid4().hex}" if existing_hist: hist: Union[str, wrappers.History] = self.gi.histories.create(hist_name) else: hist = hist_name if pass_params: params = {"Paste1": {"delimiter": "U"}} sep = "_" # 'U' maps to '_' in the paste tool else: params = None sep = "\t" # default input_map = {"Input 1": self.inputs[0], "Input 2": self.inputs[1]} sys.stderr.write(os.linesep) inv = self.wf.invoke(inputs=input_map, params=params, history=hist, inputs_by="name") out_hist = self.gi.histories.get(inv.history_id) inv.wait() last_step = inv.sorted_steps_by()[-1] out_ds = last_step.get_outputs()["out_file1"] assert out_ds.container.id == out_hist.id res = out_ds.get_contents() exp_rows = zip(*(_.splitlines() for _ in self.contents)) exp_res = ("\n".join(sep.join(t) for t in exp_rows) + "\n").encode() assert res == exp_res if isinstance(hist, wrappers.History): # i.e. existing_hist == True assert out_hist.id == hist.id out_hist.delete(purge=True) def test_existing_history(self) -> None: self._test(existing_hist=True) def test_new_history(self) -> None: self._test(existing_hist=False) def test_params(self) -> None: self._test(pass_params=True) @test_util.skip_unless_galaxy("release_19.09") class TestRunDatasetCollectionWorkflow(GalaxyObjectsTestBase): def setUp(self): super().setUp() with open(SAMPLE_WF_COLL_FN) as f: self.wf = self.gi.workflows.import_new(f.read()) self.hist = self.gi.histories.create(f"test_{uuid.uuid4().hex}") def tearDown(self): self.wf.delete() self.hist.delete(purge=True) def test_run_workflow_with_dataset_collection(self): dataset1 = self.hist.paste_content(FOO_DATA) dataset2 = self.hist.paste_content(FOO_DATA_2) collection_description = dataset_collections.CollectionDescription( name="MyDatasetList", elements=[ dataset_collections.HistoryDatasetElement(name="sample1", id=dataset1.id), dataset_collections.HistoryDatasetElement(name="sample2", id=dataset2.id), ], ) dataset_collection = self.hist.create_dataset_collection(collection_description) assert len(self.hist.content_infos) == 3 input_map = {"0": dataset_collection, "1": dataset1} inv = self.wf.invoke(input_map, history=self.hist) inv.wait() self.hist.refresh() assert len(self.hist.content_infos) == 6 last_step = inv.sorted_steps_by()[-1] out_hdca = last_step.get_output_collections()["out_file1"] assert out_hdca.collection_type == "list" assert len(out_hdca.elements) == 2 assert out_hdca.container.id == self.hist.id class TestJob(GalaxyObjectsTestBase): def test_get(self): job_prevs = self.gi.jobs.get_previews() if len(job_prevs) > 0: job_prev = job_prevs[0] assert isinstance(job_prev, wrappers.JobPreview) job = self.gi.jobs.get(job_prev.id) assert isinstance(job, wrappers.Job) assert job.id == job_prev.id for job in self.gi.jobs.list(): assert isinstance(job, wrappers.Job) bioblend-1.2.0/bioblend/_tests/TestGalaxyQuotas.py000066400000000000000000000040701444761704300222160ustar00rootroot00000000000000import uuid from . import GalaxyTestBase class TestGalaxyQuotas(GalaxyTestBase.GalaxyTestBase): def setUp(self): super().setUp() # Quota names must be unique, and they're impossible to delete # without accessing the database. self.quota_name = f"BioBlend-Test-Quota-{uuid.uuid4().hex}" self.quota = self.gi.quotas.create_quota(self.quota_name, "testing", "100 GB", "=", default="registered") def tearDown(self): self.gi.quotas.update_quota(self.quota["id"], default="registered") self.gi.quotas.update_quota(self.quota["id"], default="no") self.gi.quotas.delete_quota(self.quota["id"]) def test_create_quota(self): quota = self.gi.quotas.show_quota(self.quota["id"]) assert quota["name"] == self.quota_name assert quota["bytes"] == 107374182400 assert quota["operation"] == "=" assert quota["description"] == "testing" def test_get_quotas(self): quotas = self.gi.quotas.get_quotas() assert self.quota["id"] in [quota["id"] for quota in quotas] def test_update_quota(self): response = self.gi.quotas.update_quota( self.quota["id"], name=self.quota_name + "-new", description="asdf", default="registered", operation="-", amount=".01 TB", ) assert f"""Quota '{self.quota_name}' has been renamed to '{self.quota_name}-new'""" in response quota = self.gi.quotas.show_quota(self.quota["id"]) assert quota["name"] == self.quota_name + "-new" assert quota["bytes"] == 10995116277 assert quota["operation"] == "-" assert quota["description"] == "asdf" def test_delete_undelete_quota(self): self.gi.quotas.update_quota(self.quota["id"], default="no") response = self.gi.quotas.delete_quota(self.quota["id"]) assert response == "Deleted 1 quotas: " + self.quota_name response = self.gi.quotas.undelete_quota(self.quota["id"]) assert response == "Undeleted 1 quotas: " + self.quota_name bioblend-1.2.0/bioblend/_tests/TestGalaxyRoles.py000066400000000000000000000014341444761704300220270ustar00rootroot00000000000000import uuid from . import GalaxyTestBase class TestGalaxyRoles(GalaxyTestBase.GalaxyTestBase): def setUp(self): super().setUp() self.name = f"test_{uuid.uuid4().hex}" self.description = "automated test role" self.role = self.gi.roles.create_role(self.name, self.description) def tearDown(self): # As of 2017/07/26, deleting a role is not possible through the API pass def test_get_roles(self): roles = self.gi.roles.get_roles() for role in roles: assert role["id"] is not None assert role["name"] is not None def test_create_role(self): assert self.role["name"] == self.name assert self.role["description"] == self.description assert self.role["id"] is not None bioblend-1.2.0/bioblend/_tests/TestGalaxyToolContainerResolution.py000066400000000000000000000110761444761704300256120ustar00rootroot00000000000000""" Test functions in bioblend.galaxy.container_resolution """ from . import ( GalaxyTestBase, test_util, ) class TestGalaxyContainerResolution(GalaxyTestBase.GalaxyTestBase): @test_util.skip_unless_galaxy("release_22.05") def test_get_container_resolvers(self): container_resolvers = self.gi.container_resolution.get_container_resolvers() assert isinstance(container_resolvers, list) assert len(container_resolvers) > 0 assert isinstance(container_resolvers[0], dict) assert container_resolvers[0]["model_class"] == "ExplicitContainerResolver" assert container_resolvers[0]["resolver_type"] == "explicit" assert container_resolvers[0]["can_uninstall_dependencies"] is False assert container_resolvers[0]["builds_on_resolution"] is False @test_util.skip_unless_galaxy("release_22.05") def test_show_container_resolver(self): container_resolver = self.gi.container_resolution.show_container_resolver(0) print(container_resolver) assert isinstance(container_resolver, dict) assert container_resolver["model_class"] == "ExplicitContainerResolver" assert container_resolver["resolver_type"] == "explicit" assert container_resolver["can_uninstall_dependencies"] is False assert container_resolver["builds_on_resolution"] is False @test_util.skip_unless_galaxy("release_22.05") def test_resolve(self): tool = self.gi.container_resolution.resolve(tool_id="CONVERTER_parquet_to_csv") print(tool) assert isinstance(tool, dict) tool_requirements_only = self.gi.container_resolution.resolve( tool_id="CONVERTER_parquet_to_csv", requirements_only=True ) assert isinstance(tool_requirements_only, dict) @test_util.skip_unless_galaxy("release_22.05") def test_resolve_toolbox(self): toolbox = self.gi.container_resolution.resolve_toolbox() assert isinstance(toolbox, list) assert len(toolbox) > 0 assert isinstance(toolbox[0], dict) toolbox_by_tool_ids = self.gi.container_resolution.resolve_toolbox(tool_ids=[toolbox[0]["tool_id"]]) assert isinstance(toolbox_by_tool_ids, list) assert len(toolbox_by_tool_ids) == 1 assert isinstance(toolbox_by_tool_ids[0], dict) toolbox_by_resolver_type = self.gi.container_resolution.resolve_toolbox(resolver_type="mulled") assert isinstance(toolbox_by_resolver_type, list) assert len(toolbox_by_resolver_type) > 0 assert isinstance(toolbox_by_resolver_type[0], dict) assert len(toolbox) == len(toolbox_by_resolver_type) for tool in toolbox_by_resolver_type: print(tool) assert ( tool["status"]["dependency_type"] is None or tool["status"]["container_resolver"]["resolver_type"] == "mulled" ) toolbox_by_container_type = self.gi.container_resolution.resolve_toolbox(container_type="docker") assert isinstance(toolbox_by_container_type, list) assert len(toolbox_by_container_type) > 0 assert isinstance(toolbox_by_container_type[0], dict) assert len(toolbox) == len(toolbox_by_container_type) for tool in toolbox_by_container_type: assert tool["status"]["dependency_type"] is None or tool["status"]["dependency_type"] == "docker" assert ( tool["status"]["dependency_type"] is None or tool["status"]["container_description"]["type"] == "docker" ) toolbox_requirements_only = self.gi.container_resolution.resolve_toolbox(requirements_only=True) assert isinstance(toolbox_requirements_only, list) assert len(toolbox_requirements_only) > 0 assert isinstance(toolbox_requirements_only[0], dict) assert len(toolbox) == len(toolbox_requirements_only) # TODO unless containers are available this may fallback to conda by default? # depending on Galaxy's config # toolbox_by_index = self.gi.container_resolution.resolve_toolbox(tool_ids=[toolbox[0]['tool_id']], index=0, install=True) # assert isinstance(toolbox_by_index, list) # assert len(toolbox_by_index) > 0 # assert isinstance(toolbox_by_index[0], dict) # TODO unless containers are available this may fallback to conda by default? # depending on Galaxy's config # def test_resolve_toolbox_with_install(self): # toolbox = self.gi.container_resolution.resolve_toolbox_with_install(tool_ids=[]) # assert isinstance(toolbox, list) # assert len(toolbox) == 0 bioblend-1.2.0/bioblend/_tests/TestGalaxyToolData.py000066400000000000000000000010321444761704300224440ustar00rootroot00000000000000from . import GalaxyTestBase class TestGalaxyToolData(GalaxyTestBase.GalaxyTestBase): def test_get_data_tables(self): tables = self.gi.tool_data.get_data_tables() for table in tables: assert table["name"] is not None def test_show_data_table(self): tables = self.gi.tool_data.get_data_tables() table = self.gi.tool_data.show_data_table(tables[0]["name"]) assert table["columns"] is not None assert table["fields"] is not None assert table["name"] is not None bioblend-1.2.0/bioblend/_tests/TestGalaxyToolDependencies.py000066400000000000000000000031241444761704300241650ustar00rootroot00000000000000""" Test functions in bioblend.galaxy.tool_dependencies """ from . import ( GalaxyTestBase, test_util, ) class TestGalaxyToolDependencies(GalaxyTestBase.GalaxyTestBase): @test_util.skip_unless_galaxy("release_20.01") def test_summarize_toolbox(self): toolbox_summary = self.gi.tool_dependencies.summarize_toolbox() assert isinstance(toolbox_summary, list) assert len(toolbox_summary) > 0 toolbox_summary_by_tool = self.gi.tool_dependencies.summarize_toolbox(index_by="tools") assert isinstance(toolbox_summary_by_tool, list) assert len(toolbox_summary_by_tool) > 0 assert isinstance(toolbox_summary_by_tool[0], dict) assert "tool_ids" in toolbox_summary_by_tool[0] assert isinstance(toolbox_summary_by_tool[0]["tool_ids"], list) tool_id = toolbox_summary_by_tool[0]["tool_ids"][0] toolbox_summary_select_tool_ids = self.gi.tool_dependencies.summarize_toolbox( index_by="tools", tool_ids=[tool_id] ) assert isinstance(toolbox_summary_select_tool_ids, list) assert len(toolbox_summary_select_tool_ids) == 1 assert toolbox_summary_select_tool_ids[0]["tool_ids"][0] == tool_id @test_util.skip_unless_galaxy("release_20.01") def test_unused_dependency_paths(self): unused_paths = self.gi.tool_dependencies.unused_dependency_paths() assert isinstance(unused_paths, list) @test_util.skip_unless_galaxy("release_20.01") def test_delete_unused_dependency_paths(self): self.gi.tool_dependencies.delete_unused_dependency_paths(paths=[]) bioblend-1.2.0/bioblend/_tests/TestGalaxyToolInputs.py000066400000000000000000000024761444761704300230720ustar00rootroot00000000000000from bioblend.galaxy.tools.inputs import ( conditional, dataset, inputs, repeat, ) def test_conditional(): # Build up example inputs for random_lines1 as_dict = ( inputs() .set("num_lines", 5) .set("input", dataset("encoded1")) .set("seed_source", conditional().set("seed_source_selector", "set_seed").set("seed", "asdf")) .to_dict() ) assert as_dict["num_lines"] == 5 assert as_dict["input"]["src"] == "hda" assert as_dict["input"]["id"] == "encoded1" assert as_dict["seed_source|seed_source_selector"] == "set_seed" assert as_dict["seed_source|seed"] == "asdf" def test_repeat(): # Build up inputs for cat1 as_dict = ( inputs() .set("input1", dataset("encoded1")) .set( "queries", repeat() .instance(inputs().set_dataset_param("input2", "encoded2")) .instance(inputs().set_dataset_param("input2", "encoded3")), ) .to_dict() ) assert as_dict["input1"]["src"] == "hda" assert as_dict["input1"]["id"] == "encoded1" assert as_dict["queries_0|input2"]["src"] == "hda" assert as_dict["queries_0|input2"]["id"] == "encoded2" assert as_dict["queries_1|input2"]["src"] == "hda" assert as_dict["queries_1|input2"]["id"] == "encoded3" bioblend-1.2.0/bioblend/_tests/TestGalaxyTools.py000066400000000000000000000164051444761704300220470ustar00rootroot00000000000000""" """ import os from typing import ( Any, Dict, ) from bioblend.galaxy.tools.inputs import ( conditional, dataset, inputs, repeat, ) from . import ( GalaxyTestBase, test_util, ) class TestGalaxyTools(GalaxyTestBase.GalaxyTestBase): def test_get_tools(self): # Test requires target Galaxy is configured with at least one tool. tools = self.gi.tools.get_tools() assert len(tools) > 0 assert all(map(self._assert_is_tool_rep, tools)) def test_get_tool_panel(self): # Test requires target Galaxy is configured with at least one tool # section. tool_panel = self.gi.tools.get_tool_panel() sections = [s for s in tool_panel if "elems" in s] assert len(sections) > 0 assert all(map(self._assert_is_tool_rep, sections[0]["elems"])) def _assert_is_tool_rep(self, data): assert data["model_class"].endswith("Tool") # Special tools like SetMetadataTool may have different model_class # than Tool - but they all seem to end in tool. for key in ["name", "id", "version"]: assert key in data return True def test_paste_content(self): history = self.gi.histories.create_history(name="test_paste_data history") paste_text = "line 1\nline 2\rline 3\r\nline 4" tool_output = self.gi.tools.paste_content(paste_text, history["id"]) assert len(tool_output["outputs"]) == 1 # All lines in the resulting dataset should end with "\n" expected_contents = ("\n".join(paste_text.splitlines()) + "\n").encode() self._wait_and_verify_dataset(tool_output["outputs"][0]["id"], expected_contents) # Same with space_to_tab=True tool_output = self.gi.tools.paste_content(paste_text, history["id"], space_to_tab=True) assert len(tool_output["outputs"]) == 1 expected_contents = ("\n".join("\t".join(_.split()) for _ in paste_text.splitlines()) + "\n").encode() self._wait_and_verify_dataset(tool_output["outputs"][0]["id"], expected_contents) def test_upload_file(self): history = self.gi.histories.create_history(name="test_upload_file history") fn = test_util.get_abspath("test_util.py") file_name = "test1" tool_output = self.gi.tools.upload_file( fn, # First param could be a regular path also of course... history_id=history["id"], file_name=file_name, dbkey="?", file_type="txt", ) self._wait_for_and_verify_upload(tool_output, file_name, fn, expected_dbkey="?") def test_upload_file_dbkey(self): history = self.gi.histories.create_history(name="test_upload_file history") fn = test_util.get_abspath("test_util.py") file_name = "test1" dbkey = "hg19" tool_output = self.gi.tools.upload_file( fn, history_id=history["id"], file_name=file_name, dbkey=dbkey, file_type="txt", ) self._wait_for_and_verify_upload(tool_output, file_name, fn, expected_dbkey=dbkey) @test_util.skip_unless_tool("random_lines1") def test_run_random_lines(self): # Run second test case from randomlines.xml history_id = self.gi.histories.create_history(name="test_run_random_lines history")["id"] with open(test_util.get_abspath(os.path.join("data", "1.bed"))) as f: contents = f.read() dataset_id = self._test_dataset(history_id, contents=contents) tool_inputs = ( inputs() .set("num_lines", "1") .set("input", dataset(dataset_id)) .set("seed_source", conditional().set("seed_source_selector", "set_seed").set("seed", "asdf")) ) tool_output = self.gi.tools.run_tool(history_id=history_id, tool_id="random_lines1", tool_inputs=tool_inputs) assert len(tool_output["outputs"]) == 1 # TODO: Wait for results and verify has 1 line and is # chr5 131424298 131424460 CCDS4149.1_cds_0_0_chr5_131424299_f 0 + @test_util.skip_unless_tool("cat1") def test_run_cat1(self): history_id = self.gi.histories.create_history(name="test_run_cat1 history")["id"] dataset1_id = self._test_dataset(history_id, contents="1 2 3") dataset2_id = self._test_dataset(history_id, contents="4 5 6") dataset3_id = self._test_dataset(history_id, contents="7 8 9") tool_inputs = ( inputs() .set("input1", dataset(dataset1_id)) .set( "queries", repeat() .instance(inputs().set("input2", dataset(dataset2_id))) .instance(inputs().set("input2", dataset(dataset3_id))), ) ) tool_output = self.gi.tools.run_tool(history_id=history_id, tool_id="cat1", tool_inputs=tool_inputs) assert len(tool_output["outputs"]) == 1 # TODO: Wait for results and verify it has 3 lines - 1 2 3, 4 5 6, # and 7 8 9. @test_util.skip_unless_galaxy("release_19.05") @test_util.skip_unless_tool("CONVERTER_fasta_to_bowtie_color_index") def test_tool_dependency_install(self): installed_dependencies = self.gi.tools.install_dependencies("CONVERTER_fasta_to_bowtie_color_index") assert any( True for d in installed_dependencies if d.get("name") == "bowtie" and d.get("dependency_type") == "conda" ), f"installed_dependencies is {installed_dependencies}" status = self.gi.tools.uninstall_dependencies("CONVERTER_fasta_to_bowtie_color_index") assert status[0]["model_class"] == "NullDependency", status @test_util.skip_unless_tool("CONVERTER_fasta_to_bowtie_color_index") def test_tool_requirements(self): tool_requirements = self.gi.tools.requirements("CONVERTER_fasta_to_bowtie_color_index") assert any( True for tr in tool_requirements if {"dependency_type", "version"} <= set(tr.keys()) and tr.get("name") == "bowtie" ), f"tool_requirements is {tool_requirements}" @test_util.skip_unless_tool("CONVERTER_fasta_to_bowtie_color_index") def test_reload(self): response = self.gi.tools.reload("CONVERTER_fasta_to_bowtie_color_index") assert isinstance(response, dict) assert "message" in response assert "id" in response["message"] @test_util.skip_unless_tool("sra_source") def test_get_citations(self): citations = self.gi.tools.get_citations("sra_source") assert len(citations) == 2 def _wait_for_and_verify_upload( self, tool_output: Dict[str, Any], file_name: str, fn: str, expected_dbkey: str = "?" ) -> None: assert len(tool_output["outputs"]) == 1 output = tool_output["outputs"][0] assert output["name"] == file_name expected_contents = open(fn, "rb").read() self._wait_and_verify_dataset(output["id"], expected_contents) assert output["genome_build"] == expected_dbkey @test_util.skip_unless_tool("random_lines1") def test_get_tool_model(self): history_id = self.gi.histories.create_history(name="test_run_random_lines history")["id"] tool_model = self.gi.tools.build(tool_id="random_lines1", history_id=history_id) assert len(tool_model["inputs"]) == 3 bioblend-1.2.0/bioblend/_tests/TestGalaxyUsers.py000066400000000000000000000143411444761704300220450ustar00rootroot00000000000000import bioblend.galaxy from . import ( GalaxyTestBase, test_util, ) class TestGalaxyUsers(GalaxyTestBase.GalaxyTestBase): def test_get_users(self): users = self.gi.users.get_users() for user in users: assert user["id"] is not None assert user["email"] is not None def test_show_user(self): current_user = self.gi.users.get_current_user() user = self.gi.users.show_user(current_user["id"]) assert user["id"] == current_user["id"] assert user["username"] == current_user["username"] assert user["email"] == current_user["email"] # The 2 following tests randomly fail # assert user["nice_total_disk_usage"] == current_user["nice_total_disk_usage"] # assert user["total_disk_usage"] == current_user["total_disk_usage"] def test_create_remote_user(self): # WARNING: only admins can create users! # WARNING: Users cannot be purged through the Galaxy API, so execute # this test only on a disposable Galaxy instance! if not self.gi.config.get_config()["use_remote_user"]: self.skipTest("This Galaxy instance is not configured to use remote users") new_user_email = "newuser@example.org" user = self.gi.users.create_remote_user(new_user_email) assert user["email"] == new_user_email if self.gi.config.get_config()["allow_user_deletion"]: deleted_user = self.gi.users.delete_user(user["id"]) assert deleted_user["email"] == new_user_email assert deleted_user["deleted"] def test_create_local_user(self): # WARNING: only admins can create users! # WARNING: Users cannot be purged through the Galaxy API, so execute # this test only on a disposable Galaxy instance! if self.gi.config.get_config()["use_remote_user"]: self.skipTest("This Galaxy instance is not configured to use local users") new_username = test_util.random_string() new_user_email = f"{new_username}@example.org" password = test_util.random_string(20) new_user = self.gi.users.create_local_user(new_username, new_user_email, password) assert new_user["username"] == new_username assert new_user["email"] == new_user_email # test a BioBlend GalaxyInstance can be created using username+password user_gi = bioblend.galaxy.GalaxyInstance(url=self.gi.base_url, email=new_user_email, password=password) assert user_gi.users.get_current_user()["email"] == new_user_email # test deletion if self.gi.config.get_config()["allow_user_deletion"]: deleted_user = self.gi.users.delete_user(new_user["id"]) assert deleted_user["email"] == new_user_email assert deleted_user["deleted"] def test_get_current_user(self): user = self.gi.users.get_current_user() assert user["id"] is not None assert user["username"] is not None assert user["email"] is not None assert user["nice_total_disk_usage"] is not None assert user["total_disk_usage"] is not None def test_update_user(self): # WARNING: only admins can create users! # WARNING: Users cannot be purged through the Galaxy API, so execute # this test only on a disposable Galaxy instance! if self.gi.config.get_config()["use_remote_user"]: self.skipTest("This Galaxy instance is not configured to use local users") new_username = test_util.random_string() new_user = self.gi.users.create_local_user( new_username, f"{new_username}@example.org", test_util.random_string(20) ) new_user_id = new_user["id"] updated_username = test_util.random_string() updated_user_email = f"{updated_username}@example.org" self.gi.users.update_user(new_user_id, username=updated_username, email=updated_user_email) updated_user = self.gi.users.show_user(new_user_id) assert updated_user["username"] == updated_username assert updated_user["email"] == updated_user_email if self.gi.config.get_config()["allow_user_deletion"]: self.gi.users.delete_user(new_user_id) def test_get_user_apikey(self): # Test getting the API key of the current user, which surely has one user_id = self.gi.users.get_current_user()["id"] apikey = self.gi.users.get_user_apikey(user_id) assert apikey and apikey != "Not available." # Test getting the API key of a new user, which doesn't have one new_username = test_util.random_string() new_user_id = self.gi.users.create_local_user( new_username, f"{new_username}@example.org", test_util.random_string(20) )["id"] assert self.gi.users.get_user_apikey(new_user_id) == "Not available." @test_util.skip_unless_galaxy("release_21.01") def test_get_or_create_user_apikey(self): # Check that get_or_create_user_apikey() doesn't regenerate an existing API key user_id = self.gi.users.get_current_user()["id"] apikey = self.gi.users.get_user_apikey(user_id) assert self.gi.users.get_or_create_user_apikey(user_id) == apikey # Check that get_or_create_user_apikey() generates an API key for a new user new_username = test_util.random_string() new_user_id = self.gi.users.create_local_user( new_username, f"{new_username}@example.org", test_util.random_string(20) )["id"] new_apikey = self.gi.users.get_or_create_user_apikey(new_user_id) assert new_apikey and new_apikey != "Not available." def test_create_user_apikey(self): # Test creating an API key for a new user new_username = test_util.random_string() new_user_id = self.gi.users.create_local_user( new_username, f"{new_username}@example.org", test_util.random_string(20) )["id"] new_apikey = self.gi.users.create_user_apikey(new_user_id) assert new_apikey and new_apikey != "Not available." # Test regenerating an API key for a user that already has one regenerated_apikey = self.gi.users.create_user_apikey(new_user_id) assert regenerated_apikey and regenerated_apikey not in (new_apikey, "Not available.") bioblend-1.2.0/bioblend/_tests/TestGalaxyWorkflows.py000066400000000000000000000324421444761704300227430ustar00rootroot00000000000000import json import os import shutil import tempfile import time from typing import ( Any, Dict, List, ) import pytest from bioblend import ConnectionError from . import ( GalaxyTestBase, test_util, ) class TestGalaxyWorkflows(GalaxyTestBase.GalaxyTestBase): @test_util.skip_unless_tool("cat1") @test_util.skip_unless_tool("cat") def test_workflow_scheduling(self): path = test_util.get_abspath(os.path.join("data", "test_workflow_pause.ga")) workflow = self.gi.workflows.import_workflow_from_local_path(path) workflow_id = workflow["id"] history_id = self.gi.histories.create_history(name="TestWorkflowState")["id"] invocations = self.gi.workflows.get_invocations(workflow_id) assert len(invocations) == 0 # Try invalid invocation (no input) with pytest.raises(ConnectionError): self.gi.workflows.invoke_workflow(workflow["id"]) dataset1_id = self._test_dataset(history_id) invocation = self.gi.workflows.invoke_workflow( workflow["id"], inputs={"0": {"src": "hda", "id": dataset1_id}}, ) assert invocation["state"] == "new" invocation_id = invocation["id"] invocations = self.gi.workflows.get_invocations(workflow_id) assert len(invocations) == 1 assert invocations[0]["id"] == invocation_id def invocation_steps_by_order_index() -> Dict[int, Dict[str, Any]]: invocation = self.gi.workflows.show_invocation(workflow_id, invocation_id) return {s["order_index"]: s for s in invocation["steps"]} for _ in range(20): if 2 in invocation_steps_by_order_index(): break time.sleep(0.5) invocation = self.gi.workflows.show_invocation(workflow_id, invocation_id) assert invocation["state"] == "ready" steps = invocation_steps_by_order_index() pause_step = steps[2] assert self.gi.workflows.show_invocation_step(workflow_id, invocation_id, pause_step["id"])["action"] is None self.gi.workflows.run_invocation_step_action(workflow_id, invocation_id, pause_step["id"], action=True) assert self.gi.workflows.show_invocation_step(workflow_id, invocation_id, pause_step["id"])["action"] for _ in range(20): invocation = self.gi.workflows.show_invocation(workflow_id, invocation_id) if invocation["state"] == "scheduled": break time.sleep(0.5) invocation = self.gi.workflows.show_invocation(workflow_id, invocation_id) assert invocation["state"] == "scheduled" def test_invoke_workflow_parameters_normalized(self): path = test_util.get_abspath(os.path.join("data", "paste_columns_subworkflow.ga")) workflow_id = self.gi.workflows.import_workflow_from_local_path(path)["id"] history_id = self.gi.histories.create_history(name="TestWorkflowInvokeParametersNormalized")["id"] dataset_id = self._test_dataset(history_id) with pytest.raises(ConnectionError): self.gi.workflows.invoke_workflow( workflow_id, inputs={"0": {"src": "hda", "id": dataset_id}}, params={"1": {"1|2": "comma"}} ) self.gi.workflows.invoke_workflow( workflow_id, inputs={"0": {"src": "hda", "id": dataset_id}}, params={"1": {"1|2": "comma"}}, parameters_normalized=True, ) @test_util.skip_unless_galaxy("release_19.09") @test_util.skip_unless_tool("cat1") @test_util.skip_unless_tool("cat") def test_cancelling_workflow_scheduling(self): path = test_util.get_abspath(os.path.join("data", "test_workflow_pause.ga")) workflow = self.gi.workflows.import_workflow_from_local_path(path) workflow_id = workflow["id"] history_id = self.gi.histories.create_history(name="TestWorkflowState")["id"] dataset1_id = self._test_dataset(history_id) invocations = self.gi.workflows.get_invocations(workflow_id) assert len(invocations) == 0 invocation = self.gi.workflows.invoke_workflow( workflow["id"], inputs={"0": {"src": "hda", "id": dataset1_id}}, ) invocation_id = invocation["id"] invocations = self.gi.workflows.get_invocations(workflow_id) assert len(invocations) == 1 assert invocations[0]["id"] == invocation_id invocation = self.gi.workflows.show_invocation(workflow_id, invocation_id) assert invocation["state"] in ["new", "ready"] self.gi.workflows.cancel_invocation(workflow_id, invocation_id) invocation = self.gi.invocations.wait_for_invocation(invocation_id, check=False) assert invocation["state"] == "cancelled" def test_import_export_workflow_from_local_path(self): with pytest.raises(TypeError): self.gi.workflows.import_workflow_from_local_path(None) # type: ignore[arg-type] path = test_util.get_abspath(os.path.join("data", "paste_columns.ga")) imported_wf = self.gi.workflows.import_workflow_from_local_path(path) assert isinstance(imported_wf, dict) assert imported_wf["name"] == "paste_columns" assert imported_wf["url"].startswith("/api/workflows/") assert not imported_wf["deleted"] assert not imported_wf["published"] with pytest.raises(TypeError): self.gi.workflows.export_workflow_to_local_path(None, None, None) # type: ignore[arg-type] export_dir = tempfile.mkdtemp(prefix="bioblend_test_") try: self.gi.workflows.export_workflow_to_local_path(imported_wf["id"], export_dir) dir_contents = os.listdir(export_dir) assert len(dir_contents) == 1 export_path = os.path.join(export_dir, dir_contents[0]) with open(export_path) as f: exported_wf_dict = json.load(f) finally: shutil.rmtree(export_dir) assert isinstance(exported_wf_dict, dict) def test_import_publish_workflow_from_local_path(self): path = test_util.get_abspath(os.path.join("data", "paste_columns.ga")) imported_wf = self.gi.workflows.import_workflow_from_local_path(path, publish=True) assert isinstance(imported_wf, dict) assert not imported_wf["deleted"] assert imported_wf["published"] def test_import_export_workflow_dict(self): path = test_util.get_abspath(os.path.join("data", "paste_columns.ga")) with open(path) as f: wf_dict = json.load(f) imported_wf = self.gi.workflows.import_workflow_dict(wf_dict) assert isinstance(imported_wf, dict) assert imported_wf["name"] == "paste_columns" assert imported_wf["url"].startswith("/api/workflows/") assert not imported_wf["deleted"] assert not imported_wf["published"] exported_wf_dict = self.gi.workflows.export_workflow_dict(imported_wf["id"]) assert isinstance(exported_wf_dict, dict) def test_import_publish_workflow_dict(self): path = test_util.get_abspath(os.path.join("data", "paste_columns.ga")) with open(path) as f: wf_dict = json.load(f) imported_wf = self.gi.workflows.import_workflow_dict(wf_dict, publish=True) assert isinstance(imported_wf, dict) assert not imported_wf["deleted"] assert imported_wf["published"] def test_get_workflows(self): path = test_util.get_abspath(os.path.join("data", "paste_columns.ga")) workflow = self.gi.workflows.import_workflow_from_local_path(path) all_wfs = self.gi.workflows.get_workflows() assert len(all_wfs) > 0 wfs_with_name = self.gi.workflows.get_workflows(name=workflow["name"]) wf_list = [w for w in wfs_with_name if w["id"] == workflow["id"]] assert len(wf_list) == 1 wf_data = wf_list[0] if "create_time" in workflow: # Galaxy >= 20.01 assert wf_data["create_time"] == workflow["create_time"] else: # Galaxy <= 22.01 assert wf_data["url"] == workflow["url"] def test_show_workflow(self): path = test_util.get_abspath(os.path.join("data", "paste_columns.ga")) wf = self.gi.workflows.import_workflow_from_local_path(path) wf_data = self.gi.workflows.show_workflow(wf["id"]) assert wf_data["id"] == wf["id"] assert wf_data["name"] == wf["name"] assert wf_data["url"] == wf["url"] assert len(wf_data["steps"]) == 3 assert wf_data["inputs"] is not None def test_update_workflow_name(self): path = test_util.get_abspath(os.path.join("data", "paste_columns.ga")) wf = self.gi.workflows.import_workflow_from_local_path(path) new_name = "new name" updated_wf = self.gi.workflows.update_workflow(wf["id"], name=new_name) assert updated_wf["name"] == new_name @test_util.skip_unless_galaxy("release_21.01") def test_update_workflow_published(self): path = test_util.get_abspath(os.path.join("data", "paste_columns.ga")) wf = self.gi.workflows.import_workflow_from_local_path(path) assert not wf["published"] updated_wf = self.gi.workflows.update_workflow(wf["id"], published=True) assert updated_wf["published"] updated_wf = self.gi.workflows.update_workflow(wf["id"], published=False) assert not updated_wf["published"] @test_util.skip_unless_galaxy( "release_19.09" ) # due to Galaxy bug fixed in https://github.com/galaxyproject/galaxy/pull/9014 def test_show_workflow_versions(self): path = test_util.get_abspath(os.path.join("data", "paste_columns.ga")) wf = self.gi.workflows.import_workflow_from_local_path(path) wf_data = self.gi.workflows.show_workflow(wf["id"]) assert wf_data["version"] == 0 new_name = "new name" self.gi.workflows.update_workflow(wf["id"], name=new_name) updated_wf = self.gi.workflows.show_workflow(wf["id"]) assert updated_wf["name"] == new_name assert updated_wf["version"] == 1 updated_wf = self.gi.workflows.show_workflow(wf["id"], version=0) assert updated_wf["name"] == "paste_columns" assert updated_wf["version"] == 0 updated_wf = self.gi.workflows.show_workflow(wf["id"], version=1) assert updated_wf["name"] == new_name assert updated_wf["version"] == 1 @test_util.skip_unless_galaxy("release_19.09") def test_extract_workflow_from_history(self): path = test_util.get_abspath(os.path.join("data", "paste_columns.ga")) wf = self.gi.workflows.import_workflow_from_local_path(path) history_id = self.gi.histories.create_history(name="test_wf_invocation")["id"] dataset1_id = self._test_dataset(history_id) dataset = {"src": "hda", "id": dataset1_id} invocation_id = self.gi.workflows.invoke_workflow( wf["id"], inputs={"Input 1": dataset, "Input 2": dataset}, history_id=history_id, inputs_by="name", )["id"] invocation = self.gi.invocations.wait_for_invocation(invocation_id) wf1 = self.gi.workflows.show_workflow(invocation["workflow_id"]) datasets = self.gi.histories.show_history(invocation["history_id"], contents=True) dataset_hids = [dataset["hid"] for dataset in datasets] job_ids = [step["job_id"] for step in invocation["steps"] if step["job_id"]] for job_id in job_ids: self.gi.jobs.wait_for_job(job_id) new_workflow_name = "My new workflow!" wf2 = self.gi.workflows.extract_workflow_from_history( history_id=invocation["history_id"], workflow_name=new_workflow_name, job_ids=job_ids, dataset_hids=dataset_hids, ) wf2 = self.gi.workflows.show_workflow(wf2["id"]) assert wf2["name"] == new_workflow_name assert len(wf1["steps"]) == len(wf2["steps"]) for i in range(len(wf1["steps"])): assert wf1["steps"][str(i)]["type"] == wf2["steps"][str(i)]["type"] assert wf1["steps"][str(i)]["tool_id"] == wf2["steps"][str(i)]["tool_id"] def test_show_versions(self): path = test_util.get_abspath(os.path.join("data", "paste_columns.ga")) wf = self.gi.workflows.import_workflow_from_local_path(path) versions = self.gi.workflows.show_versions(wf["id"]) assert len(versions) == 1 version = versions[0] assert version["version"] == 0 assert "update_time" in version assert "steps" in version @test_util.skip_unless_galaxy("release_21.01") def test_refactor_workflow(self): actions: List[Dict[str, Any]] = [ {"action_type": "add_input", "type": "data", "label": "foo"}, {"action_type": "update_step_label", "label": "bar", "step": {"label": "foo"}}, ] path = test_util.get_abspath(os.path.join("data", "paste_columns.ga")) wf = self.gi.workflows.import_workflow_from_local_path(path) response = self.gi.workflows.refactor_workflow(wf["id"], actions, dry_run=True) assert len(response["action_executions"]) == len(actions) assert response["dry_run"] is True updated_steps = response["workflow"]["steps"] assert len(updated_steps) == 4 assert {step["label"] for step in updated_steps.values()} == {"bar", None, "Input 1", "Input 2"} bioblend-1.2.0/bioblend/_tests/TestToolshed.py000066400000000000000000000066251444761704300213650ustar00rootroot00000000000000import os import unittest import bioblend import bioblend.toolshed from . import test_util @test_util.skip_unless_toolshed() class TestToolshed(unittest.TestCase): def setUp(self): toolshed_url = os.environ["BIOBLEND_TOOLSHED_URL"] self.ts = bioblend.toolshed.ToolShedInstance(url=toolshed_url) def test_categories_client(self): # get_categories categories = self.ts.categories.get_categories() assert "Assembly" in [c["name"] for c in categories] # we cannot test get_categories with deleted=True as it requires administrator status # show_category visualization_category_id = [c for c in categories if c["name"] == "Visualization"][0]["id"] visualization_category = self.ts.categories.show_category(visualization_category_id) assert visualization_category["description"] == "Tools for visualizing data" # get_repositories repositories = self.ts.categories.get_repositories(visualization_category_id) repositories_reversed = self.ts.categories.get_repositories(visualization_category_id, sort_order="desc") assert repositories["repositories"][0]["model_class"] == "Repository" assert len(repositories["repositories"]) > 200 assert repositories["repositories"][0] == repositories_reversed["repositories"][-1] def test_repositories_client(self): # get_repositories repositories = self.ts.repositories.get_repositories() assert len(repositories) > 5000 assert repositories[0]["model_class"] == "Repository" repositories = self.ts.repositories.get_repositories(name="bam_to_sam", owner="devteam") assert len(repositories) == 1 bam_to_sam_repo = repositories[0] assert bam_to_sam_repo["name"] == "bam_to_sam" assert bam_to_sam_repo["owner"] == "devteam" # search_repositories samtools_search = self.ts.repositories.search_repositories("samtools", page_size=5) assert int(samtools_search["total_results"]) > 20 assert len(samtools_search["hits"]) == 5 # show_repository show_bam_to_sam_repo = self.ts.repositories.show_repository(bam_to_sam_repo["id"]) assert "SAM" in show_bam_to_sam_repo["long_description"] # test_create_repository # need to provide an API key to test this # test_update_repository # need to provide an API key to test this def test_repositories_revisions(self): # get_ordered_installable_revisions bam_to_sam_revisions = self.ts.repositories.get_ordered_installable_revisions("bam_to_sam", "devteam") assert len(bam_to_sam_revisions) >= 4 # get_repository_revision_install_info bam_to_sam_revision_install_info = self.ts.repositories.get_repository_revision_install_info( "bam_to_sam", "devteam", bam_to_sam_revisions[0] ) assert len(bam_to_sam_revision_install_info) == 3 assert bam_to_sam_revision_install_info[0].get("model_class") == "Repository" assert bam_to_sam_revision_install_info[1].get("model_class") == "RepositoryMetadata" assert bam_to_sam_revision_install_info[2].get("model_class") is None def test_tools_client(self): # search_tools samtools_search = self.ts.tools.search_tools("samtools", page_size=5) assert int(samtools_search["total_results"]) > 2000 assert len(samtools_search["hits"]) == 5 bioblend-1.2.0/bioblend/_tests/__init__.py000066400000000000000000000000001444761704300204600ustar00rootroot00000000000000bioblend-1.2.0/bioblend/_tests/data/000077500000000000000000000000001444761704300172725ustar00rootroot00000000000000bioblend-1.2.0/bioblend/_tests/data/1.bed000066400000000000000000000101521444761704300201050ustar00rootroot00000000000000chr1 147962192 147962580 CCDS989.1_cds_0_0_chr1_147962193_r 0 - chr1 147984545 147984630 CCDS990.1_cds_0_0_chr1_147984546_f 0 + chr1 148078400 148078582 CCDS993.1_cds_0_0_chr1_148078401_r 0 - chr1 148185136 148185276 CCDS996.1_cds_0_0_chr1_148185137_f 0 + chr10 55251623 55253124 CCDS7248.1_cds_0_0_chr10_55251624_r 0 - chr11 116124407 116124501 CCDS8374.1_cds_0_0_chr11_116124408_r 0 - chr11 116206508 116206563 CCDS8377.1_cds_0_0_chr11_116206509_f 0 + chr11 116211733 116212337 CCDS8378.1_cds_0_0_chr11_116211734_r 0 - chr11 1812377 1812407 CCDS7726.1_cds_0_0_chr11_1812378_f 0 + chr12 38440094 38440321 CCDS8736.1_cds_0_0_chr12_38440095_r 0 - chr13 112381694 112381953 CCDS9526.1_cds_0_0_chr13_112381695_f 0 + chr14 98710240 98712285 CCDS9949.1_cds_0_0_chr14_98710241_r 0 - chr15 41486872 41487060 CCDS10096.1_cds_0_0_chr15_41486873_r 0 - chr15 41673708 41673857 CCDS10097.1_cds_0_0_chr15_41673709_f 0 + chr15 41679161 41679250 CCDS10098.1_cds_0_0_chr15_41679162_r 0 - chr15 41826029 41826196 CCDS10101.1_cds_0_0_chr15_41826030_f 0 + chr16 142908 143003 CCDS10397.1_cds_0_0_chr16_142909_f 0 + chr16 179963 180135 CCDS10401.1_cds_0_0_chr16_179964_r 0 - chr16 244413 244681 CCDS10402.1_cds_0_0_chr16_244414_f 0 + chr16 259268 259383 CCDS10403.1_cds_0_0_chr16_259269_r 0 - chr18 23786114 23786321 CCDS11891.1_cds_0_0_chr18_23786115_r 0 - chr18 59406881 59407046 CCDS11985.1_cds_0_0_chr18_59406882_f 0 + chr18 59455932 59456337 CCDS11986.1_cds_0_0_chr18_59455933_r 0 - chr18 59600586 59600754 CCDS11988.1_cds_0_0_chr18_59600587_f 0 + chr19 59068595 59069564 CCDS12866.1_cds_0_0_chr19_59068596_f 0 + chr19 59236026 59236146 CCDS12872.1_cds_0_0_chr19_59236027_r 0 - chr19 59297998 59298008 CCDS12877.1_cds_0_0_chr19_59297999_f 0 + chr19 59302168 59302288 CCDS12878.1_cds_0_0_chr19_59302169_r 0 - chr2 118288583 118288668 CCDS2120.1_cds_0_0_chr2_118288584_f 0 + chr2 118394148 118394202 CCDS2121.1_cds_0_0_chr2_118394149_r 0 - chr2 220190202 220190242 CCDS2441.1_cds_0_0_chr2_220190203_f 0 + chr2 220229609 220230869 CCDS2443.1_cds_0_0_chr2_220229610_r 0 - chr20 33330413 33330423 CCDS13249.1_cds_0_0_chr20_33330414_r 0 - chr20 33513606 33513792 CCDS13255.1_cds_0_0_chr20_33513607_f 0 + chr20 33579500 33579527 CCDS13256.1_cds_0_0_chr20_33579501_r 0 - chr20 33593260 33593348 CCDS13257.1_cds_0_0_chr20_33593261_f 0 + chr21 32707032 32707192 CCDS13614.1_cds_0_0_chr21_32707033_f 0 + chr21 32869641 32870022 CCDS13615.1_cds_0_0_chr21_32869642_r 0 - chr21 33321040 33322012 CCDS13620.1_cds_0_0_chr21_33321041_f 0 + chr21 33744994 33745040 CCDS13625.1_cds_0_0_chr21_33744995_r 0 - chr22 30120223 30120265 CCDS13897.1_cds_0_0_chr22_30120224_f 0 + chr22 30160419 30160661 CCDS13898.1_cds_0_0_chr22_30160420_r 0 - chr22 30665273 30665360 CCDS13901.1_cds_0_0_chr22_30665274_f 0 + chr22 30939054 30939266 CCDS13903.1_cds_0_0_chr22_30939055_r 0 - chr5 131424298 131424460 CCDS4149.1_cds_0_0_chr5_131424299_f 0 + chr5 131556601 131556672 CCDS4151.1_cds_0_0_chr5_131556602_r 0 - chr5 131621326 131621419 CCDS4152.1_cds_0_0_chr5_131621327_f 0 + chr5 131847541 131847666 CCDS4155.1_cds_0_0_chr5_131847542_r 0 - chr6 108299600 108299744 CCDS5061.1_cds_0_0_chr6_108299601_r 0 - chr6 108594662 108594687 CCDS5063.1_cds_0_0_chr6_108594663_f 0 + chr6 108640045 108640151 CCDS5064.1_cds_0_0_chr6_108640046_r 0 - chr6 108722976 108723115 CCDS5067.1_cds_0_0_chr6_108722977_f 0 + chr7 113660517 113660685 CCDS5760.1_cds_0_0_chr7_113660518_f 0 + chr7 116512159 116512389 CCDS5771.1_cds_0_0_chr7_116512160_r 0 - chr7 116714099 116714152 CCDS5773.1_cds_0_0_chr7_116714100_f 0 + chr7 116945541 116945787 CCDS5774.1_cds_0_0_chr7_116945542_r 0 - chr8 118881131 118881317 CCDS6324.1_cds_0_0_chr8_118881132_r 0 - chr9 128764156 128764189 CCDS6914.1_cds_0_0_chr9_128764157_f 0 + chr9 128787519 128789136 CCDS6915.1_cds_0_0_chr9_128787520_r 0 - chr9 128882427 128882523 CCDS6917.1_cds_0_0_chr9_128882428_f 0 + chr9 128937229 128937445 CCDS6919.1_cds_0_0_chr9_128937230_r 0 - chrX 122745047 122745924 CCDS14606.1_cds_0_0_chrX_122745048_f 0 + chrX 152648964 152649196 CCDS14733.1_cds_0_0_chrX_152648965_r 0 - chrX 152691446 152691471 CCDS14735.1_cds_0_0_chrX_152691447_f 0 + chrX 152694029 152694263 CCDS14736.1_cds_0_0_chrX_152694030_r 0 - bioblend-1.2.0/bioblend/_tests/data/Galaxy-History-Test-history-for-export.tar.gz000066400000000000000000000066471444761704300300410ustar00rootroot00000000000000‹Ū“Ē`’dataset_114237.datķ\[oŪFö«öWzéC×ŅÜ/y)ŗiŻ¶/-°]4A‘#[ % $•Ųäæļ™EQ“e;µ·mxDĒ£3ߜ™9—j†×ėŗ)«Ū$mšŖž77ĶÅoOH0f?±äøūéˆ!tXbųä"Œ_LŃŠоnŅj:½øJ‹ōę6٤ucŖ¹s߇¾“Ÿś8Ū”¹)’¬Hėzöj:ū»7ˆŁ_§³¬2ic’f½1ö˜|‰Ä%fS,_üŠć¹Ą1n…÷»üaöŠ‘¹œ b…·©—śÉŌĶōŚ·8]•ÕŌÜģŹŖ±"×ė<ÉŹżF$”\™m¹1Éræ.r[ł+–n·e“6ėr EŪ}Q@Y“^Ł®üņx³Ķ y²vU(ā’™åR ¤11zöéā '˜““6Mżœą~’ǘŁ÷*äč’/Aæ|t”e±Æ–ÖcŸVę ¾«]D(wkp¦UUn’ą¶I° āG™­Nb}÷:][Ą_f¹Ä˜f‚É„Pˆ‘|ööllqį‚ )˜¶ĶĘ&ö{ļĘ)ɉ^„ō2ēK}ÉøŠ—*ĖÉ„bFC!L¬–®ž)Lcl•UZŌʕŌė«mƳĒ1Bi*W&ēd™bʳĢ™›Ęlk_i¶D([­ “Ä@ŻhēšuĀęAčŚa2Ēłį:@r½]•ö»7Ūż®(SžŚŌ¶g掘&µĶČĒYv Sńŗ,ąģę§Ųo¶N«6†²ęvēĖfuSłfšĪĒq”żėP·8+7³m’b½uP(LM[ ø-Y¾3·m|†)XoÖ>ˆĻŽ8p³Ķ½Ę4d’sķ¦Uć’$^4 [ŽÆ’›Ą0¤†Āõ†½żdĒd0‡}ē'åۃavsŽs“3ę-€±Äo̤kY®Š›ęõėļ~ŌJĻq’åu‚ąŸ•N¢0MŖ7 ˆ]¾ŁPćŒGVЈ¢ŃŠÉŹ”|}@QH*XE–+Qč)Šʧŗ(¬8¦"²DŠˆ"Pœ°<Ö8Ī Ē‚PĻQL˜‘„© J‚0;čāĶ |ŽułnÖĖÓY>­Øš9;Ø×ĖĀDæ 8Å0śģ(¶+ˆ1 -)ę"KõC££)6ÅŲ*—9‘ü§Ėō’i²ŗŌB™fYŖČRč%"OŽb’InHJ œ«ĒE±”ɬ_«ź&‘p^ŽzÖč“ĀŚļ<†aōŠa~ž¦nž¦åvj{2ÅcPūć5Ž4eO jŸæ4;ĀČ+¬ę”ģ÷m/ŗ;Y7~žB  ±q!6ʬ§Ē,śŠ’9֊#|³šj’"ė°ó!Ė©HRāYW]³roH…Į³“Ču²V|†Å_ćāk dO_|!{%“Bģķ÷üwńŒmŲē”’ó;’Ażß°`ˆ]Lłųü÷åęæ³^~įē’sęĢ„I÷ū˜Įųü’Č×I›”&m‚šœĻO4¹üK[ßå¦I›š&ē3Ō’:ÖYiŅ&„ÉłœŌm?ä£I›Ž&ē³QŪ>šÄL4‰‰hņ€<Ō¶ `•’ƒå±CPT²Ø„U‚ Į‘Šœ BA8aŻé…‡ĄXRź9B©ŒjĀ ÷:¢ š”īÓvĒ ƒ$⓺TöɄ*čŅĢ3”„1“_$A”w·ZŖ`-Χ¹ĶO IęŲD+Č! ¹cQ<Ųė2K‚hאų„}%‰c$ŽŽ!-XO‚,ķI%Ģ¢c—-€p²ŻIō°!Įž!ü YŅÓ@ˆö Ö"Ąæ+KQGėCD[3d!*ÓõEāäōqU .©¦m‡šå˜čąÄqd{]€5Ó6 Ć-RŠĄĖvB(L”QPœF„@NU?Čć^¶ė @€VŚ3Bą@ €,A= ģ’z†Ę,Lu?•8'Ū×@ƒ0„BĒ!Z:ąd;cĖX É Ņ›  *XÆ=ąI”ķD$Ą¹° œ$€ć'KzĄś ģ3pv8p²²§b pąDąN¶£“‹tJŲE¹ćĄR‡Ś1äó ‹{°„ ‘S2Š'Ūq& `•h·Žƒ±‹lĄÉŠž …ää€“ķ ¢]ĶS˜Ž#'Ā‚’KŃóF™DYu 8¤®>$>p²¢ć @ĀŽXėĄĮōG:`%źi`W NĘYż­‰p²;p Ö7`"£ŲĄj© ’(ŪÅ!BDä"ra „īUDŁĪ,x%9œ¶½Ą‡@¶P\5Xv1Č'žć!·€p²Ŗ§¦Ö<ĒzĄŹŅŽ?C5₱ œ[LX,”^nū9‰Ā6ALAŗšœnö 6|'A˜é…0ä°< œÄ-B°Ā²Æ¬aķ08Ž„=ŖEƒ µćp1Ņļłłß}æ:=ļłXAˆöłŸDü–Æ Ēēćóæńłß—ńüoŒĆæŸūó]U¾7Ūt›™ŗ’Ū䎿p±1žæĢż·£|ÉT¬—UZ­Ķs^{¼’Ć6ZŒž?ś’HϾž/ »ļ³G2Ÿ-œŪ’Įv”ē’I4ś’‹ų’ĒŁĮ܉PS{Hӝ£k’JÖ9üæ^­ż9Ģ»>¶ņŪÜ܄#ž±Ģž µµÆóōōĢ,%LdxnҜ„jåNŗęžźł«M>æ9pŲ3H½n‡į{ÆļĢg€öĢzææų”ż]!®W¤Y–“ÕŹ,ļīļŠ…Ōēéļé…yĘó”Ą`+«€Z‘ŁąÜ Ųr·/Ņpś'4Ƙ ±Sī¾Cą˼÷©żŠVŗr½/!¦Į¶37UƼ†čD{ öxöĻŅmĄÜĖž`ēŪšŗćÕW¶²7N'||×ÕŻv/½sĻŚ¢øiN÷M Ćó™¾ż×÷G$’("IĀäŚ:InĄÉw•ńżqׯŚׂg÷8|,L¾ŌKJ“Ź•‘&c°kÓLŒ3ri@ß­~³Kb‚Ō»4Į2iŅeēE~e²+ėõM{?8Ŗ¾Æ pńŚļ—]æŻmė¹§>9ŗéœ$پŖ¬Ķe0gn|øBƒ:ŚIZ•Ś;l oČ]ŸÅ tƁxŠXŤD_—¹¹śąZé?Ų×WĀmé±AĒ¢ŽE»¢Žć9žK÷Ec»L³w‡½ónl(”Ļ©²‡Vż3°“ aŖāL~š·¬ėŵIóWą¦šļīęvŠ–0' 7k Ȟ ·½Šė‚į£Ē¤uWō}ČK}Ć÷/y:޾D§RII…\Į2‡döčėæ¹'ōżßCsxeĆš½zĮنR enšĪKÜžާеÅĶĢø0—Y¹]­Æœ\Z-¢‘ī³:[øŚ‹oꅣžĘą8zgļÓbCüŻoĆw‹Ļ—?9孃ž°ßųWmœ 0?^—¦KXim·0>ŸhŠ=)ŠøKųT#AŁģÓų j¤‘Fi¤‘Fi¤‘Fi¤‘Fi¤‘Fi¤‘Fi¤‘Fi¤‘žJ’aö·Čxbioblend-1.2.0/bioblend/_tests/data/paste_columns.ga000066400000000000000000000047411444761704300224650ustar00rootroot00000000000000{ "a_galaxy_workflow": "true", "annotation": "", "format-version": "0.1", "name": "paste_columns", "steps": { "0": { "annotation": "", "id": 0, "input_connections": {}, "inputs": [ { "description": "", "name": "Input 1" } ], "name": "Input dataset", "outputs": [], "position": { "left": 10, "top": 10 }, "tool_errors": null, "tool_id": null, "tool_state": "{\"name\": \"Input 1\"}", "tool_version": null, "type": "data_input", "user_outputs": [] }, "1": { "annotation": "", "id": 1, "input_connections": {}, "inputs": [ { "description": "", "name": "Input 2" } ], "name": "Input dataset", "outputs": [], "position": { "left": 10, "top": 130 }, "tool_errors": null, "tool_id": null, "tool_state": "{\"name\": \"Input 2\"}", "tool_version": null, "type": "data_input", "user_outputs": [] }, "2": { "annotation": "", "id": 2, "input_connections": { "input1": { "id": 0, "output_name": "output" }, "input2": { "id": 1, "output_name": "output" } }, "inputs": [], "name": "Paste", "outputs": [ { "name": "out_file1", "type": "input" } ], "position": { "left": 230, "top": 10 }, "post_job_actions": {}, "tool_errors": null, "tool_id": "Paste1", "tool_state": "{\"input2\": \"null\", \"__page__\": 0, \"input1\": \"null\", \"__rerun_remap_job_id__\": null, \"delimiter\": \"\\\"T\\\"\", \"chromInfo\": \"\\\"/home/simleo/hg/galaxy-dist/tool-data/shared/ucsc/chrom/?.len\\\"\"}", "tool_version": "1.0.0", "type": "tool", "user_outputs": [] } } } bioblend-1.2.0/bioblend/_tests/data/paste_columns_collections.ga000066400000000000000000000056031444761704300250610ustar00rootroot00000000000000{ "a_galaxy_workflow": "true", "annotation": "", "format-version": "0.1", "name": "paste_columns_collections", "steps": { "0": { "annotation": "", "id": 0, "input_connections": {}, "inputs": [ { "description": "", "name": "Input Dataset Collection" } ], "label": null, "name": "Input dataset collection", "outputs": [], "position": { "left": 119.5, "top": 200 }, "tool_errors": null, "tool_id": null, "tool_state": "{\"collection_type\": \"list\", \"name\": \"Input Dataset Collection\"}", "tool_version": null, "type": "data_collection_input", "user_outputs": [], "uuid": "88591325-c867-407a-a8df-df01430f2196" }, "1": { "annotation": "", "id": 1, "input_connections": {}, "inputs": [ { "description": "", "name": "Input 2" } ], "label": null, "name": "Input dataset", "outputs": [], "position": { "left": 200, "top": 434 }, "tool_errors": null, "tool_id": null, "tool_state": "{\"name\": \"Input 2\"}", "tool_version": null, "type": "data_input", "user_outputs": [], "uuid": "64008e61-3304-4452-96ce-9564ec55cf9f" }, "2": { "annotation": "", "id": 2, "input_connections": { "input1": { "id": 0, "output_name": "output" }, "input2": { "id": 1, "output_name": "output" } }, "inputs": [], "label": null, "name": "Paste", "outputs": [ { "name": "out_file1", "type": "input" } ], "position": { "left": 420, "top": 314 }, "post_job_actions": {}, "tool_errors": null, "tool_id": "Paste1", "tool_state": "{\"input2\": \"null\", \"__page__\": 0, \"input1\": \"null\", \"__rerun_remap_job_id__\": null, \"delimiter\": \"\\\"T\\\"\", \"chromInfo\": \"\\\"/home/simleo/hg/galaxy-dist/tool-data/shared/ucsc/chrom/?.len\\\"\"}", "tool_version": "1.0.0", "type": "tool", "user_outputs": [], "uuid": "b89ede53-9967-4138-8b1a-59799f8f5cb5" } }, "uuid": "4b38804c-064d-4e84-aa02-ca1e0fe7cf8d" } bioblend-1.2.0/bioblend/_tests/data/paste_columns_subworkflow.ga000066400000000000000000000240531444761704300251270ustar00rootroot00000000000000{ "a_galaxy_workflow": "true", "annotation": "", "format-version": "0.1", "name": "swf", "steps": { "0": { "annotation": "", "content_id": null, "errors": null, "id": 0, "input_connections": {}, "inputs": [], "label": null, "name": "Input dataset", "outputs": [], "position": { "bottom": 468.3000030517578, "height": 61.80000305175781, "left": 1016, "right": 1216, "top": 406.5, "width": 200, "x": 1016, "y": 406.5 }, "tool_id": null, "tool_state": "{\"optional\": false}", "tool_version": null, "type": "data_input", "uuid": "920a449e-4a76-429f-96ab-ea22890d2e27", "workflow_outputs": [ { "label": null, "output_name": "output", "uuid": "eb9f3eab-d1e6-42f5-8514-a07bf24e37bd" } ] }, "1": { "annotation": "", "id": 1, "input_connections": { "Input 1": { "id": 0, "input_subworkflow_step_id": 0, "output_name": "output" }, "Input 2": { "id": 0, "input_subworkflow_step_id": 1, "output_name": "output" } }, "inputs": [], "label": null, "name": "paste_columns", "outputs": [], "position": { "bottom": 583.5, "height": 154, "left": 1341, "right": 1541, "top": 429.5, "width": 200, "x": 1341, "y": 429.5 }, "subworkflow": { "a_galaxy_workflow": "true", "annotation": "", "format-version": "0.1", "name": "paste_columns", "steps": { "0": { "annotation": "", "content_id": null, "errors": null, "id": 0, "input_connections": {}, "inputs": [ { "description": "", "name": "Input 1" } ], "label": "Input 1", "name": "Input dataset", "outputs": [], "position": { "bottom": 430.8000030517578, "height": 61.80000305175781, "left": 699, "right": 899, "top": 369, "width": 200, "x": 699, "y": 369 }, "tool_id": null, "tool_state": "{\"optional\": false}", "tool_version": null, "type": "data_input", "uuid": "26326d61-5683-4e20-9712-992241074d47", "workflow_outputs": [ { "label": null, "output_name": "output", "uuid": "37956f7b-a0ef-46e3-8d89-10eb70dce8c4" } ] }, "1": { "annotation": "", "content_id": null, "errors": null, "id": 1, "input_connections": {}, "inputs": [ { "description": "", "name": "Input 2" } ], "label": "Input 2", "name": "Input dataset", "outputs": [], "position": { "bottom": 514.8000030517578, "height": 61.80000305175781, "left": 651, "right": 851, "top": 453, "width": 200, "x": 651, "y": 453 }, "tool_id": null, "tool_state": "{\"optional\": false}", "tool_version": null, "type": "data_input", "uuid": "12cf3bca-5291-4b31-a75c-b12ab1f58f6a", "workflow_outputs": [ { "label": null, "output_name": "output", "uuid": "78236531-cbdd-4b1b-aaaf-203c187ee8ba" } ] }, "2": { "annotation": "", "content_id": null, "errors": null, "id": 2, "input_connections": {}, "inputs": [], "label": null, "name": "Input parameter", "outputs": [], "position": { "bottom": 618.3000030517578, "height": 61.80000305175781, "left": 664, "right": 864, "top": 556.5, "width": 200, "x": 664, "y": 556.5 }, "tool_id": null, "tool_state": "{\"parameter_type\": \"text\", \"optional\": false}", "tool_version": null, "type": "parameter_input", "uuid": "15a0ee89-4104-4e5a-9b44-65346f1604e1", "workflow_outputs": [ { "label": null, "output_name": "output", "uuid": "4b128d93-1431-4b22-8927-e10124949ad7" } ] }, "3": { "annotation": "", "content_id": "Paste1", "errors": null, "id": 3, "input_connections": { "delimiter": { "id": 2, "output_name": "output" }, "input1": { "id": 0, "output_name": "output" }, "input2": { "id": 1, "output_name": "output" } }, "inputs": [ { "description": "runtime parameter for tool Paste", "name": "input1" }, { "description": "runtime parameter for tool Paste", "name": "input2" } ], "label": null, "name": "Paste", "outputs": [ { "name": "out_file1", "type": "input" } ], "position": { "bottom": 523, "height": 154, "left": 919, "right": 1119, "top": 369, "width": 200, "x": 919, "y": 369 }, "post_job_actions": {}, "tool_id": "Paste1", "tool_state": "{\"delimiter\": {\"__class__\": \"ConnectedValue\"}, \"input1\": {\"__class__\": \"RuntimeValue\"}, \"input2\": {\"__class__\": \"RuntimeValue\"}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", "tool_version": "1.0.0", "type": "tool", "uuid": "27684163-e672-4dcf-8d22-49f4569997e7", "workflow_outputs": [ { "label": null, "output_name": "out_file1", "uuid": "2cbea582-8ae3-49a6-9550-54e976a2af92" } ] } }, "tags": "", "uuid": "e8619723-4479-4ca2-83dc-599ff92bf514" }, "tool_id": "61b51a387c27054a", "type": "subworkflow", "uuid": "665e1b4e-4380-4a02-8c01-73af8865832f", "workflow_outputs": [ { "label": null, "output_name": "3:out_file1", "uuid": "d2a8f0cc-eff4-4cdd-87a5-e2a3f391f2ea" } ] } }, "tags": [], "uuid": "ddbb0d9b-78da-4f46-8e70-6a84f31e3f2c", "version": 8 } bioblend-1.2.0/bioblend/_tests/data/select_first.ga000066400000000000000000000105011444761704300222660ustar00rootroot00000000000000{ "a_galaxy_workflow": "true", "annotation": "", "format-version": "0.1", "name": "Select first", "steps": { "0": { "annotation": "", "content_id": null, "errors": null, "id": 0, "input_connections": {}, "inputs": [], "label": null, "name": "Input dataset", "outputs": [], "tool_id": null, "tool_state": "{\"optional\": false}", "tool_version": null, "type": "data_input", "uuid": "8d9e7e74-721a-4fe6-8e7f-f7f85707fbcb", "workflow_outputs": [] }, "1": { "annotation": "", "content_id": null, "errors": null, "id": 1, "input_connections": {}, "inputs": [], "label": null, "name": "Input parameter", "outputs": [], "tool_id": null, "tool_state": "{\"parameter_type\": \"integer\", \"optional\": false}", "tool_version": null, "type": "parameter_input", "uuid": "fbb896b8-5406-4ee2-b4e6-bcfc26a9f57b", "workflow_outputs": [] }, "2": { "annotation": "", "content_id": "Show beginning1", "errors": null, "id": 2, "input_connections": { "input": { "id": 0, "output_name": "output" }, "lineNum": { "id": 1, "output_name": "output" } }, "inputs": [ { "description": "runtime parameter for tool Select first", "name": "input" } ], "label": null, "name": "Select first", "outputs": [ { "name": "out_file1", "type": "input" } ], "post_job_actions": {}, "tool_id": "Show beginning1", "tool_state": "{\"header\": \"false\", \"input\": {\"__class__\": \"RuntimeValue\"}, \"lineNum\": {\"__class__\": \"ConnectedValue\"}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", "tool_version": "1.0.0", "type": "tool", "uuid": "809c3a0a-1a95-414d-ae64-bb3e19df7b99", "workflow_outputs": [ { "label": null, "output_name": "out_file1", "uuid": "50c8e9c9-5ede-4a17-801f-21376f053dd4" } ] }, "3": { "annotation": "", "content_id": "Paste1", "errors": null, "id": 3, "input_connections": { "input1": { "id": 2, "output_name": "out_file1" }, "input2": { "id": 2, "output_name": "out_file1" } }, "inputs": [], "label": null, "name": "Paste", "outputs": [ { "name": "out_file1", "type": "input" } ], "post_job_actions": { "RenameDatasetActionout_file1": { "action_arguments": { "newname": "paste_output" }, "action_type": "RenameDatasetAction", "output_name": "out_file1" } }, "tool_id": "Paste1", "tool_state": "{\"delimiter\": \"T\", \"input1\": {\"__class__\": \"ConnectedValue\"}, \"input2\": {\"__class__\": \"ConnectedValue\"}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", "tool_version": "1.0.0", "type": "tool", "uuid": "41d799a6-bde2-46cb-a206-caa7621151a6", "workflow_outputs": [ { "label": null, "output_name": "out_file1", "uuid": "74584c3d-bf88-4bb3-846a-bfcb9ff375f8" } ] } }, "tags": [], "uuid": "d38ebab1-6534-4702-a7c3-7ca44dd9d1ae", "version": 1 }bioblend-1.2.0/bioblend/_tests/data/test_workflow_pause.ga000066400000000000000000000071131444761704300237130ustar00rootroot00000000000000{ "a_galaxy_workflow": "true", "annotation": "", "format-version": "0.1", "name": "test_workflow_pause", "steps": { "0": { "annotation": "", "id": 0, "input_connections": {}, "inputs": [ { "description": "", "name": "Input Dataset" } ], "name": "Input dataset", "outputs": [], "position": { "left": 199.9201512336731, "top": 251.4826512336731 }, "tool_errors": null, "tool_id": null, "tool_state": "{\"name\": \"Input Dataset\"}", "tool_version": null, "type": "data_input", "user_outputs": [] }, "1": { "annotation": "", "id": 1, "input_connections": { "input1": { "id": 0, "output_name": "output" } }, "inputs": [], "name": "Concatenate datasets (for test workflows)", "outputs": [ { "name": "out_file1", "type": "input" } ], "position": { "left": 516.7257237434387, "top": 187.28126573562622 }, "post_job_actions": {}, "tool_errors": null, "tool_id": "cat", "tool_state": "{\"__page__\": 0, \"__rerun_remap_job_id__\": null, \"input1\": \"null\", \"queries\": \"[]\"}", "tool_version": "1.0.0", "type": "tool", "user_outputs": [] }, "2": { "annotation": "", "id": 2, "input_connections": { "input": { "id": 1, "output_name": "out_file1" } }, "inputs": [ { "description": "", "name": "Pause for Dataset Review" } ], "name": "Pause for dataset review", "outputs": [], "position": { "left": 862.715301990509, "top": 197.28126573562622 }, "tool_errors": null, "tool_id": null, "tool_state": "{\"name\": \"Pause for Dataset Review\"}", "tool_version": null, "type": "pause", "user_outputs": [] }, "3": { "annotation": "", "id": 3, "input_connections": { "input1": { "id": 2, "output_name": "output" } }, "inputs": [], "name": "Concatenate datasets (for test workflows)", "outputs": [ { "name": "out_file1", "type": "input" } ], "position": { "left": 1181.9722595214844, "top": 181.52084350585938 }, "post_job_actions": {}, "tool_errors": null, "tool_id": "cat1", "tool_state": "{\"__page__\": 0, \"__rerun_remap_job_id__\": null, \"input1\": \"null\", \"queries\": \"[]\"}", "tool_version": "1.0.0", "type": "tool", "user_outputs": [] } }, "uuid": "9058956e-76b6-4909-bab3-c12b2cc394c7" }bioblend-1.2.0/bioblend/_tests/data/workflow_with_parameter_input.ga000066400000000000000000000067451444761704300260030ustar00rootroot00000000000000{ "a_galaxy_workflow": "true", "annotation": "", "format-version": "0.1", "name": "Workflow with parameter input", "steps": { "0": { "annotation": "", "content_id": null, "errors": null, "id": 0, "input_connections": {}, "inputs": [], "label": null, "name": "Input parameter", "outputs": [], "position": { "left": 184, "top": 251.5 }, "tool_id": null, "tool_state": "{\"optional\": false, \"parameter_type\": \"text\"}", "tool_version": null, "type": "parameter_input", "uuid": "23e0b1bb-908c-4077-a75a-6898029ce21d", "workflow_outputs": [ { "label": null, "output_name": "output", "uuid": "da74dde6-e1f4-4602-b778-748670912508" } ] }, "1": { "annotation": "", "content_id": null, "errors": null, "id": 1, "input_connections": {}, "inputs": [], "label": null, "name": "Input dataset", "outputs": [], "position": { "left": 186, "top": 342.5 }, "tool_id": null, "tool_state": "{\"optional\": false}", "tool_version": null, "type": "data_input", "uuid": "8c014439-d785-45d8-9c65-2453f31d28c7", "workflow_outputs": [ { "label": null, "output_name": "output", "uuid": "50d9c228-0e0e-4cef-b56b-71a015882f5f" } ] }, "2": { "annotation": "", "content_id": "addValue", "errors": null, "id": 2, "input_connections": { "exp": { "id": 0, "output_name": "output" }, "input": { "id": 1, "output_name": "output" } }, "inputs": [ { "description": "runtime parameter for tool Add column", "name": "input" } ], "label": null, "name": "Add column", "outputs": [ { "name": "out_file1", "type": "input" } ], "position": { "left": 546, "top": 254.5 }, "post_job_actions": {}, "tool_id": "addValue", "tool_state": "{\"__page__\": null, \"input\": {\"__class__\": \"RuntimeValue\"}, \"__rerun_remap_job_id__\": null, \"exp\": {\"__class__\": \"ConnectedValue\"}, \"iterate\": \"no\"}", "tool_version": "1.0.0", "type": "tool", "uuid": "224d22f3-bb6d-444d-ae1d-744d6e035fbc", "workflow_outputs": [ { "label": null, "output_name": "out_file1", "uuid": "e19e9691-0fe7-43c9-8d90-28a7b901691f" } ] } }, "tags": [], "uuid": "90661668-5367-4f75-89b6-44a2ecb062df", "version": 1 }bioblend-1.2.0/bioblend/_tests/pytest_galaxy_test_wrapper.py000077500000000000000000000024321444761704300244330ustar00rootroot00000000000000#!/usr/bin/env python """Wrapper around pytest to execute the bioblend Galaxy test suite against fixed instance. By default all Galaxy tests will run but a smaller subset can be executed by setting the environment variable ``BIOBLEND_TEST_SUITE`` to ``quick``. """ import os import sys from typing import ( List, NoReturn, Optional, ) try: import pytest except ImportError: pytest = None DIRECTORY = os.path.abspath(os.path.dirname(__file__)) BIOBLEND_TEST_SUITE = os.environ.get("BIOBLEND_TEST_SUITE", "full") quick_tests = [ "TestGalaxyRoles.py", "TestGalaxyRoles.py", "TestGalaxyUsers.py", "TestGalaxyToolData.py", "TestGalaxyTools.py::TestGalaxyTools::test_get_tools", # Test single upload command. ] def main(args: Optional[List[str]] = None) -> NoReturn: """Entry point that delegates to pytest.main.""" if pytest is None: raise Exception("pytest is required to use this script.") if args is None: args = sys.argv[1:] if len(args) < 2: if BIOBLEND_TEST_SUITE == "full": args.append(os.path.join(DIRECTORY)) else: for quick_test in quick_tests: args.append(os.path.join(DIRECTORY, quick_test)) sys.exit(pytest.main(args)) if __name__ == "__main__": main() bioblend-1.2.0/bioblend/_tests/template_galaxy.ini000066400000000000000000000015741444761704300222510ustar00rootroot00000000000000[server:main] use = egg:Paste#http port = ${GALAXY_PORT:-8080} [app:main] paste.app_factory = galaxy.web.buildapp:app_factory database_connection = $DATABASE_CONNECTION file_path = ${TEMP_DIR:-${GALAXY_DIR}/database}/files new_file_path = ${TEMP_DIR:-${GALAXY_DIR}/database}/tmp tool_config_file = ${GALAXY_DIR}/config/tool_conf.xml.sample,${TEMP_DIR:-${GALAXY_DIR}}/config/shed_tool_conf.xml,${GALAXY_DIR}/test/functional/tools/samples_tool_conf.xml shed_tool_config_file = ${TEMP_DIR:-${GALAXY_DIR}}/config/shed_tool_conf.xml conda_auto_init = True job_working_directory = ${TEMP_DIR:-${GALAXY_DIR}/database}/jobs_directory allow_library_path_paste = True admin_users = $BIOBLEND_GALAXY_USER_EMAIL allow_user_deletion = True allow_user_dataset_purge = True enable_beta_workflow_modules = True master_api_key = $BIOBLEND_GALAXY_MASTER_API_KEY enable_quotas = True cleanup_job = onsuccess bioblend-1.2.0/bioblend/_tests/template_galaxy.yml000066400000000000000000000014001444761704300222570ustar00rootroot00000000000000gravity: galaxy_root: ${GALAXY_DIR} gunicorn: bind: localhost:${GALAXY_PORT:-8080} galaxy: managed_config_dir: ${TEMP_DIR:-${GALAXY_DIR}}/config data_dir: ${TEMP_DIR:-${GALAXY_DIR}}/database database_connection: $DATABASE_CONNECTION tool_config_file: ${GALAXY_DIR}/config/tool_conf.xml.sample,${TEMP_DIR:-${GALAXY_DIR}}/config/shed_tool_conf.xml,${GALAXY_DIR}/${TEST_TOOLS_CONF_FILE} # Don't use $TEMP_DIR for tool_dependency_dir to save time on local testing tool_dependency_dir: ${GALAXY_DIR}/database/dependencies allow_path_paste: true admin_users: $BIOBLEND_GALAXY_USER_EMAIL allow_user_deletion: true enable_beta_workflow_modules: true master_api_key: $BIOBLEND_GALAXY_MASTER_API_KEY enable_quotas: true cleanup_job: onsuccess bioblend-1.2.0/bioblend/_tests/test_util.py000066400000000000000000000106271444761704300207550ustar00rootroot00000000000000""" General support infrastructure not tied to any particular test. """ import os import random import string import unittest from typing import ( Callable, Optional, ) import bioblend.galaxy NO_GALAXY_MESSAGE = "Externally configured Galaxy required, but not found. Set BIOBLEND_GALAXY_URL and BIOBLEND_GALAXY_API_KEY to run this test." def random_string(length: int = 8) -> str: return "".join(random.choice(string.ascii_lowercase) for _ in range(length)) def skip_unless_toolshed() -> Callable: """Decorate tests with this to skip the test if a URL for a ToolShed to run the tests is not provided. """ if "BIOBLEND_TOOLSHED_URL" not in os.environ: return unittest.skip( "Externally configured ToolShed required, but not found. Set BIOBLEND_TOOLSHED_URL (e.g. to https://testtoolshed.g2.bx.psu.edu/ ) to run this test." ) return lambda f: f def skip_unless_galaxy(min_release: Optional[str] = None) -> Callable: """Decorate tests with this to skip the test if Galaxy is not configured. """ if min_release is not None: galaxy_release = os.environ.get("GALAXY_VERSION", None) if galaxy_release is not None and galaxy_release != "dev": if not galaxy_release.startswith("release_"): raise ValueError("The value of GALAXY_VERSION environment variable should start with 'release_'") if not min_release.startswith("release_"): raise Exception("min_release should start with 'release_'") if galaxy_release[8:] < min_release[8:]: return unittest.skip(f"Testing on Galaxy {galaxy_release}, but need {min_release} to run this test.") if "BIOBLEND_GALAXY_URL" not in os.environ: return unittest.skip(NO_GALAXY_MESSAGE) if "BIOBLEND_GALAXY_API_KEY" not in os.environ and "BIOBLEND_GALAXY_MASTER_API_KEY" in os.environ: galaxy_url = os.environ["BIOBLEND_GALAXY_URL"] galaxy_master_api_key = os.environ["BIOBLEND_GALAXY_MASTER_API_KEY"] gi = bioblend.galaxy.GalaxyInstance(galaxy_url, galaxy_master_api_key) if "BIOBLEND_GALAXY_USER_EMAIL" in os.environ: galaxy_user_email = os.environ["BIOBLEND_GALAXY_USER_EMAIL"] else: galaxy_user_email = f"{random_string()}@localhost.localdomain" galaxy_user_id = None for user in gi.users.get_users(): if user["email"] == galaxy_user_email: galaxy_user_id = user["id"] break config = gi.config.get_config() if galaxy_user_id is None: if config.get("use_remote_user", False): new_user = gi.users.create_remote_user(galaxy_user_email) else: galaxy_user = galaxy_user_email.split("@", 1)[0] galaxy_password = random_string(20) # Create a new user new_user = gi.users.create_local_user(galaxy_user, galaxy_user_email, galaxy_password) galaxy_user_id = new_user["id"] if config["version_major"] >= "21.01": api_key = gi.users.get_or_create_user_apikey(galaxy_user_id) else: api_key = gi.users.get_user_apikey(galaxy_user_id) if not api_key or api_key == "Not available.": api_key = gi.users.create_user_apikey(galaxy_user_id) os.environ["BIOBLEND_GALAXY_API_KEY"] = api_key if "BIOBLEND_GALAXY_API_KEY" not in os.environ: return unittest.skip(NO_GALAXY_MESSAGE) return lambda f: f def skip_unless_tool(tool_id: str) -> Callable: """Decorate a Galaxy test method as requiring a specific tool, skip the test case if the tool is unavailable. """ def method_wrapper(method): def wrapped_method(has_gi, *args, **kwargs): tools = has_gi.gi.tools.get_tools() # In panels by default, so flatten out sections... tool_ids = [_["id"] for _ in tools] if tool_id not in tool_ids: raise unittest.SkipTest(f"Externally configured Galaxy instance requires tool {tool_id} to run test.") return method(has_gi, *args, **kwargs) # Must preserve method name so pytest can detect and report tests by # name. wrapped_method.__name__ = method.__name__ return wrapped_method return method_wrapper def get_abspath(path: str) -> str: return os.path.abspath(os.path.join(os.path.dirname(__file__), path)) bioblend-1.2.0/bioblend/config.py000066400000000000000000000020511444761704300166750ustar00rootroot00000000000000import configparser import os from typing import ( IO, Optional, ) BioBlendConfigPath = "/etc/bioblend.cfg" BioBlendConfigLocations = [BioBlendConfigPath] UserConfigPath = os.path.join(os.path.expanduser("~"), ".bioblend") BioBlendConfigLocations.append(UserConfigPath) class Config(configparser.ConfigParser): """ BioBlend allows library-wide configuration to be set in external files. These configuration files can be used to specify access keys, for example. By default we use two locations for the BioBlend configurations: * System wide: ``/etc/bioblend.cfg`` * Individual user: ``~/.bioblend`` (which works on both Windows and Unix) """ def __init__(self, path: Optional[str] = None, fp: Optional[IO[str]] = None, do_load: bool = True) -> None: super().__init__({"working_dir": "/mnt/pyami", "debug": "0"}) if do_load: if path: self.read([path]) elif fp: self.read_file(fp) else: self.read(BioBlendConfigLocations) bioblend-1.2.0/bioblend/galaxy/000077500000000000000000000000001444761704300163455ustar00rootroot00000000000000bioblend-1.2.0/bioblend/galaxy/__init__.py000066400000000000000000000102241444761704300204550ustar00rootroot00000000000000""" A base representation of an instance of Galaxy """ from typing import Optional from bioblend.galaxy import ( config, container_resolution, dataset_collections, datasets, datatypes, folders, forms, ftpfiles, genomes, groups, histories, invocations, jobs, libraries, quotas, roles, tool_data, tool_dependencies, tools, toolshed, users, visual, workflows, ) from bioblend.galaxyclient import GalaxyClient class GalaxyInstance(GalaxyClient): def __init__( self, url: str, key: Optional[str] = None, email: Optional[str] = None, password: Optional[str] = None, verify: bool = True, ) -> None: """ A base representation of a connection to a Galaxy instance, identified by the server URL and user credentials. After you have created a ``GalaxyInstance`` object, access various modules via the class fields. For example, to work with histories and get a list of all the user's histories, the following should be done:: from bioblend import galaxy gi = galaxy.GalaxyInstance(url='http://127.0.0.1:8000', key='your_api_key') hl = gi.histories.get_histories() :type url: str :param url: A FQDN or IP for a given instance of Galaxy. For example: http://127.0.0.1:8080 . If a Galaxy instance is served under a prefix (e.g., http://127.0.0.1:8080/galaxy/), supply the entire URL including the prefix (note that the prefix must end with a slash). If a Galaxy instance has HTTP Basic authentication with username and password, then the credentials should be included in the URL, e.g. http://user:pass@host:port/galaxy/ :type key: str :param key: User's API key for the given instance of Galaxy, obtained from the user preferences. If a key is not supplied, an email address and password must be and the key will automatically be created for the user. :type email: str :param email: Galaxy e-mail address corresponding to the user. Ignored if key is supplied directly. :type password: str :param password: Password of Galaxy account corresponding to the above e-mail address. Ignored if key is supplied directly. :param verify: Whether to verify the server's TLS certificate :type verify: bool """ super().__init__(url, key, email, password, verify=verify) self.libraries = libraries.LibraryClient(self) self.histories = histories.HistoryClient(self) self.workflows = workflows.WorkflowClient(self) self.invocations = invocations.InvocationClient(self) self.datasets = datasets.DatasetClient(self) self.dataset_collections = dataset_collections.DatasetCollectionClient(self) self.users = users.UserClient(self) self.genomes = genomes.GenomeClient(self) self.tools = tools.ToolClient(self) self.toolshed = toolshed.ToolShedClient(self) self.toolShed = self.toolshed # historical alias self.config = config.ConfigClient(self) self.container_resolution = container_resolution.ContainerResolutionClient(self) self.visual = visual.VisualClient(self) self.quotas = quotas.QuotaClient(self) self.groups = groups.GroupsClient(self) self.roles = roles.RolesClient(self) self.datatypes = datatypes.DatatypesClient(self) self.jobs = jobs.JobsClient(self) self.forms = forms.FormsClient(self) self.ftpfiles = ftpfiles.FTPFilesClient(self) self.tool_data = tool_data.ToolDataClient(self) self.folders = folders.FoldersClient(self) self.tool_dependencies = tool_dependencies.ToolDependenciesClient(self) def __repr__(self) -> str: """ A nicer representation of this GalaxyInstance object """ return f"GalaxyInstance object for Galaxy at {self.base_url}" bioblend-1.2.0/bioblend/galaxy/client.py000066400000000000000000000207601444761704300202020ustar00rootroot00000000000000""" An interface the clients should implement. This class is primarily a helper for the library and user code should not use it directly. """ import time from typing import ( Any, Optional, TYPE_CHECKING, ) import requests import bioblend # The following import must be preserved for compatibility because # ConnectionError class was originally defined here from bioblend import ConnectionError # noqa: I202 if TYPE_CHECKING: from bioblend.galaxyclient import GalaxyClient class Client: # The `module` attribute needs to be defined in subclasses module: str gi: "GalaxyClient" @classmethod def max_get_retries(cls) -> None: raise AttributeError("Deprecated method, please use gi's `max_get_attempts` property") @classmethod def set_max_get_retries(cls, value: int) -> None: raise AttributeError("Deprecated method, please use gi's `max_get_attempts` property") @classmethod def get_retry_delay(cls) -> None: raise AttributeError("Deprecated method, please use gi's `get_retry_delay` property") @classmethod def set_get_retry_delay(cls, value: float) -> None: raise AttributeError("Deprecated method, please use gi's `get_retry_delay` property") def __init__(self, galaxy_instance: "GalaxyClient") -> None: """ A generic Client interface defining the common fields. All clients *must* define the following field (which will be used as part of the URL composition (e.g., ``http:///api/libraries``): ``self.module = 'workflows' | 'libraries' | 'histories' | ...`` """ self.gi = galaxy_instance def _make_url(self, module_id: Optional[str] = None, deleted: bool = False, contents: bool = False) -> str: """ Compose a URL based on the provided arguments. :type module_id: str :param module_id: The encoded ID for a specific module (eg, library ID) :type deleted: bool :param deleted: If ``True``, include ``deleted`` in the URL, after the module name (eg, ``/api/libraries/deleted``) :type contents: bool :param contents: If ``True``, include 'contents' in the URL, after the module ID: ``/api/libraries//contents`` """ c_url = "/".join((self.gi.url, self.module)) if deleted: c_url = c_url + "/deleted" if module_id: c_url = "/".join((c_url, module_id)) if contents: c_url = c_url + "/contents" return c_url def _get( self, id: Optional[str] = None, deleted: bool = False, contents: bool = False, url: Optional[str] = None, params: Optional[dict] = None, json: bool = True, ) -> Any: """ Do a GET request, composing the URL from ``id``, ``deleted`` and ``contents``. Alternatively, an explicit ``url`` can be provided. If ``json`` is set to ``True``, return a decoded JSON object (and treat an empty or undecodable response as an error). The request will optionally be retried as configured by gi's ``max_get_attempts`` and ``get_retry_delay``: this offers some resilience in the presence of temporary failures. :return: The decoded response if ``json`` is set to ``True``, otherwise the response object """ if url is None: url = self._make_url(module_id=id, deleted=deleted, contents=contents) attempts_left = self.gi.max_get_attempts retry_delay = self.gi.get_retry_delay bioblend.log.debug("GET - attempts left: %s; retry delay: %s", attempts_left, retry_delay) msg = "" while attempts_left > 0: attempts_left -= 1 try: r = self.gi.make_get_request(url, params=params) except requests.exceptions.ConnectionError as e: msg = str(e) r = requests.Response() # empty Response object used when raising ConnectionError else: if r.status_code == 200: if not json: return r elif not r.content: msg = "GET: empty response" else: try: return r.json() except ValueError: msg = f"GET: invalid JSON : {r.content!r}" else: msg = f"GET: error {r.status_code}: {r.content!r}" msg = f"{msg}, {attempts_left} attempts left" if attempts_left <= 0: bioblend.log.error(msg) raise ConnectionError( msg, body=r.text, status_code=r.status_code, ) else: bioblend.log.warning(msg) time.sleep(retry_delay) def _post( self, payload: Optional[dict] = None, id: Optional[str] = None, deleted: bool = False, contents: bool = False, url: Optional[str] = None, files_attached: bool = False, ) -> Any: """ Do a generic POST request, composing the url from the contents of the arguments. Alternatively, an explicit ``url`` can be provided to use for the request. The payload dict may contain file handles (in which case the ``files_attached`` flag must be set to true). If ``files_attached`` is set to ``False``, the request body will be JSON-encoded; otherwise, it will be encoded as multipart/form-data. :type payload: dict :param payload: additional parameters to send in the body of the request :return: The decoded response. """ if not url: url = self._make_url(module_id=id, deleted=deleted, contents=contents) return self.gi.make_post_request(url, payload=payload, files_attached=files_attached) def _put( self, payload: Optional[dict] = None, id: Optional[str] = None, url: Optional[str] = None, params: Optional[dict] = None, ) -> Any: """ Do a generic PUT request, composing the url from the contents of the arguments. Alternatively, an explicit ``url`` can be provided to use for the request. :type payload: dict :param payload: additional parameters to send in the body of the request :return: The decoded response. """ if not url: url = self._make_url(module_id=id) return self.gi.make_put_request(url, payload=payload, params=params) def _patch( self, payload: Optional[dict] = None, id: Optional[str] = None, url: Optional[str] = None, params: Optional[dict] = None, ) -> Any: """ Do a generic PATCH request, composing the url from the contents of the arguments. Alternatively, an explicit ``url`` can be provided to use for the request. :type payload: dict :param payload: additional parameters to send in the body of the request :return: The decoded response. """ if not url: url = self._make_url(module_id=id) return self.gi.make_patch_request(url, payload=payload, params=params) def _delete( self, payload: Optional[dict] = None, id: Optional[str] = None, deleted: bool = False, contents: bool = False, url: Optional[str] = None, params: Optional[dict] = None, ) -> Any: """ Do a generic DELETE request, composing the url from the contents of the arguments. Alternatively, an explicit ``url`` can be provided to use for the request. :type payload: dict :param payload: additional parameters to send in the body of the request :return: The decoded response or None. """ if not url: url = self._make_url(module_id=id, deleted=deleted, contents=contents) r = self.gi.make_delete_request(url, payload=payload, params=params) if 200 <= r.status_code < 203: return r.json() elif 203 <= r.status_code < 300: return None # @see self.body for HTTP response body raise ConnectionError( f"Unexpected HTTP status code: {r.status_code}", body=r.text, status_code=r.status_code, ) bioblend-1.2.0/bioblend/galaxy/config/000077500000000000000000000000001444761704300176125ustar00rootroot00000000000000bioblend-1.2.0/bioblend/galaxy/config/__init__.py000066400000000000000000000046301444761704300217260ustar00rootroot00000000000000""" Contains possible interaction dealing with Galaxy configuration. """ from typing import TYPE_CHECKING from bioblend.galaxy.client import Client if TYPE_CHECKING: from bioblend.galaxy import GalaxyInstance class ConfigClient(Client): module = "configuration" def __init__(self, galaxy_instance: "GalaxyInstance") -> None: super().__init__(galaxy_instance) def get_config(self) -> dict: """ Get a list of attributes about the Galaxy instance. More attributes will be present if the user is an admin. :rtype: list :return: A list of attributes. For example:: {'allow_library_path_paste': False, 'allow_user_creation': True, 'allow_user_dataset_purge': True, 'allow_user_deletion': False, 'enable_unique_workflow_defaults': False, 'ftp_upload_dir': '/SOMEWHERE/galaxy/ftp_dir', 'ftp_upload_site': 'galaxy.com', 'library_import_dir': 'None', 'logo_url': None, 'support_url': 'https://galaxyproject.org/support', 'terms_url': None, 'user_library_import_dir': None, 'wiki_url': 'https://galaxyproject.org/'} """ return self._get() def get_version(self) -> dict: """ Get the current version of the Galaxy instance. :rtype: dict :return: Version of the Galaxy instance For example:: {'extra': {}, 'version_major': '17.01'} """ url = self.gi.url + "/version" return self._get(url=url) def whoami(self) -> dict: """ Return information about the current authenticated user. :rtype: dict :return: Information about current authenticated user For example:: {'active': True, 'deleted': False, 'email': 'user@example.org', 'id': '4aaaaa85aacc9caa', 'last_password_change': '2021-07-29T05:34:54.632345', 'model_class': 'User', 'username': 'julia'} """ url = self.gi.url + "/whoami" return self._get(url=url) def reload_toolbox(self) -> None: """ Reload the Galaxy toolbox (but not individual tools) :rtype: None :return: None """ url = f"{self._make_url()}/toolbox" return self._put(url=url) bioblend-1.2.0/bioblend/galaxy/container_resolution/000077500000000000000000000000001444761704300226125ustar00rootroot00000000000000bioblend-1.2.0/bioblend/galaxy/container_resolution/__init__.py000066400000000000000000000223641444761704300247320ustar00rootroot00000000000000""" Contains interactions dealing with Galaxy container resolvers. Works only with Galaxy > 22.01 """ from typing import ( List, Optional, ) from bioblend.galaxy.client import Client class ContainerResolutionClient(Client): module = "container_resolvers" def get_container_resolvers(self) -> list: """ List container resolvers :rtype: list return: List of container resolvers For example: [{'builds_on_resolution': False, 'can_uninstall_dependencies': False, 'model_class': 'CachedExplicitSingularityContainerResolver', 'resolver_type': 'cached_explicit_singularity'}, {'builds_on_resolution': False, 'can_uninstall_dependencies': False, 'model_class': 'CachedMulledSingularityContainerResolver', 'resolver_type': 'cached_mulled_singularity'}, {'builds_on_resolution': False, 'can_uninstall_dependencies': False, 'model_class': 'MulledSingularityContainerResolver', 'resolver_type': 'mulled_singularity'}] {'builds_on_resolution': False, """ url = self._make_url() return self._get(url=url) def show_container_resolver(self, index: int) -> dict: """ Show container resolver :type index: int :param index: index of the dependency resolver with respect to the dependency resolvers config file :rtype: dict return: Dict of properties of a given container resolver {'builds_on_resolution': False, 'can_uninstall_dependencies': False, 'model_class': 'CachedMulledSingularityContainerResolver', 'resolver_type': 'cached_mulled_singularity'} """ url = f"{self._make_url()}/{index}" return self._get(url=url) def resolve( self, tool_id: str, index: Optional[int] = None, resolver_type: Optional[str] = None, container_type: Optional[str] = None, requirements_only: bool = False, install: bool = False, ) -> dict: """ Resolve described requirement against specified container resolvers. :type index: int :param index: index of the dependency resolver with respect to the dependency resolvers config file :type tool_id: str :param tool_id: tool id to resolve against containers :type resolver_type: str :param resolver_type: restrict to specified resolver type :type container_type: str :param container_type: restrict to specified container type :type requirements_only: bool :param requirements_only: ignore tool containers, properties - just search based on tool requirements set to True to mimic default behavior of tool dependency API. :type install: bool :param install: allow installation of new containers (for build_mulled containers) the way job resolution will operate, defaults to False :rtype: dict For example: { 'requirements': [{'name': 'pyarrow', 'specs': [], 'type': 'package', 'version': '4.0.1'}], 'status': { 'cacheable': False, 'container_description': {'identifier': 'quay.io/biocontainers/pyarrow:4.0.1', 'resolve_dependencies': False, 'shell': '/bin/bash', 'type': 'docker'}, 'container_resolver': {'builds_on_resolution': False, 'can_uninstall_dependencies': False, 'model_class': 'MulledDockerContainerResolver', 'resolver_type': 'mulled'}, 'dependency_type': 'docker', ... }, 'tool_id': 'CONVERTER_parquet_to_csv' } """ params = {} if tool_id: params["tool_id"] = tool_id if resolver_type: params["resolver_type"] = resolver_type if container_type: params["container_type"] = container_type params["requirements_only"] = str(requirements_only) params["install"] = str(install) if index is not None: url = "/".join((self._make_url(), str(index), "resolve")) else: url = "/".join((self._make_url(), "resolve")) return self._get(url=url, params=params) def resolve_toolbox( self, index: Optional[int] = None, tool_ids: Optional[List[str]] = None, resolver_type: Optional[str] = None, container_type: Optional[str] = None, requirements_only: bool = False, install: bool = False, ) -> list: """ Apply resolve() to each tool in the toolbox and return the results as a list. See documentation for resolve() for a description of parameters that can be consumed and a description of the resulting items. :type index: int :param index: index of the dependency resolver with respect to the dependency resolvers config file :type tool_ids: list :param tool_ids: tool_ids to filter toolbox on :type resolver_type: str :param resolver_type: restrict to specified resolver type :type container_type: str :param container_type: restrict to specified container type :type requirements_only: bool :param requirements_only: ignore tool containers, properties - just search based on tool requirements set to True to mimic default behavior of tool dependency API. :type install: bool :param install: allow installation of new containers (for build_mulled containers) the way job resolution will operate, defaults to False :rtype: list For example:: [{'tool_id': 'upload1', 'status': {'model_class': 'NullDependency', 'dependency_type': None, 'exact': True, 'name': None, 'version': None, 'cacheable': False}, 'requirements': []}, ...] """ params = {} if tool_ids: params["tool_ids"] = ",".join(tool_ids) if resolver_type: params["resolver_type"] = resolver_type if container_type: params["container_type"] = container_type params["requirements_only"] = str(requirements_only) params["install"] = str(install) if index is not None: url = "/".join((self._make_url(), str(index), "toolbox")) else: url = "/".join((self._make_url(), "toolbox")) return self._get(url=url, params=params) def resolve_toolbox_with_install( self, index: Optional[int] = None, tool_ids: Optional[List[str]] = None, resolver_type: Optional[str] = None, container_type: Optional[str] = None, requirements_only: bool = False, ) -> list: """ Do the resolution of dependencies like resolve_toolbox(), but allow building and installing new containers. :type index: int :param index: index of the dependency resolver with respect to the dependency resolvers config file :type tool_ids: list :param tool_ids: tool_ids to filter toolbox on :type resolver_type: str :param resolver_type: restrict to specified resolver type :type container_type: str :param container_type: restrict to specified container type :type requirements_only: bool :param requirements_only: ignore tool containers, properties - just search based on tool requirements set to True to mimic default behavior of tool dependency API. :rtype: list of dicts :returns: dictified descriptions of the dependencies, with attribute `dependency_type: None` if no match was found. For example:: [{'requirements': [{'name': 'canu', 'specs': [], 'type': 'package', 'version': '2.2'}], 'status': {'cacheable': False, 'container_description': {'identifier': 'docker://quay.io/biocontainers/canu:2.2--ha47f30e_0', 'resolve_dependencies': False, 'shell': '/bin/bash', 'type': 'singularity'}, 'container_resolver': {'builds_on_resolution': False, 'can_uninstall_dependencies': False, 'model_class': 'MulledSingularityContainerResolver', 'resolver_type': 'mulled_singularity'}, 'dependency_type': 'singularity', 'environment_path': 'docker://quay.io/biocontainers/canu:2.2--ha47f30e_0', 'exact': True, 'model_class': 'ContainerDependency', 'name': None, 'version': None}, 'tool_id': 'toolshed.g2.bx.psu.edu/repos/bgruening/canu/canu/2.2+galaxy0'}] """ params = {} if tool_ids: params["tool_ids"] = ",".join(tool_ids) if resolver_type: params["resolver_type"] = resolver_type if container_type: params["container_type"] = container_type params["requirements_only"] = str(requirements_only) if index is not None: url = "/".join((self._make_url(), str(index), "toolbox", "install")) else: url = "/".join((self._make_url(), "toolbox", "install")) return self._post(url=url, payload=params) bioblend-1.2.0/bioblend/galaxy/dataset_collections/000077500000000000000000000000001444761704300223705ustar00rootroot00000000000000bioblend-1.2.0/bioblend/galaxy/dataset_collections/__init__.py000066400000000000000000000176301444761704300245100ustar00rootroot00000000000000import logging import time from typing import ( Any, Dict, List, Optional, TYPE_CHECKING, Union, ) from bioblend import ( CHUNK_SIZE, TimeoutException, ) from bioblend.galaxy.client import Client from bioblend.galaxy.datasets import TERMINAL_STATES if TYPE_CHECKING: from bioblend.galaxy import GalaxyInstance log = logging.getLogger(__name__) class HasElements: def __init__( self, name: str, type: str = "list", elements: Optional[Union[List[Union["CollectionElement", "SimpleElement"]], Dict[str, Any]]] = None, ) -> None: self.name = name self.type = type if isinstance(elements, dict): self.elements: List[Union["CollectionElement", "SimpleElement"]] = [ HistoryDatasetElement(name=key, id=value) for key, value in elements.values() ] elif elements: self.elements = elements def add(self, element: Union["CollectionElement", "SimpleElement"]) -> "HasElements": self.elements.append(element) return self class CollectionDescription(HasElements): def to_dict(self) -> Dict[str, Union[str, List]]: return dict(name=self.name, collection_type=self.type, element_identifiers=[e.to_dict() for e in self.elements]) class CollectionElement(HasElements): def to_dict(self) -> Dict[str, Union[str, List]]: return dict( src="new_collection", name=self.name, collection_type=self.type, element_identifiers=[e.to_dict() for e in self.elements], ) class SimpleElement: def __init__(self, value: Dict[str, str]) -> None: self.value = value def to_dict(self) -> Dict[str, str]: return self.value class HistoryDatasetElement(SimpleElement): def __init__(self, name: str, id: str) -> None: super().__init__( dict( name=name, src="hda", id=id, ) ) class HistoryDatasetCollectionElement(SimpleElement): def __init__(self, name: str, id: str) -> None: super().__init__( dict( name=name, src="hdca", id=id, ) ) class LibraryDatasetElement(SimpleElement): def __init__(self, name: str, id: str) -> None: super().__init__( dict( name=name, src="ldda", id=id, ) ) class DatasetCollectionClient(Client): gi: "GalaxyInstance" module = "dataset_collections" def __init__(self, galaxy_instance: "GalaxyInstance") -> None: super().__init__(galaxy_instance) def show_dataset_collection(self, dataset_collection_id: str, instance_type: str = "history") -> Dict[str, Any]: """ Get details of a given dataset collection of the current user :type dataset_collection_id: str :param dataset_collection_id: dataset collection ID :type instance_type: str :param instance_type: instance type of the collection - 'history' or 'library' :rtype: dict :return: element view of the dataset collection """ params = { "instance_type": instance_type, } url = self._make_url(module_id=dataset_collection_id) return self._get(id=dataset_collection_id, url=url, params=params) def download_dataset_collection(self, dataset_collection_id: str, file_path: str) -> Dict[str, Any]: """ Download a history dataset collection as an archive. :type dataset_collection_id: str :param dataset_collection_id: Encoded dataset collection ID :type file_path: str :param file_path: The path to which the archive will be downloaded :rtype: dict :return: Information about the downloaded archive. .. note:: This method downloads a ``zip`` archive for Galaxy 21.01 and later. For earlier versions of Galaxy this method downloads a ``tgz`` archive. """ url = self._make_url(module_id=dataset_collection_id) + "/download" r = self.gi.make_get_request(url, stream=True) r.raise_for_status() archive_type = "zip" if self.gi.config.get_version()["version_major"] >= "21.01" else "tgz" with open(file_path, "wb") as fp: for chunk in r.iter_content(chunk_size=CHUNK_SIZE): if chunk: fp.write(chunk) return {"file_path": file_path, "archive_type": archive_type} def wait_for_dataset_collection( self, dataset_collection_id: str, maxwait: float = 12000, interval: float = 3, proportion_complete: float = 1.0, check: bool = True, ) -> Dict[str, Any]: """ Wait until all or a specified proportion of elements of a dataset collection are in a terminal state. :type dataset_collection_id: str :param dataset_collection_id: dataset collection ID :type maxwait: float :param maxwait: Total time (in seconds) to wait for the dataset states in the dataset collection to become terminal. If not all datasets are in a terminal state within this time, a ``DatasetCollectionTimeoutException`` will be raised. :type interval: float :param interval: Time (in seconds) to wait between two consecutive checks. :type proportion_complete: float :param proportion_complete: Proportion of elements in this collection that have to be in a terminal state for this method to return. Must be a number between 0 and 1. For example: if the dataset collection contains 2 elements, and proportion_complete=0.5 is specified, then wait_for_dataset_collection will return as soon as 1 of the 2 datasets is in a terminal state. Default is 1, i.e. all elements must complete. :type check: bool :param check: Whether to check if all the terminal states of datasets in the dataset collection are 'ok'. This will raise an Exception if a dataset is in a terminal state other than 'ok'. :rtype: dict :return: Details of the given dataset collection. """ assert maxwait >= 0 assert interval > 0 assert 0 <= proportion_complete <= 1 time_left = maxwait while True: dataset_collection = self.show_dataset_collection(dataset_collection_id) states = [elem["object"]["state"] for elem in dataset_collection["elements"]] terminal_states = [state for state in states if state in TERMINAL_STATES] if set(terminal_states) not in [{"ok"}, set()]: raise Exception( f"Dataset collection {dataset_collection_id} contains elements in the " f"following non-ok terminal states: {', '.join(set(terminal_states) - {'ok'})}" ) proportion = len(terminal_states) / len(states) if proportion >= proportion_complete: return dataset_collection if time_left > 0: log.info( f"The dataset collection {dataset_collection_id} has {len(terminal_states)} out of {len(states)} datasets in a terminal state. Will wait {time_left} more s" ) time.sleep(min(time_left, interval)) time_left -= interval else: raise DatasetCollectionTimeoutException( f"Less than {proportion_complete * 100}% of datasets in the dataset collection is in a terminal state after {maxwait} s" ) class DatasetCollectionTimeoutException(TimeoutException): pass __all__ = ( "CollectionDescription", "CollectionElement", "DatasetCollectionClient", "HistoryDatasetElement", "HistoryDatasetCollectionElement", "LibraryDatasetElement", ) bioblend-1.2.0/bioblend/galaxy/datasets/000077500000000000000000000000001444761704300201555ustar00rootroot00000000000000bioblend-1.2.0/bioblend/galaxy/datasets/__init__.py000066400000000000000000000411421444761704300222700ustar00rootroot00000000000000""" Contains possible interactions with the Galaxy Datasets """ import logging import os import shlex import time import warnings from typing import ( Any, Dict, List, Optional, overload, Tuple, TYPE_CHECKING, Union, ) from requests import Response from typing_extensions import Literal import bioblend from bioblend import TimeoutException from bioblend.galaxy.client import Client if TYPE_CHECKING: from bioblend.galaxy import GalaxyInstance log = logging.getLogger(__name__) HdaLdda = Literal["hda", "ldda"] TERMINAL_STATES = {"ok", "empty", "error", "discarded", "failed_metadata"} # Non-terminal states are: 'new', 'upload', 'queued', 'running', 'paused', 'setting_metadata' class DatasetClient(Client): gi: "GalaxyInstance" module = "datasets" def __init__(self, galaxy_instance: "GalaxyInstance") -> None: super().__init__(galaxy_instance) def show_dataset(self, dataset_id: str, deleted: bool = False, hda_ldda: HdaLdda = "hda") -> Dict[str, Any]: """ Get details about a given dataset. This can be a history or a library dataset. :type dataset_id: str :param dataset_id: Encoded dataset ID :type deleted: bool :param deleted: Whether to return results for a deleted dataset :type hda_ldda: str :param hda_ldda: Whether to show a history dataset ('hda' - the default) or library dataset ('ldda'). :rtype: dict :return: Information about the HDA or LDDA """ params = dict( hda_ldda=hda_ldda, ) return self._get(id=dataset_id, deleted=deleted, params=params) def _initiate_download( self, dataset_id: str, stream_content: bool, require_ok_state: bool = True, maxwait: float = 12000 ) -> Tuple[Dict[str, Any], str, Response]: dataset = self.wait_for_dataset(dataset_id, maxwait=maxwait, check=False) if not dataset["state"] == "ok": message = f"Dataset state is not 'ok'. Dataset id: {dataset_id}, current state: {dataset['state']}" if require_ok_state: raise DatasetStateException(message) else: warnings.warn(message, DatasetStateWarning, stacklevel=2) file_ext = dataset.get("file_ext") # Resort to 'data' when Galaxy returns an empty or temporary extension if not file_ext or file_ext == "auto" or file_ext == "_sniff_": file_ext = "data" # The preferred download URL is # '/api/histories//contents//display?to_ext=' # since the old URL: # '/dataset//display?to_ext=' # does not work when using REMOTE_USER with access disabled to # everything but /api without auth download_url = dataset["download_url"] + "?to_ext=" + file_ext url = f"{self.gi.base_url}{download_url}" r = self.gi.make_get_request(url, stream=stream_content) r.raise_for_status() return dataset, file_ext, r @overload def download_dataset( self, dataset_id: str, file_path: None = None, use_default_filename: bool = True, require_ok_state: bool = True, maxwait: float = 12000, ) -> bytes: ... @overload def download_dataset( self, dataset_id: str, file_path: str, use_default_filename: bool = True, require_ok_state: bool = True, maxwait: float = 12000, ) -> str: ... def download_dataset( self, dataset_id: str, file_path: Optional[str] = None, use_default_filename: bool = True, require_ok_state: bool = True, maxwait: float = 12000, ) -> Union[bytes, str]: """ Download a dataset to file or in memory. If the dataset state is not 'ok', a ``DatasetStateException`` will be thrown, unless ``require_ok_state=False``. :type dataset_id: str :param dataset_id: Encoded dataset ID :type file_path: str :param file_path: If this argument is provided, the dataset will be streamed to disk at that path (should be a directory if ``use_default_filename=True``). If the file_path argument is not provided, the dataset content is loaded into memory and returned by the method (Memory consumption may be heavy as the entire file will be in memory). :type use_default_filename: bool :param use_default_filename: If ``True``, the exported file will be saved as ``file_path/%s``, where ``%s`` is the dataset name. If ``False``, ``file_path`` is assumed to contain the full file path including the filename. :type require_ok_state: bool :param require_ok_state: If ``False``, datasets will be downloaded even if not in an 'ok' state, issuing a ``DatasetStateWarning`` rather than raising a ``DatasetStateException``. :type maxwait: float :param maxwait: Total time (in seconds) to wait for the dataset state to become terminal. If the dataset state is not terminal within this time, a ``DatasetTimeoutException`` will be thrown. :rtype: bytes or str :return: If a ``file_path`` argument is not provided, returns the file content. Otherwise returns the local path of the downloaded file. """ dataset, file_ext, r = self._initiate_download( dataset_id, stream_content=file_path is not None, require_ok_state=require_ok_state, maxwait=maxwait ) if file_path is None: if "content-length" in r.headers and len(r.content) != int(r.headers["content-length"]): log.warning( "Transferred content size does not match content-length header (%s != %s)", len(r.content), r.headers["content-length"], ) return r.content else: if use_default_filename: # Build a useable filename filename = dataset["name"] + "." + file_ext # Now try to get a better filename from the response headers # We expect tokens 'filename' '=' to be followed by the quoted filename if "content-disposition" in r.headers: tokens = list(shlex.shlex(r.headers["content-disposition"], posix=True)) try: header_filepath = tokens[tokens.index("filename") + 2] filename = os.path.basename(header_filepath) except (ValueError, IndexError): pass file_local_path = os.path.join(file_path, filename) else: file_local_path = file_path with open(file_local_path, "wb") as fp: for chunk in r.iter_content(chunk_size=bioblend.CHUNK_SIZE): if chunk: fp.write(chunk) # Return location file was saved to return file_local_path def get_datasets( self, limit: int = 500, offset: int = 0, name: Optional[str] = None, extension: Optional[Union[str, List[str]]] = None, state: Optional[Union[str, List[str]]] = None, visible: Optional[bool] = None, deleted: Optional[bool] = None, purged: Optional[bool] = None, tool_id: Optional[str] = None, tag: Optional[str] = None, history_id: Optional[str] = None, create_time_min: Optional[str] = None, create_time_max: Optional[str] = None, update_time_min: Optional[str] = None, update_time_max: Optional[str] = None, order: str = "create_time-dsc", ) -> List[Dict[str, Any]]: """ Get the latest datasets, or select another subset by specifying optional arguments for filtering (e.g. a history ID). Since the number of datasets may be very large, ``limit`` and ``offset`` parameters are required to specify the desired range. If the user is an admin, this will return datasets for all the users, otherwise only for the current user. :type limit: int :param limit: Maximum number of datasets to return. :type offset: int :param offset: Return datasets starting from this specified position. For example, if ``limit`` is set to 100 and ``offset`` to 200, datasets 200-299 will be returned. :type name: str :param name: Dataset name to filter on. :type extension: str or list of str :param extension: Dataset extension (or list of extensions) to filter on. :type state: str or list of str :param state: Dataset state (or list of states) to filter on. :type visible: bool :param visible: Optionally filter datasets by their ``visible`` attribute. :type deleted: bool :param deleted: Optionally filter datasets by their ``deleted`` attribute. :type purged: bool :param purged: Optionally filter datasets by their ``purged`` attribute. :type tool_id: str :param tool_id: Tool ID to filter on. :type tag: str :param tag: Dataset tag to filter on. :type history_id: str :param history_id: Encoded history ID to filter on. :type create_time_min: str :param create_time_min: Show only datasets created after the provided time and date, which should be formatted as ``YYYY-MM-DDTHH-MM-SS``. :type create_time_max: str :param create_time_max: Show only datasets created before the provided time and date, which should be formatted as ``YYYY-MM-DDTHH-MM-SS``. :type update_time_min: str :param update_time_min: Show only datasets last updated after the provided time and date, which should be formatted as ``YYYY-MM-DDTHH-MM-SS``. :type update_time_max: str :param update_time_max: Show only datasets last updated before the provided time and date, which should be formatted as ``YYYY-MM-DDTHH-MM-SS``. :type order: str :param order: One or more of the following attributes for ordering datasets: ``create_time`` (default), ``extension``, ``hid``, ``history_id``, ``name``, ``update_time``. Optionally, ``-asc`` or ``-dsc`` (default) can be appended for ascending and descending order respectively. Multiple attributes can be stacked as a comma-separated list of values, e.g. ``create_time-asc,hid-dsc``. :rtype: list :param: A list of datasets """ params: Dict[str, Any] = { "limit": limit, "offset": offset, "order": order, } if history_id: params["history_id"] = history_id q: List[str] = [] qv = [] if name: q.append("name") qv.append(name) if state: op, val = self._param_to_filter(state) q.append(f"state-{op}") qv.append(val) if extension: op, val = self._param_to_filter(extension) q.append(f"extension-{op}") qv.append(val) if visible is not None: q.append("visible") qv.append(str(visible)) if deleted is not None: q.append("deleted") qv.append(str(deleted)) if purged is not None: q.append("purged") qv.append(str(purged)) if tool_id is not None: q.append("tool_id") qv.append(str(tool_id)) if tag is not None: q.append("tag") qv.append(str(tag)) if create_time_min: q.append("create_time-ge") qv.append(create_time_min) if create_time_max: q.append("create_time-le") qv.append(create_time_max) if update_time_min: q.append("update_time-ge") qv.append(update_time_min) if update_time_max: q.append("update_time-le") qv.append(update_time_max) params["q"] = q params["qv"] = qv return self._get(params=params) def _param_to_filter(self, param: Union[str, List[str]]) -> Tuple[str, str]: if isinstance(param, str): return "eq", param if isinstance(param, list): if len(param) == 1: return "eq", param.pop() return "in", ",".join(param) raise Exception("Filter param is not of type ``str`` or ``list``") def publish_dataset(self, dataset_id: str, published: bool = False) -> Dict[str, Any]: """ Make a dataset publicly available or private. For more fine-grained control (assigning different permissions to specific roles), use the ``update_permissions()`` method. :type dataset_id: str :param dataset_id: dataset ID :type published: bool :param published: Whether to make the dataset published (``True``) or private (``False``). :rtype: dict :return: Details of the updated dataset .. note:: This method works only on Galaxy 19.05 or later. """ payload: Dict[str, Any] = {"action": "remove_restrictions" if published else "make_private"} url = self._make_url(dataset_id) + "/permissions" return self.gi.datasets._put(url=url, payload=payload) def update_permissions( self, dataset_id: str, access_ids: Optional[list] = None, manage_ids: Optional[list] = None, modify_ids: Optional[list] = None, ) -> dict: """ Set access, manage or modify permissions for a dataset to a list of roles. :type dataset_id: str :param dataset_id: dataset ID :type access_ids: list :param access_ids: role IDs which should have access permissions for the dataset. :type manage_ids: list :param manage_ids: role IDs which should have manage permissions for the dataset. :type modify_ids: list :param modify_ids: role IDs which should have modify permissions for the dataset. :rtype: dict :return: Current roles for all available permission types. .. note:: This method works only on Galaxy 19.05 or later. """ payload: Dict[str, Any] = {"action": "set_permissions"} if access_ids: payload["access"] = access_ids if manage_ids: payload["manage"] = manage_ids if modify_ids: payload["modify"] = modify_ids url = self._make_url(dataset_id) + "/permissions" return self.gi.datasets._put(url=url, payload=payload) def wait_for_dataset( self, dataset_id: str, maxwait: float = 12000, interval: float = 3, check: bool = True ) -> Dict[str, Any]: """ Wait until a dataset is in a terminal state. :type dataset_id: str :param dataset_id: dataset ID :type maxwait: float :param maxwait: Total time (in seconds) to wait for the dataset state to become terminal. If the dataset state is not terminal within this time, a ``DatasetTimeoutException`` will be raised. :type interval: float :param interval: Time (in seconds) to wait between 2 consecutive checks. :type check: bool :param check: Whether to check if the dataset terminal state is 'ok'. :rtype: dict :return: Details of the given dataset. """ assert maxwait >= 0 assert interval > 0 time_left = maxwait while True: dataset = self.show_dataset(dataset_id) state = dataset["state"] if state in TERMINAL_STATES: if check and state != "ok": raise Exception(f"Dataset {dataset_id} is in terminal state {state}") return dataset if time_left > 0: log.info(f"Dataset {dataset_id} is in non-terminal state {state}. Will wait {time_left} more s") time.sleep(min(time_left, interval)) time_left -= interval else: raise DatasetTimeoutException( f"Dataset {dataset_id} is still in non-terminal state {state} after {maxwait} s" ) class DatasetStateException(Exception): pass class DatasetStateWarning(UserWarning): pass class DatasetTimeoutException(TimeoutException): pass bioblend-1.2.0/bioblend/galaxy/datatypes/000077500000000000000000000000001444761704300203435ustar00rootroot00000000000000bioblend-1.2.0/bioblend/galaxy/datatypes/__init__.py000066400000000000000000000037731444761704300224660ustar00rootroot00000000000000""" Contains possible interactions with the Galaxy Datatype """ from typing import ( Dict, List, TYPE_CHECKING, ) from bioblend.galaxy.client import Client if TYPE_CHECKING: from bioblend.galaxy import GalaxyInstance class DatatypesClient(Client): module = "datatypes" def __init__(self, galaxy_instance: "GalaxyInstance") -> None: super().__init__(galaxy_instance) def get_datatypes(self, extension_only: bool = False, upload_only: bool = False) -> List[str]: """ Get the list of all installed datatypes. :type extension_only: bool :param extension_only: Return only the extension rather than the datatype name :type upload_only: bool :param upload_only: Whether to return only datatypes which can be uploaded :rtype: list :return: A list of datatype names. For example:: ['snpmatrix', 'snptest', 'tabular', 'taxonomy', 'twobit', 'txt', 'vcf', 'wig', 'xgmml', 'xml'] """ params: Dict[str, bool] = {} if extension_only: params["extension_only"] = True if upload_only: params["upload_only"] = True return self._get(params=params) def get_sniffers(self) -> List[str]: """ Get the list of all installed sniffers. :rtype: list :return: A list of sniffer names. For example:: ['galaxy.datatypes.tabular:Vcf', 'galaxy.datatypes.binary:TwoBit', 'galaxy.datatypes.binary:Bam', 'galaxy.datatypes.binary:Sff', 'galaxy.datatypes.xml:Phyloxml', 'galaxy.datatypes.xml:GenericXml', 'galaxy.datatypes.sequence:Maf', 'galaxy.datatypes.sequence:Lav', 'galaxy.datatypes.sequence:csFasta'] """ url = self._make_url() + "/sniffers" return self._get(url=url) bioblend-1.2.0/bioblend/galaxy/folders/000077500000000000000000000000001444761704300200035ustar00rootroot00000000000000bioblend-1.2.0/bioblend/galaxy/folders/__init__.py000066400000000000000000000115341444761704300221200ustar00rootroot00000000000000""" Contains possible interactions with the Galaxy library folders """ from typing import ( Any, Dict, List, Optional, TYPE_CHECKING, Union, ) from typing_extensions import Literal from bioblend.galaxy.client import Client if TYPE_CHECKING: from bioblend.galaxy import GalaxyInstance class FoldersClient(Client): module = "folders" def __init__(self, galaxy_instance: "GalaxyInstance") -> None: super().__init__(galaxy_instance) def create_folder(self, parent_folder_id: str, name: str, description: Optional[str] = None) -> Dict[str, Any]: """ Create a folder. :type parent_folder_id: str :param parent_folder_id: Folder's description :type name: str :param name: name of the new folder :type description: str :param description: folder's description :rtype: dict :return: details of the updated folder """ payload: Dict[str, str] = {"name": name} if description: payload["description"] = description return self._post(payload=payload, id=parent_folder_id) def show_folder(self, folder_id: str, contents: bool = False) -> Dict[str, Any]: """ Display information about a folder. :type folder_id: str :param folder_id: the folder's encoded id, prefixed by 'F' :type contents: bool :param contents: True to get the contents of the folder, rather than just the folder details. :rtype: dict :return: dictionary including details of the folder """ return self._get(id=folder_id, contents=contents) def delete_folder(self, folder_id: str, undelete: bool = False) -> Dict[str, Any]: """ Marks the folder with the given ``id`` as `deleted` (or removes the `deleted` mark if the `undelete` param is True). :type folder_id: str :param folder_id: the folder's encoded id, prefixed by 'F' :type undelete: bool :param undelete: If set to True, the folder will be undeleted (i.e. the `deleted` mark will be removed) :return: detailed folder information :rtype: dict """ payload = {"undelete": undelete} return self._delete(payload=payload, id=folder_id) def update_folder(self, folder_id: str, name: str, description: Optional[str] = None) -> Dict[str, Any]: """ Update folder information. :type folder_id: str :param folder_id: the folder's encoded id, prefixed by 'F' :type name: str :param name: name of the new folder :type description: str :param description: folder's description :rtype: dict :return: details of the updated folder """ payload = {"name": name} if description: payload["description"] = description return self._put(payload=payload, id=folder_id) def get_permissions(self, folder_id: str, scope: Literal["current", "available"] = "current") -> Dict[str, Any]: """ Get the permissions of a folder. :type folder_id: str :param folder_id: the folder's encoded id, prefixed by 'F' :type scope: str :param scope: scope of permissions, either 'current' or 'available' :rtype: dict :return: dictionary including details of the folder permissions """ url = self._make_url(folder_id) + "/permissions" return self._get(url=url) def set_permissions( self, folder_id: str, action: Literal["set_permissions"] = "set_permissions", add_ids: Optional[List[str]] = None, manage_ids: Optional[List[str]] = None, modify_ids: Optional[List[str]] = None, ) -> Dict[str, Any]: """ Set the permissions of a folder. :type folder_id: str :param folder_id: the folder's encoded id, prefixed by 'F' :type action: str :param action: action to execute, only "set_permissions" is supported. :type add_ids: list of str :param add_ids: list of role IDs which can add datasets to the folder :type manage_ids: list of str :param manage_ids: list of role IDs which can manage datasets in the folder :type modify_ids: list of str :param modify_ids: list of role IDs which can modify datasets in the folder :rtype: dict :return: dictionary including details of the folder """ url = self._make_url(folder_id) + "/permissions" payload: Dict[str, Union[str, List[str]]] = {"action": action} if add_ids: payload["add_ids[]"] = add_ids if manage_ids: payload["manage_ids[]"] = manage_ids if modify_ids: payload["modify_ids[]"] = modify_ids return self._post(url=url, payload=payload) bioblend-1.2.0/bioblend/galaxy/forms/000077500000000000000000000000001444761704300174735ustar00rootroot00000000000000bioblend-1.2.0/bioblend/galaxy/forms/__init__.py000066400000000000000000000040501444761704300216030ustar00rootroot00000000000000""" Contains possible interactions with the Galaxy Forms """ from typing import ( Any, Dict, List, TYPE_CHECKING, ) from bioblend.galaxy.client import Client if TYPE_CHECKING: from bioblend.galaxy import GalaxyInstance class FormsClient(Client): module = "forms" def __init__(self, galaxy_instance: "GalaxyInstance") -> None: super().__init__(galaxy_instance) def get_forms(self) -> List[Dict[str, Any]]: """ Get the list of all forms. :rtype: list :return: Displays a collection (list) of forms. For example:: [{'id': 'f2db41e1fa331b3e', 'model_class': 'FormDefinition', 'name': 'First form', 'url': '/api/forms/f2db41e1fa331b3e'}, {'id': 'ebfb8f50c6abde6d', 'model_class': 'FormDefinition', 'name': 'second form', 'url': '/api/forms/ebfb8f50c6abde6d'}] """ return self._get() def show_form(self, form_id: str) -> Dict[str, Any]: """ Get details of a given form. :type form_id: str :param form_id: Encoded form ID :rtype: dict :return: A description of the given form. For example:: {'desc': 'here it is ', 'fields': [], 'form_definition_current_id': 'f2db41e1fa331b3e', 'id': 'f2db41e1fa331b3e', 'layout': [], 'model_class': 'FormDefinition', 'name': 'First form', 'url': '/api/forms/f2db41e1fa331b3e'} """ return self._get(id=form_id) def create_form(self, form_xml_text: str) -> List[Dict[str, Any]]: """ Create a new form. :type form_xml_text: str :param form_xml_text: Form xml to create a form on galaxy instance :rtype: list of dicts :return: List with a single dictionary describing the created form """ payload = { "xml_text": form_xml_text, } return self._post(payload=payload) bioblend-1.2.0/bioblend/galaxy/ftpfiles/000077500000000000000000000000001444761704300201615ustar00rootroot00000000000000bioblend-1.2.0/bioblend/galaxy/ftpfiles/__init__.py000066400000000000000000000013271444761704300222750ustar00rootroot00000000000000""" Contains possible interactions with the Galaxy FTP Files """ from typing import ( List, TYPE_CHECKING, ) from bioblend.galaxy.client import Client if TYPE_CHECKING: from bioblend.galaxy import GalaxyInstance class FTPFilesClient(Client): module = "ftp_files" def __init__(self, galaxy_instance: "GalaxyInstance") -> None: super().__init__(galaxy_instance) def get_ftp_files(self, deleted: bool = False) -> List[dict]: """ Get a list of local files. :type deleted: bool :param deleted: Whether to include deleted files :rtype: list :return: A list of dicts with details on individual files on FTP """ return self._get() bioblend-1.2.0/bioblend/galaxy/genomes/000077500000000000000000000000001444761704300200025ustar00rootroot00000000000000bioblend-1.2.0/bioblend/galaxy/genomes/__init__.py000066400000000000000000000070251444761704300221170ustar00rootroot00000000000000""" Contains possible interactions with the Galaxy Histories """ from typing import ( Any, Dict, Optional, TYPE_CHECKING, ) from typing_extensions import Literal from bioblend.galaxy.client import Client if TYPE_CHECKING: from bioblend.galaxy import GalaxyInstance class GenomeClient(Client): module = "genomes" def __init__(self, galaxy_instance: "GalaxyInstance") -> None: super().__init__(galaxy_instance) def get_genomes(self) -> list: """ Returns a list of installed genomes :rtype: list :return: List of installed genomes """ genomes = self._get() return genomes def show_genome( self, id: str, num: Optional[str] = None, chrom: Optional[str] = None, low: Optional[str] = None, high: Optional[str] = None, ) -> Dict[str, Any]: """ Returns information about build :type id: str :param id: Genome build ID to use :type num: str :param num: num :type chrom: str :param chrom: chrom :type low: str :param low: low :type high: str :param high: high :rtype: dict :return: Information about the genome build """ params: Dict[str, str] = {} if num: params["num"] = num if chrom: params["chrom"] = chrom if low: params["low"] = low if high: params["high"] = high return self._get(id=id, params=params) def install_genome( self, func: Literal["download", "index"] = "download", source: Optional[str] = None, dbkey: Optional[str] = None, ncbi_name: Optional[str] = None, ensembl_dbkey: Optional[str] = None, url_dbkey: Optional[str] = None, indexers: Optional[list] = None, ) -> Dict[str, Any]: """ Download and/or index a genome. :type func: str :param func: Allowed values: 'download', Download and index; 'index', Index only :type source: str :param source: Data source for this build. Can be: UCSC, Ensembl, NCBI, URL :type dbkey: str :param dbkey: DB key of the build to download, ignored unless 'UCSC' is specified as the source :type ncbi_name: str :param ncbi_name: NCBI's genome identifier, ignored unless NCBI is specified as the source :type ensembl_dbkey: str :param ensembl_dbkey: Ensembl's genome identifier, ignored unless Ensembl is specified as the source :type url_dbkey: str :param url_dbkey: DB key to use for this build, ignored unless URL is specified as the source :type indexers: list :param indexers: POST array of indexers to run after downloading (indexers[] = first, indexers[] = second, ...) :rtype: dict :return: dict( status: 'ok', job: ) If error: dict( status: 'error', error: ) """ payload: Dict[str, Any] = {} if source: payload["source"] = source if func: payload["func"] = func if dbkey: payload["dbkey"] = dbkey if ncbi_name: payload["ncbi_name"] = ncbi_name if ensembl_dbkey: payload["ensembl_dbkey"] = ensembl_dbkey if url_dbkey: payload["url_dbkey"] = url_dbkey if indexers: payload["indexers"] = indexers return self._post(payload) bioblend-1.2.0/bioblend/galaxy/groups/000077500000000000000000000000001444761704300176645ustar00rootroot00000000000000bioblend-1.2.0/bioblend/galaxy/groups/__init__.py000066400000000000000000000144121444761704300217770ustar00rootroot00000000000000""" Contains possible interactions with the Galaxy Groups """ from typing import ( Any, Dict, List, Optional, TYPE_CHECKING, ) from bioblend.galaxy.client import Client if TYPE_CHECKING: from bioblend.galaxy import GalaxyInstance class GroupsClient(Client): module = "groups" def __init__(self, galaxy_instance: "GalaxyInstance") -> None: super().__init__(galaxy_instance) def get_groups(self) -> List[Dict[str, Any]]: """ Get all (not deleted) groups. :rtype: list :return: A list of dicts with details on individual groups. For example:: [{'id': '33abac023ff186c2', 'model_class': 'Group', 'name': 'Listeria', 'url': '/api/groups/33abac023ff186c2'}, {'id': '73187219cd372cf8', 'model_class': 'Group', 'name': 'LPN', 'url': '/api/groups/73187219cd372cf8'}] """ return self._get() def show_group(self, group_id: str) -> Dict[str, Any]: """ Get details of a given group. :type group_id: str :param group_id: Encoded group ID :rtype: dict :return: A description of group For example:: {'id': '33abac023ff186c2', 'model_class': 'Group', 'name': 'Listeria', 'roles_url': '/api/groups/33abac023ff186c2/roles', 'url': '/api/groups/33abac023ff186c2', 'users_url': '/api/groups/33abac023ff186c2/users'} """ return self._get(id=group_id) def create_group( self, group_name: str, user_ids: Optional[List[str]] = None, role_ids: Optional[List[str]] = None ) -> List[Dict[str, Any]]: """ Create a new group. :type group_name: str :param group_name: A name for the new group :type user_ids: list :param user_ids: A list of encoded user IDs to add to the new group :type role_ids: list :param role_ids: A list of encoded role IDs to add to the new group :rtype: list :return: A (size 1) list with newly created group details, like:: [{'id': '7c9636938c3e83bf', 'model_class': 'Group', 'name': 'My Group Name', 'url': '/api/groups/7c9636938c3e83bf'}] """ if user_ids is None: user_ids = [] if role_ids is None: role_ids = [] payload = {"name": group_name, "user_ids": user_ids, "role_ids": role_ids} return self._post(payload) def update_group( self, group_id: str, group_name: Optional[str] = None, user_ids: Optional[List[str]] = None, role_ids: Optional[List[str]] = None, ) -> None: """ Update a group. :type group_id: str :param group_id: Encoded group ID :type group_name: str :param group_name: A new name for the group. If None, the group name is not changed. :type user_ids: list :param user_ids: New list of encoded user IDs for the group. It will substitute the previous list of users (with [] if not specified) :type role_ids: list :param role_ids: New list of encoded role IDs for the group. It will substitute the previous list of roles (with [] if not specified) :rtype: None :return: None """ if user_ids is None: user_ids = [] if role_ids is None: role_ids = [] payload = {"name": group_name, "user_ids": user_ids, "role_ids": role_ids} return self._put(payload=payload, id=group_id) def get_group_users(self, group_id: str) -> List[Dict[str, Any]]: """ Get the list of users associated to the given group. :type group_id: str :param group_id: Encoded group ID :rtype: list of dicts :return: List of group users' info """ url = self._make_url(group_id) + "/users" return self._get(url=url) def get_group_roles(self, group_id: str) -> List[Dict[str, Any]]: """ Get the list of roles associated to the given group. :type group_id: str :param group_id: Encoded group ID :rtype: list of dicts :return: List of group roles' info """ url = self._make_url(group_id) + "/roles" return self._get(url=url) def add_group_user(self, group_id: str, user_id: str) -> Dict[str, Any]: """ Add a user to the given group. :type group_id: str :param group_id: Encoded group ID :type user_id: str :param user_id: Encoded user ID to add to the group :rtype: dict :return: Added group user's info """ url = "/".join((self._make_url(group_id), "users", user_id)) return self._put(url=url) def add_group_role(self, group_id: str, role_id: str) -> Dict[str, Any]: """ Add a role to the given group. :type group_id: str :param group_id: Encoded group ID :type role_id: str :param role_id: Encoded role ID to add to the group :rtype: dict :return: Added group role's info """ url = "/".join((self._make_url(group_id), "roles", role_id)) return self._put(url=url) def delete_group_user(self, group_id: str, user_id: str) -> Dict[str, Any]: """ Remove a user from the given group. :type group_id: str :param group_id: Encoded group ID :type user_id: str :param user_id: Encoded user ID to remove from the group :rtype: dict :return: The user which was removed """ url = "/".join((self._make_url(group_id), "users", user_id)) return self._delete(url=url) def delete_group_role(self, group_id: str, role_id: str) -> Dict[str, Any]: """ Remove a role from the given group. :type group_id: str :param group_id: Encoded group ID :type role_id: str :param role_id: Encoded role ID to remove from the group :rtype: dict :return: The role which was removed """ url = "/".join((self._make_url(group_id), "roles", role_id)) return self._delete(url=url) bioblend-1.2.0/bioblend/galaxy/histories/000077500000000000000000000000001444761704300203565ustar00rootroot00000000000000bioblend-1.2.0/bioblend/galaxy/histories/__init__.py000066400000000000000000000761331444761704300225010ustar00rootroot00000000000000""" Contains possible interactions with the Galaxy Histories """ import logging import re import sys import time import typing import webbrowser from typing import ( Any, Dict, IO, List, Optional, overload, Pattern, Union, ) from typing_extensions import Literal import bioblend from bioblend import ConnectionError from bioblend.galaxy.client import Client from bioblend.galaxy.dataset_collections import CollectionDescription from bioblend.util import attach_file if typing.TYPE_CHECKING: from bioblend.galaxy import GalaxyInstance log = logging.getLogger(__name__) class HistoryClient(Client): gi: "GalaxyInstance" module = "histories" def __init__(self, galaxy_instance: "GalaxyInstance") -> None: super().__init__(galaxy_instance) def create_history(self, name: Optional[str] = None) -> Dict[str, Any]: """ Create a new history, optionally setting the ``name``. :type name: str :param name: Optional name for new history :rtype: dict :return: Dictionary containing information about newly created history """ payload = {} if name is not None: payload["name"] = name return self._post(payload) def import_history(self, file_path: Optional[str] = None, url: Optional[str] = None) -> Dict[str, Any]: """ Import a history from an archive on disk or a URL. :type file_path: str :param file_path: Path to exported history archive on disk. :type url: str :param url: URL for an exported history archive :rtype: dict :return: Dictionary containing information about the imported history """ if file_path: archive_file = attach_file(file_path) payload: Dict[str, Any] = { "archive_source": "", "archive_file": archive_file, "archive_type": "file", } else: payload = { "archive_source": url, "archive_type": "url", } return self._post(payload=payload, files_attached=file_path is not None) def _get_histories( self, name: Optional[str] = None, deleted: bool = False, filter_user_published: Optional[bool] = None, get_all_published: bool = False, slug: Optional[str] = None, all: Optional[bool] = False, ) -> List[Dict[str, Any]]: """ Hidden method to be used by both get_histories() and get_published_histories() """ assert not (filter_user_published is not None and get_all_published) params: Dict[str, Any] = {} if deleted: params.setdefault("q", []).append("deleted") params.setdefault("qv", []).append(deleted) if filter_user_published is not None: params.setdefault("q", []).append("published") params.setdefault("qv", []).append(filter_user_published) if slug is not None: params.setdefault("q", []).append("slug") params.setdefault("qv", []).append(slug) if all: params["all"] = True url = "/".join((self._make_url(), "published")) if get_all_published else None histories = self._get(url=url, params=params) if name is not None: histories = [_ for _ in histories if _["name"] == name] return histories def get_histories( self, history_id: Optional[str] = None, name: Optional[str] = None, deleted: bool = False, published: Optional[bool] = None, slug: Optional[str] = None, all: Optional[bool] = False, ) -> List[Dict[str, Any]]: """ Get all histories, or select a subset by specifying optional arguments for filtering (e.g. a history name). :type name: str :param name: History name to filter on. :type deleted: bool :param deleted: whether to filter for the deleted histories (``True``) or for the non-deleted ones (``False``) :type published: bool or None :param published: whether to filter for the published histories (``True``) or for the non-published ones (``False``). If not set, no filtering is applied. Note the filtering is only applied to the user's own histories; to access all histories published by any user, use the ``get_published_histories`` method. :type slug: str :param slug: History slug to filter on :type all: bool :param all: Whether to include histories from other users. This parameter works only on Galaxy 20.01 or later and can be specified only if the user is a Galaxy admin. :rtype: list :return: List of history dicts. .. versionchanged:: 0.17.0 Using the deprecated ``history_id`` parameter now raises a ``ValueError`` exception. """ if history_id is not None: raise ValueError( "The history_id parameter has been removed, use the show_history() method to view details of a history for which you know the ID.", ) return self._get_histories( name=name, deleted=deleted, filter_user_published=published, get_all_published=False, slug=slug, all=all ) def get_published_histories( self, name: Optional[str] = None, deleted: bool = False, slug: Optional[str] = None ) -> List[Dict[str, Any]]: """ Get all published histories (by any user), or select a subset by specifying optional arguments for filtering (e.g. a history name). :type name: str :param name: History name to filter on. :type deleted: bool :param deleted: whether to filter for the deleted histories (``True``) or for the non-deleted ones (``False``) :type slug: str :param slug: History slug to filter on :rtype: list :return: List of history dicts. """ return self._get_histories( name=name, deleted=deleted, filter_user_published=None, get_all_published=True, slug=slug ) @overload def show_history( self, history_id: str, contents: Literal[False] = False, ) -> Dict[str, Any]: ... @overload def show_history( self, history_id: str, contents: Literal[True], deleted: Optional[bool] = None, visible: Optional[bool] = None, details: Optional[str] = None, types: Optional[List[str]] = None, ) -> List[Dict[str, Any]]: ... # Fallback in case the caller provides a regular bool as contents @overload def show_history( self, history_id: str, contents: bool = False, deleted: Optional[bool] = None, visible: Optional[bool] = None, details: Optional[str] = None, types: Optional[List[str]] = None, ) -> Union[Dict[str, Any], List[Dict[str, Any]]]: pass def show_history( self, history_id: str, contents: bool = False, deleted: Optional[bool] = None, visible: Optional[bool] = None, details: Optional[str] = None, types: Optional[List[str]] = None, ) -> Union[Dict[str, Any], List[Dict[str, Any]]]: """ Get details of a given history. By default, just get the history meta information. :type history_id: str :param history_id: Encoded history ID to filter on :type contents: bool :param contents: When ``True``, instead of the history details, return a list with info for all datasets in the given history. Note that inside each dataset info dict, the id which should be used for further requests about this history dataset is given by the value of the `id` (not `dataset_id`) key. :type deleted: bool or None :param deleted: When ``contents=True``, whether to filter for the deleted datasets (``True``) or for the non-deleted ones (``False``). If not set, no filtering is applied. :type visible: bool or None :param visible: When ``contents=True``, whether to filter for the visible datasets (``True``) or for the hidden ones (``False``). If not set, no filtering is applied. :type details: str :param details: When ``contents=True``, include dataset details. Set to 'all' for the most information. :type types: list :param types: When ``contents=True``, filter for history content types. If set to ``['dataset']``, return only datasets. If set to ``['dataset_collection']``, return only dataset collections. If not set, no filtering is applied. :rtype: dict or list of dicts :return: details of the given history or list of dataset info .. note:: As an alternative to using the ``contents=True`` parameter, consider using ``gi.datasets.get_datasets(history_id=history_id)`` which offers more extensive functionality for filtering and ordering the results. """ params: Dict[str, Union[bool, list, str]] = {} if contents: if details: params["details"] = details if deleted is not None: params["deleted"] = deleted if visible is not None: params["visible"] = visible if types is not None: params["types"] = types return self._get(id=history_id, contents=contents, params=params) def delete_dataset(self, history_id: str, dataset_id: str, purge: bool = False) -> None: """ Mark corresponding dataset as deleted. :type history_id: str :param history_id: Encoded history ID :type dataset_id: str :param dataset_id: Encoded dataset ID :type purge: bool :param purge: if ``True``, also purge (permanently delete) the dataset :rtype: None :return: None .. note:: The ``purge`` option works only if the Galaxy instance has the ``allow_user_dataset_purge`` option set to ``true`` in the ``config/galaxy.yml`` configuration file. """ url = "/".join((self._make_url(history_id, contents=True), dataset_id)) payload = {} if purge is True: payload["purge"] = purge self._delete(payload=payload, url=url) def delete_dataset_collection(self, history_id: str, dataset_collection_id: str) -> None: """ Mark corresponding dataset collection as deleted. :type history_id: str :param history_id: Encoded history ID :type dataset_collection_id: str :param dataset_collection_id: Encoded dataset collection ID :rtype: None :return: None """ url = "/".join((self._make_url(history_id, contents=True), "dataset_collections", dataset_collection_id)) self._delete(url=url) def show_dataset(self, history_id: str, dataset_id: str) -> Dict[str, Any]: """ Get details about a given history dataset. :type history_id: str :param history_id: Encoded history ID :type dataset_id: str :param dataset_id: Encoded dataset ID :rtype: dict :return: Information about the dataset """ url = "/".join((self._make_url(history_id, contents=True), dataset_id)) return self._get(url=url) def show_dataset_collection(self, history_id: str, dataset_collection_id: str) -> Dict[str, Any]: """ Get details about a given history dataset collection. :type history_id: str :param history_id: Encoded history ID :type dataset_collection_id: str :param dataset_collection_id: Encoded dataset collection ID :rtype: dict :return: Information about the dataset collection """ url = "/".join((self._make_url(history_id, contents=True), "dataset_collections", dataset_collection_id)) return self._get(url=url) def show_matching_datasets( self, history_id: str, name_filter: Optional[Union[str, Pattern[str]]] = None ) -> List[Dict[str, Any]]: """ Get dataset details for matching datasets within a history. :type history_id: str :param history_id: Encoded history ID :type name_filter: str :param name_filter: Only datasets whose name matches the ``name_filter`` regular expression will be returned; use plain strings for exact matches and None to match all datasets in the history :rtype: list :return: List of dictionaries """ if isinstance(name_filter, str): name_filter = re.compile(name_filter + "$") return [ self.show_dataset(history_id, h["id"]) for h in self.show_history(history_id, contents=True) if name_filter is None or name_filter.match(h["name"]) ] def show_dataset_provenance(self, history_id: str, dataset_id: str, follow: bool = False) -> Dict[str, Any]: """ Get details related to how dataset was created (``id``, ``job_id``, ``tool_id``, ``stdout``, ``stderr``, ``parameters``, ``inputs``, etc...). :type history_id: str :param history_id: Encoded history ID :type dataset_id: str :param dataset_id: Encoded dataset ID :type follow: bool :param follow: If ``True``, recursively fetch dataset provenance information for all inputs and their inputs, etc. :rtype: dict :return: Dataset provenance information For example:: {'id': '6fbd9b2274c62ebe', 'job_id': '5471ba76f274f929', 'parameters': {'chromInfo': '"/usr/local/galaxy/galaxy-dist/tool-data/shared/ucsc/chrom/mm9.len"', 'dbkey': '"mm9"', 'experiment_name': '"H3K4me3_TAC_MACS2"', 'input_chipseq_file1': {'id': '6f0a311a444290f2', 'uuid': 'null'}, 'input_control_file1': {'id': 'c21816a91f5dc24e', 'uuid': '16f8ee5e-228f-41e2-921e-a07866edce06'}, 'major_command': '{"gsize": "2716965481.0", "bdg": "False", "__current_case__": 0, "advanced_options": {"advanced_options_selector": "off", "__current_case__": 1}, "input_chipseq_file1": 104715, "xls_to_interval": "False", "major_command_selector": "callpeak", "input_control_file1": 104721, "pq_options": {"pq_options_selector": "qvalue", "qvalue": "0.05", "__current_case__": 1}, "bw": "300", "nomodel_type": {"nomodel_type_selector": "create_model", "__current_case__": 1}}'}, 'stderr': '', 'stdout': '', 'tool_id': 'toolshed.g2.bx.psu.edu/repos/ziru-zhou/macs2/modencode_peakcalling_macs2/2.0.10.2', 'uuid': '5c0c43f5-8d93-44bd-939d-305e82f213c6'} """ url = "/".join((self._make_url(history_id, contents=True), dataset_id, "provenance")) params = {"follow": follow} return self._get(url=url, params=params) def update_history(self, history_id: str, **kwargs: Any) -> Dict[str, Any]: """ Update history metadata information. Some of the attributes that can be modified are documented below. :type history_id: str :param history_id: Encoded history ID :type name: str :param name: Replace history name with the given string :type annotation: str :param annotation: Replace history annotation with given string :type deleted: bool :param deleted: Mark or unmark history as deleted :type purged: bool :param purged: If ``True``, mark history as purged (permanently deleted). :type published: bool :param published: Mark or unmark history as published :type importable: bool :param importable: Mark or unmark history as importable :type tags: list :param tags: Replace history tags with the given list :rtype: dict :return: details of the updated history .. versionchanged:: 0.8.0 Changed the return value from the status code (type int) to a dict. """ return self._put(payload=kwargs, id=history_id) def update_dataset(self, history_id: str, dataset_id: str, **kwargs: Any) -> Dict[str, Any]: """ Update history dataset metadata. Some of the attributes that can be modified are documented below. :type history_id: str :param history_id: Encoded history ID :type dataset_id: str :param dataset_id: ID of the dataset :type name: str :param name: Replace history dataset name with the given string :type datatype: str :param datatype: Replace the datatype of the history dataset with the given string. The string must be a valid Galaxy datatype, both the current and the target datatypes must allow datatype changes, and the dataset must not be in use as input or output of a running job (including uploads), otherwise an error will be raised. :type genome_build: str :param genome_build: Replace history dataset genome build (dbkey) :type annotation: str :param annotation: Replace history dataset annotation with given string :type deleted: bool :param deleted: Mark or unmark history dataset as deleted :type visible: bool :param visible: Mark or unmark history dataset as visible :rtype: dict :return: details of the updated dataset .. versionchanged:: 0.8.0 Changed the return value from the status code (type int) to a dict. """ url = "/".join((self._make_url(history_id, contents=True), dataset_id)) return self._put(payload=kwargs, url=url) def update_dataset_collection(self, history_id: str, dataset_collection_id: str, **kwargs: Any) -> Dict[str, Any]: """ Update history dataset collection metadata. Some of the attributes that can be modified are documented below. :type history_id: str :param history_id: Encoded history ID :type dataset_collection_id: str :param dataset_collection_id: Encoded dataset_collection ID :type name: str :param name: Replace history dataset collection name with the given string :type deleted: bool :param deleted: Mark or unmark history dataset collection as deleted :type visible: bool :param visible: Mark or unmark history dataset collection as visible :rtype: dict :return: the updated dataset collection attributes .. versionchanged:: 0.8.0 Changed the return value from the status code (type int) to a dict. """ url = "/".join((self._make_url(history_id, contents=True), "dataset_collections", dataset_collection_id)) return self._put(payload=kwargs, url=url) def create_history_tag(self, history_id: str, tag: str) -> Dict[str, Any]: """ Create history tag :type history_id: str :param history_id: Encoded history ID :type tag: str :param tag: Add tag to history :rtype: dict :return: A dictionary with information regarding the tag. For example:: {'id': 'f792763bee8d277a', 'model_class': 'HistoryTagAssociation', 'user_tname': 'NGS_PE_RUN', 'user_value': None} """ # empty payload since we are adding the new tag using the url payload: Dict[str, Any] = {} url = "/".join((self._make_url(history_id), "tags", tag)) return self._post(payload, url=url) def upload_dataset_from_library(self, history_id: str, lib_dataset_id: str) -> Dict[str, Any]: """ Upload a dataset into the history from a library. Requires the library dataset ID, which can be obtained from the library contents. :type history_id: str :param history_id: Encoded history ID :type lib_dataset_id: str :param lib_dataset_id: Encoded library dataset ID :rtype: dict :return: Information about the newly created HDA """ payload = { "content": lib_dataset_id, "source": "library", "from_ld_id": lib_dataset_id, # compatibility with old API } return self._post(payload, id=history_id, contents=True) def create_dataset_collection( self, history_id: str, collection_description: Union["CollectionDescription", Dict[str, Any]] ) -> Dict[str, Any]: """ Create a new dataset collection :type history_id: str :param history_id: Encoded history ID :type collection_description: bioblend.galaxy.dataset_collections.CollectionDescription :param collection_description: a description of the dataset collection For example:: {'collection_type': 'list', 'element_identifiers': [{'id': 'f792763bee8d277a', 'name': 'element 1', 'src': 'hda'}, {'id': 'f792763bee8d277a', 'name': 'element 2', 'src': 'hda'}], 'name': 'My collection list'} :rtype: dict :return: Information about the new HDCA """ if isinstance(collection_description, CollectionDescription): collection_description_dict = collection_description.to_dict() else: collection_description_dict = collection_description payload = dict( name=collection_description_dict["name"], type="dataset_collection", collection_type=collection_description_dict["collection_type"], element_identifiers=collection_description_dict["element_identifiers"], ) return self._post(payload, id=history_id, contents=True) def delete_history(self, history_id: str, purge: bool = False) -> Dict[str, Any]: """ Delete a history. :type history_id: str :param history_id: Encoded history ID :type purge: bool :param purge: if ``True``, also purge (permanently delete) the history :rtype: dict :return: An error object if an error occurred or a dictionary containing: ``id`` (the encoded id of the history), ``deleted`` (if the history was marked as deleted), ``purged`` (if the history was purged). .. note:: The ``purge`` option works only if the Galaxy instance has the ``allow_user_dataset_purge`` option set to ``true`` in the ``config/galaxy.yml`` configuration file. """ payload = {} if purge is True: payload["purge"] = purge return self._delete(payload=payload, id=history_id) def undelete_history(self, history_id: str) -> str: """ Undelete a history :type history_id: str :param history_id: Encoded history ID :rtype: str :return: 'OK' if it was deleted """ url = self._make_url(history_id, deleted=True) + "/undelete" return self._post(url=url) def get_status(self, history_id: str) -> Dict[str, Any]: """ Returns the state of this history :type history_id: str :param history_id: Encoded history ID :rtype: dict :return: A dict documenting the current state of the history. Has the following keys: 'state' = This is the current state of the history, such as ok, error, new etc. 'state_details' = Contains individual statistics for various dataset states. 'percent_complete' = The overall number of datasets processed to completion. """ history = self.show_history(history_id) state: Dict[str, Any] = { "state": history["state"], } if history.get("state_details") is not None: state["state_details"] = history["state_details"] total_complete = sum(history["state_details"].values()) if total_complete > 0: state["percent_complete"] = 100 * history["state_details"]["ok"] / total_complete else: state["percent_complete"] = 0 return state def get_most_recently_used_history(self) -> Dict[str, Any]: """ Returns the current user's most recently used history (not deleted). :rtype: dict :return: History representation """ url = self._make_url() + "/most_recently_used" return self._get(url=url) def export_history( self, history_id: str, gzip: bool = True, include_hidden: bool = False, include_deleted: bool = False, wait: bool = False, maxwait: Optional[float] = None, ) -> str: """ Start a job to create an export archive for the given history. :type history_id: str :param history_id: history ID :type gzip: bool :param gzip: create .tar.gz archive if ``True``, else .tar :type include_hidden: bool :param include_hidden: whether to include hidden datasets in the export :type include_deleted: bool :param include_deleted: whether to include deleted datasets in the export :type wait: bool :param wait: if ``True``, block until the export is ready; else, return immediately :type maxwait: float :param maxwait: Total time (in seconds) to wait for the export to become ready. When set, implies that ``wait`` is ``True``. :rtype: str :return: ``jeha_id`` of the export, or empty if ``wait`` is ``False`` and the export is not ready. """ if maxwait is not None: assert maxwait >= 0 else: if wait: maxwait = sys.maxsize else: maxwait = 0 params = { "gzip": gzip, "include_hidden": include_hidden, "include_deleted": include_deleted, } url = f"{self._make_url(history_id)}/exports" time_left = maxwait while True: try: r = self._put(url=url, params=params) except ConnectionError as e: if e.status_code == 202: # export is not ready if time_left > 0: log.info( "Waiting for the export of history %s to complete. Will wait %i more s", history_id, time_left, ) time.sleep(1) time_left -= 1 else: return "" else: raise else: break jeha_id = r["download_url"].rsplit("/", 1)[-1] return jeha_id def download_history( self, history_id: str, jeha_id: str, outf: IO[bytes], chunk_size: int = bioblend.CHUNK_SIZE ) -> None: """ Download a history export archive. Use :meth:`export_history` to create an export. :type history_id: str :param history_id: history ID :type jeha_id: str :param jeha_id: jeha ID (this should be obtained via :meth:`export_history`) :type outf: file :param outf: output file object, open for writing in binary mode :type chunk_size: int :param chunk_size: how many bytes at a time should be read into memory :rtype: None :return: None """ url = f"{self._make_url(module_id=history_id)}/exports/{jeha_id}" r = self.gi.make_get_request(url, stream=True) r.raise_for_status() for chunk in r.iter_content(chunk_size): outf.write(chunk) def copy_dataset( self, history_id: str, dataset_id: str, source: Literal["hda", "library", "library_folder"] = "hda" ) -> Dict[str, Any]: """ Copy a dataset to a history. :type history_id: str :param history_id: history ID to which the dataset should be copied :type dataset_id: str :param dataset_id: dataset ID :type source: str :param source: Source of the dataset to be copied: 'hda' (the default), 'library' or 'library_folder' :rtype: dict :return: Information about the copied dataset """ return self.copy_content(history_id, dataset_id, source) def copy_content( self, history_id: str, content_id: str, source: Literal["hda", "hdca", "library", "library_folder"] = "hda" ) -> Dict[str, Any]: """ Copy existing content (e.g. a dataset) to a history. :type history_id: str :param history_id: ID of the history to which the content should be copied :type content_id: str :param content_id: ID of the content to copy :type source: str :param source: Source of the content to be copied: 'hda' (for a history dataset, the default), 'hdca' (for a dataset collection), 'library' (for a library dataset) or 'library_folder' (for all datasets in a library folder). :rtype: dict :return: Information about the copied content """ payload = { "content": content_id, "source": source, "type": "dataset" if source != "hdca" else "dataset_collection", } url = self._make_url(history_id, contents=True) return self._post(payload=payload, url=url) def open_history(self, history_id: str) -> None: """ Open Galaxy in a new tab of the default web browser and switch to the specified history. :type history_id: str :param history_id: ID of the history to switch to :rtype: NoneType :return: ``None`` .. warning:: After opening the specified history, all previously opened Galaxy tabs in the browser session will have the current history changed to this one, even if the interface still shows another history. Refreshing any such tab is recommended. """ url = f"{self.gi.base_url}/history/switch_to_history?hist_id={history_id}" webbrowser.open_new_tab(url) def get_extra_files(self, history_id: str, dataset_id: str) -> List[str]: """ Get extra files associated with a composite dataset, or an empty list if there are none. :type history_id: str :param history_id: history ID :type dataset_id: str :param dataset_id: dataset ID :rtype: list :return: List of extra files """ url = "/".join((self._make_url(history_id, contents=True), dataset_id, "extra_files")) return self._get(url=url) bioblend-1.2.0/bioblend/galaxy/invocations/000077500000000000000000000000001444761704300207015ustar00rootroot00000000000000bioblend-1.2.0/bioblend/galaxy/invocations/__init__.py000066400000000000000000000422461444761704300230220ustar00rootroot00000000000000""" Contains possible interactions with the Galaxy workflow invocations """ import logging import time from typing import ( Any, Dict, List, Optional, TYPE_CHECKING, ) from bioblend import ( CHUNK_SIZE, TimeoutException, ) from bioblend.galaxy.client import Client from bioblend.galaxy.workflows import InputsBy if TYPE_CHECKING: from bioblend.galaxy import GalaxyInstance log = logging.getLogger(__name__) INVOCATION_TERMINAL_STATES = {"cancelled", "failed", "scheduled"} # Invocation non-terminal states are: 'new', 'ready' class InvocationClient(Client): gi: "GalaxyInstance" module = "invocations" def __init__(self, galaxy_instance: "GalaxyInstance") -> None: super().__init__(galaxy_instance) def get_invocations( self, workflow_id: Optional[str] = None, history_id: Optional[str] = None, user_id: Optional[str] = None, include_terminal: bool = True, limit: Optional[int] = None, view: str = "collection", step_details: bool = False, ) -> List[Dict[str, Any]]: """ Get all workflow invocations, or select a subset by specifying optional arguments for filtering (e.g. a workflow ID). :type workflow_id: str :param workflow_id: Encoded workflow ID to filter on :type history_id: str :param history_id: Encoded history ID to filter on :type user_id: str :param user_id: Encoded user ID to filter on. This must be your own user ID if your are not an admin user. :type include_terminal: bool :param include_terminal: Whether to include terminal states. :type limit: int :param limit: Maximum number of invocations to return - if specified, the most recent invocations will be returned. :type view: str :param view: Level of detail to return per invocation, either 'element' or 'collection'. :type step_details: bool :param step_details: If 'view' is 'element', also include details on individual steps. :rtype: list :return: A list of workflow invocations. For example:: [{'history_id': '2f94e8ae9edff68a', 'id': 'df7a1f0c02a5b08e', 'model_class': 'WorkflowInvocation', 'state': 'new', 'update_time': '2015-10-31T22:00:22', 'uuid': 'c8aa2b1c-801a-11e5-a9e5-8ca98228593c', 'workflow_id': '03501d7626bd192f'}] """ params = {"include_terminal": include_terminal, "view": view, "step_details": step_details} if workflow_id: params["workflow_id"] = workflow_id if history_id: params["history_id"] = history_id if user_id: params["user_id"] = user_id if limit is not None: params["limit"] = limit return self._get(params=params) def show_invocation(self, invocation_id: str) -> Dict[str, Any]: """ Get a workflow invocation dictionary representing the scheduling of a workflow. This dictionary may be sparse at first (missing inputs and invocation steps) and will become more populated as the workflow is actually scheduled. :type invocation_id: str :param invocation_id: Encoded workflow invocation ID :rtype: dict :return: The workflow invocation. For example:: {'history_id': '2f94e8ae9edff68a', 'id': 'df7a1f0c02a5b08e', 'inputs': {'0': {'id': 'a7db2fac67043c7e', 'src': 'hda', 'uuid': '7932ffe0-2340-4952-8857-dbaa50f1f46a'}}, 'model_class': 'WorkflowInvocation', 'state': 'ready', 'steps': [{'action': None, 'id': 'd413a19dec13d11e', 'job_id': None, 'model_class': 'WorkflowInvocationStep', 'order_index': 0, 'state': None, 'update_time': '2015-10-31T22:00:26', 'workflow_step_id': 'cbbbf59e8f08c98c', 'workflow_step_label': None, 'workflow_step_uuid': 'b81250fd-3278-4e6a-b269-56a1f01ef485'}, {'action': None, 'id': '2f94e8ae9edff68a', 'job_id': 'e89067bb68bee7a0', 'model_class': 'WorkflowInvocationStep', 'order_index': 1, 'state': 'new', 'update_time': '2015-10-31T22:00:26', 'workflow_step_id': '964b37715ec9bd22', 'workflow_step_label': None, 'workflow_step_uuid': 'e62440b8-e911-408b-b124-e05435d3125e'}], 'update_time': '2015-10-31T22:00:26', 'uuid': 'c8aa2b1c-801a-11e5-a9e5-8ca98228593c', 'workflow_id': '03501d7626bd192f'} """ url = self._make_url(invocation_id) return self._get(url=url) def rerun_invocation( self, invocation_id: str, inputs_update: Optional[dict] = None, params_update: Optional[dict] = None, history_id: Optional[str] = None, history_name: Optional[str] = None, import_inputs_to_history: bool = False, replacement_params: Optional[dict] = None, allow_tool_state_corrections: bool = False, inputs_by: Optional[InputsBy] = None, parameters_normalized: bool = False, ) -> Dict[str, Any]: """ Rerun a workflow invocation. For more extensive documentation of all parameters, see the ``gi.workflows.invoke_workflow()`` method. :type invocation_id: str :param invocation_id: Encoded workflow invocation ID to be rerun :type inputs_update: dict :param inputs_update: If different datasets should be used to the original invocation, this should contain a mapping of workflow inputs to the new datasets and dataset collections. :type params_update: dict :param params_update: If different non-dataset tool parameters should be used to the original invocation, this should contain a mapping of the new parameter values. :type history_id: str :param history_id: The encoded history ID where to store the workflow outputs. Alternatively, ``history_name`` may be specified to create a new history. :type history_name: str :param history_name: Create a new history with the given name to store the workflow outputs. If both ``history_id`` and ``history_name`` are provided, ``history_name`` is ignored. If neither is specified, a new 'Unnamed history' is created. :type import_inputs_to_history: bool :param import_inputs_to_history: If ``True``, used workflow inputs will be imported into the history. If ``False``, only workflow outputs will be visible in the given history. :type allow_tool_state_corrections: bool :param allow_tool_state_corrections: If True, allow Galaxy to fill in missing tool state when running workflows. This may be useful for workflows using tools that have changed over time or for workflows built outside of Galaxy with only a subset of inputs defined. :type replacement_params: dict :param replacement_params: pattern-based replacements for post-job actions :type inputs_by: str :param inputs_by: Determines how inputs are referenced. Can be "step_index|step_uuid" (default), "step_index", "step_id", "step_uuid", or "name". :type parameters_normalized: bool :param parameters_normalized: Whether Galaxy should normalize the input parameters to ensure everything is referenced by a numeric step ID. Default is ``False``, but when setting parameters for a subworkflow, ``True`` is required. :rtype: dict :return: A dict describing the new workflow invocation. .. note:: This method works only on Galaxy 21.01 or later. """ invocation_details = self.show_invocation(invocation_id) workflow_id = invocation_details["workflow_id"] inputs = invocation_details["inputs"] wf_params = invocation_details["input_step_parameters"] if inputs_update: for inp, input_value in inputs_update.items(): inputs[inp] = input_value if params_update: for param, param_value in params_update.items(): wf_params[param] = param_value payload = {"inputs": inputs, "params": wf_params} if replacement_params: payload["replacement_params"] = replacement_params if history_id: payload["history"] = f"hist_id={history_id}" elif history_name: payload["history"] = history_name if not import_inputs_to_history: payload["no_add_to_history"] = True if allow_tool_state_corrections: payload["allow_tool_state_corrections"] = allow_tool_state_corrections if inputs_by is not None: payload["inputs_by"] = inputs_by if parameters_normalized: payload["parameters_normalized"] = parameters_normalized api_params = {"instance": True} url = "/".join((self.gi.url, "workflows", workflow_id, "invocations")) return self.gi.make_post_request(url=url, payload=payload, params=api_params) def cancel_invocation(self, invocation_id: str) -> Dict[str, Any]: """ Cancel the scheduling of a workflow. :type invocation_id: str :param invocation_id: Encoded workflow invocation ID :rtype: dict :return: The workflow invocation being cancelled """ url = self._make_url(invocation_id) return self._delete(url=url) def show_invocation_step(self, invocation_id: str, step_id: str) -> Dict[str, Any]: """ See the details of a particular workflow invocation step. :type invocation_id: str :param invocation_id: Encoded workflow invocation ID :type step_id: str :param step_id: Encoded workflow invocation step ID :rtype: dict :return: The workflow invocation step. For example:: {'action': None, 'id': '63cd3858d057a6d1', 'job_id': None, 'model_class': 'WorkflowInvocationStep', 'order_index': 2, 'state': None, 'update_time': '2015-10-31T22:11:14', 'workflow_step_id': '52e496b945151ee8', 'workflow_step_label': None, 'workflow_step_uuid': '4060554c-1dd5-4287-9040-8b4f281cf9dc'} """ url = self._invocation_step_url(invocation_id, step_id) return self._get(url=url) def run_invocation_step_action(self, invocation_id: str, step_id: str, action: Any) -> Dict[str, Any]: """Execute an action for an active workflow invocation step. The nature of this action and what is expected will vary based on the the type of workflow step (the only currently valid action is True/False for pause steps). :type invocation_id: str :param invocation_id: Encoded workflow invocation ID :type step_id: str :param step_id: Encoded workflow invocation step ID :type action: object :param action: Action to use when updating state, semantics depends on step type. :rtype: dict :return: Representation of the workflow invocation step """ url = self._invocation_step_url(invocation_id, step_id) payload = {"action": action} return self._put(payload=payload, url=url) def get_invocation_summary(self, invocation_id: str) -> Dict[str, Any]: """ Get a summary of an invocation, stating the number of jobs which succeed, which are paused and which have errored. :type invocation_id: str :param invocation_id: Encoded workflow invocation ID :rtype: dict :return: The invocation summary. For example:: {'states': {'paused': 4, 'error': 2, 'ok': 2}, 'model': 'WorkflowInvocation', 'id': 'a799d38679e985db', 'populated_state': 'ok'} """ url = self._make_url(invocation_id) + "/jobs_summary" return self._get(url=url) def get_invocation_step_jobs_summary(self, invocation_id: str) -> List[Dict[str, Any]]: """ Get a detailed summary of an invocation, listing all jobs with their job IDs and current states. :type invocation_id: str :param invocation_id: Encoded workflow invocation ID :rtype: list of dicts :return: The invocation step jobs summary. For example:: [{'id': 'e85a3be143d5905b', 'model': 'Job', 'populated_state': 'ok', 'states': {'ok': 1}}, {'id': 'c9468fdb6dc5c5f1', 'model': 'Job', 'populated_state': 'ok', 'states': {'running': 1}}, {'id': '2a56795cad3c7db3', 'model': 'Job', 'populated_state': 'ok', 'states': {'new': 1}}] """ url = self._make_url(invocation_id) + "/step_jobs_summary" return self._get(url=url) def get_invocation_report(self, invocation_id: str) -> Dict[str, Any]: """ Get a Markdown report for an invocation. :type invocation_id: str :param invocation_id: Encoded workflow invocation ID :rtype: dict :return: The invocation report. For example:: {'markdown': '\\n# Workflow Execution Summary of Example workflow\\n\\n ## Workflow Inputs\\n\\n\\n## Workflow Outputs\\n\\n\\n ## Workflow\\n```galaxy\\n workflow_display(workflow_id=f2db41e1fa331b3e)\\n```\\n', 'render_format': 'markdown', 'workflows': {'f2db41e1fa331b3e': {'name': 'Example workflow'}}} """ url = self._make_url(invocation_id) + "/report" return self._get(url=url) def get_invocation_report_pdf(self, invocation_id: str, file_path: str, chunk_size: int = CHUNK_SIZE) -> None: """ Get a PDF report for an invocation. :type invocation_id: str :param invocation_id: Encoded workflow invocation ID :type file_path: str :param file_path: Path to save the report """ url = self._make_url(invocation_id) + "/report.pdf" r = self.gi.make_get_request(url, stream=True) if r.status_code != 200: raise Exception( "Failed to get the PDF report, the necessary dependencies may not be installed on the Galaxy server." ) with open(file_path, "wb") as outf: for chunk in r.iter_content(chunk_size): outf.write(chunk) def get_invocation_biocompute_object(self, invocation_id: str) -> Dict[str, Any]: """ Get a BioCompute object for an invocation. :type invocation_id: str :param invocation_id: Encoded workflow invocation ID :rtype: dict :return: The BioCompute object """ url = self._make_url(invocation_id) + "/biocompute" return self._get(url=url) def wait_for_invocation( self, invocation_id: str, maxwait: float = 12000, interval: float = 3, check: bool = True ) -> Dict[str, Any]: """ Wait until an invocation is in a terminal state. :type invocation_id: str :param invocation_id: Invocation ID to wait for. :type maxwait: float :param maxwait: Total time (in seconds) to wait for the invocation state to become terminal. If the invocation state is not terminal within this time, a ``TimeoutException`` will be raised. :type interval: float :param interval: Time (in seconds) to wait between 2 consecutive checks. :type check: bool :param check: Whether to check if the invocation terminal state is 'scheduled'. :rtype: dict :return: Details of the workflow invocation. """ assert maxwait >= 0 assert interval > 0 time_left = maxwait while True: invocation = self.gi.invocations.show_invocation(invocation_id) state = invocation["state"] if state in INVOCATION_TERMINAL_STATES: if check and state != "scheduled": raise Exception(f"Invocation {invocation_id} is in terminal state {state}") return invocation if time_left > 0: log.info(f"Invocation {invocation_id} is in non-terminal state {state}. Will wait {time_left} more s") time.sleep(min(time_left, interval)) time_left -= interval else: raise TimeoutException( f"Invocation {invocation_id} is still in non-terminal state {state} after {maxwait} s" ) def _invocation_step_url(self, invocation_id: str, step_id: str) -> str: return "/".join((self._make_url(invocation_id), "steps", step_id)) __all__ = ("InvocationClient",) bioblend-1.2.0/bioblend/galaxy/jobs/000077500000000000000000000000001444761704300173025ustar00rootroot00000000000000bioblend-1.2.0/bioblend/galaxy/jobs/__init__.py000066400000000000000000000426651444761704300214300ustar00rootroot00000000000000""" Contains possible interactions with the Galaxy Jobs """ import logging import time from typing import ( Any, Dict, List, Optional, TYPE_CHECKING, ) from typing_extensions import Literal from bioblend import TimeoutException from bioblend.galaxy.client import Client if TYPE_CHECKING: from bioblend.galaxy import GalaxyInstance log = logging.getLogger(__name__) JOB_TERMINAL_STATES = {"deleted", "deleting", "error", "ok"} # Job non-terminal states are: 'deleted_new', 'failed', 'new', 'paused', # 'queued', 'resubmitted', 'running', 'upload', 'waiting' class JobsClient(Client): module = "jobs" def __init__(self, galaxy_instance: "GalaxyInstance") -> None: super().__init__(galaxy_instance) def get_jobs( self, state: Optional[str] = None, history_id: Optional[str] = None, invocation_id: Optional[str] = None, tool_id: Optional[str] = None, workflow_id: Optional[str] = None, user_id: Optional[str] = None, date_range_min: Optional[str] = None, date_range_max: Optional[str] = None, limit: int = 500, offset: int = 0, user_details: bool = False, order_by: Literal["create_time", "update_time"] = "update_time", ) -> List[Dict[str, Any]]: """ Get all jobs, or select a subset by specifying optional arguments for filtering (e.g. a state). If the user is an admin, this will return jobs for all the users, otherwise only for the current user. :type state: str or list of str :param state: Job states to filter on. :type history_id: str :param history_id: Encoded history ID to filter on. :type invocation_id: string :param invocation_id: Encoded workflow invocation ID to filter on. :type tool_id: str or list of str :param tool_id: Tool IDs to filter on. :type workflow_id: string :param workflow_id: Encoded workflow ID to filter on. :type user_id: str :param user_id: Encoded user ID to filter on. Only admin users can access the jobs of other users. :type date_range_min: str :param date_range_min: Mininum job update date (in YYYY-MM-DD format) to filter on. :type date_range_max: str :param date_range_max: Maximum job update date (in YYYY-MM-DD format) to filter on. :type limit: int :param limit: Maximum number of jobs to return. :type offset: int :param offset: Return jobs starting from this specified position. For example, if ``limit`` is set to 100 and ``offset`` to 200, jobs 200-299 will be returned. :type user_details: bool :param user_details: If ``True`` and the user is an admin, add the user email to each returned job dictionary. :type order_by: str :param order_by: Whether to order jobs by ``create_time`` or ``update_time`` (the default). :rtype: list of dict :return: Summary information for each selected job. For example:: [{'create_time': '2014-03-01T16:16:48.640550', 'exit_code': 0, 'id': 'ebfb8f50c6abde6d', 'model_class': 'Job', 'state': 'ok', 'tool_id': 'fasta2tab', 'update_time': '2014-03-01T16:16:50.657399'}, {'create_time': '2014-03-01T16:05:34.851246', 'exit_code': 0, 'id': '1cd8e2f6b131e891', 'model_class': 'Job', 'state': 'ok', 'tool_id': 'upload1', 'update_time': '2014-03-01T16:05:39.558458'}] .. note:: The following parameters work only on Galaxy 21.05 or later: ``user_id``, ``limit``, ``offset``, ``workflow_id``, ``invocation_id``. """ params: Dict[str, Any] = {"limit": limit, "offset": offset} if state: params["state"] = state if history_id: params["history_id"] = history_id if invocation_id: params["invocation_id"] = invocation_id if tool_id: params["tool_id"] = tool_id if workflow_id: params["workflow_id"] = workflow_id if user_id: params["user_id"] = user_id if date_range_min: params["date_range_min"] = date_range_min if date_range_max: params["date_range_max"] = date_range_max if user_details: params["user_details"] = user_details if order_by: params["order_by"] = order_by return self._get(params=params) def show_job(self, job_id: str, full_details: bool = False) -> Dict[str, Any]: """ Get details of a given job of the current user. :type job_id: str :param job_id: job ID :type full_details: bool :param full_details: when ``True``, the complete list of details for the given job. :rtype: dict :return: A description of the given job. For example:: {'create_time': '2014-03-01T16:17:29.828624', 'exit_code': 0, 'id': 'a799d38679e985db', 'inputs': {'input': {'id': 'ebfb8f50c6abde6d', 'src': 'hda'}}, 'model_class': 'Job', 'outputs': {'output': {'id': 'a799d38679e985db', 'src': 'hda'}}, 'params': {'chromInfo': '"/opt/galaxy-central/tool-data/shared/ucsc/chrom/?.len"', 'dbkey': '"?"', 'seq_col': '"2"', 'title_col': '["1"]'}, 'state': 'ok', 'tool_id': 'tab2fasta', 'update_time': '2014-03-01T16:17:31.930728'} """ params = {} if full_details: params["full"] = full_details return self._get(id=job_id, params=params) def _build_for_rerun(self, job_id: str) -> Dict[str, Any]: """ Get details of a given job that can be used to rerun the corresponding tool. :type job_id: str :param job_id: job ID :rtype: dict :return: A description of the given job, with all parameters required to rerun. """ url = "/".join((self._make_url(job_id), "build_for_rerun")) return self._get(url=url) def rerun_job( self, job_id: str, remap: bool = False, tool_inputs_update: Optional[Dict[str, Any]] = None, history_id: Optional[str] = None, ) -> Dict[str, Any]: """ Rerun a job. :type job_id: str :param job_id: job ID :type remap: bool :param remap: when ``True``, the job output(s) will be remapped onto the dataset(s) created by the original job; if other jobs were waiting for this job to finish successfully, they will be resumed using the new outputs of this tool run. When ``False``, new job output(s) will be created. Note that if Galaxy does not permit remapping for the job in question, specifying ``True`` will result in an error. :type tool_inputs_update: dict :param tool_inputs_update: dictionary specifying any changes which should be made to tool parameters for the rerun job. This dictionary should have the same structure as is required when submitting the ``tool_inputs`` dictionary to ``gi.tools.run_tool()``, but only needs to include the inputs or parameters to be updated for the rerun job. :type history_id: str :param history_id: ID of the history in which the job should be executed; if not specified, the same history will be used as the original job run. :rtype: dict :return: Information about outputs and the rerun job .. note:: This method works only on Galaxy 21.01 or later. """ job_rerun_params = self._build_for_rerun(job_id) job_inputs = job_rerun_params["state_inputs"] if remap: if not job_rerun_params["job_remap"]: raise ValueError("remap was set to True, but this job is not remappable.") job_inputs["rerun_remap_job_id"] = job_id def update_inputs(inputs: Dict[str, Any], tool_inputs_update: Dict[str, Any]) -> None: """Recursively update inputs with tool_inputs_update""" for input_param, input_value in tool_inputs_update.items(): if isinstance(input_value, dict): update_inputs(inputs[input_param], input_value) else: inputs[input_param] = input_value if tool_inputs_update: update_inputs(job_inputs, tool_inputs_update) url = "/".join((self.gi.url, "tools")) payload = { "history_id": history_id if history_id else job_rerun_params["history_id"], "tool_id": job_rerun_params["id"], "inputs": job_inputs, "input_format": "21.01", } return self._post(url=url, payload=payload) def get_state(self, job_id: str) -> str: """ Display the current state for a given job of the current user. :type job_id: str :param job_id: job ID :rtype: str :return: state of the given job among the following values: `new`, `queued`, `running`, `waiting`, `ok`. If the state cannot be retrieved, an empty string is returned. .. versionadded:: 0.5.3 """ return self.show_job(job_id).get("state", "") def search_jobs(self, tool_id: str, inputs: Dict[str, Any], state: Optional[str] = None) -> List[Dict[str, Any]]: """ Return jobs matching input parameters. :type tool_id: str :param tool_id: only return jobs associated with this tool ID :type inputs: dict :param inputs: return only jobs that have matching inputs :type state: str :param state: only return jobs in this state :rtype: list of dicts :return: Summary information for each matching job This method is designed to scan the list of previously run jobs and find records of jobs with identical input parameters and datasets. This can be used to minimize the amount of repeated work by simply recycling the old results. .. versionchanged:: 0.16.0 Replaced the ``job_info`` parameter with separate ``tool_id``, ``inputs`` and ``state``. """ job_info = { "tool_id": tool_id, "inputs": inputs, } if state: job_info["state"] = state url = self._make_url() + "/search" return self._post(url=url, payload=job_info) def get_metrics(self, job_id: str) -> List[Dict[str, Any]]: """ Return job metrics for a given job. :type job_id: str :param job_id: job ID :rtype: list :return: list containing job metrics .. note:: Calling ``show_job()`` with ``full_details=True`` also returns the metrics for a job if the user is an admin. This method allows to fetch metrics even as a normal user as long as the Galaxy instance has the ``expose_potentially_sensitive_job_metrics`` option set to ``true`` in the ``config/galaxy.yml`` configuration file. """ url = self._make_url(module_id=job_id) + "/metrics" return self._get(url=url) def cancel_job(self, job_id: str) -> bool: """ Cancel a job, deleting output datasets. :type job_id: str :param job_id: job ID :rtype: bool :return: ``True`` if the job was successfully cancelled, ``False`` if it was already in a terminal state before the cancellation. """ return self._delete(id=job_id) def report_error(self, job_id: str, dataset_id: str, message: str, email: Optional[str] = None) -> Dict[str, Any]: """ Report an error for a given job and dataset to the server administrators. :type job_id: str :param job_id: job ID :type dataset_id: str :param dataset_id: Dataset ID :type message: str :param message: Error message :type email: str :param email: Email for error report submission. If not specified, the email associated with the Galaxy user account is used by default. :rtype: dict :return: dict containing job error reply .. note:: This method works only on Galaxy 20.01 or later. """ payload = { "message": message, "dataset_id": dataset_id, } if email is not None: payload["email"] = email url = self._make_url(module_id=job_id) + "/error" return self._post(url=url, payload=payload) def get_common_problems(self, job_id: str) -> Dict[str, Any]: """ Query inputs and jobs for common potential problems that might have resulted in job failure. :type job_id: str :param job_id: job ID :rtype: dict :return: dict containing potential problems .. note:: This method works only on Galaxy 19.05 or later. """ url = self._make_url(module_id=job_id) + "/common_problems" return self._get(url=url) def get_inputs(self, job_id: str) -> List[Dict[str, Any]]: """ Get dataset inputs used by a job. :type job_id: str :param job_id: job ID :rtype: list of dicts :return: Inputs for the given job """ url = self._make_url(module_id=job_id) + "/inputs" return self._get(url=url) def get_outputs(self, job_id: str) -> List[Dict[str, Any]]: """ Get dataset outputs produced by a job. :type job_id: str :param job_id: job ID :rtype: list of dicts :return: Outputs of the given job """ url = self._make_url(module_id=job_id) + "/outputs" return self._get(url=url) def resume_job(self, job_id: str) -> List[Dict[str, Any]]: """ Resume a job if it is paused. :type job_id: str :param job_id: job ID :rtype: list of dicts :return: list of dictionaries containing output dataset associations """ url = self._make_url(module_id=job_id) + "/resume" return self._put(url=url) def get_destination_params(self, job_id: str) -> Dict[str, Any]: """ Get destination parameters for a job, describing the environment and location where the job is run. :type job_id: str :param job_id: job ID :rtype: dict :return: Destination parameters for the given job .. note:: This method works only on Galaxy 20.05 or later and if the user is a Galaxy admin. """ url = self._make_url(module_id=job_id) + "/destination_params" return self._get(url=url) def show_job_lock(self) -> bool: """ Show whether the job lock is active or not. If it is active, no jobs will dispatch on the Galaxy server. :rtype: bool :return: Status of the job lock .. note:: This method works only on Galaxy 20.05 or later and if the user is a Galaxy admin. """ url = self.gi.url + "/job_lock" response = self._get(url=url) return response["active"] def update_job_lock(self, active: bool = False) -> bool: """ Update the job lock status by setting ``active`` to either ``True`` or ``False``. If ``True``, all job dispatching will be blocked. :rtype: bool :return: Updated status of the job lock .. note:: This method works only on Galaxy 20.05 or later and if the user is a Galaxy admin. """ payload = { "active": active, } url = self.gi.url + "/job_lock" response = self._put(url=url, payload=payload) return response["active"] def wait_for_job( self, job_id: str, maxwait: float = 12000, interval: float = 3, check: bool = True ) -> Dict[str, Any]: """ Wait until a job is in a terminal state. :type job_id: str :param job_id: job ID :type maxwait: float :param maxwait: Total time (in seconds) to wait for the job state to become terminal. If the job state is not terminal within this time, a ``TimeoutException`` will be raised. :type interval: float :param interval: Time (in seconds) to wait between 2 consecutive checks. :type check: bool :param check: Whether to check if the job terminal state is 'ok'. :rtype: dict :return: Details of the given job. """ assert maxwait >= 0 assert interval > 0 time_left = maxwait while True: job = self.show_job(job_id) state = job["state"] if state in JOB_TERMINAL_STATES: if check and state != "ok": raise Exception(f"Job {job_id} is in terminal state {state}") return job if time_left > 0: log.info(f"Job {job_id} is in non-terminal state {state}. Will wait {time_left} more s") time.sleep(min(time_left, interval)) time_left -= interval else: raise TimeoutException(f"Job {job_id} is still in non-terminal state {state} after {maxwait} s") bioblend-1.2.0/bioblend/galaxy/libraries/000077500000000000000000000000001444761704300203215ustar00rootroot00000000000000bioblend-1.2.0/bioblend/galaxy/libraries/__init__.py000066400000000000000000000662521444761704300224450ustar00rootroot00000000000000""" Contains possible interactions with the Galaxy Data Libraries """ import logging import time from typing import ( Any, Dict, List, Optional, TYPE_CHECKING, ) from typing_extensions import Literal from bioblend.galaxy.client import Client from bioblend.galaxy.datasets import ( DatasetTimeoutException, TERMINAL_STATES, ) from bioblend.util import attach_file if TYPE_CHECKING: from bioblend.galaxy import GalaxyInstance LinkDataOnly = Literal["copy_files", "link_to_files"] log = logging.getLogger(__name__) class LibraryClient(Client): module = "libraries" def __init__(self, galaxy_instance: "GalaxyInstance") -> None: super().__init__(galaxy_instance) def create_library( self, name: str, description: Optional[str] = None, synopsis: Optional[str] = None ) -> Dict[str, Any]: """ Create a data library with the properties defined in the arguments. :type name: str :param name: Name of the new data library :type description: str :param description: Optional data library description :type synopsis: str :param synopsis: Optional data library synopsis :rtype: dict :return: Details of the created library. For example:: {'id': 'f740ab636b360a70', 'name': 'Library from bioblend', 'url': '/api/libraries/f740ab636b360a70'} """ payload = {"name": name} if description: payload["description"] = description if synopsis: payload["synopsis"] = synopsis return self._post(payload) def delete_library(self, library_id: str) -> Dict[str, Any]: """ Delete a data library. :type library_id: str :param library_id: Encoded data library ID identifying the library to be deleted :rtype: dict :return: Information about the deleted library .. warning:: Deleting a data library is irreversible - all of the data from the library will be permanently deleted. """ return self._delete(id=library_id) def _show_item(self, library_id: str, item_id: str) -> Dict[str, Any]: """ Get details about a given library item. """ url = "/".join((self._make_url(library_id, contents=True), item_id)) return self._get(url=url) def delete_library_dataset(self, library_id: str, dataset_id: str, purged: bool = False) -> Dict[str, Any]: """ Delete a library dataset in a data library. :type library_id: str :param library_id: library id where dataset is found in :type dataset_id: str :param dataset_id: id of the dataset to be deleted :type purged: bool :param purged: Indicate that the dataset should be purged (permanently deleted) :rtype: dict :return: A dictionary containing the dataset id and whether the dataset has been deleted. For example:: {'deleted': True, 'id': '60e680a037f41974'} """ url = "/".join((self._make_url(library_id, contents=True), dataset_id)) return self._delete(payload={"purged": purged}, url=url) def update_library_dataset(self, dataset_id: str, **kwargs: Any) -> Dict[str, Any]: """ Update library dataset metadata. Some of the attributes that can be modified are documented below. :type dataset_id: str :param dataset_id: id of the dataset to be updated :type name: str :param name: Replace library dataset name with the given string :type misc_info: str :param misc_info: Replace library dataset misc_info with given string :type file_ext: str :param file_ext: Replace library dataset extension (must exist in the Galaxy registry) :type genome_build: str :param genome_build: Replace library dataset genome build (dbkey) :type tags: list :param tags: Replace library dataset tags with the given list :rtype: dict :return: details of the updated dataset """ url = "/".join((self._make_url(), "datasets", dataset_id)) return self._patch(payload=kwargs, url=url) def show_dataset(self, library_id: str, dataset_id: str) -> Dict[str, Any]: """ Get details about a given library dataset. The required ``library_id`` can be obtained from the datasets's library content details. :type library_id: str :param library_id: library id where dataset is found in :type dataset_id: str :param dataset_id: id of the dataset to be inspected :rtype: dict :return: A dictionary containing information about the dataset in the library """ return self._show_item(library_id, dataset_id) def wait_for_dataset( self, library_id: str, dataset_id: str, maxwait: float = 12000, interval: float = 3 ) -> Dict[str, Any]: """ Wait until the library dataset state is terminal ('ok', 'empty', 'error', 'discarded' or 'failed_metadata'). :type library_id: str :param library_id: library id where dataset is found in :type dataset_id: str :param dataset_id: id of the dataset to wait for :type maxwait: float :param maxwait: Total time (in seconds) to wait for the dataset state to become terminal. If the dataset state is not terminal within this time, a ``DatasetTimeoutException`` will be thrown. :type interval: float :param interval: Time (in seconds) to wait between 2 consecutive checks. :rtype: dict :return: A dictionary containing information about the dataset in the library """ assert maxwait >= 0 assert interval > 0 time_left = maxwait while True: dataset = self.show_dataset(library_id, dataset_id) state = dataset["state"] if state in TERMINAL_STATES: return dataset if time_left > 0: log.info( "Dataset %s in library %s is in non-terminal state %s. Will wait %i more s", dataset_id, library_id, state, time_left, ) time.sleep(min(time_left, interval)) time_left -= interval else: raise DatasetTimeoutException( f"Dataset {dataset_id} in library {library_id} is still in non-terminal state {state} after {maxwait} s" ) def show_folder(self, library_id: str, folder_id: str) -> Dict[str, Any]: """ Get details about a given folder. The required ``folder_id`` can be obtained from the folder's library content details. :type library_id: str :param library_id: library id to inspect folders in :type folder_id: str :param folder_id: id of the folder to be inspected :rtype: dict :return: Information about the folder """ return self._show_item(library_id, folder_id) def _get_root_folder_id(self, library_id: str) -> str: """ Find the root folder (i.e. '/') of a library. :type library_id: str :param library_id: library id to find root of """ library_dict = self.show_library(library_id=library_id) return library_dict["root_folder_id"] def create_folder( self, library_id: str, folder_name: str, description: Optional[str] = None, base_folder_id: Optional[str] = None ) -> List[Dict[str, Any]]: """ Create a folder in a library. :type library_id: str :param library_id: library id to use :type folder_name: str :param folder_name: name of the new folder in the data library :type description: str :param description: description of the new folder in the data library :type base_folder_id: str :param base_folder_id: id of the folder where to create the new folder. If not provided, the root folder will be used :rtype: list :return: List with a single dictionary containing information about the new folder """ # Get root folder ID if no ID was provided if base_folder_id is None: base_folder_id = self._get_root_folder_id(library_id) # Compose the payload payload = { "name": folder_name, "folder_id": base_folder_id, "create_type": "folder", } if description is not None: payload["description"] = description return self._post(payload, id=library_id, contents=True) def get_folders( self, library_id: str, folder_id: Optional[str] = None, name: Optional[str] = None ) -> List[Dict[str, Any]]: """ Get all the folders in a library, or select a subset by specifying a folder name for filtering. :type library_id: str :param library_id: library id to use :type name: str :param name: Folder name to filter on. For ``name`` specify the full path of the folder starting from the library's root folder, e.g. ``/subfolder/subsubfolder``. :rtype: list :return: list of dicts each containing basic information about a folder .. versionchanged:: 1.1.1 Using the deprecated ``folder_id`` parameter now raises a ``ValueError`` exception. """ if folder_id is not None: raise ValueError( "The folder_id parameter has been removed, use the show_folder() method to view details of a folder for which you know the ID." ) library_contents = self.show_library(library_id=library_id, contents=True) if name is not None: folders = [_ for _ in library_contents if _["type"] == "folder" and _["name"] == name] else: folders = [_ for _ in library_contents if _["type"] == "folder"] return folders def get_libraries( self, library_id: Optional[str] = None, name: Optional[str] = None, deleted: Optional[bool] = False ) -> List[Dict[str, Any]]: """ Get all libraries, or select a subset by specifying optional arguments for filtering (e.g. a library name). :type name: str :param name: Library name to filter on. :type deleted: bool :param deleted: If ``False`` (the default), return only non-deleted libraries. If ``True``, return only deleted libraries. If ``None``, return both deleted and non-deleted libraries. :rtype: list :return: list of dicts each containing basic information about a library .. versionchanged:: 1.1.1 Using the deprecated ``library_id`` parameter now raises a ``ValueError`` exception. """ if library_id is not None: raise ValueError( "The library_id parameter has been removed, use the show_library() method to view details of a library for which you know the ID." ) libraries = self._get(params={"deleted": deleted}) if name is not None: libraries = [_ for _ in libraries if _["name"] == name] return libraries def show_library(self, library_id: str, contents: bool = False) -> Dict[str, Any]: """ Get information about a library. :type library_id: str :param library_id: filter for library by library id :type contents: bool :param contents: whether to get contents of the library (rather than just the library details) :rtype: dict :return: details of the given library """ return self._get(id=library_id, contents=contents) def _do_upload(self, library_id: str, **kwargs: Any) -> List[Dict[str, Any]]: """ Set up the POST request and do the actual data upload to a data library. This method should not be called directly but instead refer to the methods specific for the desired type of data upload. """ folder_id = kwargs.get("folder_id") if folder_id is None: folder_id = self._get_root_folder_id(library_id) files_attached = False # Compose the payload dict payload = { "folder_id": folder_id, "file_type": kwargs.get("file_type", "auto"), "dbkey": kwargs.get("dbkey", "?"), "create_type": "file", "tag_using_filenames": kwargs.get("tag_using_filenames", False), "preserve_dirs": kwargs.get("preserve_dirs", False), } if kwargs.get("roles"): payload["roles"] = kwargs["roles"] if kwargs.get("link_data_only") and kwargs["link_data_only"] != "copy_files": payload["link_data_only"] = "link_to_files" if kwargs.get("tags"): payload["tags"] = kwargs["tags"] # upload options if kwargs.get("file_url") is not None: payload["upload_option"] = "upload_file" payload["files_0|url_paste"] = kwargs["file_url"] elif kwargs.get("pasted_content") is not None: payload["upload_option"] = "upload_file" payload["files_0|url_paste"] = kwargs["pasted_content"] elif kwargs.get("server_dir") is not None: payload["upload_option"] = "upload_directory" payload["server_dir"] = kwargs["server_dir"] elif kwargs.get("file_local_path") is not None: payload["upload_option"] = "upload_file" payload["files_0|file_data"] = attach_file(kwargs["file_local_path"]) files_attached = True elif kwargs.get("filesystem_paths") is not None: payload["upload_option"] = "upload_paths" payload["filesystem_paths"] = kwargs["filesystem_paths"] try: return self._post(payload, id=library_id, contents=True, files_attached=files_attached) finally: if payload.get("files_0|file_data") is not None: payload["files_0|file_data"].close() def upload_file_from_url( self, library_id: str, file_url: str, folder_id: Optional[str] = None, file_type: str = "auto", dbkey: str = "?", tags: Optional[List[str]] = None, ) -> List[Dict[str, Any]]: """ Upload a file to a library from a URL. :type library_id: str :param library_id: id of the library where to place the uploaded file :type file_url: str :param file_url: URL of the file to upload :type folder_id: str :param folder_id: id of the folder where to place the uploaded file. If not provided, the root folder will be used :type file_type: str :param file_type: Galaxy file format name :type dbkey: str :param dbkey: Dbkey :type tags: list :param tags: A list of tags to add to the datasets :rtype: list :return: List with a single dictionary containing information about the LDDA """ return self._do_upload( library_id, file_url=file_url, folder_id=folder_id, file_type=file_type, dbkey=dbkey, tags=tags ) def upload_file_contents( self, library_id: str, pasted_content: str, folder_id: Optional[str] = None, file_type: str = "auto", dbkey: str = "?", tags: Optional[List[str]] = None, ) -> List[Dict[str, Any]]: """ Upload pasted_content to a data library as a new file. :type library_id: str :param library_id: id of the library where to place the uploaded file :type pasted_content: str :param pasted_content: Content to upload into the library :type folder_id: str :param folder_id: id of the folder where to place the uploaded file. If not provided, the root folder will be used :type file_type: str :param file_type: Galaxy file format name :type dbkey: str :param dbkey: Dbkey :type tags: list :param tags: A list of tags to add to the datasets :rtype: list :return: List with a single dictionary containing information about the LDDA """ return self._do_upload( library_id, pasted_content=pasted_content, folder_id=folder_id, file_type=file_type, dbkey=dbkey, tags=tags ) def upload_file_from_local_path( self, library_id: str, file_local_path: str, folder_id: Optional[str] = None, file_type: str = "auto", dbkey: str = "?", tags: Optional[List[str]] = None, ) -> List[Dict[str, Any]]: """ Read local file contents from file_local_path and upload data to a library. :type library_id: str :param library_id: id of the library where to place the uploaded file :type file_local_path: str :param file_local_path: path of local file to upload :type folder_id: str :param folder_id: id of the folder where to place the uploaded file. If not provided, the root folder will be used :type file_type: str :param file_type: Galaxy file format name :type dbkey: str :param dbkey: Dbkey :type tags: list :param tags: A list of tags to add to the datasets :rtype: list :return: List with a single dictionary containing information about the LDDA """ return self._do_upload( library_id, file_local_path=file_local_path, folder_id=folder_id, file_type=file_type, dbkey=dbkey, tags=tags, ) def upload_file_from_server( self, library_id: str, server_dir: str, folder_id: Optional[str] = None, file_type: str = "auto", dbkey: str = "?", link_data_only: Optional[LinkDataOnly] = None, roles: str = "", preserve_dirs: bool = False, tag_using_filenames: bool = False, tags: Optional[List[str]] = None, ) -> List[Dict[str, Any]]: """ Upload all files in the specified subdirectory of the Galaxy library import directory to a library. :type library_id: str :param library_id: id of the library where to place the uploaded file :type server_dir: str :param server_dir: relative path of the subdirectory of ``library_import_dir`` to upload. All and only the files (i.e. no subdirectories) contained in the specified directory will be uploaded :type folder_id: str :param folder_id: id of the folder where to place the uploaded files. If not provided, the root folder will be used :type file_type: str :param file_type: Galaxy file format name :type dbkey: str :param dbkey: Dbkey :type link_data_only: str :param link_data_only: either 'copy_files' (default) or 'link_to_files'. Setting to 'link_to_files' symlinks instead of copying the files :type roles: str :param roles: ??? :type preserve_dirs: bool :param preserve_dirs: Indicate whether to preserve the directory structure when importing dir :type tag_using_filenames: bool :param tag_using_filenames: Indicate whether to generate dataset tags from filenames. .. versionchanged:: 0.14.0 Changed the default from ``True`` to ``False``. :type tags: list :param tags: A list of tags to add to the datasets :rtype: list :return: List with a single dictionary containing information about the LDDA .. note:: This method works only if the Galaxy instance has the ``library_import_dir`` option configured in the ``config/galaxy.yml`` configuration file. """ return self._do_upload( library_id, server_dir=server_dir, folder_id=folder_id, file_type=file_type, dbkey=dbkey, link_data_only=link_data_only, roles=roles, preserve_dirs=preserve_dirs, tag_using_filenames=tag_using_filenames, tags=tags, ) def upload_from_galaxy_filesystem( self, library_id: str, filesystem_paths: str, folder_id: Optional[str] = None, file_type: str = "auto", dbkey: str = "?", link_data_only: Optional[LinkDataOnly] = None, roles: str = "", preserve_dirs: bool = False, tag_using_filenames: bool = False, tags: Optional[List[str]] = None, ) -> List[Dict[str, Any]]: """ Upload a set of files already present on the filesystem of the Galaxy server to a library. :type library_id: str :param library_id: id of the library where to place the uploaded file :type filesystem_paths: str :param filesystem_paths: file paths on the Galaxy server to upload to the library, one file per line :type folder_id: str :param folder_id: id of the folder where to place the uploaded files. If not provided, the root folder will be used :type file_type: str :param file_type: Galaxy file format name :type dbkey: str :param dbkey: Dbkey :type link_data_only: str :param link_data_only: either 'copy_files' (default) or 'link_to_files'. Setting to 'link_to_files' symlinks instead of copying the files :type roles: str :param roles: ??? :type preserve_dirs: bool :param preserve_dirs: Indicate whether to preserve the directory structure when importing dir :type tag_using_filenames: bool :param tag_using_filenames: Indicate whether to generate dataset tags from filenames. .. versionchanged:: 0.14.0 Changed the default from ``True`` to ``False``. :type tags: list :param tags: A list of tags to add to the datasets :rtype: list :return: List of dictionaries containing information about each uploaded LDDA. .. note:: This method works only if the Galaxy instance has the ``allow_path_paste`` option set to ``true`` in the ``config/galaxy.yml`` configuration file. """ return self._do_upload( library_id, filesystem_paths=filesystem_paths, folder_id=folder_id, file_type=file_type, dbkey=dbkey, link_data_only=link_data_only, roles=roles, preserve_dirs=preserve_dirs, tag_using_filenames=tag_using_filenames, tags=tags, ) def copy_from_dataset( self, library_id: str, dataset_id: str, folder_id: Optional[str] = None, message: str = "" ) -> Dict[str, Any]: """ Copy a Galaxy dataset into a library. :type library_id: str :param library_id: id of the library where to place the uploaded file :type dataset_id: str :param dataset_id: id of the dataset to copy from :type folder_id: str :param folder_id: id of the folder where to place the uploaded files. If not provided, the root folder will be used :type message: str :param message: message for copying action :rtype: dict :return: LDDA information """ if folder_id is None: folder_id = self._get_root_folder_id(library_id) payload = { "folder_id": folder_id, "create_type": "file", "from_hda_id": dataset_id, "ldda_message": message, } return self._post(payload, id=library_id, contents=True) def get_library_permissions(self, library_id: str) -> Dict[str, Any]: """ Get the permissions for a library. :type library_id: str :param library_id: id of the library :rtype: dict :return: dictionary with all applicable permissions' values """ url = self._make_url(library_id) + "/permissions" return self._get(url=url) def get_dataset_permissions(self, dataset_id: str) -> Dict[str, Any]: """ Get the permissions for a dataset. :type dataset_id: str :param dataset_id: id of the dataset :rtype: dict :return: dictionary with all applicable permissions' values """ url = "/".join((self._make_url(), "datasets", dataset_id, "permissions")) return self._get(url=url) def set_library_permissions( self, library_id: str, access_in: Optional[List[str]] = None, modify_in: Optional[List[str]] = None, add_in: Optional[List[str]] = None, manage_in: Optional[List[str]] = None, ) -> Dict[str, Any]: """ Set the permissions for a library. Note: it will override all security for this library even if you leave out a permission type. :type library_id: str :param library_id: id of the library :type access_in: list :param access_in: list of role ids :type modify_in: list :param modify_in: list of role ids :type add_in: list :param add_in: list of role ids :type manage_in: list :param manage_in: list of role ids :rtype: dict :return: General information about the library """ payload: Dict[str, List[str]] = {} if access_in: payload["LIBRARY_ACCESS_in"] = access_in if modify_in: payload["LIBRARY_MODIFY_in"] = modify_in if add_in: payload["LIBRARY_ADD_in"] = add_in if manage_in: payload["LIBRARY_MANAGE_in"] = manage_in url = self._make_url(library_id) + "/permissions" return self._post(payload, url=url) def set_dataset_permissions( self, dataset_id: str, access_in: Optional[List[str]] = None, modify_in: Optional[List[str]] = None, manage_in: Optional[List[str]] = None, ) -> Dict[str, Any]: """ Set the permissions for a dataset. Note: it will override all security for this dataset even if you leave out a permission type. :type dataset_id: str :param dataset_id: id of the dataset :type access_in: list :param access_in: list of role ids :type modify_in: list :param modify_in: list of role ids :type manage_in: list :param manage_in: list of role ids :rtype: dict :return: dictionary with all applicable permissions' values """ # we need here to define an action payload: Dict[str, Any] = { "action": "set_permissions", } if access_in: payload["access_ids[]"] = access_in if modify_in: payload["modify_ids[]"] = modify_in if manage_in: payload["manage_ids[]"] = manage_in url = "/".join((self._make_url(), "datasets", dataset_id, "permissions")) return self._post(payload, url=url) bioblend-1.2.0/bioblend/galaxy/objects/000077500000000000000000000000001444761704300177765ustar00rootroot00000000000000bioblend-1.2.0/bioblend/galaxy/objects/__init__.py000066400000000000000000000001451444761704300221070ustar00rootroot00000000000000from .galaxy_instance import GalaxyInstance # noqa: F401 from .wrappers import * # noqa: F401,F403 bioblend-1.2.0/bioblend/galaxy/objects/client.py000066400000000000000000000475331444761704300216420ustar00rootroot00000000000000""" Clients for interacting with specific Galaxy entity types. Classes in this module should not be instantiated directly, but used via their handles in :class:`~.galaxy_instance.GalaxyInstance`. """ import abc import json from collections.abc import Sequence from typing import ( Any, cast, Dict, Generic, List, Optional, overload, Type, TYPE_CHECKING, Union, ) from typing_extensions import Literal import bioblend from bioblend.galaxy.datasets import HdaLdda from . import wrappers if TYPE_CHECKING: from .galaxy_instance import GalaxyInstance class ObjClient(abc.ABC): def __init__(self, obj_gi: "GalaxyInstance") -> None: self.obj_gi = obj_gi self.gi = self.obj_gi.gi self.log = bioblend.log @abc.abstractmethod def get(self, id_: str) -> wrappers.Wrapper: """ Retrieve the object corresponding to the given id. """ @abc.abstractmethod def get_previews(self, **kwargs: Any) -> list: """ Get a list of object previews. Previews entity summaries provided by REST collection URIs, e.g. ``http://host:port/api/libraries``. Being the most lightweight objects associated to the various entities, these are the ones that should be used to retrieve their basic info. :rtype: list :return: a list of object previews """ @abc.abstractmethod def list(self) -> list: """ Get a list of objects. This method first gets the entity summaries, then gets the complete description for each entity with an additional GET call, so may be slow. :rtype: list :return: a list of objects """ def _select_id(self, id_: Optional[str] = None, name: Optional[str] = None) -> str: """ Return the id that corresponds to the given id or name info. """ if id_ is None and name is None: raise ValueError("Neither id nor name provided") if id_ is not None and name is not None: raise ValueError("Both id and name provided") if id_ is None: id_list = [_.id for _ in self.get_previews(name=name)] if len(id_list) > 1: raise ValueError(f"Ambiguous name '{name}'") if not id_list: raise ValueError(f"Name '{name}' not found") return id_list[0] else: return id_ def _get_dict(self, meth_name: str, reply: Optional[Union[Dict[str, Any], List[Dict[str, Any]]]]) -> Dict[str, Any]: if reply is None: raise RuntimeError(f"{meth_name}: no reply") elif isinstance(reply, dict): return reply try: return reply[0] except (TypeError, IndexError): raise RuntimeError(f"{meth_name}: unexpected reply: {reply!r}") class ObjDatasetContainerClient( ObjClient, Generic[wrappers.DatasetContainerSubtype, wrappers.DatasetContainerPreviewSubtype] ): CONTAINER_TYPE: Type[wrappers.DatasetContainer] CONTAINER_PREVIEW_TYPE: Type[wrappers.DatasetContainerPreview] def __init__(self, obj_gi: "GalaxyInstance") -> None: super().__init__(obj_gi=obj_gi) gi_client = getattr(self.gi, self.CONTAINER_TYPE.API_MODULE) get_fname = f"get_{self.CONTAINER_TYPE.API_MODULE}" self._get_f = getattr(gi_client, get_fname) show_fname = f"show_{self.CONTAINER_TYPE.__name__.lower()}" self._show_f = getattr(gi_client, show_fname) def get_previews( self, name: Optional[str] = None, deleted: bool = False, **kwargs: Any ) -> List[wrappers.DatasetContainerPreviewSubtype]: dicts = self._get_f(name=name, deleted=deleted, **kwargs) return [ cast(wrappers.DatasetContainerPreviewSubtype, self.CONTAINER_PREVIEW_TYPE(_, gi=self.obj_gi)) for _ in dicts ] def get(self, id_: str) -> wrappers.DatasetContainerSubtype: """ Retrieve the dataset container corresponding to the given id. """ cdict = self._show_f(id_) c_infos = self._show_f(id_, contents=True) if not isinstance(c_infos, Sequence): raise RuntimeError(f"{self._show_f.__name__}: unexpected reply: {c_infos!r}") c_infos = [self.CONTAINER_TYPE.CONTENT_INFO_TYPE(_) for _ in c_infos] return cast(wrappers.DatasetContainerSubtype, self.CONTAINER_TYPE(cdict, content_infos=c_infos, gi=self.obj_gi)) class ObjLibraryClient(ObjDatasetContainerClient[wrappers.Library, wrappers.LibraryPreview]): """ Interacts with Galaxy libraries. """ CONTAINER_TYPE = wrappers.Library CONTAINER_PREVIEW_TYPE = wrappers.LibraryPreview def create(self, name: str, description: Optional[str] = None, synopsis: Optional[str] = None) -> wrappers.Library: """ Create a data library with the properties defined in the arguments. :rtype: :class:`~.wrappers.Library` :return: the library just created """ res = self.gi.libraries.create_library(name, description, synopsis) lib_info = self._get_dict("create_library", res) return self.get(lib_info["id"]) def list(self, name: Optional[str] = None, deleted: bool = False) -> List[wrappers.Library]: """ Get libraries owned by the user of this Galaxy instance. :type name: str :param name: return only libraries with this name :type deleted: bool :param deleted: if ``True``, return libraries that have been deleted :rtype: list of :class:`~.wrappers.Library` """ dicts = self.gi.libraries.get_libraries(name=name, deleted=deleted) if not deleted: # return Library objects only for not-deleted libraries since Galaxy # does not filter them out and Galaxy release_14.08 and earlier # crashes when trying to get a deleted library return [self.get(_["id"]) for _ in dicts if not _["deleted"]] else: return [self.get(_["id"]) for _ in dicts] def delete(self, id_: Optional[str] = None, name: Optional[str] = None) -> None: """ Delete the library with the given id or name. Fails if multiple libraries have the specified name. .. warning:: Deleting a data library is irreversible - all of the data from the library will be permanently deleted. """ id_ = self._select_id(id_=id_, name=name) res = self.gi.libraries.delete_library(id_) if not isinstance(res, dict): raise RuntimeError(f"delete_library: unexpected reply: {res!r}") class ObjHistoryClient(ObjDatasetContainerClient[wrappers.History, wrappers.HistoryPreview]): """ Interacts with Galaxy histories. """ CONTAINER_TYPE = wrappers.History CONTAINER_PREVIEW_TYPE = wrappers.HistoryPreview def create(self, name: Optional[str] = None) -> wrappers.History: """ Create a new Galaxy history, optionally setting its name. :rtype: :class:`~.wrappers.History` :return: the history just created """ res = self.gi.histories.create_history(name=name) hist_info = self._get_dict("create_history", res) return self.get(hist_info["id"]) def list(self, name: Optional[str] = None, deleted: bool = False) -> List[wrappers.History]: """ Get histories owned by the user of this Galaxy instance. :type name: str :param name: return only histories with this name :type deleted: bool :param deleted: if ``True``, return histories that have been deleted :rtype: list of :class:`~.wrappers.History` """ dicts = self.gi.histories.get_histories(name=name, deleted=deleted) return [self.get(_["id"]) for _ in dicts] def delete(self, id_: Optional[str] = None, name: Optional[str] = None, purge: bool = False) -> None: """ Delete the history with the given id or name. Fails if multiple histories have the same name. :type purge: bool :param purge: if ``True``, also purge (permanently delete) the history .. note:: The ``purge`` option works only if the Galaxy instance has the ``allow_user_dataset_purge`` option set to ``true`` in the ``config/galaxy.yml`` configuration file. """ id_ = self._select_id(id_=id_, name=name) res = self.gi.histories.delete_history(id_, purge=purge) if not isinstance(res, dict): raise RuntimeError(f"delete_history: unexpected reply: {res!r}") class ObjWorkflowClient(ObjClient): """ Interacts with Galaxy workflows. """ def import_new(self, src: Union[str, Dict[str, Any]], publish: bool = False) -> wrappers.Workflow: """ Imports a new workflow into Galaxy. :type src: dict or str :param src: deserialized (dictionary) or serialized (str) JSON dump of the workflow (this is normally obtained by exporting a workflow from Galaxy). :type publish: bool :param publish: if ``True`` the uploaded workflow will be published; otherwise it will be visible only by the user which uploads it (default). :rtype: :class:`~.wrappers.Workflow` :return: the workflow just imported """ if isinstance(src, dict): wf_dict = src else: try: wf_dict = json.loads(src) except (TypeError, ValueError): raise ValueError(f"src not supported: {src!r}") wf_info = self.gi.workflows.import_workflow_dict(wf_dict, publish) return self.get(wf_info["id"]) def import_shared(self, id_: str) -> wrappers.Workflow: """ Imports a shared workflow to the user's space. :type id_: str :param id_: workflow id :rtype: :class:`~.wrappers.Workflow` :return: the workflow just imported """ wf_info = self.gi.workflows.import_shared_workflow(id_) return self.get(wf_info["id"]) def get(self, id_: str) -> wrappers.Workflow: """ Retrieve the workflow corresponding to the given id. :rtype: :class:`~.wrappers.Workflow` :return: the workflow corresponding to ``id_`` """ res = self.gi.workflows.show_workflow(id_) wf_dict = self._get_dict("show_workflow", res) return wrappers.Workflow(wf_dict, gi=self.obj_gi) # the 'deleted' option is not available for workflows def get_previews( self, name: Optional[str] = None, published: bool = False, **kwargs: Any ) -> List[wrappers.WorkflowPreview]: dicts = self.gi.workflows.get_workflows(name=name, published=published, **kwargs) return [wrappers.WorkflowPreview(_, gi=self.obj_gi) for _ in dicts] # the 'deleted' option is not available for workflows def list(self, name: Optional[str] = None, published: bool = False) -> List[wrappers.Workflow]: """ Get workflows owned by the user of this Galaxy instance. :type name: str :param name: return only workflows with this name :type published: bool :param published: if ``True``, return also published workflows :rtype: list of :class:`~.wrappers.Workflow` """ dicts = self.gi.workflows.get_workflows(name=name, published=published) return [self.get(_["id"]) for _ in dicts] def delete(self, id_: Optional[str] = None, name: Optional[str] = None) -> None: """ Delete the workflow with the given id or name. Fails if multiple workflows have the specified name. .. warning:: Deleting a workflow is irreversible - all of the data from the workflow will be permanently deleted. """ id_ = self._select_id(id_=id_, name=name) self.gi.workflows.delete_workflow(id_) class ObjInvocationClient(ObjClient): """ Interacts with Galaxy Invocations. """ def get(self, id_: str) -> wrappers.Invocation: """ Get an invocation by ID. :rtype: Invocation :param: invocation object """ inv_dict = self.gi.invocations.show_invocation(id_) return wrappers.Invocation(inv_dict, self.obj_gi) def get_previews(self, **kwargs: Any) -> List[wrappers.InvocationPreview]: """ Get previews of all invocations. :rtype: list of InvocationPreview :param: previews of invocations """ inv_list = self.gi.invocations.get_invocations(**kwargs) return [wrappers.InvocationPreview(inv_dict, gi=self.obj_gi) for inv_dict in inv_list] def list( self, workflow: Optional[wrappers.Workflow] = None, history: Optional[wrappers.History] = None, include_terminal: bool = True, limit: Optional[int] = None, ) -> List[wrappers.Invocation]: """ Get full listing of workflow invocations, or select a subset by specifying optional arguments for filtering (e.g. a workflow). :type workflow: wrappers.Workflow :param workflow: Include only invocations associated with this workflow :type history: wrappers.History :param history: Include only invocations associated with this history :param include_terminal: bool :param: Whether to include invocations in terminal states :type limit: int :param limit: Maximum number of invocations to return - if specified, the most recent invocations will be returned. :rtype: list of Invocation :param: invocation objects """ inv_dict_list = self.gi.invocations.get_invocations( workflow_id=workflow.id if workflow else None, history_id=history.id if history else None, include_terminal=include_terminal, limit=limit, view="element", step_details=True, ) return [wrappers.Invocation(inv_dict, self.obj_gi) for inv_dict in inv_dict_list] class ObjToolClient(ObjClient): """ Interacts with Galaxy tools. """ def get(self, id_: str, io_details: bool = False, link_details: bool = False) -> wrappers.Tool: """ Retrieve the tool corresponding to the given id. :type io_details: bool :param io_details: if True, get also input and output details :type link_details: bool :param link_details: if True, get also link details :rtype: :class:`~.wrappers.Tool` :return: the tool corresponding to ``id_`` """ res = self.gi.tools.show_tool(id_, io_details=io_details, link_details=link_details) tool_dict = self._get_dict("show_tool", res) return wrappers.Tool(tool_dict, gi=self.obj_gi) def get_previews(self, name: Optional[str] = None, trackster: bool = False, **kwargs: Any) -> List[wrappers.Tool]: """ Get the list of tools installed on the Galaxy instance. :type name: str :param name: return only tools with this name :type trackster: bool :param trackster: if True, only tools that are compatible with Trackster are returned :rtype: list of :class:`~.wrappers.Tool` """ dicts = self.gi.tools.get_tools(name=name, trackster=trackster, **kwargs) return [wrappers.Tool(_, gi=self.obj_gi) for _ in dicts] # the 'deleted' option is not available for tools def list(self, name: Optional[str] = None, trackster: bool = False) -> List[wrappers.Tool]: """ Get the list of tools installed on the Galaxy instance. :type name: str :param name: return only tools with this name :type trackster: bool :param trackster: if True, only tools that are compatible with Trackster are returned :rtype: list of :class:`~.wrappers.Tool` """ # dicts = self.gi.tools.get_tools(name=name, trackster=trackster) # return [self.get(_['id']) for _ in dicts] # As of 2015/04/15, GET /api/tools returns also data manager tools for # non-admin users, see # https://trello.com/c/jyl0cvFP/2633-api-tool-list-filtering-doesn-t-filter-data-managers-for-non-admins # Trying to get() a data manager tool would then return a 404 Not Found # error. # Moreover, the dicts returned by gi.tools.get_tools() are richer than # those returned by get(), so make this an alias for get_previews(). return self.get_previews(name, trackster) class ObjJobClient(ObjClient): """ Interacts with Galaxy jobs. """ def get(self, id_: str, full_details: bool = False) -> wrappers.Job: """ Retrieve the job corresponding to the given id. :type full_details: bool :param full_details: if ``True``, return the complete list of details for the given job. :rtype: :class:`~.wrappers.Job` :return: the job corresponding to ``id_`` """ res = self.gi.jobs.show_job(id_, full_details) job_dict = self._get_dict("show_job", res) return wrappers.Job(job_dict, gi=self.obj_gi) def get_previews(self, **kwargs: Any) -> List[wrappers.JobPreview]: dicts = self.gi.jobs.get_jobs(**kwargs) return [wrappers.JobPreview(_, gi=self.obj_gi) for _ in dicts] def list(self) -> List[wrappers.Job]: """ Get the list of jobs of the current user. :rtype: list of :class:`~.wrappers.Job` """ dicts = self.gi.jobs.get_jobs() return [self.get(_["id"]) for _ in dicts] class ObjDatasetClient(ObjClient): """ Interacts with Galaxy datasets. """ @overload def get(self, id_: str, hda_ldda: Literal["hda"] = "hda") -> wrappers.HistoryDatasetAssociation: ... @overload def get(self, id_: str, hda_ldda: Literal["ldda"]) -> wrappers.LibraryDatasetDatasetAssociation: ... def get(self, id_: str, hda_ldda: HdaLdda = "hda") -> wrappers.Dataset: """ Retrieve the dataset corresponding to the given id. :type hda_ldda: str :param hda_ldda: Whether to show a history dataset ('hda' - the default) or library dataset ('ldda') :rtype: :class:`~.wrappers.HistoryDatasetAssociation` or :class:`~.wrappers.LibraryDatasetDatasetAssociation` :return: the history or library dataset corresponding to ``id_`` """ res = self.gi.datasets.show_dataset(id_, hda_ldda=hda_ldda) ds_dict = self._get_dict("show_dataset", res) if hda_ldda == "hda": hist = self.obj_gi.histories.get(ds_dict["history_id"]) return wrappers.HistoryDatasetAssociation(ds_dict, hist, gi=self.obj_gi) elif hda_ldda == "ldda": lib = self.obj_gi.libraries.get(ds_dict["parent_library_id"]) return wrappers.LibraryDatasetDatasetAssociation(ds_dict, lib, gi=self.obj_gi) else: raise ValueError(f"Unsupported value for hda_ldda: {hda_ldda}") def get_previews(self, **kwargs: Any) -> list: raise NotImplementedError() def list(self) -> list: raise NotImplementedError() class ObjDatasetCollectionClient(ObjClient): """ Interacts with Galaxy dataset collections. """ def get(self, id_: str) -> wrappers.HistoryDatasetCollectionAssociation: """ Retrieve the dataset collection corresponding to the given id. :rtype: :class:`~.wrappers.HistoryDatasetCollectionAssociation` :return: the history dataset collection corresponding to ``id_`` """ res = self.gi.dataset_collections.show_dataset_collection(id_) ds_dict = self._get_dict("show_dataset_collection", res) hist = self.obj_gi.histories.get(ds_dict["history_id"]) return wrappers.HistoryDatasetCollectionAssociation(ds_dict, hist, gi=self.obj_gi) def get_previews(self, **kwargs: Any) -> list: raise NotImplementedError() def list(self) -> list: raise NotImplementedError() bioblend-1.2.0/bioblend/galaxy/objects/galaxy_instance.py000066400000000000000000000071411444761704300235240ustar00rootroot00000000000000""" A representation of a Galaxy instance based on oo wrappers. """ import time from typing import ( Iterable, List, Optional, ) import bioblend import bioblend.galaxy from bioblend.galaxy.datasets import TERMINAL_STATES from . import ( client, wrappers, ) def _get_error_info(dataset: wrappers.Dataset) -> str: msg = dataset.id try: msg += f" ({dataset.name}): {dataset.misc_info}" except Exception: # avoid 'error while generating an error report' msg += ": error" return msg class GalaxyInstance: """ A representation of an instance of Galaxy, identified by a URL and a user's API key. :type url: str :param url: a FQDN or IP for a given instance of Galaxy. For example: ``http://127.0.0.1:8080`` :type api_key: str :param api_key: user's API key for the given instance of Galaxy, obtained from the Galaxy web UI. This is actually a factory class which instantiates the entity-specific clients. Example: get a list of all histories for a user with API key 'foo':: from bioblend.galaxy.objects import * gi = GalaxyInstance('http://127.0.0.1:8080', 'foo') histories = gi.histories.list() """ def __init__( self, url: str, api_key: Optional[str] = None, email: Optional[str] = None, password: Optional[str] = None, verify: bool = True, ) -> None: self.gi = bioblend.galaxy.GalaxyInstance(url, api_key, email, password, verify) self.log = bioblend.log self.datasets = client.ObjDatasetClient(self) self.dataset_collections = client.ObjDatasetCollectionClient(self) self.histories = client.ObjHistoryClient(self) self.libraries = client.ObjLibraryClient(self) self.workflows = client.ObjWorkflowClient(self) self.invocations = client.ObjInvocationClient(self) self.tools = client.ObjToolClient(self) self.jobs = client.ObjJobClient(self) def _wait_datasets( self, datasets: Iterable[wrappers.Dataset], polling_interval: float, break_on_error: bool = True ) -> None: """ Wait for datasets to come out of the pending states. :type datasets: :class:`~collections.Iterable` of :class:`~.wrappers.Dataset` :param datasets: datasets :type polling_interval: float :param polling_interval: polling interval in seconds :type break_on_error: bool :param break_on_error: if ``True``, raise a RuntimeError exception as soon as at least one of the datasets is in the 'error' state. .. warning:: This is a blocking operation that can take a very long time. Also, note that this method does not return anything; however, each input dataset is refreshed (possibly multiple times) during the execution. """ def poll(ds_list: Iterable[wrappers.Dataset]) -> List[wrappers.Dataset]: pending = [] for ds in ds_list: ds.refresh() if break_on_error and ds.state == "error": raise RuntimeError(_get_error_info(ds)) if not ds.state: self.log.warning("Dataset %s has an empty state", ds.id) elif ds.state not in TERMINAL_STATES: self.log.info(f"Dataset {ds.id} is in non-terminal state {ds.state}") pending.append(ds) return pending self.log.info("Waiting for datasets") while datasets: datasets = poll(datasets) time.sleep(polling_interval) bioblend-1.2.0/bioblend/galaxy/objects/wrappers.py000066400000000000000000001737611444761704300222320ustar00rootroot00000000000000# pylint: disable=W0622,E1101 """ A basic object-oriented interface for Galaxy entities. """ import abc import json from collections.abc import ( Mapping, Sequence, ) from typing import ( Any, Callable, cast, ClassVar, Dict, Generic, IO, Iterable, Iterator, List, Optional, Set, Tuple, Type, TYPE_CHECKING, TypeVar, Union, ) from typing_extensions import Literal import bioblend from bioblend.galaxy.workflows import InputsBy from bioblend.util import abstractclass if TYPE_CHECKING: from . import client from .galaxy_instance import GalaxyInstance __all__ = ( "Wrapper", "Step", "Workflow", "LibraryContentInfo", "HistoryContentInfo", "DatasetContainer", "History", "Library", "Folder", "Dataset", "HistoryDatasetAssociation", "DatasetCollection", "HistoryDatasetCollectionAssociation", "LibraryDatasetDatasetAssociation", "LibraryDataset", "Tool", "Job", "LibraryPreview", "HistoryPreview", "WorkflowPreview", ) WrapperSubtype = TypeVar("WrapperSubtype", bound="Wrapper") @abstractclass class Wrapper: """ Abstract base class for Galaxy entity wrappers. Wrapper instances wrap deserialized JSON dictionaries such as the ones obtained by the Galaxy web API, converting key-based access to attribute-based access (e.g., ``library['name'] -> library.name``). Dict keys that are converted to attributes are listed in the ``BASE_ATTRS`` class variable: this is the 'stable' interface. Note that the wrapped dictionary is accessible via the ``wrapped`` attribute. """ BASE_ATTRS: Tuple[str, ...] = ("id",) gi: Optional["GalaxyInstance"] id: str is_modified: bool wrapped: dict _cached_parent: Optional["Wrapper"] def __init__( self, wrapped: Dict[str, Any], parent: Optional["Wrapper"] = None, gi: Optional["GalaxyInstance"] = None ) -> None: """ :type wrapped: dict :param wrapped: JSON-serializable dictionary :type parent: :class:`Wrapper` :param parent: the parent of this wrapper :type gi: :class:`GalaxyInstance` :param gi: the GalaxyInstance through which we can access this wrapper """ if not isinstance(wrapped, Mapping): raise TypeError("wrapped object must be a mapping type") # loads(dumps(x)) is a bit faster than deepcopy and allows type checks try: dumped = json.dumps(wrapped) except (TypeError, ValueError): raise ValueError("wrapped object must be JSON-serializable") object.__setattr__(self, "wrapped", json.loads(dumped)) for k in self.BASE_ATTRS: object.__setattr__(self, k, self.wrapped.get(k)) object.__setattr__(self, "_cached_parent", parent) object.__setattr__(self, "is_modified", False) object.__setattr__(self, "gi", gi) @property def parent(self) -> Optional["Wrapper"]: """ The parent of this wrapper. """ return self._cached_parent @property def is_mapped(self) -> bool: """ ``True`` if this wrapper is mapped to an actual Galaxy entity. """ return self.id is not None def unmap(self) -> None: """ Disconnect this wrapper from Galaxy. """ object.__setattr__(self, "id", None) def clone(self: WrapperSubtype) -> WrapperSubtype: """ Return an independent copy of this wrapper. """ return self.__class__(self.wrapped) def touch(self) -> None: """ Mark this wrapper as having been modified since its creation. """ object.__setattr__(self, "is_modified", True) if self.parent: self.parent.touch() def to_json(self) -> str: """ Return a JSON dump of this wrapper. """ return json.dumps(self.wrapped) @classmethod def from_json(cls: Type[WrapperSubtype], jdef: str) -> WrapperSubtype: """ Build a new wrapper from a JSON dump. """ return cls(json.loads(jdef)) # FIXME: things like self.x[0] = 'y' do NOT call self.__setattr__ def __setattr__(self, name: str, value: str) -> None: if name not in self.wrapped: raise AttributeError("can't set attribute") self.wrapped[name] = value object.__setattr__(self, name, value) self.touch() def __repr__(self) -> str: return f"{self.__class__.__name__}({self.wrapped!r})" class Step(Wrapper): """ Workflow step. Steps are the main building blocks of a Galaxy workflow. A step can be: an input (type ``data_collection_input``, ``data_input`` or ``parameter_input``), a computational tool (type ``tool``), a subworkflow (type ``subworkflow``) or a pause (type ``pause``). """ BASE_ATTRS = Wrapper.BASE_ATTRS + ( "input_steps", "name", "tool_id", "tool_inputs", "tool_version", "type", ) input_steps: Dict[str, Dict] type: str tool_id: Optional[str] tool_inputs: Dict tool_version: Optional[str] def __init__(self, step_dict: Dict[str, Any], parent: Wrapper) -> None: super().__init__(step_dict, parent=parent, gi=parent.gi) try: stype = step_dict["type"] except KeyError: raise ValueError("not a step dict") if stype not in {"data_collection_input", "data_input", "parameter_input", "pause", "subworkflow", "tool"}: raise ValueError(f"Unknown step type: {stype!r}") class InvocationStep(Wrapper): """ Invocation step. """ BASE_ATTRS = Wrapper.BASE_ATTRS + ( "action", "job_id", "order_index", "state", "update_time", "workflow_step_id", "workflow_step_label", "workflow_step_uuid", ) action: Optional[object] gi: "GalaxyInstance" job_id: str order_index: int state: str update_time: str workflow_step_id: str workflow_step_label: str workflow_step_uuid: str @property def parent(self) -> Wrapper: ret = super().parent assert ret is not None return ret def __init__(self, wrapped: Dict[str, Any], parent: Wrapper, gi: "GalaxyInstance") -> None: super().__init__(wrapped, parent, gi) def refresh(self) -> "InvocationStep": """ Re-fetch the attributes pertaining to this object. :return: self """ step_dict = self.gi.gi.invocations.show_invocation_step(self.parent.id, self.id) self.__init__(step_dict, parent=self.parent, gi=self.gi) # type: ignore[misc] return self def get_outputs(self) -> Dict[str, "HistoryDatasetAssociation"]: """ Get the output datasets of the invocation step :rtype: dict of `HistoryDatasetAssociation` :return: dictionary mapping output names to history datasets """ if not hasattr(self, "outputs"): self.refresh() return {name: self.gi.datasets.get(out_dict["id"]) for name, out_dict in self.wrapped["outputs"].items()} def get_output_collections(self) -> Dict[str, "HistoryDatasetCollectionAssociation"]: """ Get the output dataset collections of the invocation step :rtype: dict of `HistoryDatasetCollectionAssociation` :return: dictionary mapping output names to history dataset collections """ if not hasattr(self, "output_collections"): self.refresh() return { name: self.gi.dataset_collections.get(out_coll_dict["id"]) for name, out_coll_dict in self.wrapped["output_collections"].items() } class Workflow(Wrapper): """ Workflows represent ordered sequences of computations on Galaxy. A workflow defines a sequence of steps that produce one or more results from an input dataset. """ BASE_ATTRS = Wrapper.BASE_ATTRS + ( "deleted", "inputs", "latest_workflow_uuid", "name", "owner", "published", "steps", "tags", ) dag: Dict[str, Set[str]] deleted: bool input_labels_to_ids: Dict[str, Set[str]] inputs: Dict[str, Dict] inv_dag: Dict[str, Set[str]] missing_ids: List name: str owner: str POLLING_INTERVAL = 10 # for output state monitoring published: bool sink_ids: Set[str] source_ids: Set[str] steps: Dict[str, Step] tags: List[str] tool_labels_to_ids: Dict[str, Set[str]] def __init__(self, wf_dict: Dict[str, Any], gi: Optional["GalaxyInstance"] = None) -> None: super().__init__(wf_dict, gi=gi) if gi: tools_list_by_id = [t.id for t in gi.tools.get_previews()] else: tools_list_by_id = [] missing_ids = [] tool_labels_to_ids: Dict[str, Set[str]] = {} for k, v in self.steps.items(): step_dict = cast(Dict[str, Any], v) # convert step ids to str for consistency with outer keys step_dict["id"] = str(step_dict["id"]) for i in step_dict["input_steps"].values(): i["source_step"] = str(i["source_step"]) step = Step(step_dict, self) self.steps[k] = step if step.type == "tool": if not step.tool_inputs or step.tool_id not in tools_list_by_id: missing_ids.append(k) assert step.tool_id tool_labels_to_ids.setdefault(step.tool_id, set()).add(step.id) input_labels_to_ids: Dict[str, Set[str]] = {} for id_, d in self.inputs.items(): input_labels_to_ids.setdefault(d["label"], set()).add(id_) object.__setattr__(self, "input_labels_to_ids", input_labels_to_ids) object.__setattr__(self, "tool_labels_to_ids", tool_labels_to_ids) dag, inv_dag = self._get_dag() heads, tails = set(dag), set(inv_dag) object.__setattr__(self, "dag", dag) object.__setattr__(self, "inv_dag", inv_dag) object.__setattr__(self, "source_ids", heads - tails) assert ( set(self.inputs) == self.data_collection_input_ids | self.data_input_ids | self.parameter_input_ids ), f"inputs is {self.inputs!r}, while data_collection_input_ids is {self.data_collection_input_ids!r}, data_input_ids is {self.data_input_ids!r} and parameter_input_ids is {self.parameter_input_ids!r}" object.__setattr__(self, "sink_ids", tails - heads) object.__setattr__(self, "missing_ids", missing_ids) def _get_dag(self) -> Tuple[Dict[str, Set[str]], Dict[str, Set[str]]]: """ Return the workflow's DAG. For convenience, this method computes a 'direct' (step => successors) and an 'inverse' (step => predecessors) representation of the same DAG. For instance, a workflow with a single tool *c*, two inputs *a, b* and three outputs *d, e, f* is represented by (direct):: {'a': {'c'}, 'b': {'c'}, 'c': {'d', 'e', 'f'}} and by (inverse):: {'c': {'a', 'b'}, 'd': {'c'}, 'e': {'c'}, 'f': {'c'}} """ dag: Dict[str, Set[str]] = {} inv_dag: Dict[str, Set[str]] = {} for s in self.steps.values(): assert isinstance(s, Step) for i in s.input_steps.values(): head, tail = i["source_step"], s.id dag.setdefault(head, set()).add(tail) inv_dag.setdefault(tail, set()).add(head) return dag, inv_dag def sorted_step_ids(self) -> List[str]: """ Return a topological sort of the workflow's DAG. """ ids: List[str] = [] source_ids = self.source_ids.copy() inv_dag = {k: v.copy() for k, v in self.inv_dag.items()} while source_ids: head = source_ids.pop() ids.append(head) for tail in self.dag.get(head, set()): incoming = inv_dag[tail] incoming.remove(head) if not incoming: source_ids.add(tail) return ids @property def data_input_ids(self) -> Set[str]: """ Return the ids of data input steps for this workflow. """ return {id_ for id_, s in self.steps.items() if s.type == "data_input"} @property def data_collection_input_ids(self) -> Set[str]: """ Return the ids of data collection input steps for this workflow. """ return {id_ for id_, s in self.steps.items() if s.type == "data_collection_input"} @property def parameter_input_ids(self) -> Set[str]: """ Return the ids of parameter input steps for this workflow. """ return {id_ for id_, s in self.steps.items() if s.type == "parameter_input"} @property def tool_ids(self) -> Set[str]: """ Return the ids of tool steps for this workflow. """ return {id_ for id_, s in self.steps.items() if s.type == "tool"} @property def input_labels(self) -> Set[str]: """ Return the labels of this workflow's input steps. """ return set(self.input_labels_to_ids) @property def is_runnable(self) -> bool: """ Return True if the workflow can be run on Galaxy. A workflow is considered runnable on a Galaxy instance if all of the tools it uses are installed in that instance. """ return not self.missing_ids @staticmethod def _convert_input_map(input_map: Dict[str, Any]) -> Dict[str, Any]: """ Convert ``input_map`` to the format required by the Galaxy web API. :type input_map: dict :param input_map: a mapping to datasets or dataset collections :rtype: dict :return: a mapping in the format required by the Galaxy web API. """ ret = {} for key, value in input_map.items(): if isinstance( value, ( HistoryDatasetAssociation, HistoryDatasetCollectionAssociation, LibraryDatasetDatasetAssociation, LibraryDataset, ), ): ret[key] = {"id": value.id, "src": value.SRC} else: ret[key] = value return ret # I think we should deprecate this method - NS def preview(self) -> "WorkflowPreview": assert self.gi is not None try: return [_ for _ in self.gi.workflows.get_previews(published=True) if _.id == self.id][0] except IndexError: raise ValueError(f"no object for id {self.id}") def export(self) -> Dict[str, Any]: """ Export a re-importable representation of the workflow. :rtype: dict :return: a JSON-serializable dump of the workflow """ assert self.gi is not None return self.gi.gi.workflows.export_workflow_dict(self.id) def delete(self) -> None: """ Delete this workflow. .. warning:: Deleting a workflow is irreversible - all of the data from the workflow will be permanently deleted. """ assert self.gi is not None self.gi.workflows.delete(id_=self.id) self.unmap() def invoke( self, inputs: Optional[Dict[str, Any]] = None, params: Optional[Dict[str, Any]] = None, history: Optional[Union[str, "History"]] = None, import_inputs_to_history: bool = False, replacement_params: Optional[Dict[str, Any]] = None, allow_tool_state_corrections: bool = True, inputs_by: Optional[InputsBy] = None, parameters_normalized: bool = False, ) -> "Invocation": """ Invoke the workflow. This will cause a workflow to be scheduled and return an object describing the workflow invocation. :type inputs: dict :param inputs: A mapping of workflow inputs to datasets and dataset collections. The datasets source can be a LibraryDatasetDatasetAssociation (``ldda``), LibraryDataset (``ld``), HistoryDatasetAssociation (``hda``), or HistoryDatasetCollectionAssociation (``hdca``). The map must be in the following format: ``{'': dataset or collection}`` (e.g. ``{'2': HistoryDatasetAssociation()}``) This map may also be indexed by the UUIDs of the workflow steps, as indicated by the ``uuid`` property of steps returned from the Galaxy API. Alternatively workflow steps may be addressed by the label that can be set in the workflow editor. If using uuid or label you need to also set the ``inputs_by`` parameter to ``step_uuid`` or ``name``. :type params: dict :param params: A mapping of non-datasets tool parameters (see below) :type history: History or str :param history: The history in which to store the workflow output, or the name of a new history to create. If ``None``, a new 'Unnamed history' is created. :type import_inputs_to_history: bool :param import_inputs_to_history: If ``True``, used workflow inputs will be imported into the history. If ``False``, only workflow outputs will be visible in the given history. :type allow_tool_state_corrections: bool :param allow_tool_state_corrections: If True, allow Galaxy to fill in missing tool state when running workflows. This may be useful for workflows using tools that have changed over time or for workflows built outside of Galaxy with only a subset of inputs defined. :type replacement_params: dict :param replacement_params: pattern-based replacements for post-job actions (see below) :type inputs_by: str :param inputs_by: Determines how inputs are referenced. Can be "step_index|step_uuid" (default), "step_index", "step_id", "step_uuid", or "name". :type parameters_normalized: bool :param parameters_normalized: Whether Galaxy should normalize ``params`` to ensure everything is referenced by a numeric step ID. Default is ``False``, but when setting ``params`` for a subworkflow, ``True`` is required. :rtype: Invocation :return: the workflow invocation The ``params`` dict should be specified as follows:: {STEP_ID: PARAM_DICT, ...} where PARAM_DICT is:: {PARAM_NAME: VALUE, ...} For backwards compatibility, the following (deprecated) format is also supported for ``params``:: {TOOL_ID: PARAM_DICT, ...} in which case PARAM_DICT affects all steps with the given tool id. If both by-tool-id and by-step-id specifications are used, the latter takes precedence. Finally (again, for backwards compatibility), PARAM_DICT can also be specified as:: {'param': PARAM_NAME, 'value': VALUE} Note that this format allows only one parameter to be set per step. For a ``repeat`` parameter, the names of the contained parameters needs to be specified as ``_|``, with the repeat index starting at 0. For example, if the tool XML contains:: then the PARAM_DICT should be something like:: {... "cutoff_0|name": "n_genes", "cutoff_0|min": "2", "cutoff_1|name": "n_counts", "cutoff_1|min": "4", ...} At the time of this writing, it is not possible to change the number of times the contained parameters are repeated. Therefore, the parameter indexes can go from 0 to n-1, where n is the number of times the repeated element was added when the workflow was saved in the Galaxy UI. The ``replacement_params`` dict should map parameter names in post-job actions (PJAs) to their runtime values. For instance, if the final step has a PJA like the following:: {'RenameDatasetActionout_file1': {'action_arguments': {'newname': '${output}'}, 'action_type': 'RenameDatasetAction', 'output_name': 'out_file1'}} then the following renames the output dataset to 'foo':: replacement_params = {'output': 'foo'} see also `this email thread `_. .. warning:: Historically, workflow invocation consumed a ``dataset_map`` data structure that was indexed by unencoded workflow step IDs. These IDs would not be stable across Galaxy instances. The new ``inputs`` property is instead indexed by either the ``order_index`` property (which is stable across workflow imports) or the step UUID which is also stable. """ assert self.gi is not None if not self.is_mapped: raise RuntimeError("workflow is not mapped to a Galaxy object") if not self.is_runnable: missing_tools_str = ", ".join(f"{self.steps[step_id].tool_id}[{step_id}]" for step_id in self.missing_ids) raise RuntimeError(f"workflow has missing tools: {missing_tools_str}") inv_dict = self.gi.gi.workflows.invoke_workflow( workflow_id=self.id, inputs=self._convert_input_map(inputs or {}), params=params, history_id=history.id if isinstance(history, History) else None, history_name=history if isinstance(history, str) else None, import_inputs_to_history=import_inputs_to_history, replacement_params=replacement_params, allow_tool_state_corrections=allow_tool_state_corrections, inputs_by=inputs_by, parameters_normalized=parameters_normalized, ) return self.gi.invocations.get(inv_dict["id"]) class Invocation(Wrapper): """ Invocation of a workflow. This causes the steps of a workflow to be executed in sequential order. """ BASE_ATTRS = Wrapper.BASE_ATTRS + ( "history_id", "state", "update_time", "uuid", "workflow_id", ) gi: "GalaxyInstance" history_id: str state: str steps: List[InvocationStep] update_time: str uuid: str workflow_id: str def __init__(self, inv_dict: Dict[str, Any], gi: "GalaxyInstance") -> None: super().__init__(inv_dict, gi=gi) self.steps = [InvocationStep(step, parent=self, gi=gi) for step in inv_dict["steps"]] self.inputs = [{**v, "label": k} for k, v in inv_dict["inputs"].items()] def sorted_step_ids(self) -> List[str]: """ Get the step IDs sorted based on this order index. :rtype: list of str :param: sorted step IDs """ return [step.id for step in sorted(self.steps, key=lambda step: step.order_index)] def step_states(self) -> Set[str]: """ Get the set of step states for this invocation. :rtype: set :param: step states """ return {step.state for step in self.steps} def number_of_steps(self) -> int: """ Get the number of steps for this invocation. :rtype: int :param: number of steps """ return len(self.steps) def sorted_steps_by( self, indices: Optional[Iterable[int]] = None, states: Optional[Iterable[Union[str, None]]] = None, step_ids: Optional[Iterable[str]] = None, ) -> List[InvocationStep]: """ Get steps for this invocation, or get a subset by specifying optional parameters for filtering. :type indices: list of int :param indices: return steps that have matching order_index :type states: list of str :param states: return steps that have matching states :type step_ids: list of str :param step_ids: return steps that have matching step_ids :rtype: list of InvocationStep :param: invocation steps """ steps: Union[List[InvocationStep], filter] = self.steps if indices is not None: steps = filter(lambda step: step.order_index in indices, steps) if states is not None: steps = filter(lambda step: step.state in states, steps) if step_ids is not None: steps = filter(lambda step: step.id in step_ids, steps) return sorted(steps, key=lambda step: step.order_index) def cancel(self) -> None: """ Cancel this invocation. .. note:: On success, this method updates the Invocation object's internal variables. """ inv_dict = self.gi.gi.invocations.cancel_invocation(self.id) self.__init__(inv_dict, gi=self.gi) # type: ignore[misc] def refresh(self) -> "Invocation": """ Re-fetch the attributes pertaining to this object. :return: self """ inv_dict = self.gi.gi.invocations.show_invocation(self.id) self.__init__(inv_dict, gi=self.gi) # type: ignore[misc] return self def run_step_actions(self, steps: List[InvocationStep], actions: List[object]) -> None: """ Run actions for active steps of this invocation. :type steps: list of InvocationStep :param steps: list of steps to run actions on :type actions: list of objects :param actions: list of actions to run .. note:: On success, this method updates the Invocation object's internal step variables. """ if not len(steps) == len(actions): raise RuntimeError( f"Different number of ``steps`` ({len(steps)}) and ``actions`` ({len(actions)}) in ``{self}.run_step_actions()``" ) step_dict_list = [ self.gi.gi.invocations.run_invocation_step_action(self.id, step.id, action) for step, action in zip(steps, actions) ] for step, step_dict in zip(steps, step_dict_list): step.__init__(step_dict, parent=self, gi=self.gi) # type: ignore[misc] def summary(self) -> Dict[str, Any]: """ Get a summary for this invocation. :rtype: dict :param: invocation summary """ return self.gi.gi.invocations.get_invocation_summary(self.id) def step_jobs_summary(self) -> List[Dict[str, Any]]: """ Get a summary for this invocation's step jobs. :rtype: list of dicts :param: step job summaries """ return self.gi.gi.invocations.get_invocation_step_jobs_summary(self.id) def report(self) -> Dict[str, Any]: """ Get a dictionary containing a Markdown report for this invocation. :rtype: dict :param: invocation report """ return self.gi.gi.invocations.get_invocation_report(self.id) def save_report_pdf(self, file_path: str, chunk_size: int = bioblend.CHUNK_SIZE) -> None: """ Download a PDF report for this invocation. :type file_path: str :param file_path: path to save the report :type chunk_size: int :param chunk_size: chunk size in bytes for reading remote data """ self.gi.gi.invocations.get_invocation_report_pdf(self.id, file_path, chunk_size) def biocompute_object(self) -> Dict[str, Any]: """ Get a BioCompute object for this invocation. :rtype: dict :param: BioCompute object """ return self.gi.gi.invocations.get_invocation_biocompute_object(self.id) def wait(self, maxwait: float = 12000, interval: float = 3, check: bool = True) -> None: """ Wait for this invocation to reach a terminal state. :type maxwait: float :param maxwait: upper limit on waiting time :type interval: float :param interval: polling interval in secconds :type check: bool :param check: if ``true``, raise an error if the terminal state is not 'scheduled' .. note:: On success, this method updates the Invocation object's internal variables. """ inv_dict = self.gi.gi.invocations.wait_for_invocation(self.id, maxwait=maxwait, interval=interval, check=check) self.__init__(inv_dict, gi=self.gi) # type: ignore[misc] DatasetSubtype = TypeVar("DatasetSubtype", bound="Dataset") class Dataset(Wrapper, metaclass=abc.ABCMeta): """ Abstract base class for Galaxy datasets. """ BASE_ATTRS = Wrapper.BASE_ATTRS + ( "data_type", "file_ext", "file_name", "file_size", "genome_build", "misc_info", "name", "state", ) container: "DatasetContainer" genome_build: str gi: "GalaxyInstance" misc_info: str name: str POLLING_INTERVAL = 1 # for state monitoring state: str def __init__(self, ds_dict: Dict[str, Any], container: "DatasetContainer", gi: "GalaxyInstance") -> None: super().__init__(ds_dict, gi=gi) object.__setattr__(self, "container", container) @property @abc.abstractmethod def _stream_url(self) -> str: """ Return the URL to stream this dataset. """ def get_stream(self, chunk_size: int = bioblend.CHUNK_SIZE) -> Iterator[bytes]: """ Open dataset for reading and return an iterator over its contents. :type chunk_size: int :param chunk_size: read this amount of bytes at a time """ kwargs: Dict[str, Any] = {"stream": True} if isinstance(self, LibraryDataset): kwargs["params"] = {"ld_ids%5B%5D": self.id} r = self.gi.gi.make_get_request(self._stream_url, **kwargs) if isinstance(self, LibraryDataset) and r.status_code == 500: # compatibility with older Galaxy releases kwargs["params"] = {"ldda_ids%5B%5D": self.id} r = self.gi.gi.make_get_request(self._stream_url, **kwargs) r.raise_for_status() return r.iter_content(chunk_size) # FIXME: client can't close r def peek(self, chunk_size: int = bioblend.CHUNK_SIZE) -> bytes: """ Open dataset for reading and return the first chunk. See :meth:`.get_stream` for param info. """ try: return next(self.get_stream(chunk_size=chunk_size)) except StopIteration: return b"" def download(self, file_object: IO[bytes], chunk_size: int = bioblend.CHUNK_SIZE) -> None: """ Open dataset for reading and save its contents to ``file_object``. :type file_object: file :param file_object: output file object See :meth:`.get_stream` for info on other params. """ for chunk in self.get_stream(chunk_size=chunk_size): file_object.write(chunk) def get_contents(self, chunk_size: int = bioblend.CHUNK_SIZE) -> bytes: """ Open dataset for reading and return its **full** contents. See :meth:`.get_stream` for param info. """ return b"".join(self.get_stream(chunk_size=chunk_size)) def refresh(self: DatasetSubtype) -> DatasetSubtype: """ Re-fetch the attributes pertaining to this object. :return: self """ gi_client = getattr(self.gi.gi, self.container.API_MODULE) ds_dict = gi_client.show_dataset(self.container.id, self.id) self.__init__(ds_dict, self.container, self.gi) # type: ignore[misc] return self def wait(self, polling_interval: float = POLLING_INTERVAL, break_on_error: bool = True) -> None: """ Wait for this dataset to come out of the pending states. :type polling_interval: float :param polling_interval: polling interval in seconds :type break_on_error: bool :param break_on_error: if ``True``, raise a RuntimeError exception if the dataset ends in the 'error' state. .. warning:: This is a blocking operation that can take a very long time. Also, note that this method does not return anything; however, this dataset is refreshed (possibly multiple times) during the execution. """ self.gi._wait_datasets([self], polling_interval=polling_interval, break_on_error=break_on_error) class HistoryDatasetAssociation(Dataset): """ Maps to a Galaxy ``HistoryDatasetAssociation``. """ BASE_ATTRS = Dataset.BASE_ATTRS + ("annotation", "deleted", "purged", "tags", "visible") SRC = "hda" annotation: str deleted: bool purged: bool @property def _stream_url(self) -> str: base_url = self.gi.gi.histories._make_url(module_id=self.container.id, contents=True) return f"{base_url}/{self.id}/display" def get_stream(self, chunk_size: int = bioblend.CHUNK_SIZE) -> Iterator[bytes]: """ Open dataset for reading and return an iterator over its contents. :type chunk_size: int :param chunk_size: read this amount of bytes at a time """ _, _, r = self.gi.gi.datasets._initiate_download( self.id, stream_content=True, ) return r.iter_content(chunk_size) # FIXME: client can't close r def update(self, **kwargs: Any) -> "HistoryDatasetAssociation": """ Update this history dataset metadata. Some of the attributes that can be modified are documented below. :type name: str :param name: Replace history dataset name with the given string :type genome_build: str :param genome_build: Replace history dataset genome build (dbkey) :type annotation: str :param annotation: Replace history dataset annotation with given string :type deleted: bool :param deleted: Mark or unmark history dataset as deleted :type visible: bool :param visible: Mark or unmark history dataset as visible """ res = self.gi.gi.histories.update_dataset(self.container.id, self.id, **kwargs) # Refresh also the history because the dataset may have been (un)deleted self.container.refresh() self.__init__(res, self.container, gi=self.gi) # type: ignore[misc] return self def delete(self, purge: bool = False) -> None: """ Delete this history dataset. :type purge: bool :param purge: if ``True``, also purge (permanently delete) the dataset .. note:: The ``purge`` option works only if the Galaxy instance has the ``allow_user_dataset_purge`` option set to ``true`` in the ``config/galaxy.yml`` configuration file. """ self.gi.gi.histories.delete_dataset(self.container.id, self.id, purge=purge) self.container.refresh() self.refresh() DatasetCollectionSubtype = TypeVar("DatasetCollectionSubtype", bound="DatasetCollection") class DatasetCollection(Wrapper, metaclass=abc.ABCMeta): """ Abstract base class for Galaxy dataset collections. """ BASE_ATTRS = Wrapper.BASE_ATTRS + ( "collection_type", "deleted", "name", "state", ) container: Union["DatasetCollection", "History"] API_MODULE = "dataset_collections" collection_type: str deleted: bool gi: "GalaxyInstance" def __init__( self, dsc_dict: Dict[str, Any], container: Union["DatasetCollection", "History"], gi: "GalaxyInstance" ) -> None: super().__init__(dsc_dict, gi=gi) object.__setattr__(self, "container", container) def refresh(self: DatasetCollectionSubtype) -> DatasetCollectionSubtype: """ Re-fetch the attributes pertaining to this object. :return: self """ gi_client = getattr(self.gi.gi, self.container.API_MODULE) dsc_dict = gi_client.show_dataset_collection(self.container.id, self.id) self.__init__(dsc_dict, self.container, self.gi) # type: ignore[misc] return self @abc.abstractmethod def delete(self) -> None: """ Delete this dataset collection. """ class HistoryDatasetCollectionAssociation(DatasetCollection): """ Maps to a Galaxy ``HistoryDatasetCollectionAssociation``. """ BASE_ATTRS = DatasetCollection.BASE_ATTRS + ("tags", "visible", "elements") SRC = "hdca" elements: List[Dict] def delete(self) -> None: self.gi.gi.histories.delete_dataset_collection(self.container.id, self.id) self.container.refresh() self.refresh() @abstractclass class LibRelatedDataset(Dataset): """ Base class for LibraryDatasetDatasetAssociation and LibraryDataset classes. """ @property def _stream_url(self) -> str: base_url = self.gi.gi.libraries._make_url() return f"{base_url}/datasets/download/uncompressed" class LibraryDatasetDatasetAssociation(LibRelatedDataset): """ Maps to a Galaxy ``LibraryDatasetDatasetAssociation``. """ BASE_ATTRS = LibRelatedDataset.BASE_ATTRS + ("deleted",) SRC = "ldda" class LibraryDataset(LibRelatedDataset): """ Maps to a Galaxy ``LibraryDataset``. """ SRC = "ld" file_name: str def delete(self, purged: bool = False) -> None: """ Delete this library dataset. :type purged: bool :param purged: if ``True``, also purge (permanently delete) the dataset """ self.gi.gi.libraries.delete_library_dataset(self.container.id, self.id, purged=purged) self.container.refresh() self.refresh() def update(self, **kwargs: Any) -> "LibraryDataset": """ Update this library dataset metadata. Some of the attributes that can be modified are documented below. :type name: str :param name: Replace history dataset name with the given string :type genome_build: str :param genome_build: Replace history dataset genome build (dbkey) """ res = self.gi.gi.libraries.update_library_dataset(self.id, **kwargs) self.container.refresh() self.__init__(res, self.container, gi=self.gi) # type: ignore[misc] return self @abstractclass class ContentInfo(Wrapper): """ Instances of this class wrap dictionaries obtained by getting ``/api/{histories,libraries}//contents`` from Galaxy. """ BASE_ATTRS = Wrapper.BASE_ATTRS + ( "name", "type", ) class LibraryContentInfo(ContentInfo): """ Instances of this class wrap dictionaries obtained by getting ``/api/libraries//contents`` from Galaxy. """ class HistoryContentInfo(ContentInfo): """ Instances of this class wrap dictionaries obtained by getting ``/api/histories//contents`` from Galaxy. """ BASE_ATTRS = ContentInfo.BASE_ATTRS + ("deleted", "state", "visible") DatasetContainerSubtype = TypeVar("DatasetContainerSubtype", bound="DatasetContainer") class DatasetContainer(Wrapper, Generic[DatasetSubtype], metaclass=abc.ABCMeta): """ Abstract base class for dataset containers (histories and libraries). """ BASE_ATTRS = Wrapper.BASE_ATTRS + ( "deleted", "name", ) API_MODULE: str CONTENT_INFO_TYPE: Type[ContentInfo] DS_TYPE: ClassVar[Callable] content_infos: List[ContentInfo] deleted: bool gi: "GalaxyInstance" name: str obj_gi_client: "client.ObjDatasetContainerClient" def __init__( self, c_dict: Dict[str, Any], content_infos: Optional[List[ContentInfo]] = None, gi: Optional["GalaxyInstance"] = None, ) -> None: """ :type content_infos: list of :class:`ContentInfo` :param content_infos: info objects for the container's contents """ super().__init__(c_dict, gi=gi) if content_infos is None: content_infos = [] object.__setattr__(self, "content_infos", content_infos) object.__setattr__(self, "obj_gi_client", getattr(self.gi, self.API_MODULE)) @property def dataset_ids(self) -> List[str]: """ Return the ids of the contained datasets. """ return [_.id for _ in self.content_infos if _.type == "file"] # I think we should deprecate this method - NS def preview(self) -> "DatasetContainerPreview": getf = self.obj_gi_client.get_previews # self.state could be stale: check both regular and deleted containers try: p = [_ for _ in getf() if _.id == self.id][0] except IndexError: try: p = [_ for _ in getf(deleted=True) if _.id == self.id][0] except IndexError: raise ValueError(f"no object for id {self.id}") return p def refresh(self: DatasetContainerSubtype) -> DatasetContainerSubtype: """ Re-fetch the attributes pertaining to this object. :return: self """ fresh = self.obj_gi_client.get(self.id) self.__init__(fresh.wrapped, content_infos=fresh.content_infos, gi=self.gi) # type: ignore[misc] return self def get_dataset(self, ds_id: str) -> DatasetSubtype: """ Retrieve the dataset corresponding to the given id. :type ds_id: str :param ds_id: dataset id :rtype: :class:`~.HistoryDatasetAssociation` or :class:`~.LibraryDataset` :return: the dataset corresponding to ``ds_id`` """ gi_client = getattr(self.gi.gi, self.API_MODULE) ds_dict = gi_client.show_dataset(self.id, ds_id) return self.DS_TYPE(ds_dict, self, gi=self.gi) def get_datasets(self, name: Optional[str] = None) -> List[DatasetSubtype]: """ Get all datasets contained inside this dataset container. :type name: str :param name: return only datasets with this name :rtype: list of :class:`~.HistoryDatasetAssociation` or list of :class:`~.LibraryDataset` :return: datasets with the given name contained inside this container .. note:: when filtering library datasets by name, specify their full paths starting from the library's root folder, e.g., ``/seqdata/reads.fastq``. Full paths are available through the ``content_infos`` attribute of :class:`~.Library` objects. """ if name is None: ds_ids = self.dataset_ids else: ds_ids = [_.id for _ in self.content_infos if _.name == name] return [self.get_dataset(_) for _ in ds_ids] @abc.abstractmethod def delete(self) -> None: """ Delete this dataset container. """ class History(DatasetContainer[HistoryDatasetAssociation]): """ Maps to a Galaxy history. """ BASE_ATTRS = DatasetContainer.BASE_ATTRS + ( "annotation", "published", "state", "state_ids", "state_details", "tags", ) DS_TYPE = HistoryDatasetAssociation DSC_TYPE = HistoryDatasetCollectionAssociation CONTENT_INFO_TYPE = HistoryContentInfo API_MODULE = "histories" annotation: str published: bool tags: List[str] def update(self, **kwargs: Any) -> "History": """ Update history metadata information. Some of the attributes that can be modified are documented below. :type name: str :param name: Replace history name with the given string :type annotation: str :param annotation: Replace history annotation with the given string :type deleted: bool :param deleted: Mark or unmark history as deleted :type purged: bool :param purged: If True, mark history as purged (permanently deleted). :type published: bool :param published: Mark or unmark history as published :type importable: bool :param importable: Mark or unmark history as importable :type tags: list :param tags: Replace history tags with the given list """ # TODO: wouldn't it be better if name and annotation were attributes? self.gi.gi.histories.update_history(self.id, **kwargs) self.refresh() return self def delete(self, purge: bool = False) -> None: """ Delete this history. :type purge: bool :param purge: if ``True``, also purge (permanently delete) the history .. note:: The ``purge`` option works only if the Galaxy instance has the ``allow_user_dataset_purge`` option set to ``true`` in the ``config/galaxy.yml`` configuration file. """ self.gi.histories.delete(id_=self.id, purge=purge) self.refresh() self.unmap() def import_dataset(self, lds: LibraryDataset) -> HistoryDatasetAssociation: """ Import a dataset into the history from a library. :type lds: :class:`~.LibraryDataset` :param lds: the library dataset to import :rtype: :class:`~.HistoryDatasetAssociation` :return: the imported history dataset """ if not self.is_mapped: raise RuntimeError("history is not mapped to a Galaxy object") if not isinstance(lds, LibraryDataset): raise TypeError("lds is not a LibraryDataset") res = self.gi.gi.histories.upload_dataset_from_library(self.id, lds.id) if not isinstance(res, Mapping): raise RuntimeError(f"upload_dataset_from_library: unexpected reply: {res!r}") self.refresh() return self.get_dataset(res["id"]) def upload_file(self, path: str, **kwargs: Any) -> HistoryDatasetAssociation: """ Upload the file specified by ``path`` to this history. :type path: str :param path: path of the file to upload See :meth:`~bioblend.galaxy.tools.ToolClient.upload_file` for the optional parameters. :rtype: :class:`~.HistoryDatasetAssociation` :return: the uploaded dataset """ out_dict = self.gi.gi.tools.upload_file(path, self.id, **kwargs) self.refresh() return self.get_dataset(out_dict["outputs"][0]["id"]) upload_dataset = upload_file def upload_from_ftp(self, path: str, **kwargs: Any) -> HistoryDatasetAssociation: """ Upload the file specified by ``path`` from the user's FTP directory to this history. :type path: str :param path: path of the file in the user's FTP directory See :meth:`~bioblend.galaxy.tools.ToolClient.upload_file` for the optional parameters. :rtype: :class:`~.HistoryDatasetAssociation` :return: the uploaded dataset """ out_dict = self.gi.gi.tools.upload_from_ftp(path, self.id, **kwargs) self.refresh() return self.get_dataset(out_dict["outputs"][0]["id"]) def paste_content(self, content: str, **kwargs: Any) -> HistoryDatasetAssociation: """ Upload a string to a new dataset in this history. :type content: str :param content: content of the new dataset to upload See :meth:`~bioblend.galaxy.tools.ToolClient.upload_file` for the optional parameters (except file_name). :rtype: :class:`~.HistoryDatasetAssociation` :return: the uploaded dataset """ out_dict = self.gi.gi.tools.paste_content(content, self.id, **kwargs) self.refresh() return self.get_dataset(out_dict["outputs"][0]["id"]) def export( self, gzip: bool = True, include_hidden: bool = False, include_deleted: bool = False, wait: bool = False, maxwait: Optional[int] = None, ) -> str: """ Start a job to create an export archive for this history. See :meth:`~bioblend.galaxy.histories.HistoryClient.export_history` for parameter and return value info. """ return self.gi.gi.histories.export_history( self.id, gzip=gzip, include_hidden=include_hidden, include_deleted=include_deleted, wait=wait, maxwait=maxwait, ) def download(self, jeha_id: str, outf: IO[bytes], chunk_size: int = bioblend.CHUNK_SIZE) -> None: """ Download an export archive for this history. Use :meth:`export` to create an export and get the required ``jeha_id``. See :meth:`~bioblend.galaxy.histories.HistoryClient.download_history` for parameter and return value info. """ return self.gi.gi.histories.download_history(self.id, jeha_id, outf, chunk_size=chunk_size) def create_dataset_collection( self, collection_description: bioblend.galaxy.dataset_collections.CollectionDescription ) -> "HistoryDatasetCollectionAssociation": """ Create a new dataset collection in the history by providing a collection description. :type collection_description: bioblend.galaxy.dataset_collections.CollectionDescription :param collection_description: a description of the dataset collection :rtype: :class:`~.HistoryDatasetCollectionAssociation` :return: the new dataset collection """ dataset_collection = self.gi.gi.histories.create_dataset_collection(self.id, collection_description) self.refresh() return self.get_dataset_collection(dataset_collection["id"]) def get_dataset_collection(self, dsc_id: str) -> "HistoryDatasetCollectionAssociation": """ Retrieve the dataset collection corresponding to the given id. :type dsc_id: str :param dsc_id: dataset collection id :rtype: :class:`~.HistoryDatasetCollectionAssociation` :return: the dataset collection corresponding to ``dsc_id`` """ dsc_dict = self.gi.gi.histories.show_dataset_collection(self.id, dsc_id) return self.DSC_TYPE(dsc_dict, self, gi=self.gi) class Library(DatasetContainer[LibraryDataset]): """ Maps to a Galaxy library. """ BASE_ATTRS = DatasetContainer.BASE_ATTRS + ("description", "synopsis") DS_TYPE = LibraryDataset CONTENT_INFO_TYPE = LibraryContentInfo API_MODULE = "libraries" description: str synopsis: str @property def folder_ids(self) -> List[str]: """ Return the ids of the contained folders. """ return [_.id for _ in self.content_infos if _.type == "folder"] def delete(self) -> None: """ Delete this library. """ self.gi.libraries.delete(id_=self.id) self.refresh() self.unmap() def _pre_upload(self, folder: Optional["Folder"]) -> Optional[str]: """ Return the id of the given folder, after sanity checking. """ if not self.is_mapped: raise RuntimeError("library is not mapped to a Galaxy object") return None if folder is None else folder.id def upload_data(self, data: str, folder: Optional["Folder"] = None, **kwargs: Any) -> LibraryDataset: """ Upload data to this library. :type data: str :param data: dataset contents :type folder: :class:`~.Folder` :param folder: a folder object, or ``None`` to upload to the root folder :rtype: :class:`~.LibraryDataset` :return: the dataset object that represents the uploaded content Optional keyword arguments: ``file_type``, ``dbkey``. """ fid = self._pre_upload(folder) res = self.gi.gi.libraries.upload_file_contents(self.id, data, folder_id=fid, **kwargs) self.refresh() return self.get_dataset(res[0]["id"]) def upload_from_url(self, url: str, folder: Optional["Folder"] = None, **kwargs: Any) -> LibraryDataset: """ Upload data to this library from the given URL. :type url: str :param url: URL from which data should be read See :meth:`.upload_data` for info on other params. """ fid = self._pre_upload(folder) res = self.gi.gi.libraries.upload_file_from_url(self.id, url, folder_id=fid, **kwargs) self.refresh() return self.get_dataset(res[0]["id"]) def upload_from_local(self, path: str, folder: Optional["Folder"] = None, **kwargs: Any) -> LibraryDataset: """ Upload data to this library from a local file. :type path: str :param path: local file path from which data should be read See :meth:`.upload_data` for info on other params. """ fid = self._pre_upload(folder) res = self.gi.gi.libraries.upload_file_from_local_path(self.id, path, folder_id=fid, **kwargs) self.refresh() return self.get_dataset(res[0]["id"]) def upload_from_galaxy_fs( self, paths: Union[str, Iterable[str]], folder: Optional["Folder"] = None, link_data_only: Literal["copy_files", "link_to_files"] = "copy_files", **kwargs: Any, ) -> List[LibraryDataset]: """ Upload data to this library from filesystem paths on the server. :type paths: str or :class:`~collections.abc.Iterable` of str :param paths: server-side file paths from which data should be read :type link_data_only: str :param link_data_only: either 'copy_files' (default) or 'link_to_files'. Setting to 'link_to_files' symlinks instead of copying the files :rtype: list of :class:`~.LibraryDataset` :return: the dataset objects that represent the uploaded content See :meth:`.upload_data` for info on other params. .. note:: This method works only if the Galaxy instance has the ``allow_path_paste`` option set to ``true`` in the ``config/galaxy.yml`` configuration file. """ fid = self._pre_upload(folder) if isinstance(paths, str): paths = (paths,) paths = "\n".join(paths) res = self.gi.gi.libraries.upload_from_galaxy_filesystem( self.id, paths, folder_id=fid, link_data_only=link_data_only, **kwargs ) if res is None: raise RuntimeError("upload_from_galaxy_filesystem: no reply") if not isinstance(res, Sequence): raise RuntimeError(f"upload_from_galaxy_filesystem: unexpected reply: {res!r}") self.refresh() return [self.get_dataset(ds_info["id"]) for ds_info in res] def copy_from_dataset( self, hda: HistoryDatasetAssociation, folder: Optional["Folder"] = None, message: str = "" ) -> LibraryDataset: """ Copy a history dataset into this library. :type hda: :class:`~.HistoryDatasetAssociation` :param hda: history dataset to copy into the library See :meth:`.upload_data` for info on other params. """ fid = self._pre_upload(folder) res = self.gi.gi.libraries.copy_from_dataset(self.id, hda.id, folder_id=fid, message=message) self.refresh() return self.get_dataset(res["library_dataset_id"]) def create_folder( self, name: str, description: Optional[str] = None, base_folder: Optional["Folder"] = None ) -> "Folder": """ Create a folder in this library. :type name: str :param name: folder name :type description: str :param description: optional folder description :type base_folder: :class:`~.Folder` :param base_folder: parent folder, or ``None`` to create in the root folder :rtype: :class:`~.Folder` :return: the folder just created """ bfid = None if base_folder is None else base_folder.id res = self.gi.gi.libraries.create_folder(self.id, name, description=description, base_folder_id=bfid) self.refresh() return self.get_folder(res[0]["id"]) def get_folder(self, f_id: str) -> "Folder": """ Retrieve the folder corresponding to the given id. :rtype: :class:`~.Folder` :return: the folder corresponding to ``f_id`` """ f_dict = self.gi.gi.libraries.show_folder(self.id, f_id) return Folder(f_dict, self, gi=self.gi) @property def root_folder(self) -> "Folder": """ The root folder of this library. :rtype: :class:`~.Folder` :return: the root folder of this library """ return self.get_folder(self.gi.gi.libraries._get_root_folder_id(self.id)) class Folder(Wrapper): """ Maps to a folder in a Galaxy library. """ BASE_ATTRS = Wrapper.BASE_ATTRS + ( "deleted", "description", "item_count", "name", ) container: Library description: str gi: "GalaxyInstance" name: str _cached_parent: Optional["Folder"] def __init__(self, f_dict: Dict[str, Any], container: Library, gi: "GalaxyInstance") -> None: super().__init__(f_dict, gi=gi) object.__setattr__(self, "container", container) @property def parent(self) -> Optional["Folder"]: """ The parent folder of this folder. The parent of the root folder is ``None``. :rtype: :class:`~.Folder` :return: the parent of this folder """ if self._cached_parent is None: object.__setattr__(self, "_cached_parent", self._get_parent()) return self._cached_parent def _get_parent(self) -> Optional["Folder"]: """ Return the parent folder of this folder. """ parent_id = self.wrapped["parent_id"] if parent_id is None: return None return self.container.get_folder(parent_id) def refresh(self) -> "Folder": """ Re-fetch the attributes pertaining to this object. :return: self """ f_dict = self.gi.gi.libraries.show_folder(self.container.id, self.id) self.__init__(f_dict, self.container, gi=self.gi) # type: ignore[misc] return self class Tool(Wrapper): """ Maps to a Galaxy tool. """ BASE_ATTRS = Wrapper.BASE_ATTRS + ( "name", "version", ) gi: "GalaxyInstance" POLLING_INTERVAL = 10 # for output state monitoring def run( self, inputs: Dict[str, Any], history: History, wait: bool = False, polling_interval: float = POLLING_INTERVAL ) -> List[HistoryDatasetAssociation]: """ Execute this tool in the given history with inputs from dict ``inputs``. :type inputs: dict :param inputs: dictionary of input datasets and parameters for the tool (see below) :type history: :class:`History` :param history: the history where to execute the tool :type wait: bool :param wait: whether to wait while the returned datasets are in a pending state :type polling_interval: float :param polling_interval: polling interval in seconds :rtype: list of :class:`HistoryDatasetAssociation` :return: list of output datasets The ``inputs`` dict should contain input datasets and parameters in the (largely undocumented) format used by the Galaxy API. Some examples can be found in `Galaxy's API test suite `_. The value of an input dataset can also be a :class:`Dataset` object, which will be automatically converted to the needed format. """ for k, v in inputs.items(): if isinstance(v, Dataset): inputs[k] = {"src": v.SRC, "id": v.id} # type: ignore out_dict = self.gi.gi.tools.run_tool(history.id, self.id, inputs) outputs = [history.get_dataset(_["id"]) for _ in out_dict["outputs"]] if wait: self.gi._wait_datasets(outputs, polling_interval=polling_interval) return outputs class Job(Wrapper): """ Maps to a Galaxy job. """ BASE_ATTRS = Wrapper.BASE_ATTRS + ("state",) DatasetContainerPreviewSubtype = TypeVar("DatasetContainerPreviewSubtype", bound="DatasetContainerPreview") @abstractclass class DatasetContainerPreview(Wrapper): """ Abstract base class for dataset container (history and library) 'previews'. """ BASE_ATTRS = Wrapper.BASE_ATTRS + ( "deleted", "name", ) class LibraryPreview(DatasetContainerPreview): """ Models Galaxy library 'previews'. Instances of this class wrap dictionaries obtained by getting ``/api/libraries`` from Galaxy. """ class HistoryPreview(DatasetContainerPreview): """ Models Galaxy history 'previews'. Instances of this class wrap dictionaries obtained by getting ``/api/histories`` from Galaxy. """ BASE_ATTRS = DatasetContainerPreview.BASE_ATTRS + ( "annotation", "published", "purged", "tags", ) class WorkflowPreview(Wrapper): """ Models Galaxy workflow 'previews'. Instances of this class wrap dictionaries obtained by getting ``/api/workflows`` from Galaxy. """ BASE_ATTRS = Wrapper.BASE_ATTRS + ( "deleted", "latest_workflow_uuid", "name", "number_of_steps", "owner", "published", "show_in_tool_panel", "tags", ) class InvocationPreview(Wrapper): """ Models Galaxy invocation 'previews'. Instances of this class wrap dictionaries obtained by getting ``/api/invocations`` from Galaxy. """ BASE_ATTRS = Wrapper.BASE_ATTRS + ( "history_id", "id", "state", "update_time", "uuid", "workflow_id", ) history_id: str state: str update_time: str uuid: str workflow_id: str class JobPreview(Wrapper): """ Models Galaxy job 'previews'. Instances of this class wrap dictionaries obtained by getting ``/api/jobs`` from Galaxy. """ BASE_ATTRS = Wrapper.BASE_ATTRS + ("state",) bioblend-1.2.0/bioblend/galaxy/quotas/000077500000000000000000000000001444761704300176615ustar00rootroot00000000000000bioblend-1.2.0/bioblend/galaxy/quotas/__init__.py000066400000000000000000000163511444761704300220000ustar00rootroot00000000000000""" Contains possible interactions with the Galaxy Quota """ from typing import ( Any, Dict, List, Optional, TYPE_CHECKING, ) from typing_extensions import Literal from bioblend.galaxy.client import Client if TYPE_CHECKING: from bioblend.galaxy import GalaxyInstance QuotaOperations = Literal["+", "-", "="] class QuotaClient(Client): module = "quotas" def __init__(self, galaxy_instance: "GalaxyInstance") -> None: super().__init__(galaxy_instance) def get_quotas(self, deleted: bool = False) -> List[Dict[str, Any]]: """ Get a list of quotas :type deleted: bool :param deleted: Only return quota(s) that have been deleted :rtype: list :return: A list of dicts with details on individual quotas. For example:: [{'id': '0604c8a56abe9a50', 'model_class': 'Quota', 'name': 'test ', 'url': '/api/quotas/0604c8a56abe9a50'}, {'id': '1ee267091d0190af', 'model_class': 'Quota', 'name': 'workshop', 'url': '/api/quotas/1ee267091d0190af'}] """ return self._get(deleted=deleted) def show_quota(self, quota_id: str, deleted: bool = False) -> Dict[str, Any]: """ Display information on a quota :type quota_id: str :param quota_id: Encoded quota ID :type deleted: bool :param deleted: Search for quota in list of ones already marked as deleted :rtype: dict :return: A description of quota. For example:: {'bytes': 107374182400, 'default': [], 'description': 'just testing', 'display_amount': '100.0 GB', 'groups': [], 'id': '0604c8a56abe9a50', 'model_class': 'Quota', 'name': 'test ', 'operation': '=', 'users': []} """ return self._get(id=quota_id, deleted=deleted) def create_quota( self, name: str, description: str, amount: str, operation: QuotaOperations, default: Optional[Literal["no", "registered", "unregistered"]] = "no", in_users: Optional[List[str]] = None, in_groups: Optional[List[str]] = None, ) -> Dict[str, Any]: """ Create a new quota :type name: str :param name: Name for the new quota. This must be unique within a Galaxy instance. :type description: str :param description: Quota description :type amount: str :param amount: Quota size (E.g. ``10000MB``, ``99 gb``, ``0.2T``, ``unlimited``) :type operation: str :param operation: One of (``+``, ``-``, ``=``) :type default: str :param default: Whether or not this is a default quota. Valid values are ``no``, ``unregistered``, ``registered``. None is equivalent to ``no``. :type in_users: list of str :param in_users: A list of user IDs or user emails. :type in_groups: list of str :param in_groups: A list of group IDs or names. :rtype: dict :return: A description of quota. For example:: {'url': '/galaxy/api/quotas/386f14984287a0f7', 'model_class': 'Quota', 'message': "Quota 'Testing' has been created with 1 associated users and 0 associated groups.", 'id': '386f14984287a0f7', 'name': 'Testing'} """ payload: Dict[str, Any] = { "name": name, "description": description, "amount": amount, "operation": operation, "default": default, } if in_users: payload["in_users"] = in_users if in_groups: payload["in_groups"] = in_groups return self._post(payload) def update_quota( self, quota_id: str, name: Optional[str] = None, description: Optional[str] = None, amount: Optional[str] = None, operation: Optional[QuotaOperations] = None, default: str = "no", in_users: Optional[List[str]] = None, in_groups: Optional[List[str]] = None, ) -> str: """ Update an existing quota :type quota_id: str :param quota_id: Encoded quota ID :type name: str :param name: Name for the new quota. This must be unique within a Galaxy instance. :type description: str :param description: Quota description. If you supply this parameter, but not the name, an error will be thrown. :type amount: str :param amount: Quota size (E.g. ``10000MB``, ``99 gb``, ``0.2T``, ``unlimited``) :type operation: str :param operation: One of (``+``, ``-``, ``=``). If you wish to change this value, you must also provide the ``amount``, otherwise it will not take effect. :type default: str :param default: Whether or not this is a default quota. Valid values are ``no``, ``unregistered``, ``registered``. Calling this method with ``default="no"`` on a non-default quota will throw an error. Not passing this parameter is equivalent to passing ``no``. :type in_users: list of str :param in_users: A list of user IDs or user emails. :type in_groups: list of str :param in_groups: A list of group IDs or names. :rtype: str :return: A semicolon separated list of changes to the quota. For example:: "Quota 'Testing-A' has been renamed to 'Testing-B'; Quota 'Testing-e' is now '-100.0 GB'; Quota 'Testing-B' is now the default for unregistered users" """ payload: Dict[str, Any] = {"default": default} if name: payload["name"] = name if description: payload["description"] = description if amount: payload["amount"] = amount if operation: payload["operation"] = operation if in_users: payload["in_users"] = in_users if in_groups: payload["in_groups"] = in_groups return self._put(id=quota_id, payload=payload) def delete_quota(self, quota_id: str) -> str: """ Delete a quota Before a quota can be deleted, the quota must not be a default quota. :type quota_id: str :param quota_id: Encoded quota ID. :rtype: str :return: A description of the changes, mentioning the deleted quota. For example:: "Deleted 1 quotas: Testing-B" """ return self._delete(id=quota_id) def undelete_quota(self, quota_id: str) -> str: """ Undelete a quota :type quota_id: str :param quota_id: Encoded quota ID. :rtype: str :return: A description of the changes, mentioning the undeleted quota. For example:: "Undeleted 1 quotas: Testing-B" """ url = self._make_url(quota_id, deleted=True) + "/undelete" return self._post(url=url, payload={"id": quota_id}) bioblend-1.2.0/bioblend/galaxy/roles/000077500000000000000000000000001444761704300174715ustar00rootroot00000000000000bioblend-1.2.0/bioblend/galaxy/roles/__init__.py000066400000000000000000000057231444761704300216110ustar00rootroot00000000000000""" Contains possible interactions with the Galaxy Roles """ from typing import ( Any, Dict, List, Optional, TYPE_CHECKING, ) from bioblend.galaxy.client import Client if TYPE_CHECKING: from bioblend.galaxy import GalaxyInstance class RolesClient(Client): module = "roles" def __init__(self, galaxy_instance: "GalaxyInstance") -> None: super().__init__(galaxy_instance) def get_roles(self) -> List[Dict[str, Any]]: """ Displays a collection (list) of roles. :rtype: list :return: A list of dicts with details on individual roles. For example:: [{"id": "f2db41e1fa331b3e", "model_class": "Role", "name": "Foo", "url": "/api/roles/f2db41e1fa331b3e"}, {"id": "f597429621d6eb2b", "model_class": "Role", "name": "Bar", "url": "/api/roles/f597429621d6eb2b"}] """ return self._get() def show_role(self, role_id: str) -> Dict[str, Any]: """ Display information on a single role :type role_id: str :param role_id: Encoded role ID :rtype: dict :return: Details of the given role. For example:: {"description": "Private Role for Foo", "id": "f2db41e1fa331b3e", "model_class": "Role", "name": "Foo", "type": "private", "url": "/api/roles/f2db41e1fa331b3e"} """ return self._get(id=role_id) def create_role( self, role_name: str, description: str, user_ids: Optional[List[str]] = None, group_ids: Optional[List[str]] = None, ) -> Dict[str, Any]: """ Create a new role. :type role_name: str :param role_name: A name for the new role :type description: str :param description: Description for the new role :type user_ids: list :param user_ids: A list of encoded user IDs to add to the new role :type group_ids: list :param group_ids: A list of encoded group IDs to add to the new role :rtype: dict :return: Details of the newly created role. For example:: {'description': 'desc', 'url': '/api/roles/ebfb8f50c6abde6d', 'model_class': 'Role', 'type': 'admin', 'id': 'ebfb8f50c6abde6d', 'name': 'Foo'} .. versionchanged:: 0.15.0 Changed the return value from a 1-element list to a dict. """ if user_ids is None: user_ids = [] if group_ids is None: group_ids = [] payload = {"name": role_name, "description": description, "user_ids": user_ids, "group_ids": group_ids} ret = self._post(payload) if isinstance(ret, list): # Galaxy release_20.09 and earlier returned a 1-element list ret = ret[0] return ret bioblend-1.2.0/bioblend/galaxy/tool_data/000077500000000000000000000000001444761704300203135ustar00rootroot00000000000000bioblend-1.2.0/bioblend/galaxy/tool_data/__init__.py000066400000000000000000000055061444761704300224320ustar00rootroot00000000000000""" Contains possible interactions with the Galaxy Tool data tables """ from typing import ( Any, Dict, List, TYPE_CHECKING, ) from bioblend.galaxy.client import Client if TYPE_CHECKING: from bioblend.galaxy import GalaxyInstance class ToolDataClient(Client): module = "tool_data" def __init__(self, galaxy_instance: "GalaxyInstance") -> None: super().__init__(galaxy_instance) def get_data_tables(self) -> List[Dict[str, Any]]: """ Get the list of all data tables. :rtype: list :return: A list of dicts with details on individual data tables. For example:: [{"model_class": "TabularToolDataTable", "name": "fasta_indexes"}, {"model_class": "TabularToolDataTable", "name": "bwa_indexes"}] """ return self._get() def show_data_table(self, data_table_id: str) -> Dict[str, Any]: """ Get details of a given data table. :type data_table_id: str :param data_table_id: ID of the data table :rtype: dict :return: A description of the given data table and its content. For example:: {'columns': ['value', 'dbkey', 'name', 'path'], 'fields': [['test id', 'test', 'test name', '/opt/galaxy-dist/tool-data/test/seq/test id.fa']], 'model_class': 'TabularToolDataTable', 'name': 'all_fasta'} """ return self._get(id=data_table_id) def reload_data_table(self, data_table_id: str) -> Dict[str, Any]: """ Reload a data table. :type data_table_id: str :param data_table_id: ID of the data table :rtype: dict :return: A description of the given data table and its content. For example:: {'columns': ['value', 'dbkey', 'name', 'path'], 'fields': [['test id', 'test', 'test name', '/opt/galaxy-dist/tool-data/test/seq/test id.fa']], 'model_class': 'TabularToolDataTable', 'name': 'all_fasta'} """ url = self._make_url(data_table_id) + "/reload" return self._get(url=url) def delete_data_table(self, data_table_id: str, values: str) -> Dict[str, Any]: """ Delete an item from a data table. :type data_table_id: str :param data_table_id: ID of the data table :type values: str :param values: a "|" separated list of column contents, there must be a value for all the columns of the data table :rtype: dict :return: Remaining contents of the given data table """ payload = {"values": values} return self._delete(payload=payload, id=data_table_id) bioblend-1.2.0/bioblend/galaxy/tool_dependencies/000077500000000000000000000000001444761704300220305ustar00rootroot00000000000000bioblend-1.2.0/bioblend/galaxy/tool_dependencies/__init__.py000066400000000000000000000103401444761704300241370ustar00rootroot00000000000000""" Contains interactions dealing with Galaxy dependency resolvers. """ from typing import ( Any, Dict, List, Optional, TYPE_CHECKING, ) from typing_extensions import Literal from bioblend.galaxy.client import Client if TYPE_CHECKING: from bioblend.galaxy import GalaxyInstance class ToolDependenciesClient(Client): module = "dependency_resolvers" def __init__(self, galaxy_instance: "GalaxyInstance") -> None: super().__init__(galaxy_instance) def summarize_toolbox( self, index: Optional[int] = None, tool_ids: Optional[List[str]] = None, resolver_type: Optional[str] = None, include_containers: bool = False, container_type: Optional[str] = None, index_by: Literal["requirements", "tools"] = "requirements", ) -> list: """ Summarize requirements across toolbox (for Tool Management grid). :type index: int :param index: index of the dependency resolver with respect to the dependency resolvers config file :type tool_ids: list :param tool_ids: tool_ids to return when index_by=tools :type resolver_type: str :param resolver_type: restrict to specified resolver type :type include_containers: bool :param include_containers: include container resolvers in resolution :type container_type: str :param container_type: restrict to specified container type :type index_by: str :param index_by: By default results are grouped by requirements. Set to 'tools' to return one entry per tool. :rtype: list of dicts :returns: dictified descriptions of the dependencies, with attribute `dependency_type: None` if no match was found. For example:: [{'requirements': [{'name': 'galaxy_sequence_utils', 'specs': [], 'type': 'package', 'version': '1.1.4'}, {'name': 'bx-python', 'specs': [], 'type': 'package', 'version': '0.8.6'}], 'status': [{'cacheable': False, 'dependency_type': None, 'exact': True, 'model_class': 'NullDependency', 'name': 'galaxy_sequence_utils', 'version': '1.1.4'}, {'cacheable': False, 'dependency_type': None, 'exact': True, 'model_class': 'NullDependency', 'name': 'bx-python', 'version': '0.8.6'}], 'tool_ids': ['vcf_to_maf_customtrack1']}] .. note:: This method works only on Galaxy 20.01 or later and if the user is a Galaxy admin. It relies on an experimental API particularly tied to the GUI and therefore is subject to breaking changes. """ assert index_by in ["tools", "requirements"], "index_by must be one of 'tools' or 'requirements'." params = { "include_containers": str(include_containers), "index_by": index_by, } if index: params["index"] = str(index) if tool_ids: params["tool_ids"] = ",".join(tool_ids) if resolver_type: params["resolver_type"] = resolver_type if container_type: params["container_type"] = container_type url = "/".join((self._make_url(), "toolbox")) return self._get(url=url, params=params) def unused_dependency_paths(self) -> List[str]: """ List unused dependencies """ url = "/".join((self._make_url(), "unused_paths")) return self._get(url=url) def delete_unused_dependency_paths(self, paths: List[str]) -> None: """ Delete unused paths :type paths: list :param paths: paths to delete """ payload: Dict[str, Any] = {"paths": paths} url = "/".join((self._make_url(), "unused_paths")) self._put(url=url, payload=payload) bioblend-1.2.0/bioblend/galaxy/tools/000077500000000000000000000000001444761704300175055ustar00rootroot00000000000000bioblend-1.2.0/bioblend/galaxy/tools/__init__.py000066400000000000000000000547511444761704300216320ustar00rootroot00000000000000""" Contains possible interaction dealing with Galaxy tools. """ from os.path import basename from typing import ( Any, Dict, List, Optional, TYPE_CHECKING, Union, ) from typing_extensions import Literal from bioblend.galaxy.client import Client from bioblend.galaxyclient import UPLOAD_CHUNK_SIZE from bioblend.util import attach_file from .inputs import InputsBuilder if TYPE_CHECKING: from bioblend.galaxy import GalaxyInstance class ToolClient(Client): gi: "GalaxyInstance" module = "tools" def __init__(self, galaxy_instance: "GalaxyInstance") -> None: super().__init__(galaxy_instance) def get_tools( self, tool_id: Optional[str] = None, name: Optional[str] = None, trackster: Optional[bool] = None ) -> List[Dict[str, Any]]: """ Get all tools, or select a subset by specifying optional arguments for filtering (e.g. a tool name). :type name: str :param name: Tool name to filter on. :type trackster: bool :param trackster: whether to return only tools that are compatible with Trackster :rtype: list :return: List of tool descriptions. .. seealso:: bioblend.galaxy.toolshed.get_repositories() .. versionchanged:: 1.1.1 Using the deprecated ``tool_id`` parameter now raises a ``ValueError`` exception. """ if tool_id is not None: raise ValueError( "The tool_id parameter has been removed, use the show_tool() method to view details of a tool for which you know the ID." ) tools = self._raw_get_tool(in_panel=False, trackster=trackster) if name is not None: tools = [_ for _ in tools if _["name"] == name] return tools def get_tool_panel(self) -> List[Dict[str, Any]]: """ Get a list of available tool elements in Galaxy's configured toolbox. :rtype: list :return: List containing tools (if not in sections) or tool sections with nested tool descriptions. .. seealso:: bioblend.galaxy.toolshed.get_repositories() """ return self._raw_get_tool(in_panel=True) def _raw_get_tool(self, in_panel: Optional[bool] = None, trackster: Optional[bool] = None) -> List[Dict[str, Any]]: params = { "in_panel": in_panel, "trackster": trackster, } return self._get(params=params) def requirements(self, tool_id: str) -> List[Dict[str, Any]]: """ Return the resolver status for a specific tool. :type tool_id: str :param tool_id: id of the requested tool :rtype: list :return: List containing a resolver status dict for each tool requirement. For example:: [{'cacheable': False, 'dependency_resolver': {'auto_init': True, 'auto_install': False, 'can_uninstall_dependencies': True, 'ensure_channels': 'iuc,conda-forge,bioconda,defaults', 'model_class': 'CondaDependencyResolver', 'prefix': '/mnt/galaxy/tool_dependencies/_conda', 'resolver_type': 'conda', 'resolves_simple_dependencies': True, 'use_local': False, 'versionless': False}, 'dependency_type': 'conda', 'environment_path': '/mnt/galaxy/tool_dependencies/_conda/envs/__blast@2.10.1', 'exact': True, 'model_class': 'MergedCondaDependency', 'name': 'blast', 'version': '2.10.1'}] .. note:: This method works only if the user is a Galaxy admin. """ url = self._make_url(tool_id) + "/requirements" return self._get(url=url) def reload(self, tool_id: str) -> dict: """ Reload the specified tool in the toolbox. Any changes that have been made to the wrapper since the tool was last reloaded will take effect. :type tool_id: str :param tool_id: id of the requested tool :rtype: dict :param: dict containing the id, name, and version of the reloaded tool. For example:: {'message': {'id': 'toolshed.g2.bx.psu.edu/repos/lparsons/cutadapt/cutadapt/3.4+galaxy1', 'name': 'Cutadapt', 'version': '3.4+galaxy1'}} .. note:: This method works only if the user is a Galaxy admin. """ url = self._make_url(tool_id) + "/reload" return self._put(url=url) def get_citations(self, tool_id: str) -> List[dict]: """ Get BibTeX citations for a given tool ID. :type tool_id: str :param tool_id: id of the requested tool :rtype: list of dicts :param: list containing the citations """ url = self._make_url(tool_id) + "/citations" return self._get(url=url) def install_dependencies(self, tool_id: str) -> List[Dict[str, Any]]: """ Install dependencies for a given tool via a resolver. This works only for Conda currently. :type tool_id: str :param tool_id: id of the requested tool :rtype: list of dicts :return: List of tool requirement status dictionaries .. note:: This method works only if the user is a Galaxy admin. """ url = self._make_url(tool_id) + "/install_dependencies" return self._post(url=url) def uninstall_dependencies(self, tool_id: str) -> dict: """ Uninstall dependencies for a given tool via a resolver. This works only for Conda currently. :type tool_id: str :param tool_id: id of the requested tool :rtype: dict :return: Tool requirement status .. note:: This method works only if the user is a Galaxy admin. """ url = self._make_url(tool_id) + "/dependencies" return self._delete(url=url) def show_tool(self, tool_id: str, io_details: bool = False, link_details: bool = False) -> Dict[str, Any]: """ Get details of a given tool. :type tool_id: str :param tool_id: id of the requested tool :type io_details: bool :param io_details: whether to get also input and output details :type link_details: bool :param link_details: whether to get also link details :rtype: dict :return: Information about the tool's interface """ params = { "io_details": io_details, "link_details": link_details, } return self._get(id=tool_id, params=params) def build( self, tool_id: str, inputs: Optional[Dict[str, Any]] = None, tool_version: Optional[str] = None, history_id: Optional[str] = None, ) -> Dict[str, Any]: """ This method returns the tool model, which includes an updated input parameter array for the given tool, based on user-defined "inputs". :type inputs: dict :param inputs: (optional) inputs for the payload. For example:: { "num_lines": "1", "input": { "values": [ { "src": "hda", "id": "4d366c1196c36d18" } ] }, "seed_source|seed_source_selector": "no_seed", } :type tool_id: str :param tool_id: id of the requested tool :type history_id: str :param history_id: id of the requested history :type tool_version: str :param tool_version: version of the requested tool :rtype: dict :return: Returns a tool model including dynamic parameters and updated values, repeats block etc. For example:: { "model_class": "Tool", "id": "random_lines1", "name": "Select random lines", "version": "2.0.2", "description": "from a file", "labels": [], "edam_operations": [], "edam_topics": [], "hidden": "", "is_workflow_compatible": True, "xrefs": [], "config_file": "/Users/joshij/galaxy/tools/filters/randomlines.xml", "panel_section_id": "textutil", "panel_section_name": "Text Manipulation", "form_style": "regular", "inputs": [ { "model_class": "IntegerToolParameter", "name": "num_lines", "argument": None, "type": "integer", "label": "Randomly select", "help": "lines", "refresh_on_change": False, "min": None, "max": None, "optional": False, "hidden": False, "is_dynamic": False, "value": "1", "area": False, "datalist": [], "default_value": "1", "text_value": "1", }, ], "help": 'This tool selects N random lines from a file, with no repeats, and preserving ordering.', "citations": False, "sharable_url": None, "message": "", "warnings": "", "versions": ["2.0.2"], "requirements": [], "errors": {}, "tool_errors": None, "state_inputs": { "num_lines": "1", "input": {"values": [{"id": "4d366c1196c36d18", "src": "hda"}]}, "seed_source": {"seed_source_selector": "no_seed", "__current_case__": 0}, }, "job_id": None, "job_remap": None, "history_id": "c9468fdb6dc5c5f1", "display": True, "action": "/tool_runner/index", "license": None, "creator": None, "method": "post", "enctype": "application/x-www-form-urlencoded", } """ params: Dict[str, Union[str, Dict]] = {} if inputs: params["inputs"] = inputs if tool_version: params["tool_version"] = tool_version if history_id: params["history_id"] = history_id url = "/".join((self.gi.url, "tools", tool_id, "build")) return self._post(payload=params, url=url) def run_tool( self, history_id: str, tool_id: str, tool_inputs: Union[InputsBuilder, dict], input_format: Literal["21.01", "legacy"] = "legacy", data_manager_mode: Optional[Literal["populate", "dry_run", "bundle"]] = None, ) -> Dict[str, Any]: """ Runs tool specified by ``tool_id`` in history indicated by ``history_id`` with inputs from ``dict`` ``tool_inputs``. :type history_id: str :param history_id: encoded ID of the history in which to run the tool :type tool_id: str :param tool_id: ID of the tool to be run :type data_manager_mode: str :param data_manager_mode: Possible values are 'populate', 'dry_run' and 'bundle'. 'populate' is the default behavior for data manager tools and results in tool data table files being updated after the data manager job completes. 'dry_run' will skip any processing after the data manager job completes 'bundle' will create a data manager bundle that can be imported on other Galaxy servers. :type tool_inputs: dict :param tool_inputs: dictionary of input datasets and parameters for the tool (see below) :type input_format: string :param input_format: input format for the payload. Possible values are the default 'legacy' (where inputs nested inside conditionals or repeats are identified with e.g. '|') or '21.01' (where inputs inside conditionals or repeats are nested elements). :rtype: dict :return: Information about outputs and job For example:: {'implicit_collections': [], 'jobs': [{'create_time': '2019-05-08T12:26:16.067372', 'exit_code': None, 'id': '7dd125b61b35d782', 'model_class': 'Job', 'state': 'new', 'tool_id': 'cut1', 'update_time': '2019-05-08T12:26:16.067389'}], 'output_collections': [], 'outputs': [{'create_time': '2019-05-08T12:26:15.997739', 'data_type': 'galaxy.datatypes.tabular.Tabular', 'deleted': False, 'file_ext': 'tabular', 'file_size': 0, 'genome_build': '?', 'hda_ldda': 'hda', 'hid': 42, 'history_content_type': 'dataset', 'history_id': 'df8fe5ddadbf3ab1', 'id': 'aeb65580396167f3', 'metadata_column_names': None, 'metadata_column_types': None, 'metadata_columns': None, 'metadata_comment_lines': None, 'metadata_data_lines': None, 'metadata_dbkey': '?', 'metadata_delimiter': '\t', 'misc_blurb': 'queued', 'misc_info': None, 'model_class': 'HistoryDatasetAssociation', 'name': 'Cut on data 1', 'output_name': 'out_file1', 'peek': None, 'purged': False, 'state': 'new', 'tags': [], 'update_time': '2019-05-08T12:26:16.069798', 'uuid': 'd91d10af-7546-45be-baa9-902010661466', 'visible': True}]} The ``tool_inputs`` dict should contain input datasets and parameters in the (largely undocumented) format used by the Galaxy API. If you are unsure how to construct this dict for the tool you want to run, you can obtain a template by executing the ``build()`` method and taking the value of ``state_inputs`` from its output, then modifying it as you require. You can also check the examples in `Galaxy's API test suite `_. """ payload: Dict[str, Union[str, Dict]] = { "history_id": history_id, "tool_id": tool_id, "input_format": input_format, } if isinstance(tool_inputs, InputsBuilder): payload["inputs"] = tool_inputs.to_dict() else: payload["inputs"] = tool_inputs if data_manager_mode: payload["data_manager_mode"] = data_manager_mode return self._post(payload) def upload_file( self, path: str, history_id: str, storage: Optional[str] = None, metadata: Optional[dict] = None, chunk_size: Optional[int] = UPLOAD_CHUNK_SIZE, **kwargs: Any, ) -> Dict[str, Any]: """ Upload the file specified by ``path`` to the history specified by ``history_id``. :type path: str :param path: path of the file to upload :type history_id: str :param history_id: id of the history where to upload the file :type storage: str :param storage: Local path to store URLs resuming uploads :type metadata: dict :param metadata: Metadata to send with upload request :type chunk_size: int :param chunk_size: Number of bytes to send in each chunk :type file_name: str :param file_name: (optional) name of the new history dataset :type file_type: str :param file_type: (optional) Galaxy datatype for the new dataset, default is auto :type dbkey: str :param dbkey: (optional) genome dbkey :type to_posix_lines: bool :param to_posix_lines: if ``True`` (the default), convert universal line endings to POSIX line endings. Set to ``False`` when uploading a gzip, bz2 or zip archive containing a binary file :type space_to_tab: bool :param space_to_tab: whether to convert spaces to tabs. Default is ``False``. Applicable only if to_posix_lines is ``True`` :type auto_decompress: bool :param auto_decompress: Automatically decompress files if the uploaded file is compressed and the file type is not one that supports compression (e.g. ``fastqsanger.gz``). Default is ``False``. :rtype: dict :return: Information about the created upload job .. note:: The following parameters work only on Galaxy 22.01 or later: ``storage``, ``metadata``, ``chunk_size``, ``auto_decompress``. """ if self.gi.config.get_version()["version_major"] >= "22.01": # Use the tus protocol uploader = self.gi.get_tus_uploader(path, storage=storage, metadata=metadata, chunk_size=chunk_size) uploader.upload() return self.post_to_fetch(path, history_id, uploader.session_id, **kwargs) else: if "file_name" not in kwargs: kwargs["file_name"] = basename(path) payload = self._upload_payload(history_id, **kwargs) payload["files_0|file_data"] = attach_file(path, name=kwargs["file_name"]) try: return self._post(payload, files_attached=True) finally: payload["files_0|file_data"].close() def post_to_fetch(self, path: str, history_id: str, session_id: str, **kwargs: Any) -> Dict[str, Any]: """ Make a POST request to the Fetch API after performing a tus upload. This is called by :meth:`upload_file` after performing an upload. This method is useful if you want to control the tus uploader yourself (e.g. to report on progress):: uploader = gi.get_tus_uploader(path, storage=storage) while uploader.offset < uploader.file_size: uploader.upload_chunk() # perform other actions... gi.tools.post_to_fetch(path, history_id, uploader.session_id, **upload_kwargs) :type session_id: str :param session_id: Session ID returned by the tus service See :meth:`upload_file` for additional parameters. :rtype: dict :return: Information about the created upload job """ payload = self._fetch_payload(path, history_id, session_id, **kwargs) url = "/".join((self.gi.url, "tools/fetch")) return self._post(payload, url=url) def upload_from_ftp(self, path: str, history_id: str, **kwargs: Any) -> Dict[str, Any]: """ Upload the file specified by ``path`` from the user's FTP directory to the history specified by ``history_id``. :type path: str :param path: path of the file in the user's FTP directory :type history_id: str :param history_id: id of the history where to upload the file See :meth:`upload_file` for the optional parameters. :rtype: dict :return: Information about the created upload job """ payload = self._upload_payload(history_id, **kwargs) payload["files_0|ftp_files"] = path return self._post(payload) def paste_content(self, content: str, history_id: str, **kwargs: Any) -> Dict[str, Any]: """ Upload a string to a new dataset in the history specified by ``history_id``. :type content: str :param content: content of the new dataset to upload or a list of URLs (one per line) to upload :type history_id: str :param history_id: id of the history where to upload the content :rtype: dict :return: Information about the created upload job See :meth:`upload_file` for the optional parameters. """ payload = self._upload_payload(history_id, **kwargs) payload["files_0|url_paste"] = content return self._post(payload, files_attached=False) put_url = paste_content def _upload_payload(self, history_id: str, **kwargs: Any) -> Dict[str, Any]: tool_input: Dict[str, Any] = { "file_type": kwargs.get("file_type", "auto"), "dbkey": kwargs.get("dbkey", "?"), "files_0|type": "upload_dataset", } if not kwargs.get("to_posix_lines", True): tool_input["files_0|to_posix_lines"] = False elif kwargs.get("space_to_tab", False): tool_input["files_0|space_to_tab"] = "Yes" if "file_name" in kwargs: tool_input["files_0|NAME"] = kwargs["file_name"] return { "history_id": history_id, "tool_id": kwargs.get("tool_id", "upload1"), "inputs": tool_input, } def _fetch_payload(self, path: str, history_id: str, session_id: str, **kwargs: Any) -> dict: file_name = kwargs.get("file_name", basename(path)) element = { "src": "files", "ext": kwargs.get("file_type", "auto"), "dbkey": kwargs.get("dbkey", "?"), "to_posix_lines": kwargs.get("to_posix_lines", True), "space_to_tab": kwargs.get("space_to_tab", False), "name": file_name, } payload = { "history_id": history_id, "targets": [ { "destination": {"type": "hdas"}, "elements": [element], } ], "files_0|file_data": {"session_id": session_id, "name": file_name}, "auto_decompress": kwargs.get("auto_decompress", False), } return payload bioblend-1.2.0/bioblend/galaxy/tools/inputs.py000066400000000000000000000041571444761704300214100ustar00rootroot00000000000000from typing import ( Any, Dict, Iterator, List, Optional, Tuple, Union, ) class InputsBuilder: """ """ def __init__(self) -> None: self._input_dict: Dict[str, Any] = {} def set(self, name: str, input: Any) -> "InputsBuilder": self._input_dict[name] = input return self def set_param(self, name: str, value: Any) -> "InputsBuilder": return self.set(name, param(value=value)) def set_dataset_param(self, name: str, value: str, src: str = "hda") -> "InputsBuilder": return self.set(name, dataset(value, src=src)) def to_dict(self) -> Dict[str, Any]: values = {} for key, value in self.flat_iter(): if hasattr(value, "value"): value = value.value values[key] = value return values def flat_iter(self, prefix: Optional[str] = None) -> Iterator[Tuple[str, Any]]: for key, value in self._input_dict.items(): effective_key = key if prefix is None else f"{prefix}|{key}" if hasattr(value, "flat_iter"): yield from value.flat_iter(effective_key) else: yield effective_key, value class RepeatBuilder: def __init__(self) -> None: self._instances: List[InputsBuilder] = [] def instance(self, inputs: InputsBuilder) -> "RepeatBuilder": self._instances.append(inputs) return self def flat_iter(self, prefix: str) -> Iterator[Tuple[str, Any]]: for index, instance in enumerate(self._instances): index_prefix = f"{prefix}_{index}" yield from instance.flat_iter(index_prefix) class Param: def __init__(self, value: Any) -> None: self.value = value class DatasetParam(Param): def __init__(self, value: Union[Dict[str, str], str], src: str = "hda") -> None: if not isinstance(value, dict): value = dict(src=src, id=value) super().__init__(value) inputs = InputsBuilder repeat = RepeatBuilder conditional = InputsBuilder param = Param dataset = DatasetParam __all__ = ("inputs", "repeat", "conditional", "param") bioblend-1.2.0/bioblend/galaxy/toolshed/000077500000000000000000000000001444761704300201665ustar00rootroot00000000000000bioblend-1.2.0/bioblend/galaxy/toolshed/__init__.py000066400000000000000000000173711444761704300223100ustar00rootroot00000000000000""" Interaction with a Galaxy Tool Shed. """ from typing import ( Any, Dict, List, Optional, TYPE_CHECKING, ) from bioblend.galaxy.client import Client if TYPE_CHECKING: from bioblend.galaxy import GalaxyInstance class ToolShedClient(Client): module = "tool_shed_repositories" def __init__(self, galaxy_instance: "GalaxyInstance") -> None: super().__init__(galaxy_instance) def get_repositories(self) -> List[Dict[str, Any]]: """ Get the list of all installed Tool Shed repositories on this Galaxy instance. :rtype: list :return: a list of dictionaries containing information about repositories present in the Tool Shed. For example:: [{'changeset_revision': '4afe13ac23b6', 'deleted': False, 'dist_to_shed': False, 'error_message': '', 'name': 'velvet_toolsuite', 'owner': 'edward-kirton', 'status': 'Installed'}] .. versionchanged:: 0.4.1 Changed method name from ``get_tools`` to ``get_repositories`` to better align with the Tool Shed concepts .. seealso:: bioblend.galaxy.tools.get_tool_panel() """ return self._get() def show_repository(self, toolShed_id: str) -> Dict[str, Any]: """ Get details of a given Tool Shed repository as it is installed on this Galaxy instance. :type toolShed_id: str :param toolShed_id: Encoded Tool Shed ID :rtype: dict :return: Information about the tool For example:: {'changeset_revision': 'b17455fb6222', 'ctx_rev': '8', 'owner': 'aaron', 'status': 'Installed', 'url': '/api/tool_shed_repositories/82de4a4c7135b20a'} .. versionchanged:: 0.4.1 Changed method name from ``show_tool`` to ``show_repository`` to better align with the Tool Shed concepts """ return self._get(id=toolShed_id) def install_repository_revision( self, tool_shed_url: str, name: str, owner: str, changeset_revision: str, install_tool_dependencies: bool = False, install_repository_dependencies: bool = False, install_resolver_dependencies: bool = False, tool_panel_section_id: Optional[str] = None, new_tool_panel_section_label: Optional[str] = None, ) -> Dict[str, Any]: """ Install a specified repository revision from a specified Tool Shed into this Galaxy instance. This example demonstrates installation of a repository that contains valid tools, loading them into a section of the Galaxy tool panel or creating a new tool panel section. You can choose if tool dependencies or repository dependencies should be installed through the Tool Shed, (use ``install_tool_dependencies`` or ``install_repository_dependencies``) or through a resolver that supports installing dependencies (use ``install_resolver_dependencies``). Note that any combination of the three dependency resolving variables is valid. Installing the repository into an existing tool panel section requires the tool panel config file (e.g., tool_conf.xml, shed_tool_conf.xml, etc) to contain the given tool panel section:
:type tool_shed_url: str :param tool_shed_url: URL of the Tool Shed from which the repository should be installed from (e.g., ``https://testtoolshed.g2.bx.psu.edu``) :type name: str :param name: The name of the repository that should be installed :type owner: str :param owner: The name of the repository owner :type changeset_revision: str :param changeset_revision: The revision of the repository to be installed :type install_tool_dependencies: bool :param install_tool_dependencies: Whether or not to automatically handle tool dependencies (see https://galaxyproject.org/toolshed/tool-dependency-recipes/ for more details) :type install_repository_dependencies: bool :param install_repository_dependencies: Whether or not to automatically handle repository dependencies (see https://galaxyproject.org/toolshed/defining-repository-dependencies/ for more details) :type install_resolver_dependencies: bool :param install_resolver_dependencies: Whether or not to automatically install resolver dependencies (e.g. conda). :type tool_panel_section_id: str :param tool_panel_section_id: The ID of the Galaxy tool panel section where the tool should be insterted under. Note that you should specify either this parameter or the ``new_tool_panel_section_label``. If both are specified, this one will take precedence. :type new_tool_panel_section_label: str :param new_tool_panel_section_label: The name of a Galaxy tool panel section that should be created and the repository installed into. """ payload: Dict[str, Any] = {} payload["tool_shed_url"] = tool_shed_url payload["name"] = name payload["owner"] = owner payload["changeset_revision"] = changeset_revision payload["install_tool_dependencies"] = install_tool_dependencies payload["install_repository_dependencies"] = install_repository_dependencies payload["install_resolver_dependencies"] = install_resolver_dependencies if tool_panel_section_id: payload["tool_panel_section_id"] = tool_panel_section_id elif new_tool_panel_section_label: payload["new_tool_panel_section_label"] = new_tool_panel_section_label url = self._make_url() + "/new/install_repository_revision" return self._post(url=url, payload=payload) def uninstall_repository_revision( self, name: str, owner: str, changeset_revision: str, tool_shed_url: str, remove_from_disk: bool = True ) -> Dict[str, Any]: """ Uninstalls a specified repository revision from this Galaxy instance. :type name: str :param name: The name of the repository :type owner: str :param owner: The owner of the repository :type changeset_revision: str :param changeset_revision: The revision of the repository to uninstall :type tool_shed_url: str :param tool_shed_url: URL of the Tool Shed from which the repository was installed from (e.g., ``https://testtoolshed.g2.bx.psu.edu``) :type remove_from_disk: bool :param remove_from_disk: whether to also remove the repository from disk (the default) or only deactivate it :rtype: dict :return: If successful, a dictionary with a message noting the removal """ payload: Dict[str, Any] = { "tool_shed_url": tool_shed_url, "name": name, "owner": owner, "changeset_revision": changeset_revision, "remove_from_disk": remove_from_disk, } return self._delete(params=payload) bioblend-1.2.0/bioblend/galaxy/users/000077500000000000000000000000001444761704300175065ustar00rootroot00000000000000bioblend-1.2.0/bioblend/galaxy/users/__init__.py000066400000000000000000000205271444761704300216250ustar00rootroot00000000000000""" Contains possible interaction dealing with Galaxy users. Most of these methods must be executed by a registered Galaxy admin user. """ from typing import ( Any, Dict, List, Optional, TYPE_CHECKING, ) from bioblend import ConnectionError from bioblend.galaxy.client import Client if TYPE_CHECKING: from bioblend.galaxy import GalaxyInstance class UserClient(Client): module = "users" def __init__(self, galaxy_instance: "GalaxyInstance") -> None: super().__init__(galaxy_instance) def get_users( self, deleted: bool = False, f_email: Optional[str] = None, f_name: Optional[str] = None, f_any: Optional[str] = None, ) -> List[Dict[str, Any]]: """ Get a list of all registered users. If ``deleted`` is set to ``True``, get a list of deleted users. :type deleted: bool :param deleted: Whether to include deleted users :type f_email: str :param f_email: filter for user emails. The filter will be active for non-admin users only if the Galaxy instance has the ``expose_user_email`` option set to ``true`` in the ``config/galaxy.yml`` configuration file. :type f_name: str :param f_name: filter for user names. The filter will be active for non-admin users only if the Galaxy instance has the ``expose_user_name`` option set to ``true`` in the ``config/galaxy.yml`` configuration file. :type f_any: str :param f_any: filter for user email or name. Each filter will be active for non-admin users only if the Galaxy instance has the corresponding ``expose_user_*`` option set to ``true`` in the ``config/galaxy.yml`` configuration file. :rtype: list :return: a list of dicts with user details. For example:: [{'email': 'a_user@example.org', 'id': 'dda47097d9189f15', 'url': '/api/users/dda47097d9189f15'}] """ params: Dict[str, Any] = {} if f_email: params["f_email"] = f_email if f_name: params["f_name"] = f_name if f_any: params["f_any"] = f_any return self._get(deleted=deleted, params=params) def show_user(self, user_id: str, deleted: bool = False) -> Dict[str, Any]: """ Display information about a user. :type user_id: str :param user_id: encoded user ID :type deleted: bool :param deleted: whether to return results for a deleted user :rtype: dict :return: a dictionary containing information about the user """ return self._get(id=user_id, deleted=deleted) def create_remote_user(self, user_email: str) -> Dict[str, Any]: """ Create a new Galaxy remote user. .. note:: This method works only if the Galaxy instance has the ``allow_user_creation`` and ``use_remote_user`` options set to ``true`` in the ``config/galaxy.yml`` configuration file. Also note that setting ``use_remote_user`` will require an upstream authentication proxy server; however, if you do not have one, access to Galaxy via a browser will not be possible. :type user_email: str :param user_email: email of the user to be created :rtype: dict :return: a dictionary containing information about the created user """ payload = { "remote_user_email": user_email, } return self._post(payload) def create_local_user(self, username: str, user_email: str, password: str) -> Dict[str, Any]: """ Create a new Galaxy local user. .. note:: This method works only if the Galaxy instance has the ``allow_user_creation`` option set to ``true`` and ``use_remote_user`` option set to ``false`` in the ``config/galaxy.yml`` configuration file. :type username: str :param username: username of the user to be created :type user_email: str :param user_email: email of the user to be created :type password: str :param password: password of the user to be created :rtype: dict :return: a dictionary containing information about the created user """ payload = { "username": username, "email": user_email, "password": password, } return self._post(payload) def get_current_user(self) -> Dict[str, Any]: """ Display information about the user associated with this Galaxy connection. :rtype: dict :return: a dictionary containing information about the current user """ url = self._make_url() + "/current" return self._get(url=url) def create_user_apikey(self, user_id: str) -> str: """ Create a new API key for a given user. :type user_id: str :param user_id: encoded user ID :rtype: str :return: the API key for the user """ url = self._make_url(user_id) + "/api_key" payload = { "user_id": user_id, } return self._post(payload, url=url) def delete_user(self, user_id: str, purge: bool = False) -> Dict[str, Any]: """ Delete a user. .. note:: This method works only if the Galaxy instance has the ``allow_user_deletion`` option set to ``true`` in the ``config/galaxy.yml`` configuration file. :type user_id: str :param user_id: encoded user ID :type purge: bool :param purge: if ``True``, also purge (permanently delete) the history :rtype: dict :return: a dictionary containing information about the deleted user """ params = {} if purge is True: params["purge"] = purge return self._delete(id=user_id, params=params) def get_user_apikey(self, user_id: str) -> str: """ Get the current API key for a given user. :type user_id: str :param user_id: encoded user ID :rtype: str :return: the API key for the user, or 'Not available.' if it doesn't exist yet. """ try: url = self._make_url(user_id) + "/api_key/detailed" return self._get(url=url)["key"] except ConnectionError as e: if e.status_code == 204: return "Not available." elif e.status_code != 404: raise # Galaxy 22.05 or earlier url = self._make_url(user_id) + "/api_key/inputs" return self._get(url=url)["inputs"][0]["value"] def get_or_create_user_apikey(self, user_id: str) -> str: """ Get the current API key for a given user, creating one if it doesn't exist yet. :type user_id: str :param user_id: encoded user ID :rtype: str :return: the API key for the user .. note:: This method works only on Galaxy 21.01 or later. """ url = self._make_url(user_id) + "/api_key" return self._get(url=url) def update_user(self, user_id: str, user_data: Optional[Dict] = None, **kwargs: Any) -> Dict[str, Any]: """ Update user information. You can either pass the attributes you want to change in the user_data dictionary, or provide them separately as keyword arguments. For attributes that cannot be expressed as keywords (e.g. extra_user_preferences use a `|` sign), pass them in user_data. :type user_id: str :param user_id: encoded user ID :type user_data: dict :param user_data: a dict containing the values to be updated, eg. { "username" : "newUsername", "email": "new@email" } :type username: str :param username: Replace user name with the given string :type email: str :param email: Replace user email with the given string :rtype: dict :return: details of the updated user """ if user_data is None: user_data = {} user_data.update(kwargs) url = self._make_url(user_id) + "/information/inputs" return self._put(url=url, payload=user_data, id=user_id) bioblend-1.2.0/bioblend/galaxy/visual/000077500000000000000000000000001444761704300176505ustar00rootroot00000000000000bioblend-1.2.0/bioblend/galaxy/visual/__init__.py000066400000000000000000000036641444761704300217720ustar00rootroot00000000000000""" Contains possible interactions with the Galaxy visualization """ from typing import ( Any, Dict, List, TYPE_CHECKING, ) from bioblend.galaxy.client import Client if TYPE_CHECKING: from bioblend.galaxy import GalaxyInstance class VisualClient(Client): module = "visualizations" def __init__(self, galaxy_instance: "GalaxyInstance") -> None: super().__init__(galaxy_instance) def get_visualizations(self) -> List[Dict[str, Any]]: """ Get the list of all visualizations. :rtype: list :return: A list of dicts with details on individual visualizations. For example:: [{'dbkey': 'eschColi_K12', 'id': 'df1c7c96fc427c2d', 'title': 'AVTest1', 'type': 'trackster', 'url': '/api/visualizations/df1c7c96fc427c2d'}, {'dbkey': 'mm9', 'id': 'a669f50f8bf55b02', 'title': 'Bam to Bigwig', 'type': 'trackster', 'url': '/api/visualizations/a669f50f8bf55b02'}] """ return self._get() def show_visualization(self, visual_id: str) -> Dict[str, Any]: """ Get details of a given visualization. :type visual_id: str :param visual_id: Encoded visualization ID :rtype: dict :return: A description of the given visualization. For example:: {'annotation': None, 'dbkey': 'mm9', 'id': '18df9134ea75e49c', 'latest_revision': { ... }, 'model_class': 'Visualization', 'revisions': ['aa90649bb3ec7dcb', '20622bc6249c0c71'], 'slug': 'visualization-for-grant-1', 'title': 'Visualization For Grant', 'type': 'trackster', 'url': '/u/azaron/v/visualization-for-grant-1', 'user_id': '21e4aed91386ca8b'} """ return self._get(id=visual_id) bioblend-1.2.0/bioblend/galaxy/workflows/000077500000000000000000000000001444761704300204025ustar00rootroot00000000000000bioblend-1.2.0/bioblend/galaxy/workflows/__init__.py000066400000000000000000000725611444761704300225260ustar00rootroot00000000000000""" Contains possible interactions with the Galaxy Workflows """ import json import os from typing import ( Any, Dict, List, Optional, TYPE_CHECKING, ) from typing_extensions import Literal from bioblend.galaxy.client import Client if TYPE_CHECKING: from bioblend.galaxy import GalaxyInstance InputsBy = Literal["step_index|step_uuid", "step_index", "step_id", "step_uuid", "name"] # type: ignore[name-defined] class WorkflowClient(Client): module = "workflows" def __init__(self, galaxy_instance: "GalaxyInstance") -> None: super().__init__(galaxy_instance) # the 'deleted' option is not available for workflows def get_workflows( self, workflow_id: Optional[str] = None, name: Optional[str] = None, published: bool = False ) -> List[Dict[str, Any]]: """ Get all workflows, or select a subset by specifying optional arguments for filtering (e.g. a workflow name). :type name: str :param name: Workflow name to filter on. :type published: bool :param published: if ``True``, return also published workflows :rtype: list :return: A list of workflow dicts. For example:: [{'id': '92c56938c2f9b315', 'name': 'Simple', 'url': '/api/workflows/92c56938c2f9b315'}] .. versionchanged:: 1.1.1 Using the deprecated ``workflow_id`` parameter now raises a ``ValueError`` exception. """ if workflow_id is not None: raise ValueError( "The workflow_id parameter has been removed, use the show_workflow() method to view details of a workflow for which you know the ID." ) params: Dict[str, Any] = {} if published: params["show_published"] = True workflows = self._get(params=params) if name is not None: workflows = [_ for _ in workflows if _["name"] == name] return workflows def show_workflow(self, workflow_id: str, version: Optional[int] = None) -> Dict[str, Any]: """ Display information needed to run a workflow. :type workflow_id: str :param workflow_id: Encoded workflow ID :type version: int :param version: Workflow version to show :rtype: dict :return: A description of the workflow and its inputs. For example:: {'id': '92c56938c2f9b315', 'inputs': {'23': {'label': 'Input Dataset', 'value': ''}}, 'name': 'Simple', 'url': '/api/workflows/92c56938c2f9b315'} """ params: Dict[str, Any] = {} if version is not None: params["version"] = version return self._get(id=workflow_id, params=params) def get_workflow_inputs(self, workflow_id: str, label: str) -> List[str]: """ Get a list of workflow input IDs that match the given label. If no input matches the given label, an empty list is returned. :type workflow_id: str :param workflow_id: Encoded workflow ID :type label: str :param label: label to filter workflow inputs on :rtype: list :return: list of workflow inputs matching the label query """ wf = self._get(id=workflow_id) inputs = wf["inputs"] return [id for id in inputs if inputs[id]["label"] == label] def import_workflow_dict(self, workflow_dict: Dict[str, Any], publish: bool = False) -> Dict[str, Any]: """ Imports a new workflow given a dictionary representing a previously exported workflow. :type workflow_dict: dict :param workflow_dict: dictionary representing the workflow to be imported :type publish: bool :param publish: if ``True`` the uploaded workflow will be published; otherwise it will be visible only by the user which uploads it (default) :rtype: dict :return: Information about the imported workflow. For example:: {'name': 'Training: 16S rRNA sequencing with mothur: main tutorial', 'tags': [], 'deleted': false, 'latest_workflow_uuid': '368c6165-ccbe-4945-8a3c-d27982206d66', 'url': '/api/workflows/94bac0a90086bdcf', 'number_of_steps': 44, 'published': false, 'owner': 'jane-doe', 'model_class': 'StoredWorkflow', 'id': '94bac0a90086bdcf'} """ payload = {"workflow": workflow_dict, "publish": publish} url = self._make_url() + "/upload" return self._post(url=url, payload=payload) def import_workflow_from_local_path(self, file_local_path: str, publish: bool = False) -> Dict[str, Any]: """ Imports a new workflow given the path to a file containing a previously exported workflow. :type file_local_path: str :param file_local_path: File to upload to the server for new workflow :type publish: bool :param publish: if ``True`` the uploaded workflow will be published; otherwise it will be visible only by the user which uploads it (default) :rtype: dict :return: Information about the imported workflow. For example:: {'name': 'Training: 16S rRNA sequencing with mothur: main tutorial', 'tags': [], 'deleted': false, 'latest_workflow_uuid': '368c6165-ccbe-4945-8a3c-d27982206d66', 'url': '/api/workflows/94bac0a90086bdcf', 'number_of_steps': 44, 'published': false, 'owner': 'jane-doe', 'model_class': 'StoredWorkflow', 'id': '94bac0a90086bdcf'} """ with open(file_local_path) as fp: workflow_json = json.load(fp) return self.import_workflow_dict(workflow_json, publish) def import_shared_workflow(self, workflow_id: str) -> Dict[str, Any]: """ Imports a new workflow from the shared published workflows. :type workflow_id: str :param workflow_id: Encoded workflow ID :rtype: dict :return: A description of the workflow. For example:: {'id': 'ee0e2b4b696d9092', 'model_class': 'StoredWorkflow', 'name': 'Super workflow that solves everything!', 'published': False, 'tags': [], 'url': '/api/workflows/ee0e2b4b696d9092'} """ payload = {"shared_workflow_id": workflow_id} url = self._make_url() return self._post(url=url, payload=payload) def export_workflow_dict(self, workflow_id: str, version: Optional[int] = None) -> Dict[str, Any]: """ Exports a workflow. :type workflow_id: str :param workflow_id: Encoded workflow ID :type version: int :param version: Workflow version to export :rtype: dict :return: Dictionary representing the requested workflow """ params: Dict[str, Any] = {} if version is not None: params["version"] = version url = "/".join((self._make_url(), "download", workflow_id)) return self._get(url=url, params=params) def export_workflow_to_local_path( self, workflow_id: str, file_local_path: str, use_default_filename: bool = True ) -> None: """ Exports a workflow in JSON format to a given local path. :type workflow_id: str :param workflow_id: Encoded workflow ID :type file_local_path: str :param file_local_path: Local path to which the exported file will be saved. (Should not contain filename if use_default_name=True) :type use_default_filename: bool :param use_default_filename: If the use_default_name parameter is True, the exported file will be saved as file_local_path/Galaxy-Workflow-%s.ga, where %s is the workflow name. If use_default_name is False, file_local_path is assumed to contain the full file path including filename. :rtype: None :return: None """ workflow_dict = self.export_workflow_dict(workflow_id) if use_default_filename: filename = f"Galaxy-Workflow-{workflow_dict['name']}.ga" file_local_path = os.path.join(file_local_path, filename) with open(file_local_path, "w") as fp: json.dump(workflow_dict, fp) def update_workflow(self, workflow_id: str, **kwargs: Any) -> Dict[str, Any]: """ Update a given workflow. :type workflow_id: str :param workflow_id: Encoded workflow ID :type workflow: dict :param workflow: dictionary representing the workflow to be updated :type name: str :param name: New name of the workflow :type annotation: str :param annotation: New annotation for the workflow :type menu_entry: bool :param menu_entry: Whether the workflow should appear in the user's menu :type tags: list of str :param tags: Replace workflow tags with the given list :type published: bool :param published: Whether the workflow should be published or unpublished :rtype: dict :return: Dictionary representing the updated workflow """ return self._put(payload=kwargs, id=workflow_id) def invoke_workflow( self, workflow_id: str, inputs: Optional[dict] = None, params: Optional[dict] = None, history_id: Optional[str] = None, history_name: Optional[str] = None, import_inputs_to_history: bool = False, replacement_params: Optional[dict] = None, allow_tool_state_corrections: bool = False, inputs_by: Optional[InputsBy] = None, parameters_normalized: bool = False, require_exact_tool_versions: bool = True, ) -> Dict[str, Any]: """ Invoke the workflow identified by ``workflow_id``. This will cause a workflow to be scheduled and return an object describing the workflow invocation. :type workflow_id: str :param workflow_id: Encoded workflow ID :type inputs: dict :param inputs: A mapping of workflow inputs to datasets and dataset collections. The datasets source can be a LibraryDatasetDatasetAssociation (``ldda``), LibraryDataset (``ld``), HistoryDatasetAssociation (``hda``), or HistoryDatasetCollectionAssociation (``hdca``). The map must be in the following format: ``{'': {'id': , 'src': '[ldda, ld, hda, hdca]'}}`` (e.g. ``{'2': {'id': '29beef4fadeed09f', 'src': 'hda'}}``) This map may also be indexed by the UUIDs of the workflow steps, as indicated by the ``uuid`` property of steps returned from the Galaxy API. Alternatively workflow steps may be addressed by the label that can be set in the workflow editor. If using uuid or label you need to also set the ``inputs_by`` parameter to ``step_uuid`` or ``name``. :type params: dict :param params: A mapping of non-datasets tool parameters (see below) :type history_id: str :param history_id: The encoded history ID where to store the workflow output. Alternatively, ``history_name`` may be specified to create a new history. :type history_name: str :param history_name: Create a new history with the given name to store the workflow output. If both ``history_id`` and ``history_name`` are provided, ``history_name`` is ignored. If neither is specified, a new 'Unnamed history' is created. :type import_inputs_to_history: bool :param import_inputs_to_history: If ``True``, used workflow inputs will be imported into the history. If ``False``, only workflow outputs will be visible in the given history. :type allow_tool_state_corrections: bool :param allow_tool_state_corrections: If True, allow Galaxy to fill in missing tool state when running workflows. This may be useful for workflows using tools that have changed over time or for workflows built outside of Galaxy with only a subset of inputs defined. :type replacement_params: dict :param replacement_params: pattern-based replacements for post-job actions (see below) :type inputs_by: str :param inputs_by: Determines how inputs are referenced. Can be "step_index|step_uuid" (default), "step_index", "step_id", "step_uuid", or "name". :type parameters_normalized: bool :param parameters_normalized: Whether Galaxy should normalize ``params`` to ensure everything is referenced by a numeric step ID. Default is ``False``, but when setting ``params`` for a subworkflow, ``True`` is required. :type require_exact_tool_versions: bool :param require_exact_tool_versions: Whether invocation should fail if Galaxy does not have the exact tool versions. Default is ``True``. Parameter does not any effect for Galaxy versions < 22.05. :rtype: dict :return: A dict containing the workflow invocation describing the scheduling of the workflow. For example:: {'history_id': '2f94e8ae9edff68a', 'id': 'df7a1f0c02a5b08e', 'inputs': {'0': {'id': 'a7db2fac67043c7e', 'src': 'hda', 'uuid': '7932ffe0-2340-4952-8857-dbaa50f1f46a'}}, 'model_class': 'WorkflowInvocation', 'state': 'ready', 'steps': [{'action': None, 'id': 'd413a19dec13d11e', 'job_id': None, 'model_class': 'WorkflowInvocationStep', 'order_index': 0, 'state': None, 'update_time': '2015-10-31T22:00:26', 'workflow_step_id': 'cbbbf59e8f08c98c', 'workflow_step_label': None, 'workflow_step_uuid': 'b81250fd-3278-4e6a-b269-56a1f01ef485'}, {'action': None, 'id': '2f94e8ae9edff68a', 'job_id': 'e89067bb68bee7a0', 'model_class': 'WorkflowInvocationStep', 'order_index': 1, 'state': 'new', 'update_time': '2015-10-31T22:00:26', 'workflow_step_id': '964b37715ec9bd22', 'workflow_step_label': None, 'workflow_step_uuid': 'e62440b8-e911-408b-b124-e05435d3125e'}], 'update_time': '2015-10-31T22:00:26', 'uuid': 'c8aa2b1c-801a-11e5-a9e5-8ca98228593c', 'workflow_id': '03501d7626bd192f'} The ``params`` dict should be specified as follows:: {STEP_ID: PARAM_DICT, ...} where PARAM_DICT is:: {PARAM_NAME: VALUE, ...} For backwards compatibility, the following (deprecated) format is also supported for ``params``:: {TOOL_ID: PARAM_DICT, ...} in which case PARAM_DICT affects all steps with the given tool id. If both by-tool-id and by-step-id specifications are used, the latter takes precedence. Finally (again, for backwards compatibility), PARAM_DICT can also be specified as:: {'param': PARAM_NAME, 'value': VALUE} Note that this format allows only one parameter to be set per step. For a ``repeat`` parameter, the names of the contained parameters needs to be specified as ``_|``, with the repeat index starting at 0. For example, if the tool XML contains:: then the PARAM_DICT should be something like:: {... "cutoff_0|name": "n_genes", "cutoff_0|min": "2", "cutoff_1|name": "n_counts", "cutoff_1|min": "4", ...} At the time of this writing, it is not possible to change the number of times the contained parameters are repeated. Therefore, the parameter indexes can go from 0 to n-1, where n is the number of times the repeated element was added when the workflow was saved in the Galaxy UI. The ``replacement_params`` dict should map parameter names in post-job actions (PJAs) to their runtime values. For instance, if the final step has a PJA like the following:: {'RenameDatasetActionout_file1': {'action_arguments': {'newname': '${output}'}, 'action_type': 'RenameDatasetAction', 'output_name': 'out_file1'}} then the following renames the output dataset to 'foo':: replacement_params = {'output': 'foo'} see also `this email thread `_. .. warning:: Historically, workflow invocation consumed a ``dataset_map`` data structure that was indexed by unencoded workflow step IDs. These IDs would not be stable across Galaxy instances. The new ``inputs`` property is instead indexed by either the ``order_index`` property (which is stable across workflow imports) or the step UUID which is also stable. """ payload: Dict[str, Any] = {} if inputs: payload["inputs"] = inputs if params: payload["parameters"] = params if replacement_params: payload["replacement_params"] = replacement_params if history_id: payload["history"] = f"hist_id={history_id}" elif history_name: payload["history"] = history_name if not import_inputs_to_history: payload["no_add_to_history"] = True if allow_tool_state_corrections: payload["allow_tool_state_corrections"] = allow_tool_state_corrections if inputs_by is not None: payload["inputs_by"] = inputs_by payload["require_exact_tool_versions"] = require_exact_tool_versions if parameters_normalized: payload["parameters_normalized"] = parameters_normalized url = self._invocations_url(workflow_id) return self._post(payload, url=url) def show_invocation(self, workflow_id: str, invocation_id: str) -> Dict[str, Any]: """ Get a workflow invocation object representing the scheduling of a workflow. This object may be sparse at first (missing inputs and invocation steps) and will become more populated as the workflow is actually scheduled. :type workflow_id: str :param workflow_id: Encoded workflow ID :type invocation_id: str :param invocation_id: Encoded workflow invocation ID :rtype: dict :return: The workflow invocation. For example:: {'history_id': '2f94e8ae9edff68a', 'id': 'df7a1f0c02a5b08e', 'inputs': {'0': {'id': 'a7db2fac67043c7e', 'src': 'hda', 'uuid': '7932ffe0-2340-4952-8857-dbaa50f1f46a'}}, 'model_class': 'WorkflowInvocation', 'state': 'ready', 'steps': [{'action': None, 'id': 'd413a19dec13d11e', 'job_id': None, 'model_class': 'WorkflowInvocationStep', 'order_index': 0, 'state': None, 'update_time': '2015-10-31T22:00:26', 'workflow_step_id': 'cbbbf59e8f08c98c', 'workflow_step_label': None, 'workflow_step_uuid': 'b81250fd-3278-4e6a-b269-56a1f01ef485'}, {'action': None, 'id': '2f94e8ae9edff68a', 'job_id': 'e89067bb68bee7a0', 'model_class': 'WorkflowInvocationStep', 'order_index': 1, 'state': 'new', 'update_time': '2015-10-31T22:00:26', 'workflow_step_id': '964b37715ec9bd22', 'workflow_step_label': None, 'workflow_step_uuid': 'e62440b8-e911-408b-b124-e05435d3125e'}], 'update_time': '2015-10-31T22:00:26', 'uuid': 'c8aa2b1c-801a-11e5-a9e5-8ca98228593c', 'workflow_id': '03501d7626bd192f'} """ url = self._invocation_url(workflow_id, invocation_id) return self._get(url=url) def get_invocations(self, workflow_id: str) -> List[Dict[str, Any]]: """ Get a list containing all the workflow invocations corresponding to the specified workflow. For more advanced filtering use InvocationClient.get_invocations(). :type workflow_id: str :param workflow_id: Encoded workflow ID :rtype: list :return: A list of workflow invocations. For example:: [{'history_id': '2f94e8ae9edff68a', 'id': 'df7a1f0c02a5b08e', 'model_class': 'WorkflowInvocation', 'state': 'new', 'update_time': '2015-10-31T22:00:22', 'uuid': 'c8aa2b1c-801a-11e5-a9e5-8ca98228593c', 'workflow_id': '03501d7626bd192f'}] """ url = self._invocations_url(workflow_id) return self._get(url=url) def cancel_invocation(self, workflow_id: str, invocation_id: str) -> Dict[str, Any]: """ Cancel the scheduling of a workflow. :type workflow_id: str :param workflow_id: Encoded workflow ID :type invocation_id: str :param invocation_id: Encoded workflow invocation ID :rtype: dict :return: The workflow invocation being cancelled """ url = self._invocation_url(workflow_id, invocation_id) return self._delete(url=url) def show_invocation_step(self, workflow_id: str, invocation_id: str, step_id: str) -> Dict[str, Any]: """ See the details of a particular workflow invocation step. :type workflow_id: str :param workflow_id: Encoded workflow ID :type invocation_id: str :param invocation_id: Encoded workflow invocation ID :type step_id: str :param step_id: Encoded workflow invocation step ID :rtype: dict :return: The workflow invocation step. For example:: {'action': None, 'id': '63cd3858d057a6d1', 'job_id': None, 'model_class': 'WorkflowInvocationStep', 'order_index': 2, 'state': None, 'update_time': '2015-10-31T22:11:14', 'workflow_step_id': '52e496b945151ee8', 'workflow_step_label': None, 'workflow_step_uuid': '4060554c-1dd5-4287-9040-8b4f281cf9dc'} """ url = self._invocation_step_url(workflow_id, invocation_id, step_id) return self._get(url=url) def run_invocation_step_action( self, workflow_id: str, invocation_id: str, step_id: str, action: Any ) -> Dict[str, Any]: """Execute an action for an active workflow invocation step. The nature of this action and what is expected will vary based on the the type of workflow step (the only currently valid action is True/False for pause steps). :type workflow_id: str :param workflow_id: Encoded workflow ID :type invocation_id: str :param invocation_id: Encoded workflow invocation ID :type step_id: str :param step_id: Encoded workflow invocation step ID :type action: object :param action: Action to use when updating state, semantics depends on step type. :rtype: dict :return: Representation of the workflow invocation step """ url = self._invocation_step_url(workflow_id, invocation_id, step_id) payload = {"action": action} return self._put(payload=payload, url=url) def delete_workflow(self, workflow_id: str) -> None: """ Delete a workflow identified by `workflow_id`. :type workflow_id: str :param workflow_id: Encoded workflow ID .. warning:: Deleting a workflow is irreversible in Galaxy versions < 23.01 - all workflow data will be permanently deleted. """ self._delete(id=workflow_id) def refactor_workflow( self, workflow_id: str, actions: List[Dict[str, Any]], dry_run: bool = False ) -> Dict[str, Any]: """ Refactor workflow with given actions. :type workflow_id: str :param workflow_id: Encoded workflow ID :type actions: list of dicts :param actions: Actions to use for refactoring the workflow. The following actions are supported: update_step_label, update_step_position, update_output_label, update_name, update_annotation, update_license, update_creator, update_report, add_step, add_input, disconnect, connect, fill_defaults, fill_step_defaults, extract_input, extract_legacy_parameter, remove_unlabeled_workflow_outputs, upgrade_all_steps, upgrade_subworkflow, upgrade_tool. An example value for the ``actions`` argument might be:: actions = [ {"action_type": "add_input", "type": "data", "label": "foo"}, {"action_type": "update_step_label", "label": "bar", "step": {"label": "foo"}}, ] :type dry_run: bool :param dry_run: When true, perform a dry run where the existing workflow is preserved. The refactored workflow is returned in the output of the method, but not saved on the Galaxy server. :rtype: dict :return: Dictionary containing logged messages for the executed actions and the refactored workflow. """ payload = { "actions": actions, "dry_run": dry_run, } url = "/".join((self._make_url(workflow_id), "refactor")) return self._put(payload=payload, url=url) def extract_workflow_from_history( self, history_id: str, workflow_name: str, job_ids: Optional[List[str]] = None, dataset_hids: Optional[List[str]] = None, dataset_collection_hids: Optional[List[str]] = None, ) -> Dict[str, Any]: """ Extract a workflow from a history. :type history_id: str :param history_id: Encoded history ID :type workflow_name: str :param workflow_name: Name of the workflow to create :type job_ids: list :param job_ids: Optional list of job IDs to filter the jobs to extract from the history :type dataset_hids: list :param dataset_hids: Optional list of dataset hids corresponding to workflow inputs when extracting a workflow from history :type dataset_collection_hids: list :param dataset_collection_hids: Optional list of dataset collection hids corresponding to workflow inputs when extracting a workflow from history :rtype: dict :return: A description of the created workflow """ payload = { "from_history_id": history_id, "job_ids": job_ids if job_ids else [], "dataset_ids": dataset_hids if dataset_hids else [], "dataset_collection_ids": dataset_collection_hids if dataset_collection_hids else [], "workflow_name": workflow_name, } return self._post(payload=payload) def show_versions(self, workflow_id: str) -> List[Dict[str, Any]]: """ Get versions for a workflow. :type workflow_id: str :param workflow_id: Encoded workflow ID :rtype: list of dicts :return: Ordered list of version descriptions for this workflow """ url = self._make_url(workflow_id) + "/versions" return self._get(url=url) def _invocation_step_url(self, workflow_id: str, invocation_id: str, step_id: str) -> str: return "/".join((self._invocation_url(workflow_id, invocation_id), "steps", step_id)) def _invocation_url(self, workflow_id: str, invocation_id: str) -> str: return "/".join((self._invocations_url(workflow_id), invocation_id)) def _invocations_url(self, workflow_id: str) -> str: return "/".join((self._make_url(workflow_id), "invocations")) __all__ = ("WorkflowClient",) bioblend-1.2.0/bioblend/galaxyclient.py000066400000000000000000000316221444761704300201220ustar00rootroot00000000000000""" Helper class for Galaxy and ToolShed Instance object This class is primarily a helper for the library and user code should not use it directly. A base representation of an instance """ import base64 import contextlib import json import logging from typing import ( Any, Optional, ) import requests import tusclient.client import tusclient.exceptions import tusclient.storage.filestorage import tusclient.uploader from requests_toolbelt import MultipartEncoder from bioblend import ConnectionError from bioblend.util import FileStream log = logging.getLogger(__name__) UPLOAD_CHUNK_SIZE = 10**7 class GalaxyClient: def __init__( self, url: str, key: Optional[str] = None, email: Optional[str] = None, password: Optional[str] = None, verify: bool = True, timeout: Optional[float] = None, ) -> None: """ :param verify: Whether to verify the server's TLS certificate :type verify: bool :param timeout: Timeout for requests operations, set to None for no timeout (the default). :type timeout: float """ self.verify = verify self.timeout = timeout # Make sure the URL scheme is defined (otherwise requests will not work) if not url.lower().startswith("http"): found_scheme = None # Try to guess the scheme, starting from the more secure for scheme in ("https://", "http://"): log.warning(f"Missing scheme in url, trying with {scheme}") with contextlib.suppress(requests.RequestException): r = requests.get( scheme + url, timeout=self.timeout, verify=self.verify, ) r.raise_for_status() found_scheme = scheme break else: raise ValueError(f"Missing scheme in url {url}") url = found_scheme + url self.base_url = url.rstrip("/") # All of Galaxy's and ToolShed's API's are rooted at /api so make that the url self.url = f"{self.base_url}/api" # If key has been supplied, use it; otherwise just set email and # password and grab user's key before first request. if key: self._key: Optional[str] = key else: self._key = None self.email = email self.password = password self.json_headers: dict = {"Content-Type": "application/json"} # json_headers needs to be set before key can be defined, otherwise authentication with email/password causes an error self.json_headers["x-api-key"] = self.key # Number of attempts before giving up on a GET request. self._max_get_attempts = 1 # Delay in seconds between subsequent retries. self._get_retry_delay = 10.0 @property def max_get_attempts(self) -> int: """ The maximum number of attempts for a GET request. Default: 1 """ return self._max_get_attempts @max_get_attempts.setter def max_get_attempts(self, value: int) -> None: """ Set the maximum number of attempts for GET requests. A value greater than one causes failed GET requests to be retried `value` - 1 times. """ if value < 1: raise ValueError(f"Number of attempts must be >= 1 (got: {value})") self._max_get_attempts = value @property def get_retry_delay(self) -> float: """ The delay (in seconds) to wait before retrying a failed GET request. Default: 10.0 """ return self._get_retry_delay @get_retry_delay.setter def get_retry_delay(self, value: float) -> None: """ Set the delay (in seconds) to wait before retrying a failed GET request. """ if value < 0: raise ValueError(f"Retry delay must be >= 0 (got: {value})") self._get_retry_delay = value def make_get_request(self, url: str, **kwargs: Any) -> requests.Response: """ Make a GET request using the provided ``url``. Keyword arguments are the same as in requests.request. If ``verify`` is not provided, ``self.verify`` will be used. :rtype: requests.Response :return: the response object. """ headers = self.json_headers kwargs.setdefault("timeout", self.timeout) kwargs.setdefault("verify", self.verify) r = requests.get(url, headers=headers, **kwargs) return r def make_post_request( self, url: str, payload: Optional[dict] = None, params: Optional[dict] = None, files_attached: bool = False ) -> Any: """ Make a POST request using the provided ``url`` and ``payload``. The ``payload`` must be a dict that contains the request values. The payload dict may contain file handles (in which case the files_attached flag must be set to true). :return: The decoded response. """ def my_dumps(d: dict) -> dict: """ Apply ``json.dumps()`` to the values of the dict ``d`` if they are not of type ``FileStream``. """ for k, v in d.items(): if not isinstance(v, (FileStream, str, bytes)): d[k] = json.dumps(v) return d # Compute data, headers, params arguments for request.post, # leveraging the requests-toolbelt library if any files have # been attached. if files_attached: payload_copy = payload.copy() if payload is not None else {} if params: payload_copy.update(params) data = MultipartEncoder(fields=my_dumps(payload_copy)) headers = self.json_headers.copy() headers["Content-Type"] = data.content_type post_params = None else: data = json.dumps(payload) if payload is not None else None headers = self.json_headers post_params = params r = requests.post( url, params=post_params, data=data, headers=headers, timeout=self.timeout, allow_redirects=False, verify=self.verify, ) if r.status_code == 200: try: return r.json() except Exception as e: raise ConnectionError( f"Request was successful, but cannot decode the response content: {e}", body=r.content, status_code=r.status_code, ) # @see self.body for HTTP response body raise ConnectionError( f"Unexpected HTTP status code: {r.status_code}", body=r.text, status_code=r.status_code, ) def make_delete_request( self, url: str, payload: Optional[dict] = None, params: Optional[dict] = None ) -> requests.Response: """ Make a DELETE request using the provided ``url`` and the optional arguments. :type payload: dict :param payload: a JSON-serializable dictionary :rtype: requests.Response :return: the response object. """ data = json.dumps(payload) if payload is not None else None headers = self.json_headers r = requests.delete( url, params=params, data=data, headers=headers, timeout=self.timeout, allow_redirects=False, verify=self.verify, ) return r def make_put_request(self, url: str, payload: Optional[dict] = None, params: Optional[dict] = None) -> Any: """ Make a PUT request using the provided ``url`` with required payload. :type payload: dict :param payload: a JSON-serializable dictionary :return: The decoded response. """ data = json.dumps(payload) if payload is not None else None headers = self.json_headers r = requests.put( url, params=params, data=data, headers=headers, timeout=self.timeout, allow_redirects=False, verify=self.verify, ) if r.status_code == 200: try: return r.json() except Exception as e: raise ConnectionError( f"Request was successful, but cannot decode the response content: {e}", body=r.content, status_code=r.status_code, ) # @see self.body for HTTP response body raise ConnectionError( f"Unexpected HTTP status code: {r.status_code}", body=r.text, status_code=r.status_code, ) def make_patch_request(self, url: str, payload: Optional[dict] = None, params: Optional[dict] = None) -> Any: """ Make a PATCH request using the provided ``url`` with required payload. :type payload: dict :param payload: a JSON-serializable dictionary :return: The decoded response. """ data = json.dumps(payload) if payload is not None else None headers = self.json_headers r = requests.patch( url, params=params, data=data, headers=headers, timeout=self.timeout, allow_redirects=False, verify=self.verify, ) if r.status_code == 200: try: return r.json() except Exception as e: raise ConnectionError( f"Request was successful, but cannot decode the response content: {e}", body=r.content, status_code=r.status_code, ) # @see self.body for HTTP response body raise ConnectionError( f"Unexpected HTTP status code: {r.status_code}", body=r.text, status_code=r.status_code, ) def get_tus_uploader( self, path: str, url: str = "/upload/resumable_upload", storage: Optional[str] = None, metadata: Optional[dict] = None, chunk_size: Optional[int] = UPLOAD_CHUNK_SIZE, ) -> tusclient.uploader.Uploader: """ Return the tus client uploader object for uploading to the Galaxy tus endpoint :type path: str :param path: path of the file to upload :type url: str :param url: URL (relative to base URL) of the upload endpoint :type storage: str :param storage: Local path to store URLs resuming uploads :type metadata: dict :param metadata: Metadata to send with upload request :type chunk_size: int :param chunk_size: Number of bytes to send in each chunk :rtype: tusclient.uploader.Uploader :return: tus uploader object """ headers = {"x-api-key": self.key} client = tusclient.client.TusClient(self.url + url, headers=headers) if storage: storage = tusclient.storage.filestorage.FileStorage(storage) try: return client.uploader( file_path=path, chunk_size=chunk_size, metadata=metadata, store_url=storage is not None, url_storage=storage, ) except tusclient.exceptions.TusCommunicationError as exc: raise ConnectionError( f"Unexpected HTTP status code: {exc.status_code}", body=str(exc), status_code=exc.status_code, ) @property def key(self) -> Optional[str]: if not self._key and self.email is not None and self.password is not None: unencoded_credentials = f"{self.email}:{self.password}" authorization = base64.b64encode(unencoded_credentials.encode()) headers = self.json_headers.copy() headers["Authorization"] = authorization auth_url = f"{self.url}/authenticate/baseauth" # Use lower level method instead of make_get_request() because we # need the additional Authorization header. r = requests.get( auth_url, headers=headers, timeout=self.timeout, verify=self.verify, ) if r.status_code != 200: raise Exception("Failed to authenticate user.") response = r.json() if isinstance(response, str): # bug in Tool Shed response = json.loads(response) self._key = response["api_key"] return self._key def _tus_uploader_session_id(self: tusclient.uploader.Uploader) -> str: return self.url.rsplit("/", 1)[1] # monkeypatch a session_id property on to uploader tusclient.uploader.Uploader.session_id = property(_tus_uploader_session_id) bioblend-1.2.0/bioblend/py.typed000066400000000000000000000000001444761704300165450ustar00rootroot00000000000000bioblend-1.2.0/bioblend/toolshed/000077500000000000000000000000001444761704300167015ustar00rootroot00000000000000bioblend-1.2.0/bioblend/toolshed/__init__.py000066400000000000000000000045001444761704300210110ustar00rootroot00000000000000""" A base representation of an instance of Tool Shed """ from typing import Optional from bioblend.galaxyclient import GalaxyClient from bioblend.toolshed import ( categories, repositories, tools, ) class ToolShedInstance(GalaxyClient): def __init__( self, url: str, key: Optional[str] = None, email: Optional[str] = None, password: Optional[str] = None, verify: bool = True, ) -> None: """ A base representation of a connection to a ToolShed instance, identified by the ToolShed URL and user credentials. After you have created a ``ToolShedInstance`` object, access various modules via the class fields. For example, to work with repositories and get a list of all public repositories, the following should be done:: from bioblend import toolshed ts = toolshed.ToolShedInstance(url='https://testtoolshed.g2.bx.psu.edu') rl = ts.repositories.get_repositories() tools = ts.tools.search_tools('fastq') :type url: str :param url: A FQDN or IP for a given instance of ToolShed. For example: https://testtoolshed.g2.bx.psu.edu . If a ToolShed instance is served under a prefix (e.g. http://127.0.0.1:8080/toolshed/), supply the entire URL including the prefix (note that the prefix must end with a slash). :type key: str :param key: If required, user's API key for the given instance of ToolShed, obtained from the user preferences. :type email: str :param email: ToolShed e-mail address corresponding to the user. Ignored if key is supplied directly. :type password: str :param password: Password of ToolShed account corresponding to the above e-mail address. Ignored if key is supplied directly. :param verify: Whether to verify the server's TLS certificate :type verify: bool """ super().__init__(url, key, email, password, verify=verify) self.categories = categories.ToolShedCategoryClient(self) self.repositories = repositories.ToolShedRepositoryClient(self) self.tools = tools.ToolShedToolClient(self) bioblend-1.2.0/bioblend/toolshed/categories/000077500000000000000000000000001444761704300210265ustar00rootroot00000000000000bioblend-1.2.0/bioblend/toolshed/categories/__init__.py000066400000000000000000000130131444761704300231350ustar00rootroot00000000000000""" Interaction with a Tool Shed instance categories """ from typing import ( Any, Dict, List, TYPE_CHECKING, ) from typing_extensions import Literal from bioblend.galaxy.client import Client if TYPE_CHECKING: from bioblend.toolshed import ToolShedInstance class ToolShedCategoryClient(Client): module = "categories" def __init__(self, toolshed_instance: "ToolShedInstance") -> None: super().__init__(toolshed_instance) def get_categories(self, deleted: bool = False) -> List[Dict[str, Any]]: """ Returns a list of dictionaries that contain descriptions of the repository categories found on the given Tool Shed instance. :type deleted: bool :param deleted: whether to show deleted categories. Requires administrator access to the Tool Shed instance. :rtype: list :return: A list of dictionaries containing information about repository categories present in the Tool Shed. For example:: [{'deleted': False, 'description': 'Tools for manipulating data', 'id': '175812cd7caaf439', 'model_class': 'Category', 'name': 'Text Manipulation', 'url': '/api/categories/175812cd7caaf439'}] .. versionadded:: 0.5.2 """ return self._get(deleted=deleted) def show_category(self, category_id: str) -> Dict[str, Any]: """ Get details of a given category. :type category_id: str :param category_id: Encoded category ID :rtype: dict :return: details of the given category """ return self._get(id=category_id) def get_repositories( self, category_id: str, sort_key: Literal["name", "owner"] = "name", sort_order: Literal["asc", "desc"] = "asc" ) -> Dict[str, Any]: """ Returns a dictionary of information for a repository category including a list of repositories belonging to the category. :type category_id: str :param category_id: Encoded category ID :type sort_key: str :param sort_key: key for sorting. Options are 'name' or 'owner' (default 'name'). :type sort_order: str :param sort_order: ordering of sorted output. Options are 'asc' or 'desc' (default 'asc'). :rtype: dict :return: A dict containing information about the category including a list of repository dicts. For example:: {'deleted': False, 'description': 'Tools for constructing and analyzing 3-dimensional shapes and ' 'their properties', 'id': '589548af7e391bcf', 'model_class': 'Category', 'name': 'Constructive Solid Geometry', 'repositories': [{'create_time': '2016-08-23T18:53:23.845013', 'deleted': False, 'deprecated': False, 'description': 'Adds a surface field to a selected shape ' 'based on a given mathematical expression', 'homepage_url': 'https://github.com/gregvonkuster/galaxy-csg', 'id': 'af2ccc53697b064c', 'metadata': {'0:e12b55e960de': {'changeset_revision': 'e12b55e960de', 'downloadable': True, 'has_repository_dependencies': False, 'id': 'dfe022067783215f', 'includes_datatypes': False, 'includes_tool_dependencies': False, 'includes_tools': True, 'includes_tools_for_display_in_tool_panel': True, 'includes_workflows': False, 'malicious': False, 'missing_test_components': False, 'model_class': 'RepositoryMetadata', 'numeric_revision': 0, 'repository_id': 'af2ccc53697b064c'}}, 'model_class': 'Repository', 'name': 'icqsol_add_surface_field_from_expression', 'owner': 'iuc', 'private': False, 'remote_repository_url': 'https://github.com/gregvonkuster/galaxy-csg', 'times_downloaded': 152, 'type': 'unrestricted', 'user_id': 'b563abc230aa8fd0'}, # ... ], 'repository_count': 11, 'url': '/api/categories/589548af7e391bcf'} """ params: Dict[str, Any] = {} if sort_key: params.update({"sort_key": sort_key}) if sort_order: params.update({"sort_order": sort_order}) url = self._make_url(category_id) + "/repositories" return self._get(url=url, params=params) bioblend-1.2.0/bioblend/toolshed/repositories/000077500000000000000000000000001444761704300214305ustar00rootroot00000000000000bioblend-1.2.0/bioblend/toolshed/repositories/__init__.py000066400000000000000000000551551444761704300235540ustar00rootroot00000000000000""" Interaction with a Tool Shed instance repositories """ from typing import ( Any, Dict, List, Optional, TYPE_CHECKING, Union, ) from typing_extensions import Literal from bioblend.galaxy.client import Client from bioblend.util import attach_file if TYPE_CHECKING: from bioblend.toolshed import ToolShedInstance class ToolShedRepositoryClient(Client): module = "repositories" def __init__(self, toolshed_instance: "ToolShedInstance") -> None: super().__init__(toolshed_instance) def get_repositories(self, name: Optional[str] = None, owner: Optional[str] = None) -> List[Dict[str, Any]]: """ Get all repositories in a Galaxy Tool Shed, or select a subset by specifying optional arguments for filtering (e.g. a repository name). :type name: str :param name: Repository name to filter on. :type owner: str :param owner: Repository owner to filter on. :rtype: list :return: Returns a list of dictionaries containing information about repositories present in the Tool Shed. For example:: [{'category_ids': ['c1df3132f6334b0e', 'f6d7b0037d901d9b'], 'create_time': '2020-02-09T16:24:37.098176', 'deleted': False, 'deprecated': False, 'description': 'Order Contigs', 'homepage_url': '', 'id': '287bd69f724b99ce', 'model_class': 'Repository', 'name': 'best_tool_ever', 'owner': 'billybob', 'private': False, 'remote_repository_url': '', 'times_downloaded': 0, 'type': 'unrestricted', 'user_id': '5cefd48bc04af6d4'}] .. versionchanged:: 0.4.1 Changed method name from ``get_tools`` to ``get_repositories`` to better align with the Tool Shed concepts. """ params = {} if name: params["name"] = name if owner: params["owner"] = owner return self._get(params=params) def search_repositories( self, q: str, page: int = 1, page_size: int = 10, ) -> Dict[str, Any]: """ Search for repositories in a Galaxy Tool Shed. :type q: str :param q: query string for searching purposes :type page: int :param page: page requested :type page_size: int :param page_size: page size requested :rtype: dict :return: dictionary containing search hits as well as metadata for the search. For example:: {'hits': [{'matched_terms': [], 'repository': {'approved': 'no', 'categories': 'fastq manipulation', 'description': 'Convert export file to fastq', 'full_last_updated': '2015-01-18 09:48 AM', 'homepage_url': '', 'id': 'bdfa208f0cf6504e', 'last_updated': 'less than a year', 'long_description': 'This is a simple too to convert Solexas Export files to FASTQ files.', 'name': 'export_to_fastq', 'remote_repository_url': '', 'repo_lineage': "['0:c9e926d9d87e', '1:38859774da87']" 'repo_owner_username': 'louise', 'times_downloaded': 164}, 'score': 4.92}, {'matched_terms': [], 'repository': {'approved': 'no', 'categories': 'fastq manipulation', 'description': 'Convert BAM file to fastq', 'full_last_updated': '2015-04-07 11:57 AM', 'homepage_url': '', 'id': '175812cd7caaf439', 'last_updated': 'less than a month', 'long_description': 'Use Picards SamToFastq to convert a BAM file to fastq. Useful for storing reads as BAM in Galaxy and converting to fastq when needed for analysis.', 'name': 'bam_to_fastq', 'remote_repository_url': '', 'repo_lineage': "['0:a0af255e28c1', '1:2523cb0fb84c', '2:2656247b5253']" 'repo_owner_username': 'brad-chapman', 'times_downloaded': 138}, 'score': 4.14}], 'hostname': 'https://testtoolshed.g2.bx.psu.edu/', 'page': '1', 'page_size': '2', 'total_results': '64'} """ params = dict(q=q, page=page, page_size=page_size) return self._get(params=params) def show_repository( self, toolShed_id: str, ) -> Dict[str, Any]: """ Display information of a repository from Tool Shed :type toolShed_id: str :param toolShed_id: Encoded Tool Shed ID :rtype: dict :return: Information about the tool. For example:: {'category_ids': ['c1df3132f6334b0e', 'f6d7b0037d901d9b'], 'create_time': '2020-02-22T20:39:15.548491', 'deleted': False, 'deprecated': False, 'description': 'Order Contigs', 'homepage_url': '', 'id': '287bd69f724b99ce', 'long_description': '', 'model_class': 'Repository', 'name': 'best_tool_ever', 'owner': 'billybob', 'private': False, 'remote_repository_url': '', 'times_downloaded': 0, 'type': 'unrestricted', 'user_id': '5cefd48bc04af6d4'} .. versionchanged:: 0.4.1 Changed method name from ``show_tool`` to ``show_repository`` to better align with the Tool Shed concepts. """ return self._get(id=toolShed_id) def get_ordered_installable_revisions( self, name: str, owner: str, ) -> List[str]: """ Returns the ordered list of changeset revision hash strings that are associated with installable revisions. As in the changelog, the list is ordered oldest to newest. :type name: str :param name: the name of the repository :type owner: str :param owner: the owner of the repository :rtype: list :return: List of changeset revision hash strings from oldest to newest """ url = self._make_url() + "/get_ordered_installable_revisions" params = {"name": name, "owner": owner} r = self._get(url=url, params=params) return r def get_repository_revision_install_info( self, name: str, owner: str, changeset_revision: str, ) -> List[Dict[str, Any]]: """ Return a list of dictionaries of metadata about a certain changeset revision for a single tool. :type name: str :param name: the name of the repository :type owner: str :param owner: the owner of the repository :type changeset_revision: str :param changeset_revision: the changeset_revision of the RepositoryMetadata object associated with the repository :rtype: List of dictionaries :return: Returns a list of the following dictionaries: #. a dictionary defining the repository #. a dictionary defining the repository revision (RepositoryMetadata) #. a dictionary including the additional information required to install the repository For example:: [{'create_time': '2020-08-20T13:17:08.818518', 'deleted': False, 'deprecated': False, 'description': 'Galaxy Freebayes Bayesian genetic variant detector tool', 'homepage_url': '', 'id': '491b7a3fddf9366f', 'long_description': 'Galaxy Freebayes Bayesian genetic variant detector tool originally included in the Galaxy code distribution but migrated to the tool shed.', 'model_class': 'Repository', 'name': 'freebayes', 'owner': 'devteam', 'private': False, 'remote_repository_url': '', 'times_downloaded': 269, 'type': 'unrestricted', 'url': '/api/repositories/491b7a3fddf9366f', 'user_id': '1de29d50c3c44272'}, {'changeset_revision': 'd291dc763c4c', 'do_not_test': False, 'downloadable': True, 'has_repository_dependencies': False, 'id': '504be8aaa652c154', 'includes_datatypes': False, 'includes_tool_dependencies': True, 'includes_tools': True, 'includes_tools_for_display_in_tool_panel': True, 'includes_workflows': False, 'malicious': False, 'missing_test_components': False, 'model_class': 'RepositoryMetadata', 'numeric_revision': 0, 'repository_id': '491b7a3fddf9366f', 'url': '/api/repository_revisions/504be8aaa652c154'}, 'valid_tools': [{'add_to_tool_panel': True, 'description': '- Bayesian genetic variant detector', 'guid': 'testtoolshed.g2.bx.psu.edu/repos/devteam/freebayes/freebayes/0.0.3', 'id': 'freebayes', 'name': 'FreeBayes', 'requirements': [{'name': 'freebayes', 'type': 'package', 'version': '0.9.6_9608597d12e127c847ae03aa03440ab63992fedf'}, {'name': 'samtools', 'type': 'package', 'version': '0.1.18'}], 'tests': [{'inputs': [['reference_source|reference_source_selector', 'history'], ['options_type|options_type_selector', 'basic'], ['reference_source|ref_file', 'phiX.fasta'], ['reference_source|input_bams_0|input_bam', 'fake_phiX_reads_1.bam']], 'name': 'Test-1', 'outputs': [['output_vcf', 'freebayes_out_1.vcf.contains']], 'required_files': ['fake_phiX_reads_1.bam', 'phiX.fasta', 'freebayes_out_1.vcf.contains']}], 'tool_config': '/srv/toolshed/test/var/data/repos/000/repo_708/freebayes.xml', 'tool_type': 'default', 'version': '0.0.3', 'version_string_cmd': None}]}, {'freebayes': ['Galaxy Freebayes Bayesian genetic variant detector tool', 'http://testtoolshed.g2.bx.psu.edu/repos/devteam/freebayes', 'd291dc763c4c', '9', 'devteam', {}, {'freebayes/0.9.6_9608597d12e127c847ae03aa03440ab63992fedf': {'changeset_revision': 'd291dc763c4c', 'name': 'freebayes', 'repository_name': 'freebayes', 'repository_owner': 'devteam', 'type': 'package', 'version': '0.9.6_9608597d12e127c847ae03aa03440ab63992fedf'}, 'samtools/0.1.18': {'changeset_revision': 'd291dc763c4c', 'name': 'samtools', 'repository_name': 'freebayes', 'repository_owner': 'devteam', 'type': 'package', 'version': '0.1.18'}}]}] """ url = self._make_url() + "/get_repository_revision_install_info" params = {"name": name, "owner": owner, "changeset_revision": changeset_revision} return self._get(url=url, params=params) def repository_revisions( self, downloadable: Optional[bool] = None, malicious: Optional[bool] = None, missing_test_components: Optional[bool] = None, includes_tools: Optional[bool] = None, ) -> List[Dict[str, Any]]: """ Returns a (possibly filtered) list of dictionaries that include information about all repository revisions. The following parameters can be used to filter the list. :type downloadable: bool :param downloadable: Can the tool be downloaded :type malicious: bool :param malicious: :type missing_test_components: bool :param missing_test_components: :type includes_tools: bool :param includes_tools: :rtype: List of dictionaries :return: Returns a (possibly filtered) list of dictionaries that include information about all repository revisions. For example:: [{'changeset_revision': '6e26c5a48e9a', 'downloadable': True, 'has_repository_dependencies': False, 'id': '92250afff777a169', 'includes_datatypes': False, 'includes_tool_dependencies': False, 'includes_tools': True, 'includes_tools_for_display_in_tool_panel': True, 'includes_workflows': False, 'malicious': False, 'missing_test_components': False, 'model_class': 'RepositoryMetadata', 'numeric_revision': None, 'repository_id': '78f2604ff5e65707', 'url': '/api/repository_revisions/92250afff777a169'}, {'changeset_revision': '15a54fa11ad7', 'downloadable': True, 'has_repository_dependencies': False, 'id': 'd3823c748ae2205d', 'includes_datatypes': False, 'includes_tool_dependencies': False, 'includes_tools': True, 'includes_tools_for_display_in_tool_panel': True, 'includes_workflows': False, 'malicious': False, 'missing_test_components': False, 'model_class': 'RepositoryMetadata', 'numeric_revision': None, 'repository_id': 'f9662009da7bfce0', 'url': '/api/repository_revisions/d3823c748ae2205d'}] """ # Not using '_make_url' or '_get' to create url since the module id used # to create url is not the same as needed for this method url = self.gi.url + "/repository_revisions" params = {} if downloadable is not None: params["downloadable"] = downloadable if malicious is not None: params["malicious"] = malicious if missing_test_components is not None: params["missing_test_components"] = missing_test_components if includes_tools is not None: params["includes_tools"] = includes_tools return self._get(url=url, params=params) def show_repository_revision( self, metadata_id: str, ) -> Dict[str, Any]: """ Returns a dictionary that includes information about a specified repository revision. :type metadata_id: str :param metadata_id: Encoded repository metadata ID :rtype: dict :return: Returns a dictionary that includes information about a specified repository revision. For example:: {'changeset_revision': '7602de1e7f32', 'downloadable': True, 'has_repository_dependencies': False, 'id': '504be8aaa652c154', 'includes_datatypes': False, 'includes_tool_dependencies': False, 'includes_tools': True, 'includes_tools_for_display_in_tool_panel': True, 'includes_workflows': False, 'malicious': False, 'missing_test_components': True, 'model_class': 'RepositoryMetadata', 'numeric_revision': None, 'repository_dependencies': [], 'repository_id': '491b7a3fddf9366f', 'url': '/api/repository_revisions/504be8aaa652c154'} """ # Not using '_make_url' or '_get' to create url since the module id used # to create url is not the same as needed for this method # since metadata_id has to be defined, easy to create the url here url = "/".join((self.gi.url, "repository_revisions", metadata_id)) return self._get(url=url) def update_repository(self, id: str, tar_ball_path: str, commit_message: Optional[str] = None) -> Dict[str, Any]: """ Update the contents of a Tool Shed repository with specified tar ball. :type id: str :param id: Encoded repository ID :type tar_ball_path: str :param tar_ball_path: Path to file containing tar ball to upload. :type commit_message: str :param commit_message: Commit message used for the underlying Mercurial repository backing Tool Shed repository. :rtype: dict :return: Returns a dictionary that includes repository content warnings. Most valid uploads will result in no such warning and an exception will be raised generally if there are problems. For example a successful upload will look like:: {'content_alert': '', 'message': ''} .. versionadded:: 0.5.2 """ url = self._make_url(id) + "/changeset_revision" payload: Dict[str, Any] = {"file": attach_file(tar_ball_path)} if commit_message is not None: payload["commit_message"] = commit_message try: return self._post(payload=payload, files_attached=True, url=url) finally: payload["file"].close() def create_repository( self, name: str, synopsis: str, description: Optional[str] = None, type: Literal["unrestricted", "repository_suite_definition", "tool_dependency_definition"] = "unrestricted", remote_repository_url: Optional[str] = None, homepage_url: Optional[str] = None, category_ids: Optional[List[str]] = None, ) -> Dict[str, Any]: """ Create a new repository in a Tool Shed. :type name: str :param name: Name of the repository :type synopsis: str :param synopsis: Synopsis of the repository :type description: str :param description: Optional description of the repository :type type: str :param type: type of the repository. One of "unrestricted", "repository_suite_definition", or "tool_dependency_definition" :type remote_repository_url: str :param remote_repository_url: Remote URL (e.g. GitHub/Bitbucket repository) :type homepage_url: str :param homepage_url: Upstream's homepage for the project :type category_ids: list :param category_ids: List of encoded category IDs :rtype: dict :return: a dictionary containing information about the new repository. For example:: {"deleted": false, "deprecated": false, "description": "new_synopsis", "homepage_url": "https://github.com/galaxyproject/", "id": "8cf91205f2f737f4", "long_description": "this is some repository", "model_class": "Repository", "name": "new_repo_17", "owner": "qqqqqq", "private": false, "remote_repository_url": "https://github.com/galaxyproject/tools-devteam", "times_downloaded": 0, "type": "unrestricted", "user_id": "adb5f5c93f827949"} """ payload: Dict[str, Union[str, List[str]]] = { "name": name, "synopsis": synopsis, } if description is not None: payload["description"] = description if description is not None: payload["description"] = description if type is not None: payload["type"] = type if remote_repository_url is not None: payload["remote_repository_url"] = remote_repository_url if homepage_url is not None: payload["homepage_url"] = homepage_url if category_ids is not None: payload["category_ids[]"] = category_ids return self._post(payload) def update_repository_metadata( self, toolShed_id: str, name: Optional[str] = None, synopsis: Optional[str] = None, description: Optional[str] = None, remote_repository_url: Optional[str] = None, homepage_url: Optional[str] = None, category_ids: Optional[List[str]] = None, ) -> Dict[str, Any]: """ Update metadata of a Tool Shed repository. :type toolShed_id: str :param name: ID of the repository to update :type name: str :param name: New name of the repository :type synopsis: str :param synopsis: New synopsis of the repository :type description: str :param description: New description of the repository :type remote_repository_url: str :param remote_repository_url: New remote URL (e.g. GitHub/Bitbucket repository) :type homepage_url: str :param homepage_url: New upstream homepage for the project :type category_ids: list :param category_ids: New list of encoded category IDs :rtype: dict :return: a dictionary containing information about the updated repository. """ payload: Dict[str, Union[str, List[str]]] = {} if name: payload["name"] = name if synopsis: payload["synopsis"] = synopsis if description: payload["description"] = description if remote_repository_url: payload["remote_repository_url"] = remote_repository_url if homepage_url: payload["homepage_url"] = homepage_url if category_ids: payload["category_ids"] = category_ids return self._put(id=toolShed_id, payload=payload) bioblend-1.2.0/bioblend/toolshed/tools/000077500000000000000000000000001444761704300200415ustar00rootroot00000000000000bioblend-1.2.0/bioblend/toolshed/tools/__init__.py000066400000000000000000000037761444761704300221670ustar00rootroot00000000000000""" Interaction with a Tool Shed instance tools """ from typing import ( Any, Dict, TYPE_CHECKING, ) from bioblend.galaxy.client import Client if TYPE_CHECKING: from bioblend.toolshed import ToolShedInstance class ToolShedToolClient(Client): gi: "ToolShedInstance" module = "tools" def __init__(self, toolshed_instance: "ToolShedInstance") -> None: super().__init__(toolshed_instance) def search_tools(self, q: str, page: int = 1, page_size: int = 10) -> Dict[str, Any]: """ Search for tools in a Galaxy Tool Shed. :type q: str :param q: query string for searching purposes :type page: int :param page: page requested :type page_size: int :param page_size: page size requested :rtype: dict :return: dictionary containing search hits as well as metadata for the search. For example:: {'hits': [{'matched_terms': [], 'score': 3.0, 'tool': {'description': 'convert between various FASTQ quality formats', 'id': '69819b84d55f521efda001e0926e7233', 'name': 'FASTQ Groomer', 'repo_name': None, 'repo_owner_username': 'devteam'}}, {'matched_terms': [], 'score': 3.0, 'tool': {'description': 'converts a bam file to fastq files.', 'id': '521e282770fd94537daff87adad2551b', 'name': 'Defuse BamFastq', 'repo_name': None, 'repo_owner_username': 'jjohnson'}}], 'hostname': 'https://testtoolshed.g2.bx.psu.edu/', 'page': '1', 'page_size': '2', 'total_results': '118'} """ params = {"q": q, "page": page, "page_size": page_size} return self._get(params=params) bioblend-1.2.0/bioblend/util/000077500000000000000000000000001444761704300160355ustar00rootroot00000000000000bioblend-1.2.0/bioblend/util/__init__.py000066400000000000000000000027711444761704300201550ustar00rootroot00000000000000import os from typing import ( Any, IO, NamedTuple, Optional, Type, TypeVar, ) class FileStream(NamedTuple): name: str fd: IO def close(self) -> None: self.fd.close() def attach_file(path: str, name: Optional[str] = None) -> FileStream: """ Attach a path to a request payload object. :type path: str :param path: Path to file to attach to payload. :type name: str :param name: Name to give file, if different than actual pathname. :rtype: object :return: Returns an object compatible with requests post operation and capable of being closed with a ``close()`` method. """ if name is None: name = os.path.basename(path) return FileStream(name, open(path, "rb")) T = TypeVar("T") def abstractclass(decorated_cls: Type[T]) -> Type[T]: """ Decorator that marks a class as abstract even without any abstract method Adapted from https://stackoverflow.com/a/49013561/4503125 """ def clsnew(cls: Type[T], *args: Any, **kwargs: Any) -> T: # assert issubclass(cls, decorated_cls) if cls is decorated_cls: cls_name = getattr(decorated_cls, "__name__", str(decorated_cls)) raise TypeError(f"Can't instantiate abstract class {cls_name}") return super(decorated_cls, cls).__new__(cls) # type: ignore[misc] decorated_cls.__new__ = clsnew # type: ignore[assignment] return decorated_cls __all__ = ( "abstractclass", "attach_file", ) bioblend-1.2.0/docs/000077500000000000000000000000001444761704300142325ustar00rootroot00000000000000bioblend-1.2.0/docs/Makefile000066400000000000000000000126701444761704300157000ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Blend.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Blend.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/Blend" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Blend" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." bioblend-1.2.0/docs/_static/000077500000000000000000000000001444761704300156605ustar00rootroot00000000000000bioblend-1.2.0/docs/_static/.empty000066400000000000000000000000001444761704300170050ustar00rootroot00000000000000bioblend-1.2.0/docs/api_docs/000077500000000000000000000000001444761704300160135ustar00rootroot00000000000000bioblend-1.2.0/docs/api_docs/galaxy/000077500000000000000000000000001444761704300173005ustar00rootroot00000000000000bioblend-1.2.0/docs/api_docs/galaxy/all.rst000066400000000000000000000041241444761704300206030ustar00rootroot00000000000000============================================= API documentation for interacting with Galaxy ============================================= GalaxyInstance -------------- .. autoclass:: bioblend.galaxy.GalaxyInstance .. automethod:: bioblend.galaxy.GalaxyInstance.__init__ ----- .. _libraries-api: Config ------ .. automodule:: bioblend.galaxy.config ----- Datasets -------- .. automodule:: bioblend.galaxy.datasets ----- Dataset collections ------------------- .. automodule:: bioblend.galaxy.dataset_collections ----- Datatypes --------- .. automodule:: bioblend.galaxy.datatypes ----- Folders ------- .. automodule:: bioblend.galaxy.folders ----- Forms ----- .. automodule:: bioblend.galaxy.forms ----- FTP files --------- .. automodule:: bioblend.galaxy.ftpfiles ----- Genomes ------- .. automodule:: bioblend.galaxy.genomes Groups ------ .. automodule:: bioblend.galaxy.groups ----- Histories --------- .. automodule:: bioblend.galaxy.histories ----- Invocations ----------- .. automodule:: bioblend.galaxy.invocations ----- Jobs ---- .. automodule:: bioblend.galaxy.jobs ----- Libraries --------- .. automodule:: bioblend.galaxy.libraries ----- Quotas ------ .. automodule:: bioblend.galaxy.quotas ----- Roles ----- .. automodule:: bioblend.galaxy.roles ----- Tools ----- .. automodule:: bioblend.galaxy.tools ----- Tool data tables ---------------- .. automodule:: bioblend.galaxy.tool_data ----- Tool dependencies ----------------- .. automodule:: bioblend.galaxy.tool_dependencies ----- ToolShed -------- .. automodule:: bioblend.galaxy.toolshed ----- Users ----- .. automodule:: bioblend.galaxy.users ----- Visual ------ .. automodule:: bioblend.galaxy.visual ----- .. _workflows-api: Workflows --------- .. automodule:: bioblend.galaxy.workflows .. _objects-api: ========================== Object-oriented Galaxy API ========================== .. autoclass:: bioblend.galaxy.objects.galaxy_instance.GalaxyInstance Client ------ .. automodule:: bioblend.galaxy.objects.client Wrappers -------- .. automodule:: bioblend.galaxy.objects.wrappers bioblend-1.2.0/docs/api_docs/galaxy/docs.rst000066400000000000000000000464101444761704300207670ustar00rootroot00000000000000=================== Usage documentation =================== This page describes some sample use cases for the Galaxy API and provides examples for these API calls. In addition to this page, there are functional examples of complete scripts in the ``docs/examples`` directory of the BioBlend source code repository. Connect to a Galaxy server ~~~~~~~~~~~~~~~~~~~~~~~~~~ To connect to a running Galaxy server, you will need an account on that Galaxy instance and an API key for the account. Instructions on getting an API key can be found at https://galaxyproject.org/develop/api/ . To open a connection call:: from bioblend.galaxy import GalaxyInstance gi = GalaxyInstance(url='http://example.galaxy.url', key='your-API-key') We now have a ``GalaxyInstance`` object which allows us to interact with the Galaxy server under our account, and access our data. If the account is a Galaxy admin account we also will be able to use this connection to carry out admin actions. .. _view-histories-and-datasets: View Histories and Datasets ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Methods for accessing histories and datasets are grouped under ``GalaxyInstance.histories.*`` and ``GalaxyInstance.datasets.*`` respectively. To get information on the Histories currently in your account, call:: >>> gi.histories.get_histories() [{'id': 'f3c2b0f3ecac9f02', 'name': 'RNAseq_DGE_BASIC_Prep', 'url': '/api/histories/f3c2b0f3ecac9f02'}, {'id': '8a91dcf1866a80c2', 'name': 'June demo', 'url': '/api/histories/8a91dcf1866a80c2'}] This returns a list of dictionaries containing basic metadata, including the id and name of each History. In this case, we have two existing Histories in our account, 'RNAseq_DGE_BASIC_Prep' and 'June demo'. To get more detailed information about a History we can pass its id to the ``show_history`` method:: >>> gi.histories.show_history('f3c2b0f3ecac9f02', contents=False) {'annotation': '', 'contents_url': '/api/histories/f3c2b0f3ecac9f02/contents', 'id': 'f3c2b0f3ecac9f02', 'name': 'RNAseq_DGE_BASIC_Prep', 'nice_size': '93.5 MB', 'state': 'ok', 'state_details': {'discarded': 0, 'empty': 0, 'error': 0, 'failed_metadata': 0, 'new': 0, 'ok': 7, 'paused': 0, 'queued': 0, 'running': 0, 'setting_metadata': 0, 'upload': 0}, 'state_ids': {'discarded': [], 'empty': [], 'error': [], 'failed_metadata': [], 'new': [], 'ok': ['d6842fb08a76e351', '10a4b652da44e82a', '81c601a2549966a0', 'a154f05e3bcee26b', '1352fe19ddce0400', '06d549c52d753e53', '9ec54455d6279cc7'], 'paused': [], 'queued': [], 'running': [], 'setting_metadata': [], 'upload': []}} .. _example-dataset: This gives us a dictionary containing the History's metadata. With ``contents=False`` (the default), we only get a list of ids of the datasets contained within the History; with ``contents=True`` we would get metadata on each dataset. We can also directly access more detailed information on a particular dataset by passing its id to the ``show_dataset`` method:: >>> gi.datasets.show_dataset('10a4b652da44e82a') {'data_type': 'fastqsanger', 'deleted': False, 'file_size': 16527060, 'genome_build': 'dm3', 'id': 17499, 'metadata_data_lines': None, 'metadata_dbkey': 'dm3', 'metadata_sequences': None, 'misc_blurb': '15.8 MB', 'misc_info': 'Noneuploaded fastqsanger file', 'model_class': 'HistoryDatasetAssociation', 'name': 'C1_R2_1.chr4.fq', 'purged': False, 'state': 'ok', 'visible': True} Uploading Datasets to a History ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To upload a local file to a Galaxy server, you can run the ``upload_file`` method, supplying the path to a local file:: >>> gi.tools.upload_file('test.txt', 'f3c2b0f3ecac9f02') {'implicit_collections': [], 'jobs': [{'create_time': '2015-07-28T17:52:39.756488', 'exit_code': None, 'id': '9752b387803d3e1e', 'model_class': 'Job', 'state': 'new', 'tool_id': 'upload1', 'update_time': '2015-07-28T17:52:39.987509'}], 'output_collections': [], 'outputs': [{'create_time': '2015-07-28T17:52:39.331176', 'data_type': 'galaxy.datatypes.data.Text', 'deleted': False, 'file_ext': 'auto', 'file_size': 0, 'genome_build': '?', 'hda_ldda': 'hda', 'hid': 16, 'history_content_type': 'dataset', 'history_id': 'f3c2b0f3ecac9f02', 'id': '59c76a119581e190', 'metadata_data_lines': None, 'metadata_dbkey': '?', 'misc_blurb': None, 'misc_info': None, 'model_class': 'HistoryDatasetAssociation', 'name': 'test.txt', 'output_name': 'output0', 'peek': '
', 'purged': False, 'state': 'queued', 'tags': [], 'update_time': '2015-07-28T17:52:39.611887', 'uuid': 'ff0ee99b-7542-4125-802d-7a193f388e7e', 'visible': True}]} If files are greater than 2GB in size, they will need to be uploaded via FTP. Importing files from the user's FTP folder can be done via running the upload tool again:: >>> gi.tools.upload_from_ftp('test.txt', 'f3c2b0f3ecac9f02') {'implicit_collections': [], 'jobs': [{'create_time': '2015-07-28T17:57:43.704394', 'exit_code': None, 'id': '82b264d8c3d11790', 'model_class': 'Job', 'state': 'new', 'tool_id': 'upload1', 'update_time': '2015-07-28T17:57:43.910958'}], 'output_collections': [], 'outputs': [{'create_time': '2015-07-28T17:57:43.209041', 'data_type': 'galaxy.datatypes.data.Text', 'deleted': False, 'file_ext': 'auto', 'file_size': 0, 'genome_build': '?', 'hda_ldda': 'hda', 'hid': 17, 'history_content_type': 'dataset', 'history_id': 'f3c2b0f3ecac9f02', 'id': 'a676e8f07209a3be', 'metadata_data_lines': None, 'metadata_dbkey': '?', 'misc_blurb': None, 'misc_info': None, 'model_class': 'HistoryDatasetAssociation', 'name': 'test.txt', 'output_name': 'output0', 'peek': '
', 'purged': False, 'state': 'queued', 'tags': [], 'update_time': '2015-07-28T17:57:43.544407', 'uuid': '2cbe8f0a-4019-47c4-87e2-005ce35b8449', 'visible': True}]} View Data Libraries ~~~~~~~~~~~~~~~~~~~ Methods for accessing Data Libraries are grouped under ``GalaxyInstance.libraries.*``. Most Data Library methods are available to all users, but as only administrators can create new Data Libraries within Galaxy, the ``create_folder`` and ``create_library`` methods can only be called using an API key belonging to an admin account. We can view the Data Libraries available to our account using:: >>> gi.libraries.get_libraries() [{'id': '8e6f930d00d123ea', 'name': 'RNA-seq workshop data', 'url': '/api/libraries/8e6f930d00d123ea'}, {'id': 'f740ab636b360a70', 'name': '1000 genomes', 'url': '/api/libraries/f740ab636b360a70'}] This gives a list of metadata dictionaries with basic information on each library. We can get more information on a particular Data Library by passing its id to the ``show_library`` method:: >>> gi.libraries.show_library('8e6f930d00d123ea') {'contents_url': '/api/libraries/8e6f930d00d123ea/contents', 'description': 'RNA-Seq workshop data', 'name': 'RNA-Seq', 'synopsis': 'Data for the RNA-Seq tutorial'} Upload files to a Data Library ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ We can get files into Data Libraries in several ways: by uploading from our local machine, by retrieving from a URL, by passing the new file content directly into the method, or by importing a file from the filesystem on the Galaxy server. For instance, to upload a file from our machine we might call: >>> gi.libraries.upload_file_from_local_path('8e6f930d00d123ea', '/local/path/to/mydata.fastq', file_type='fastqsanger') Note that we have provided the id of the destination Data Library, and in this case we have specified the type that Galaxy should assign to the new dataset. The default value for ``file_type`` is 'auto', in which case Galaxy will attempt to guess the dataset type. View Workflows ~~~~~~~~~~~~~~ Methods for accessing workflows are grouped under ``GalaxyInstance.workflows.*``. To get information on the Workflows currently in your account, use:: >>> gi.workflows.get_workflows() [{'id': 'e8b85ad72aefca86', 'name': 'TopHat + cufflinks part 1', 'url': '/api/workflows/e8b85ad72aefca86'}, {'id': 'b0631c44aa74526d', 'name': 'CuffDiff', 'url': '/api/workflows/b0631c44aa74526d'}] This returns a list of metadata dictionaries. We can get the details of a particular Workflow, including its steps, by passing its id to the ``show_workflow`` method:: >>> gi.workflows.show_workflow('e8b85ad72aefca86') {'id': 'e8b85ad72aefca86', 'inputs': {'252': {'label': 'Input RNA-seq fastq', 'value': ''}}, 'name': 'TopHat + cufflinks part 1', 'steps': {'250': {'id': 250, 'input_steps': {'input1': {'source_step': 252, 'step_output': 'output'}}, 'tool_id': 'tophat', 'type': 'tool'}, '251': {'id': 251, 'input_steps': {'input': {'source_step': 250, 'step_output': 'accepted_hits'}}, 'tool_id': 'cufflinks', 'type': 'tool'}, '252': {'id': 252, 'input_steps': {}, 'tool_id': None, 'type': 'data_input'}}, 'url': '/api/workflows/e8b85ad72aefca86'} Export or import a workflow ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Workflows can be exported from or imported into Galaxy. This makes it possible to archive workflows, or to move them between Galaxy instances. To export a workflow, we can call:: >>> workflow_dict = gi.workflows.export_workflow_dict('e8b85ad72aefca86') This gives us a complex dictionary representing the workflow. We can import this dictionary as a new workflow with:: >>> gi.workflows.import_workflow_dict(workflow_dict) {'id': 'c0bacafdfe211f9a', 'name': 'TopHat + cufflinks part 1 (imported from API)', 'url': '/api/workflows/c0bacafdfe211f9a'} This call returns a dictionary containing basic metadata on the new workflow. Since in this case we have imported the dictionary into the original Galaxy instance, we now have a duplicate of the original workflow in our account: >>> gi.workflows.get_workflows() [{'id': 'c0bacafdfe211f9a', 'name': 'TopHat + cufflinks part 1 (imported from API)', 'url': '/api/workflows/c0bacafdfe211f9a'}, {'id': 'e8b85ad72aefca86', 'name': 'TopHat + cufflinks part 1', 'url': '/api/workflows/e8b85ad72aefca86'}, {'id': 'b0631c44aa74526d', 'name': 'CuffDiff', 'url': '/api/workflows/b0631c44aa74526d'}] Instead of using dictionaries directly, workflows can be exported to or imported from files on the local disk using the ``export_workflow_to_local_path`` and ``import_workflow_from_local_path`` methods. See the :ref:`API reference ` for details. .. Note:: If we export a workflow from one Galaxy instance and import it into another, Galaxy will only run it without modification if it has the same versions of the tool wrappers installed. This is to ensure reproducibility. Otherwise, we will need to manually update the workflow to use the new tool versions. Invoke a workflow ~~~~~~~~~~~~~~~~~ To invoke a workflow, we need to tell Galaxy which datasets to use for which workflow inputs. We can use datasets from histories or data libraries. Examine the workflow above. We can see that it takes only one input file. That is: >>> wf = gi.workflows.show_workflow('e8b85ad72aefca86') >>> wf['inputs'] {'252': {'label': 'Input RNA-seq fastq', 'value': ''}} There is one input, labelled 'Input RNA-seq fastq'. This input is passed to the Tophat tool and should be a fastq file. We will use the dataset we examined above, under :ref:`view-histories-and-datasets`, which had name 'C1_R2_1.chr4.fq' and id '10a4b652da44e82a'. To specify the inputs, we build a data map and pass this to the ``invoke_workflow`` method. This data map is a nested dictionary object which maps inputs to datasets. We call:: >>> datamap = {'252': {'src':'hda', 'id':'10a4b652da44e82a'}} >>> gi.workflows.invoke_workflow('e8b85ad72aefca86', inputs=datamap, history_name='New output history') {'history': '0a7b7992a7cabaec', 'outputs': ['33be8ad9917d9207', 'fbee1c2dc793c114', '85866441984f9e28', '1c51aa78d3742386', 'a68e8770e52d03b4', 'c54baf809e3036ac', 'ba0db8ce6cd1fe8f', 'c019e4cf08b2ac94']} In this case the only input id is '252' and the corresponding dataset id is '10a4b652da44e82a'. We have specified the dataset source to be 'hda' (HistoryDatasetAssociation) since the dataset is stored in a History. See the :ref:`API reference ` for allowed dataset specifications. We have also requested that a new History be created and used to store the results of the run, by setting ``history_name='New output history'``. The ``invoke_workflow`` call submits all the jobs which need to be run to the Galaxy workflow engine, with the appropriate dependencies so that they will run in order. The call returns immediately, so we can continue to submit new jobs while waiting for this workflow to execute. ``invoke_workflow`` returns the a dictionary describing the workflow invocation. If we view the output History immediately after calling ``invoke_workflow``, we will see something like:: >>> gi.histories.show_history('0a7b7992a7cabaec') {'annotation': '', 'contents_url': '/api/histories/0a7b7992a7cabaec/contents', 'id': '0a7b7992a7cabaec', 'name': 'New output history', 'nice_size': '0 bytes', 'state': 'queued', 'state_details': {'discarded': 0, 'empty': 0, 'error': 0, 'failed_metadata': 0, 'new': 0, 'ok': 0, 'paused': 0, 'queued': 8, 'running': 0, 'setting_metadata': 0, 'upload': 0}, 'state_ids': {'discarded': [], 'empty': [], 'error': [], 'failed_metadata': [], 'new': [], 'ok': [], 'paused': [], 'queued': ['33be8ad9917d9207', 'fbee1c2dc793c114', '85866441984f9e28', '1c51aa78d3742386', 'a68e8770e52d03b4', 'c54baf809e3036ac', 'ba0db8ce6cd1fe8f', 'c019e4cf08b2ac94'], 'running': [], 'setting_metadata': [], 'upload': []}} In this case, because the submitted jobs have not had time to run, the output History contains 8 datasets in the 'queued' state and has a total size of 0 bytes. If we make this call again later we should instead see completed output files. View Users ~~~~~~~~~~ Methods for managing users are grouped under ``GalaxyInstance.users.*``. User management is only available to Galaxy administrators, that is, the API key used to connect to Galaxy must be that of an admin account. To get a list of users, call: >>> gi.users.get_users() [{'email': 'userA@example.org', 'id': '975a9ce09b49502a', 'quota_percent': None, 'url': '/api/users/975a9ce09b49502a'}, {'email': 'userB@example.org', 'id': '0193a95acf427d2c', 'quota_percent': None, 'url': '/api/users/0193a95acf427d2c'}] Using BioBlend for raw API calls ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ BioBlend can be used to make HTTP requests to the Galaxy API in a more convenient way than using e.g. the ``requests`` Python library. There are 5 available methods corresponding to the most common HTTP methods: ``make_get_request``, ``make_post_request``, ``make_put_request``, ``make_delete_request`` and ``make_patch_request``. One advantage of using these methods is that the API keys stored in the ``GalaxyInstance`` object is automatically added to the request. To make a GET request to the Galaxy API with BioBlend, call: >>> gi.make_get_request(gi.base_url + "/api/version").json() {'version_major': '19.05', 'extra': {}} To make a POST request to the Galaxy API with BioBlend, call: >>> gi.make_post_request(gi.base_url + "/api/histories", payload={"name": "test history"}) {'importable': False, 'create_time': '2019-07-05T20:10:04.823716', 'contents_url': '/api/histories/a77b3f95070d689a/contents', 'id': 'a77b3f95070d689a', 'size': 0, 'user_id': '5b732999121d4593', 'username_and_slug': None, 'annotation': None, 'state_details': {'discarded': 0, 'ok': 0, 'failed_metadata': 0, 'upload': 0, 'paused': 0, 'running': 0, 'setting_metadata': 0, 'error': 0, 'new': 0, 'queued': 0, 'empty': 0}, 'state': 'new', 'empty': True, 'update_time': '2019-07-05T20:10:04.823742', 'tags': [], 'deleted': False, 'genome_build': None, 'slug': None, 'name': 'test history', 'url': '/api/histories/a77b3f95070d689a', 'state_ids': {'discarded': [], 'ok': [], 'failed_metadata': [], 'upload': [], 'paused': [], 'running': [], 'setting_metadata': [], 'error': [], 'new': [], 'queued': [], 'empty': []}, 'published': False, 'model_class': 'History', 'purged': False} bioblend-1.2.0/docs/api_docs/lib_config.rst000066400000000000000000000003741444761704300206440ustar00rootroot00000000000000==================================== Configuration documents for BioBlend ==================================== BioBlend -------- .. automodule:: bioblend :members: Config ------ .. automodule:: bioblend.config :members: :undoc-members: bioblend-1.2.0/docs/api_docs/toolshed/000077500000000000000000000000001444761704300176345ustar00rootroot00000000000000bioblend-1.2.0/docs/api_docs/toolshed/all.rst000066400000000000000000000010171444761704300211350ustar00rootroot00000000000000========================================================== API documentation for interacting with the Galaxy Toolshed ========================================================== ToolShedInstance ---------------- .. autoclass:: bioblend.toolshed.ToolShedInstance .. automethod:: bioblend.toolshed.ToolShedInstance.__init__ Categories ---------- .. automodule:: bioblend.toolshed.categories Repositories ------------ .. automodule:: bioblend.toolshed.repositories Tools ----- .. automodule:: bioblend.toolshed.tools bioblend-1.2.0/docs/conf.py000066400000000000000000000202231444761704300155300ustar00rootroot00000000000000# BioBlend documentation build configuration file, created by # sphinx-quickstart on Wed Jun 6 11:51:19 2012. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import os import sys # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath(os.path.join(os.getcwd(), ".."))) from bioblend import get_version # noqa: E402 # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ["sphinx.ext.autodoc", "sphinx.ext.todo", "sphinx.ext.coverage", "sphinx.ext.viewcode", "sphinx_rtd_theme"] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix of source filenames. source_suffix = ".rst" # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = "index" # General information about the project. project = "BioBlend" copyright = "2012-2023, Galaxy Project" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # The short X.Y version. version = get_version() # The full version, including alpha/beta/rc tags. release = get_version() # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # List of autodoc directive flags that should be automatically applied to all # autodoc directives autodoc_default_options = { "members": True, "undoc-members": True, } # Include the __init__ method's doc string in addition to the class doc string # in the documentation. autoclass_content = "both" # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = "BioBlenddoc" # -- Options for LaTeX output -------------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # 'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ("index", "BioBlend.tex", "BioBlend Documentation", "Galaxy Project", "manual"), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [("index", "bioblend", "BioBlend Documentation", ["Galaxy Project"], 1)] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( "index", "BioBlend", "BioBlend Documentation", "Galaxy Project", "BioBlend", "One line description of project.", "Miscellaneous", ), ] # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' bioblend-1.2.0/docs/examples/000077500000000000000000000000001444761704300160505ustar00rootroot00000000000000bioblend-1.2.0/docs/examples/create_user_get_api_key.py000066400000000000000000000015261444761704300232670ustar00rootroot00000000000000""" This example creates a new user and prints her API key. It is also used to initialize a Galaxy server in Continuous Integration testing of BioBlend. Usage: python3 create_user_get_api_key.py """ import sys import bioblend.galaxy if len(sys.argv) != 6: print( "Usage: python3 create_user_get_api_key.py " ) sys.exit(1) galaxy_url = sys.argv[1] galaxy_api_key = sys.argv[2] # Initiating Galaxy connection gi = bioblend.galaxy.GalaxyInstance(galaxy_url, galaxy_api_key) # Create a new user and get a new API key for her new_user = gi.users.create_local_user(sys.argv[3], sys.argv[4], sys.argv[5]) new_api_key = gi.users.create_user_apikey(new_user["id"]) print(new_api_key) bioblend-1.2.0/docs/examples/list_data_libraries.py000066400000000000000000000014161444761704300224240ustar00rootroot00000000000000""" This example retrieves details of all the Data Libraries available to us and lists information on them. Usage: python3 list_data_libraries.py """ import sys from bioblend.galaxy import GalaxyInstance if len(sys.argv) != 3: print("Usage: python3 list_data_libraries.py ") sys.exit(1) galaxy_url = sys.argv[1] galaxy_key = sys.argv[2] print("Initiating Galaxy connection") gi = GalaxyInstance(url=galaxy_url, key=galaxy_key) print("Retrieving Data Library list") libraries = gi.libraries.get_libraries() if len(libraries) == 0: print("There are no Data Libraries available.") else: print("\nData Libraries:") for lib_dict in libraries: print(f"{lib_dict['name']} : {lib_dict['id']}") bioblend-1.2.0/docs/examples/list_histories.py000066400000000000000000000016571444761704300214770ustar00rootroot00000000000000""" This example retrieves details of all the Histories in our Galaxy account and lists information on them. Usage: python list_histories.py """ import sys from bioblend.galaxy import GalaxyInstance if len(sys.argv) != 3: print("Usage: python list_histories.py ") sys.exit(1) galaxy_url = sys.argv[1] galaxy_key = sys.argv[2] print("Initiating Galaxy connection") gi = GalaxyInstance(url=galaxy_url, key=galaxy_key) print("Retrieving History list") histories = gi.histories.get_histories() if len(histories) == 0: print("There are no Histories in your account.") else: print("\nHistories:") for hist_dict in histories: # As an example, we retrieve a piece of metadata (the size) using show_history hist_details = gi.histories.show_history(hist_dict["id"]) print(f"{hist_dict['name']} ({hist_details['size']}) : {hist_dict['id']}") bioblend-1.2.0/docs/examples/list_workflows.py000066400000000000000000000013711444761704300215140ustar00rootroot00000000000000""" This example retrieves details of all the Workflows in our Galaxy account and lists information on them. Usage: python list_workflows.py """ import sys from bioblend.galaxy import GalaxyInstance if len(sys.argv) != 3: print("Usage: python list_workflows.py ") sys.exit(1) galaxy_url = sys.argv[1] galaxy_key = sys.argv[2] print("Initiating Galaxy connection") gi = GalaxyInstance(url=galaxy_url, key=galaxy_key) print("Retrieving Workflows list") workflows = gi.workflows.get_workflows() if len(workflows) == 0: print("There are no Workflows in your account.") else: print("\nWorkflows:") for wf_dict in workflows: print(f"{wf_dict['name']} : {wf_dict['id']}") bioblend-1.2.0/docs/examples/objects/000077500000000000000000000000001444761704300175015ustar00rootroot00000000000000bioblend-1.2.0/docs/examples/objects/README.txt000066400000000000000000000035261444761704300212050ustar00rootroot00000000000000BioBlend.objects Examples ========================= Microbiology ------------ This directory contains three examples of interaction with real-world microbiology workflows hosted by CRS4's Orione Galaxy server: * bacterial re-sequencing (w2_bacterial_reseq.py); * bacterial de novo assembly (w3_bacterial_denovo.py); * metagenomics (w5_metagenomics.py). All examples use workflows and datasets publicly available on Orione. Before you can run them, you have to register and obtain an API key: * go to https://orione.crs4.it and register -- or log in, if you are already registered -- through the "User" menu at the top of the page; * open "User" -> "API Keys"; * generate an API key if you don't have one. In the example file, replace YOUR_API_KEY with your API key (or assign its value to the GALAXY_API_KEY environment variable), then run it: export GALAXY_API_KEY=000this_should_be_your_api_key00 python w2_bacterial_reseq.py The job can take a long time to complete: before exiting, the script runs the workflow asynchronously, then displays the name and id of the output history on standard output. In the Galaxy web UI, click the gear icon at the top right corner of the History panel, select "Saved Histories" and look for the name of the output history in the center frame; finally, choose "switch" from the history's drop-down menu to make it the current one and follow the job as it evolves on Galaxy. Toy Example ----------- The small.py file contains a "toy" example that should run much faster (once the cluster's resource manager allows it to run) than the above ones. In this case, the script waits for the job to complete and downloads its results to a local file. See Also -------- Cuccuru et al., "Orione, a web-based framework for NGS analysis in microbiology". Bioinformatics (2014). http://dx.doi.org/10.1093/bioinformatics/btu135 bioblend-1.2.0/docs/examples/objects/__init__.py000066400000000000000000000000001444761704300216000ustar00rootroot00000000000000bioblend-1.2.0/docs/examples/objects/common.py000066400000000000000000000001331444761704300213400ustar00rootroot00000000000000def get_one(iterable): seq = list(iterable) assert len(seq) == 1 return seq[0] bioblend-1.2.0/docs/examples/objects/list_data_libraries.py000066400000000000000000000013661444761704300240610ustar00rootroot00000000000000""" This example retrieves details of all the Data Libraries available to us and lists information on them. Usage: python list_data_libraries.py """ import sys from bioblend.galaxy.objects import GalaxyInstance if len(sys.argv) != 3: print("Usage: python list_data_libraries.py ") sys.exit(1) galaxy_url = sys.argv[1] galaxy_key = sys.argv[2] print("Initiating Galaxy connection") gi = GalaxyInstance(galaxy_url, galaxy_key) print("Retrieving Data Library list") libraries = gi.libraries.get_previews() if len(libraries) == 0: print("There are no Data Libraries available.") else: print("\nData Libraries:") for lib in libraries: print(f"{lib.name} : {lib.id}") bioblend-1.2.0/docs/examples/objects/list_histories.py000066400000000000000000000020651444761704300231220ustar00rootroot00000000000000""" This example retrieves details of all the Histories in our Galaxy account and lists information on them. Usage: python list_histories.py """ import sys from bioblend.galaxy.objects import GalaxyInstance if len(sys.argv) != 3: print("Usage: python list_histories.py ") sys.exit(1) galaxy_url = sys.argv[1] galaxy_key = sys.argv[2] print("Initiating Galaxy connection") gi = GalaxyInstance(galaxy_url, galaxy_key) print("Retrieving History list") # histories.get_previews() returns a list of HistoryPreview objects, which contain only basic information # histories.list() method returns a list of History objects, which contain more extended information # As an example, we will use a piece of metadata (the size) from the 'wrapped' data attribute of History histories = gi.histories.list() if len(histories) == 0: print("There are no Histories in your account.") else: print("\nHistories:") for hist in histories: print(f"{hist.name} ({hist.wrapped['nice_size']}) : {hist.id}") bioblend-1.2.0/docs/examples/objects/list_workflows.py000066400000000000000000000013431444761704300231440ustar00rootroot00000000000000""" This example retrieves details of all the Workflows in our Galaxy account and lists information on them. Usage: python list_workflows.py """ import sys from bioblend.galaxy.objects import GalaxyInstance if len(sys.argv) != 3: print("Usage: python list_workflows.py ") sys.exit(1) galaxy_url = sys.argv[1] galaxy_key = sys.argv[2] print("Initiating Galaxy connection") gi = GalaxyInstance(galaxy_url, galaxy_key) print("Retrieving Workflows list") workflows = gi.workflows.get_previews() if len(workflows) == 0: print("There are no Workflows in your account.") else: print("\nWorkflows:") for wf in workflows: print(f"{wf.name} : {wf.id}") bioblend-1.2.0/docs/examples/objects/small.ga000066400000000000000000000057151444761704300211320ustar00rootroot00000000000000{ "a_galaxy_workflow": "true", "annotation": "", "format-version": "0.1", "name": "get_col", "steps": { "0": { "annotation": "", "id": 0, "input_connections": {}, "inputs": [ { "description": "", "name": "input_tsv" } ], "name": "Input dataset", "outputs": [], "position": { "left": 200, "top": 200 }, "tool_errors": null, "tool_id": null, "tool_state": "{\"name\": \"input_tsv\"}", "tool_version": null, "type": "data_input", "user_outputs": [] }, "1": { "annotation": "", "id": 1, "input_connections": { "input": { "id": 0, "output_name": "output" } }, "inputs": [], "name": "Remove beginning", "outputs": [ { "name": "out_file1", "type": "input" } ], "position": { "left": 420, "top": 200 }, "post_job_actions": { "HideDatasetActionout_file1": { "action_arguments": {}, "action_type": "HideDatasetAction", "output_name": "out_file1" } }, "tool_errors": null, "tool_id": "Remove beginning1", "tool_state": "{\"input\": \"null\", \"__rerun_remap_job_id__\": null, \"chromInfo\": \"\\\"/SHARE/USERFS/els7/users/biobank/galaxy/tool-data/shared/ucsc/chrom/?.len\\\"\", \"num_lines\": \"\\\"2\\\"\", \"__page__\": 0}", "tool_version": "1.0.0", "type": "tool", "user_outputs": [] }, "2": { "annotation": "", "id": 2, "input_connections": { "input": { "id": 1, "output_name": "out_file1" } }, "inputs": [], "name": "Cut", "outputs": [ { "name": "out_file1", "type": "tabular" } ], "position": { "left": 640, "top": 200 }, "post_job_actions": {}, "tool_errors": null, "tool_id": "Cut1", "tool_state": "{\"__page__\": 0, \"__rerun_remap_job_id__\": null, \"delimiter\": \"\\\"T\\\"\", \"columnList\": \"\\\"c1\\\"\", \"input\": \"null\", \"chromInfo\": \"\\\"/SHARE/USERFS/els7/users/biobank/galaxy/tool-data/shared/ucsc/chrom/?.len\\\"\"}", "tool_version": "1.0.2", "type": "tool", "user_outputs": [] } } } bioblend-1.2.0/docs/examples/objects/small.py000066400000000000000000000035001444761704300211610ustar00rootroot00000000000000import os import sys import tempfile from common import get_one # noqa:I100,I201 from bioblend.galaxy.objects import GalaxyInstance # This is a "toy" example that should run much faster # (once the cluster's resource manager allows it to run) than the # real-world ones. The workflow, which is imported from local disk, # removes two header lines from a tabular file, then extracts one of # the columns. The input dataset is publicly available on CRS4's # Orione Galaxy server. URL = "https://orione.crs4.it" API_KEY = os.getenv("GALAXY_API_KEY", "YOUR_API_KEY") if API_KEY == "YOUR_API_KEY": sys.exit("API_KEY not set, see the README.txt file") gi = GalaxyInstance(URL, API_KEY) # import the workflow from the JSON dump with open("small.ga") as f: wf = gi.workflows.import_new(f.read()) # Select the "Orione SupMat" library library_name = "Orione SupMat" library = get_one(gi.libraries.list(name=library_name)) # Select the input dataset ds_name = "/RNA-Seq - Listeria monocytogenes/Listeria_monocytogenes_EGD_e_uid61583/NC_003210.rnt" ld = get_one(library.get_datasets(name=ds_name)) input_map = {"input_tsv": ld} # Run the workflow on a new history with the selected dataset as # input, overriding the index of the column to remove; wait until the # computation is complete. history_name = "get_col output" params = {"Cut1": {"columnList": "c2"}} print(f"Running workflow: {wf.name} [{wf.id}]") outputs, out_hist = wf.run(input_map, history_name, params=params, wait=True) print("Job has finished") assert out_hist.name == history_name print(f"Output history: {out_hist.name} [{out_hist.id}]") # Save results to local disk out_ds = get_one([_ for _ in outputs if _.name == "Cut on data 1"]) with tempfile.NamedTemporaryFile(prefix="bioblend_", delete=False) as f: out_ds.download(f) print(f'Output downloaded to "{f.name}"') bioblend-1.2.0/docs/examples/objects/w2_bacterial_reseq.py000066400000000000000000000034611444761704300236140ustar00rootroot00000000000000import os import sys from common import get_one # noqa:I100,I201 from bioblend.galaxy.objects import GalaxyInstance URL = "https://orione.crs4.it" API_KEY = os.getenv("GALAXY_API_KEY", "YOUR_API_KEY") if API_KEY == "YOUR_API_KEY": sys.exit("API_KEY not set, see the README.txt file") gi = GalaxyInstance(URL, API_KEY) # Select "W2 - Bacterial re-sequencing | Paired-end" from published workflows workflow_name = "W2 - Bacterial re-sequencing | Paired-end" previews = gi.workflows.get_previews(name=workflow_name, published=True) p = get_one(_ for _ in previews if _.published) # Import the workflow to user space iw = gi.workflows.import_shared(p.id) # Create a new history history_name = f"{workflow_name} output" h = gi.histories.create(history_name) # Select the "Orione SupMat" library library_name = "Orione SupMat" library = get_one(gi.libraries.list(name=library_name)) # Select the datasets ds_names = [ "/Whole genome - Escherichia coli/E coli DH10B MiSeq R1.fastq", "/Whole genome - Escherichia coli/E coli DH10B MiSeq R2.fastq", "/Whole genome - Escherichia coli/E coli DH10B - Reference", ] input_labels = [ "Forward Reads", "Reverse Reads", "Reference Genome", ] input_map = { label: h.import_dataset(get_one(library.get_datasets(name=name))) for name, label in zip(ds_names, input_labels) } # Set custom parameters for the "check_contigs" and "sspace" tools params = { "check_contigs": {"genomesize": 5.0}, # affects both occurrences "sspace": {"insert": 300, "error": 0.5, "minoverlap": 35}, } # Run the workflow on a new history with the selected datasets as inputs outputs, out_hist = iw.run(input_map, h, params=params) assert out_hist.name == history_name print(f"Running workflow: {iw.name} [{iw.id}]") print(f"Output history: {out_hist.name} [{out_hist.id}]") bioblend-1.2.0/docs/examples/objects/w3_bacterial_denovo.py000066400000000000000000000045141444761704300237700ustar00rootroot00000000000000import json import os import sys from common import get_one # noqa:I100,I201 from bioblend.galaxy.objects import GalaxyInstance URL = "https://orione.crs4.it" API_KEY = os.getenv("GALAXY_API_KEY", "YOUR_API_KEY") if API_KEY == "YOUR_API_KEY": sys.exit("API_KEY not set, see the README.txt file") gi = GalaxyInstance(URL, API_KEY) # Select "W3 - Bacterial de novo assembly | Paired-end" from published workflows workflow_name = "W3 - Bacterial de novo assembly | Paired-end" previews = gi.workflows.get_previews(name=workflow_name, published=True) p = get_one(_ for _ in previews if _.published) # Import the workflow to user space iw = gi.workflows.import_shared(p.id) # Create a new history history_name = f"{workflow_name} output" h = gi.histories.create(history_name) # Select the "Orione SupMat" library library_name = "Orione SupMat" library = get_one(gi.libraries.list(name=library_name)) # Select the datasets ds_names = [ "/Whole genome - Escherichia coli/E coli DH10B MiSeq R1.fastq", "/Whole genome - Escherichia coli/E coli DH10B MiSeq R2.fastq", ] input_labels = [ "Left/Forward FASTQ Reads", "Right/Reverse FASTQ Reads", ] input_map = { label: h.import_dataset(get_one(library.get_datasets(name=name))) for name, label in zip(ds_names, input_labels) } # Set the "hash_length" parameter to different values for the 3 "velveth" steps lengths = {"19", "23", "29"} ws_ids = iw.tool_labels_to_ids["velveth"] assert len(ws_ids) == len(lengths) params = {id_: {"hash_length": v} for id_, v in zip(ws_ids, lengths)} # Set the "ins_length" runtime parameter to the same value for the 3 # "velvetg" steps tool_id = "velvetg" ws_ids = iw.tool_labels_to_ids[tool_id] step = iw.steps[next(iter(ws_ids))] # arbitrarily pick one params[tool_id] = {"reads": json.loads(step.tool_inputs["reads"]).copy()} params[tool_id]["reads"]["ins_length"] = -1 # Set more custom parameters params["cisarunner"] = {"genomesize": 5000000} params["check_contigs"] = {"genomesize": 5.0} params["toolshed.g2.bx.psu.edu/repos/edward-kirton/abyss_toolsuite/abyss/1.0.0"] = {"k": 41} # Run the workflow on a new history with the selected datasets as inputs outputs, out_hist = iw.run(input_map, h, params=params) assert out_hist.name == history_name print(f"Running workflow: {iw.name} [{iw.id}]") print(f"Output history: {out_hist.name} [{out_hist.id}]") bioblend-1.2.0/docs/examples/objects/w5_galaxy_api.py000066400000000000000000000054421444761704300226110ustar00rootroot00000000000000import json import os import sys # This example, provided for comparison with w5_metagenomics.py, # contains the code required to run the metagenomics workflow # *without* BioBlend. URL = os.getenv("GALAXY_URL", "https://orione.crs4.it").rstrip("/") API_URL = f"{URL}/api" API_KEY = os.getenv("GALAXY_API_KEY", "YOUR_API_KEY") if API_KEY == "YOUR_API_KEY": sys.exit("API_KEY not set, see the README.txt file") # Clone the galaxy git repository and replace # YOUR_GALAXY_PATH with the clone's local path in the following code, e.g.: # cd /tmp # git clone https://github.com/galaxyproject/galaxy # GALAXY_PATH = '/tmp/galaxy' GALAXY_PATH = "YOUR_GALAXY_PATH" sys.path.insert(1, os.path.join(GALAXY_PATH, "scripts/api")) import common # noqa: E402,I100,I202 # Select "W5 - Metagenomics" from published workflows workflow_name = "W5 - Metagenomics" workflows = common.get(API_KEY, f"{API_URL}/workflows?show_published=True") w = [_ for _ in workflows if _["published"] and _["name"] == workflow_name] assert len(w) == 1 w = w[0] # Import the workflow to user space data = {"workflow_id": w["id"]} iw = common.post(API_KEY, f"{API_URL}/workflows/import", data) iw_details = common.get(API_KEY, f"{API_URL}/workflows/{iw['id']}") # Select the "Orione SupMat" library library_name = "Orione SupMat" libraries = common.get(API_KEY, f"{API_URL}/libraries") filtered_libraries = [_ for _ in libraries if _["name"] == library_name] assert len(filtered_libraries) == 1 library = filtered_libraries[0] # Select the "/Metagenomics/MetagenomicsDataset.fq" dataset ds_name = "/Metagenomics/MetagenomicsDataset.fq" contents = common.get(API_KEY, f"{API_URL}/libraries/{library['id']}/contents") ld = [_ for _ in contents if _["type"] == "file" and _["name"] == ds_name] assert len(ld) == 1 ld = ld[0] # Select the blastn step ws = [_ for _ in iw_details["steps"].values() if _["tool_id"] and "blastn" in _["tool_id"]] assert len(ws) == 1 ws = ws[0] tool_id = ws["tool_id"] # Get (a copy of) the parameters dict for the selected step ws_parameters = ws["tool_inputs"].copy() for k, v in ws_parameters.items(): ws_parameters[k] = json.loads(v) # Run the workflow on a new history with the selected dataset # as input, setting the BLAST db to "16SMicrobial-20131106" history_name = f"{workflow_name} output" ws_parameters["db_opts"]["database"] = "16SMicrobial-20131106" data = { "workflow_id": iw["id"], "parameters": {tool_id: {"db_opts": ws_parameters["db_opts"]}}, } assert len(iw_details["inputs"]) == 1 input_step_id = iw_details["inputs"].keys()[0] data["ds_map"] = {input_step_id: {"src": "ld", "id": ld["id"]}} data["history"] = history_name r_dict = common.post(API_KEY, f"{API_URL}/workflows", data) print(f"Running workflow: {iw['name']} [{iw['id']}]") print(f"Output history: {history_name} [{r_dict['history']}]") bioblend-1.2.0/docs/examples/objects/w5_metagenomics.py000066400000000000000000000034241444761704300231440ustar00rootroot00000000000000import json import os import sys from common import get_one # noqa:I100,I201 from bioblend.galaxy.objects import GalaxyInstance URL = "https://orione.crs4.it" API_KEY = os.getenv("GALAXY_API_KEY", "YOUR_API_KEY") if API_KEY == "YOUR_API_KEY": sys.exit("API_KEY not set, see the README.txt file") gi = GalaxyInstance(URL, API_KEY) # Select "W5 - Metagenomics" from published workflows workflow_name = "W5 - Metagenomics" previews = gi.workflows.get_previews(name=workflow_name, published=True) p = get_one(_ for _ in previews if _.published) # Import the workflow to user space iw = gi.workflows.import_shared(p.id) # Create a new history history_name = f"{workflow_name} output" h = gi.histories.create(history_name) # Select the "Orione SupMat" library library_name = "Orione SupMat" library = get_one(gi.libraries.list(name=library_name)) # Select the "/Metagenomics/MetagenomicsDataset.fq" dataset ds_name = "/Metagenomics/MetagenomicsDataset.fq" input_map = {"Input Dataset": h.import_dataset(get_one(library.get_datasets(name=ds_name)))} # Select the blastn step tool_id = "toolshed.g2.bx.psu.edu/repos/devteam/ncbi_blast_plus/ncbi_blastn_wrapper/0.1.00" step_id = get_one(iw.tool_labels_to_ids[tool_id]) ws = iw.steps[step_id] # Get (a copy of) the parameters dict for the selected step ws_parameters = ws.tool_inputs.copy() # Run the workflow on a new history with the selected dataset # as input, setting the BLAST db to "16SMicrobial-20131106" params = {tool_id: {"db_opts": json.loads(ws_parameters["db_opts"])}} params[tool_id]["db_opts"]["database"] = "16SMicrobial-20131106" outputs, out_hist = iw.run(input_map, h, params=params) assert out_hist.name == history_name print(f"Running workflow: {iw.name} [{iw.id}]") print(f"Output history: {out_hist.name} [{out_hist.id}]") bioblend-1.2.0/docs/examples/run_imported_workflow.py000066400000000000000000000071001444761704300230610ustar00rootroot00000000000000""" This example demonstrates running a tophat+cufflinks workflow over paired-end data. This is a task we could not do using Galaxy's GUI batch mode, because the inputs need to be paired. The workflow is imported from a json file (previously exported from Galaxy), and the input data files from URLs. This example creates a new Data Library, so you must be a Galaxy Admin on the instance you run the script against. Also note that a Galaxy Workflow will only run without modification if it finds the expected versions of tool wrappers installed on the Galaxy instance. This is to ensure reproducibility. In this case we expect Tophat wrapper 1.5.0 and Cufflinks wrapper 0.0.5. Usage: python3 run_imported_workflow.py """ import sys from bioblend import galaxy # Specify workflow and data to import into Galaxy workflow_file = "tophat_cufflinks_pairedend_workflow.ga" import_file_pairs = [ ("https://bioblend.s3.amazonaws.com/C1_R1_1.chr4.fq", "https://bioblend.s3.amazonaws.com/C1_R1_2.chr4.fq"), ("https://bioblend.s3.amazonaws.com/C1_R2_1.chr4.fq", "https://bioblend.s3.amazonaws.com/C1_R2_2.chr4.fq"), ("https://bioblend.s3.amazonaws.com/C1_R3_1.chr4.fq", "https://bioblend.s3.amazonaws.com/C1_R3_2.chr4.fq"), ] # Specify names of Library and History that will be created in Galaxy # In this simple example, these will be created even if items with the same name already exist. library_name = "Imported data for API demo" output_history_name = "Output from API demo" if len(sys.argv) != 3: print("Usage: python3 run_imported_workflow.py ") sys.exit(1) galaxy_url = sys.argv[1] galaxy_key = sys.argv[2] print("Initiating Galaxy connection") gi = galaxy.GalaxyInstance(url=galaxy_url, key=galaxy_key) print("Importing workflow") wf_import_dict = gi.workflows.import_workflow_from_local_path(workflow_file) workflow = wf_import_dict["id"] print(f"Creating data library '{library_name}'") library_dict = gi.libraries.create_library(library_name) library = library_dict["id"] print("Importing data") # Import each pair of files, and track the resulting identifiers. dataset_ids = [] filenames = {} for file1, file2 in import_file_pairs: dataset1 = gi.libraries.upload_file_from_url(library, file1, file_type="fastqsanger") dataset2 = gi.libraries.upload_file_from_url(library, file2, file_type="fastqsanger") id1, id2 = dataset1[0]["id"], dataset2[0]["id"] filenames[id1] = file1 filenames[id2] = file2 dataset_ids.append((id1, id2)) print(f"Creating output history '{output_history_name}'") outputhist_dict = gi.histories.create_history(output_history_name) outputhist = outputhist_dict["id"] print(f"Will run workflow on {len(dataset_ids)} pairs of files") # Get the input step IDs from the workflow. # We use the BioBlend convenience function get_workflow_inputs to retrieve inputs by label. input1 = gi.workflows.get_workflow_inputs(workflow, label="Input fastq readpair-1")[0] input2 = gi.workflows.get_workflow_inputs(workflow, label="Input fastq readpair-2")[0] # For each pair of datasets we imported, run the imported workflow # For each input we need to build a datamap dict with 'src' set to 'ld', as we stored our data in a Galaxy Library for data1, data2 in dataset_ids: print(f"Initiating workflow run on files {filenames[data1]}, {filenames[data2]}") datamap = { input1: {"src": "ld", "id": data1}, input2: {"src": "ld", "id": data2}, } invocation = gi.workflows.invoke_workflow( workflow, inputs=datamap, history_id=outputhist, import_inputs_to_history=True ) bioblend-1.2.0/docs/examples/tophat_cufflinks_pairedend_workflow.ga000066400000000000000000000124531444761704300256760ustar00rootroot00000000000000{ "a_galaxy_workflow": "true", "annotation": "", "format-version": "0.1", "name": "TopHat + cufflinks paired-end", "steps": { "0": { "annotation": "", "id": 0, "input_connections": {}, "inputs": [ { "description": "", "name": "Input fastq readpair-1" } ], "name": "Input dataset", "outputs": [], "position": { "left": 200, "top": 308 }, "tool_errors": null, "tool_id": null, "tool_state": "{\"name\": \"Input fastq readpair-1\"}", "tool_version": null, "type": "data_input", "user_outputs": [] }, "1": { "annotation": "", "id": 1, "input_connections": {}, "inputs": [ { "description": "", "name": "Input fastq readpair-2" } ], "name": "Input dataset", "outputs": [], "position": { "left": 177.7833251953125, "top": 395.26666259765625 }, "tool_errors": null, "tool_id": null, "tool_state": "{\"name\": \"Input fastq readpair-2\"}", "tool_version": null, "type": "data_input", "user_outputs": [] }, "2": { "annotation": "", "id": 2, "input_connections": { "input1": { "id": 0, "output_name": "output" }, "singlePaired|input2": { "id": 1, "output_name": "output" } }, "inputs": [], "name": "Tophat for Illumina", "outputs": [ { "name": "insertions", "type": "bed" }, { "name": "deletions", "type": "bed" }, { "name": "junctions", "type": "bed" }, { "name": "accepted_hits", "type": "bam" } ], "position": { "left": 436, "top": 280 }, "post_job_actions": { "HideDatasetActiondeletions": { "action_arguments": {}, "action_type": "HideDatasetAction", "output_name": "deletions" }, "HideDatasetActioninsertions": { "action_arguments": {}, "action_type": "HideDatasetAction", "output_name": "insertions" } }, "tool_errors": null, "tool_id": "tophat", "tool_state": "{\"__page__\": 0, \"input1\": \"null\", \"refGenomeSource\": \"{\\\"genomeSource\\\": \\\"indexed\\\", \\\"index\\\": \\\"dm3\\\", \\\"__current_case__\\\": 0}\", \"singlePaired\": \"{\\\"input2\\\": null, \\\"sPaired\\\": \\\"paired\\\", \\\"pParams\\\": {\\\"pSettingsType\\\": \\\"preSet\\\", \\\"__current_case__\\\": 0}, \\\"__current_case__\\\": 1, \\\"mate_inner_distance\\\": \\\"20\\\"}\"}", "tool_version": "1.5.0", "type": "tool", "user_outputs": [] }, "3": { "annotation": "", "id": 3, "input_connections": { "input": { "id": 2, "output_name": "accepted_hits" } }, "inputs": [], "name": "Cufflinks", "outputs": [ { "name": "genes_expression", "type": "tabular" }, { "name": "transcripts_expression", "type": "tabular" }, { "name": "assembled_isoforms", "type": "gtf" }, { "name": "total_map_mass", "type": "txt" } ], "position": { "left": 679, "top": 342 }, "post_job_actions": {}, "tool_errors": null, "tool_id": "cufflinks", "tool_state": "{\"min_isoform_fraction\": \"\\\"0.1\\\"\", \"multiread_correct\": \"\\\"Yes\\\"\", \"singlePaired\": \"{\\\"sPaired\\\": \\\"No\\\", \\\"__current_case__\\\": 0}\", \"__page__\": 0, \"pre_mrna_fraction\": \"\\\"0.15\\\"\", \"bias_correction\": \"{\\\"do_bias_correction\\\": \\\"No\\\", \\\"__current_case__\\\": 1}\", \"max_intron_len\": \"\\\"300000\\\"\", \"reference_annotation\": \"{\\\"use_ref\\\": \\\"No\\\", \\\"__current_case__\\\": 0}\", \"global_model\": \"null\", \"do_normalization\": \"\\\"No\\\"\", \"input\": \"null\"}", "tool_version": "0.0.5", "type": "tool", "user_outputs": [] } } } bioblend-1.2.0/docs/index.rst000066400000000000000000000111111444761704300160660ustar00rootroot00000000000000======== BioBlend ======== About ===== .. include:: ../ABOUT.rst Installation ============ Stable releases of BioBlend are best installed via ``pip`` from PyPI:: $ python3 -m pip install bioblend Alternatively, the most current source code from our `Git repository`_ can be installed with:: $ python3 -m pip install git+https://github.com/galaxyproject/bioblend After installing the library, you will be able to simply import it into your Python environment with ``import bioblend``. For details on the available functionality, see the `API documentation`_. BioBlend requires a number of Python libraries. These libraries are installed automatically when BioBlend itself is installed, regardless whether it is installed via PyPi_ or by running ``python3 setup.py install`` command. The current list of required libraries is always available from `setup.py`_ in the source code repository. If you also want to run tests locally, some extra libraries are required. To install them, run:: $ python3 setup.py test Usage ===== To get started using BioBlend, install the library as described above. Once the library becomes available on the given system, it can be developed against. The developed scripts do not need to reside in any particular location on the system. It is probably best to take a look at the example scripts in ``docs/examples`` source directory and browse the `API documentation`_. Beyond that, it's up to your creativity :). Development =========== Anyone interested in contributing or tweaking the library is more then welcome to do so. To start, simply fork the `Git repository`_ on Github and start playing with it. Then, issue pull requests. API Documentation ================= BioBlend's API focuses around and matches the services it wraps. Thus, there are two top-level sets of APIs, each corresponding to a separate service and a corresponding step in the automation process. *Note* that each of the service APIs can be used completely independently of one another. Effort has been made to keep the structure and naming of those API's consistent across the library but because they do bridge different services, some discrepancies may exist. Feel free to point those out and/or provide fixes. For Galaxy, an alternative :ref:`object-oriented API ` is also available. This API provides an explicit modeling of server-side Galaxy instances and their relationships, providing higher-level methods to perform operations such as retrieving all datasets for a given history, etc. Note that, at the moment, the oo API is still incomplete, providing access to a more restricted set of Galaxy modules with respect to the standard one. Galaxy API ~~~~~~~~~~ API used to manipulate genomic analyses within Galaxy, including data management and workflow execution. .. toctree:: :maxdepth: 3 :glob: api_docs/galaxy/* Toolshed API ~~~~~~~~~~~~ API used to interact with the Galaxy Toolshed, including repository management. .. toctree:: :maxdepth: 3 :glob: api_docs/toolshed/* Configuration ============= BioBlend allows library-wide configuration to be set in external files. These configuration files can be used to specify access keys, for example. .. toctree:: :maxdepth: 1 :glob: api_docs/lib_config Testing ======= If you would like to do more than just a mock test, you need to point BioBlend to an instance of Galaxy. Do so by exporting the following two variables:: $ export BIOBLEND_GALAXY_URL=http://127.0.0.1:8080 $ export BIOBLEND_GALAXY_API_KEY= The unit tests, stored in the ``tests`` folder, can be run using `pytest `_. From the project root:: $ pytest Getting help ============ If you have run into issues, found a bug, or can't seem to find an answer to your question regarding the use and functionality of BioBlend, please use the `Github Issues `_ page to ask your question. Related documentation ===================== Links to other documentation and libraries relevant to this library: * `Galaxy API documentation `_ * `Blend4j `_: Galaxy API wrapper for Java * `clj-blend `_: Galaxy API wrapper for Clojure Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` .. References/hyperlinks used above .. _Git repository: https://github.com/galaxyproject/bioblend .. _PyPi: https://pypi.org/project/bioblend/ .. _setup.py: https://github.com/galaxyproject/bioblend/blob/main/setup.py bioblend-1.2.0/docs/requirements.txt000066400000000000000000000000421444761704300175120ustar00rootroot00000000000000sphinx>=2 sphinx-rtd-theme>=0.5.2 bioblend-1.2.0/pyproject.toml000066400000000000000000000006021444761704300162140ustar00rootroot00000000000000[build-system] requires = ["setuptools", "wheel"] build-backend = "setuptools.build_meta" [tool.black] include = '\.pyi?$' line-length = 120 target-version = ['py37'] [tool.darker] isort = true [tool.ruff] select = ["E", "F", "B", "UP"] target-version = "py37" # Exceptions: # B9 flake8-bugbear opinionated warnings # E501 is line length (delegated to black) ignore = ["B9", "E501"] bioblend-1.2.0/pytest.ini000066400000000000000000000001411444761704300153270ustar00rootroot00000000000000[pytest] log_cli = true log_cli_level = INFO python_files = Test*.py testpaths = bioblend/_tests bioblend-1.2.0/run_bioblend_tests.sh000077500000000000000000000133551444761704300175340ustar00rootroot00000000000000#!/bin/sh set -e show_help () { echo "Usage: $0 -g GALAXY_DIR [-p PORT] [-e TOX_ENV] [-t BIOBLEND_TESTS] [-r GALAXY_REV] [-c] Run tests for BioBlend. Useful for Continuous Integration testing. *Please note* that this script overwrites the main.pid file and appends to the main.log file inside the specified Galaxy directory (-g). Options: -g GALAXY_DIR Path of the local Galaxy git repository. -p PORT Port to use for the Galaxy server. Defaults to 8080. -e TOX_ENV Work against specified tox environments. Defaults to py37. -t BIOBLEND_TESTS Subset of tests to run, e.g. 'tests/TestGalaxyObjects.py::TestHistory::test_create_delete' . Defaults to all tests. -r GALAXY_REV Branch or commit of the local Galaxy git repository to checkout. -v GALAXY_PYTHON Python to use for the Galaxy virtual environment. -c Force removal of the temporary directory created for Galaxy, even if some test failed." } get_abs_dirname () { # $1 : relative dirname cd "$1" && pwd } e_val=py37 GALAXY_PORT=8080 while getopts 'hcg:e:p:t:r:v:' option; do case $option in h) show_help exit;; c) c_val=1;; g) GALAXY_DIR=$(get_abs_dirname "$OPTARG");; e) e_val=$OPTARG;; p) GALAXY_PORT=$OPTARG;; t) t_val=$OPTARG;; r) r_val=$OPTARG;; v) GALAXY_PYTHON=$OPTARG;; *) show_help exit 1;; esac done if [ -z "$GALAXY_DIR" ]; then echo "Error: missing -g value." show_help exit 1 fi # Install BioBlend BIOBLEND_DIR=$(get_abs_dirname "$(dirname "$0")") if ! command -v tox >/dev/null; then cd "${BIOBLEND_DIR}" if [ ! -d .venv ]; then virtualenv -p python3 .venv fi . .venv/bin/activate python3 -m pip install --upgrade "tox>=1.8.0" fi # Setup Galaxy version cd "${GALAXY_DIR}" if [ -n "${r_val}" ]; then # Update repository (may change the sample files or the list of eggs) git fetch git checkout "${r_val}" if git show-ref -q --verify "refs/heads/${r_val}" 2>/dev/null; then # ${r_val} is a branch export GALAXY_VERSION=${r_val} git pull --ff-only fi else BRANCH=$(git rev-parse --abbrev-ref HEAD) case $BRANCH in dev | release_*) export GALAXY_VERSION=$BRANCH ;; esac fi # Setup Galaxy virtualenv if [ -n "${GALAXY_PYTHON}" ]; then if [ ! -d .venv ]; then virtualenv -p "${GALAXY_PYTHON}" .venv fi export GALAXY_PYTHON fi # Setup Galaxy master API key and admin user TEMP_DIR=$(mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir') echo "Created temporary directory $TEMP_DIR" mkdir "${TEMP_DIR}/config" "${TEMP_DIR}/database" printf "\n\n\n" "$TEMP_DIR/shed_tools" > "$TEMP_DIR/config/shed_tool_conf.xml" # Export BIOBLEND_ environment variables to be used in BioBlend tests BIOBLEND_GALAXY_MASTER_API_KEY=$(LC_ALL=C tr -dc A-Za-z0-9 < /dev/urandom | head -c 32) export BIOBLEND_GALAXY_MASTER_API_KEY export BIOBLEND_GALAXY_USER_EMAIL="${USER}@localhost.localdomain" DATABASE_CONNECTION=${DATABASE_CONNECTION:-"sqlite:///${TEMP_DIR}/database/universe.sqlite?isolation_level=IMMEDIATE"} # Update psycopg2 requirement to a version that doesn't use 2to3 for Galaxy release 19.05, see https://github.com/psycopg/psycopg2/issues/1419 sed -i.bak -e 's/psycopg2-binary==2.7.4/psycopg2-binary==2.8.4/' lib/galaxy/dependencies/conditional-requirements.txt # Start Galaxy and wait for successful server start export GALAXY_SKIP_CLIENT_BUILD=1 if grep -q wait_arg_set run.sh ; then # Galaxy 22.01 or earlier. # Export GALAXY_CONFIG_FILE environment variable to be used by run_galaxy.sh export GALAXY_CONFIG_FILE="${TEMP_DIR}/config/galaxy.ini" eval "echo \"$(cat "${BIOBLEND_DIR}/tests/template_galaxy.ini")\"" > "${GALAXY_CONFIG_FILE}" GALAXY_RUN_ALL=1 "${BIOBLEND_DIR}/run_galaxy.sh" --daemon --wait else # Galaxy is controlled via gravity, paste/uwsgi are replaced by gunicorn # and the `--wait` option does not work any more. # Export GALAXY_CONFIG_FILE environment variable to be used by run.sh export GALAXY_CONFIG_FILE="${TEMP_DIR}/config/galaxy.yml" if [ -f test/functional/tools/samples_tool_conf.xml ]; then # Galaxy 22.05 or earlier TEST_TOOLS_CONF_FILE=test/functional/tools/samples_tool_conf.xml else TEST_TOOLS_CONF_FILE=test/functional/tools/sample_tool_conf.xml fi eval "echo \"$(cat "${BIOBLEND_DIR}/tests/template_galaxy.yml")\"" > "${GALAXY_CONFIG_FILE}" export GRAVITY_STATE_DIR="${TEMP_DIR}/database/gravity" ./run.sh --daemon if ! .venv/bin/galaxyctl -h > /dev/null; then echo 'galaxyctl status not working' exit 1 fi while true; do sleep 1 if .venv/bin/galaxyctl status | grep -q 'gunicorn.*RUNNING'; then break else echo 'gunicorn not running yet' fi done while true; do sleep 1 if grep -q "[Ss]erving on http://127.0.0.1:${GALAXY_PORT}" "${GRAVITY_STATE_DIR}/log/gunicorn.log"; then break else echo 'Galaxy not serving yet' fi done fi export BIOBLEND_GALAXY_URL=http://localhost:${GALAXY_PORT} # Run the tests cd "${BIOBLEND_DIR}" set +e # don't stop the script if tox fails if [ -n "${t_val}" ]; then tox -e "${e_val}" -- "${t_val}" else tox -e "${e_val}" fi exit_code=$? # Stop Galaxy echo 'Stopping Galaxy' cd "${GALAXY_DIR}" if grep -q wait_arg_set run.sh ; then GALAXY_RUN_ALL=1 "${BIOBLEND_DIR}/run_galaxy.sh" --daemon stop else ./run.sh --daemon stop fi # Remove temporary directory if -c is specified or if all tests passed if [ -n "${c_val}" ] || [ $exit_code -eq 0 ]; then rm -rf "$TEMP_DIR" fi exit $exit_code bioblend-1.2.0/run_galaxy.sh000077500000000000000000000061631444761704300160200ustar00rootroot00000000000000#!/bin/sh #This script should be run from inside the Galaxy base directory # If there is a file that defines a shell environment specific to this # instance of Galaxy, source the file. if [ -z "$GALAXY_LOCAL_ENV_FILE" ]; then GALAXY_LOCAL_ENV_FILE='./config/local_env.sh' fi if [ -f $GALAXY_LOCAL_ENV_FILE ]; then . $GALAXY_LOCAL_ENV_FILE fi ./scripts/common_startup.sh || exit 1 # If there is a .venv/ directory, assume it contains a virtualenv that we # should run this instance in. if [ -d .venv ]; then echo "Activating virtualenv at %s/.venv\n" "$(pwd)" . .venv/bin/activate fi python ./scripts/check_python.py || exit 1 if [ -z "$GALAXY_CONFIG_FILE" ]; then if [ -f universe_wsgi.ini ]; then GALAXY_CONFIG_FILE=universe_wsgi.ini elif [ -f config/galaxy.ini ]; then GALAXY_CONFIG_FILE=config/galaxy.ini else GALAXY_CONFIG_FILE=config/galaxy.ini.sample fi export GALAXY_CONFIG_FILE fi if [ -n "$GALAXY_RUN_ALL" ]; then servers=$(sed -n 's/^\[server:\(.*\)\]/\1/ p' "$GALAXY_CONFIG_FILE" | xargs echo) if ! echo "$@" | grep -q 'daemon\|restart'; then echo "ERROR: \$GALAXY_RUN_ALL cannot be used without the '--daemon', '--stop-daemon', 'restart', 'start' or 'stop' arguments to run.sh" exit 1 fi (echo "$@" | grep -q -e '--daemon\|restart') && (echo "$@" | grep -q -e '--wait') WAIT=$? ARGS=$(echo "$@" | sed 's/--wait//') for server in $servers; do if [ $WAIT -eq 0 ]; then python ./scripts/paster.py serve "$GALAXY_CONFIG_FILE" --server-name="$server" --pid-file="$server.pid" --log-file="$server.log" $ARGS while true; do sleep 1 # Grab the current pid from the pid file and remove any trailing space if ! current_pid_in_file=$(sed -e 's/[[:space:]]*$//' "$server.pid"); then echo "A Galaxy process died, interrupting" >&2 exit 1 fi if [ -n "$current_pid_in_file" ]; then echo "Found PID $current_pid_in_file in '$server.pid', monitoring '$server.log'" else echo "No PID found in '$server.pid' yet" continue fi # Search for all pids in the logs and tail for the last one latest_pid=$(grep '^Starting server in PID [0-9]\+\.$' "$server.log" | sed 's/^Starting server in PID \([0-9]\{1,\}\).$/\1/' | tail -n 1) # If they're equivalent, then the current pid file agrees with our logs # and we've succesfully started [ -n "$latest_pid" ] && [ "$latest_pid" -eq "$current_pid_in_file" ] && break done echo else echo "Handling $server with log file $server.log..." python ./scripts/paster.py serve "$GALAXY_CONFIG_FILE" --server-name="$server" --pid-file="$server.pid" --log-file="$server.log" $@ fi done else # Handle only 1 server, whose name can be specified with --server-name parameter (defaults to "main") python ./scripts/paster.py serve "$GALAXY_CONFIG_FILE" $@ fi bioblend-1.2.0/setup.cfg000066400000000000000000000050121444761704300151210ustar00rootroot00000000000000[bdist_wheel] universal = 1 [flake8] exclude = .eggs .git .tox .venv build # E203 is whitespace before ':'; we follow black's formatting here. See https://black.readthedocs.io/en/stable/faq.html#why-are-flake8-s-e203-and-w503-violated # E501 is line length, managed by black # W503 is line breaks before binary operators, which has been reversed in PEP 8. ignore = E203,E501,E741,SFS3,W503 [metadata] author = Enis Afgan author_email = afgane@gmail.com classifiers = Development Status :: 5 - Production/Stable Intended Audience :: Developers License :: OSI Approved :: MIT License Operating System :: OS Independent Programming Language :: Python :: 3 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 Topic :: Scientific/Engineering :: Bio-Informatics description = Library for interacting with the Galaxy API license = MIT license_files = CITATION LICENSE long_description = file: README.rst long_description_content_type = text/x-rst maintainer = Nicola Soranzo maintainer_email = nicola.soranzo@earlham.ac.uk name = bioblend project_urls = Bug Tracker = https://github.com/galaxyproject/bioblend/issues Documentation = https://bioblend.readthedocs.io/ Source Code = https://github.com/galaxyproject/bioblend url = https://bioblend.readthedocs.io/ version = attr: bioblend.__version__ [mypy] check_untyped_defs = True disallow_incomplete_defs = True disallow_subclassing_any = True disallow_untyped_calls = True disallow_untyped_decorators = True disallow_untyped_defs = True ignore_missing_imports = True no_implicit_optional = True no_implicit_reexport = True pretty = True show_error_codes = True strict_equality = True warn_redundant_casts = True warn_unused_ignores = True warn_unreachable = True [mypy-bioblend._tests.*] disallow_untyped_defs = False # Allow testing that a function return value is None disable_error_code = func-returns-value [options] install_requires = requests>=2.20.0 requests-toolbelt>=0.5.1,!=0.9.0 tuspy typing-extensions packages = find: python_requires = >=3.7 [options.entry_points] console_scripts = bioblend-galaxy-tests = bioblend._tests.pytest_galaxy_test_wrapper:main [testing] [options.extras_require] testing = pytest [options.package_data] bioblend = _tests/data/* py.typed [options.packages.find] exclude = tests bioblend-1.2.0/setup.py000066400000000000000000000000461444761704300150140ustar00rootroot00000000000000from setuptools import setup setup() bioblend-1.2.0/tests000077700000000000000000000000001444761704300173642bioblend/_testsustar00rootroot00000000000000bioblend-1.2.0/tox.ini000066400000000000000000000010401444761704300146100ustar00rootroot00000000000000[tox] envlist = lint, py37 [testenv] commands = pytest {posargs} deps = pytest passenv = BIOBLEND_GALAXY_API_KEY BIOBLEND_GALAXY_MASTER_API_KEY BIOBLEND_GALAXY_URL BIOBLEND_GALAXY_USER_EMAIL BIOBLEND_TEST_JOB_TIMEOUT GALAXY_VERSION BIOBLEND_TOOLSHED_URL [testenv:lint] commands = ruff . flake8 . black --check --diff . isort --check --diff . mypy bioblend/ deps = black flake8 flake8-bugbear flake8-sfs isort mypy ruff types-requests skip_install = true