diff --git a/.github/codecov.yml b/.github/codecov.yml new file mode 100644 index 00000000000..69cb76019a4 --- /dev/null +++ b/.github/codecov.yml @@ -0,0 +1 @@ +comment: false diff --git a/.github/workflows/api.yaml b/.github/workflows/api.yaml index d4eb52b1e7a..6f8e17ddc1a 100644 --- a/.github/workflows/api.yaml +++ b/.github/workflows/api.yaml @@ -10,10 +10,13 @@ on: - 'client/**' - 'doc/**' - 'lib/galaxy_test/selenium/**' + schedule: + # Run at midnight UTC every Tuesday + - cron: '0 0 * * 2' env: + GALAXY_DEPENDENCIES_INSTALL_WEASYPRINT: '1' GALAXY_TEST_DBURI: 'postgresql://postgres:postgres@localhost:5432/galaxy?client_encoding=utf8' GALAXY_TEST_RAISE_EXCEPTION_ON_HISTORYLESS_HDA: '1' - GALAXY_DEPENDENCIES_INSTALL_WEASYPRINT: '1' GALAXY_CONFIG_SQLALCHEMY_WARN_20: '1' concurrency: group: api-${{ github.ref }} @@ -35,6 +38,10 @@ jobs: ports: - 5432:5432 steps: + - if: github.event_name == 'schedule' + run: | + echo "GALAXY_CONFIG_OVERRIDE_METADATA_STRATEGY=extended" >> $GITHUB_ENV + echo "GALAXY_CONFIG_OVERRIDE_OUTPUTS_TO_WORKING_DIRECTORY=true" >> $GITHUB_ENV - uses: actions/checkout@v2 with: path: 'galaxy root' @@ -53,11 +60,15 @@ jobs: - name: Cache galaxy venv uses: actions/cache@v2 with: - path: .venv + path: 'galaxy root/.venv' key: gxy-venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('galaxy root/requirements.txt') }}-api - name: Run tests - run: ./run_tests.sh --skip_flakey_fails -api + run: ./run_tests.sh --coverage --skip_flakey_fails -api working-directory: 'galaxy root' + - uses: codecov/codecov-action@v2 + with: + flags: api + working-directory: 'galaxy root' - uses: actions/upload-artifact@v2 if: failure() with: diff --git a/.github/workflows/converter_tests.yaml b/.github/workflows/converter_tests.yaml index db123cd8f48..4625cbe3b7e 100644 --- a/.github/workflows/converter_tests.yaml +++ b/.github/workflows/converter_tests.yaml @@ -8,6 +8,9 @@ on: paths-ignore: - 'client/**' - 'doc/**' + schedule: + # Run at midnight UTC every Tuesday + - cron: '0 0 * * 2' env: GALAXY_TEST_RAISE_EXCEPTION_ON_HISTORYLESS_HDA: '1' concurrency: @@ -21,6 +24,10 @@ jobs: matrix: python-version: ['3.7'] steps: + - if: github.event_name == 'schedule' + run: | + echo "GALAXY_CONFIG_OVERRIDE_METADATA_STRATEGY=extended" >> $GITHUB_ENV + echo "GALAXY_CONFIG_OVERRIDE_OUTPUTS_TO_WORKING_DIRECTORY=true" >> $GITHUB_ENV - uses: actions/checkout@v2 with: path: 'galaxy root' diff --git a/.github/workflows/cwl_conformance.yaml b/.github/workflows/cwl_conformance.yaml index 9e5cf7bdbb7..18c9596452c 100644 --- a/.github/workflows/cwl_conformance.yaml +++ b/.github/workflows/cwl_conformance.yaml @@ -53,11 +53,15 @@ jobs: - name: Cache galaxy venv uses: actions/cache@v2 with: - path: .venv + path: 'galaxy root/.venv' key: gxy-venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('galaxy root/requirements.txt') }} - name: Run tests - run: ./run_tests.sh --skip_flakey_fails -cwl lib/galaxy_test/api/cwl -- -m "${{ matrix.marker }} and ${{ matrix.conformance-version }}" + run: ./run_tests.sh --coverage --skip_flakey_fails -cwl lib/galaxy_test/api/cwl -- -m "${{ matrix.marker }} and ${{ matrix.conformance-version }}" working-directory: 'galaxy root' + - uses: codecov/codecov-action@v2 + with: + flags: cwl-conformance + working-directory: 'galaxy root' - uses: actions/upload-artifact@v2 if: failure() with: diff --git a/.github/workflows/first_startup.yaml b/.github/workflows/first_startup.yaml index 62f4b2647e5..94d78bf2c9e 100644 --- a/.github/workflows/first_startup.yaml +++ b/.github/workflows/first_startup.yaml @@ -22,10 +22,6 @@ jobs: fail-fast: false matrix: python-version: ['3.7', '3.10'] - webserver: ['uwsgi'] - include: - - python-version: '3.10' - webserver: 'dev' defaults: run: shell: bash -l {0} @@ -57,11 +53,6 @@ jobs: yarn-lock-file: 'galaxy root/client/yarn.lock' - name: Install tox run: pip install tox - # Use this job to test the latest migrations - - run: wget -q https://github.com/jmchilton/galaxy-downloads/raw/master/db_gx_rev_0141.sqlite - - run: mv db_gx_rev_0141.sqlite 'galaxy root'/database/universe.sqlite - name: run tests run: tox -e first_startup working-directory: 'galaxy root' - env: - APP_WEBSERVER: ${{ matrix.webserver }} diff --git a/.github/workflows/framework.yaml b/.github/workflows/framework.yaml index c0c3b997428..58a55bbc1e2 100644 --- a/.github/workflows/framework.yaml +++ b/.github/workflows/framework.yaml @@ -10,6 +10,9 @@ on: - 'client/**' - 'doc/**' - 'lib/galaxy_test/selenium/**' + schedule: + # Run at midnight UTC every Tuesday + - cron: '0 0 * * 2' env: GALAXY_TEST_DBURI: 'postgresql://postgres:postgres@localhost:5432/galaxy?client_encoding=utf8' GALAXY_TEST_RAISE_EXCEPTION_ON_HISTORYLESS_HDA: '1' @@ -33,6 +36,10 @@ jobs: ports: - 5432:5432 steps: + - if: github.event_name == 'schedule' + run: | + echo "GALAXY_CONFIG_OVERRIDE_METADATA_STRATEGY=extended" >> $GITHUB_ENV + echo "GALAXY_CONFIG_OVERRIDE_OUTPUTS_TO_WORKING_DIRECTORY=true" >> $GITHUB_ENV - uses: actions/checkout@v2 with: path: 'galaxy root' @@ -51,11 +58,15 @@ jobs: - name: Cache galaxy venv uses: actions/cache@v2 with: - path: .venv + path: 'galaxy root/.venv' key: gxy-venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('galaxy root/requirements.txt') }}-framework - name: Run tests - run: ./run_tests.sh --framework + run: ./run_tests.sh --coverage --framework working-directory: 'galaxy root' + - uses: codecov/codecov-action@v2 + with: + flags: framework + working-directory: 'galaxy root' - uses: actions/upload-artifact@v2 if: failure() with: diff --git a/.github/workflows/integration.yaml b/.github/workflows/integration.yaml index 25bb0d3783a..9be78baa1b2 100644 --- a/.github/workflows/integration.yaml +++ b/.github/workflows/integration.yaml @@ -10,6 +10,9 @@ on: - 'client/**' - 'doc/**' - 'lib/galaxy_test/selenium/**' + schedule: + # Run at midnight UTC every Tuesday + - cron: '0 0 * * 2' env: GALAXY_TEST_AMQP_URL: 'amqp://localhost:5672//' GALAXY_TEST_DBURI: 'postgresql://postgres:postgres@localhost:5432/galaxy?client_encoding=utf8' @@ -41,6 +44,11 @@ jobs: ports: - 5672:5672 steps: + - if: github.event_name == 'schedule' + run: | + echo "GALAXY_CONFIG_OVERRIDE_METADATA_STRATEGY=extended" >> $GITHUB_ENV + # Skip outputs_to_working_directory: true in integration tests, doesn't work with pulsar + # echo "GALAXY_CONFIG_OVERRIDE_OUTPUTS_TO_WORKING_DIRECTORY=true" >> $GITHUB_ENV - name: Prune unused docker image, volumes and containers run: docker system prune -a -f - name: Clean dotnet folder for space @@ -74,15 +82,19 @@ jobs: - name: Cache galaxy venv uses: actions/cache@v2 with: - path: .venv + path: 'galaxy root/.venv' key: gxy-venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('galaxy root/requirements.txt') }}-integration - name: Install ffmpeg run: sudo apt-get update && sudo apt-get -y install ffmpeg - name: Run tests run: | . .ci/minikube-test-setup/start_services.sh - ./run_tests.sh -integration test/integration -- --num-shards=4 --shard-id=${{ matrix.chunk }} + ./run_tests.sh --coverage -integration test/integration -- --num-shards=4 --shard-id=${{ matrix.chunk }} working-directory: 'galaxy root' + - uses: codecov/codecov-action@v2 + with: + flags: integration + working-directory: 'galaxy root' - uses: actions/upload-artifact@v2 if: failure() with: diff --git a/.github/workflows/integration_selenium.yaml b/.github/workflows/integration_selenium.yaml index 6ee668d9517..e3a45b93f87 100644 --- a/.github/workflows/integration_selenium.yaml +++ b/.github/workflows/integration_selenium.yaml @@ -6,6 +6,9 @@ on: pull_request: paths-ignore: - 'doc/**' + schedule: + # Run at midnight UTC every Tuesday + - cron: '0 0 * * 2' env: GALAXY_SKIP_CLIENT_BUILD: '0' GALAXY_TEST_DBURI: 'postgresql://postgres:postgres@localhost:5432/galaxy?client_encoding=utf8' @@ -13,6 +16,7 @@ env: GALAXY_TEST_SELENIUM_RETRIES: 1 YARN_INSTALL_OPTS: --frozen-lockfile GALAXY_CONFIG_SQLALCHEMY_WARN_20: '1' + GALAXY_DEPENDENCIES_INSTALL_WEASYPRINT: '1' concurrency: group: integration-selenium-${{ github.ref }} cancel-in-progress: true @@ -33,6 +37,10 @@ jobs: ports: - 5432:5432 steps: + - if: github.event_name == 'schedule' + run: | + echo "GALAXY_CONFIG_OVERRIDE_METADATA_STRATEGY=extended" >> $GITHUB_ENV + echo "GALAXY_CONFIG_OVERRIDE_OUTPUTS_TO_WORKING_DIRECTORY=true" >> $GITHUB_ENV - name: Prune unused docker image, volumes and containers run: docker system prune -a -f - uses: actions/checkout@v2 @@ -53,15 +61,19 @@ jobs: - name: Cache galaxy venv uses: actions/cache@v2 with: - path: .venv + path: 'galaxy root/.venv' key: gxy-venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('galaxy root/requirements.txt') }}-integration-selenium - uses: mvdbeek/gha-yarn-cache@master with: yarn-lock-file: 'galaxy root/client/yarn.lock' - uses: nanasess/setup-chromedriver@master - name: Run tests - run: ./run_tests.sh -integration test/integration_selenium + run: ./run_tests.sh --coverage -integration test/integration_selenium working-directory: 'galaxy root' + - uses: codecov/codecov-action@v2 + with: + flags: integration-selenium + working-directory: 'galaxy root' - uses: actions/upload-artifact@v2 if: failure() with: diff --git a/.github/workflows/jest.yaml b/.github/workflows/jest.yaml index 65bfd466843..fd9992f558d 100644 --- a/.github/workflows/jest.yaml +++ b/.github/workflows/jest.yaml @@ -3,9 +3,11 @@ on: push: paths: - 'client/**' + - '.github/workflows/jest.yaml' pull_request: paths: - 'client/**' + - '.github/workflows/jest.yaml' concurrency: group: client-${{ github.ref }} cancel-in-progress: true @@ -14,7 +16,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node: [14] + node: [16] steps: - uses: actions/checkout@v2 - name: Setup node diff --git a/.github/workflows/performance.yaml b/.github/workflows/performance.yaml index 596ee09871e..7b797e6360d 100644 --- a/.github/workflows/performance.yaml +++ b/.github/workflows/performance.yaml @@ -10,6 +10,9 @@ on: - 'client/**' - 'doc/**' - 'lib/galaxy_test/selenium/**' + schedule: + # Run at midnight UTC every Tuesday + - cron: '0 0 * * 2' env: GALAXY_TEST_DBURI: 'postgresql://postgres:postgres@localhost:5432/galaxy?client_encoding=utf8' concurrency: @@ -32,6 +35,10 @@ jobs: ports: - 5432:5432 steps: + - if: github.event_name == 'schedule' + run: | + echo "GALAXY_CONFIG_OVERRIDE_METADATA_STRATEGY=extended" >> $GITHUB_ENV + echo "GALAXY_CONFIG_OVERRIDE_OUTPUTS_TO_WORKING_DIRECTORY=true" >> $GITHUB_ENV - uses: actions/checkout@v2 with: path: 'galaxy root' @@ -50,7 +57,7 @@ jobs: - name: Cache galaxy venv uses: actions/cache@v2 with: - path: .venv + path: 'galaxy root/.venv' key: gxy-venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('galaxy root/requirements.txt') }}-performance - name: Run tests run: ./run_tests.sh --ci_test_metrics --structured_data_html --structured_data_report_file "test.json" --skip_flakey_fails -api lib/galaxy_test/performance diff --git a/.github/workflows/reports_startup.yaml b/.github/workflows/reports_startup.yaml index 59a42974dd3..afc25b4b56d 100644 --- a/.github/workflows/reports_startup.yaml +++ b/.github/workflows/reports_startup.yaml @@ -12,7 +12,6 @@ concurrency: group: reports-startup-${{ github.ref }} cancel-in-progress: true jobs: - test: name: Reports startup test runs-on: ubuntu-latest @@ -41,16 +40,16 @@ jobs: with: path: ~/.cache/pip key: pip-cache-${{ matrix.python-version }}-${{ hashFiles('galaxy root/requirements.txt') }} - - name: Cache tox env + - name: Cache galaxy venv uses: actions/cache@v2 with: - path: .tox - key: tox-cache-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('galaxy root/requirements.txt') }}-reports-startup + path: 'galaxy root/.venv' + key: gxy-venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('galaxy root/requirements.txt') }}-reports-startup - uses: mvdbeek/gha-yarn-cache@master with: yarn-lock-file: 'galaxy root/client/yarn.lock' - name: Install tox run: pip install tox - - name: run tests + - name: Run tests run: tox -e reports_startup working-directory: 'galaxy root' diff --git a/.github/workflows/selenium.yaml b/.github/workflows/selenium.yaml index 844f4cc0495..fad8b7dd169 100644 --- a/.github/workflows/selenium.yaml +++ b/.github/workflows/selenium.yaml @@ -6,6 +6,9 @@ on: pull_request: paths-ignore: - 'doc/**' + schedule: + # Run at midnight UTC every Tuesday + - cron: '0 0 * * 2' env: GALAXY_CONFIG_GALAXY_URL_PREFIX: '/galaxypf' GALAXY_TEST_DBURI: 'postgresql://postgres:postgres@localhost:5432/galaxy?client_encoding=utf8' @@ -36,6 +39,10 @@ jobs: ports: - 5432:5432 steps: + - if: github.event_name == 'schedule' + run: | + echo "GALAXY_CONFIG_OVERRIDE_METADATA_STRATEGY=extended" >> $GITHUB_ENV + echo "GALAXY_CONFIG_OVERRIDE_OUTPUTS_TO_WORKING_DIRECTORY=true" >> $GITHUB_ENV - uses: actions/checkout@v2 with: path: 'galaxy root' @@ -54,15 +61,19 @@ jobs: - name: Cache galaxy venv uses: actions/cache@v2 with: - path: .venv + path: 'galaxy root/.venv' key: gxy-venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('galaxy root/requirements.txt') }}-selenium - uses: mvdbeek/gha-yarn-cache@master with: yarn-lock-file: 'galaxy root/client/yarn.lock' - uses: nanasess/setup-chromedriver@master - name: Run tests - run: ./run_tests.sh -selenium lib/galaxy_test/selenium -- --num-shards=3 --shard-id=${{ matrix.chunk }} + run: ./run_tests.sh --coverage -selenium lib/galaxy_test/selenium -- --num-shards=3 --shard-id=${{ matrix.chunk }} working-directory: 'galaxy root' + - uses: codecov/codecov-action@v2 + with: + flags: selenium + working-directory: 'galaxy root' - uses: actions/upload-artifact@v2 if: failure() with: diff --git a/.github/workflows/selenium_beta.yaml b/.github/workflows/selenium_beta.yaml index 5eeb2a8dab3..986600591e4 100644 --- a/.github/workflows/selenium_beta.yaml +++ b/.github/workflows/selenium_beta.yaml @@ -6,6 +6,9 @@ on: pull_request: paths-ignore: - 'doc/**' + schedule: + # Run at midnight UTC every Tuesday + - cron: '0 0 * * 2' env: GALAXY_CONFIG_GALAXY_URL_PREFIX: '/galaxypf' GALAXY_TEST_DBURI: 'postgresql://postgres:postgres@localhost:5432/galaxy?client_encoding=utf8' @@ -37,6 +40,10 @@ jobs: ports: - 5432:5432 steps: + - if: github.event_name == 'schedule' + run: | + echo "GALAXY_CONFIG_OVERRIDE_METADATA_STRATEGY=extended" >> $GITHUB_ENV + echo "GALAXY_CONFIG_OVERRIDE_OUTPUTS_TO_WORKING_DIRECTORY=true" >> $GITHUB_ENV - uses: actions/checkout@v2 with: path: 'galaxy root' @@ -55,15 +62,19 @@ jobs: - name: Cache galaxy venv uses: actions/cache@v2 with: - path: .venv + path: 'galaxy root/.venv' key: gxy-venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('galaxy root/requirements.txt') }}-selenium-beta - uses: mvdbeek/gha-yarn-cache@master with: yarn-lock-file: 'galaxy root/client/yarn.lock' - uses: nanasess/setup-chromedriver@master - name: Run tests - run: ./run_tests.sh -selenium lib/galaxy_test/selenium -- --num-shards=3 --shard-id=${{ matrix.chunk }} + run: ./run_tests.sh --coverage -selenium lib/galaxy_test/selenium -- --num-shards=3 --shard-id=${{ matrix.chunk }} working-directory: 'galaxy root' + - uses: codecov/codecov-action@v2 + with: + flags: selenium-beta-history + working-directory: 'galaxy root' - uses: actions/upload-artifact@v2 if: failure() with: diff --git a/.github/workflows/toolshed.yaml b/.github/workflows/toolshed.yaml index b82f40963cd..e29fb389455 100644 --- a/.github/workflows/toolshed.yaml +++ b/.github/workflows/toolshed.yaml @@ -50,7 +50,7 @@ jobs: - name: Cache galaxy venv uses: actions/cache@v2 with: - path: .venv + path: 'galaxy root/.venv' key: gxy-venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('galaxy root/requirements.txt') }}-toolshed - name: Run tests run: './run_tests.sh -toolshed' diff --git a/.github/workflows/api_paste.yaml b/.github/workflows/unit-postgres.yaml similarity index 69% rename from .github/workflows/api_paste.yaml rename to .github/workflows/unit-postgres.yaml index 989cf1c0df4..122273c1310 100644 --- a/.github/workflows/api_paste.yaml +++ b/.github/workflows/unit-postgres.yaml @@ -1,4 +1,4 @@ -name: API (legacy paste) tests +name: Unit w/postgres tests on: push: paths-ignore: @@ -11,24 +11,21 @@ on: - 'doc/**' - 'lib/galaxy_test/selenium/**' env: - GALAXY_TEST_DBURI: 'postgresql://postgres:postgres@localhost:5432/galaxy?client_encoding=utf8' - GALAXY_TEST_USE_UVICORN: false - GALAXY_TEST_RAISE_EXCEPTION_ON_HISTORYLESS_HDA: '1' - GALAXY_CONFIG_SQLALCHEMY_WARN_20: '1' + GALAXY_TEST_DBURI: 'postgresql://postgres:postgres@localhost:5432/postgres?client_encoding=utf8' # using postgres as the db concurrency: - group: api-legacy-${{ github.ref }} + group: py-unit-postgres-${{ github.ref }} cancel-in-progress: true jobs: test: name: Test runs-on: ubuntu-latest strategy: + fail-fast: false matrix: python-version: ['3.7'] services: postgres: - # Run tests on the oldest Galaxy-supported version - image: postgres:9.6 + image: postgres:13 env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres @@ -54,13 +51,8 @@ jobs: - name: Cache galaxy venv uses: actions/cache@v2 with: - path: .venv - key: gxy-venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('galaxy root/requirements.txt') }}-api-paste - - name: Run tests - run: ./run_tests.sh --skip_flakey_fails -api + path: 'galaxy root/.venv' + key: gxy-venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('galaxy root/requirements.txt') }}-unit-postgres + - name: Run migration tests + run: ./run_tests.sh -unit test/unit/data/model/migrations/test_migrations.py working-directory: 'galaxy root' - - uses: actions/upload-artifact@v2 - if: failure() - with: - name: API (legacy paste) test results (${{ matrix.python-version }}) - path: 'galaxy root/run_api_tests.html' diff --git a/.github/workflows/unit.yaml b/.github/workflows/unit.yaml index 510d09c0469..7cf9bdb11b8 100644 --- a/.github/workflows/unit.yaml +++ b/.github/workflows/unit.yaml @@ -37,18 +37,20 @@ jobs: with: path: ~/.cache/pip key: pip-cache-${{ matrix.python-version }}-${{ hashFiles('galaxy root/requirements.txt') }} - - name: Cache tox env + - name: Cache galaxy venv uses: actions/cache@v2 with: - path: .tox - key: tox-cache-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('galaxy root/requirements.txt') }}-unit + path: 'galaxy root/.venv' + key: gxy-venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('galaxy root/requirements.txt') }}-unit - name: Install ffmpeg run: sudo apt-get update && sudo apt-get -y install ffmpeg - - name: Install tox - run: pip install tox - name: Run tests - run: tox -e unit + run: ./run_tests.sh --coverage -u working-directory: 'galaxy root' + - uses: codecov/codecov-action@v2 + with: + flags: py-unit + working-directory: 'galaxy root' - uses: actions/upload-artifact@v2 if: failure() with: diff --git a/.k8s_ci.Dockerfile b/.k8s_ci.Dockerfile index 06e5bbcaca8..0129ae9d591 100644 --- a/.k8s_ci.Dockerfile +++ b/.k8s_ci.Dockerfile @@ -22,6 +22,7 @@ ARG STAGE1_BASE=python:3.7-slim ARG FINAL_STAGE_BASE=$STAGE1_BASE ARG GALAXY_USER=galaxy ARG GALAXY_PLAYBOOK_REPO=https://github.com/galaxyproject/galaxy-docker-k8s +ARG GALAXY_PLAYBOOK_BRANCH=v2.0.0 ARG GIT_COMMIT=unspecified ARG BUILD_DATE=unspecified @@ -34,6 +35,7 @@ FROM $STAGE1_BASE AS stage1 ARG DEBIAN_FRONTEND=noninteractive ARG SERVER_DIR ARG GALAXY_PLAYBOOK_REPO +ARG GALAXY_PLAYBOOK_BRANCH # Init Env ENV LC_ALL=en_US.UTF-8 @@ -57,7 +59,7 @@ RUN set -xe; \ WORKDIR /tmp/ansible RUN rm -rf * ENV LC_ALL en_US.UTF-8 -RUN git clone --depth 1 $GALAXY_PLAYBOOK_REPO galaxy-docker +RUN git clone --depth 1 --branch $GALAXY_PLAYBOOK_BRANCH $GALAXY_PLAYBOOK_REPO galaxy-docker WORKDIR /tmp/ansible/galaxy-docker RUN ansible-galaxy install -r requirements.yml -p roles --force-with-deps @@ -176,6 +178,10 @@ COPY --chown=$GALAXY_USER:$GALAXY_USER --from=server_build $ROOT_DIR . COPY --chown=$GALAXY_USER:$GALAXY_USER --from=client_build $SERVER_DIR/static ./server/static WORKDIR $SERVER_DIR + +# The data in version.json will be displayed in Galaxy's /api/version endpoint +RUN printf "{\n \"git_commit\": \"$(cat GITREVISION)\",\n \"build_date\": \"$BUILD_DATE\",\n \"image_tag\": \"$IMAGE_TAG\"\n}\n" > version.json + EXPOSE 8080 USER $GALAXY_USER diff --git a/Makefile b/Makefile index 5e79240bcc3..47f22310abc 100644 --- a/Makefile +++ b/Makefile @@ -38,8 +38,8 @@ docs-develop: ## Fast doc generation and more warnings (for development) setup-venv: if [ ! -f $(VENV)/bin/activate ]; then bash scripts/common_startup.sh --dev-wheels; fi -diff-format: ## Format Python code changes - $(IN_VENV) darker -r $(TARGET_BRANCH) +diff-format: ## Format Python code changes since last commit + $(IN_VENV) darker . format: ## Format Python code base $(IN_VENV) isort . @@ -64,9 +64,6 @@ open-docs: docs _open-docs ## generate Sphinx HTML documentation and open in bro open-project: ## open project on github $(OPEN_RESOURCE) $(PROJECT_URL) -uwsgi-rebuild-validation: ## rebuild uwsgi_config.yml kwalify schema against latest uwsgi master. - $(CONFIG_MANAGE) build_uwsgi_yaml - tool-shed-config-validate: ## validate tool shed YAML configuration file $(CONFIG_MANAGE) validate tool_shed diff --git a/README.rst b/README.rst index f260d040869..34d382dc68b 100644 --- a/README.rst +++ b/README.rst @@ -24,12 +24,12 @@ Community support is available at `Galaxy Help Galaxy Quickstart ================= -Galaxy requires Python 3.6 . To check your Python version, run: +Galaxy requires Python 3.7 . To check your Python version, run: .. code:: console $ python -V - Python 3.6.10 + Python 3.7.6 Start Galaxy: diff --git a/client/gulpfile.js b/client/gulpfile.js index c71fa66555c..05796bd6f16 100644 --- a/client/gulpfile.js +++ b/client/gulpfile.js @@ -35,16 +35,16 @@ const DIST_PLUGIN_BUILD_IDS = ["new_user"]; const PLUGIN_BUILD_IDS = Array.prototype.concat(DIST_PLUGIN_BUILD_IDS, STATIC_PLUGIN_BUILD_IDS); -const paths = { - node_modules: "./node_modules", - plugin_dirs: [ +const PATHS = { + nodeModules: "./node_modules", + pluginDirs: [ "../config/plugins/{visualizations,interactive_environments,welcome_page}/*/static/**/*", "../config/plugins/{visualizations,interactive_environments,welcome_page}/*/*/static/**/*", ], - plugin_build_modules: [ + pluginBuildModules: [ `../config/plugins/{visualizations,welcome_page}/{${PLUGIN_BUILD_IDS.join(",")}}/package.json`, ], - lib_locs: { + stagedLibraries: { // This is a stepping stone towards having all this staged // automatically. Eventually, this dictionary and staging step will // not be necessary. @@ -57,13 +57,12 @@ const paths = { requirejs: ["require.js", "require.js"], underscore: ["underscore.js", "underscore.js"], }, - libs: ["src/libs/**/*.js"], }; function stageLibs(callback) { - Object.keys(paths.lib_locs).forEach((lib) => { - var p1 = path.resolve(path.join(paths.node_modules, lib, paths.lib_locs[lib][0])); - var p2 = path.resolve(path.join("src", "libs", paths.lib_locs[lib][1])); + Object.keys(PATHS.stagedLibraries).forEach((lib) => { + var p1 = path.resolve(path.join(PATHS.nodeModules, lib, PATHS.stagedLibraries[lib][0])); + var p2 = path.resolve(path.join("src", "libs", PATHS.stagedLibraries[lib][1])); if (fs.existsSync(p1)) { del.sync(p2); fs.createReadStream(p1).pipe(fs.createWriteStream(p2)); @@ -78,45 +77,49 @@ function stageLibs(callback) { } function fonts() { - return src(path.resolve(path.join(paths.node_modules, "font-awesome/fonts/**/*"))).pipe( + return src(path.resolve(path.join(PATHS.nodeModules, "font-awesome/fonts/**/*"))).pipe( dest("../static/images/fonts") ); } function stagePlugins() { - return src(paths.plugin_dirs).pipe(dest("../static/plugins/")); + return src(PATHS.pluginDirs).pipe(dest("../static/plugins/")); } -function buildPlugins(callback) { +function buildPlugins(callback, forceRebuild) { /* - * Walk plugin_build_modules glob and attempt to build modules. + * Walk pluginBuildModules glob and attempt to build modules. * */ - paths.plugin_build_modules.map((build_module) => { + PATHS.pluginBuildModules.map((build_module) => { glob(build_module, {}, (er, files) => { files.map((file) => { - let skip_build = false; + let skipBuild = false; const f = path.join(process.cwd(), file).slice(0, -12); - const plugin_name = path.dirname(file).split(path.sep).pop(); - const hash_file_path = path.join( + const pluginName = path.dirname(file).split(path.sep).pop(); + const hashFilePath = path.join( f, - DIST_PLUGIN_BUILD_IDS.indexOf(plugin_name) > -1 ? "dist" : "static", + DIST_PLUGIN_BUILD_IDS.indexOf(pluginName) > -1 ? "dist" : "static", "plugin_build_hash.txt" ); - if (fs.existsSync(hash_file_path)) { - skip_build = - child_process.spawnSync("git", ["diff", "--quiet", `$(cat ${hash_file_path})`, "--", f], { - stdio: "inherit", - shell: true, - }).status === 0; + if (!!forceRebuild) { + skipBuild = false; } else { - console.log(`No build hashfile detected for ${plugin_name}, generating now.`); + if (fs.existsSync(hashFilePath)) { + skipBuild = + child_process.spawnSync("git", ["diff", "--quiet", `$(cat ${hashFilePath})`, "--", f], { + stdio: "inherit", + shell: true, + }).status === 0; + } else { + console.log(`No build hashfile detected for ${pluginName}, generating now.`); + } } - if (skip_build) { - console.log(`No changes detected for ${plugin_name}`); + if (skipBuild) { + console.log(`No changes detected for ${pluginName}`); } else { - console.log(`Installing Dependencies for ${plugin_name}`); + console.log(`Installing Dependencies for ${pluginName}`); child_process.spawnSync( "yarn", ["install", "--production=false", "--network-timeout=300000", "--check-files"], @@ -126,9 +129,19 @@ function buildPlugins(callback) { shell: true, } ); - console.log(`Building ${plugin_name}`); - child_process.spawnSync("yarn", ["build"], { cwd: f, stdio: "inherit", shell: true }); - child_process.exec(`(git rev-parse HEAD 2>/dev/null || echo \`\`) > ${hash_file_path}`); + console.log(`Building ${pluginName}`); + + if ( + child_process.spawnSync("yarn", ["build"], { cwd: f, stdio: "inherit", shell: true }).status === + 0 + ) { + console.log(`Successfully built, saving build state to ${hashFilePath}`); + child_process.exec(`(git rev-parse HEAD 2>/dev/null || echo \`\`) > ${hashFilePath}`); + } else { + console.error( + `Error building ${pluginName}, not saving build state. Please report this issue to the Galaxy Team.` + ); + } } }); }); @@ -136,12 +149,17 @@ function buildPlugins(callback) { return callback(); } +function forceBuildPlugins(callback) { + return buildPlugins(callback, true); +} + function cleanPlugins() { return del(["../static/plugins/{visualizations,interactive_environments,welcome_page}/*"], { force: true }); } const client = parallel(fonts, stageLibs); const plugins = series(buildPlugins, cleanPlugins, stagePlugins); +const pluginsRebuild = series(forceBuildPlugins, cleanPlugins, stagePlugins); function watchPlugins() { const BUILD_PLUGIN_WATCH_GLOB = [ @@ -152,5 +170,6 @@ function watchPlugins() { module.exports.client = client; module.exports.plugins = plugins; +module.exports.pluginsRebuild = pluginsRebuild; module.exports.watchPlugins = watchPlugins; module.exports.default = parallel(client, plugins); diff --git a/client/package.json b/client/package.json index f03123c8ffb..a454c820aba 100644 --- a/client/package.json +++ b/client/package.json @@ -20,7 +20,7 @@ "@fortawesome/free-regular-svg-icons": "^5.15.4", "@fortawesome/free-solid-svg-icons": "^5.15.4", "@fortawesome/vue-fontawesome": "^2.0.6", - "@galaxyproject/bootstrap-tour": "^0.12.1", + "@galaxyproject/bootstrap-tour": "0.12.2", "@handsontable/vue": "^2.0.0-beta1", "@hirez_io/observer-spy": "^2.1.2", "@johmun/vue-tags-input": "^2.1.0", @@ -57,17 +57,12 @@ "jspdf": "^2.5.1", "linkify-html": "^3.0.5", "linkifyjs": "^3.0.5", + "localforage": "^1.10.0", "markdown-it": "^12.3.2", "markdown-it-regexp": "^0.4.0", "moment": "2.29.1", "object-hash": "^2.2.0", "popper.js": "^1.16.1", - "pouchdb": "^7.2.2", - "pouchdb-adapter-memory": "^7.2.2", - "pouchdb-debug": "^7.2.1", - "pouchdb-erase": "^1.0.2", - "pouchdb-find": "^7.2.2", - "pouchdb-upsert": "^2.2.0", "pretty-bytes": "^5.6.0", "proper-skip-list": "^4.1.0", "pyre-to-regexp": "^0.0.5", @@ -97,12 +92,12 @@ "vue-router": "^3.5.3", "vue-rx": "^6.2.0", "vue-scrollto": "^2.20.0", + "vue-virtual-scroll-list": "^2.3.3", "vuedraggable": "2.24.3", "vueisotope": "^3.1.2", "vuex": "^3.6.2", "vuex-cache": "^3.4.0", - "vuex-persist": "^3.1.3", - "vuex-persistedstate": "^4.1.0", + "vuex-persist": "2.3", "xml-beautifier": "^0.5.0" }, "scripts": { @@ -160,7 +155,7 @@ "jest-raw-loader": "^1.0.1", "jest-transform-yaml": "1.0.0", "json-loader": "^0.5.7", - "karma": "^6.3.13", + "karma": "^6.3.16", "karma-chrome-launcher": "^3.1.0", "karma-polyfill": "^1.1.0", "karma-qunit": "^4.1.2", diff --git a/client/src/components/Collections/PairedListCollectionCreator.test.js b/client/src/components/Collections/PairedListCollectionCreator.test.js index ce54a281bd0..393dd33884f 100644 --- a/client/src/components/Collections/PairedListCollectionCreator.test.js +++ b/client/src/components/Collections/PairedListCollectionCreator.test.js @@ -45,12 +45,11 @@ describe("PairedListCollectionCreator", () => { }); await wrapper.vm.$nextTick(); //change filter to .1.fastq/.2.fastq - wrapper.find("div.forward-unpaired-filter > div.input-group-append > button").trigger("click"); - wrapper + await wrapper.find("div.forward-unpaired-filter > div.input-group-append > button").trigger("click"); + await wrapper .findAll("div.dropdown-menu > a.dropdown-item") .wrappers.find((e) => e.text() == ".1.fastq") .trigger("click"); - await wrapper.vm.$nextTick(); //assert forward filter const forwardFilter = wrapper.find("div.forward-unpaired-filter > input").element.value; expect(forwardFilter).toBe(".1.fastq"); @@ -58,8 +57,7 @@ describe("PairedListCollectionCreator", () => { const reverseFilter = wrapper.find("div.reverse-unpaired-filter > input").element.value; expect(reverseFilter).toBe(".2.fastq"); // click Autopair - wrapper.find("a.autopair-link").trigger("click"); - await wrapper.vm.$nextTick(); + await wrapper.find("a.autopair-link").trigger("click"); //assert pair-name longer name const pairname = wrapper.find("span.pair-name"); expect(pairname.text()).toBe("DP134_1_FS_PSII_FSB_42C_A10"); @@ -83,12 +81,11 @@ describe("PairedListCollectionCreator", () => { }); await wrapper.vm.$nextTick(); //change filter to .1.fastq/.2.fastq - wrapper.find("div.forward-unpaired-filter > div.input-group-append > button").trigger("click"); - wrapper + await wrapper.find("div.forward-unpaired-filter > div.input-group-append > button").trigger("click"); + await wrapper .findAll("div.dropdown-menu > a.dropdown-item") .wrappers.find((e) => e.text() == ".1.fastq") .trigger("click"); - await wrapper.vm.$nextTick(); //assert forward filter const forwardFilter = wrapper.find("div.forward-unpaired-filter > input").element.value; expect(forwardFilter).toBe(".1.fastq"); @@ -96,8 +93,7 @@ describe("PairedListCollectionCreator", () => { const reverseFilter = wrapper.find("div.reverse-unpaired-filter > input").element.value; expect(reverseFilter).toBe(".2.fastq"); // click Autopair - wrapper.find("a.autopair-link").trigger("click"); - await wrapper.vm.$nextTick(); + await wrapper.find("a.autopair-link").trigger("click"); //assert pair-name longer name const pairname = wrapper.find("span.pair-name"); expect(pairname.text()).toBe("UII_moo_1"); @@ -121,10 +117,8 @@ describe("PairedListCollectionCreator", () => { }); await wrapper.vm.$nextTick(); //change filter to _R1/_R2 - wrapper.find("div.forward-unpaired-filter > input").setValue("_R1"); - await wrapper.vm.$nextTick(); - wrapper.find("div.reverse-unpaired-filter > input").setValue("_R2"); - await wrapper.vm.$nextTick(); + await wrapper.find("div.forward-unpaired-filter > input").setValue("_R1"); + await wrapper.find("div.reverse-unpaired-filter > input").setValue("_R2"); //assert forward filter const forwardFilter = wrapper.find("div.forward-unpaired-filter > input").element.value; expect(forwardFilter).toBe("_R1"); @@ -132,8 +126,7 @@ describe("PairedListCollectionCreator", () => { const reverseFilter = wrapper.find("div.reverse-unpaired-filter > input").element.value; expect(reverseFilter).toBe("_R2"); // click Autopair - wrapper.find("a.autopair-link").trigger("click"); - await wrapper.vm.$nextTick(); + await wrapper.find("a.autopair-link").trigger("click"); //assert all pairs matched expect(wrapper.findAll("li.dataset unpaired").length == 0).toBeTruthy(); }); diff --git a/client/src/components/Common/ButtonSpinner.vue b/client/src/components/Common/ButtonSpinner.vue index fe45de35fb9..56531098ef6 100644 --- a/client/src/components/Common/ButtonSpinner.vue +++ b/client/src/components/Common/ButtonSpinner.vue @@ -2,13 +2,7 @@ Please wait... - + {{ title }} @@ -37,10 +31,6 @@ export default { type: String, default: null, }, - disabled: { - type: Boolean, - default: false, - }, }, }; diff --git a/client/src/components/DatasetInformation/DatasetAttributes.test.js b/client/src/components/DatasetInformation/DatasetAttributes.test.js index 58d99ddfbca..049facec4e2 100644 --- a/client/src/components/DatasetInformation/DatasetAttributes.test.js +++ b/client/src/components/DatasetInformation/DatasetAttributes.test.js @@ -1,4 +1,3 @@ -import Vue from "vue"; import axios from "axios"; import { mount } from "@vue/test-utils"; import { getLocalVue } from "jest/helpers"; @@ -9,8 +8,8 @@ import flushPromises from "flush-promises"; const localVue = getLocalVue(); -function buildWrapper(conversion_disable = false) { - return mount(DatasetAttributes, { +async function buildWrapper(conversion_disable = false) { + const wrapper = mount(DatasetAttributes, { propsData: { datasetId: "dataset_id", }, @@ -29,14 +28,15 @@ function buildWrapper(conversion_disable = false) { FormElement: false, }, }); + await flushPromises(); + return wrapper; } describe("DatasetAttributes", () => { it("check rendering", async () => { const axiosMock = new MockAdapter(axios); axiosMock.onPut(`/dataset/set_edit`).reply(200, { message: "success", status: "success" }); - const wrapper = buildWrapper(); - await Vue.nextTick(); + const wrapper = await buildWrapper(); expect(wrapper.findAll("button").length).toBe(6); expect(wrapper.findAll("#attribute_text").length).toBe(1); expect(wrapper.findAll("#conversion_text").length).toBe(1); @@ -50,8 +50,7 @@ describe("DatasetAttributes", () => { }); it("check rendering without conversion option", async () => { - const wrapper = buildWrapper(true); - await Vue.nextTick(); + const wrapper = await buildWrapper(true); expect(wrapper.findAll("button").length).toBe(5); expect(wrapper.findAll("#attribute_text").length).toBe(1); expect(wrapper.findAll("#conversion_text").length).toBe(0); diff --git a/client/src/components/DatasetInformation/DatasetAttributes.vue b/client/src/components/DatasetInformation/DatasetAttributes.vue index 4602c63fa88..b4cd0e20406 100644 --- a/client/src/components/DatasetInformation/DatasetAttributes.vue +++ b/client/src/components/DatasetInformation/DatasetAttributes.vue @@ -4,7 +4,7 @@ {{ messageText | l }} - +
diff --git a/client/src/components/Details/DatasetDetails.vue b/client/src/components/DatasetInformation/DatasetDetails.vue similarity index 96% rename from client/src/components/Details/DatasetDetails.vue rename to client/src/components/DatasetInformation/DatasetDetails.vue index 6d622e18068..83066d5e2c7 100644 --- a/client/src/components/Details/DatasetDetails.vue +++ b/client/src/components/DatasetInformation/DatasetDetails.vue @@ -2,7 +2,7 @@ + v-slot="{ result: dataset, loading: isDatasetLoading, error: datasetLoadingError }">
@@ -10,8 +10,8 @@ + auto-refresh + v-slot="{ result: job, loading: isJobLoading }">
diff --git a/client/src/components/DatasetInformation/DatasetError.test.js b/client/src/components/DatasetInformation/DatasetError.test.js index 1d1ccfa44f3..39c8b513a29 100644 --- a/client/src/components/DatasetInformation/DatasetError.test.js +++ b/client/src/components/DatasetInformation/DatasetError.test.js @@ -29,7 +29,6 @@ function buildWrapper(has_duplicate_inputs = true, has_empty_inputs = true, user result: { has_duplicate_inputs: has_duplicate_inputs, has_empty_inputs: has_empty_inputs }, }), DatasetProvider: MockProvider({ - resultLabel: "item", result: { id: "dataset_id", creating_job: "creating_job" }, }), FontAwesomeIcon: false, diff --git a/client/src/components/DatasetInformation/DatasetError.vue b/client/src/components/DatasetInformation/DatasetError.vue index 8ba10deb55b..92228b3b6b6 100644 --- a/client/src/components/DatasetInformation/DatasetError.vue +++ b/client/src/components/DatasetInformation/DatasetError.vue @@ -4,7 +4,7 @@

Failed to access Dataset details.

{{ errorMessage }} - + - +

Dataset Information

diff --git a/client/src/components/Form/Elements/FormBoolean.test.js b/client/src/components/Form/Elements/FormBoolean.test.js index 90149c2442c..5e7855ee44f 100644 --- a/client/src/components/Form/Elements/FormBoolean.test.js +++ b/client/src/components/Form/Elements/FormBoolean.test.js @@ -1,7 +1,6 @@ import { mount } from "@vue/test-utils"; import { getLocalVue } from "jest/helpers"; import FormBoolean from "./FormBoolean"; -import Vue from "vue"; const localVue = getLocalVue(); @@ -20,12 +19,11 @@ describe("FormBoolean", () => { it("check initial value and value change", async () => { const input = wrapper.find("input"); expect(wrapper.vm.currentValue).toBe(false); - wrapper.setProps({ value: true }); - await Vue.nextTick(); + await wrapper.setProps({ value: true }); expect(wrapper.vm.currentValue).toBe(true); - input.trigger("click"); + await input.trigger("click"); expect(input.element.checked).toBe(false); - input.trigger("click"); + await input.trigger("click"); expect(input.element.checked).toBe(true); expect(wrapper.emitted().input[0][0]).toBe(true); }); diff --git a/client/src/components/Form/Elements/FormNumber.test.js b/client/src/components/Form/Elements/FormNumber.test.js index c4dafc03082..2805a76ecac 100644 --- a/client/src/components/Form/Elements/FormNumber.test.js +++ b/client/src/components/Form/Elements/FormNumber.test.js @@ -52,9 +52,8 @@ describe("FormInput", () => { const props = { value: 50, type: "float", min: 10, max: 100 }; const wrapper = await mountFormNumber(props); const input = await getInput(wrapper); - input.setValue(number); - input.trigger("change"); - await flushPromises(); + await input.setValue(number); + await input.trigger("change"); const alert = await getAlert(wrapper); expect(alert.exists()).toBeTruthy(); expect(alert.text().includes(`${number} is out`)).toBeTruthy(); diff --git a/client/src/components/Form/FormElement.test.js b/client/src/components/Form/FormElement.test.js index 90679aa5ae3..a8589f74ddd 100644 --- a/client/src/components/Form/FormElement.test.js +++ b/client/src/components/Form/FormElement.test.js @@ -1,6 +1,5 @@ import { mount } from "@vue/test-utils"; import { getLocalVue } from "jest/helpers"; -import Vue from "vue"; import FormElement from "./FormElement"; const localVue = getLocalVue(); @@ -46,19 +45,17 @@ describe("FormElement", () => { expect(wrapper.find(".ui-form-title-text").text()).toEqual("title_text"); expect(wrapper.findAll("span[title='Disable']").length).toEqual(1); expect(wrapper.emitted().input[0][0]).toEqual("initial_value"); - wrapper.find(".ui-form-collapsible-icon").trigger("click"); + await wrapper.find(".ui-form-collapsible-icon").trigger("click"); expect(wrapper.emitted().input[1][0]).toEqual("collapsible_value"); expect(wrapper.emitted().input[1][1]).toEqual("input"); - await Vue.nextTick(); await wrapper.setProps({ collapsedEnableText: "Enable Collapsible", collapsedDisableText: "Disable Collapsible", }); expect(wrapper.findAll("span[title='Enable Collapsible']").length).toEqual(1); expect(wrapper.findAll("span[title='Disable Collapsible']").length).toEqual(0); - wrapper.find(".ui-form-collapsible-icon").trigger("click"); + await wrapper.find(".ui-form-collapsible-icon").trigger("click"); expect(wrapper.emitted().input[2][0]).toEqual("default_value"); - await Vue.nextTick(); expect(wrapper.findAll("span[title='Disable Collapsible']").length).toEqual(1); expect(wrapper.findAll("span[title='Enable Collapsible']").length).toEqual(0); }); diff --git a/client/src/components/History/Content/Collection/CollectionDescription.vue b/client/src/components/History/Content/Collection/CollectionDescription.vue new file mode 100644 index 00000000000..f38dff0cacf --- /dev/null +++ b/client/src/components/History/Content/Collection/CollectionDescription.vue @@ -0,0 +1,35 @@ + + + diff --git a/client/src/components/History/Content/ContentItem.vue b/client/src/components/History/Content/ContentItem.vue new file mode 100644 index 00000000000..a52bc326cd4 --- /dev/null +++ b/client/src/components/History/Content/ContentItem.vue @@ -0,0 +1,218 @@ + + + + diff --git a/client/src/components/History/Content/Dataset/DatasetActions.vue b/client/src/components/History/Content/Dataset/DatasetActions.vue new file mode 100644 index 00000000000..5a8cd3bd2c8 --- /dev/null +++ b/client/src/components/History/Content/Dataset/DatasetActions.vue @@ -0,0 +1,159 @@ + + + diff --git a/client/src/components/History/Content/Dataset/DatasetDetails.vue b/client/src/components/History/Content/Dataset/DatasetDetails.vue new file mode 100644 index 00000000000..b75532b1deb --- /dev/null +++ b/client/src/components/History/Content/Dataset/DatasetDetails.vue @@ -0,0 +1,52 @@ + + + diff --git a/client/src/components/History/ExpandedItems.js b/client/src/components/History/Content/ExpandedItems.js similarity index 100% rename from client/src/components/History/ExpandedItems.js rename to client/src/components/History/Content/ExpandedItems.js diff --git a/client/src/components/History/Content/GenericElement.vue b/client/src/components/History/Content/GenericElement.vue new file mode 100644 index 00000000000..98d7690927f --- /dev/null +++ b/client/src/components/History/Content/GenericElement.vue @@ -0,0 +1,37 @@ + + + diff --git a/client/src/components/History/Content/GenericItem.vue b/client/src/components/History/Content/GenericItem.vue new file mode 100644 index 00000000000..e0b75c4c55a --- /dev/null +++ b/client/src/components/History/Content/GenericItem.vue @@ -0,0 +1,76 @@ + + + diff --git a/client/src/components/History/SelectedItems.js b/client/src/components/History/Content/SelectedItems.js similarity index 77% rename from client/src/components/History/SelectedItems.js rename to client/src/components/History/Content/SelectedItems.js index 6583239c704..27fdc74632b 100644 --- a/client/src/components/History/SelectedItems.js +++ b/client/src/components/History/Content/SelectedItems.js @@ -1,5 +1,5 @@ /** - * This is simple list management for the monent, but if I remember right we have plans for a more + * This is simple list management for the moment, but if I remember right we have plans for a more * abstracted selection scheme in the future for purposes of handling large bulk operations on the * server, this is a good place to implement that logic. */ @@ -28,10 +28,10 @@ export default { const key = this.getItemKey(item); return this.items.has(key); }, - setSelected(item, bSelected) { + setSelected(item, selected) { const key = this.getItemKey(item); const newSelected = new Map(this.items); - bSelected ? newSelected.set(key, item) : newSelected.delete(key); + selected ? newSelected.set(key, item) : newSelected.delete(key); this.items = newSelected; }, selectItems(items = []) { @@ -42,14 +42,6 @@ export default { }); this.items = new Map(newEntries); }, - // unselectItems(items = []) { - // const doomedKeys = items.map((item) => this.getItemKey(item)); - // const existing = this.items.entries(); - // const newEntries = Array.from(existing).filter(([key]) => { - // return !doomedKeys.includes(key); - // }); - // this.items = new Map(newEntries); - // }, reset() { this.items = new Map(); }, @@ -77,7 +69,6 @@ export default { showSelection: this.showSelection, setShowSelection: this.setShowSelection, selectItems: this.selectItems, - // unselectItems: this.unselectItems, isSelected: this.isSelected, setSelected: this.setSelected, resetSelection: this.reset, diff --git a/client/src/components/History/Content/contentState.js b/client/src/components/History/Content/contentState.js new file mode 100644 index 00000000000..b59fdcbeae8 --- /dev/null +++ b/client/src/components/History/Content/contentState.js @@ -0,0 +1,67 @@ +/* + Client representation of state and state messages. See: https://github.com/galaxyproject/galaxy/blob/dev/lib/galaxy/model/__init__.py#L3292 + for a list of available states. +*/ +export default { + /** deleted while uploading */ + discarded: { + status: "danger", + text: "The job creating this dataset was cancelled before completion.", + icon: "exclamation-triangle", + }, + /** has no data */ + empty: { + status: "success", + text: "No data", + }, + /** the tool producing this dataset failed */ + error: { + status: "danger", + text: "An error occurred with this dataset", + icon: "exclamation-triangle", + }, + /** metadata discovery/setting failed or errored (but otherwise ok) */ + failed_metadata: { + status: "danger", + }, + /** was created without a tool */ + new: { + status: "warning", + text: "This is a new dataset and not all of its data are available yet.", + icon: "clock", + }, + /** has successfully completed running */ + ok: { + status: "success", + }, + /** the job that will produce the dataset paused */ + paused: { + status: "info", + text: "This job is paused. Use the 'Resume Paused Jobs' in the history menu to resume.", + icon: "pause", + }, + /** the job that will produce the dataset queued in the runner */ + queued: { + status: "warning", + text: "This job is waiting to run.", + icon: "clock", + }, + /** the job that will produce the dataset is running */ + running: { + status: "warning", + text: "This job is currently running.", + icon: "clock", + }, + /** metadata for the dataset is being discovered/set */ + setting_metadata: { + status: "warning", + text: "Metadata is being auto-detected.", + icon: "clock", + }, + /** is uploading and not ready */ + upload: { + status: "warning", + text: "This dataset is currently uploading.", + icon: "clock", + }, +}; diff --git a/client/src/components/History/ContentItem/CollectionContentItem.js b/client/src/components/History/ContentItem/CollectionContentItem.js deleted file mode 100644 index eb785ff8d8e..00000000000 --- a/client/src/components/History/ContentItem/CollectionContentItem.js +++ /dev/null @@ -1,19 +0,0 @@ -import ContentItem from "./ContentItem"; - -export default { - mixins: [ContentItem], - - computed: { - contentItemComponent() { - const { history_content_type } = this.item; - switch (history_content_type) { - case "dataset": - return "Subdataset"; - case "dataset_collection": - return "Subcollection"; - default: - return "Placeholder"; - } - }, - }, -}; diff --git a/client/src/components/History/ContentItem/ContentItem.js b/client/src/components/History/ContentItem/ContentItem.js deleted file mode 100644 index 1dd82051f2a..00000000000 --- a/client/src/components/History/ContentItem/ContentItem.js +++ /dev/null @@ -1,83 +0,0 @@ -/** - * This is a single item in the list. The main job of the contentItem is to - * pick a specific selector for the :is attribute of the generic component - * placeholder. This should be a dataset, collection, or other component - * depending on the specific list implementation. - * - * The mixin also pulls out "selected", and "expanded" properties which are - * derived from the current component state rather than the data itself. - */ - -import Placeholder from "./Placeholder"; -import Dataset from "./Dataset"; -import DatasetCollection from "./DatasetCollection"; -import Subdataset from "./Subdataset"; -import Subcollection from "./Subcollection"; - -export default { - template: ` - - `, - - components: { - Placeholder, - Dataset, - DatasetCollection, - Subdataset, - Subcollection, - }, - - props: { - item: { type: Object, required: true }, - index: { type: Number, required: false, default: null }, - rowKey: { type: [Number, String], required: false, default: "" }, - writable: { type: Boolean, required: false, default: true }, - }, - - data: () => ({ - suppressFocus: false, - }), - - methods: { - setFocus(index) { - if (this.suppressFocus) { - return; - } - const ul = this.$el.closest(".scroller"); - const el = ul.querySelector(`[tabindex="${index}"]`); - if (el) { - el.focus(); - } - }, - }, - - computed: { - loading() { - return !this.item; - }, - contentItemComponent() { - // override me - return "Placeholder"; - }, - }, - - created() { - this.$root.$on("bv::dropdown::show", () => (this.suppressFocus = true)); - this.$root.$on("bv::dropdown::hide", () => (this.suppressFocus = false)); - }, -}; diff --git a/client/src/components/History/ContentItem/Dataset/Dataset.vue b/client/src/components/History/ContentItem/Dataset/Dataset.vue deleted file mode 100644 index 6aca4d447a2..00000000000 --- a/client/src/components/History/ContentItem/Dataset/Dataset.vue +++ /dev/null @@ -1,56 +0,0 @@ - - - diff --git a/client/src/components/History/ContentItem/Dataset/DatasetMenu.vue b/client/src/components/History/ContentItem/Dataset/DatasetMenu.vue deleted file mode 100644 index e92327216b6..00000000000 --- a/client/src/components/History/ContentItem/Dataset/DatasetMenu.vue +++ /dev/null @@ -1,294 +0,0 @@ - - - diff --git a/client/src/components/History/ContentItem/Dataset/DatasetUI.vue b/client/src/components/History/ContentItem/Dataset/DatasetUI.vue deleted file mode 100644 index d485504b441..00000000000 --- a/client/src/components/History/ContentItem/Dataset/DatasetUI.vue +++ /dev/null @@ -1,249 +0,0 @@ - - - - - diff --git a/client/src/components/History/ContentItem/Dataset/Summary/Discarded.vue b/client/src/components/History/ContentItem/Dataset/Summary/Discarded.vue deleted file mode 100644 index 39b826904da..00000000000 --- a/client/src/components/History/ContentItem/Dataset/Summary/Discarded.vue +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/client/src/components/History/ContentItem/Dataset/Summary/Empty.vue b/client/src/components/History/ContentItem/Dataset/Summary/Empty.vue deleted file mode 100644 index f12e0bfcd33..00000000000 --- a/client/src/components/History/ContentItem/Dataset/Summary/Empty.vue +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/client/src/components/History/ContentItem/Dataset/Summary/Error.vue b/client/src/components/History/ContentItem/Dataset/Summary/Error.vue deleted file mode 100644 index cb56f2931c1..00000000000 --- a/client/src/components/History/ContentItem/Dataset/Summary/Error.vue +++ /dev/null @@ -1,11 +0,0 @@ - diff --git a/client/src/components/History/ContentItem/Dataset/Summary/New.vue b/client/src/components/History/ContentItem/Dataset/Summary/New.vue deleted file mode 100644 index 9fa4c6ac11e..00000000000 --- a/client/src/components/History/ContentItem/Dataset/Summary/New.vue +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/client/src/components/History/ContentItem/Dataset/Summary/NotViewable.vue b/client/src/components/History/ContentItem/Dataset/Summary/NotViewable.vue deleted file mode 100644 index fdfc3e1dc25..00000000000 --- a/client/src/components/History/ContentItem/Dataset/Summary/NotViewable.vue +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/client/src/components/History/ContentItem/Dataset/Summary/Ok.vue b/client/src/components/History/ContentItem/Dataset/Summary/Ok.vue deleted file mode 100644 index 0b956d72c46..00000000000 --- a/client/src/components/History/ContentItem/Dataset/Summary/Ok.vue +++ /dev/null @@ -1,18 +0,0 @@ - diff --git a/client/src/components/History/ContentItem/Dataset/Summary/Paused.vue b/client/src/components/History/ContentItem/Dataset/Summary/Paused.vue deleted file mode 100644 index f0e6de337dd..00000000000 --- a/client/src/components/History/ContentItem/Dataset/Summary/Paused.vue +++ /dev/null @@ -1,8 +0,0 @@ - diff --git a/client/src/components/History/ContentItem/Dataset/Summary/Queued.vue b/client/src/components/History/ContentItem/Dataset/Summary/Queued.vue deleted file mode 100644 index 5a52e59880e..00000000000 --- a/client/src/components/History/ContentItem/Dataset/Summary/Queued.vue +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/client/src/components/History/ContentItem/Dataset/Summary/Running.vue b/client/src/components/History/ContentItem/Dataset/Summary/Running.vue deleted file mode 100644 index 74737e71980..00000000000 --- a/client/src/components/History/ContentItem/Dataset/Summary/Running.vue +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/client/src/components/History/ContentItem/Dataset/Summary/SettingMetadata.vue b/client/src/components/History/ContentItem/Dataset/Summary/SettingMetadata.vue deleted file mode 100644 index 4b9e960ab15..00000000000 --- a/client/src/components/History/ContentItem/Dataset/Summary/SettingMetadata.vue +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/client/src/components/History/ContentItem/Dataset/Summary/Unknown.vue b/client/src/components/History/ContentItem/Dataset/Summary/Unknown.vue deleted file mode 100644 index 5c67826c244..00000000000 --- a/client/src/components/History/ContentItem/Dataset/Summary/Unknown.vue +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/client/src/components/History/ContentItem/Dataset/Summary/Upload.vue b/client/src/components/History/ContentItem/Dataset/Summary/Upload.vue deleted file mode 100644 index d82b1910e42..00000000000 --- a/client/src/components/History/ContentItem/Dataset/Summary/Upload.vue +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/client/src/components/History/ContentItem/Dataset/Summary/index.vue b/client/src/components/History/ContentItem/Dataset/Summary/index.vue deleted file mode 100644 index 44861d1daa7..00000000000 --- a/client/src/components/History/ContentItem/Dataset/Summary/index.vue +++ /dev/null @@ -1,53 +0,0 @@ - - - diff --git a/client/src/components/History/ContentItem/Dataset/index.js b/client/src/components/History/ContentItem/Dataset/index.js deleted file mode 100644 index 3572794c330..00000000000 --- a/client/src/components/History/ContentItem/Dataset/index.js +++ /dev/null @@ -1,5 +0,0 @@ -import Dataset from "./Dataset"; - -export { default as DatasetUI } from "./DatasetUI"; -export { default as DatasetMenu } from "./DatasetMenu"; -export default Dataset; diff --git a/client/src/components/History/ContentItem/DatasetCollection/DatasetCollection.vue b/client/src/components/History/ContentItem/DatasetCollection/DatasetCollection.vue deleted file mode 100644 index 14e7e38b900..00000000000 --- a/client/src/components/History/ContentItem/DatasetCollection/DatasetCollection.vue +++ /dev/null @@ -1,56 +0,0 @@ - - - diff --git a/client/src/components/History/ContentItem/DatasetCollection/DscDescription.vue b/client/src/components/History/ContentItem/DatasetCollection/DscDescription.vue deleted file mode 100644 index e61a881662c..00000000000 --- a/client/src/components/History/ContentItem/DatasetCollection/DscDescription.vue +++ /dev/null @@ -1,26 +0,0 @@ - - - diff --git a/client/src/components/History/ContentItem/DatasetCollection/DscMenu.vue b/client/src/components/History/ContentItem/DatasetCollection/DscMenu.vue deleted file mode 100644 index b5cf70aedfe..00000000000 --- a/client/src/components/History/ContentItem/DatasetCollection/DscMenu.vue +++ /dev/null @@ -1,108 +0,0 @@ - - - diff --git a/client/src/components/History/ContentItem/DatasetCollection/DscUI.vue b/client/src/components/History/ContentItem/DatasetCollection/DscUI.vue deleted file mode 100644 index 547b3b8e35c..00000000000 --- a/client/src/components/History/ContentItem/DatasetCollection/DscUI.vue +++ /dev/null @@ -1,114 +0,0 @@ - - - - - - - diff --git a/client/src/components/History/ContentItem/DatasetCollection/JobStateProgress.vue b/client/src/components/History/ContentItem/DatasetCollection/JobStateProgress.vue deleted file mode 100644 index 86ee5227adc..00000000000 --- a/client/src/components/History/ContentItem/DatasetCollection/JobStateProgress.vue +++ /dev/null @@ -1,44 +0,0 @@ - - - - - diff --git a/client/src/components/History/ContentItem/DatasetCollection/index.js b/client/src/components/History/ContentItem/DatasetCollection/index.js deleted file mode 100644 index 4d7f796c22b..00000000000 --- a/client/src/components/History/ContentItem/DatasetCollection/index.js +++ /dev/null @@ -1,6 +0,0 @@ -import DatasetCollection from "./DatasetCollection"; - -export { default as DscUI } from "./DscUI"; -export { default as DscMenu } from "./DscMenu"; -export { default as JobStateProgress } from "./JobStateProgress"; -export default DatasetCollection; diff --git a/client/src/components/History/ContentItem/GenericContentItem/DatasetCollectionContents.vue b/client/src/components/History/ContentItem/GenericContentItem/DatasetCollectionContents.vue deleted file mode 100644 index 157e8280316..00000000000 --- a/client/src/components/History/ContentItem/GenericContentItem/DatasetCollectionContents.vue +++ /dev/null @@ -1,38 +0,0 @@ - - diff --git a/client/src/components/History/ContentItem/GenericContentItem/DatasetCollectionUIWrapper.test.js b/client/src/components/History/ContentItem/GenericContentItem/DatasetCollectionUIWrapper.test.js deleted file mode 100644 index a990dd10193..00000000000 --- a/client/src/components/History/ContentItem/GenericContentItem/DatasetCollectionUIWrapper.test.js +++ /dev/null @@ -1,61 +0,0 @@ -import DatasetCollectionUIWrapper from "./DatasetCollectionUIWrapper"; -import DscUI from "components/History/ContentItem/DatasetCollection/DscUI"; -import { shallowMount } from "@vue/test-utils"; -import flushPromises from "flush-promises"; -import datasetCollectionRaw from "components/providers/History/test/json/DatasetCollection.json"; -import datasetCollectionContent from "components/providers/History/test/json/DatasetCollection.nested.json"; - -describe("DatasetUIWrapper.vue with Dataset", () => { - let wrapper; - let propsData; - let onHide; - let onUnhide; - let onDelete; - let onUndelete; - - beforeEach(async () => { - propsData = { - item: datasetCollectionRaw, - }; - onHide = jest.spyOn(DatasetCollectionUIWrapper.methods, "onHide").mockImplementation(); - onUnhide = jest.spyOn(DatasetCollectionUIWrapper.methods, "onUnhide").mockImplementation(); - onDelete = jest.spyOn(DatasetCollectionUIWrapper.methods, "onDelete").mockImplementation(); - onUndelete = jest.spyOn(DatasetCollectionUIWrapper.methods, "onUndelete").mockImplementation(); - wrapper = shallowMount(DatasetCollectionUIWrapper, { - propsData, - }); - }); - - it("loads DscUI component", async () => { - expect(wrapper.findComponent(DscUI).exists()).toBeTruthy(); - }); - - it("reacts to all emittted events", async () => { - expect(onHide).not.toHaveBeenCalled(); - expect(onUnhide).not.toHaveBeenCalled(); - expect(onDelete).not.toHaveBeenCalled(); - expect(onUndelete).not.toHaveBeenCalled(); - wrapper.findComponent(DscUI).vm.$emit("hide"); - expect(onHide).toHaveBeenCalled(); - wrapper.findComponent(DscUI).vm.$emit("unhide"); - expect(onUnhide).toHaveBeenCalled(); - wrapper.findComponent(DscUI).vm.$emit("delete"); - expect(onDelete).toHaveBeenCalled(); - wrapper.findComponent(DscUI).vm.$emit("undelete"); - expect(onUndelete).toHaveBeenCalled(); - }); - it("creates DatasetCollection", async () => { - const datasetCollection = wrapper.vm.datasetCollection; - expect(datasetCollection.collectionType).toBe("list of pairs"); - }); - it("manages collection expansion", async () => { - expect(wrapper.vm.expand).toBeFalsy(); - wrapper.findComponent(DscUI).vm.$emit("viewCollection", wrapper.vm.datasetCollection); - expect(wrapper.vm.expand).toBeTruthy(); - }); - it("build dsc from collection content", async () => { - wrapper.setProps({ item: datasetCollectionContent }); - await flushPromises(); - expect(wrapper.vm.datasetCollection.name).toBe(datasetCollectionContent.element_identifier); - }); -}); diff --git a/client/src/components/History/ContentItem/GenericContentItem/DatasetCollectionUIWrapper.vue b/client/src/components/History/ContentItem/GenericContentItem/DatasetCollectionUIWrapper.vue deleted file mode 100644 index ebf8d890fd1..00000000000 --- a/client/src/components/History/ContentItem/GenericContentItem/DatasetCollectionUIWrapper.vue +++ /dev/null @@ -1,98 +0,0 @@ - - diff --git a/client/src/components/History/ContentItem/GenericContentItem/DatasetUIWrapper.test.js b/client/src/components/History/ContentItem/GenericContentItem/DatasetUIWrapper.test.js deleted file mode 100644 index 52aa4cd0e64..00000000000 --- a/client/src/components/History/ContentItem/GenericContentItem/DatasetUIWrapper.test.js +++ /dev/null @@ -1,51 +0,0 @@ -import DatasetUIWrapper from "./DatasetUIWrapper"; -import DatasetUI from "components/History/ContentItem/Dataset/DatasetUI"; -import { shallowMount } from "@vue/test-utils"; -import raw from "components/providers/History/test/json/Dataset.json"; - -describe("DatasetUIWrapper.vue with Dataset", () => { - let wrapper; - let propsData; - let onDelete; - let onUnhide; - let onUndelete; - let updateDataset; - - beforeEach(async () => { - propsData = { - item: raw, - }; - onDelete = jest.spyOn(DatasetUIWrapper.methods, "onDelete").mockImplementation(); - onUnhide = jest.spyOn(DatasetUIWrapper.methods, "onUnhide").mockImplementation(); - onUndelete = jest.spyOn(DatasetUIWrapper.methods, "onUndelete").mockImplementation(); - updateDataset = jest.spyOn(DatasetUIWrapper.methods, "onUpdate").mockImplementation(); - wrapper = shallowMount(DatasetUIWrapper, { - propsData, - }); - }); - - it("loads DatasetUI component", async () => { - expect(wrapper.findComponent(DatasetUI).exists()).toBeTruthy(); - }); - - it("reacts to all emittted events", async () => { - expect(onDelete).not.toHaveBeenCalled(); - expect(onUnhide).not.toHaveBeenCalled(); - expect(onUndelete).not.toHaveBeenCalled(); - expect(updateDataset).not.toHaveBeenCalled(); - wrapper.findComponent(DatasetUI).vm.$emit("delete"); - expect(onDelete).toHaveBeenCalled(); - wrapper.findComponent(DatasetUI).vm.$emit("undelete"); - expect(onUndelete).toHaveBeenCalled(); - wrapper.findComponent(DatasetUI).vm.$emit("unhide"); - expect(onUnhide).toHaveBeenCalled(); - wrapper.findComponent(DatasetUI).vm.$emit("update"); - expect(updateDataset).toHaveBeenCalled(); - }); - it("handles element identifier", async () => { - expect(wrapper.vm.dataset.canEditName).toBeTruthy(); - await wrapper.setProps({ element_identifier: "a cool identifier" }); - expect(wrapper.vm.dataset.title).toBe("a cool identifier"); - expect(wrapper.vm.dataset.canEditName).toBeFalsy(); - }); -}); diff --git a/client/src/components/History/ContentItem/GenericContentItem/DatasetUIWrapper.vue b/client/src/components/History/ContentItem/GenericContentItem/DatasetUIWrapper.vue deleted file mode 100644 index 3a43a30dd55..00000000000 --- a/client/src/components/History/ContentItem/GenericContentItem/DatasetUIWrapper.vue +++ /dev/null @@ -1,62 +0,0 @@ - - - diff --git a/client/src/components/History/ContentItem/GenericContentItem/GenericHistoryContent.vue b/client/src/components/History/ContentItem/GenericContentItem/GenericHistoryContent.vue deleted file mode 100644 index 734f68bc1b8..00000000000 --- a/client/src/components/History/ContentItem/GenericContentItem/GenericHistoryContent.vue +++ /dev/null @@ -1,42 +0,0 @@ - - - diff --git a/client/src/components/History/ContentItem/HistoryContentItem.js b/client/src/components/History/ContentItem/HistoryContentItem.js deleted file mode 100644 index c6d8c6632dc..00000000000 --- a/client/src/components/History/ContentItem/HistoryContentItem.js +++ /dev/null @@ -1,22 +0,0 @@ -import ContentItem from "./ContentItem"; - -export default { - mixins: [ContentItem], - - computed: { - contentItemComponent() { - if (this.item.id === undefined) { - return "Placeholder"; - } - const { history_content_type } = this.item; - switch (history_content_type) { - case "dataset": - return "Dataset"; - case "dataset_collection": - return "DatasetCollection"; - default: - return "Placeholder"; - } - }, - }, -}; diff --git a/client/src/components/History/ContentItem/Placeholder.vue b/client/src/components/History/ContentItem/Placeholder.vue deleted file mode 100644 index 555f0a7325e..00000000000 --- a/client/src/components/History/ContentItem/Placeholder.vue +++ /dev/null @@ -1,18 +0,0 @@ - - - diff --git a/client/src/components/History/ContentItem/Subcollection.vue b/client/src/components/History/ContentItem/Subcollection.vue deleted file mode 100644 index 0a7a632f405..00000000000 --- a/client/src/components/History/ContentItem/Subcollection.vue +++ /dev/null @@ -1,29 +0,0 @@ - - - - - diff --git a/client/src/components/History/ContentItem/Subdataset.vue b/client/src/components/History/ContentItem/Subdataset.vue deleted file mode 100644 index 3f9f4498305..00000000000 --- a/client/src/components/History/ContentItem/Subdataset.vue +++ /dev/null @@ -1,60 +0,0 @@ - - - - - diff --git a/client/src/components/History/ContentItem/index.js b/client/src/components/History/ContentItem/index.js deleted file mode 100644 index 32d866ab9d0..00000000000 --- a/client/src/components/History/ContentItem/index.js +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Scrolling components for history contents and collection contents. - */ - -import "./styles.scss"; - -export { default as ContentItem } from "./ContentItem"; -export { default as CollectionContentItem } from "./CollectionContentItem"; -export { default as HistoryContentItem } from "./HistoryContentItem"; diff --git a/client/src/components/History/ContentItem/styles.scss b/client/src/components/History/ContentItem/styles.scss deleted file mode 100644 index 03fcc5f41dd..00000000000 --- a/client/src/components/History/ContentItem/styles.scss +++ /dev/null @@ -1,32 +0,0 @@ -@import "theme/blue.scss"; -@import "scss/mixins.scss"; - -.content-item { - outline: none; - border-bottom: 2px solid white; - &:focus-within { - box-shadow: inset 0 0 1em 0.25em rgba(255, 255, 255, 0.25); - } -} - -// Nav across the top of the content item -.content-item .content-top-menu { - min-height: 2em; - - .content-title { - .hid { - color: adjust-color($text-color, $alpha: -0.6); - font-size: 80%; - user-select: none; - display: inline-block; - text-align: right; - min-width: 0.25rem; - &:not(:empty)::after { - content: ":"; - } - } - .name { - font-weight: 700; - } - } -} \ No newline at end of file diff --git a/client/src/components/History/CurrentCollection/CollectionDetails.vue b/client/src/components/History/CurrentCollection/CollectionDetails.vue new file mode 100644 index 00000000000..d978ac6627e --- /dev/null +++ b/client/src/components/History/CurrentCollection/CollectionDetails.vue @@ -0,0 +1,34 @@ + + + diff --git a/client/src/components/History/CurrentCollection/CollectionNavigation.vue b/client/src/components/History/CurrentCollection/CollectionNavigation.vue new file mode 100644 index 00000000000..5cadde37654 --- /dev/null +++ b/client/src/components/History/CurrentCollection/CollectionNavigation.vue @@ -0,0 +1,37 @@ + + + diff --git a/client/src/components/History/CurrentCollection/CollectionOperations.vue b/client/src/components/History/CurrentCollection/CollectionOperations.vue index ee6f3518da5..c346b91ad5a 100644 --- a/client/src/components/History/CurrentCollection/CollectionOperations.vue +++ b/client/src/components/History/CurrentCollection/CollectionOperations.vue @@ -1,32 +1,25 @@ diff --git a/client/src/components/History/CurrentCollection/Details.vue b/client/src/components/History/CurrentCollection/Details.vue deleted file mode 100644 index d13afdc23a4..00000000000 --- a/client/src/components/History/CurrentCollection/Details.vue +++ /dev/null @@ -1,128 +0,0 @@ - - - - - diff --git a/client/src/components/History/CurrentCollection/Panel.vue b/client/src/components/History/CurrentCollection/Panel.vue deleted file mode 100644 index a29f2e98701..00000000000 --- a/client/src/components/History/CurrentCollection/Panel.vue +++ /dev/null @@ -1,126 +0,0 @@ - - - - - diff --git a/client/src/components/History/CurrentCollection/TopNav.vue b/client/src/components/History/CurrentCollection/TopNav.vue deleted file mode 100644 index ad02a1ee399..00000000000 --- a/client/src/components/History/CurrentCollection/TopNav.vue +++ /dev/null @@ -1,71 +0,0 @@ - - - diff --git a/client/src/components/History/CurrentHistory/HistoryDetails.vue b/client/src/components/History/CurrentHistory/HistoryDetails.vue new file mode 100644 index 00000000000..990c1f8f744 --- /dev/null +++ b/client/src/components/History/CurrentHistory/HistoryDetails.vue @@ -0,0 +1,37 @@ + + + diff --git a/client/src/components/History/HistoryDropdown.vue b/client/src/components/History/CurrentHistory/HistoryDropdown.vue similarity index 100% rename from client/src/components/History/HistoryDropdown.vue rename to client/src/components/History/CurrentHistory/HistoryDropdown.vue diff --git a/client/src/components/History/HistoryEmpty.vue b/client/src/components/History/CurrentHistory/HistoryEmpty.vue similarity index 100% rename from client/src/components/History/HistoryEmpty.vue rename to client/src/components/History/CurrentHistory/HistoryEmpty.vue diff --git a/client/src/components/History/ContentFilters.vue b/client/src/components/History/CurrentHistory/HistoryFilters.vue similarity index 78% rename from client/src/components/History/ContentFilters.vue rename to client/src/components/History/CurrentHistory/HistoryFilters.vue index ad10bc26d6f..93ce6cd08b3 100644 --- a/client/src/components/History/ContentFilters.vue +++ b/client/src/components/History/CurrentHistory/HistoryFilters.vue @@ -31,7 +31,6 @@ diff --git a/client/src/components/History/CurrentHistoryPanel.vue b/client/src/components/History/CurrentHistoryPanel.vue deleted file mode 100644 index 0ec1c505d5e..00000000000 --- a/client/src/components/History/CurrentHistoryPanel.vue +++ /dev/null @@ -1,35 +0,0 @@ - - - diff --git a/client/src/components/History/DeleteModal.vue b/client/src/components/History/DeleteModal.vue deleted file mode 100644 index 6626d3f4ce4..00000000000 --- a/client/src/components/History/DeleteModal.vue +++ /dev/null @@ -1,28 +0,0 @@ - - - diff --git a/client/src/components/History/EditorMenu.vue b/client/src/components/History/EditorMenu.vue deleted file mode 100644 index 8b0b81a422f..00000000000 --- a/client/src/components/History/EditorMenu.vue +++ /dev/null @@ -1,73 +0,0 @@ - - - - - diff --git a/client/src/components/History/History.vue b/client/src/components/History/History.vue deleted file mode 100644 index e4586eb72bf..00000000000 --- a/client/src/components/History/History.vue +++ /dev/null @@ -1,146 +0,0 @@ - - - diff --git a/client/src/components/History/HistoryDetails.vue b/client/src/components/History/HistoryDetails.vue deleted file mode 100644 index f3a6b48a7b6..00000000000 --- a/client/src/components/History/HistoryDetails.vue +++ /dev/null @@ -1,151 +0,0 @@ - - - - - diff --git a/client/src/components/History/HistoryPanel.vue b/client/src/components/History/HistoryPanel.vue deleted file mode 100644 index 57bfaafc0f5..00000000000 --- a/client/src/components/History/HistoryPanel.vue +++ /dev/null @@ -1,52 +0,0 @@ - - - - - diff --git a/client/src/components/History/HistoryTags.vue b/client/src/components/History/HistoryTags.vue deleted file mode 100644 index 3623963e158..00000000000 --- a/client/src/components/History/HistoryTags.vue +++ /dev/null @@ -1,62 +0,0 @@ - - - diff --git a/client/src/components/History/Index.vue b/client/src/components/History/Index.vue new file mode 100644 index 00000000000..a426ab9155f --- /dev/null +++ b/client/src/components/History/Index.vue @@ -0,0 +1,61 @@ + + + diff --git a/client/src/components/History/Layout/Details.vue b/client/src/components/History/Layout/Details.vue new file mode 100644 index 00000000000..3d3454d07a0 --- /dev/null +++ b/client/src/components/History/Layout/Details.vue @@ -0,0 +1,120 @@ + + + diff --git a/client/src/components/History/Layout.vue b/client/src/components/History/Layout/Layout.vue similarity index 50% rename from client/src/components/History/Layout.vue rename to client/src/components/History/Layout/Layout.vue index cbcf3e1ee3f..aa7e53e29fd 100644 --- a/client/src/components/History/Layout.vue +++ b/client/src/components/History/Layout/Layout.vue @@ -1,47 +1,41 @@ - - diff --git a/client/src/components/History/CopyModal.vue b/client/src/components/History/Modals/CopyModal.vue similarity index 98% rename from client/src/components/History/CopyModal.vue rename to client/src/components/History/Modals/CopyModal.vue index 5d93707c302..3abd7b3c152 100644 --- a/client/src/components/History/CopyModal.vue +++ b/client/src/components/History/Modals/CopyModal.vue @@ -45,7 +45,7 @@ - - diff --git a/client/src/components/History/StatusIcon.vue b/client/src/components/History/StatusIcon.vue deleted file mode 100644 index 82fa5d5b0a1..00000000000 --- a/client/src/components/History/StatusIcon.vue +++ /dev/null @@ -1,54 +0,0 @@ - - - diff --git a/client/src/components/History/adapters/HistoryPanelProxy.js b/client/src/components/History/adapters/HistoryPanelProxy.js index 66dc13d49fc..4ebbe3336da 100644 --- a/client/src/components/History/adapters/HistoryPanelProxy.js +++ b/client/src/components/History/adapters/HistoryPanelProxy.js @@ -5,8 +5,7 @@ */ import { mountVueComponent } from "utils/mountVueComponent"; -import CurrentHistoryPanel from "../CurrentHistoryPanel"; -// import { genericProxy } from "utils/proxy"; +import HistoryIndex from "components/History/Index"; // mvc nonsense import $ from "jquery"; @@ -88,7 +87,7 @@ export const HistoryPanelProxy = Backbone.View.extend({ $("#right").addClass("beta").prepend(this.$el); const container = this.$el[0]; - const mountHistory = mountVueComponent(CurrentHistoryPanel); + const mountHistory = mountVueComponent(HistoryIndex); mountHistory({}, container); return this; diff --git a/client/src/components/History/index.js b/client/src/components/History/index.js deleted file mode 100644 index 55b124ab919..00000000000 --- a/client/src/components/History/index.js +++ /dev/null @@ -1,10 +0,0 @@ -// Right-hand panel -export { default as HistoryPanel } from "./HistoryPanel"; -export { default as CurrentHistoryPanel } from "./CurrentHistoryPanel"; - -// Adapter: backbone view that mounts HistoryPanel -export { default as HistoryPanelProxy } from "./adapters/HistoryPanelProxy"; - -// rxjs debugging panel -// import { initSpy } from "utils/observable/rxjsDebugging"; -// initSpy(); diff --git a/client/src/components/History/model/CurveFit.js b/client/src/components/History/model/CurveFit.js deleted file mode 100644 index 9d863e798d9..00000000000 --- a/client/src/components/History/model/CurveFit.js +++ /dev/null @@ -1,146 +0,0 @@ -/** - * Houses linear regression and polynomial fitting methods in the API of - * a Map. Set values to insert data points, Get other x values to return - * approximations (or the actual datapoints if they exist) - */ - -import { linear, polynomial } from "regression"; -import { map, pipe, filter, windowAhead } from "iter-tools"; - -class CurveFitInputError extends Error {} -class CurveFitSampleError extends Error {} - -const xAscending = ([xa], [xb]) => xa - xb; -const isDefined = filter((pt) => pt !== undefined); - -export class CurveFit extends Map { - constructor(data) { - super(data); - this._domain = [-Infinity, Infinity]; - this.xPrecision = 0; - this.yPrecision = 0; - } - - // cast keys and vals as numbers - set(key, val) { - const x = this.round(key, this.xPrecision); - const y = this.round(val, this.yPrecision); - if (isFinite(x) && isFinite(y) && val !== undefined && val !== null && this.valInDomain(x)) { - super.set(x, y); - } - } - - get(key, opts = {}) { - if (isNaN(key)) { - const errMsg = "Please provide a numeric input"; - throw new CurveFitInputError(errMsg); - } - - const { interpolate = false, order = 2, precision = this.yPrecision } = opts; - const x = this.round(key, this.xPrecision); - const inDomain = this.valInDomain(x); - - let result = undefined; - if (inDomain) { - if (this.has(x)) { - result = super.get(x); - } else { - const method = interpolate ? this.linearInterpolate : this.curveFit; - result = method.call(this, x, { order, precision }); - } - } - - return result; - } - - get hasData() { - return this.size >= 2; - } - - get dataPoints() { - return Array.from(this.entries()).sort(xAscending); - } - - get domain() { - return this._domain; - } - - set domain(newDomain) { - this.pruneDomain(newDomain); - this._domain = newDomain; - } - - // fancier round function, has precision option so we can round to the - // nearest indicated power of 10 - round(n, precision) { - const factor = 10 ** precision; - return Math.round(+n * factor) / factor; - } - - pruneDomain(domain) { - const notInDomain = (val) => !this.valInDomain(val, domain); - const doomed = filter(notInDomain, this.keys()); - for (const x of doomed) { - this.delete(x); - } - } - - valInDomain(x, domain = this.domain) { - const [bottom, top] = domain; - return x >= bottom && x <= top; - } - - pointInDomain([x], domain = this.domain) { - return this.valInDomain(x, domain); - } - - curveFit(x, opts = {}) { - if (this.size < 2) { - const errMsg = "Need at least 2 points to perform an approximation"; - throw new CurveFitSampleError(errMsg); - } - const approxMethod = this.size == 2 ? linear : polynomial; - const { predict } = approxMethod(this.dataPoints, opts); - const [, y] = predict(x); - return y; - } - - // finds closest two data points and does a linear interpolation between - // them at the provided x value - linearInterpolate(x, opts = {}) { - const fn = this.getPointWiseFilter(x); - const [bookends] = Array.from(fn(this.dataPoints)); - if (bookends && bookends.length) { - const { predict } = linear(bookends, opts); - const [, y] = predict(x); - return y; - } - return undefined; - } - - // finds the data points to the left and right of indicated xval - getPointWiseFilter(x) { - return pipe( - windowAhead(2), - map((w) => Array.from(isDefined(w))), - filter((pts) => { - if (pts.length == 2) { - const [[x1], [x2]] = pts; - return x > x1 && x < x2; - } - return false; - }) - ); - } - - clone() { - const newFit = new CurveFit(); - newFit.xPrecision = this.xPrecision; - newFit.yPrecision = this.yPrecision; - newFit.domain = this.domain; - for (const [x, y] of this.entries()) { - newFit.set(x, y); - } - return newFit; - } -} diff --git a/client/src/components/History/model/CurveFit.test.js b/client/src/components/History/model/CurveFit.test.js deleted file mode 100644 index a79bbde6ff6..00000000000 --- a/client/src/components/History/model/CurveFit.test.js +++ /dev/null @@ -1,213 +0,0 @@ -import { CurveFit } from "./CurveFit"; - -describe("CurveFit utility", () => { - describe("size prop", () => { - test("should be a map with a size prop", () => { - const fit = new CurveFit(); - expect(fit.size).toBe(0); - }); - - test("can accept new data points", () => { - const fit = new CurveFit(); - fit.set(0, 0); - fit.set(100, 100); - expect(fit.size).toBe(2); - expect(fit.dataPoints.length).toEqual(2); - }); - - test("converts all inputs to numeric values", () => { - const fit = new CurveFit(); - fit.set("0", 0); - fit.set("100", "100"); - expect(fit.dataPoints.length).toEqual(2); - expect(fit.dataPoints).toEqual([ - [0, 0], - [100, 100], - ]); - }); - }); - - describe("dataPoints prop", () => { - test("has a data property which is an array of arrays", () => { - const fit = new CurveFit(); - fit.set(100, 100); - fit.set(20, 3000); - fit.set(0, 0); - expect(fit.dataPoints).toEqual([ - [0, 0], - [20, 3000], - [100, 100], - ]); - }); - }); - - describe("xPrecision & yPrecision", () => { - test("integers (precision = 0)", () => { - const fit = new CurveFit(); - fit.xPrecision = 0; - fit.yPrecision = 0; - fit.set(100.234234, 100.234234); - fit.set(20.2342342342, 3000.2342342342); - fit.set(0.000003, 0.000002); - - expect(fit.dataPoints).toEqual([ - [0, 0], - [20, 3000], - [100, 100], - ]); - }); - - test("positive precision", () => { - const fit = new CurveFit(); - fit.xPrecision = 1; - fit.yPrecision = 2; - fit.set(100.234234, 100.234234); - fit.set(20.2342342342, 3000.2342342342); - fit.set(0.000003, 0.000002); - - expect(fit.dataPoints).toEqual([ - [0, 0], - [20.2, 3000.23], - [100.2, 100.23], - ]); - }); - - test("negative precision", () => { - const fit = new CurveFit(); - fit.xPrecision = -1; - fit.yPrecision = -2; - fit.set(111.234234, 22222.234234); - fit.set(20.2342342342, 3333.2342342342); - fit.set(2.11111111, 40.2222222); - - expect(fit.dataPoints).toEqual([ - [0, 0], - [20, 3300], - [110, 22200], - ]); - }); - }); - - describe("fit a 2 point line", () => { - // given (0,0), (100,300), expect (50,150) - test("find the middle of a line when the data set is only 2 points", () => { - const fit = new CurveFit(); - fit.set(100, 300); - fit.set(0, 0); - - expect(fit.get(0)).toBe(0); - expect(fit.get(100)).toBe(300); - expect(fit.get(50)).toBe(150); - }); - }); - - describe("fit a point on simple curves", () => { - test("y = x^2", () => { - const fit = new CurveFit(); - fit.set(0, 0); - fit.set(2, 4); - fit.set(3, 9); - - expect(fit.get(4, { order: 2 })).toEqual(16); - expect(fit.get(4, { order: 3 })).toEqual(16); - expect(fit.get(4, { order: 4 })).toEqual(16); - }); - - test("y = x^3", () => { - const fit = new CurveFit(); - fit.set(0, 0); - fit.set(1, 1); - fit.set(2, 8); - fit.set(3, 27); - - expect(fit.get(4, { order: 3 })).toEqual(64); - }); - - test("approximation should be in the ballpark", () => { - const fit = new CurveFit(); - fit.set(0, 0); - fit.set(1, 1); - fit.set(2, 8); - fit.set(3, 27); - - // approximate with only 2 terms, but w know it's really a cubic, - // should be off but in the ballpark - const approx = fit.get(4, { order: 2 }); - expect(approx).toBeGreaterThan(Math.pow(3, 3)); - expect(approx).toBeLessThan(Math.pow(5, 3)); - }); - }); - - describe("domain prop", () => { - let fit; - beforeEach(() => { - fit = new CurveFit(); - fit.domain = [0, 2]; - fit.set(0, 0); - fit.set(1, 1); - fit.set(2, 8); - fit.set(3, 1000000000); - }); - - test("resetting domain removes datapoints outside of new domain", () => { - fit.domain = [0, 1]; - expect(fit.dataPoints).toEqual([ - [0, 0], - [1, 1], - ]); - }); - - test("can restrict evaluated datapoints based on a set domain of inputs", () => { - expect(fit.dataPoints).toEqual([ - [0, 0], - [1, 1], - [2, 8], - ]); - }); - - test("appoximations will ignore datapoints outside domain", () => { - expect(fit.get(3, { order: 3 })).toBe(undefined); - fit.domain = [0, 3]; - expect(fit.get(3, { order: 3 })).toBe(27); - }); - }); - - // estimates value by drawing a line between the two closest points and - // doing a linear approx - describe("linear pointwise interpolation", () => { - let fit; - beforeEach(() => { - fit = new CurveFit(); - fit.xPrecision = 1; - fit.domain = [0, 3]; - fit.set(0.0, 0); - fit.set(1.0, 1); - fit.set(2.0, 8); - fit.set(3.0, 32); - }); - - test("2.5 -> 20", () => { - expect(fit.linearInterpolate(2.5)).toEqual(20); - expect(fit.get(2.5, { interpolate: true })).toEqual(20); - }); - }); - - describe("clone", () => { - let fit; - beforeEach(() => { - fit = new CurveFit(); - fit.xPrecision = 1; - fit.domain = [0, 3]; - fit.set(0.0, 0); - fit.set(1.0, 1); - fit.set(2.0, 8); - }); - - test("clone, hope for same data", () => { - const existingData = fit.dataPoints; - const newMap = fit.clone(); - const cloneData = newMap.dataPoints; - expect(existingData).toEqual(cloneData); - }); - }); -}); diff --git a/client/src/components/History/model/Dataset.test.js b/client/src/components/History/model/Dataset.test.js index 68ab03cc4ab..5a5c7891ef2 100644 --- a/client/src/components/History/model/Dataset.test.js +++ b/client/src/components/History/model/Dataset.test.js @@ -1,7 +1,7 @@ import { Content } from "./Content"; import { Dataset } from "./Dataset"; -import raw from "components/providers/History/test/json/Dataset.json"; +import raw from "components/providers/test/json/Dataset.json"; const elementIdentifier = "collection_element1"; diff --git a/client/src/components/History/model/DatasetCollection.test.js b/client/src/components/History/model/DatasetCollection.test.js index e339c89f52c..e9df257c261 100644 --- a/client/src/components/History/model/DatasetCollection.test.js +++ b/client/src/components/History/model/DatasetCollection.test.js @@ -3,15 +3,15 @@ import { DatasetCollection } from "./DatasetCollection"; import { STATES } from "./states"; // test data -import raw from "components/providers/History/test/json/DatasetCollection.json"; +import raw from "components/providers/test/json/DatasetCollection.json"; // error on load -import rawError from "components/providers/History/test/json/DatasetCollection.error.json"; +import rawError from "components/providers/test/json/DatasetCollection.error.json"; // mixed bag -import rawProcessing from "components/providers/History/test/json/DatasetCollection.processing.json"; -import rawHomogeneous from "components/providers/History/test/json/DatasetCollection.homogeneous.json"; -import rawHeterogeneous from "components/providers/History/test/json/DatasetCollection.heterogeneous.json"; +import rawProcessing from "components/providers/test/json/DatasetCollection.processing.json"; +import rawHomogeneous from "components/providers/test/json/DatasetCollection.homogeneous.json"; +import rawHeterogeneous from "components/providers/test/json/DatasetCollection.heterogeneous.json"; describe("DatasetCollection", () => { const model = new DatasetCollection(raw); diff --git a/client/src/components/History/model/ScrollPos.js b/client/src/components/History/model/ScrollPos.js deleted file mode 100644 index dde873dfb64..00000000000 --- a/client/src/components/History/model/ScrollPos.js +++ /dev/null @@ -1,16 +0,0 @@ -export class ScrollPos { - constructor(props = {}) { - const { cursor = null, key = null } = props; - this.cursor = key === null && cursor === null ? 0 : cursor; - this.key = key; - } -} - -ScrollPos.create = function (props) { - const o = new ScrollPos(props); - return Object.freeze(o); -}; - -ScrollPos.equals = function (a, b) { - return a.cursor === b.cursor && a.key === b.key; -}; diff --git a/client/src/components/providers/History/SearchParams.js b/client/src/components/History/model/SearchParams.js similarity index 58% rename from client/src/components/providers/History/SearchParams.js rename to client/src/components/History/model/SearchParams.js index 23a85b7dba3..a697423d253 100644 --- a/client/src/components/providers/History/SearchParams.js +++ b/client/src/components/History/model/SearchParams.js @@ -1,6 +1,3 @@ -import deepEqual from "deep-equal"; -import { isString } from "underscore"; - const pairSplitRE = /(\w+=\w+)|(\w+="(\w|\s)+")/g; const scrubFieldRE = /[^\w]/g; const scrubQuotesRE = /'|"/g; @@ -22,17 +19,7 @@ const validTextFields = new Set([ "tags", ]); -// alias search field to internal field (requestd name: pouch field name) -const pouchFieldAlias = { - format: "file_ext", - database: "genome_build", - description: "annotation", - tag: "tags", - deleted: "isDeleted", - type: "history_content_type", -}; - -// maps user-field -> server querystring field +// Maps user-field -> server querystring field // if maps to null, that filter not available on server const serverFieldAlias = { tags: "tag", @@ -40,8 +27,8 @@ const serverFieldAlias = { genome_build: null, }; -// Convert actualy boolean into objectively incorrect python value our server accepts -const dumbBool = (val) => (val ? "True" : "False"); +// Convert boolean into python value as currently expected by the api +const pythonBool = (val) => (val ? "True" : "False"); export class SearchParams { constructor(props = {}) { @@ -51,20 +38,6 @@ export class SearchParams { Object.assign(this, props); } - get pageSize() { - return SearchParams.pageSize; - } - - clone() { - return new SearchParams(this); - } - - // need this because of what Vue does to objects to make them reactive - export() { - const { filterText, showDeleted, showHidden } = this; - return { filterText, showDeleted, showHidden }; - } - // Filtering, parses single text input into a map of field->value // in the case of multiples, maps to field -> [value, value] get textCriteria() { @@ -107,28 +80,6 @@ export class SearchParams { return criteria; } - // Generates an array of pouchDB selector objects - // { field: { $operator: value }} - get pouchFilters() { - const filters = Array.from(this.criteria) - // generate multiple objects for duplicated field=val entries - // these will be AND-ed together in the final pouch selector - // map userfield to pouchfield, userfield = what the user typed in the box - // pouchfield = the actual field in the cache - .map(([userField, val]) => [this.getPouchFieldName(userField), val]) - .map(([pouchField, val]) => { - const vals = Array.isArray(val) ? val : [val]; - return vals.map((v) => [pouchField, v]); - }) - .flat() - .map(([pouchField, val]) => { - const comparator = isString(val) ? { $regex: new RegExp(val) } : { $eq: val }; - return { [pouchField]: comparator }; - }); - - return filters; - } - // Creates an object of query criteria for use with the api get historyContentQueryFields() { return Array.from(this.criteria) @@ -152,7 +103,7 @@ export class SearchParams { // deleted serverField was reserved by pouchDB, needed to rename it to "isDeleted" case "deleted": case "visible": - serverVal = dumbBool(val); + serverVal = pythonBool(val); break; // no text searching in some fields @@ -178,14 +129,6 @@ export class SearchParams { }, {}); } - // legacy q/qv syntax for content api - // TODO: Delete when we no longer use q/qv - get legacyContentQueryString() { - return Object.entries(this.historyContentQueryFields) - .map(([f, v]) => `q=${f}&qv=${v}`) - .join("&"); - } - // Standard query string (field=val&field=val...) get historyContentQueryString() { return Object.entries(this.historyContentQueryFields) @@ -193,36 +136,8 @@ export class SearchParams { .join("&"); } - // maps friendly user field name to internal data field if necessary - getPouchFieldName(field) { - const cleanName = field.replace(scrubFieldRE, ""); - return cleanName in pouchFieldAlias ? pouchFieldAlias[cleanName] : cleanName; - } - getServerFieldName(field) { const cleanName = field.replace(scrubFieldRE, ""); return cleanName in serverFieldAlias ? serverFieldAlias[cleanName] : cleanName; } - - // output current state to log - report(label = "params", collapsed = true) { - const { showDeleted, showHidden, filterText } = this; - const consoleOpen = collapsed ? console.groupCollapsed : console.group; - consoleOpen.call(console, label); - console.log("showDeleted", showDeleted); - console.log("showHidden", showHidden); - console.log("filterText", filterText); - console.groupEnd(); - } } - -// Statics - -SearchParams.pageSize = 30; - -SearchParams.equals = function (a, b) { - if (a !== undefined && b !== undefined) { - return deepEqual(a.export(), b.export()); - } - return false; -}; diff --git a/client/src/components/History/model/crud.js b/client/src/components/History/model/crud.js index f009023f492..abf1db00c8c 100644 --- a/client/src/components/History/model/crud.js +++ b/client/src/components/History/model/crud.js @@ -1,38 +1,39 @@ -import { bulkCacheContent } from "components/providers/History/caching"; -import { bulkContentUpdate, getAllContentByFilter } from "./queries"; +import { contentUpdate, getAllContentByFilter } from "./queries"; /** - * Content crud operations, usually an ajax call + a cache function + * Content operations */ -export const updateSelectedContent = (updates) => async (history, type_ids) => { - // single ajax call with wierd syntax because.... reasons - const changes = await bulkContentUpdate(history, type_ids, updates); - const cacheResult = await bulkCacheContent(changes, true); - console.log("cacheResult", changes, cacheResult); - return cacheResult; -}; +export async function hideSelectedContent(history, type_ids) { + return await contentUpdate(history, type_ids, { + visible: false, + }); +} -export const hideSelectedContent = updateSelectedContent({ - visible: false, -}); +export async function unhideSelectedContent(history, type_ids) { + return await contentUpdate(history, type_ids, { + visible: true, + }); +} -export const unhideSelectedContent = updateSelectedContent({ - visible: true, -}); +export async function deleteSelectedContent(history, type_ids) { + return await contentUpdate(history, type_ids, { + deleted: true, + }); +} -export const deleteSelectedContent = updateSelectedContent({ - deleted: true, -}); +export async function undeleteSelectedContent(history, type_ids) { + return await contentUpdate(history, type_ids, { + deleted: false, + }); +} -export const undeleteSelectedContent = updateSelectedContent({ - deleted: false, -}); - -export const purgeSelectedContent = updateSelectedContent({ - deleted: true, - purged: true, -}); +export async function purgeSelectedContent(history, type_ids) { + return await contentUpdate(history, type_ids, { + deleted: true, + purged: true, + }); +} export async function unhideAllHiddenContent(history) { const filter = { visible: false }; diff --git a/client/src/components/History/model/index.js b/client/src/components/History/model/index.js index 6e488a3de07..bc36542a48d 100644 --- a/client/src/components/History/model/index.js +++ b/client/src/components/History/model/index.js @@ -4,8 +4,6 @@ export { DatasetCollection } from "./DatasetCollection"; export { JobStateSummary } from "./JobStateSummary"; export { DateStore } from "./DateStore"; export { History } from "./History"; -export { ScrollPos } from "./ScrollPos"; -export { CurveFit } from "./CurveFit"; export { STATES } from "./states"; // crud functions, ajax call + cache diff --git a/client/src/components/History/model/queries.js b/client/src/components/History/model/queries.js index ee92eb052dd..953b104214c 100644 --- a/client/src/components/History/model/queries.js +++ b/client/src/components/History/model/queries.js @@ -80,9 +80,7 @@ function formData(fields = {}) { // #region History Queries -const stdHistoryParams = { - view: "betawebclient", -}; +const stdHistoryParams = {}; /** * Return list of available histories @@ -274,23 +272,23 @@ export async function updateContentFields(content, newFields = {}) { } /** - * Bulk update endpoint (TODO: rewrite) + * Updates history item contents * * @param {*} history * @param {*} type_ids * @param {*} fields */ -export async function bulkContentUpdate(history, type_ids = [], fields = {}) { +export async function contentUpdate(history, type_ids = [], fields = {}) { const items = type_ids.map((type_id) => { const [history_content_type, id] = type_id.split("-"); return { id, type_id, history_content_type }; }); const { id } = history; - const url = `/histories/${id}/contents?view=betawebclient`; + const url = `/histories/${id}/contents`; const payload = Object.assign({}, fields, { items }); const response = await api.put(url, payload); - console.log("bulkContentUpdate response", response); + console.debug("Submitted request to update selected content.", response); return doResponse(response); } @@ -309,7 +307,7 @@ export async function createDatasetCollection(history, inputs = {}) { }; const payload = Object.assign({}, defaults, inputs); - const url = `/histories/${history.id}/contents?view=betawebclient`; // keys=${keys}`; + const url = `/histories/${history.id}/contents`; const response = await api.post(url, payload); return doResponse(response); } diff --git a/client/src/components/History/readme.md b/client/src/components/History/readme.md deleted file mode 100644 index ed74cef8944..00000000000 --- a/client/src/components/History/readme.md +++ /dev/null @@ -1,69 +0,0 @@ -### Beta History Panel Component Tree -This is not intended to be a complete listing, but a general idea of how the components are intended -to interact with each other. - -```html static - - - - - - - - - - - - - - - - - - - - - - - - () - - - - - - - - - - - - - - - -
- - - - - () - - - - - - - - - - -``` \ No newline at end of file diff --git a/client/src/components/HistoryExport/ToLink.vue b/client/src/components/HistoryExport/ToLink.vue index e468c02fc76..041f95effdd 100644 --- a/client/src/components/HistoryExport/ToLink.vue +++ b/client/src/components/HistoryExport/ToLink.vue @@ -17,7 +17,11 @@
Link for download ready - . Use this link to download the archive or import it on another Galaxy server. +

Use this link to download the archive or import it on another Galaxy server.

+ History archives are removed at regular intervals. For permanent storage download the archive, export + to a remote file or import the archive on another Galaxy server. +

An out of date export is ready .

diff --git a/client/src/components/IconButton.vue b/client/src/components/IconButton.vue deleted file mode 100644 index c1c3f1d0171..00000000000 --- a/client/src/components/IconButton.vue +++ /dev/null @@ -1,50 +0,0 @@ - - - - - diff --git a/client/src/components/JobInformation/JobInformation.test.js b/client/src/components/JobInformation/JobInformation.test.js index 16c89702379..4881f5d93ec 100644 --- a/client/src/components/JobInformation/JobInformation.test.js +++ b/client/src/components/JobInformation/JobInformation.test.js @@ -1,14 +1,11 @@ -import Vuex from "vuex"; import axios from "axios"; import MockAdapter from "axios-mock-adapter"; import { mount } from "@vue/test-utils"; import { getLocalVue } from "jest/helpers"; import JobInformation from "./JobInformation"; -import datasetResponse from "components/DatasetInformation/testData/datasetResponse"; import jobResponse from "./testData/jobInformationResponse.json"; import flushPromises from "flush-promises"; -import createCache from "vuex-cache"; jest.mock("app"); @@ -16,26 +13,6 @@ const JOB_ID = "test_id"; const localVue = getLocalVue(); -const jobStore = new Vuex.Store({ - plugins: [createCache()], - modules: { - jobStore: { - actions: { - fetchDataset: jest.fn(), - fetchJob: jest.fn(), - }, - getters: { - dataset: (state) => (hda_id) => { - return datasetResponse; - }, - job: (state) => (hda_id) => { - return jobResponse; - }, - }, - }, - }, -}); - describe("JobInformation/JobInformation.vue", () => { let wrapper; let jobInfoTable; @@ -44,6 +21,7 @@ describe("JobInformation/JobInformation.vue", () => { beforeEach(() => { axiosMock = new MockAdapter(axios); axiosMock.onGet(new RegExp(`api/configuration/decode/*`)).reply(200, { decoded_id: 123 }); + axiosMock.onGet("/api/jobs/test_id?full=True").reply(200, jobResponse); }); afterEach(() => { @@ -65,14 +43,12 @@ describe("JobInformation/JobInformation.vue", () => { const propsData = { job_id: JOB_ID, }; - wrapper = mount(JobInformation, { - store: jobStore, propsData, localVue, }); - jobInfoTable = wrapper.find("#job-information"); await flushPromises(); + jobInfoTable = wrapper.find("#job-information"); }); it("job information table should be rendered", async () => { diff --git a/client/src/components/JobInformation/JobInformation.vue b/client/src/components/JobInformation/JobInformation.vue index 47e4043fcb5..5375af2f3dd 100644 --- a/client/src/components/JobInformation/JobInformation.vue +++ b/client/src/components/JobInformation/JobInformation.vue @@ -1,5 +1,6 @@ diff --git a/client/src/components/JobInformation/JobOutputs.vue b/client/src/components/JobInformation/JobOutputs.vue index a9f822fa877..feba2eecd7b 100644 --- a/client/src/components/JobInformation/JobOutputs.vue +++ b/client/src/components/JobInformation/JobOutputs.vue @@ -14,7 +14,11 @@ {{ value[0].label || name }}
@@ -23,11 +27,11 @@ diff --git a/client/src/components/RuleCollectionBuilder.vue b/client/src/components/RuleCollectionBuilder.vue index b100b6304fd..be324dfd116 100644 --- a/client/src/components/RuleCollectionBuilder.vue +++ b/client/src/components/RuleCollectionBuilder.vue @@ -339,7 +339,8 @@ + :saved-rules="this.savedRules" + :ruleColHeaders="colHeadersPerRule" />
Rows contain differing numbers of columns, there was likely a problem parsing your data. diff --git a/client/src/components/SelectionDialog/SelectionDialog.test.js b/client/src/components/SelectionDialog/SelectionDialog.test.js index 8e4bb928f86..c0c89d14ddf 100644 --- a/client/src/components/SelectionDialog/SelectionDialog.test.js +++ b/client/src/components/SelectionDialog/SelectionDialog.test.js @@ -36,8 +36,7 @@ describe("SelectionDialog.vue", () => { expect(wrapper.get(".fa-spinner")); expect(wrapper.get(".fa-spinner").text()).toBe(""); //expect(wrapper.get("#tree-options")).toThrow(); - wrapper.setProps({ optionsShow: true }); - await localVue.nextTick(); + await wrapper.setProps({ optionsShow: true }); expect(() => wrapper.get(".fa-spinner")).toThrow(); expect(wrapper.get("#tree-options")); }); @@ -50,10 +49,8 @@ describe("SelectionDialog.vue", () => { it("hideModal called on click cancel", async () => { expect(calledHide).toBe(false); expect(wrapper.get(".selection-dialog-modal-cancel")); - wrapper.setProps({ optionsShow: true }); - await localVue.nextTick(); - wrapper.find(".selection-dialog-modal-cancel").trigger("click"); - await localVue.nextTick(); + await wrapper.setProps({ optionsShow: true }); + await wrapper.find(".selection-dialog-modal-cancel").trigger("click"); expect(calledHide).toBe(true); }); }); diff --git a/client/src/components/Tags/StatelessTags.test.js b/client/src/components/Tags/StatelessTags.test.js index b65e510ff42..31f8f8e2335 100644 --- a/client/src/components/Tags/StatelessTags.test.js +++ b/client/src/components/Tags/StatelessTags.test.js @@ -1,7 +1,6 @@ import { mount, createLocalVue } from "@vue/test-utils"; import StatelessTags from "./StatelessTags"; import _l from "utils/localization"; -import flushPromises from "flush-promises"; describe("Tags/StatelessTags.vue", () => { const localVue = createLocalVue(); @@ -13,11 +12,10 @@ describe("Tags/StatelessTags.vue", () => { beforeEach(async () => { wrapper = mount(StatelessTags, { localVue }); - wrapper.setProps({ + await wrapper.setProps({ value: testTags, }); emitted = wrapper.emitted(); - await wrapper.vm.$nextTick(); }); it("should render a div for each tag", () => { @@ -47,8 +45,7 @@ describe("Tags/StatelessTags.vue", () => { it("should change internal model representation when new tag list assigned", async () => { expect(wrapper.vm.tagModels.length).toBe(3); const newTags = ["floob", "clown", "hoohah", "doodoo"]; - wrapper.setProps({ value: newTags }); - await flushPromises(); + await wrapper.setProps({ value: newTags }); expect(wrapper.vm.tagModels.length).toBe(newTags.length); }); }); diff --git a/client/src/components/Tags/Tags.test.js b/client/src/components/Tags/Tags.test.js index 468b62c431e..86e2986a81e 100644 --- a/client/src/components/Tags/Tags.test.js +++ b/client/src/components/Tags/Tags.test.js @@ -85,14 +85,7 @@ describe("Tags/Tags.vue", () => { const newTags = ["asdfadsadf", "gfhjfghjf"]; testStore.dispatch("updateTags", { key: storeKey, tags: newTags }); - // TODO: figure out how to make the computed observableTags - // prop update when the store does. This works in the real code, - // but does not update in this test environment. The following - // brute force mechanism of changing a different dependency works - // and effectively recalculates the computed value, but it should - // not be necessary - wrapper.setProps({ storeKey: "thisshouldbeunnecessary" }); - wrapper.setProps({ storeKey }); + await wrapper.setProps({ storeKey }); const observed = wrapper.vm.observedTags; expect(observed.length).toBe(newTags.length); diff --git a/client/src/components/Tool/ToolCard.vue b/client/src/components/Tool/ToolCard.vue index 3bc9b591045..8338deac6ab 100644 --- a/client/src/components/Tool/ToolCard.vue +++ b/client/src/components/Tool/ToolCard.vue @@ -107,7 +107,7 @@ diff --git a/client/src/components/Workflow/Editor/RefactorConfirmationModal.test.js b/client/src/components/Workflow/Editor/RefactorConfirmationModal.test.js index 68dcf0fe03a..71691f833a5 100644 --- a/client/src/components/Workflow/Editor/RefactorConfirmationModal.test.js +++ b/client/src/components/Workflow/Editor/RefactorConfirmationModal.test.js @@ -24,10 +24,9 @@ describe("RefactorConfirmationModal.vue", () => { }); it("should not attempt a dry run refactor is there are no actions", async () => { - wrapper.setProps({ + await wrapper.setProps({ refactorActions: [], }); - await flushPromises(); expect(refactor.mock.calls.length).toBe(0); }); @@ -37,7 +36,7 @@ describe("RefactorConfirmationModal.vue", () => { error("foo"); }) ); - wrapper.setProps({ + await wrapper.setProps({ refactorActions: [{ action_type: TEST_ACTION_TYPE }], }); await flushPromises(); @@ -57,7 +56,7 @@ describe("RefactorConfirmationModal.vue", () => { }); }) ); - wrapper.setProps({ + await wrapper.setProps({ refactorActions: [{ action_type: TEST_ACTION_TYPE }], }); await flushPromises(); @@ -91,10 +90,9 @@ describe("RefactorConfirmationModal.vue", () => { }); }) ); - wrapper.setProps({ + await wrapper.setProps({ refactorActions: [{ action_type: TEST_ACTION_TYPE }], }); - await flushPromises(); expect(wrapper.emitted().onWorkflowError).toBeFalsy(); // called with dry run... diff --git a/client/src/components/Workflow/Editor/ZoomControl.test.js b/client/src/components/Workflow/Editor/ZoomControl.test.js index 80fad50ab12..d66f89ac335 100644 --- a/client/src/components/Workflow/Editor/ZoomControl.test.js +++ b/client/src/components/Workflow/Editor/ZoomControl.test.js @@ -15,14 +15,11 @@ describe("ZoomControl", () => { }); const buttons = wrapper.findAll("button"); expect(buttons.length).toBe(3); - buttons.at(0).trigger("click"); - await localVue.nextTick(); + await buttons.at(0).trigger("click"); expect(wrapper.emitted().onZoom[0][0]).toBe(9); - buttons.at(1).trigger("click"); - await localVue.nextTick(); + await buttons.at(1).trigger("click"); expect(wrapper.emitted().onZoom[1][0]).toBe(10); - buttons.at(2).trigger("click"); - await localVue.nextTick(); + await buttons.at(2).trigger("click"); expect(wrapper.emitted().onZoom[2][0]).toBe(11); }); }); diff --git a/client/src/components/Workflow/Invocations.test.js b/client/src/components/Workflow/Invocations.test.js index 8ac4d64ed3f..9f43a94a238 100644 --- a/client/src/components/Workflow/Invocations.test.js +++ b/client/src/components/Workflow/Invocations.test.js @@ -9,10 +9,13 @@ import moment from "moment"; const localVue = getLocalVue(); describe("Invocations.vue without invocation", () => { + let axiosMock; let wrapper; let propsData; beforeEach(async () => { + axiosMock = new MockAdapter(axios); + axiosMock.onAny().reply(200, [], { total_matches: "0" }); propsData = { ownerGrid: false, }; @@ -22,6 +25,10 @@ describe("Invocations.vue without invocation", () => { }); }); + afterEach(() => { + axiosMock.reset(); + }); + it("title should be shown", async () => { expect(wrapper.find("#invocations-title").text()).toBe("Workflow Invocations"); }); @@ -38,7 +45,9 @@ describe("Invocations.vue with invocation", () => { beforeEach(async () => { axiosMock = new MockAdapter(axios); - axiosMock.onGet("/api/invocations").reply(200, [mockInvocationData], { total_matches: "1" }); + axiosMock + .onGet("/api/invocations", { params: { limit: 50, offset: 0, include_terminal: false } }) + .reply(200, [mockInvocationData], { total_matches: "1" }); propsData = { ownerGrid: false, loading: false, @@ -50,6 +59,9 @@ describe("Invocations.vue with invocation", () => { getWorkflowByInstanceId: (state) => (id) => { return { id: "workflowId" }; }, + getHistoryById: (state) => (id) => { + return { id: "historyId" }; + }, getHistoryNameById: () => () => "history name", }, stubs: { @@ -61,6 +73,10 @@ describe("Invocations.vue with invocation", () => { }); }); + afterEach(() => { + axiosMock.reset(); + }); + it("renders one row", async () => { const rows = wrapper.findAll("tbody > tr").wrappers; expect(rows.length).toBe(1); diff --git a/client/src/components/Workflow/Run/WorkflowRun.test.js b/client/src/components/Workflow/Run/WorkflowRun.test.js index 2e2bf583864..63e954522cf 100644 --- a/client/src/components/Workflow/Run/WorkflowRun.test.js +++ b/client/src/components/Workflow/Run/WorkflowRun.test.js @@ -57,6 +57,11 @@ describe("WorkflowRun.vue", () => { expect(model.hasUpgradeMessages).toBe(false); expect(model.hasStepVersionChanges).toBe(false); expect(model.wpInputs.wf_param.label).toBe("wf_param"); + // all steps are expanded since data and parameter steps are expanded by default, + // the same is true for tools with unconnected data inputs. + model.steps.forEach((step) => { + expect(step.expanded).toBe(true); + }); }); it("displays submission error", async () => { diff --git a/client/src/components/Workflow/Run/WorkflowRunForm.vue b/client/src/components/Workflow/Run/WorkflowRunForm.vue index 8b63094e327..5e32a7179f8 100644 --- a/client/src/components/Workflow/Run/WorkflowRunForm.vue +++ b/client/src/components/Workflow/Run/WorkflowRunForm.vue @@ -63,7 +63,7 @@ import CurrentUser from "components/providers/CurrentUser"; import FormDisplay from "components/Form/FormDisplay"; import FormCard from "components/Form/FormCard"; import FormElement from "components/Form/FormElement"; -import UserHistories from "components/providers/History/UserHistories"; +import UserHistories from "components/providers/UserHistories"; import WorkflowRunDefaultStep from "./WorkflowRunDefaultStep"; import WorkflowRunToolStep from "./WorkflowRunToolStep"; import { invokeWorkflow } from "./services"; @@ -206,6 +206,9 @@ export default { // Tool form always wants a list of invocations back // so that inputs can be batched. batch: true, + // the user is already warned if tool versions are wrong, + // they can still choose to invoke the workflow anyway. + require_exact_tool_versions: false, }; console.debug("WorkflowRunForm::onExecute()", "Ready for submission.", jobDef); diff --git a/client/src/components/Workflow/Run/WorkflowRunFormSimple.vue b/client/src/components/Workflow/Run/WorkflowRunFormSimple.vue index e95f828c33a..4718b9365e1 100644 --- a/client/src/components/Workflow/Run/WorkflowRunFormSimple.vue +++ b/client/src/components/Workflow/Run/WorkflowRunFormSimple.vue @@ -2,18 +2,9 @@
Workflow: {{ model.name }} - +
- - {{ errorMessage }} - - + Expand to full workflow form.
@@ -47,7 +38,6 @@ export default { }, data() { return { - errorMessage: null, formData: {}, inputTypes: {}, }; @@ -80,23 +70,12 @@ export default { }); return inputs; }, - runButtonDisabled() { - return this.errorMessage !== null; - }, }, methods: { - onValidation(validationError) { - this.errorMessage = validationError - ? "Please address the issues highlighted below before proceeding." - : null; - }, onChange(data) { this.formData = data; }, onExecute() { - if (this.errorMessage) { - return; - } const replacementParams = {}; const inputs = {}; for (const inputName in this.formData) { @@ -114,6 +93,7 @@ export default { inputs_by: "step_index", batch: true, use_cached_job: this.useJobCache, + require_exact_tool_versions: false, }; if (this.targetHistory == "current") { data.history_id = this.model.historyId; diff --git a/client/src/components/Workflow/Run/model.js b/client/src/components/Workflow/Run/model.js index fe1eeab994f..b6be54f4cf5 100644 --- a/client/src/components/Workflow/Run/model.js +++ b/client/src/components/Workflow/Run/model.js @@ -25,6 +25,7 @@ export class WorkflowRunModel { let hasReplacementParametersInToolForm = false; _.each(runData.steps, (step, i) => { + const isParameterStep = step.step_type == "parameter_input"; var title = `${parseInt(i + 1)}: ${step.step_label || step.step_name}`; if (step.annotation) { title += ` - ${step.annotation}`; @@ -36,7 +37,7 @@ export class WorkflowRunModel { { index: i, fixed_title: _.escape(title), - expanded: i == 0 || isDataStep(step), + expanded: i == 0 || isDataStep(step) || isParameterStep, errors: step.messages, }, step @@ -44,7 +45,7 @@ export class WorkflowRunModel { this.steps[i] = step; this.links[i] = []; this.parms[i] = {}; - if (step.step_type == "parameter_input" && step.step_label) { + if (isParameterStep && step.step_label) { this.parameterInputLabels.push(step.step_label); } }); diff --git a/client/src/components/Workflow/Run/testdata/run1.json b/client/src/components/Workflow/Run/testdata/run1.json index 9250e365e85..818c22ba5f2 100644 --- a/client/src/components/Workflow/Run/testdata/run1.json +++ b/client/src/components/Workflow/Run/testdata/run1.json @@ -748,6 +748,32 @@ "action": "/tool_runner/index", "model_class": "Tool", "display": true + }, + { + "inputs":[ + { + "area":false, + "argument":null, + "datalist":[], + "help":null, + "hidden":false, + "is_dynamic":false, + "label":"", + "model_class":"TextToolParameter", + "name":"input", + "optional":false, + "refresh_on_change":false, + "type":"text", + "value":null + } + ], + "output_connections":[], + "replacement_parameters":[], + "step_index":4, + "step_label":"", + "step_name":"Input parameter", + "step_type":"parameter_input", + "step_version":null } ], "has_upgrade_messages": false, diff --git a/client/src/components/WorkflowInvocationState/JobStep.test.js b/client/src/components/WorkflowInvocationState/JobStep.test.js index 1de49f115ad..1221f7fb172 100644 --- a/client/src/components/WorkflowInvocationState/JobStep.test.js +++ b/client/src/components/WorkflowInvocationState/JobStep.test.js @@ -35,13 +35,11 @@ describe("DatasetUIWrapper.vue with Dataset", () => { expect(wrapper.vm.toggledItems["1"]).toBe(undefined); expect(wrapper.find(".expanded").exists()).toBeFalsy(); // expand - wrapper.find("tbody").find("tbody").find("tr").trigger("click"); - await localVue.nextTick(); + await wrapper.find("tbody").find("tbody").find("tr").trigger("click"); expect(wrapper.vm.toggledItems["1"]).toBeTruthy(); expect(wrapper.find(".expanded").exists()).toBeTruthy(); - wrapper.find("tbody").find("tbody").find("tr").trigger("click"); + await wrapper.find("tbody").find("tbody").find("tr").trigger("click"); // close again - await localVue.nextTick(); expect(wrapper.vm.toggledItems["1"]).toBeFalsy(); expect(wrapper.find(".expanded").exists()).toBeFalsy(); }); @@ -50,16 +48,14 @@ describe("DatasetUIWrapper.vue with Dataset", () => { expect(wrapper.vm.toggledItems["1"]).toBe(undefined); expect(wrapper.find(".expanded").exists()).toBeFalsy(); // expand - wrapper.find("tbody").find("tbody").find("tr").trigger("click"); - await localVue.nextTick(); + await wrapper.find("tbody").find("tbody").find("tr").trigger("click"); expect(wrapper.vm.toggledItems["1"]).toBeTruthy(); expect(wrapper.find(".expanded").exists()).toBeTruthy(); // 2 collapsed rows, plus 1 expanded row expect(wrapper.find("tbody").findAll("tr").length).toBe(3); // update data const additionalJob = { ...jobs[0], id: 3 }; - wrapper.setProps({ jobs: [...jobs, additionalJob] }); - await localVue.nextTick(); + await wrapper.setProps({ jobs: [...jobs, additionalJob] }); // verify new data is displayed expect(wrapper.find("tbody").findAll("tr").length).toBe(4); // verify first row is still expanded diff --git a/client/src/components/WorkflowInvocationState/WorkflowInvocationDetails.vue b/client/src/components/WorkflowInvocationState/WorkflowInvocationDetails.vue index 1945ed4513e..2877ba03c59 100644 --- a/client/src/components/WorkflowInvocationState/WorkflowInvocationDetails.vue +++ b/client/src/components/WorkflowInvocationState/WorkflowInvocationDetails.vue @@ -14,7 +14,7 @@ :key="input.id" :data-label="dataInputStepLabel(key, input)"> {{ dataInputStepLabel(key, input) }} - +
@@ -23,7 +23,7 @@ Outputs
{{ key }}: - +
@@ -32,7 +32,7 @@ Output Collections
{{ key }}: - +
@@ -51,17 +51,16 @@ diff --git a/client/src/components/WorkflowInvocationState/WorkflowInvocationState.vue b/client/src/components/WorkflowInvocationState/WorkflowInvocationState.vue index 928e69e9dd4..dc28a0bc158 100644 --- a/client/src/components/WorkflowInvocationState/WorkflowInvocationState.vue +++ b/client/src/components/WorkflowInvocationState/WorkflowInvocationState.vue @@ -133,7 +133,7 @@ export default { return stepStates; }, invocationAndJobTerminal: function () { - return this.invocationSchedulingTerminal && this.jobStatesTerminal; + return !!(this.invocationSchedulingTerminal && this.jobStatesTerminal); }, invocationLink: function () { return getUrl(`workflows/invocations/report?id=${this.invocationId}`); diff --git a/client/src/components/WorkflowInvocationState/WorkflowInvocationStep.vue b/client/src/components/WorkflowInvocationState/WorkflowInvocationStep.vue index 92a493d86d0..c7b35ab9c66 100644 --- a/client/src/components/WorkflowInvocationState/WorkflowInvocationStep.vue +++ b/client/src/components/WorkflowInvocationState/WorkflowInvocationStep.vue @@ -11,7 +11,8 @@ + auto-refresh + v-slot="{ result: stepDetails, loading }">
@@ -21,7 +22,7 @@ Output Datasets
{{ name }} - +
Output Dataset Collections
{{ name }} - +
@@ -40,16 +41,20 @@ - + :parameters="[invocation.input_step_parameters[stepDetails.workflow_step_label]]" /> +
Workflow invocation for this step is not yet scheduled. -

+
This step consumes outputs from these steps:
  • import("components/WorkflowInvocationState/WorkflowInvocationState"), }, props: { diff --git a/client/src/components/plugins/legacyNavigation.js b/client/src/components/plugins/legacyNavigation.js index a0c64f920c6..0efac6066b1 100644 --- a/client/src/components/plugins/legacyNavigation.js +++ b/client/src/components/plugins/legacyNavigation.js @@ -7,67 +7,71 @@ import { getGalaxyInstance } from "app"; import { redirectToUrl, prependPath } from "utils/redirect"; import _l from "utils/localization"; +// wrapper for backbone router +export function backboneRoute(path, ...args) { + try { + getGalaxyInstance().router.push(path, ...args); + } catch (err) { + console.warn("Failed galaxy route change", err, ...arguments); + throw err; + } +} + +// wrapper for scratchbook/multiview feature +export function iframeAdd({ path, title = "Galaxy", tryIframe = true }) { + const Galaxy = getGalaxyInstance(); + if (Galaxy.frame && Galaxy.frame.active) { + Galaxy.frame.add({ + url: prependPath(path), + title: _l(title), + }); + return true; + } else if (tryIframe) { + return iframeRedirect(path); + } else { + return false; + } +} + +// straight ifrme redirect +export function iframeRedirect(path, target = "galaxy_main") { + try { + const targetFrame = window.frames[target]; + if (!targetFrame) { + throw new Error(`Requested frame ${target} doesn't exist`); + } + targetFrame.location = prependPath(path); + return true; + } catch (err) { + console.warn("Failed iframe redirect", err, ...arguments); + throw err; + } +} + +// straight window.location +export function redirect(path) { + console.log("legacyNavigation: go", path); + redirectToUrl(prependPath(path)); +} + +// arbitrary Galaxy wrapper +export function useGalaxy(fn) { + return fn(getGalaxyInstance()); +} + +// wrapper for navigation to be used as mixin export const legacyNavigationMixin = { methods: { - // straight window.location - redirect(path) { - console.log("legacyNavigation: go", path); - redirectToUrl(prependPath(path)); - }, - - // weird wrapper for Galaxy's wierd frame navigation - // checks for existing galaxy frame reference, uses that or - // loads a brand new document in the iframe - iframeAdd({ path, title = "Galaxy", tryIframe = true }) { - const Galaxy = getGalaxyInstance(); - if (Galaxy.frame && Galaxy.frame.active) { - Galaxy.frame.add({ - url: prependPath(path), - title: _l(title), - }); - return true; - } else if (tryIframe) { - return this.iframeRedirect(path); - } else { - return false; - } - }, - - // straight ifrme redirect - iframeRedirect(path, target = "galaxy_main") { - try { - const targetFrame = window.frames[target]; - if (!targetFrame) { - throw new Error(`Requested frame ${target} doesn't exist`); - } - targetFrame.location = prependPath(path); - return true; - } catch (err) { - console.warn("Failed iframe redirect", err, ...arguments); - throw err; - } - }, - - // galaxy router, wrapper for backbone router - backboneRoute(path, ...args) { - try { - getGalaxyInstance().router.push(path, ...args); - } catch (err) { - console.warn("Failed galaxy route change", err, ...arguments); - throw err; - } - }, - - // arbitrary Galaxy wrapper - useGalaxy(fn) { - return fn(getGalaxyInstance()); - }, - - // prepend server prefix from configs to a url + redirect, + iframeAdd, + iframeRedirect, + backboneRoute, + useGalaxy, prependPath, }, }; +// wrapper for navigation to be used as plugin export const legacyNavigationPlugin = { install(Vue) { Vue.mixin(legacyNavigationMixin); diff --git a/client/src/components/providers/DatasetCollectionProvider.js b/client/src/components/providers/DatasetCollectionProvider.js index 0192b64eedd..1ad2c15a2a8 100644 --- a/client/src/components/providers/DatasetCollectionProvider.js +++ b/client/src/components/providers/DatasetCollectionProvider.js @@ -1,11 +1,18 @@ -import { default as RxProviderMixin } from "./rxProviders"; -import { datasetCollectionMonitor } from "./monitors"; +import axios from "axios"; +import { getAppRoot } from "onload/loadConfig"; +import { rethrowSimple } from "utils/simple-error"; +import { SingleQueryProvider } from "components/providers/SingleQueryProvider"; -export default { - mixins: [RxProviderMixin], - methods: { - buildMonitor() { - return datasetCollectionMonitor(); - }, - }, -}; +async function getDatasetCollection({ id }) { + const url = `${getAppRoot()}api/dataset_collections/${id}?instance_type=history`; + try { + const { data } = await axios.get(url); + return data; + } catch (e) { + rethrowSimple(e); + } +} + +// There isn't really a good way to know when to stop polling for HDCA updates, +// but we know the populated_state should at least be ok. +export default SingleQueryProvider(getDatasetCollection, (result) => result.populated_state === "ok"); diff --git a/client/src/components/providers/DatasetProvider.js b/client/src/components/providers/DatasetProvider.js index 7ee82132e0f..7aeb33777b7 100644 --- a/client/src/components/providers/DatasetProvider.js +++ b/client/src/components/providers/DatasetProvider.js @@ -1,11 +1,20 @@ import axios from "axios"; import { getAppRoot } from "onload/loadConfig"; import { SingleQueryProvider } from "components/providers/SingleQueryProvider"; -import { default as RxProviderMixin } from "./rxProviders"; -import { datasetMonitor } from "./monitors"; import { rethrowSimple } from "utils/simple-error"; +import { stateIsTerminal } from "./utils"; -async function datasetAttributes({ id }) { +async function getDataset({ id }) { + const url = `${getAppRoot()}api/datasets/${id}`; + try { + const { data } = await axios.get(url); + return data; + } catch (e) { + rethrowSimple(e); + } +} + +async function getDatasetAttributes({ id }) { const url = `${getAppRoot()}dataset/get_edit?dataset_id=${id}`; try { const { data } = await axios.get(url); @@ -15,12 +24,5 @@ async function datasetAttributes({ id }) { } } -export const DatasetAttributesProvider = SingleQueryProvider(datasetAttributes); -export default { - mixins: [RxProviderMixin], - methods: { - buildMonitor() { - return datasetMonitor(); - }, - }, -}; +export const DatasetAttributesProvider = SingleQueryProvider(getDatasetAttributes); +export default SingleQueryProvider(getDataset, stateIsTerminal); diff --git a/client/src/components/providers/History/CollectionContentProvider/CollectionContentProvider.js b/client/src/components/providers/History/CollectionContentProvider/CollectionContentProvider.js deleted file mode 100644 index 4e0c94f1e75..00000000000 --- a/client/src/components/providers/History/CollectionContentProvider/CollectionContentProvider.js +++ /dev/null @@ -1,39 +0,0 @@ -import { DatasetCollection } from "components/History/model/DatasetCollection"; -import { SearchParams } from "components/providers/History/SearchParams"; -import { ContentProvider, processContentStreams } from "../ContentProvider"; -import { collectionPayload } from "./collectionPayload"; - -export default { - mixins: [ContentProvider], - - props: { - params: { type: SearchParams, default: () => new SearchParams({ showHidden: false }) }, - }, - - computed: { - dsc() { - if (this.parent instanceof DatasetCollection) { - return this.parent; - } - return new DatasetCollection(this.parent); - }, - }, - - watch: { - dsc(newDsc, oldDsc) { - if (!(newDsc.id == oldDsc.id)) { - this.resetScrollPos(); - } - }, - }, - - methods: { - initStreams() { - const { debouncePeriod, pageSize, params$, scrollPos$, debug } = this; - const parent$ = this.watch$("dsc", true); - const sources = { params$, parent$, scrollPos$ }; - const settings = { debouncePeriod, pageSize, debug }; - return processContentStreams(collectionPayload, sources, settings); - }, - }, -}; diff --git a/client/src/components/providers/History/CollectionContentProvider/CollectionContentProvider.test.js b/client/src/components/providers/History/CollectionContentProvider/CollectionContentProvider.test.js deleted file mode 100644 index a59c41d2b23..00000000000 --- a/client/src/components/providers/History/CollectionContentProvider/CollectionContentProvider.test.js +++ /dev/null @@ -1,160 +0,0 @@ -import { map } from "rxjs/operators"; -import { watchUntil, getLocalVue } from "jest/helpers"; -import { shallowMount } from "@vue/test-utils"; -import { - cacheContent, - cacheCollectionContent, - wipeDatabase, - loadDscContent, -} from "components/providers/History/caching"; -import { bulkCacheDscContent } from "components/providers/History/caching/db/observables"; -import { summarizeCacheOperation } from "components/providers/History/caching/loadHistoryContents"; -import { SearchParams } from "components/providers/History/SearchParams"; -import CollectionContentProvider from "./CollectionContentProvider"; -import { sameVueObj, payloadChange } from "components/providers/History/test/providerTestHelpers"; -import { testCollection, testCollectionContent } from "components/providers/History/test/testCollection"; -import { defaultPayload } from "../ContentProvider"; - -// mocking -jest.mock("app"); -jest.mock("components/providers/History/caching"); - -// fake server call -// prettier-ignore -loadDscContent.mockImplementation((cfg) => (inputs$) => { - return inputs$.pipe( - map((inputs) => { - const [url, , pagination] = inputs; - const { offset, limit } = pagination; - // console.log("mock loader", url, pagination); - const result = testCollectionContent - .filter((row) => url == row.parent_url) - .slice(offset, offset + limit); - return result; - }), - bulkCacheDscContent(), - summarizeCacheOperation() - ); -}); - -let dsc; -const localVue = getLocalVue(); - -beforeEach(async () => { - dsc = await cacheContent(testCollection, true); -}); - -afterEach(async () => { - dsc = null; - await wipeDatabase(); -}); - -describe("CollectionContentProvider", () => { - const pageSize = 5; - const debouncePeriod = 250; - let wrapper; - - afterEach(async () => { - if (wrapper) { - wrapper.destroy(); - } - wrapper = undefined; - }); - - describe("blank initialization state", () => { - // current payload - beforeEach(async () => { - wrapper = await shallowMount(CollectionContentProvider, { - localVue, - propsData: { - parent: dsc, - pageSize, - debug: false, - debouncePeriod, - }, - scopedSlots: { - default() {}, - }, - }); - await wrapper.vm.$nextTick(); - }); - - test("should expose update methods", () => { - // allows child scroller component to update the position in the history - // via an event handler - expect(wrapper.vm.setScrollPos).toBeInstanceOf(Function); - }); - - test("should show an empty payload", () => { - expect(SearchParams.equals(new SearchParams(), wrapper.vm.params)).toBe(true); - expect(sameVueObj(wrapper.vm.payload, defaultPayload)); - }); - }); - - describe("waiting for simulated server loads", () => { - beforeEach(async () => { - wrapper = await shallowMount(CollectionContentProvider, { - localVue, - propsData: { - parent: dsc, - pageSize, - debug: false, - debouncePeriod, - }, - scopedSlots: { - default: function (props) { - // reportPayload(props.payload, { indexKey: "element_index" }); - // payload = props.payload; - }, - }, - }); - - // eslint-disable-next-line no-unused-vars - const spinUp = await watchUntil(wrapper.vm, function () { - const allDone = wrapper.vm.payload.contents.length > 0; - // console.log("done?", allDone); - return allDone; - }); - - // console.log(spinUp); - }); - - test("initial load, no scrolling", async () => { - const payload = wrapper.vm.payload; - expect(payload).toBeDefined(); - // reportPayload(payload, { indexKey: "element_index" }); - // reportPayload(initialPayload, { indexKey: "element_index" }); - const { contents, topRows, bottomRows, startKey, totalMatches } = payload; - expect(topRows).toEqual(0); - expect(contents.length).toBeGreaterThanOrEqual(2 * wrapper.vm.pageSize); - expect(totalMatches).toEqual(testCollectionContent.length); - expect(bottomRows).toBeGreaterThan(0); - expect(bottomRows).toEqual(testCollectionContent.length - contents.length); - - const testStartIndex = testCollectionContent[0].element_index; - expect(startKey).toEqual(testStartIndex); - }); - - test("should update to reflect independent cache changes", async () => { - let hasFoobar; - const payload = wrapper.vm.payload; - - // check initial emit, for foobars - expect(payload).toBeDefined(); - hasFoobar = payload.contents.some((o) => o.foobar == 123); - expect(hasFoobar).toBeFalsy(); - - // add a foobar - const testChild = payload.contents[0]; - testChild.foobar = 123; - const updateResult = await cacheCollectionContent(testChild, true); - expect(updateResult.foobar).toEqual(123); - - // check subsequent payload for a foobar - const updatedPayload = await payloadChange({ vm: wrapper.vm }); - // reportPayload(updatedPayload, { label: "updated", indexKey: "element_index" }); - hasFoobar = updatedPayload.contents.some((o) => o.foobar == 123); - expect(hasFoobar).toBeTruthy(); - }); - }); -}); diff --git a/client/src/components/providers/History/CollectionContentProvider/collectionPayload.js b/client/src/components/providers/History/CollectionContentProvider/collectionPayload.js deleted file mode 100644 index b2e56de0860..00000000000 --- a/client/src/components/providers/History/CollectionContentProvider/collectionPayload.js +++ /dev/null @@ -1,77 +0,0 @@ -import { of, Observable, Subject, partition, merge } from "rxjs"; -import { tap, map, pluck, switchMap, publish, distinctUntilChanged, share, throttleTime } from "rxjs/operators"; -import { chunk } from "utils/observable"; -import { SearchParams } from "components/providers/History/SearchParams"; -import { loadDscContent } from "components/providers/History/caching"; -import { watchCollection } from "./watchCollection"; - -// prettier-ignore -export const collectionPayload = (cfg = {}) => { - const { - parent: dsc, - filters = new SearchParams(), - pageSize = SearchParams.pageSize, - debug = false, - loadingEvents$ = new Subject(), - debouncePeriod = 100, - chunkSize = pageSize - } = cfg; - - const { totalElements: totalMatches, contents_url } = dsc; - - return publish((pos$) => { - - // split scroll position into objects where we exactly know which key to focus on, and those - // where we have to figure out where to start - const [ noKey$, hasKey$ ] = partition(pos$, (pos) => pos.key === null); - - // if we don't know the exact elementIndex we need to calculate it from the cursor (portion - // of the total scroller height) and the total matches which we should have from the dsc props - const calculatedIndex$ = noKey$.pipe( - pluck('cursor'), - map((cursor) => Math.round(cursor * totalMatches)), - ); - const knownIndex$ = hasKey$.pipe(pluck('key')); - const elementIndex$ = merge(knownIndex$, calculatedIndex$).pipe(share()); - - // chunk the input to the server request so we don't request for every single mouse move - const chunked$ = elementIndex$.pipe( - chunk(chunkSize), - distinctUntilChanged(), - ); - - // overlap one page before and one page after, 3 total pages queried - const pagination$ = chunked$.pipe( - map(targetRow => { - const offset = Math.max(0, targetRow - pageSize); - const limit = 3 * pageSize; - return { offset, limit, targetRow }; - }), - ); - - const serverLoad$ = pagination$.pipe( - throttleTime(debouncePeriod, undefined, { trailing: true }), - switchMap(pagination => of([contents_url, filters, pagination]).pipe( - tap(() => loadingEvents$.next(true)), - loadDscContent({ debug }), - tap(() => loadingEvents$.next(false)), - )), - ); - - const payload$ = elementIndex$.pipe( - watchCollection({ dsc, filters, ...cfg}), - map((result) => { - const { contents } = result; - const topRows = contents.length ? contents[0].element_index : 0; - const bottomRows = Math.max(0, totalMatches - contents.length - topRows); - return { ...result, topRows, bottomRows, totalMatches }; - }), - ); - - return new Observable((obs) => { - const watchSub = payload$.subscribe(obs); - watchSub.add(serverLoad$.subscribe()); - return watchSub; - }); - }) -}; diff --git a/client/src/components/providers/History/CollectionContentProvider/index.js b/client/src/components/providers/History/CollectionContentProvider/index.js deleted file mode 100644 index 2f49418948b..00000000000 --- a/client/src/components/providers/History/CollectionContentProvider/index.js +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Same basic format as historyContentProvider, but it should be simpler than - * the history because: - * - * 1. Collections are immutable after creation so they don't change over time, - * meaning there is no polling and no need to do "random access" as we do for - * histories since we can just limit/offset to reach everything since the - * total matches count will always be the same. A "total matches" exists in - * the basic content record in the form of "element_count", and we already - * have that value in the DSC which is passed in. - * - * 2. There is no filtering for collections on the server so all we can do is - * page through the results - */ - -import CollectionContentProvider from "./CollectionContentProvider"; - -export default CollectionContentProvider; diff --git a/client/src/components/providers/History/CollectionContentProvider/watchCollection.js b/client/src/components/providers/History/CollectionContentProvider/watchCollection.js deleted file mode 100644 index 2d024c61819..00000000000 --- a/client/src/components/providers/History/CollectionContentProvider/watchCollection.js +++ /dev/null @@ -1,38 +0,0 @@ -import { of } from "rxjs"; -import { map } from "rxjs/operators"; -import { aggregateCacheUpdates } from "../ContentProvider"; -import { monitorCollectionContent } from "components/providers/History/caching"; -import { SEEK } from "components/providers/History/caching/enums"; -import { SearchParams } from "components/providers/History/SearchParams"; -// import { show } from "utils/observable"; - -// prettier-ignore -export const watchCollection = (cfg = {}) => elementIndex$ => { - const { - dsc, - filters = new SearchParams(), - pageSize = SearchParams.pageSize, - debug = false, - keyField = "element_index", - keyDirection = SEEK.ASC, - ...otherConfig - } = cfg; - - // builds a monitor observable based on a element_index - const monitor = (idx) => of([dsc.contents_url, filters, idx]).pipe( - monitorCollectionContent({ pageSize, debug: false, keyField }), - ); - - // handles the plumbing for observing the query (monitor) and collecting the output into an - // aggregate ordered list focused around the current target key (element_index in this case) - return elementIndex$.pipe( - map(val => parseInt(val)), - aggregateCacheUpdates(monitor, { - keyField, - keyDirection, - pageSize, - debug, - ...otherConfig, - }), - ); -}; diff --git a/client/src/components/providers/History/CollectionContentProvider/watchCollection.test.js b/client/src/components/providers/History/CollectionContentProvider/watchCollection.test.js deleted file mode 100644 index 55f7307ce58..00000000000 --- a/client/src/components/providers/History/CollectionContentProvider/watchCollection.test.js +++ /dev/null @@ -1,125 +0,0 @@ -import { of, timer } from "rxjs"; -import { takeUntil, share } from "rxjs/operators"; -import { ObserverSpy } from "@hirez_io/observer-spy"; -import { watchCollection } from "./watchCollection"; -import { cacheCollectionContent, bulkCacheDscContent, wipeDatabase } from "components/providers/History/caching"; -import { testCollectionContent, testCollection } from "components/providers/History/test/testCollection"; -import { untilNthEmission } from "jest/helpers"; - -// need to mock monitorHistoryContent which is instantiated inside the worker, -// but we can't actually fire up threads.js in a unit test because you can't -// call expose() on the main thread, so we mock the adapter functions which -// generate observables from the worker. -jest.mock("app"); -jest.mock("components/providers/History/caching"); - -const debouncePeriod = 250; -const safetyTimeout = 1000; -const pageSize = 5; -const pluckAll = (arr, prop) => arr.map((o) => o[prop]); - -let cachedCollectionContent; - -beforeEach(async () => { - await wipeDatabase(); - cachedCollectionContent = await bulkCacheDscContent(testCollectionContent, true); - // const cachedIds = pluckAll(cachedCollectionContent, "_id"); - // console.log(cachedIds); -}); - -afterEach(async () => { - await wipeDatabase(); -}); - -describe("watchCollection", () => { - test("should observe pre-inserted content on the initial emission", async () => { - // the srource stream for the operator is an element_index - const watcher$ = of(0).pipe( - watchCollection({ - dsc: testCollection, - pageSize, - debouncePeriod, - debug: false, - }), - takeUntil(timer(safetyTimeout)) - ); - - // spy on output of observable, wait for it to end - const spy = new ObserverSpy(); - watcher$.subscribe(spy); - await spy.onComplete(); - - expect(spy.receivedNext()).toBe(true); - expect(spy.receivedComplete()).toBe(true); - expect(spy.getValuesLength()).toBe(1); - - const payload = spy.getFirstValue(); - { - const { startKey, startKeyIndex, targetKey, contents } = payload; - - // at the top of the list, should get the match (element_index = 0) + 2 pages - expect(startKey).toEqual(0); - expect(startKeyIndex).toEqual(0); - expect(targetKey).toEqual(0); - expect(Array.isArray(contents)).toBe(true); - - // should get back the exact match (element_index = 0, the following page, + 1 - // additional page of buffer-room after the current page - const returnedIndexes = pluckAll(contents, "element_index"); - // console.log(returnedIndexes); - - expect(contents.length).toBeGreaterThanOrEqual(2 * pageSize); - expect(returnedIndexes).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); - } - }); - - test("should observe subsequent updates", async () => { - // the srource stream for the operator is an element_index - const watcher$ = of(0).pipe( - watchCollection({ - dsc: testCollection, - debouncePeriod, - pageSize, - }), - takeUntil(timer(safetyTimeout)), - share() - ); - - const spy = new ObserverSpy(); - watcher$.subscribe(spy); - - // After the first event, change some stuff on the first row - // need to delay a little to wait for first emissions to run through - // otherwise the debouncing will think the 2nd update is the same as the - await untilNthEmission(watcher$, 1); - const testChild = cachedCollectionContent[0]; - testChild.foobar = 123; - const updateResult = await cacheCollectionContent(testChild, true); - // console.log("after update", updateResult); - expect(updateResult.foobar).toEqual(123); - expect(updateResult._rev).not.toEqual(testCollection._rev); - - await spy.onComplete(); - expect(spy.receivedNext()).toBe(true); - expect(spy.receivedComplete()).toBe(true); - expect(spy.getValuesLength()).toEqual(2); - - const firstPayload = spy.getFirstValue(); - { - const { contents } = firstPayload; - expect(Array.isArray(contents)).toBe(true); - // no foobar because this happened before the update - const foobars = pluckAll(contents, "foobar"); - expect(foobars.every((val) => val === undefined)).toBe(true); - } - - const lastPayload = spy.getLastValue(); - { - const { contents } = lastPayload; - expect(Array.isArray(contents)).toBe(true); - // should have a foobar - const foobars = pluckAll(contents, "foobar"); - expect(foobars.every((val) => val === undefined)).toBe(false); - } - }); -}); diff --git a/client/src/components/providers/History/ContentProvider/ContentProvider.js b/client/src/components/providers/History/ContentProvider/ContentProvider.js deleted file mode 100644 index e4f7fe88a54..00000000000 --- a/client/src/components/providers/History/ContentProvider/ContentProvider.js +++ /dev/null @@ -1,128 +0,0 @@ -import { NEVER } from "rxjs"; -import { ScrollPos } from "components/History/model/ScrollPos"; -import { SearchParams } from "components/providers/History/SearchParams"; -import { isValidNumber } from "utils/validation"; - -// first emission, emitted when parent (history) or filters changes to reset the view -export const defaultPayload = { - contents: [], - targetKey: null, - startKey: null, - startKeyIndex: 0, - topRows: 0, - bottomRows: 0, - totalMatches: 0, -}; - -export const ContentProvider = { - props: { - parent: { type: Object, required: true }, - params: { type: SearchParams, default: () => new SearchParams() }, - pageSize: { type: Number, default: SearchParams.pageSize }, - debouncePeriod: { type: Number, default: 1000 }, - debug: { type: Boolean, default: false }, - }, - - computed: { - busy() { - return this.loading || this.scrolling; - }, - slotProps() { - return { - // actual content delivery - payload: this.payload, - - // local vars/settings/props passthrough - scrollPos: this.scrollPos, - loading: this.loading, - scrolling: this.scrolling, - busy: this.busy, - params: this.params, - pageSize: this.pageSize, - - // update methods - setScrollPos: this.setScrollPos, - manualReload: this.manualReload, - }; - }, - }, - - data() { - return { - payload: defaultPayload, - scrollPos: new ScrollPos(), - loading: false, - scrolling: false, - }; - }, - - watch: { - params(newParams, oldParams) { - if (!SearchParams.equals(newParams, oldParams)) { - this.resetScrollPos(); - } - }, - }, - - created() { - this.params$ = this.watch$("params"); - this.scrollPos$ = this.watch$("scrollPos"); - const { payload$, loading$, scrolling$, resetPos$ = NEVER } = this.initStreams(); - - this.listenTo(scrolling$, (val) => (this.scrolling = val)); - this.listenTo(loading$, (val) => (this.loading = val)); - this.listenTo(resetPos$, (pos) => this.setScrollPos(pos)); - - // render output - this.listenTo(payload$, { - next: (payload) => this.setPayload(payload), - error: (err) => console.warn("error in cache$", err), - complete: () => console.warn("why did cache$ complete?"), - }); - }, - - methods: { - resetScrollPos() { - this.setScrollPos(); - }, - - initStreams() { - console.warn("Override initStreams in ContentProvider"); - return { payload$: NEVER, loading$: NEVER, scrolling$: NEVER, resetPos$: NEVER }; - }, - - /** - * Handler for the scroller to update its position, either by key or - * cursor (0-1 value representing how far down it is) - * @param {object} Object consisting of key and cursor props - */ - setScrollPos({ cursor = null, key = null } = {}) { - if (isValidNumber(cursor) || key !== null) { - this.scrollPos = ScrollPos.create({ cursor, key }); - } - }, - - /** - * After bulk operations there is a need to trigger a fresh load. - */ - manualReload() { - this.setScrollPos(this.scrollPos$.value); - }, - - /** - * Render cache observable results to the payload property. It's best to - * set everything at once to avoid multiple render passes. - * @param {object} result Cache observable response - */ - setPayload(props = {}) { - const payload = Object.assign({}, defaultPayload, props); - this.$set(this, "payload", payload); - }, - }, - - render() { - return this.$scopedSlots.default(this.slotProps); - }, -}; - -export default ContentProvider; diff --git a/client/src/components/providers/History/ContentProvider/aggregateCacheUpdates.js b/client/src/components/providers/History/ContentProvider/aggregateCacheUpdates.js deleted file mode 100644 index eae7be69a01..00000000000 --- a/client/src/components/providers/History/ContentProvider/aggregateCacheUpdates.js +++ /dev/null @@ -1,109 +0,0 @@ -// Generalized bi-directional watch and aggregator. -// Looks at cache from starting index, then looks up and down - -import { throwError, from, EMPTY } from "rxjs"; -import { - map, - debounceTime, - distinctUntilChanged, - groupBy, - reduce, - mergeMap, - switchMap, - withLatestFrom, - timeoutWith, - ignoreElements, -} from "rxjs/operators"; -import { show, debounceBurst, shareButDie } from "utils/observable"; -import { SearchParams } from "components/providers/History/SearchParams"; -import { processContentUpdate, newUpdateMap, buildContentResult, getKeyForUpdateMap } from "./aggregation"; -import { SEEK } from "components/providers/History/caching/enums"; -import { reportPayload } from "./helpers"; - -// prettier-ignore -export const aggregateCacheUpdates = (monitor, cfg = {}) => (src$) => { - // monitor is a function that generates the update observable - if (!(monitor instanceof Function)) { - return throwError("monitor should be a function in aggregateCacheUpdates"); - } - - const { - // number of rows we roughly expect to be in the window - pageSize = SearchParams.pageSize, - - // input and output throttling - debouncePeriod = 250, - - // field and key order for the contents - keyField = "hid", - keyDirection = SEEK.DESC, - - // reduces duplicate updates to the skiplist - // which can be expensive - updateGroupLifetime = 2 * debouncePeriod, - - debug = false, - } = cfg; - - // function that extracts the key from a document - const getKey = getKeyForUpdateMap(keyField); - - // incoming target key - const targetKey$ = src$.pipe(shareButDie(1)); - - // avoids creating a new monitor for every single key emission - // chunk to ceiling if descending, floor if ascending - const monitorInputKey$ = targetKey$.pipe( - distinctUntilChanged(), - show(debug, (key) => console.log("monitorInputKey", key)), - ); - - // incoming stream of cache updates - // Monitor generation function is passed in via config so we can use it against different types of queries - const cacheUpdates$ = monitorInputKey$.pipe( - switchMap(monitor), - show(debug, (change) => console.log("monitor output", change)), - ); - - // groups updates by skiplist key and debounces those - // streams individually so we're not pointlessly updating - // when we're just going to change it again shortly - const groupedUpdates$ = cacheUpdates$.pipe( - groupBy( - update => update.key, - update => update, - updateByKey$ => updateByKey$.pipe( - timeoutWith(updateGroupLifetime, EMPTY), - ignoreElements() - ) - ), - // eliminates rapid double updates to same item - mergeMap(updateByKey$ => updateByKey$.pipe( - debounceTime(0.25 * debouncePeriod), - )), - debounceBurst(debouncePeriod), - show(debug, changeBurst => console.log('burst', changeBurst.map(o => o.key))), - ); - - // aggregates changes into a skiplist. - const storage = newUpdateMap(); - const currentMap$ = groupedUpdates$.pipe( - mergeMap(updateList => from(updateList).pipe( - reduce(processContentUpdate, storage) - )), - show(debug, () => console.log('grouped update burst done')), - ); - - const contentListInputs$ = currentMap$.pipe( - withLatestFrom(targetKey$), - ); - - // query skiplist with scroll position (transformed into a key from the skiplist) - // to produce list of nearby content - const contentList$ = contentListInputs$.pipe( - map(buildContentResult({ pageSize, keyDirection, getKey })), - show(debug, payload => reportPayload(payload, { indexKey: keyField, label: "aggregateCacheUpdates payload" })), - ); - - return contentList$; -}; diff --git a/client/src/components/providers/History/ContentProvider/aggregation.js b/client/src/components/providers/History/ContentProvider/aggregation.js deleted file mode 100644 index 40e17f342be..00000000000 --- a/client/src/components/providers/History/ContentProvider/aggregation.js +++ /dev/null @@ -1,158 +0,0 @@ -/** - * Functions for aggregating incoming cache updates for presentation to the - * scroller. As cache changes roll in, the updates are summarized and sorted in - * a SkipList for fast, ordered retrieval. - * - * TODO: PouchDB has similar aggregation capabilities but when last I looked they - * were too difficult to get working properly with dynamically changing queries. - * I feel like this is the duty of the caching layer, however, and we should - * investigate again. - */ - -import SkipList from "proper-skip-list"; -import { pipe, take, filter } from "iter-tools"; -import { SEEK } from "components/providers/History/caching/enums"; -import { SearchParams } from "components/providers/History/SearchParams"; - -// utils for buildContentResult -const distance = (a, b) => Math.abs(+a - +b); - -export const buildContentResult = (cfg = {}) => { - const { keyDirection = SEEK.ASC, pageSize = SearchParams.pageSize } = cfg; - - if (!SEEK.isValid(keyDirection)) { - throw new Error(`buildContentResult: invalid sort direction: ${keyDirection}`); - } - - return ([updateMap, targetKey]) => { - // console.log("buildContentResult", updateMap, rawTargetKey, typeof targetKey); - - if (updateMap === undefined) { - throw new Error("missing skiplist"); - } - if (targetKey === undefined) { - throw new Error("missing target key"); - } - // make sure key is an int - if (!Number.isFinite(targetKey)) { - console.warn("Target key should be numeric", updateMap, targetKey); - throw new Error("buildContentResult: targetKey must be a numeric value", targetKey); - } - - const { matchingValue, desc, asc } = updateMap.findEntries(targetKey); - - // We want 3 whole pages, 1 behind us, the one we're on, and 1 ahead of us, this means - // we have to figure out what "behind" and "in front" mean based on the key direction - - // if the keys are ascending, "down" the list is the asc iterator - // if the keys are ascending, "up" the list is the desc iterator - - const isAscending = keyDirection == SEEK.ASC; - const topIterator = isAscending ? desc : asc; - const bottomIterator = isAscending ? asc : desc; - - const topFilter = pipe( - filter(([k]) => (isAscending ? k < targetKey : k > targetKey)), - take(pageSize) - ); - - const bottomFilter = pipe( - filter(([k]) => (isAscending ? k > targetKey : k < targetKey)), - take(2 * pageSize) - ); - - const top = Array.from(topFilter(topIterator)); - const bottom = Array.from(bottomFilter(bottomIterator)); - - // assemble contents array by combining the results - const match = matchingValue ? [[targetKey, matchingValue]] : []; - const contentEntries = [...top.reverse(), ...match, ...bottom]; - - // startKey is the closest key in the result set to the requested targetKey - const { startKey, startKeyIndex } = findClosestEntry(targetKey, contentEntries); - - const contents = contentEntries.map(([k, v]) => v); - - return { - // list of contents from the skiplist around the targetKey - contents, - - // original value used to query the skiplist, should be in - // the middle of the contents list - targetKey, - - // closest key to the targetKey that actually exists in the slice - startKey, - - // index of the closest row matching the targetKey, also should - // be in the middle of the contents - startKeyIndex, - }; - }; -}; - -/** - * Finds closest of keys by measuring numeric distance from targetKey - * - * @param {String|Number} targetKey Key to match - * @param {String|Number} entries content entries from the map i.e. list of [key,val] - * - * @return {Object} startKey, startKeyIndex - */ -const findClosestEntry = (targetKey, entries = []) => { - const startingVal = { startKey: null, startKeyIndex: -1, distance: Infinity }; - return entries - .filter((entry) => Array.isArray(entry)) - .reduce((result, entry, idx) => { - const [key] = entry; - const myDistance = distance(targetKey, key); - return myDistance < result.distance ? { startKey: key, startKeyIndex: idx, distance: myDistance } : result; - }, startingVal); -}; - -/** - * Fresh update map, gets built when inputs change - */ -export const newUpdateMap = () => { - return new SkipList(); -}; - -/** - * Generates a scan function for consuming updates from the pouchdb-live-find. - * Adds or removes incoming docs from the skiplist - */ -export const processContentUpdate = (resultMap, update) => { - const { doc, key, match } = update; - - // make sure key is an int - const storageKey = parseInt(key); - - if (match) { - resultMap.upsert(storageKey, doc); - } else if (resultMap.has(storageKey)) { - resultMap.delete(storageKey); - } - - return resultMap; -}; - -// Configurable function returns a key for the updateMap from a doc during upate operations -// aggregate results in a map for each set of id + params -export const getKeyForUpdateMap = (keyField) => (doc) => { - let rawKey; - if (keyField in doc) { - rawKey = doc[keyField]; - } else { - // won't have a full doc on a delete, but it's embeedded in the id - const parts = doc._id.split("-"); - rawKey = parseInt(parts[1]); - } - - const result = parseInt(rawKey); - if (isNaN(result)) { - console.warn("Non integer key for document", doc); - throw new Error("Key for update map should be an integer"); - } - - return result; -}; diff --git a/client/src/components/providers/History/ContentProvider/aggregation.test.js b/client/src/components/providers/History/ContentProvider/aggregation.test.js deleted file mode 100644 index a0788a9d7ab..00000000000 --- a/client/src/components/providers/History/ContentProvider/aggregation.test.js +++ /dev/null @@ -1,283 +0,0 @@ -import SkipList from "proper-skip-list"; -import { buildContentResult } from "./aggregation"; -import { SEEK } from "components/providers/History/caching/enums"; - -jest.mock("app"); -jest.mock("components/providers/History/caching"); - -describe("buildContentResult", () => { - describe("query history content from aggregate storage", () => { - const updateList = new SkipList(); - - // history content is keyed by hid, descending order - const keyDirection = SEEK.DESC; - const getKey = (item) => item.hid; - const pageSize = 2; - - // sample content, indexed by hid - const hids = [100, 95, 61, 60, 50, 44, 41, 17, 1].sort(); - hids.forEach((hid) => updateList.upsert(hid, { hid })); - - test("query key exists, query key in middle", () => { - const queryKey = 60; - const summarize = buildContentResult({ keyDirection, pageSize, getKey }); - - // choose a hid we know is in the results - const { contents, targetKey, startKey, startKeyIndex } = summarize([updateList, queryKey]); - - // returns the key we used to query the skiplist - expect(targetKey).toEqual(queryKey); - - // the closest key to targetKey in the event that targetKey does not exist - expect(startKey).toEqual(queryKey); - - // the array index of startKey - expect(startKeyIndex).toEqual(2); - - // pageSize causes aggregation to back up one page from start of list, then delivers - // the previous page, the exact match, then 2 pages after the match - const resultKeys = contents.map(getKey); - expect(resultKeys.length).toBeLessThanOrEqual(3 * pageSize + 1); - expect(resultKeys).toEqual([95, 61, 60, 50, 44, 41, 17]); - }); - - test("query key exists, query key at top", () => { - const queryKey = 100; - const summarize = buildContentResult({ keyDirection, pageSize, getKey }); - - // choose a hid we know is in the results - const { contents, targetKey, startKey, startKeyIndex } = summarize([updateList, queryKey]); - - expect(startKey).toEqual(queryKey); - expect(startKeyIndex).toEqual(0); - expect(targetKey).toEqual(queryKey); - - const resultKeys = contents.map(getKey); - // match (top) + 2 pages after - expect(resultKeys).toEqual([100, 95, 61, 60, 50]); - }); - - test("query key exists, query key at bottom", () => { - const queryKey = 1; - const summarize = buildContentResult({ keyDirection, pageSize, getKey }); - - // choose a hid we know is in the results - const { contents, targetKey, startKey, startKeyIndex } = summarize([updateList, queryKey]); - - expect(startKey).toEqual(queryKey); - expect(startKeyIndex).toEqual(2); - expect(targetKey).toEqual(queryKey); - - const resultKeys = contents.map(getKey); - expect(resultKeys).toEqual([41, 17, 1]); - }); - - test("query key doesn't exist, query key in middle", () => { - const queryKey = 51; - const summarize = buildContentResult({ keyDirection, pageSize, getKey }); - - // choose a hid we know is in the results - const { contents, targetKey, startKey, startKeyIndex } = summarize([updateList, queryKey]); - - expect(startKey).toEqual(50); - expect(startKeyIndex).toEqual(2); - expect(targetKey).toEqual(queryKey); - - const resultKeys = contents.map(getKey); - expect(resultKeys).toEqual([61, 60, 50, 44, 41, 17]); - }); - - test("query key doesn't exist, query key near top", () => { - const queryKey = 99; - const summarize = buildContentResult({ keyDirection, pageSize, getKey }); - - // choose a hid we know is in the results - const { contents, targetKey, startKey, startKeyIndex } = summarize([updateList, queryKey]); - - expect(startKey).toEqual(100); - expect(startKeyIndex).toEqual(0); - expect(targetKey).toEqual(queryKey); - - const resultKeys = contents.map(getKey); - expect(resultKeys).toEqual([100, 95, 61, 60, 50]); - }); - - test("query key doesn't exist, query key near bottom", () => { - const queryKey = 16; - const summarize = buildContentResult({ keyDirection, pageSize, getKey }); - - // choose a hid we know is in the results - const { contents, targetKey, startKey, startKeyIndex } = summarize([updateList, queryKey]); - - expect(startKey).toEqual(17); - expect(startKeyIndex).toEqual(1); - expect(targetKey).toEqual(queryKey); - - const resultKeys = contents.map(getKey); - expect(resultKeys).toEqual([41, 17, 1]); - }); - - test("query key beyond upper limit", () => { - const queryKey = 200; - const summarize = buildContentResult({ keyDirection, pageSize, getKey }); - - // choose a hid we know is in the results - const { contents, targetKey, startKey, startKeyIndex } = summarize([updateList, queryKey]); - - expect(startKey).toEqual(100); - expect(startKeyIndex).toEqual(0); - expect(targetKey).toEqual(queryKey); - - const resultKeys = contents.map(getKey); - expect(resultKeys).toEqual([100, 95, 61, 60]); - }); - - test("query key below lower limit", () => { - const queryKey = -300; - const summarize = buildContentResult({ keyDirection, pageSize, getKey }); - - // choose a hid we know is in the results - const { contents, targetKey, startKey, startKeyIndex } = summarize([updateList, queryKey]); - - expect(startKey).toEqual(1); - expect(startKeyIndex).toEqual(1); - expect(targetKey).toEqual(queryKey); - - const resultKeys = contents.map(getKey); - expect(resultKeys).toEqual([17, 1]); - }); - }); - - describe("query collection content from aggregate storage", () => { - const updateList = new SkipList(); - - // collection content is keyed by element_index, ascending order - const keyDirection = SEEK.ASC; - const getKey = (item) => item.element_index; - const pageSize = 2; - - // sample content, indexed by hid - const element_indexes = [1, 2, 3, 4, 5, 6, 7, 8, 9]; - element_indexes.forEach((i) => updateList.upsert(i, { element_index: i })); - - test("query key exists, query key in middle", () => { - const queryKey = 5; - const summarize = buildContentResult({ keyDirection, pageSize, getKey }); - - // choose a hid we know is in the results - const { contents, targetKey, startKey, startKeyIndex } = summarize([updateList, queryKey]); - - expect(startKey).toEqual(queryKey); - expect(startKeyIndex).toEqual(2); - expect(targetKey).toEqual(queryKey); - - const resultKeys = contents.map(getKey); - expect(resultKeys).toEqual([3, 4, 5, 6, 7, 8, 9]); - }); - - test("query key exists, query key at top", () => { - const queryKey = 1; - const summarize = buildContentResult({ keyDirection, pageSize, getKey }); - - // choose a hid we know is in the results - const { contents, targetKey, startKey, startKeyIndex } = summarize([updateList, queryKey]); - - expect(startKey).toEqual(queryKey); - expect(startKeyIndex).toEqual(0); - expect(targetKey).toEqual(queryKey); - - const resultKeys = contents.map(getKey); - expect(resultKeys).toEqual([1, 2, 3, 4, 5]); - }); - - test("query key exists, query key at bottom", () => { - const queryKey = 9; - const summarize = buildContentResult({ keyDirection, pageSize, getKey }); - - // choose a hid we know is in the results - const { contents, targetKey, startKey, startKeyIndex } = summarize([updateList, queryKey]); - - expect(startKey).toEqual(queryKey); - expect(startKeyIndex).toEqual(2); - expect(targetKey).toEqual(queryKey); - - const resultKeys = contents.map(getKey); - expect(resultKeys).toEqual([7, 8, 9]); - }); - - test("query key doesn't exist, query key in middle", () => { - const queryKey = 4.2; - const summarize = buildContentResult({ keyDirection, pageSize, getKey }); - - // choose a hid we know is in the results - const { contents, targetKey, startKey, startKeyIndex } = summarize([updateList, queryKey]); - - expect(startKey).toEqual(4); - expect(startKeyIndex).toEqual(1); - expect(targetKey).toEqual(queryKey); - - const resultKeys = contents.map(getKey); - expect(resultKeys).toEqual([3, 4, 5, 6, 7, 8]); - }); - - test("query key doesn't exist, query key near top", () => { - const queryKey = 0.8; - const summarize = buildContentResult({ keyDirection, pageSize, getKey }); - - // choose a hid we know is in the results - const { contents, targetKey, startKey, startKeyIndex } = summarize([updateList, queryKey]); - - expect(startKey).toEqual(1); - expect(startKeyIndex).toEqual(0); - expect(targetKey).toEqual(queryKey); - - const resultKeys = contents.map(getKey); - expect(resultKeys).toEqual([1, 2, 3, 4]); - }); - - test("query key doesn't exist, query key near bottom", () => { - const queryKey = 8.1; - const summarize = buildContentResult({ keyDirection, pageSize, getKey }); - - // choose a hid we know is in the results - const { contents, targetKey, startKey, startKeyIndex } = summarize([updateList, queryKey]); - - expect(startKey).toEqual(8); - expect(startKeyIndex).toEqual(1); - expect(targetKey).toEqual(queryKey); - - const resultKeys = contents.map(getKey); - expect(resultKeys).toEqual([7, 8, 9]); - }); - - test("query key beyond upper limit", () => { - const queryKey = -1999; - const summarize = buildContentResult({ keyDirection, pageSize, getKey }); - - // choose a hid we know is in the results - const { contents, targetKey, startKey, startKeyIndex } = summarize([updateList, queryKey]); - - expect(startKey).toEqual(1); - expect(startKeyIndex).toEqual(0); - expect(targetKey).toEqual(queryKey); - - const resultKeys = contents.map(getKey); - expect(resultKeys).toEqual([1, 2, 3, 4]); - }); - - test("query key below lower limit", () => { - const queryKey = 300; - const summarize = buildContentResult({ keyDirection, pageSize, getKey }); - - // choose a hid we know is in the results - const { contents, targetKey, startKey, startKeyIndex } = summarize([updateList, queryKey]); - - expect(startKey).toEqual(9); - expect(startKeyIndex).toEqual(1); - expect(targetKey).toEqual(queryKey); - - const resultKeys = contents.map(getKey); - expect(resultKeys).toEqual([8, 9]); - }); - }); -}); diff --git a/client/src/components/providers/History/ContentProvider/helpers.js b/client/src/components/providers/History/ContentProvider/helpers.js deleted file mode 100644 index 7ec77cd0259..00000000000 --- a/client/src/components/providers/History/ContentProvider/helpers.js +++ /dev/null @@ -1,27 +0,0 @@ -import { SearchParams } from "components/providers/History/SearchParams"; - -// match object props -export const propMatch = (name) => (a, b) => { - return a[name] === b[name]; -}; - -// comparator for distinct() style operators inputs are an array of [id, SearchParams] -export const inputsSame = ([a0, a1], [b0, b1]) => { - return a0 == b0 && SearchParams.equals(a1, b1); -}; - -// inputs + hid/cursor comparison -export const loadInputsSame = ([inputsA, cursorA], [inputsB, cursorB]) => { - return cursorA == cursorB && inputsSame(inputsA, inputsB); -}; - -export const paginationEqual = (a, b) => { - return a.offset == b.offset && a.limit == b.limit; -}; - -// debugging func for looking at abbreviated payload output -export const reportPayload = (p, keyField = "hid") => { - const { contents = [], ...theRest } = p; - const keys = contents.map((o) => o[keyField]); - return { keys, ...theRest }; -}; diff --git a/client/src/components/providers/History/ContentProvider/index.js b/client/src/components/providers/History/ContentProvider/index.js deleted file mode 100644 index ae450991567..00000000000 --- a/client/src/components/providers/History/ContentProvider/index.js +++ /dev/null @@ -1,18 +0,0 @@ -/** - * A component mixin and a set of observable utilities used in common by HicstoryContentProvider and - * CollectionContentProvider - */ - -// base component definition for History & Collection content providers -export { ContentProvider, defaultPayload } from "./ContentProvider"; - -// aggreagtion functions collect an initial query and merge in subsequent updates which appear int -// he cache over time to present a unified list of results ordered by a relevant key -export { buildContentResult, newUpdateMap, processContentUpdate, getKeyForUpdateMap } from "./aggregation"; -export { aggregateCacheUpdates } from "./aggregateCacheUpdates"; - -// configurable operators encapsulating shared behavior for content and collections -export { processContentStreams } from "./processContentStreams"; - -// utils -export { paginationEqual } from "./helpers"; diff --git a/client/src/components/providers/History/ContentProvider/processContentStreams.js b/client/src/components/providers/History/ContentProvider/processContentStreams.js deleted file mode 100644 index 72141b0f6cd..00000000000 --- a/client/src/components/providers/History/ContentProvider/processContentStreams.js +++ /dev/null @@ -1,70 +0,0 @@ -/** - * General plumbing for the 2 types of content providers. This function returns 3 observables: - * loading, scrolling, and payload. Payload's the most important and the operator that genrates the - * payload from the inputs is passed in as a parameter to this factory. - */ -import { distinctUntilChanged, share, startWith, switchMap } from "rxjs/operators"; -import { activity, whenAny, show } from "utils/observable"; -import { propMatch } from "./helpers"; -import { ScrollPos } from "components/History/model/ScrollPos"; -import { SearchParams } from "components/providers/History/SearchParams"; -import { defaultPayload } from "../ContentProvider"; -import { Subject } from "rxjs"; - -/** - * Takes incoming history, filters and scroll position and generates loading, scrolling and payload - * observable for rendering the content. payload$ is an observable that emits the data that should - * be displaye - * - * @param {function} payloadOperator observable operator that delivers the payload for this list - * @param {object} sources object containing params$, history$, scrollPos$ source observables - * @param {object} settings configuration parameter object - * - * @return {object} payload$, loading$, scrolling$ observables - */ -// prettier-ignore -export function processContentStreams(payloadOperator, sources = {}, settings = {}) { - const { debouncePeriod, debug = false } = settings; - - // clean incoming source streams - const parent$ = sources.parent$.pipe( - distinctUntilChanged(propMatch("id")), - show(debug, (parent) => console.log('processContentStreams: parent changed', parent)), - ); - const params$ = sources.params$.pipe( - distinctUntilChanged(SearchParams.equals), - show(debug, (params) => console.log('processContentStreams: params changed', params)), - ); - const pos$ = sources.scrollPos$.pipe( - show(debug, (pos) => console.log('processContentStreams: pos changed', pos)), - distinctUntilChanged(ScrollPos.equals), - ); - const scrolling$ = sources.scrollPos$.pipe( - activity(200) - ); - - const loadingEvents$ = new Subject(); - const loading$ = loadingEvents$.pipe( - activity(debouncePeriod), - startWith(true) - ); - - const resetPos$ = new Subject(); - - // The actual loader, when history or params change we poll against the api and - // set up a cachewatcher, both of which are controlled by the scroll position - const loaderReset$ = whenAny(parent$, params$); - const payload$ = loaderReset$.pipe( - switchMap(([parent, filters]) => pos$.pipe( - show(debug, pos => { - console.clear(); - console.log("processContentStreams: pos", JSON.stringify(pos)); - }), - payloadOperator({ parent, filters, loadingEvents$, resetPos$, ...settings }), - startWith({ ...defaultPayload, placeholder: true }), - )), - share() - ); - - return { payload$, scrolling$, loading$, resetPos$ }; -} diff --git a/client/src/components/providers/History/DscProvider/DscProvider.js b/client/src/components/providers/History/DscProvider/DscProvider.js deleted file mode 100644 index 855cd2a770c..00000000000 --- a/client/src/components/providers/History/DscProvider/DscProvider.js +++ /dev/null @@ -1,95 +0,0 @@ -/** - * Given a collection, tracks cache changes to the object. - * - * Dataset collections are stored in two places depending on how deeply nested - * it is in the tree. At the top level are root dataset collections which are - * elements of a history's contents. These source their data from the content - * cache. Then inside a dataset collection, it's possible to nest other - * collections arbitrarily, these collections come from the collections cache - * and the api does not necessarily deliver the same fields for both scenarios, - * but we run both raw objects into a DatasetCollection model class. - */ - -import { of } from "rxjs"; -import { map, pluck, switchMap, distinctUntilChanged } from "rxjs/operators"; -import { whenAny } from "utils/observable/whenAny"; -import { monitorContentQuery, monitorDscQuery } from "components/providers/History/caching"; -import { DatasetCollection } from "components/History/model/DatasetCollection"; -import deepEqual from "deep-equal"; - -// passed object should have a pouch _id for monitoring -const hasPouchId = (o) => "_id" in o; - -export default { - props: { - collection: { type: Object, required: true, validator: hasPouchId }, - isRoot: { type: Boolean, required: true }, - }, - data() { - return { - raw: this.collection, - }; - }, - computed: { - dsc() { - const cleanObj = JSON.parse(JSON.stringify(this.raw)); - return new DatasetCollection(cleanObj); - }, - }, - watch: { - collection(newVal, oldVal) { - if (newVal && !deepEqual(newVal, oldVal)) { - this.raw = newVal; - } - }, - dsc(newVal, oldVal) { - if (newVal && !deepEqual(newVal, oldVal)) { - this.$emit("update:collection", newVal); - } - }, - }, - // prettier-ignore - created() { - const root$ = this.watch$("isRoot", true).pipe( - distinctUntilChanged() - ); - - const id$ = this.watch$("collection", true).pipe( - pluck("_id"), - distinctUntilChanged() - ); - - const monitor$ = whenAny(id$, root$).pipe( - switchMap((inputs) => { - const [_id, isRoot] = inputs; - const monitorOperator = isRoot ? monitorContentQuery : monitorDscQuery; - const selector = { _id }; - const request = { selector, offset: 0, limit: 1 }; - - return of(request).pipe( - monitorOperator(), - map((update) => { - const { initialMatches = [], doc = null, deleted } = update; - if (deleted) { - return null; - } - let updatedDoc = doc; - if (initialMatches.length == 1) { - updatedDoc = initialMatches[0]; - } - return updatedDoc; - }), - ); - }) - ); - - this.$subscribeTo(monitor$, (doc) => { - this.raw = doc; - }); - }, - render() { - return this.$scopedSlots.default({ - dsc: this.dsc, - }); - }, -}; diff --git a/client/src/components/providers/History/DscProvider/DscProvider.test.js b/client/src/components/providers/History/DscProvider/DscProvider.test.js deleted file mode 100644 index 37173bcb43c..00000000000 --- a/client/src/components/providers/History/DscProvider/DscProvider.test.js +++ /dev/null @@ -1,131 +0,0 @@ -/* eslint-disable no-unused-vars */ -import { mountRenderless, watchForChange } from "jest/helpers"; -import { wipeDatabase } from "components/providers/History/caching"; -import { cacheContent, cacheCollectionContent } from "components/providers/History/caching"; -import { DatasetCollection } from "components/History/model/DatasetCollection"; -import DscProvider from "./DscProvider"; - -import { testCollection, testNestedCollection } from "components/providers/History/test/testCollection"; - -// Imports dependencies without firing up worker because that blows with Jest -jest.mock("app"); -jest.mock("components/providers/History/caching"); - -beforeEach(wipeDatabase); -afterEach(wipeDatabase); - -describe("DscProvider", () => { - let wrapper; - - afterEach(async () => { - if (wrapper) { - wrapper.destroy(); - } - wrapper = undefined; - }); - - describe("monitors a root collection (direct child of a history)", () => { - let rootCollection; - - beforeEach(async () => { - rootCollection = await cacheContent(testCollection, true); - expect(rootCollection).toBeDefined(); - - wrapper = await mountRenderless(DscProvider, { - propsData: { - collection: rootCollection, - isRoot: true, - }, - }); - expect(wrapper).toBeDefined(); - - const { newVal: dsc } = await watchForChange({ vm: wrapper.vm, propName: "dsc" }); - expect(dsc._id).toEqual(rootCollection._id); - }); - - afterEach(() => { - rootCollection = undefined; - }); - - test("should successfully mount and emit the pre-cached data", () => { - // compare output to input prop, should be the same because we didn't change anything - const root = new DatasetCollection(rootCollection); - const emitted = new DatasetCollection(wrapper.vm.dsc); - const { _rev, cached_at, ...originalProps } = root; - const { _rev: new_rev, cached_at: new_cached_at, ...newProps } = emitted; - expect(originalProps).toEqual(newProps); - }); - - test("should emit updates as they occur", async () => { - const { cached_at: old_cached_at, _rev: old_rev, ...oldProps } = wrapper.vm.dsc; - - // write a new field to the collection - const fooVal = "abc123"; - rootCollection.foo = fooVal; - const updatedRoot = await cacheContent(rootCollection, true); - expect(updatedRoot.foo).toEqual(fooVal); - - // wait for next update then check the foo val - await wrapper.vm.$nextTick(); - - // all same props except _rev and cached_at and foo - const { cached_at, _rev, foo, ...newProps } = wrapper.vm.dsc; - expect(foo).toEqual(fooVal); - expect(old_cached_at).toBeLessThan(cached_at); - expect(old_rev).not.toEqual(_rev); - expect(newProps).toEqual(oldProps); - }); - }); - - describe("monitors nested collection", () => { - let nestedCollection; - const parent_url = "/foo"; - - beforeEach(async () => { - // cache a collection root - nestedCollection = await cacheCollectionContent(testNestedCollection, true); - expect(nestedCollection).toBeDefined(); - - // mount and monitor - wrapper = await mountRenderless(DscProvider, { - propsData: { - collection: nestedCollection, - isRoot: false, - }, - }); - expect(wrapper).toBeDefined(); - - const { newVal: dsc } = await watchForChange({ vm: wrapper.vm, propName: "dsc" }); - expect(dsc._id).toEqual(nestedCollection._id); - }); - - afterEach(() => { - nestedCollection = undefined; - }); - - test("should successfully mount and emit the pre-cached data", () => { - const orig = new DatasetCollection(nestedCollection); - const emitted = new DatasetCollection(wrapper.vm.dsc); - const { _rev, cached_at, ...originalProps } = orig; - const { _rev: new_rev, cached_at: new_cached_at, ...newProps } = emitted; - expect(originalProps).toEqual(newProps); - }); - - test("should emit updates as they occur", async () => { - const { cached_at: old_cached_at, _rev: old_rev, ...oldProps } = wrapper.vm.dsc; - - // write a new field to the collection - const fooVal = "abc123"; - nestedCollection.foo = fooVal; - const updateResult = await cacheCollectionContent(nestedCollection, true); - expect(updateResult.foo).toEqual(fooVal); - - // wait for next update then check the foo val - // await wait(500); - await wrapper.vm.$nextTick(); - - // all same props except _rev and cached_at and foo - expect(wrapper.vm.dsc.foo).toEqual(fooVal); - }); - }); -}); diff --git a/client/src/components/providers/History/DscProvider/index.js b/client/src/components/providers/History/DscProvider/index.js deleted file mode 100644 index 57b55f84fcf..00000000000 --- a/client/src/components/providers/History/DscProvider/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import DscProvider from "./DscProvider"; - -export default DscProvider; diff --git a/client/src/components/providers/History/HistoryContentProvider/HistoryContentProvider.js b/client/src/components/providers/History/HistoryContentProvider/HistoryContentProvider.js deleted file mode 100644 index b6d04598c22..00000000000 --- a/client/src/components/providers/History/HistoryContentProvider/HistoryContentProvider.js +++ /dev/null @@ -1,34 +0,0 @@ -import { ContentProvider, processContentStreams } from "../ContentProvider"; -import { contentPayload } from "./contentPayload"; - -export default { - mixins: [ContentProvider], - - props: { - disablePoll: { type: Boolean, default: false }, - }, - - computed: { - history() { - return this.parent; - }, - }, - - watch: { - history(newHistory, oldHistory) { - if (!(newHistory.id == oldHistory.id)) { - this.resetScrollPos(); - } - }, - }, - - methods: { - initStreams() { - const { disablePoll, debouncePeriod, pageSize, params$, scrollPos$, debug } = this; - const parent$ = this.watch$("history"); - const sources = { params$, parent$, scrollPos$ }; - const settings = { disablePoll, debouncePeriod, pageSize, debug }; - return processContentStreams(contentPayload, sources, settings); - }, - }, -}; diff --git a/client/src/components/providers/History/HistoryContentProvider/HistoryContentProvider.test.js b/client/src/components/providers/History/HistoryContentProvider/HistoryContentProvider.test.js deleted file mode 100644 index be79728f2d8..00000000000 --- a/client/src/components/providers/History/HistoryContentProvider/HistoryContentProvider.test.js +++ /dev/null @@ -1,161 +0,0 @@ -import { shallowMount } from "@vue/test-utils"; -import { watchUntil, getLocalVue } from "jest/helpers"; -import { sameVueObj, payloadChange } from "components/providers/History/test/providerTestHelpers"; -import HistoryContentProvider from "./HistoryContentProvider"; -import { SearchParams } from "components/providers/History/SearchParams"; -import { bulkCacheContent, wipeDatabase } from "components/providers/History/caching"; -import { defaultPayload } from "../ContentProvider"; -import { serverContent, testHistory, testHistoryContent } from "components/providers/History/test/testHistory"; - -// reads hids -const payloadHids = (payload) => payload.contents?.map((o) => o.hid) || []; -const localVue = getLocalVue(); - -// mocking -jest.mock("app"); -jest.mock("components/providers/History/caching"); -jest.mock("./loadContents"); - -afterEach(() => wipeDatabase()); - -describe("HistoryContentProvider", () => { - let wrapper; - - beforeEach(async () => {}); - - afterEach(async () => { - if (wrapper) { - await wrapper.destroy(); - } - }); - - describe("blank initialization state", () => { - beforeEach(async () => { - wrapper = await shallowMount(HistoryContentProvider, { - localVue, - propsData: { - parent: testHistory, - pageSize: 10, - debug: false, - debouncePeriod: 200, - }, - scopedSlots: { - default() {}, - }, - }); - // wait for default emit - await payloadChange({ vm: wrapper.vm }); - }); - - test("should expose update methods", async () => { - expect(wrapper.vm.setScrollPos).toBeInstanceOf(Function); - }); - - test("should show an empty payload", () => { - expect(SearchParams.equals(new SearchParams(), wrapper.vm.params)).toBe(true); - expect(sameVueObj(wrapper.vm.payload, defaultPayload)); - }); - }); - - describe("with pre-inserted content", () => { - beforeEach(async () => { - // eslint-disable-next-line no-unused-vars - const cachedContent = await bulkCacheContent(testHistoryContent, true); - - wrapper = await shallowMount(HistoryContentProvider, { - localVue, - propsData: { - parent: testHistory, - pageSize: 10, - debug: false, - debouncePeriod: 200, - }, - scopedSlots: { - default() {}, - }, - }); - - // eslint-disable-next-line no-unused-vars - const spinUp = await watchUntil(wrapper.vm, function () { - const allDone = wrapper.vm.payload.contents.length > 0; - // console.log("done?", allDone); - return allDone; - }); - }); - - // If the cache adds some content, should still be at the top, but the - // bottomRows should change to reflect the difference between the - // returned content rows and the total matches - - test("initial load, no scrolling", async () => { - const payload = wrapper.vm.payload; - // reportPayload(payload); - - // test data on server - const allContent = serverContent(); - // maximum hid in the fake server content - const maxServerHid = allContent[0].hid; - expect(payload).toBeDefined(); - - const { contents, topRows, bottomRows, startKey, totalMatches } = payload; - expect(topRows).toEqual(0); - expect(contents.length).toBeGreaterThanOrEqual(2 * wrapper.vm.pageSize); - expect(bottomRows).toBeGreaterThan(0); - expect(bottomRows).toEqual(allContent.length - contents.length); - expect(startKey).toEqual(maxServerHid); - expect(totalMatches).toEqual(allContent.length); - }); - - // When the scroller moves, we may end up over a known content row, in - // which case the scroller handler will get an exact HID match, if so, - // we should have new content values from the hidMap and some topRows - // bottomRows - - test("scroll to known key", async () => { - const payload = wrapper.vm.payload; - - // shift the scroller down to half way through that content list - // make sure it's visible and not hidden - const targetElement = testHistoryContent[Math.floor(testHistoryContent.length / 2)]; - expect(targetElement.visible).toBe(true); - expect(targetElement.deleted).toBe(false); - - const targetKey = targetElement.hid; - expect(targetKey).toBeDefined(); - expect(targetKey).toBeGreaterThan(0); - - // console.log("setting scroll pos to targetKey", targetKey); - wrapper.vm.setScrollPos({ key: targetKey }); - - // need to wait for it to settle down - await await payloadChange({ vm: wrapper.vm }); - - // check to see if what we want is there - const scrollPayload = wrapper.vm.payload; - expect(payload).toBeDefined(); - const { startKey } = scrollPayload; - const hids = payloadHids(scrollPayload); - // console.log("hids", hids); - - expect(hids).toContain(targetKey); - expect(startKey).toEqual(targetKey); - }); - - // This simulates dragging the scrollbar to a place in the history where - // there is no exact match of HID. The provider should still render - // contents around the targetHID, if they exist - - test("should show results when we pick a HID by scroller height", async () => { - // set scroller to non-existent hid - wrapper.vm.setScrollPos({ cursor: 0.52323 }); - const scrollPayload = await payloadChange({ vm: wrapper.vm }); - const { contents, startKey, topRows, bottomRows, totalMatches } = scrollPayload; - const hids = payloadHids(scrollPayload); - - expect(hids).toContain(startKey); - expect(startKey).toBeGreaterThan(0); - expect(topRows).toBeGreaterThan(0); - expect(bottomRows).toEqual(totalMatches - contents.length - topRows); - }); - }); -}); diff --git a/client/src/components/providers/History/HistoryContentProvider/__mocks__/loadContents.js b/client/src/components/providers/History/HistoryContentProvider/__mocks__/loadContents.js deleted file mode 100644 index 3965b9f2d11..00000000000 --- a/client/src/components/providers/History/HistoryContentProvider/__mocks__/loadContents.js +++ /dev/null @@ -1,34 +0,0 @@ -// common mock implementation -// having a hard time making jest manually mock this properly - -import { map } from "rxjs/operators"; -import { getPropRange } from "components/providers/History/caching/loadHistoryContents"; -import { serverContent } from "components/providers/History/test/testHistory"; - -export const loadContents = jest.fn().mockImplementation((config) => { - const { filters } = config; - - return map((pagination) => { - const { offset, limit } = pagination; - - // server side result set - const searchResults = serverContent(filters); - const { min: minHid, max: maxHid } = getPropRange(searchResults, "hid"); - - // window slice returned - const result = searchResults.slice(offset, offset + limit); - const { min: minContentHid, max: maxContentHid } = getPropRange(result, "hid"); - - return { - summary: {}, - matches: result.length, - totalMatches: searchResults.length, - minHid, - maxHid, - minContentHid, - maxContentHid, - limit, - offset, - }; - }); -}); diff --git a/client/src/components/providers/History/HistoryContentProvider/contentPayload.js b/client/src/components/providers/History/HistoryContentProvider/contentPayload.js deleted file mode 100644 index e619011be88..00000000000 --- a/client/src/components/providers/History/HistoryContentProvider/contentPayload.js +++ /dev/null @@ -1,351 +0,0 @@ -import { combineLatest, concat, race, timer, merge, partition, Observable, BehaviorSubject, Subject } from "rxjs"; -import { - debounceTime, - distinctUntilChanged, - filter, - map, - mapTo, - mergeAll, - pairwise, - pluck, - publish, - share, - shareReplay, - take, - tap, - window, - withLatestFrom, -} from "rxjs/operators"; -import { chunk, show } from "utils/observable"; -import { isValidNumber } from "utils/validation"; -import { CurveFit } from "components/History/model/CurveFit"; -import { ScrollPos } from "components/History/model/ScrollPos"; -import { SearchParams } from "components/providers/History/SearchParams"; -import { loadContents } from "./loadContents"; -import { watchHistoryContents } from "./watchHistoryContents"; -import { default as store } from "store/index"; -import { defaultPayload } from "../ContentProvider"; - -/** - * Observable operator accepts history, search filters as config values, then uses an observable - * scroll position to periodically poll the server for new data and watch the cache for - * corresponding updates for a window of content defined by the cfg props. - * - * @param {object} cfg history, filters, other operation parameters - * @return {Observable} Observable that emits payloads suitable for use in the scroller - */ -// prettier-ignore -export const contentPayload = (cfg = {}) => { - const { - parent: history, - filters = new SearchParams(), - pageSize = SearchParams.pageSize, - debouncePeriod = 250, - loadingTimeout = 2000, - disablePoll = false, - debug = false, - // optional feedback indicators - loadingEvents$ = new Subject(), - resetPos$ = new Subject(), - chunkSize = pageSize - } = cfg; - - // These running totals are shared between instances of content payload because a lot of the - // stats do not get returned on every single pol. That means the most recent values need to be - // preserved when the user switches the filters back and forth - const totalMatches$ = getSubject("totalMatches", history, filters, () => new BehaviorSubject(history.hidItems)); - const cursorToHid$ = getSubject("cursorToHid", history, filters, () => new BehaviorSubject(createCursorToHid(history))); - const hidToTopRows$ = getSubject("hidToTopRows", history, filters, () => new BehaviorSubject(createHidToTopRows(history))); - - return publish((pos$) => { - - // #region establish HID by either having exact value or estimating from scroll position - - const [ noKey$, hasKey$ ] = partition(pos$, (pos) => pos.key === null); - - const knownHid$ = hasKey$.pipe( - pluck('key') - ); - - const cursor$ = noKey$.pipe( - pluck('cursor'), - ); - - // only have cursor height, need to make a guess - const estimatedHid$ = combineLatest(cursor$, cursorToHid$).pipe( - map((inputs) => estimateHid(...inputs)), - map(hid => Math.round(hid)), - distinctUntilChanged(), - show(debug, hid => console.log("estimatedHid", hid, history)), - ); - - const hid$ = merge(knownHid$, estimatedHid$).pipe( - distinctUntilChanged(), - filter(hid => !isNaN(hid)), - share(), - ); - - // #endregion - - // #region server loading - - const serverHid$ = hid$.pipe( - chunk({ chunkSize, debug, label: "serverHid" }), - distinctUntilChanged(), - debounceTime(debouncePeriod), - show(debug, hid => console.log("serverHid", hid)), - share(), - ); - - const serverLoad$ = serverHid$.pipe( - tap(() => loadingEvents$.next(true)), - loadContents({ history, filters, disablePoll, debug }), - tap(() => loadingEvents$.next(false)), - tap((response) => updateVuexHistory(history.id, response)), - share(), - ); - - //#endregion - - // #region Cache monitor - - // after a fresh server request has been made, wait for first response before allowing - // an input to the cache, avoids bounciness - const cacheHid$ = hid$.pipe( - window(serverHid$), - mergeAll(), - debounceTime(100), - ); - - const cacheMonitor$ = cacheHid$.pipe( - watchHistoryContents({ history, filters, pageSize, debouncePeriod, debug }), - show(debug, (result) => console.log("cacheMonitor.contents (hid)", result.contents.map(o => o.hid))), - shareReplay(1) - ); - - const cachePayload$ = cacheMonitor$.pipe( - withLatestFrom(totalMatches$, hidToTopRows$), - map(buildPayload(pageSize)), - show(debug, (payload) => console.log("payload", payload)), - ); - - // An empty cache response, wait a while then emit an empty payload - const noResults$ = timer(loadingTimeout).pipe( - mapTo({ ...defaultPayload, noResults: true }), - take(1) - ); - const noInitialResults$ = concat(noResults$, cachePayload$); - const payload$ = race(noInitialResults$, cachePayload$); - - // #endregion - - // #region Reposition View to accomodate recent updates - - // When new updates to the history come in, we should shift the scrollPos so that we can see - // them. Without some kind of explicit move, the system will think they're just random - // updates that are happening outside the region of interest, and there would be no - // particular reason to shift the view. But that's not ideal when you're already looking at - // the top of the history and expect to see the most recent updates as they come in. - - const adjustedScrollPos$ = serverLoad$.pipe( - pairwise(), - filter(([a,b]) => !isNaN(a.maxHid) && !isNaN(b.maxHid)), - withLatestFrom(pos$, hid$), - map(([[lastResponse, response], pos, hid]) => { - const updatesAtTop = response.maxHid >= lastResponse.maxHid; - - const scrollerExactlyAtTop = pos.cursor === 0 || pos.key === lastResponse.maxHid; - const fudge = 2; - const scrollNearLastTop = pos.key !== null ? Math.abs(pos.key - lastResponse.maxContentHid) < fudge : false; - const scrollerAtTop = scrollerExactlyAtTop || scrollNearLastTop; - - if (updatesAtTop && scrollerAtTop) { - return ScrollPos.create({ cursor: 0, key: response.maxContentHid }); - } - return null; - }), - filter(Boolean), - ); - - // #endregion - - // #region serverLoad side-effects, keeps track of important stats via feedback loops - - const newCursorToHid$ = serverLoad$.pipe( - withLatestFrom(cursorToHid$), - map(([result, fit]) => adjustCursorToHid(result, fit)) - ); - - const newHidToTopRows$ = serverLoad$.pipe( - withLatestFrom(hidToTopRows$), - map(([result, fit]) => adjustHidToTopRows(result, fit)) - ); - - const newMatches$ = serverLoad$.pipe( - pluck('totalMatches'), - map(val => parseInt(val)), - ); - - //#endregion - - return new Observable((obs) => { - const sub = payload$.subscribe(obs); - - // Tie stat feedback subscriptions to main payload sub - // Make sure not to combineLatest these subject accumulators or we'll - // end up with an infinite loop - sub.add(newCursorToHid$.subscribe(cursorToHid$)); - sub.add(newHidToTopRows$.subscribe(hidToTopRows$)); - sub.add(newMatches$.subscribe(totalMatches$)); - sub.add(adjustedScrollPos$.subscribe(resetPos$)); - - return sub; - }) - }) -}; - -const estimateHid = (cursor, fit) => { - // do an estimate/retrieval - const result = fit.get(cursor, { interpolate: true }); - if (isValidNumber(result)) { - return result; - } - - // give them the top of the data - if (fit.hasData) { - console.log("invalid curve fit", result, cursor, fit.domain); - return fit.get(0); - } - - // we're screwed - return undefined; -}; - -const adjustCursorToHid = (result, fit) => { - const { maxHid, minHid, maxContentHid, minContentHid, matches, totalMatchesUp, totalMatches } = result; - - // set 0, 1 for min/max hids - fit.set(0, maxHid); - fit.set(1, minHid); - - // find cursor for top & bottom of returned content - if (totalMatches > 0) { - fit.set(totalMatchesUp / totalMatches, maxContentHid); - fit.set((matches + totalMatchesUp) / totalMatches, minContentHid); - } - - return fit; -}; - -const adjustHidToTopRows = (result, fit) => { - const { - // min and max of returned data - minContentHid, - maxContentHid, - - // endpoints for all of history, that match filters - minHid, - maxHid, - - // num matches up/down from request hid - matchesUp, - matchesDown, - matches, - - // total matches up/down from request hid - totalMatchesUp, - totalMatches, - } = result; - - if (matches > 0) { - fit.set(maxContentHid, totalMatchesUp - matchesUp); - fit.set(minContentHid, Math.max(0, totalMatchesUp + matchesDown - 1)); - } - - fit.set(maxHid, 0); - if (totalMatches > 0) { - fit.set(minHid, totalMatches - 1); - } - - fit.domain = [minHid, maxHid]; - - return fit; -}; - -const buildPayload = (pageSize) => (inputs) => { - // cacheWatchResult is the value from the cache watcher - // hidToTopRows is a dataset for estimating toprows from HID - // since we can't possibly have that information at the time - // of the cache-read - const [cacheWatchResult, totalMatches, hidToTopRows] = inputs; - - // contents is the list from the aggregator - // targetKey was the request value for the aggregation - const { contents = [] } = cacheWatchResult; - - let topRows = 0; - let bottomRows = Math.max(0, totalMatches - contents.length - 1); - - // missing rows above & below content - if (hidToTopRows.size && contents.length && totalMatches > pageSize) { - const maxHid = contents[0].hid; - const rowsAboveTop = hidToTopRows.get(maxHid, { interpolate: true }); - topRows = Math.max(0, Math.round(rowsAboveTop)); - bottomRows = Math.max(0, totalMatches - topRows - contents.length); - } - - return { topRows, bottomRows, totalMatches, ...cacheWatchResult }; -}; - -const createHidToTopRows = (history) => { - const fit = new CurveFit(); - fit.domain = [0.0, +history.hidItems]; - fit.xPrecision = 3; - fit.yPrecision = 3; - fit.set(history.hidItems, 0.0); - fit.set(0.0, history.hidItems); - return fit; -}; - -const createCursorToHid = (history) => { - const fit = new CurveFit(); - fit.domain = [0.0, 1.0]; - fit.xPrecision = 3; - fit.yPrecision = 2; - fit.set(0.0, history.hidItems); - fit.set(1.0, 1); - return fit; -}; - -// Need to stash these subjects in memory since we don't get the counts -// back on every single poll request - -const subjects = new Map(); - -const getSubject = (label, history, filters, subjectFactory) => { - const key = makeSubjectKey(label, history, filters); - if (!subjects.has(key)) { - subjects.set(key, subjectFactory()); - } - return subjects.get(key); -}; - -const makeSubjectKey = (label, history, filters) => { - return JSON.stringify({ label, historyId: history.id, filters: filters.export() }); -}; - -/** - * Updates the vuex history when a polling result comes back. - * TODO: consider storing histories in cache instead of Vuex, since some users have very large lists - * of histories. - * - * @param {string} historyId History id from poll result - * @param {object} pollResponse Poll summary response - */ -const updateVuexHistory = (historyId, pollResponse) => { - const getter = store.getters["betaHistory/getHistoryById"]; - const existingHistory = getter(historyId); - const { historySize: size, historyEmpty: empty } = pollResponse; - const history = { ...existingHistory, size, empty }; - store.dispatch("betaHistory/setHistory", history); -}; diff --git a/client/src/components/providers/History/HistoryContentProvider/contentPayload.test.js b/client/src/components/providers/History/HistoryContentProvider/contentPayload.test.js deleted file mode 100644 index ef3ea321186..00000000000 --- a/client/src/components/providers/History/HistoryContentProvider/contentPayload.test.js +++ /dev/null @@ -1,154 +0,0 @@ -import { BehaviorSubject, timer } from "rxjs"; -import { takeUntil, share } from "rxjs/operators"; -import { ObserverSpy } from "@hirez_io/observer-spy"; -import { ScrollPos } from "components/History/model/ScrollPos"; -import { bulkCacheContent, wipeDatabase } from "components/providers/History/caching"; -import { contentPayload } from "./contentPayload"; -import { loadContents } from "./loadContents"; -import { serverContent, testHistory, testHistoryContent } from "components/providers/History/test/testHistory"; -import { untilNthEmission } from "jest/helpers"; - -jest.mock("app"); -jest.mock("components/providers/History/caching"); -jest.mock("./loadContents"); - -// Create a history and a set of filters then wire up a scrollPos to -// the contentPayload operator, check the payloads that come out - -describe("contentPayload operator", () => { - let sub; - const safetyTimeout = 2000; - const disablePoll = true; - const debouncePeriod = 250; - - beforeEach(async () => { - await wipeDatabase(); - await bulkCacheContent(testHistoryContent, true); - }); - - afterEach(async () => { - if (sub) { - sub.unsubscribe(); - } - await wipeDatabase(); - }); - - // The first emission any time we subscribe to the payload should - // be the top of the history, this is because we do not yet have any - // way to gauge how big the history is, so our first emission is always - // from the top of the list, which is where we reset the scroller when - // the inputs change anyway - - test("single emission, no scrolling, top of list", async () => { - const spy = new ObserverSpy(); - const start = new ScrollPos(); - const input$ = new BehaviorSubject(start); - const pageSize = 10; - - // prettier-ignore - sub = input$.pipe( - contentPayload({ - parent: testHistory, - pageSize, - disablePoll, - debouncePeriod - }), - takeUntil(timer(safetyTimeout)) - ).subscribe(spy); - - await spy.onComplete(); - - // should finish - expect(spy.receivedComplete()).toBe(true); - - // should call the loader once - expect(loadContents.mock.calls.length).toEqual(1); - - // should only emit one result - expect(spy.receivedNext()).toBe(true); - expect(spy.getValues().length).toEqual(1); - - // expect a payload with results from the top of the list (cursor = 0) - const { contents } = spy.getFirstValue(); - - expect(contents).toBeDefined(); - expect(contents).toBeInstanceOf(Array); - expect(contents.length).toBeGreaterThanOrEqual(2 * pageSize); - - const payloadHids = contents.map((c) => c.hid); - expect(payloadHids[0]).toEqual(testHistoryContent[0].hid); - }); - - // The first emission must be from the top of the list, but then - // the user moves the scroller down half-way. Should get results - // from the middle of the sample contents - - test("move half way down the list", async () => { - const pageSize = 5; - - // scroll position (cursor/key) - const pos$ = new BehaviorSubject(ScrollPos.create()); - - // subscribe to payload emitter - // first emission needs to be at top of list - const payload$ = pos$.pipe( - contentPayload({ - parent: testHistory, - pageSize, - disablePoll, - }), - takeUntil(timer(safetyTimeout)), - share() - ); - - const spy = new ObserverSpy(); - sub = payload$.subscribe(spy); - - // wait until first emission, then move scroller - await untilNthEmission(payload$, 1); - pos$.next(ScrollPos.create({ cursor: 0.5 })); - - await spy.onComplete(); - expect(spy.receivedComplete()).toBe(true); - expect(spy.receivedNext()).toBe(true); - // spy.getValues().forEach(reportPayload); - - // should call the loader once - expect(loadContents.mock.calls.length).toBeGreaterThan(0); - - const serverResults = serverContent(); - const serverResultHids = serverResults.map((o) => o.hid); - - const initialPayload = spy.getFirstValue(); - { - const { contents, startKey, startKeyIndex, topRows, bottomRows, totalMatches } = initialPayload; - const listLength = 2 * pageSize; - const payloadHids = contents.map((o) => o.hid); - const historyHids = serverResultHids.slice(startKeyIndex, contents.length); - - expect(contents).toBeDefined(); - expect(contents).toBeInstanceOf(Array); - expect(contents.length).toBeGreaterThanOrEqual(listLength); - expect(startKey).toEqual(serverResultHids[0]); - expect(startKeyIndex).toEqual(0); - expect(topRows).toEqual(0); - expect(totalMatches).toEqual(serverResults.length); - expect(payloadHids).toEqual(historyHids); - expect(topRows + bottomRows + contents.length).toEqual(totalMatches); - } - - const secondPayload = spy.getLastValue(); - { - const { contents, startKey, startKeyIndex, topRows, bottomRows, totalMatches } = secondPayload; - const payloadHids = contents.map((o) => o.hid); - - expect(contents).toBeDefined(); - expect(contents).toBeInstanceOf(Array); - expect(topRows).toBeGreaterThan(0); - expect(totalMatches).toEqual(serverResults.length); - expect(startKeyIndex).toBeGreaterThan(0); - expect(payloadHids).toContain(startKey); - expect(topRows + bottomRows + contents.length).toEqual(totalMatches); - } - }); -}); diff --git a/client/src/components/providers/History/HistoryContentProvider/index.js b/client/src/components/providers/History/HistoryContentProvider/index.js deleted file mode 100644 index 244046fb02a..00000000000 --- a/client/src/components/providers/History/HistoryContentProvider/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import HistoryContentProvider from "./HistoryContentProvider"; - -export default HistoryContentProvider; diff --git a/client/src/components/providers/History/HistoryContentProvider/loadContents.js b/client/src/components/providers/History/HistoryContentProvider/loadContents.js deleted file mode 100644 index f42ffd81f46..00000000000 --- a/client/src/components/providers/History/HistoryContentProvider/loadContents.js +++ /dev/null @@ -1,57 +0,0 @@ -import { defer, of, throwError } from "rxjs"; -import { switchMap, debounceTime, startWith, repeatWhen } from "rxjs/operators"; -import { decay } from "utils/observable"; -import { monitorXHR } from "utils/observable/monitorXHR"; -import { loadHistoryContents } from "components/providers/History/caching"; -import { SearchParams } from "components/providers/History/SearchParams"; - -// prettier-ignore -export const loadContents = (cfg = {}) => { - const { - history, - filters, - initialInterval = 2 * 1000, - maxInterval = 10 * initialInterval, - disablePoll = false, - debug = false, - windowSize = 2 * SearchParams.pageSize, - } = cfg; - - if (history === undefined) { - return throwError(new Error("Missing history in loadContents")); - } - if (filters === undefined) { - return throwError(new Error("Missing filters in loadContents")); - } - - const { id } = history; - - const singleLoad = (hid) => defer(() => of([id, filters, hid]).pipe( - loadHistoryContents({ windowSize }), - )); - - const poll = (request$) => resetPoll(100).pipe( - switchMap(() => request$.pipe( - repeatWhen(completed$ => completed$.pipe( - decay({ initialInterval, maxInterval, debug }), - )) - )) - ); - - return switchMap((hid) => { - const request$ = singleLoad(hid); - return disablePoll ? request$ : poll(request$); - }) -}; - -// history, tools routes all refresh -// prettier-ignore -const resetPoll = (debouncePeriod) => { - const routes = [/api\/(history|tools|histories)/]; - const methods = ["POST", "PUT", "DELETE"]; - - return monitorXHR({ methods, routes }).pipe( - startWith(true), - debounceTime(debouncePeriod) - ); -}; diff --git a/client/src/components/providers/History/HistoryContentProvider/readme.md b/client/src/components/providers/History/HistoryContentProvider/readme.md deleted file mode 100644 index 71b633e8127..00000000000 --- a/client/src/components/providers/History/HistoryContentProvider/readme.md +++ /dev/null @@ -1,73 +0,0 @@ -The history content provider is where the grunt-work of loading, polling and caching -happens. The output of the provider is a list of current data suitable for rendering in the companion Scroller component. - -### Design - -The API of the provider is a renderless Vue component. Its inputs are the history and the -currently selected filters. Internally, it tracks the user's current scroll position on the scrolling list then combines those 3 values to ultimately expose an object containing all the -currently-visible content for the history along with some other values that are useful -for the scroller to render the visible window of data. - -#### Composition -```html static - - - - - - - - - - - -``` - -#### History Content Provider exposed slot props (outputs) -```js -{ - // a function to update the internal scroll position - setScrollPos: fn, - - // current content window - payload: { - contents: [] - topRows: 10 - botttomRows: 100, - startKey: "123", - }, - - // flags - loading: true, - scrolling: false, -} -``` - -### Implementation - -Because actually delivering that data is kind of a complex process (see dependency graph below) and has multiple goals, Vue's rather -simplistic observability system is not by itself be an ideal means to actually deliver the -constantly shifting payload value. Instead, we used RxJS and its more sophisticated, but admittedly -more complex API. - -The goal of this section is to explain how the RxJS works inside the history content provider to -deliver the payload from the 3 changing inputs. - -I believe it's easiest to follow RxJS streams by simply drawing a map of them. An rxjs operator is a -functional transformation of a source observable emitter into a new observable emitter and therefore -a combined composition of them (often called a stream) is essentially its own dependency tree. All -you have to do is connect the inputs to the outputs. - -### RxJS Flowchart - diff --git a/client/src/components/providers/History/HistoryContentProvider/watchHistoryContents.js b/client/src/components/providers/History/HistoryContentProvider/watchHistoryContents.js deleted file mode 100644 index da47d46b08f..00000000000 --- a/client/src/components/providers/History/HistoryContentProvider/watchHistoryContents.js +++ /dev/null @@ -1,37 +0,0 @@ -import { of } from "rxjs"; -import { map } from "rxjs/operators"; -import { aggregateCacheUpdates } from "../ContentProvider"; -import { monitorHistoryContent } from "components/providers/History/caching"; -import { SEEK } from "components/providers/History/caching/enums"; -import { SearchParams } from "components/providers/History/SearchParams"; - -// prettier-ignore -export const watchHistoryContents = (cfg = {}) => hid$ => { - const { - history, - filters = new SearchParams(), - pageSize = SearchParams.pageSize, - debug = false, - keyField = "hid", - keyDirection = SEEK.DESC, - ...otherConfig - } = cfg; - - // builds a monitor observable based on a hid - const monitor = (hid) => of([history.id, filters, hid]).pipe( - monitorHistoryContent({ pageSize, debug, keyField }) - ); - - // handles the plumbing for observing the query (monitor) and collecting the output into an - // aggregate ordered list focused around the current target key (hid in this case) - return hid$.pipe( - map(val => parseInt(val)), - aggregateCacheUpdates(monitor, { - keyField, - keyDirection, - pageSize, - debug, - ...otherConfig - }), - ); -}; diff --git a/client/src/components/providers/History/HistoryContentProvider/watchHistoryContents.test.js b/client/src/components/providers/History/HistoryContentProvider/watchHistoryContents.test.js deleted file mode 100644 index b6cccd40eb0..00000000000 --- a/client/src/components/providers/History/HistoryContentProvider/watchHistoryContents.test.js +++ /dev/null @@ -1,211 +0,0 @@ -import { of, isObservable, timer } from "rxjs"; -import { take, takeUntil } from "rxjs/operators"; -import { ObserverSpy } from "@hirez_io/observer-spy"; -import { untilNthEmission } from "jest/helpers"; - -import { SearchParams } from "components/providers/History/SearchParams"; -import { buildContentId } from "components/providers/History/caching/db/observables"; -import { watchHistoryContents } from "./watchHistoryContents"; -import { cacheContent, getCachedContent, bulkCacheContent, wipeDatabase } from "components/providers/History/caching"; - -import historyContent from "components/providers/History/test/json/historyContent.json"; - -// need to mock monitorHistoryContent which is instantiated inside the worker, -// but we can't actually fire up threads.js in a unit test because you can't -// call expose() on the main thread, so we mock the adapter functions which -// generate observables from the worker. -jest.mock("app"); -jest.mock("components/providers/History/caching"); - -beforeEach(async () => { - await wipeDatabase(); - // eslint-disable-next-line no-unused-vars - const cachedContent = await bulkCacheContent(historyContent, true); - // console.log(cachedContent.map((o) => o.hid)); -}); - -afterEach(async () => { - await wipeDatabase(); -}); - -// We're testing observables and we're assuming they complete, -// but we add a takeUntil() to each one in the event that something -// is wrong to avoid making the tests take forever -const safetyTimeout = 1000; - -describe("watchHistoryContents", () => { - const firstDoc = historyContent[0]; - - describe("simple loading scenario", () => { - const hid$ = of(firstDoc.hid); - const history = { id: firstDoc.history_id }; - const filters = new SearchParams(); - const pageSize = 5; - - test("should observe pre-inserted content on the initial emission", async () => { - const watcher$ = hid$.pipe( - watchHistoryContents({ - history, - filters, - pageSize, - debug: false, - }), - takeUntil(timer(safetyTimeout)) - ); - - expect(isObservable(watcher$)).toBe(true); - - // spy on output of observable, wait for it to end - const spy = new ObserverSpy(); - watcher$.subscribe(spy); - await spy.onComplete(); - - expect(spy.receivedNext()).toBe(true); - expect(spy.receivedComplete()).toBe(true); - expect(spy.getValuesLength()).toBe(1); - - const { startKey, contents } = spy.getFirstValue(); - // we know there should be an exactd match - expect(startKey).toEqual(firstDoc.hid); - expect(Array.isArray(contents)).toEqual(true); - // Top of list, match + 2 * pageSize - expect(contents.length).toBeGreaterThanOrEqual(2 * pageSize); - }); - - test("should observe subsequently updated data", async () => { - const watcher$ = hid$.pipe( - watchHistoryContents({ - history, - filters, - debouncePeriod: 100, - pageSize: 5, - }), - takeUntil(timer(safetyTimeout)) - ); - - // spy on output of observable, wait for it to end - const spy = new ObserverSpy(); - watcher$.subscribe(spy); - - // wait for the first event (take(n) as a promise for easier testing) - await untilNthEmission(watcher$, 1, safetyTimeout); - - // then update the first doc, need to delay longer than - // the debounce on the watch operator or the aggregation will - // think it's all part of the same update - const docId = buildContentId(firstDoc); - const lookup = await getCachedContent(docId); - expect(lookup.hid).toEqual(firstDoc.hid); - expect(lookup._id).toEqual(docId); - - lookup.foo = "abc"; - const update = await cacheContent(lookup); - expect(update.updated).toEqual(true); - expect(update.id).toEqual(lookup._id); - expect(update.id).toEqual(docId); - - // Should see 2 sets of content, the last of which should have the - // foo field in one of its entries - await spy.onComplete(); - expect(spy.receivedNext()).toBe(true); - expect(spy.receivedComplete()).toBe(true); - expect(spy.getValuesLength()).toBe(2); - - const { contents: lastContents } = spy.getLastValue(); - expect(lastContents.some((doc) => doc.foo == "abc")).toBe(true); - }); - }); - - describe("should respect filter inputs", () => { - const hid$ = of(firstDoc.hid); - const history = { id: firstDoc.history_id }; - let filters; - - beforeEach(() => { - filters = new SearchParams(); - }); - - test("deleted flag", async () => { - filters.showDeleted = true; - - const watcher$ = hid$.pipe( - watchHistoryContents({ - history, - filters, - pageSize: 5, - }), - take(1), - takeUntil(timer(safetyTimeout)) - ); - expect(isObservable(watcher$)).toBe(true); - - // spy on output of observable, wait for it to end - const spy = new ObserverSpy(); - watcher$.subscribe(spy); - await spy.onComplete(); - - expect(spy.receivedNext()).toBe(true); - expect(spy.receivedComplete()).toBe(true); - - const { contents } = spy.getFirstValue(); - expect(Array.isArray(contents)).toEqual(true); - expect(contents.every((doc) => doc.isDeleted == true)).toBe(true); - }); - - test("hidden flag", async () => { - filters.showHidden = true; - - const watcher$ = hid$.pipe( - watchHistoryContents({ - history, - filters, - pageSize: 5, - }), - take(1), - takeUntil(timer(safetyTimeout)) - ); - expect(isObservable(watcher$)).toBe(true); - - // spy on output of observable, wait for it to end - const spy = new ObserverSpy(); - watcher$.subscribe(spy); - await spy.onComplete(); - - expect(spy.receivedNext()).toBe(true); - expect(spy.receivedComplete()).toBe(true); - - const { contents } = spy.getFirstValue(); - expect(Array.isArray(contents)).toEqual(true); - expect(contents.every((doc) => doc.visible == false)).toBe(true); - }); - - test("deleted and hidden flag", async () => { - filters.showDeleted = true; - filters.showHidden = true; - - const watcher$ = hid$.pipe( - watchHistoryContents({ - history, - filters, - pageSize: 5, - }), - take(1), - takeUntil(timer(safetyTimeout)) - ); - expect(isObservable(watcher$)).toBe(true); - - // spy on output of observable, wait for it to end - const spy = new ObserverSpy(); - watcher$.subscribe(spy); - await spy.onComplete(); - - expect(spy.receivedNext()).toBe(true); - expect(spy.receivedComplete()).toBe(true); - - const { contents } = spy.getFirstValue(); - expect(Array.isArray(contents)).toEqual(true); - expect(contents.every((doc) => doc.visible == false)).toBe(true); - expect(contents.every((doc) => doc.isDeleted == true)).toBe(true); - }); - }); -}); diff --git a/client/src/components/providers/History/SearchParams.test.js b/client/src/components/providers/History/SearchParams.test.js deleted file mode 100644 index e921b5a9ce2..00000000000 --- a/client/src/components/providers/History/SearchParams.test.js +++ /dev/null @@ -1,376 +0,0 @@ -// I'm using this as the reference for the requirement: -// https://training.galaxyproject.org/training-material/topics/galaxy-interface/tutorials/history/tutorial.html#advanced-searching - -import { SearchParams } from "./SearchParams"; -import { matchesSelector } from "pouchdb-selector-core"; -import { STATES } from "components/History/model/states"; - -// #region Test generators -const testPouchSelector = (userField, searchVal, goodDoc, badDoc) => { - let pouchFieldName; - let params; - let selector; - - beforeEach(() => { - params = new SearchParams(); - params.filterText = `${userField}="${searchVal}"`; - selector = { $and: params.pouchFilters }; - pouchFieldName = params.getPouchFieldName(userField); - }); - - describe("pouchdb selector", () => { - it("should have a row in the selector", () => { - const hasRow = selector.$and.some((row) => row[pouchFieldName] !== undefined); - expect(hasRow).toBeTruthy(); - }); - - it("should return results with matching name field", () => { - expect(matchesSelector(goodDoc, selector)).toBeTruthy(); - }); - - it("should not return results where the name string appears in other fields", () => { - expect(matchesSelector(badDoc, selector)).toBeFalsy(); - }); - }); -}; - -const testUrlGeneration = (userField, searchVal) => { - describe("request querystring", () => { - it("should generate a query string with the expected server-side parameter", () => { - const params = new SearchParams(); - params.filterText = `${userField}="${searchVal}"`; - const serverField = params.getServerFieldName(userField); - if (serverField) { - const qs = params.historyContentQueryString; - const queryParam = `${serverField}-contains=${encodeURIComponent(searchVal)}`; - expect(qs.includes(queryParam)).toBeTruthy(); - } - }); - }); -}; - -// TODO: fix api to actually process the fields that it says it will -const testNotInUrl = (userField, searchVal) => { - describe("request querystring", () => { - it("should not send this filter, because it does not work on the api as described", () => { - const params = new SearchParams(); - params.filterText = `${userField}="${searchVal}"`; - const serverField = params.getServerFieldName(userField); - const qs = params.historyContentQueryString; - expect(qs.includes(serverField)).toBeFalsy(); - }); - }); -}; - -//#endregion - -describe("history contents search field text parsing", () => { - describe("single field criteria", () => { - // name="FASTQC on" - // Any datasets with “FASTQC on” in the title, but avoids items which - // have “FASTQC on” in other fields like the description or annotation. - describe("name", () => { - const searchField = "name"; - const searchTerm = "FASTQC on"; - - const goodDoc = { - _id: "anything", - name: "asfasdfasdfasdf FASTQC onsdfasdfasdf", - visible: true, - isDeleted: false, - }; - - const badDoc = { - _id: "anything", - otherField: "asfasdfasdfasdf FASTQC onsdfasdfasdf", - visible: true, - isDeleted: false, - }; - - testPouchSelector(searchField, searchTerm, goodDoc, badDoc); - testUrlGeneration(searchField, searchTerm); - }); - - // format=vcf - // Datasets with a specific format. Some formats are hierarchical, e.g. - // searching for fastq will find fastq files but also fastqsanger and - // fastqillumina files. You can see more formats in the upload dialogue - describe("format", () => { - const searchField = "format"; - const searchTerm = "vcf"; - - const goodDoc = { - _id: "anything", - file_ext: "vcf", - visible: true, - isDeleted: false, - }; - - const badDoc = { - _id: "anything", - file_ext: "notgonnamatch", - visible: true, - isDeleted: false, - }; - - testPouchSelector(searchField, searchTerm, goodDoc, badDoc); - testUrlGeneration(searchField, searchTerm); - }); - - // database=hg19 Datasets with a specific reference genome - describe("database", () => { - const searchField = "database"; - const searchTerm = "hg19"; - - const goodDoc = { - _id: "anything", - genome_build: "hg19", - visible: true, - isDeleted: false, - }; - - const badDoc = { - _id: "anything", - genome_build: "somethingelse", - visible: true, - isDeleted: false, - }; - - testPouchSelector(searchField, searchTerm, goodDoc, badDoc); - testUrlGeneration(searchField, searchTerm); - }); - - // annotation="first of five" - describe("annotation", () => { - const searchField = "annotation"; - const searchTerm = "first of fiv"; - - const goodDoc = { - _id: "anything", - annotation: "this is the first of five", - visible: true, - isDeleted: false, - }; - - const badDoc = { - _id: "anything", - annotation: "somethingelse", - visible: true, - isDeleted: false, - }; - - testPouchSelector(searchField, searchTerm, goodDoc, badDoc); - testNotInUrl(searchField, searchTerm); - }); - - // NOTE: is annotation different from description? - // description="This is data of a Borneo Orangutan" for dataset summary - describe("description (same as annotation?)", () => { - const searchField = "description"; - const searchTerm = "This is data of a Borneo Orangutan"; - - const goodDoc = { - _id: "anything", - annotation: "asdfasdfThis is data of a Borneo Orangutansdfasdf", - visible: true, - isDeleted: false, - }; - - const badDoc = { - _id: "anything", - annotation: "somethingelse", - visible: true, - isDeleted: false, - }; - - testPouchSelector(searchField, searchTerm, goodDoc, badDoc); - testNotInUrl(searchField, searchTerm); - }); - - // info="started mapping" - // for searching on job’s info field. - // describe("job info", () => {}); - - // tag=experiment1 tag=to_publish - // for searching on (a partial) dataset tag. - describe("tags", () => { - const searchField = "tags"; - const searchTerm = "experiment1"; - - const goodDoc = { - _id: "anything", - tags: ["experiment1", "experiment2"], - visible: true, - isDeleted: false, - }; - - const badDoc = { - _id: "anything", - tags: ["nodice"], - visible: true, - isDeleted: false, - }; - - testPouchSelector(searchField, searchTerm, goodDoc, badDoc); - testUrlGeneration(searchField, searchTerm); - }); - - // hid=25 - // A specific history item ID (based on the ordering in the history) - describe("hid", () => { - const searchField = "hid"; - const searchTerm = 25; - - const goodDoc = { - _id: "anything", - hid: 25, - visible: true, - isDeleted: false, - }; - - const badDoc = { - _id: "anything", - hid: 200, - visible: true, - isDeleted: false, - }; - - testPouchSelector(searchField, searchTerm, goodDoc, badDoc); - - describe("request querystring", () => { - it("hid should show up with an =, no text-search", () => { - const params = new SearchParams(); - params.filterText = `${searchField}="${searchTerm}"`; - const qs = params.historyContentQueryString; - const serverField = params.getServerFieldName(searchField); - const fragment = `${serverField}=${searchTerm}`; - expect(qs.includes(fragment)).toBeTruthy(); - }); - }); - }); - - // state=error - // To show only datasets in a given state. Other options include ok, - // running, paused, and new. - describe("state", () => { - const searchField = "state"; - const searchTerm = STATES.OK; - - const goodDoc = { - _id: "anything", - hid: 25, - visible: true, - state: STATES.OK, - isDeleted: false, - }; - - const badDoc = { - _id: "anything", - hid: 200, - visible: true, - isDeleted: false, - state: STATES.ERROR, - }; - - testPouchSelector(searchField, searchTerm, goodDoc, badDoc); - - describe("request querystring", () => { - it("state should show up with an =, no text-search", () => { - const params = new SearchParams(); - params.filterText = `${searchField}="${searchTerm}"`; - const qs = params.historyContentQueryString; - const serverField = params.getServerFieldName(searchField); - const fragment = `${serverField}=${searchTerm}`; - expect(qs.includes(fragment)).toBeTruthy(); - }); - }); - }); - }); - - describe("multiple field criteria", () => { - it("should allow a boolean AND combination of 2 filters", () => { - const goodDoc = { - _id: "anything", - name: "sadfasdffooasdfasdf", - tags: ["abc", "def", "blah"], - visible: true, - isDeleted: false, - }; - - const onlyOneMatch = { - _id: "anything", - name: "sadfasdffooasdfasdf", - visible: true, - isDeleted: false, - }; - - const theOtherMatch = { - _id: "anything", - tags: ["abc", "def", "blah"], - visible: true, - isDeleted: false, - }; - - const params = new SearchParams(); - params.filterText = "name=foo tag=blah"; - - const selector = { $and: params.pouchFilters }; - - expect(matchesSelector(goodDoc, selector)).toBeTruthy(); - expect(matchesSelector(onlyOneMatch, selector)).toBeFalsy(); - expect(matchesSelector(theOtherMatch, selector)).toBeFalsy(); - }); - }); - - // TODO: There is a bug in the mongo query syntax implementation that stops us from having - // two simultaneous criteria for the same field with the same type of operator, example - // can't have { $and: [ { tag: { $regex: /abc/ }, { tag: { $regex: /def/ }}]} - // https://github.com/pouchdb/pouchdb/issues/8265 - - // I'll have to come up with a better solution for that AND intersection feature. - - xdescribe("deferring this functionality for now", () => { - it("should allow a boolean AND combination of 2 of the same filter", () => { - const doc = { - _id: "anything", - name: "abc def", - visible: true, - isDeleted: false, - }; - - // this should match - const params = new SearchParams(); - params.filterText = "name=abc name=def"; - const selector = { $and: params.pouchFilters }; - expect(matchesSelector(doc, selector)).toBeTruthy(); - - // this should not, it fails because of the bug which overwrites the regex on the field, - // making it impossible to run 2 regexes against "name" - const params2 = new SearchParams(); - params2.filterText = "name=sfsfssf name=def"; - const selector2 = params2.buildHistoryContentSelector(); - expect(matchesSelector(doc, selector2)).toBeFalsy(); - }); - - it("should allow a boolean AND combination of 2 filters of for the same field, partial matches", () => { - const doc = { - _id: "anything", - tags: ["abcasdfasdf", "def"], - visible: true, - isDeleted: false, - }; - - // this should match - const params = new SearchParams(); - params.filterText = "tags=abc tag=def"; - const selector = { $and: params.pouchFilters }; - expect(matchesSelector(doc, selector)).toBeTruthy(); - - // this should not - const params2 = new SearchParams(); - params2.filterText = "tags=foo tag=def"; - const selector2 = { $and: params2.pouchFilters }; - expect(matchesSelector(doc, selector2)).toBeFalsy(); - }); - }); -}); diff --git a/client/src/components/providers/History/UserHistories/index.js b/client/src/components/providers/History/UserHistories/index.js deleted file mode 100644 index ab5a8643022..00000000000 --- a/client/src/components/providers/History/UserHistories/index.js +++ /dev/null @@ -1,2 +0,0 @@ -import UserHistories from "./UserHistories"; -export default UserHistories; diff --git a/client/src/components/providers/History/caching/CacheApi.js b/client/src/components/providers/History/caching/CacheApi.js deleted file mode 100644 index b3a7adf619c..00000000000 --- a/client/src/components/providers/History/caching/CacheApi.js +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Separated api from Worker definition so that we can use it without the worker - * if desired. - * - * Note that database events are only consistent within the same database - * instance, so a cache Event from outside the worker will not be heard by the - * watchers inside the worker because those are actually 2 separate db - * instances. - */ - -import { content$, dscContent$ } from "./db/observables"; -import { monitorQuery } from "./db/monitorQuery"; - -export * from "./db/promises"; - -export { loadDscContent } from "./loadDscContent"; -export { loadHistoryContents, clearHistoryDateStore } from "./loadHistoryContents"; -export { monitorHistoryContent, monitorCollectionContent } from "./monitorHistoryContent"; -export { wipeDatabase } from "./db/wipeDatabase"; - -// generic content query monitor -export const monitorContentQuery = (cfg = {}) => { - return monitorQuery({ db$: content$, ...cfg }); -}; - -// generic collection content monitor -export const monitorDscQuery = (cfg = {}) => { - return monitorQuery({ db$: dscContent$, ...cfg }); -}; diff --git a/client/src/components/providers/History/caching/__mocks__/index.js b/client/src/components/providers/History/caching/__mocks__/index.js deleted file mode 100644 index 7f829455d4f..00000000000 --- a/client/src/components/providers/History/caching/__mocks__/index.js +++ /dev/null @@ -1,10 +0,0 @@ -// For testing, bypass the worker convrsions -// and wrap each function in a jest mock wrapper - -const cacheapi = require("../CacheApi"); - -const mockedModule = Object.entries(cacheapi).reduce((mod, [fnName, orig]) => { - return { ...mod, [fnName]: jest.fn(orig) }; -}, {}); - -module.exports = mockedModule; diff --git a/client/src/components/providers/History/caching/asObservable.js b/client/src/components/providers/History/caching/asObservable.js deleted file mode 100644 index d79b5032659..00000000000 --- a/client/src/components/providers/History/caching/asObservable.js +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Worker client utility - * - * asObservable takes an observable transformation and persists its state while - * it is running inside the worker. Normally when threads.js invokes an - * observable, it runs once, its subscription ends and subsequent calls to that - * same exposed function will start a new subscription. - * - * With asObservable, a subject is created inside the worker and subsequent - * method calls put their new value on that Subject, which is connected to the - * wrapped observable. Therefore the observable state is preserved until it is - * explicitly unsubscribed from the outside, and distinct(), scan(), or other - * stateful observable operators will continue to work. - */ -import { Subject, of } from "rxjs"; -import { startWith } from "rxjs/operators"; - -// prettier-ignore -export const asObservable = (operator) => { - const currentSubs = new Map(); - - // process notifications - // materialize exposed a "kind" variable for all observable messages, - // it's either N,C,E for next, complete, error - return ({ id, cfg = {}, value, kind }) => { - - if (kind == "N") { - if (!currentSubs.has(id)) { - const input$ = new Subject(); - const output$ = input$.pipe(startWith(value), operator(cfg)); - currentSubs.set(id, { input$, output$ }); - return output$; - } - const sub = currentSubs.get(id); - sub.input$.next(value); - } - - if (kind == "C" || kind == "E") { - const sub = currentSubs.get(id); - if (sub) { - sub.input$.complete(); - currentSubs.delete(id); - } - } - - return of(null); - }; -}; diff --git a/client/src/components/providers/History/caching/db/cacheIndices.js b/client/src/components/providers/History/caching/db/cacheIndices.js deleted file mode 100644 index 556d8e0d68c..00000000000 --- a/client/src/components/providers/History/caching/db/cacheIndices.js +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Indices are required by pouchdb to use the pouuchdb.find functionality. These - * are the indices we keep on the main history content database - */ - -export const contentIndices = [ - { - index: { - fields: [{ cached_at: "desc" }], - }, - name: "by cache time", - ddoc: "idx-content-history-cached_at", - }, -]; - -/** - * ...and the collection contents - */ - -export const dscIndices = [ - { - index: { - fields: [{ cached_at: "desc" }], - }, - name: "by cache time", - ddoc: "idx-dsc-contents-cached_at", - }, -]; diff --git a/client/src/components/providers/History/caching/db/changes.js b/client/src/components/providers/History/caching/db/changes.js deleted file mode 100644 index 535856cd074..00000000000 --- a/client/src/components/providers/History/caching/db/changes.js +++ /dev/null @@ -1,45 +0,0 @@ -import { pipe, Observable } from "rxjs"; -import { switchMap, filter, share } from "rxjs/operators"; - -// feed observables, keyed by underlying database instance -export const feeds = new Map(); - -/** - * Returns an observable with all the change events from the indicated database - * @param {Observable} db$ Pouch database observable - */ -// prettier-ignore -export const changes = (cfg = {}) => { - return pipe( - switchMap((db) => { - if (!feeds.has(db)) { - feeds.set(db, buildFeed(db, cfg)); - } - return feeds.get(db); - }), - // filter out index creation which can appear as a change - filter(({ id }) => !id.includes("_design")), - ); -}; - -/** - * Creates an observable that emits changes to the indicated db instance - * @param {PouchDB} db - * @param {object} cfg - */ -const buildFeed = (db, cfg = {}) => { - const { live = true, returnDocs = true, include_docs = true, since = "now", timeout = false } = cfg; - - const feed$ = new Observable((obs) => { - const changeOpts = { live, include_docs, returnDocs, since, timeout }; - const feed = db.changes(changeOpts); - feed.on("change", (update) => obs.next(update)); - feed.on("error", (err) => obs.error(err)); - return () => { - feed.cancel(); - feeds.delete(db); - }; - }); - - return feed$.pipe(share()); -}; diff --git a/client/src/components/providers/History/caching/db/changes.test.js b/client/src/components/providers/History/caching/db/changes.test.js deleted file mode 100644 index b02d20e5398..00000000000 --- a/client/src/components/providers/History/caching/db/changes.test.js +++ /dev/null @@ -1,172 +0,0 @@ -import { timer } from "rxjs"; -import { takeWhile, share, takeUntil } from "rxjs/operators"; -import { content$, dscContent$ } from "./observables"; -import { wipeDatabase } from "./wipeDatabase"; -import { - bulkCacheContent, - cacheContent, - getCachedContent, - bulkCacheDscContent, - getCachedCollectionContent, - cacheCollectionContent, -} from "./promises"; -import { changes, feeds } from "./changes"; -import { wait } from "jest/helpers"; - -// test data -import historyContent from "components/providers/History/test/json/historyContent.json"; -import collectionContent from "components/providers/History/test/json/collectionContent.json"; - -// https://github.com/hirezio/observer-spy/blob/master/README.md -import { ObserverSpy } from "@hirez_io/observer-spy"; - -beforeEach(wipeDatabase); -afterEach(wipeDatabase); - -describe("changes operator", () => { - describe("history content (content$)", () => { - test("hears updates when we add stuff to the history content cache", async () => { - // listen to changes until dumb fake prop appears - const update$ = content$.pipe( - changes(), - takeWhile((change) => change.doc.floobar == undefined, true), - share() - ); - - // spy on emissions - const spy = new ObserverSpy(); - update$.subscribe(spy); - - // put some stuff in th cache - const cacheResults = await bulkCacheContent(historyContent); - expect(cacheResults.length).toEqual(historyContent.length); - - // pull out one item, modify it - const testItem = await getCachedContent(cacheResults[0].id); - expect(testItem._id).toEqual(cacheResults[0].id); - testItem.floobar = Math.random(); - const updateResult = await cacheContent(testItem); - expect(updateResult.updated).toBeTruthy(); - - // wait for updates to complete, which should happen since we added "floobar" - await spy.onComplete(); - - // last update should be the testItem we saved - const emits = spy.getValues(); - const lastEmit = spy.getLastValue(); - expect(spy.receivedNext()).toBe(true); - expect(spy.receivedComplete()).toBe(true); - expect(lastEmit.id).toEqual(testItem._id); - expect(lastEmit.doc.floobar).toEqual(testItem.floobar); - expect(emits.length).toEqual(historyContent.length + 1); - - // original insert + later update - const updates = emits.filter((update) => update.doc._id == testItem._id); - expect(updates.length).toEqual(2); - - // only one should have the "floobar" field - const floobars = emits.filter((update) => update.doc.floobar !== undefined); - expect(floobars.length).toEqual(1); - }); - }); - - describe("collection content (dscContent$)", () => { - test("hears updates when we add stuff to the collection content cache", async () => { - // listen to changes until dumb fake prop appears - const update$ = dscContent$.pipe( - changes(), - takeWhile((change) => change.doc.floobar == undefined, true), - share() - ); - - // spy on emissions - const spy = new ObserverSpy(); - update$.subscribe(spy); - - // preprocess collection content to add parent_url - const fakeParent = "/api/123/blah"; - const processedCollection = collectionContent.map((doc) => { - doc.parent_url = fakeParent; - return doc; - }); - - // put some stuff in the cache - const cacheResults = await bulkCacheDscContent(processedCollection); - expect(cacheResults.length).toEqual(processedCollection.length); - - // pull out one item, modify it - const testItem = await getCachedCollectionContent(cacheResults[0].id); - expect(testItem._id).toEqual(cacheResults[0].id); - testItem.floobar = Math.random(); - const updateResult = await cacheCollectionContent(testItem); - expect(updateResult.updated).toBeTruthy(); - - // wait for updates to complete, which should happen since we added "floobar" - await spy.onComplete(); - - // last update should be the testItem we saved - const emits = spy.getValues(); - const lastEmit = spy.getLastValue(); - expect(spy.receivedNext()).toBe(true); - expect(spy.receivedComplete()).toBe(true); - expect(lastEmit.id).toEqual(testItem._id); - expect(lastEmit.doc.floobar).toEqual(testItem.floobar); - expect(emits.length).toEqual(collectionContent.length + 1); - - // original insert + later update - const updates = emits.filter((update) => update.doc._id == testItem._id); - expect(updates.length).toEqual(2); - - // only one should have the "floobar" field - const floobars = emits.filter((update) => update.doc.floobar !== undefined); - expect(floobars.length).toEqual(1); - }); - }); - - describe("change feed should be shared", () => { - const lifeTime = 300; - const spinUpTime = 100; - - test("subscriging should show 1 feed, unsubscribing should show 0", async () => { - // subscribe to changes once - const feed$ = content$.pipe(changes(), takeUntil(timer(lifeTime))); - const spy = new ObserverSpy(); - feed$.subscribe(spy); - - // give it a little time to put itself together - await wait(spinUpTime); - expect(feeds.size).toEqual(1); - - // complete, share() should now remove instance - await spy.onComplete(); - expect(feeds.size).toEqual(0); - }); - - test("subscribing 2 times should result in one feed if it's the same DB", async () => { - // subscribe to changes onece - const feed$ = content$.pipe(changes(), takeUntil(timer(2 * lifeTime))); - const spy = new ObserverSpy(); - feed$.subscribe(spy); - - // give it a little time to put itself together - await wait(spinUpTime); - expect(feeds.size).toEqual(1); - - // subscribe again - const feed2$ = content$.pipe(changes(), takeUntil(timer(lifeTime))); - const spy2 = new ObserverSpy(); - feed2$.subscribe(spy2); - - await wait(spinUpTime); - expect(feeds.size).toEqual(1); - - // unsub from 2nd feed - await spy2.onComplete(); - expect(feeds.size).toEqual(1); - - // unsub from 1st feed - await spy.onComplete(); - expect(feeds.size).toEqual(0); - }); - }); -}); diff --git a/client/src/components/providers/History/caching/db/db.test.js b/client/src/components/providers/History/caching/db/db.test.js deleted file mode 100644 index 5b9d172f551..00000000000 --- a/client/src/components/providers/History/caching/db/db.test.js +++ /dev/null @@ -1,251 +0,0 @@ -import isPromise from "is-promise"; -import { isObservable } from "rxjs"; -import { wipeDatabase } from "./wipeDatabase"; -import { content$, dscContent$, buildContentId, buildCollectionId, prepContent, prepDscContent } from "./observables"; -import { firstValueFrom } from "utils/observable/firstValueFrom"; - -import { - cacheContent, - getCachedContent, - uncacheContent, - bulkCacheContent, - getContentByTypeId, - bulkCacheDscContent, -} from "./promises"; - -// test data -import historyContent from "components/providers/History/test/json/historyContent.json"; -import collectionContent from "components/providers/History/test/json/collectionContent.json"; - -beforeEach(wipeDatabase); -afterEach(wipeDatabase); - -describe("database observables", () => { - describe("content$", () => { - it("should be an observable", () => { - expect(content$).toBeDefined; - expect(isObservable(content$)).toBeTruthy; - }); - - it("should yield a pouchdb instance", async () => { - const dbPromise = firstValueFrom(content$); - expect(isPromise(dbPromise)).toBeTrue; - const db = await dbPromise; - expect(db).toBeDefined; - expect(db.constructor.name).toEqual("PouchDB"); - }); - - it("should only build once", async () => { - const stuff = await firstValueFrom(content$); - const stuff2 = await firstValueFrom(content$); - const stuff3 = await firstValueFrom(content$); - expect(stuff).toStrictEqual(stuff2); - expect(stuff).toStrictEqual(stuff3); - }); - }); - - describe("dscContent$", () => { - it("should be an observable", () => { - expect(dscContent$).toBeDefined; - expect(isObservable(dscContent$)).toBeTruthy; - }); - - it("should yield a pouchdb instance", async () => { - const dbPromise = firstValueFrom(dscContent$); - expect(isPromise(dbPromise)).toBeTrue; - const db = await dbPromise; - expect(db).toBeDefined; - expect(db.constructor.name).toEqual("PouchDB"); - }); - - it("should only build once", async () => { - const stuff = await firstValueFrom(dscContent$); - const stuff2 = await firstValueFrom(dscContent$); - const stuff3 = await firstValueFrom(dscContent$); - expect(stuff).toStrictEqual(stuff2); - expect(stuff).toStrictEqual(stuff3); - }); - }); -}); - -describe("history content operators and functions", () => { - const testDoc = historyContent[0]; - const testId = buildContentId(testDoc); - - describe("buildContentId", () => { - it("should add the history and hid together", () => { - const id = buildContentId(testDoc); - expect(id).toContain(String(testDoc.history_id)); - expect(id).toContain(String(testDoc.hid)); - }); - }); - - describe("prepContent", () => { - const doc = prepContent(testDoc); - - it("should generate an _id for the content", () => { - expect(doc._id).toContain(String(testDoc.history_id)); - expect(doc._id).toContain(String(testDoc.hid)); - }); - - it("should rename the deleted field because pouchdb reserves that prop", () => { - expect(doc.deleted).toBeUndefined; - expect(doc.isDeleted).toBeDefined; - expect(doc.isDeleted).toEqual(testDoc.deleted); - }); - }); - - describe("cacheContent", () => { - it("should cache a single document", async () => { - const cacheSummary = await cacheContent(testDoc); - expect(cacheSummary).toBeDefined; - expect(cacheSummary.id).toEqual(testId); - }); - }); - - describe("getCachedContent", () => { - it("should lookup cached content", async () => { - const cacheSummary = await cacheContent(testDoc); - expect(cacheSummary).toBeDefined; - expect(cacheSummary.id).toEqual(testId); - - const lookup = await getCachedContent(testId); - expect(lookup).toBeDefined; - expect(lookup._id).toEqual(testId); - }); - }); - - describe("uncacheContent", () => { - it("should erase a previously cached item", async () => { - await cacheContent(testDoc); - const cachedDoc = await getCachedContent(testId); - const deleteResult = await uncacheContent(cachedDoc); - expect(deleteResult.ok).toBeTruthy; - const lookup = await getCachedContent(testId); - expect(lookup).toEqual(null); - }); - }); - - describe("getContentByTypeId", () => { - it("should retrieve cached content using the type_id", async () => { - // cache some content - const { id } = await cacheContent(testDoc); - - // look it up using _id - const { history_id, type_id } = await getCachedContent(id); - - // lookup by type_id - const docByType = await getContentByTypeId(history_id, type_id); - expect(docByType.type_id).toBe(type_id); - }); - }); - - describe("cacheContent and return doc", () => { - it("should cache raw props and return with an _id and a cached_at time", async () => { - const doc = await cacheContent(testDoc, true); - expect(doc._id).toEqual(buildContentId(testDoc)); - expect(doc.cached_at).toExist; - expect(typeof doc.cached_at).toEqual("number"); - expect(doc.cached_at).toBeGreaterThan(0); - }); - }); - - describe("bulkCacheContent", () => { - it("should put a whole line of stuff into the cache", async () => { - const bulkResult = await bulkCacheContent(historyContent); - expect(bulkResult.length).toEqual(historyContent.length); - const cachedIds = bulkResult.map((item) => item.id); - const inputIds = historyContent.map((doc) => buildContentId(doc)); - expect(cachedIds).toEqual(inputIds); - }); - }); -}); - -describe("collection content operators and functions", () => { - describe("prepDscContent", () => { - it("should process a raw doc (insert from ajax)", () => { - const raw = collectionContent[0]; - raw.parent_url = "foo"; // required step - const dsc = prepDscContent(raw); - expect(dsc).toExist; - expect(dsc._id).toContain(raw.parent_url); - expect(dsc._id).toContain(String(raw.element_index)); - }); - - // Warrants more testing because we have to untwist the api response format as - // well as working on existing (already converted) objects - - test("should return valid results for pre-processed cache results (on update)", () => { - // simulate a collection that has already been cached - - const updateMe = { - id: "fb85969571388350", - type_id: "dataset_collection-fb85969571388350", - model_class: "DatasetCollection", - history_content_type: "dataset_collection", - name: "ABC", - element_identifier: "ABC", - element_index: 10000, - element_type: "dataset_collection", - parent_url: "/abc/def/ghi", - contents_url: "/api/dataset_collections/5a1cff6882ddb5b2/contents/fb85969571388350", - collection_type: "paired", - cached_at: 1602265731626, - _id: "/abc/def/ghi-000000010000", - _rev: "6-377d7786587cd5e9e4524a736c228a90", - }; - - const processed = prepDscContent(updateMe); - expect(processed._id).toEqual(buildCollectionId(updateMe)); - expect(processed.history_content_type).toBe("dataset_collection"); - expect(processed.id).toEqual(updateMe.id); - expect(processed.type_id).toEqual(updateMe.type_id); - }); - }); - - describe("bulkCacheDscContent", () => { - // we need to add parent_url to every item in the collection contents cache - // becahse the contents_url for the collection the contents came from is - // going to be the parent_url and part of the _id - const parent_url = "fakecollection"; - const collectionData = collectionContent.map((doc) => { - doc.parent_url = parent_url; - return doc; - }); - - it("should put a whole line of stuff into the cache", async () => { - const bulkSummary = await bulkCacheDscContent(collectionData); - expect(bulkSummary.length).toEqual(collectionContent.length); - const cachedIds = bulkSummary.map((item) => item.id); - const inputIds = collectionData.map((doc) => buildCollectionId(doc)); - expect(cachedIds).toEqual(inputIds); - }); - - it("should retrieve that stuff later", async () => { - const bulkSummary = await bulkCacheDscContent(collectionData); - expect(bulkSummary.length).toEqual(collectionContent.length); - }); - }); -}); - -describe("wipeDatabase", () => { - const testDoc = historyContent[0]; - const testId = buildContentId(testDoc); - - it("should clear out stuff so each test is clean", async () => { - const cacheSummary = await cacheContent(testDoc); - expect(cacheSummary.id).toEqual(testId); - - // retrieve the cached doc to make sure it's in there - const retrievedDoc = await getCachedContent(testId); - expect(retrievedDoc).toExist; - expect(retrievedDoc._id).toEqual(testId); - - // wipe - await wipeDatabase(); - - // try to retrieve again - const lookupAgain = await getCachedContent(testId); - expect(lookupAgain).toEqual(null); - }); -}); diff --git a/client/src/components/providers/History/caching/db/find.js b/client/src/components/providers/History/caching/db/find.js deleted file mode 100644 index b363a505d49..00000000000 --- a/client/src/components/providers/History/caching/db/find.js +++ /dev/null @@ -1,42 +0,0 @@ -import { pipe } from "rxjs"; -import { mergeMap } from "rxjs/operators"; -import { show, needs } from "utils/observable"; - -/** - * Pouchdb-find as an operator - * https://pouchdb.com/guides/mango-queries.html - * - * @param {Observable} db$ Observable pouchDb instance - */ -export const find = (db$, cfg = {}) => { - const { label = "find", debug = false } = cfg; - - return pipe( - show(debug, (request) => console.log(`${label} -> request`, request)), - needs(db$), - mergeMap(async (inputs) => { - const [request, db] = inputs; - - const { index } = request; - if (index !== undefined) { - const indexResponse = await db.createIndex({ index }); - const { result: idxResult } = indexResponse; - if (idxResult !== "created" && idxResult !== "exists") { - throw new Error("Unknown index creation result", indexResponse); - } - } - - let docs; - try { - const findResponse = await db.find(request); - docs = findResponse.docs || []; - } catch (err) { - console.warn("find() error", err, request, db.name); - throw err; - } - - return docs; - }), - show(debug, (result) => console.log(`${label} -> result`, result)) - ); -}; diff --git a/client/src/components/providers/History/caching/db/find.test.js b/client/src/components/providers/History/caching/db/find.test.js deleted file mode 100644 index 0bd627589a0..00000000000 --- a/client/src/components/providers/History/caching/db/find.test.js +++ /dev/null @@ -1,368 +0,0 @@ -import { Subject } from "rxjs"; -import { take } from "rxjs/operators"; -import { ObserverSpy } from "@hirez_io/observer-spy"; - -import { wipeDatabase } from "./wipeDatabase"; -import { bulkCacheContent, bulkCacheDscContent } from "./promises"; -import { content$, dscContent$, buildContentId } from "./observables"; -import { find } from "./find"; - -// test data -import historyContent from "components/providers/History/test/json/historyContent.json"; -import collectionContent from "components/providers/History/test/json/collectionContent.json"; - -beforeEach(wipeDatabase); -afterEach(wipeDatabase); - -describe("find operator", () => { - // setup request subject, find observable, and spy to look at output - const request$ = new Subject(); - - // unsub, if requred - let obs$; - let spy; - - describe("content database (content$) queries", () => { - beforeEach(async () => await bulkCacheContent(historyContent)); - - beforeEach(() => { - obs$ = request$.pipe(find(content$), take(1)); - spy = new ObserverSpy(); - obs$.subscribe(spy); - }); - - test("should select everything with a blank selector", async () => { - const request = { selector: {} }; - request$.next(request); - await spy.onComplete(); - - const results = spy.getFirstValue(); - expect(spy.receivedNext()).toBe(true); - expect(spy.receivedComplete()).toBe(true); - expect(results.length).toEqual(historyContent.length); - }); - - test("should return everything with a blank selector, while building a custom index", async () => { - const request = { - selector: {}, - sort: [{ _id: "desc" }], - index: { - fields: ["_id"], - sort: [{ _id: "desc" }], - name: "foo index", - ddoc: "idx-foo", - }, - }; - request$.next(request); - await spy.onComplete(); - - const results = spy.getFirstValue(); - expect(spy.receivedNext()).toBe(true); - expect(spy.receivedComplete()).toBe(true); - expect(results.length).toEqual(historyContent.length); - }); - - test("should return with a limit", async () => { - const limit = 3; - const request = { - selector: {}, - sort: [{ _id: "desc" }], - limit, - index: { - fields: ["_id"], - sort: [{ _id: "desc" }], - name: "foo index", - ddoc: "idx-foo", - }, - }; - request$.next(request); - await spy.onComplete(); - - const results = spy.getFirstValue(); - expect(spy.receivedNext()).toBe(true); - expect(spy.receivedComplete()).toBe(true); - expect(results.length).toEqual(limit); - }); - - test("descending history-hid query", async () => { - const { history_id, hid } = historyContent[0]; - const request = { - selector: { - _id: { $lte: `${history_id}-${hid}` }, - }, - sort: [{ _id: "desc" }], - limit: 3, - index: { - fields: ["_id"], - sort: [{ _id: "desc" }], - name: "content history_id and hid descending", - ddoc: "idx-historyid-hid-desc", - }, - }; - request$.next(request); - await spy.onComplete(); - - const results = spy.getFirstValue(); - expect(spy.receivedNext()).toBe(true); - expect(spy.receivedComplete()).toBe(true); - expect(results.length).toEqual(3); - - results.forEach((doc) => { - expect(doc.history_id).toEqual(history_id); - expect(doc.hid).toBeLessThanOrEqual(hid); - }); - }); - - test("descending with visible flag", async () => { - const { history_id, hid } = historyContent[0]; - const request = { - selector: { - _id: { $lte: `${history_id}-${hid}` }, - visible: false, - }, - sort: [{ _id: "desc" }], - limit: 3, - index: { - fields: ["_id"], - sort: [{ _id: "desc" }], - name: "content history_id and hid descending", - ddoc: "idx-historyid-hid-desc", - }, - }; - request$.next(request); - await spy.onComplete(); - - const results = spy.getFirstValue(); - expect(spy.receivedNext()).toBe(true); - expect(spy.receivedComplete()).toBe(true); - - // only put two visible items in the test data - const visibleRows = historyContent.filter((row) => row.visible == false); - expect(results.length).toEqual(visibleRows.length); - - results.forEach((doc) => { - expect(doc.history_id).toEqual(history_id); - expect(doc.hid).toBeLessThanOrEqual(hid); - expect(doc.visible).toEqual(false); - }); - }); - - test("descending with isDeleted flag", async () => { - const { history_id, hid } = historyContent[0]; - const request = { - selector: { - _id: { $lte: `${history_id}-${hid}` }, - isDeleted: true, - }, - sort: [{ _id: "desc" }], - limit: 3, - index: { - fields: ["_id"], - sort: [{ _id: "desc" }], - name: "content history_id and hid descending", - ddoc: "idx-historyid-hid-desc", - }, - }; - request$.next(request); - await spy.onComplete(); - - const results = spy.getFirstValue(); - // console.log(results); - expect(spy.receivedNext()).toBe(true); - expect(spy.receivedComplete()).toBe(true); - - // only put two visible items in the test data - const undeletedRows = historyContent.filter((row) => { - return row.hid <= hid && row.deleted === true; - }); - expect(results.length).toEqual(undeletedRows.length); - - results.forEach((doc) => { - expect(doc.history_id).toEqual(history_id); - expect(doc.hid).toBeLessThanOrEqual(hid); - expect(doc.isDeleted).toEqual(true); - }); - }); - - test("ascending history-hid query", async () => { - const doc = historyContent[5]; - const { history_id, hid } = doc; - const targetHid = buildContentId(doc); - const request = { - selector: { - _id: { $gt: targetHid }, - }, - sort: [{ _id: "desc" }], - limit: 3, - index: { - fields: ["_id"], - sort: [{ _id: "desc" }], - name: "content history_id and hid descending", - ddoc: "idx-historyid-hid-desc", - }, - }; - request$.next(request); - await spy.onComplete(); - - const results = spy.getFirstValue(); - expect(spy.receivedNext()).toBe(true); - expect(spy.receivedComplete()).toBe(true); - - // no additional fitlers should show all undeleted and visible rows - const expectedRows = historyContent.filter((row) => { - return row.hid > hid && row.deleted === false && row.visible == true; - }); - expect(results.length).toEqual(expectedRows.length); - - results.forEach((doc) => { - expect(doc.history_id).toEqual(history_id); - expect(doc.hid).toBeGreaterThan(hid); - }); - }); - - test("ascending with visible flag", async () => { - // start at the 5th row and look upwards - const doc = historyContent[5]; - const { hid } = doc; - const targetId = buildContentId(doc); - const request = { - selector: { - _id: { $gt: targetId }, - visible: false, - }, - sort: [{ _id: "desc" }], - limit: 3, - index: { - fields: ["_id"], - sort: [{ _id: "desc" }], - name: "content history_id and hid descending", - ddoc: "idx-historyid-hid-desc", - }, - }; - request$.next(request); - await spy.onComplete(); - - const results = spy.getFirstValue(); - expect(spy.receivedNext()).toBe(true); - expect(spy.receivedComplete()).toBe(true); - - // count the invisible rows where the hid > targetId; - const expectedRows = historyContent.filter((row) => { - return row.hid > hid && row.visible === false; - }); - expect(results.length).toEqual(expectedRows.length); - }); - - test("ascending with isDeleted flag", async () => { - const doc = historyContent[5]; - const { history_id, hid } = doc; - const targetId = buildContentId(doc); - const request = { - selector: { - _id: { $gt: targetId }, - isDeleted: true, - }, - sort: [{ _id: "desc" }], - limit: 3, - index: { - fields: ["_id"], - sort: [{ _id: "desc" }], - name: "content history_id and hid descending", - ddoc: "idx-historyid-hid-desc", - }, - }; - request$.next(request); - await spy.onComplete(); - - const results = spy.getFirstValue(); - expect(spy.receivedNext()).toBe(true); - expect(spy.receivedComplete()).toBe(true); - - // only put one deleted near the top - const expectedRows = historyContent.filter((row) => { - return row.hid > hid && row.deleted === true; - }); - expect(results.length).toEqual(expectedRows.length); - - results.forEach((doc) => { - expect(doc.history_id).toEqual(history_id); - expect(doc.hid).toBeGreaterThan(hid); - expect(doc.isDeleted).toEqual(true); - }); - }); - }); - - describe("collection content database (dscContent$) queries", () => { - // put some stuff in the cache - // preprocess the collection content - const fakeParent = "/foo/bar"; - const dscTestContent = collectionContent.map((props) => { - props.parent_url = fakeParent; - return props; - }); - - beforeEach(async () => await bulkCacheDscContent(dscTestContent)); - - beforeEach(() => { - obs$ = request$.pipe(find(dscContent$), take(1)); - spy = new ObserverSpy(); - obs$.subscribe(spy); - }); - - test("should select everything with a blank selector", async () => { - const request = { selector: {} }; - request$.next(request); - await spy.onComplete(); - - const results = spy.getFirstValue(); - expect(spy.receivedNext()).toBe(true); - expect(spy.receivedComplete()).toBe(true); - expect(results.length).toEqual(collectionContent.length); - }); - - test("ascending element_index", async () => { - const request = { - selector: { - _id: { $gte: `${fakeParent}-0` }, - }, - sort: [{ _id: "asc" }], - index: { - fields: ["_id"], - sort: [{ _id: "asc" }], - name: "ascending index", - ddoc: "idx-collection-ascending", - }, - }; - request$.next(request); - await spy.onComplete(); - - const results = spy.getFirstValue(); - expect(spy.receivedNext()).toBe(true); - expect(spy.receivedComplete()).toBe(true); - expect(results.length).toEqual(collectionContent.length); - }); - - test("ascending element_index with name regex", async () => { - const request = { - selector: { - _id: { $gte: `${fakeParent}-0` }, - element_identifier: { $regex: /M117C1/i }, - }, - sort: [{ _id: "asc" }], - index: { - fields: ["_id"], - sort: [{ _id: "asc" }], - name: "ascending index", - ddoc: "idx-collection-ascending", - }, - }; - request$.next(request); - await spy.onComplete(); - - const results = spy.getFirstValue(); - expect(spy.receivedNext()).toBe(true); - expect(spy.receivedComplete()).toBe(true); - expect(results.length).toBeLessThan(collectionContent.length); - }); - }); -}); diff --git a/client/src/components/providers/History/caching/db/index.js b/client/src/components/providers/History/caching/db/index.js deleted file mode 100644 index 0e77b861485..00000000000 --- a/client/src/components/providers/History/caching/db/index.js +++ /dev/null @@ -1,16 +0,0 @@ -export { - content$, - dscContent$, - getCachedContent, - cacheContent, - uncacheContent, - bulkCacheContent, - bulkCacheDscContent, -} from "./observables"; - -export { find } from "./find"; -export { changes } from "./changes"; -export { monitorQuery } from "./monitorQuery"; - -// utility function -export { wipeDatabase } from "./wipeDatabase"; diff --git a/client/src/components/providers/History/caching/db/monitorQuery.js b/client/src/components/providers/History/caching/db/monitorQuery.js deleted file mode 100644 index 74e0537e258..00000000000 --- a/client/src/components/providers/History/caching/db/monitorQuery.js +++ /dev/null @@ -1,56 +0,0 @@ -import deepEqual from "deep-equal"; -import { isObservable, concat, of } from "rxjs"; -import { concatAll, map, switchMap, debounceTime, distinctUntilChanged, pluck, filter } from "rxjs/operators"; -import { matchesSelector } from "pouchdb-selector-core"; -import { find } from "./find"; -import { changes } from "./changes"; -import { show } from "utils/observable"; - -/** - * Turns a selector into a live cache result emitter. Emits objects with - * a document object and a match boolean flag ({ doc, match: true }) indicating - * whether or not this document matches the request$ selector - * - * @param {Observable} db$ Observable PouchDB instance - * @param {Observable} request$ Observable Pouchdb-find configuration - */ -// prettier-ignore -export const monitorQuery = (cfg = {}) => (request$) => { - const { db$, inputDebounce = 0, debug = false, label } = cfg; - - if (!isObservable(db$)) { - throw new Error("Please pass a pouch database observable to monitorQuery"); - } - - const debouncedRequest$ = request$.pipe( - debounceTime(inputDebounce), - distinctUntilChanged(deepEqual), - ); - - return debouncedRequest$.pipe( - switchMap((request) => { - const { selector } = request; - const docMatch = doc => matchesSelector(doc, selector); - - // do a search of the cache first - const initial$ = of(request).pipe( - find(db$, { debug, label }), - concatAll(), - map((doc) => ({ doc, match: true, initial: true })), - show(debug, (change) => console.log("monitorQuery: initial", change)), - ); - - // later changes - const updates$ = db$.pipe( - changes({ debug, label }), - pluck("doc"), - filter(Boolean), - map((doc) => ({ doc, match: docMatch(doc), update: true })), - show(debug, (change) => console.log("monitorQuery: update", change)), - ); - - return concat(initial$, updates$); - }), - show(debug, (change) => console.log("monitorQuery", change)), - ); -}; diff --git a/client/src/components/providers/History/caching/db/monitorQuery.test.js b/client/src/components/providers/History/caching/db/monitorQuery.test.js deleted file mode 100644 index 61d393034fc..00000000000 --- a/client/src/components/providers/History/caching/db/monitorQuery.test.js +++ /dev/null @@ -1,398 +0,0 @@ -import { of, timer } from "rxjs"; -import { takeUntil } from "rxjs/operators"; -import { ObserverSpy } from "@hirez_io/observer-spy"; -import { wait } from "jest/helpers"; -import { wipeDatabase } from "./wipeDatabase"; - -import { monitorQuery } from "./monitorQuery"; -import { content$, dscContent$ } from "./observables"; -import { cacheContent, cacheCollectionContent, bulkCacheContent, bulkCacheDscContent } from "./promises"; - -// test data -import historyContent from "components/providers/History/test/json/historyContent.json"; -import collectionContent from "components/providers/History/test/json/collectionContent.json"; - -jest.mock("app"); -jest.mock("../../caching"); - -const monitorSpinUp = 100; -const monitorSafetyTimeout = 800; - -const pluckAll = (arr, prop) => arr.map((o) => o[prop]); - -afterEach(wipeDatabase); - -// prettier-ignore -describe("monitorQuery: history content", () => { - - describe("initial results", () => { - test("first monitor event has initial results reflecting existing matches in db", async () => { - const cachedContent = await bulkCacheContent(historyContent, true); - const cachedContentIds = new Set(pluckAll(cachedContent, "_id")); - - // create new monitor with query selector hid > 160 - const cutoffHid = 120; - const selector = { hid: { $gt: cutoffHid } }; - const monitor$ = of({ selector }).pipe( - monitorQuery({ db$: content$ }), - takeUntil(timer(monitorSafetyTimeout)), - ); - - // // listen to observable, wait for complete - const spy = new ObserverSpy(); - monitor$.subscribe(spy); - await spy.onComplete(); - - // should get one emit out with just the intial matches - const emits = spy.getValues(); - expect(spy.getValuesLength()).toBeGreaterThan(0); - expect(spy.getValuesLength()).toBeLessThanOrEqual(cachedContentIds.size); - emits.forEach(({ doc, match, initial }) => { - // we aren't changing anything, results should be initial matches - expect(match).toBeTruthy(); - // initial results have an identifying flag - expect(initial).toBe(true); - // every doc should match the selector - expect(doc => doc.hid > cutoffHid).toBeTruthy(); - // every doc should be in the initially cached items - expect(cachedContentIds.has(doc._id)).toBeTruthy(); - }); - }); - }); - - describe("updates", () => { - - test("INSERT: adding to cache after instantiation should emit a new doc", async () => { - - const cutoffHid = 50; - const selector = { hid: { $gt: cutoffHid } }; - const monitor$ = of({ selector }).pipe( - monitorQuery({ db$: content$ }), - takeUntil(timer(monitorSafetyTimeout)), - ); - - const spy = new ObserverSpy(); - monitor$.subscribe(spy); - - // wait for the monitor to spin-up so the insert doesn't look like the initial vals - await wait(monitorSpinUp); - - // Insert a row, make sure it should match the selector - const insertedContent = await cacheContent(historyContent[0], true); - expect(insertedContent.hid > cutoffHid).toBeTruthy(); - - // wait for observable end - await spy.onComplete(); - - const emits = spy.getValues(); - expect(spy.getValuesLength()).toEqual(1); - emits.forEach(({ doc, match, initial, update }) => { - expect(doc.hid > cutoffHid).toBeTruthy(); - expect(match).toBe(true); - expect(initial).toBeUndefined(); - expect(update).toBe(true); - }) - }); - - test("UPDATE: updating a previously emitted doc should emit an update event", async () => { - const cutoffHid = 50; - - // Insert a row, make sure it should match the selector - const insertedContent = await cacheContent(historyContent[0], true); - expect(insertedContent.hid > cutoffHid).toBeTruthy(); - - // subscribe to listener - const selector = { hid: { $gt: cutoffHid } }; - const monitor$ = of({ selector }).pipe( - monitorQuery({ db$: content$ }), - takeUntil(timer(monitorSafetyTimeout)), - ); - const spy = new ObserverSpy(); - monitor$.subscribe(spy); - - // wait for the monitor to spin-up so the insert doesn't look like the initial vals - await wait(monitorSpinUp); - - // update the content - const fooVal = 123; - insertedContent.foo = fooVal; - const updatedContent = await cacheContent(insertedContent, true); - expect(updatedContent._id).toEqual(insertedContent._id); - expect(updatedContent.foo).toEqual(fooVal); - - // wait for observable end - await spy.onComplete(); - expect(spy.getValuesLength()).toEqual(2); // initial insert + later update - - // check first event - const firstEvent = spy.getValueAt(0); - { - const { doc, match, initial, update } = firstEvent; - expect(doc.foo).toBeUndefined(); - expect(match).toBe(true); - expect(initial).toBe(true); - expect(update).toBeUndefined(); - } - - // check update event - const secondEvent = spy.getValueAt(1); - { - const { doc, match, initial, update } = secondEvent; - expect(doc.foo).toBe(fooVal); - expect(match).toBe(true); - expect(initial).toBeUndefined(); - expect(update).toBe(true); - } - }); - - test("DELETE: cache update causes element to no longer be in selection", async () => { - const cutoffHid = 50; - - // Insert a row, make sure it should match the selector - const insertedContent = await cacheContent(historyContent[0], true); - expect(insertedContent.hid > cutoffHid).toBeTruthy(); - - // subscribe to listener - const selector = { hid: { $gt: cutoffHid } }; - const monitor$ = of({ selector }).pipe( - monitorQuery({ db$: content$ }), - takeUntil(timer(monitorSafetyTimeout)), - ); - const spy = new ObserverSpy(); - monitor$.subscribe(spy); - - // wait for the monitor to spin-up so the insert doesn't look like the initial vals - await wait(monitorSpinUp); - - // update the content to be outside the selector - const badHid = -10000; - insertedContent.hid = badHid; // no longer > cutoffHid - const updatedContent = await cacheContent(insertedContent, true); - expect(updatedContent._id).toEqual(insertedContent._id); - expect(updatedContent.hid).toEqual(badHid); - - // wait for observable end - await spy.onComplete(); - expect(spy.getValuesLength()).toEqual(2); // initial insert + later update - - // check first event - const firstEvent = spy.getValueAt(0); - { - const { doc, match, initial, update } = firstEvent; - expect(doc).toBeDefined(); - expect(doc).toBeInstanceOf(Object); - expect(doc.hid).not.toEqual(badHid); - expect(match).toBe(true); - expect(initial).toBe(true); - expect(update).toBeUndefined(); - } - - // check update event - const secondEvent = spy.getValueAt(1); - { - const { doc, match, initial, update } = secondEvent; - expect(doc).toBeDefined(); - expect(doc).toBeInstanceOf(Object); - expect(doc.hid).toEqual(badHid); - expect(match).toBe(false); // no longer matches selector - expect(initial).toBeUndefined(); - expect(update).toBe(true); - } - }); - }); -}); - -describe("monitorQuery: collection content", () => { - // doctor and cache sample content - const fakeParentUrl = "/abc/def/ghi"; - const testContent = collectionContent.map((doc) => { - doc.parent_url = fakeParentUrl; - return doc; - }); - - describe("initial results", () => { - test("first monitor event has initial results reflecting existing matches in db", async () => { - // insert some stuff - const cachedDscContent = await bulkCacheDscContent(testContent, true); - const cachedContentIds = new Set(pluckAll(cachedDscContent, "_id")); - - // create new monitor - const limitIndex = 1; - const selector = { - parent_url: fakeParentUrl, - element_index: { $gt: limitIndex }, - }; - const monitor$ = of({ selector }).pipe( - monitorQuery({ db$: dscContent$ }), - takeUntil(timer(monitorSafetyTimeout)) - ); - - // wait til done - const spy = new ObserverSpy(); - monitor$.subscribe(spy); - await spy.onComplete(); - - // check emits - const emits = spy.getValues(); - expect(spy.getValuesLength()).toBeGreaterThan(0); - expect(spy.getValuesLength()).toBeLessThanOrEqual(cachedContentIds.size); - emits.forEach(({ doc, match, initial, update }) => { - expect(match).toBe(true); - expect(initial).toBe(true); - expect(update).toBeUndefined(); - expect(cachedContentIds.has(doc._id)).toBeTruthy(); - expect(doc.element_index > limitIndex).toBeTruthy(); - expect(doc.parent_url).toEqual(fakeParentUrl); - }); - }); - }); - - describe("updates", () => { - test("INSERT: adding to cache after instantiation should emit a new doc", async () => { - // createa monitor - const limitIndex = 0; - const selector = { - parent_url: fakeParentUrl, - element_index: { $gt: limitIndex }, - }; - const monitor$ = of({ selector }).pipe( - monitorQuery({ db$: dscContent$ }), - takeUntil(timer(monitorSafetyTimeout)) - ); - - const spy = new ObserverSpy(); - monitor$.subscribe(spy); - - // wait for the monitor to spin-up so the insert doesn't look like the initial vals - await wait(monitorSpinUp); - - // Insert a row, make sure it should match the selector - const insertedContent = await cacheCollectionContent(testContent[3], true); - expect(insertedContent.element_index > limitIndex).toBeTruthy(); - - // wait for observable end - await spy.onComplete(); - - const emits = spy.getValues(); - expect(spy.getValuesLength()).toEqual(1); - emits.forEach(({ doc, match, initial, update }) => { - expect(doc.element_index > limitIndex).toBeTruthy(); - expect(doc._id).toEqual(insertedContent._id); - expect(match).toBe(true); - expect(initial).toBeUndefined(); - expect(update).toBe(true); - }); - }); - - test("UPDATE: updating a previously emitted doc should emit an update event", async () => { - const limitIndex = 0; - - // Insert a row, make sure it should match the selector - const insertedContent = await cacheCollectionContent(testContent[1], true); - expect(insertedContent.element_index > limitIndex).toBeTruthy(); - - // create monitor - const selector = { - parent_url: fakeParentUrl, - element_index: { $gt: limitIndex }, - }; - const monitor$ = of({ selector }).pipe( - monitorQuery({ db$: dscContent$ }), - takeUntil(timer(monitorSafetyTimeout)) - ); - - const spy = new ObserverSpy(); - monitor$.subscribe(spy); - - // wait for the monitor to spin-up so the insert doesn't look like the initial vals - await wait(monitorSpinUp); - - // update the content - const fooVal = 123; - insertedContent.foo = fooVal; - const updatedContent = await cacheCollectionContent(insertedContent, true); - expect(updatedContent._id).toEqual(insertedContent._id); - expect(updatedContent.foo).toEqual(fooVal); - - // wait for observable end - await spy.onComplete(); - expect(spy.getValuesLength()).toEqual(2); // initial insert + later update - - // check first event - const firstEvent = spy.getValueAt(0); - { - const { doc, match, initial, update } = firstEvent; - expect(doc.foo).toBeUndefined(); - expect(match).toBe(true); - expect(initial).toBe(true); - expect(update).toBeUndefined(); - } - - // check update event - const secondEvent = spy.getValueAt(1); - { - const { doc, match, initial, update } = secondEvent; - expect(doc.foo).toBe(fooVal); - expect(match).toBe(true); - expect(initial).toBeUndefined(); - expect(update).toBe(true); - } - }); - - test("DELETE: cache update causes element to no longer be in selection", async () => { - const limitIndex = 0; - - // Insert a row, make sure it should match the selector - const insertedContent = await cacheCollectionContent(testContent[1], true); - expect(insertedContent.element_index > limitIndex).toBeTruthy(); - - // create monitor - const selector = { - parent_url: fakeParentUrl, - element_index: { $gt: limitIndex }, - }; - const monitor$ = of({ selector }).pipe( - monitorQuery({ db$: dscContent$ }), - takeUntil(timer(monitorSafetyTimeout)) - ); - - const spy = new ObserverSpy(); - monitor$.subscribe(spy); - - // update the content to be outside the selector - const badIndex = -10000; - insertedContent.element_index = badIndex; // no longer > cutoffHid - const updatedContent = await cacheCollectionContent(insertedContent, true); - expect(updatedContent._id).toEqual(insertedContent._id); - expect(updatedContent.element_index).toEqual(badIndex); - - // wait for observable end - await spy.onComplete(); - expect(spy.getValuesLength()).toEqual(2); // initial insert + later update - - // check first event - const firstEvent = spy.getValueAt(0); - { - const { doc, match, initial, update } = firstEvent; - expect(doc).toBeDefined(); - expect(doc).toBeInstanceOf(Object); - expect(doc.element_index).not.toEqual(badIndex); - expect(match).toBe(true); - expect(initial).toBe(true); - expect(update).toBeUndefined(); - } - - // check update event - const secondEvent = spy.getValueAt(1); - { - const { doc, match, initial, update } = secondEvent; - expect(doc).toBeDefined(); - expect(doc).toBeInstanceOf(Object); - expect(doc.element_index).toEqual(badIndex); - expect(match).toBe(false); // no longer matches selector - expect(initial).toBeUndefined(); - expect(update).toBe(true); - } - }); - }); -}); diff --git a/client/src/components/providers/History/caching/db/observables.js b/client/src/components/providers/History/caching/db/observables.js deleted file mode 100644 index 6e0f65f0cd1..00000000000 --- a/client/src/components/providers/History/caching/db/observables.js +++ /dev/null @@ -1,181 +0,0 @@ -/** - * Observable api for caching the galaxy history contents and collection contents - */ - -import config from "config"; -import { pipe } from "rxjs"; -import { map } from "rxjs/operators"; -import { collection, cacheItem, uncacheItem, getItemByKey, bulkCache } from "./pouch"; -import { contentIndices, dscIndices } from "./cacheIndices"; - -// helper operator that applies function to each element of sourced array -// prettier-ignore -const applyEach = (fn) => pipe( - map((list) => list.map(fn)) -); - -// pad zeros so ids sort properly -const zeroPad = (num, places) => String(num).padStart(places, "0"); - -/** - * History Content & associated operators - */ - -export const content$ = collection( - { - name: "galaxy-content", - indexes: contentIndices, - }, - config -); - -// prettier-ignore -export const getCachedContent = (key = "_id") => pipe( - getItemByKey(content$, key) -); - -// prettier-ignore -export const cacheContent = (returnDoc = false) => pipe( - map(prepContent), - cacheItem(content$, returnDoc), - map((result) => (returnDoc ? result.doc : result)) -); - -// prettier-ignore -export const uncacheContent = () => pipe( - uncacheItem(content$) -); - -// prettier-ignore -export const bulkCacheContent = (returnDocs = false) => pipe( - applyEach(prepContent), - bulkCache(content$, returnDocs), - map((list) => (returnDocs ? list.map((result) => result.doc) : list)) -); - -export const buildContentId = ({ history_id, hid }) => { - const paddedHid = zeroPad(hid, 12); - return `${history_id}-${paddedHid}`; -}; - -export const prepContent = (props) => { - const { history_content_type, id, type_id: origTypeId, ...theRest } = props; - const type_id = origTypeId ? origTypeId : `${history_content_type}-${id}`; - const _id = buildContentId(props); - - const doc = { - _id, - history_content_type, - id, - type_id, - ...theRest, - }; - - return fixDeleted(doc); -}; - -/** - * Collection content (drill down into a collection). Since we don't really - * update dsc content, all I think we need is the bulk operator which gets run - * when the scroller loads new data - */ - -export const dscContent$ = collection( - { - name: "galaxy-collection-content", - indexes: dscIndices, - }, - config -); - -// prettier-ignore -export const getCachedCollectionContent = (key = "_id") => pipe( - getItemByKey(dscContent$, key) -); - -// prettier-ignore -export const cacheCollectionContent = (returnDoc) => pipe( - map(prepDscContent), - cacheItem(dscContent$, returnDoc), - map((result) => (returnDoc ? result.doc : result)) -); - -// prettier-ignore -export const bulkCacheDscContent = (returnDocs = false) => pipe( - applyEach(prepDscContent), - bulkCache(dscContent$, returnDocs), - map((list) => (returnDocs ? list.map((result) => result.doc) : list)) -); - -// need to deal with both the bent api format that comes in from the ajax call -// as well as re-processing an existing collection item on a subsequent update -// cache operation. -export const prepDscContent = (rawProps) => { - // console.log("prepDscContent", rawProps); - - const { object = {}, ...rootProps } = rawProps; - const props = { ...rootProps, ...object }; - const { id, model_class, element_identifier, ...otherObjectFields } = props; - - // Validation - if (id === undefined) { - throw new Error("missing id"); - } - if (model_class === undefined) { - throw new Error("missing model_class"); - } - - const history_content_type = model_class == "HistoryDatasetAssociation" ? "dataset" : "dataset_collection"; - const type_id = `${history_content_type}-${id}`; - - const newProps = { - // parent content url + counter as that is the most likely query - _id: buildCollectionId(props), - - // make a type_id so we can re-use all our functions which depend on it - id, - type_id, - model_class, - history_content_type, - - // need to rename for cache indexing - name: element_identifier, - element_identifier, - - // move stuff out of "object" and into root of cached packet - ...otherObjectFields, - }; - - const clean = fixDeleted(newProps); - - return clean; -}; - -export const buildCollectionId = (props) => { - const { parent_url, element_index } = props; - - // this is not part of the returned api data, we need to provide it after the - // ajax call from the contents_url we got this from - if (undefined == parent_url) { - throw new Error("missing required parent_url"); - } - if (undefined == element_index) { - throw new Error("missing rquired element_index"); - } - const paddedIndex = zeroPad(element_index, 12); - return `${parent_url}-${paddedIndex}`; -}; - -/** - * We need to rename the deleted property because pouchDB uses that field, - * which is unfortunate - * @param {Object} props Raw document props - */ -const fixDeleted = (props) => { - // console.log(props); - if (Object.prototype.hasOwnProperty.call(props, "deleted")) { - const { deleted: isDeleted, ...theRest } = props; - return { isDeleted, ...theRest }; - } - return props; -}; diff --git a/client/src/components/providers/History/caching/db/pouch.js b/client/src/components/providers/History/caching/db/pouch.js deleted file mode 100644 index c76fa03c202..00000000000 --- a/client/src/components/providers/History/caching/db/pouch.js +++ /dev/null @@ -1,233 +0,0 @@ -/** - * Generic pouch db operators, These are low-level pouchdb access operators - * intended to be used to create more specific operators. - */ - -import moment from "moment"; -import deepEqual from "deep-equal"; -import { defer, pipe, from } from "rxjs"; -import { tap, filter, mergeMap, reduce, shareReplay } from "rxjs/operators"; -import { needs } from "utils/observable"; -import { dasherize } from "underscore.string"; - -import PouchDB from "pouchdb"; -import PouchAdapterMemory from "pouchdb-adapter-memory"; -import PouchUpsert from "pouchdb-upsert"; -import PouchFind from "pouchdb-find"; -import PouchErase from "pouchdb-erase"; -// import PouchDebug from "pouchdb-debug"; - -PouchDB.plugin(PouchAdapterMemory); -PouchDB.plugin(PouchUpsert); -PouchDB.plugin(PouchFind); -PouchDB.plugin(PouchErase); -// PouchDB.plugin(PouchDebug); - -// debugging stuff -// PouchDB.debug.enable('pouchdb:find'); -// const show = (obj) => console.log(JSON.stringify(obj, null, 4)); - -// Instance storage map, keyed by database name -export const dbs = new Map(); - -/** - * Generate an observable that initializes and shares a pouchdb instance. - * - * @param {object} options pouchdb initialization configs - * @return {Observable} observable that emits the pouch instance - */ -export const collection = (opts, appConfig) => { - return defer(() => { - const coll = buildCollection(opts, appConfig); - const name = collectionName(opts, appConfig); - return from(coll).pipe(tap((db) => dbs.set(name, db))); - }).pipe( - filter((db) => db instanceof PouchDB), - shareReplay(1) - ); -}; - -async function buildCollection(opts, appConfig) { - if (!opts) { - throw new Error("collection: Missing database config"); - } - if (!appConfig) { - throw new Error("collection: Missing application config"); - } - - const name = collectionName(opts, appConfig); - // eslint-disable-next-line no-unused-vars - const { indexes = [], ...otherOpts } = opts; - const dbConfig = { ...appConfig.caching, ...otherOpts, name }; - - // make instance - const db = await new PouchDB(dbConfig); - - // pre-install common indexes - await installCollectionIndexes(db, indexes); - - return db; -} - -function collectionName(opts, appConfig) { - const { name: dbName } = opts; - const { name: envName } = appConfig; - return dasherize(`${dbName} ${envName}`); -} - -async function installCollectionIndexes(db, indexes = []) { - const promises = indexes.map((idx) => db.createIndex(idx)); - if (promises.length) { - await Promise.all(promises); - } - return db; -} - -/** - * Retrieves an object from the cache - * - * @param {Observable} db$ Observable of a pouchdb instance - * @param {string} keyName Field to lookup object by - * @returns {Function} Observable operator - */ -// prettier-ignore -export const getItemByKey = (db$, keyName = "_id") => pipe( - needs(db$), - mergeMap(async ([keyValue, db]) => { - let doc = null; - if (keyName == "_id") { - doc = await getDocByDocId(db, keyValue); - } else { - doc = await getDocByKeyValue(db, keyName, keyValue); - } - return doc; - }) -); - -async function getDocByDocId(db, docId) { - let doc = null; - try { - doc = await db.get(docId); - } catch (err) { - // pouch throws a 404 when a .get doesn't find anything, - // which in my mind is a perfectly legitimate result, so - // we're returning a null here, otherwise rethrow - const { status } = err; - if (status !== 404) { - throw err; - } - } - return doc; -} - -async function getDocByKeyValue(db, keyName, keyValue) { - let doc = null; - - // otherwise do a find by keyname - const searchConfig = { - selector: { [keyName]: keyValue }, - limit: 1, - }; - - const response = await db.find(searchConfig); - if (response.warning) { - console.warn(response.warning, searchConfig); - } - - const { docs } = response; - if (docs && docs.length > 0) { - doc = docs[0]; - } - - return doc; -} - -/** - * Operator that caches all source documents in the indicated collection. - * Upserts new fields into the old document, so the props need not be complete, - * just needs to have the _id. - * - * Adds a cached_at timestamp and fixes pouchdb specific field names like - * "deleted" - * - * @source Observable stream of docs to cache - * @param {Observable} db$ Observable pouchdb instance - * @returns {Function} Observable operator - */ -// prettier-ignore -export const cacheItem = (db$, returnDoc = false) => pipe( - needs(db$), - mergeMap(async ([item, db]) => { - - const result = await db.upsert(item._id, (existing) => { - // eslint-disable-next-line no-unused-vars - const { _rev, cached_at, ...existingFields } = existing; - - // ignore if what we're caching is the same as what's in there - const same = deepEqual(item, existingFields); - if (same) {return false;} - - return { - ...existing, - ...item, - cached_at: moment().valueOf(), - }; - }); - - if (returnDoc) { - result.doc = await getDocByDocId(db, result.id); - } - - return result; - }) -); - -/** - * Doing it the dumb way for now, will try a true bulkDocs call later. - * - * @source Observable of docs to cache - * @param {Observable} db$ Observable of a PouchDB instance - * @returns {Function} Observable operator - */ -// prettier-ignore -export const bulkCache = (db$, returnDocs = false) => pipe( - mergeMap((list) => - from(list).pipe( - cacheItem(db$, returnDocs), - reduce((result, item) => { - result.push(item); - return result; - }, []) - ) - ) -); - -/** - * Creates an operator that will delete the source document from the configured - * database observable. In pouchdb, deleting just means setting _deleted to - * true. - * - * @source Observable stream of documents to uncache - * @param {Observable} db$ Observable of the pouchdb instance - * @returns {Function} Observable operator - */ -// prettier-ignore -export const uncacheItem = (db$) => pipe( - needs(db$), - mergeMap(([doomedDoc, db]) => { - return db.remove(doomedDoc); - }) -); - -/** - * Delete existing indexes in a pouchdb database. - * - * @param {PouchDB} db pouch db instance - * @returns {Promise} - */ -export async function deleteIndexes(db) { - const response = await db.getIndexes(); - const doomedIndexes = response.indexes.filter((idx) => idx.ddoc !== null); - const promises = doomedIndexes.map((idx) => db.deleteIndex(idx)); - return await Promise.all(promises); -} diff --git a/client/src/components/providers/History/caching/db/promises.js b/client/src/components/providers/History/caching/db/promises.js deleted file mode 100644 index fd4cfbedfee..00000000000 --- a/client/src/components/providers/History/caching/db/promises.js +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Same functionality as exposed in observables, but in promise form for one-off - * utilities. A little easier to use if all you need to do is one thing. - * However, for more complicated processing, I do recommend making the attempt - * work in streams as you will have a lot more control over the throughput. - */ - -import { of } from "rxjs"; -import { firstValueFrom } from "utils/observable/firstValueFrom"; - -import { - content$, - cacheContent as cacheContentOp, - getCachedContent as getCachedContentOp, - uncacheContent as uncacheContentOp, - bulkCacheContent as bulkCacheContentOp, - bulkCacheDscContent as bulkCacheDscContentOp, - getCachedCollectionContent as getCachedCollectionContentOp, - cacheCollectionContent as cacheCollectionContentOp, -} from "./observables"; - -import { find } from "./find"; - -// result is an object pouch returns with the ID a rev, and whether it was -// actually updated or not (won't be if identical) -export const cacheContent = (rawProps, returnDoc) => firstValueFrom(of(rawProps).pipe(cacheContentOp(returnDoc))); -export const getCachedContent = (docId) => firstValueFrom(of(docId).pipe(getCachedContentOp())); -export const uncacheContent = (doc) => firstValueFrom(of(doc).pipe(uncacheContentOp())); -export const bulkCacheContent = (list, returnDocs) => firstValueFrom(of(list).pipe(bulkCacheContentOp(returnDocs))); - -// collection content, collections are supposedly immutable after creation so -// all we really have is a bulk-load -export const cacheCollectionContent = (rawProps, returnDoc) => - firstValueFrom(of(rawProps).pipe(cacheCollectionContentOp(returnDoc))); -export const getCachedCollectionContent = (docId) => firstValueFrom(of(docId).pipe(getCachedCollectionContentOp())); -export const bulkCacheDscContent = (list, returnDocs) => - firstValueFrom(of(list).pipe(bulkCacheDscContentOp(returnDocs))); - -// examples of how to fine-tune these to get specialized functionality -export const getContentByTypeId = async (history_id, type_id) => { - const selector = { history_id, type_id }; - const index = { fields: ["history_id", "type_id"] }; - const request = { selector, index, limit: 1 }; - const obs$ = of(request).pipe(find(content$)); - const results = await firstValueFrom(obs$); - return results.length > 0 ? results[0] : null; -}; diff --git a/client/src/components/providers/History/caching/db/wipeDatabase.js b/client/src/components/providers/History/caching/db/wipeDatabase.js deleted file mode 100644 index 0e5c5b6b2dc..00000000000 --- a/client/src/components/providers/History/caching/db/wipeDatabase.js +++ /dev/null @@ -1,10 +0,0 @@ -import { dbs } from "./pouch"; - -/** - * Erases all stored database instances - */ -export async function wipeDatabase() { - for (const db of dbs.values()) { - await db.erase(); - } -} diff --git a/client/src/components/providers/History/caching/enums.js b/client/src/components/providers/History/caching/enums.js deleted file mode 100644 index b99f163f1a5..00000000000 --- a/client/src/components/providers/History/caching/enums.js +++ /dev/null @@ -1,9 +0,0 @@ -// seek types - -const seeks = { ASC: "asc", DESC: "desc" }; - -seeks.isValid = (val) => { - return Object.values(SEEK).some((v) => v == val); -}; - -export const SEEK = seeks; diff --git a/client/src/components/providers/History/caching/index.js b/client/src/components/providers/History/caching/index.js deleted file mode 100644 index b913e6f3ba0..00000000000 --- a/client/src/components/providers/History/caching/index.js +++ /dev/null @@ -1,18 +0,0 @@ -// Exposes worker functions as promises or Observable operators - -export { monitorContentQuery, monitorDscQuery, monitorHistoryContent, monitorCollectionContent } from "./CacheApi"; - -export { loadHistoryContents, loadDscContent } from "./CacheApi"; - -export { - cacheContent, - getCachedContent, - uncacheContent, - bulkCacheContent, - cacheCollectionContent, - getCachedCollectionContent, - bulkCacheDscContent, - getContentByTypeId, -} from "./CacheApi"; - -export { wipeDatabase, clearHistoryDateStore } from "./CacheApi"; diff --git a/client/src/components/providers/History/caching/loadDscContent.js b/client/src/components/providers/History/caching/loadDscContent.js deleted file mode 100644 index 249c49738b8..00000000000 --- a/client/src/components/providers/History/caching/loadDscContent.js +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Manual loading operators for content lists. These get run as the user scrolls - * or filters data in the listing for immediate lookups. All of them will run - * one or more ajax calls against the api and cache ther results. - */ -import { map, pluck, withLatestFrom, publish } from "rxjs/operators"; -import { nth } from "utils/observable"; -import { requestWithUpdateTime } from "./operators/requestWithUpdateTime"; -import { bulkCacheDscContent } from "./db"; -import { SearchParams } from "components/providers/History/SearchParams"; -import { prependPath } from "utils/redirect"; -import { summarizeCacheOperation, dateStore } from "./loadHistoryContents"; -import { show } from "utils/observable"; - -/** - * Load collection content (drill down) - * Params: contents_url + search params + element_index - */ -export const loadDscContent = (cfg = {}) => { - const { debug = false } = cfg; - - return publish((inputs$) => { - const url$ = inputs$.pipe(nth(0)); - - return inputs$.pipe( - map(buildDscContentUrl), - map(prependPath), - show(debug, (url) => console.log("Sending collection request:", url)), - requestWithUpdateTime({ dateStore }), - pluck("response"), - withLatestFrom(url$), - map(([rows, parent_url]) => { - return rows.map((row) => ({ ...row, parent_url })); - }), - bulkCacheDscContent(), - summarizeCacheOperation() - ); - }); -}; - -// Collection + params -> request url w/o update_time -// ignore params for now, we don't filter collection contents yet. -const buildDscContentUrl = (inputs) => { - const [base, , pagination] = inputs; - const { offset = 0, limit = SearchParams.pageSize } = pagination; - const skipClause = offset ? `offset=${offset}` : ""; - const limitClause = limit ? `limit=${limit}` : ""; - const qs = [skipClause, limitClause].filter((o) => o.length).join("&"); - return `${base}?${qs}`; -}; diff --git a/client/src/components/providers/History/caching/loadHistoryContents.js b/client/src/components/providers/History/caching/loadHistoryContents.js deleted file mode 100644 index 28eaae5248f..00000000000 --- a/client/src/components/providers/History/caching/loadHistoryContents.js +++ /dev/null @@ -1,143 +0,0 @@ -import { zip } from "rxjs"; -import { map, pluck, share, filter } from "rxjs/operators"; -import { hydrate } from "utils/observable"; -import { areDefined } from "utils/validation"; -import { requestWithUpdateTime } from "./operators/requestWithUpdateTime"; -import { prependPath } from "utils/redirect"; -import { bulkCacheContent } from "./db"; -import { SearchParams } from "components/providers/History/SearchParams"; -import { createDateStore } from "components/History/model/DateStore"; - -// Shared datestore for the request opertor for both loading and polling -export const dateStore = createDateStore(); - -export const clearHistoryDateStore = async () => { - console.log("clearHistoryDateStore"); - dateStore.clear(); -}; - -/** - * Turn [historyId, params, pagination] into content requests. Cache results and send - * back statistics about what was returned for this query. - */ -// prettier-ignore -export const loadHistoryContents = (cfg = {}) => (rawInputs$) => { - const { - noInitial = false, - windowSize = SearchParams.pageSize - } = cfg; - - const inputs$ = rawInputs$.pipe( - hydrate([undefined, SearchParams]), - ); - - const responseQualifier = ajaxResponse => ajaxResponse.status == 200 && ajaxResponse.response.length > 0; - - const ajaxResponse$ = inputs$.pipe( - map(([id, params, hid]) => { - const baseUrl = `/api/histories/${id}/contents/near/${hid}/${windowSize}`; - return `${baseUrl}?${params.historyContentQueryString}`; - }), - map(prependPath), - requestWithUpdateTime({ dateStore, noInitial, responseQualifier, dateFieldName: "since" }), - ); - - const validResponses$ = ajaxResponse$.pipe( - filter(response => response.status == 200), - share(), - ); - - const cacheSummary$ = validResponses$.pipe( - pluck("response"), - bulkCacheContent(), - summarizeCacheOperation(), - ); - - return zip(validResponses$, cacheSummary$).pipe( - map(([ajaxResponse, summary]) => { - const { xhr, response = [] } = ajaxResponse; - const { max: maxContentHid, min: minContentHid } = getPropRange(response, "hid"); - - // return an int or undefined if the field does not exist in the headers - const headerInt = field => { - const raw = xhr.getResponseHeader(field); - return raw === null ? undefined : parseInt(raw); - }; - - const headerBool = field => { - const raw = xhr.getResponseHeader(field); - return (raw == "true" || raw == "1"); - } - - // header counts - const matchesUp = headerInt("matches_up"); - const matchesDown = headerInt("matches_down"); - const totalMatchesUp = headerInt("total_matches_up"); - const totalMatchesDown = headerInt("total_matches_down"); - const minHid = headerInt("min_hid"); - const maxHid = headerInt("max_hid"); - const historySize = headerInt("history_size"); - const historyEmpty = headerBool("history_empty"); - - const matches = areDefined(matchesUp, matchesDown) ? matchesUp + matchesDown : undefined; - const totalMatches = areDefined(totalMatchesUp, totalMatchesDown) ? totalMatchesUp + totalMatchesDown : undefined; - - return { - summary, - matches, - totalMatches, - minHid, - maxHid, - minContentHid, // minimum hid in the returned result - maxContentHid, // maximum hid in the returned result - - matchesUp, - matchesDown, - totalMatchesUp, - totalMatchesDown, - - // new history size - historySize, - historyEmpty - }; - }) - ); -}; - -/** - * Once data was cached, there's no need to send everything back over into the - * main thread since the cache watcher will pick up those new values. This just - * summarizes what pouchdb did during the bulk cache and sends back some stats. - */ -// prettier-ignore -export const summarizeCacheOperation = () => { - return map((list) => { - const cached = list.filter((result) => result.updated || result.ok); - return { - updatedItems: cached.length, - totalReceived: list.length, - }; - }) -} - -/** - * Gets min and max values from an array of objects - * - * @param {Array} list Array of objects, each with a propName - * @param {string} propName Name of prop to measure range - */ -export const getPropRange = (list, propName) => { - const narrowRange = (range, row) => { - const val = parseInt(row[propName], 10); - range.max = Math.max(range.max, val); - range.min = Math.min(range.min, val); - return range; - }; - - const everywhere = { - min: Infinity, - max: -Infinity, - }; - - return list.reduce(narrowRange, everywhere); -}; diff --git a/client/src/components/providers/History/caching/monitorHistoryContent.js b/client/src/components/providers/History/caching/monitorHistoryContent.js deleted file mode 100644 index 28599c9e263..00000000000 --- a/client/src/components/providers/History/caching/monitorHistoryContent.js +++ /dev/null @@ -1,190 +0,0 @@ -import { merge } from "rxjs"; -import { map, distinctUntilChanged, publish, withLatestFrom, share } from "rxjs/operators"; -import { content$, dscContent$, buildContentId, buildCollectionId } from "./db/observables"; -import { monitorQuery } from "./db/monitorQuery"; -import { SearchParams } from "components/providers/History/SearchParams"; -import { deepEqual } from "deep-equal"; -import { hydrate } from "utils/observable"; -import { SEEK } from "./enums"; -import { matchesSelector } from "pouchdb-selector-core"; - -// history contents monitor -export const monitorHistoryContent = (cfg = {}) => { - return twoWayMonitor({ - ...cfg, - db$: content$, - buildRequest: buildContentPouchRequest, - aggregationKeyField: "hid", - }); -}; - -// collection contents monitor -export const monitorCollectionContent = (cfg = {}) => { - return twoWayMonitor({ - ...cfg, - db$: dscContent$, - buildRequest: buildCollectionPouchRequest, - aggregationKeyField: "element_index", - }); -}; - -/** - * Search cache upward and downward from the scroll HID, filtering out results - * which do not match params, we don't want to return the entire result set for - * 2 reasons: it might be huge, and we might not yet have large sections of the - * contents if the user is rapidly dragging the scrollbar to different regions - */ -// prettier-ignore -export const twoWayMonitor = (cfg = {}) => (src$) => { - const { - db$, - aggregationKeyField, - buildRequest = buildContentPouchRequest, - pageSize = SearchParams.pageSize, - inputDebounce = 100, - debug = false, - } = cfg; - - return src$.pipe( - hydrate([undefined, SearchParams]), - publish(input$ => { - - const upRequest$ = input$.pipe( - map(buildRequest({ seek: SEEK.ASC, pageSize })), - distinctUntilChanged(deepEqual), - share(), - ); - - const downRequest$ = input$.pipe( - map(buildRequest({ seek: SEEK.DESC, pageSize })), - distinctUntilChanged(deepEqual), - share(), - ); - - // one page up - const up$ = upRequest$.pipe( - monitorQuery({ db$, inputDebounce, debug, label: "up" }), - alsoMatchWith(downRequest$), - ); - - // current page + next page = 2 pages down - const down$ = downRequest$.pipe( - monitorQuery({ db$, inputDebounce, debug, label: "down" }), - alsoMatchWith(upRequest$), - ); - - return merge(up$, down$) - }), - map((change) => { - // don't bother sending back non-matching results - // we're just going to delete them from the skiplist - // using the aggregation key, so no need to serialize them - const { match, doc, ...more} = change; - const key = doc[aggregationKeyField]; - return match ? { key, match, doc, ...more } : { key, match, ...more }; - }) - ); -}; - -// utility operator, checks to see if change matches with the other selector -const alsoMatchWith = (req$) => (change$) => { - // As the scroller moves, a given row may no longer match in one query - // but it may in the other, simply because it has moved from the "up" results - // to the "down" results - - return change$.pipe( - withLatestFrom(req$), - map(([change, otherRequest]) => { - const result = { ...change }; - result.match = change.match || matchesSelector(result.doc, otherRequest.selector); - return result; - }) - ); -}; - -/** - * Generate a PouchDB selector for a specified history, filters, and a rough - * guess of where to start looking. - * - * Can't use skip because there might be big un-cached regions of the history - * and we need to be able to select without loading everything - */ -export const buildContentPouchRequest = - (cfg = {}) => - (inputs) => { - const { seek, pageSize = SearchParams.pageSize } = cfg; - const [history_id, params, hid] = inputs; - - // look up or down from target hid - const targetId = buildContentId({ history_id, hid }); - - // SEEK.ASC means the top seek, HID > target - const comparator = seek == SEEK.ASC ? "$gt" : "$lte"; - - // one page above the target, then 2 after to get the current page and some buffer - const pages = seek == SEEK.ASC ? 1 : 2; - const limit = pages * pageSize; - - // index, will build if not existent - const filterFields = Array.from(params.criteria.keys()).map((f) => params.getPouchFieldName(f)); - const fields = ["_id", "history_id", ...filterFields]; - const ddoc = "idx-" + fields.sort().join("-"); - - const request = { - selector: { - $and: [ - // doc id + direction (up/down) - { _id: { [comparator]: targetId } }, - { history_id: history_id }, - ...params.pouchFilters, - ], - }, - sort: [{ _id: seek }], - limit, - index: { - fields, - ddoc, - }, - }; - - return request; - }; - -// filters currently unused, but they could be if we add some filter UI -export const buildCollectionPouchRequest = - (cfg = {}) => - (inputs) => { - const { seek, pageSize = SearchParams.pageSize } = cfg; - - // eslint-disable-next-line no-unused-vars - const [parent_url, filters, element_index] = inputs; - const targetId = buildCollectionId({ parent_url, element_index }); - - // SEEK.ASC means above the target means element_index < target - const comparator = seek == SEEK.ASC ? "$lt" : "$gte"; - - // 3 pages total, 1 before, the current one, 1 after; - const pages = seek == SEEK.ASC ? 1 : 2; - const limit = pages * pageSize; - - // when searching upward we invert the sort because we want the - // results closest to the target - const idSort = seek == SEEK.ASC ? "desc" : "asc"; - - const request = { - selector: { - _id: { [comparator]: targetId }, - parent_url: { $eq: parent_url }, - }, - sort: [{ _id: idSort }], - limit, - index: { - fields: ["_id"], - sort: [{ _id: "asc" }], - name: "collection contents by parent_url and element_index ascending", - ddoc: "idx-dsc-contents-by-url-and-element-asc", - }, - }; - - return request; - }; diff --git a/client/src/components/providers/History/caching/monitorHistoryContent.test.js b/client/src/components/providers/History/caching/monitorHistoryContent.test.js deleted file mode 100644 index 64fbc309525..00000000000 --- a/client/src/components/providers/History/caching/monitorHistoryContent.test.js +++ /dev/null @@ -1,122 +0,0 @@ -import { timer, of } from "rxjs"; -import { takeUntil } from "rxjs/operators"; -import { firstValueFrom } from "utils/observable/firstValueFrom"; -import { wipeDatabase } from "./db/wipeDatabase"; -import { wait } from "jest/helpers"; -import { ObserverSpy } from "@hirez_io/observer-spy"; - -import { SearchParams } from "components/providers/History/SearchParams"; -import { content$ } from "./db/observables"; -import { bulkCacheContent, cacheContent } from "./db/promises"; -import { find } from "./db/find"; -import { buildContentPouchRequest, monitorHistoryContent } from "./monitorHistoryContent"; - -// test data -import historyContent from "components/providers/History/test/json/historyContent.json"; - -jest.mock("app"); -jest.mock("../caching"); - -beforeEach(wipeDatabase); -afterEach(wipeDatabase); - -const monitorSpinUp = 400; -const monitorSafetyTimeout = 1000; -const selectorHasField = (selector, field) => selector.$and.some((row) => row[field] !== undefined); - -describe("buildContentPouchRequest", () => { - test("should turn inputs into a pouch request suitable for find", () => { - const fn = buildContentPouchRequest(); - expect(fn).toBeInstanceOf(Function); - - const { history_id, hid } = historyContent[0]; - const inputs = [history_id, new SearchParams(), hid]; - const request = fn(inputs); - - expect(request).toBeDefined(); - expect(request.selector).toBeDefined(); - expect(request.selector.$and).toBeDefined(); - expect(selectorHasField(request.selector, "_id")).toBeTruthy(); - expect(selectorHasField(request.selector, "history_id")).toBeTruthy(); - }); -}); - -describe("monitorHistoryContent", () => { - let cachedContentMap; - - // cache sample content, store in a map for later, keyed by HID - beforeEach(async () => { - const cachedContent = await bulkCacheContent(historyContent, true); - const cachedContentEntries = cachedContent.map((o) => [o.hid, o]); - cachedContentMap = new Map(cachedContentEntries); - }); - - afterEach(() => (cachedContentMap = null)); - - test("sanity check all the data is in there", async () => { - const { history_id } = historyContent[0]; - const request = { selector: { history_id } }; - const obs$ = of(request).pipe(find(content$)); - const result = await firstValueFrom(obs$); - expect(result.length).toEqual(historyContent.length); - }); - - test("should see the pre-inserted content if params match", async () => { - const { history_id, hid } = historyContent[0]; - const monitor$ = of([history_id, new SearchParams(), hid]).pipe( - monitorHistoryContent({ - inputDebounce: 50, - pageSize: 10, // get everything without pagination for now - }), - takeUntil(timer(monitorSafetyTimeout)) // safety - ); - - const spy = new ObserverSpy(); - monitor$.subscribe(spy); - await spy.onComplete(); - expect(spy.getValuesLength()).toBeGreaterThan(0); - - spy.getValues().forEach((evt) => { - const { key, match, doc } = evt; - expect(match).toBe(true); - expect(doc.hid).toEqual(key); - expect(cachedContentMap.has(key)).toBe(true); - // we had default params - expect(doc.isDeleted).toBe(false); - expect(doc.visible).toBe(true); - }); - }); - - test("should see subsequent updates", async () => { - const firstDoc = historyContent[0]; - const { history_id, hid } = firstDoc; - const inputs = [history_id, new SearchParams(), hid]; - - const monitor$ = of(inputs).pipe( - monitorHistoryContent({ inputDebounce: 50 }), - takeUntil(timer(monitorSafetyTimeout)) // safety - ); - - // monitor observable - const spy = new ObserverSpy(); - monitor$.subscribe(spy); - - // wait for first emission before updating, this lets all - // the initial matches emit - await wait(monitorSpinUp); - - // update one doc - const firstHid = historyContent[0].hid; - const updateMe = cachedContentMap.get(firstHid); - const fakeVal = 123; - updateMe.foobar = fakeVal; - const updatedContent = await cacheContent(updateMe, true); - expect(updatedContent.foobar).toEqual(fakeVal); - - // wait until monitor completes - await spy.onComplete(); - - const lastEmit = spy.getLastValue(); - expect(lastEmit.doc.foobar).toEqual(fakeVal); - }); -}); diff --git a/client/src/components/providers/History/caching/operators/hydrate.js b/client/src/components/providers/History/caching/operators/hydrate.js deleted file mode 100644 index 01212726423..00000000000 --- a/client/src/components/providers/History/caching/operators/hydrate.js +++ /dev/null @@ -1,16 +0,0 @@ -import { pipe } from "rxjs"; -import { map } from "rxjs/operators"; - -/** - * passing objects into the worker removes class information, Pass in an array - * of constructors to be assigned, in order of the input array. - */ -// prettier-ignore -export const hydrate = (constructors = []) => pipe( - map((inputs) => { - return inputs.map((rawInput, i) => { - const C = constructors[i]; - return C ? new C(rawInput) : rawInput; - }); - }) -) diff --git a/client/src/components/providers/History/caching/operators/lastCachedDate.js b/client/src/components/providers/History/caching/operators/lastCachedDate.js deleted file mode 100644 index df2537daae0..00000000000 --- a/client/src/components/providers/History/caching/operators/lastCachedDate.js +++ /dev/null @@ -1,36 +0,0 @@ -import moment from "moment"; -import { pipe } from "rxjs"; -import { map } from "rxjs/operators"; -import { find } from "../db/find"; - -/** - * Returns latest cache date from the cache. - * - * This seemingly simple calculation is complicated because the galaxy API - * currently returns update_times that are more precise than javascript dates. - * - * For example. The server will return an update_time as: - * 2020-07-02T17:25:09.385026 - * - * When parsing a javascript date, however, we don't have that many decimals. - * 2020-07-02T17:25:09Z - * - * This means that trying to perform an inequality filter: - * update_time-gt=2020-07-02T17:25:09Z - * - * ...will always fail because of those extra fractions of a millisecond. And - * given the granularity of the date storage it is not safe to "just add one - * more" to the outgoing value, given that there may indeed be lost records if we do that. - * - * source stream: pouchdb-find query config - */ -// prettier-ignore -export const lastCachedDate = (db$) => pipe( - find(db$), - map((docs) => { - if (!docs.length) {return null;} - const dates = docs.map((d) => d.cached_at); - const maxDate = Math.max(...dates); - return moment.utc(maxDate).toISOString(); - }) -) diff --git a/client/src/components/providers/History/caching/operators/needs.js b/client/src/components/providers/History/caching/operators/needs.js deleted file mode 100644 index 3900a76f90e..00000000000 --- a/client/src/components/providers/History/caching/operators/needs.js +++ /dev/null @@ -1,12 +0,0 @@ -// I'm actually unclear on why I needed this operator to make pouchdb -// observables work. This bears further investigation. To my knowledge -// withLatestFrom() does the same thing as this operator, yet this works and -// withLatestFrom() doesn't when used with pouchdb observable creation. - -import { zip, of, pipe } from "rxjs"; -import { concatMap } from "rxjs/operators"; - -// prettier-ignore -export const needs = (dep$) => pipe( - concatMap((src) => zip(of(src), dep$)) -); diff --git a/client/src/components/providers/History/caching/operators/requestWithUpdateTime.js b/client/src/components/providers/History/caching/operators/requestWithUpdateTime.js deleted file mode 100644 index a3af2891d91..00000000000 --- a/client/src/components/providers/History/caching/operators/requestWithUpdateTime.js +++ /dev/null @@ -1,71 +0,0 @@ -import moment from "moment"; -import { of, pipe } from "rxjs"; -import { tap, map, mergeMap } from "rxjs/operators"; -import { ajax } from "rxjs/ajax"; -import { createDateStore } from "components/History/model/DateStore"; - -/** - * Global url date-store, keeps track of the last time a specific url was - * requested Can reset the update_time tracking by passing in a new datestore to - * the operator configs - */ -export const requestDateStore = createDateStore("requestWithUpdateTime default"); - -/*** - * Check datestore to get the last time we did this request. Append - * an update_time clause at the end of the GET url so we only take - * the fresh updates. Mark the time after the request for future requests - */ -// prettier-ignore -export const requestWithUpdateTime = (config = {}) => { - const { - dateStore = requestDateStore, - bufferSeconds = 0, - dateFieldName = "update_time-gt", - requestTime = moment.utc(), - // indicates we don't want initial results - noInitial = false, - responseQualifier = (response) => response.status == 200 - } = config; - - // mark and flag this update-time, append to next request with same base - return pipe( - tap((url) => { - if (noInitial && !dateStore.has(url)) { - dateStore.set(url, moment.utc()); - } - }), - mergeMap((baseUrl) => of(baseUrl).pipe( - appendUpdateTime({ dateStore, bufferSeconds, dateFieldName }), - mergeMap(ajax), - tap((response) => { - if (responseQualifier(response)) { - dateStore.set(baseUrl, requestTime) - } - }) - )) - ); -}; - -/** - * Takes a base URL appends an update_time-gt restriction based on the lst - * time this URL was requestd as indicated by the dateStore. - * (Async in case we choose to store the date in indexDb instead of localStorage) - */ -// prettier-ignore -const appendUpdateTime = (cfg = {}) => { - const { - dateStore = requestDateStore, - dateFieldName = "update_time-gt", - } = cfg; - - return pipe( - map((baseUrl) => { - if (!dateStore.has(baseUrl)) {return baseUrl;} - const lastRequest = dateStore.get(baseUrl); - const parts = [baseUrl, `${dateFieldName}=${lastRequest.toISOString()}`]; - const separator = baseUrl.includes("?") ? "&" : "?"; - return parts.join(separator); - }) - ); -}; diff --git a/client/src/components/providers/History/caching/operators/throttleDistinct.js b/client/src/components/providers/History/caching/operators/throttleDistinct.js deleted file mode 100644 index 8db11375dff..00000000000 --- a/client/src/components/providers/History/caching/operators/throttleDistinct.js +++ /dev/null @@ -1,32 +0,0 @@ -// same behavior as as utils/observable/throttleDistinct, -// but uses external state storage so we can put it in nested switchMaps, etc. - -import moment from "moment"; -import { pipe } from "rxjs"; -import { filter } from "rxjs/operators"; -import { createDateStore } from "../../model/DateStore"; - -export const throttleDistinctDateStore = createDateStore("throttleDistinct default"); - -// prettier-ignore -export const throttleDistinct = (config = {}) => { - const { - timeout = 1000, - dateStore = throttleDistinctDateStore - } = config; - - return pipe( - filter((val) => { - const now = moment(); - let ok = true; - if (dateStore.has(val)) { - const lastRequest = dateStore.get(val); - ok = now - lastRequest > timeout; - } - if (ok) { - dateStore.set(val, now); - } - return ok; - }) - ); -}; diff --git a/client/src/components/providers/History/index.js b/client/src/components/providers/History/index.js deleted file mode 100644 index a9b8b60b39c..00000000000 --- a/client/src/components/providers/History/index.js +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Providers are renderless components that take raw inputs and monitor the - * cache or other observables to emit end results as slot props. - */ - -// These are pretty complex. Input parameters from "the top" are the History or -// selected collection, but they also expose slot-prop methods to update -// internal data such as user-provided filter parameters or the physical -// position of the scroll cursor in the list UI -export { default as HistoryContentProvider } from "./HistoryContentProvider"; -export { default as CollectionContentProvider } from "./CollectionContentProvider"; - -// The DscProvider is only complex because we store dataset collection data in -// two places depending on its place in the data hierarchy. At the root level, a -// dataset collection is stored as history content. But a collection can nest -// other collections, and sub-collections are stored as collection-content -export { default as DscProvider } from "./DscProvider"; - -// Management for current user's histories. Largely a passthrough of store methods -export { default as UserHistories } from "./UserHistories"; diff --git a/client/src/components/providers/History/test/json/History.json b/client/src/components/providers/History/test/json/History.json deleted file mode 100644 index 95cf20b9cda..00000000000 --- a/client/src/components/providers/History/test/json/History.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "importable": false, - "username_and_slug": null, - "hid_counter": 101, - "contents_url": "/api/histories/2f94e8ae9edff68a/contents", - "annotation": "this is the annotation", - "genome_build": null, - "create_time": "2020-06-22T20:24:57.630416", - "empty": false, - "purged": false, - "update_time": "2020-07-07T21:03:42.514499", - "user_id": "f2db41e1fa331b3e", - "name": "160-ish items", - "slug": null, - "tags": [], - "published": false, - "nice_size": "708 bytes", - "size": 708, - "url": "/api/histories/2f94e8ae9edff68a", - "deleted": false, - "state": "ok", - "id": "2f94e8ae9edff68a" -} diff --git a/client/src/components/providers/History/test/json/collectionContent.json b/client/src/components/providers/History/test/json/collectionContent.json deleted file mode 100644 index 66dcfbd45a6..00000000000 --- a/client/src/components/providers/History/test/json/collectionContent.json +++ /dev/null @@ -1,842 +0,0 @@ -[ - { - "model_class": "DatasetCollectionElement", - "id": "068bb135123e39c3", - "element_type": "hda", - "element_index": 0, - "element_identifier": "0", - "object": { - "id": "e746d19c9c8cc71c", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "cc0e2ba29cc1d8af", - "element_type": "hda", - "element_index": 1, - "element_identifier": "1", - "object": { - "id": "bd2618511bd6d954", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "088766ba409c97ae", - "element_type": "hda", - "element_index": 2, - "element_identifier": "10", - "object": { - "id": "b1aecce06b45adc8", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "e48823c5da673276", - "element_type": "hda", - "element_index": 3, - "element_identifier": "100", - "object": { - "id": "01cda2950d5e63c3", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "cf17043882bf0911", - "element_type": "hda", - "element_index": 4, - "element_identifier": "1000", - "object": { - "id": "5f2de0d744e8c295", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "a088eb6d1a3dbcab", - "element_type": "hda", - "element_index": 5, - "element_identifier": "10000", - "object": { - "id": "43bce5ef002caad3", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "35cddac7ff46d42d", - "element_type": "hda", - "element_index": 6, - "element_identifier": "100000", - "object": { - "id": "5fb23b4e4319a7c5", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "eac6cd9007b1908f", - "element_type": "hda", - "element_index": 7, - "element_identifier": "100001", - "object": { - "id": "80233f5f28a6c72f", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "aa4f9dc4708b54b6", - "element_type": "hda", - "element_index": 8, - "element_identifier": "100002", - "object": { - "id": "1c5156c6a0363f35", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "cbd3da2c4f117e14", - "element_type": "hda", - "element_index": 9, - "element_identifier": "100003", - "object": { - "id": "af091cf96ab275f3", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "68a588a2a9524bc5", - "element_type": "hda", - "element_index": 10, - "element_identifier": "100004", - "object": { - "id": "97b2c68eeabafb3d", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "a58d0afa3c515242", - "element_type": "hda", - "element_index": 11, - "element_identifier": "100005", - "object": { - "id": "33867d668bd83fde", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "f0c8870154edf2d6", - "element_type": "hda", - "element_index": 12, - "element_identifier": "100006", - "object": { - "id": "1914213279532687", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "018c8c5d62a8df97", - "element_type": "hda", - "element_index": 13, - "element_identifier": "100007", - "object": { - "id": "713b988311601359", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "d0c151b99af9360b", - "element_type": "hda", - "element_index": 14, - "element_identifier": "100008", - "object": { - "id": "f38a73020009daba", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "29eac6dcbd14f883", - "element_type": "hda", - "element_index": 15, - "element_identifier": "100009", - "object": { - "id": "622229cf50f04387", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "a158574d58234118", - "element_type": "hda", - "element_index": 16, - "element_identifier": "10001", - "object": { - "id": "b68d17846c7fbc83", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "2b98540a14173828", - "element_type": "hda", - "element_index": 17, - "element_identifier": "100010", - "object": { - "id": "6eeb1155086b617b", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "8e114d54b2d1f419", - "element_type": "hda", - "element_index": 18, - "element_identifier": "100011", - "object": { - "id": "e7746068f68fc864", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "749fa1ed60051f98", - "element_type": "hda", - "element_index": 19, - "element_identifier": "100012", - "object": { - "id": "8051d6590bfd64e9", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "911f2df796ad747c", - "element_type": "hda", - "element_index": 20, - "element_identifier": "100013", - "object": { - "id": "5cba928dd1a05f48", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "faa6c009defd91d0", - "element_type": "hda", - "element_index": 21, - "element_identifier": "100014", - "object": { - "id": "eac6ce67595195ec", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "6129b0ae6f8527f0", - "element_type": "hda", - "element_index": 22, - "element_identifier": "100015", - "object": { - "id": "98d427a4631d635a", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "789438fb3dff9d11", - "element_type": "hda", - "element_index": 23, - "element_identifier": "100016", - "object": { - "id": "ddf8d1e1eb7b0fb7", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "a7d7479d06608eca", - "element_type": "hda", - "element_index": 24, - "element_identifier": "100017", - "object": { - "id": "7083648ea486ebf6", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "440b7cc14c8c4d06", - "element_type": "hda", - "element_index": 25, - "element_identifier": "100018", - "object": { - "id": "aaf0f2ebe11ae45c", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "fb4f28349f5baf2d", - "element_type": "hda", - "element_index": 26, - "element_identifier": "100019", - "object": { - "id": "c78ca882684b7434", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "87fd7db76c544224", - "element_type": "hda", - "element_index": 27, - "element_identifier": "10002", - "object": { - "id": "c9c548cca771bdbc", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "a9431fe40e66ea34", - "element_type": "hda", - "element_index": 28, - "element_identifier": "100020", - "object": { - "id": "2fbde361d6a31e7e", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "43acf2f33287edcb", - "element_type": "hda", - "element_index": 29, - "element_identifier": "100021", - "object": { - "id": "b73abcd044761674", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "9a74eaab42e02827", - "element_type": "hda", - "element_index": 30, - "element_identifier": "100022", - "object": { - "id": "7326e2004b6d8758", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "6d8e4d965b771ea5", - "element_type": "hda", - "element_index": 31, - "element_identifier": "100023", - "object": { - "id": "e16a1758d8c32a91", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "c69237419b0ccecb", - "element_type": "hda", - "element_index": 32, - "element_identifier": "100024", - "object": { - "id": "16d9d7a5407b90bb", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "2d09ebff4dc98b05", - "element_type": "hda", - "element_index": 33, - "element_identifier": "100025", - "object": { - "id": "be6aaa8b33c20354", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "359f5b7c99fbb2a6", - "element_type": "hda", - "element_index": 34, - "element_identifier": "100026", - "object": { - "id": "f0d2dd7ce1a0d1d7", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "890b48f1534ba81c", - "element_type": "hda", - "element_index": 35, - "element_identifier": "100027", - "object": { - "id": "a4243929ecd1e137", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "4220951a437ba60e", - "element_type": "hda", - "element_index": 36, - "element_identifier": "100028", - "object": { - "id": "efdbb5c431ed86c4", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "f306434746a46cb6", - "element_type": "hda", - "element_index": 37, - "element_identifier": "100029", - "object": { - "id": "0c7525f276af0cb2", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "abc747938ed8a600", - "element_type": "hda", - "element_index": 38, - "element_identifier": "10003", - "object": { - "id": "0032a856b26c744e", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "63636a455c5b21fa", - "element_type": "hda", - "element_index": 39, - "element_identifier": "100030", - "object": { - "id": "aa1ea414cbd09e79", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "e8c3a4e75936ee34", - "element_type": "hda", - "element_index": 40, - "element_identifier": "100031", - "object": { - "id": "9a0d644f6e3ebb6f", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "25dce1f99c32a06f", - "element_type": "hda", - "element_index": 41, - "element_identifier": "100032", - "object": { - "id": "c6d7ec9dd85960fd", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "0f308c8bc2dba01e", - "element_type": "hda", - "element_index": 42, - "element_identifier": "100033", - "object": { - "id": "c1e28fb74593e606", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "134707f5254d3467", - "element_type": "hda", - "element_index": 43, - "element_identifier": "100034", - "object": { - "id": "edb9e58b575cfb18", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "af2a47bf25ac31c6", - "element_type": "hda", - "element_index": 44, - "element_identifier": "100035", - "object": { - "id": "595375e544772627", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "9b4142aa13cacd8d", - "element_type": "hda", - "element_index": 45, - "element_identifier": "100036", - "object": { - "id": "d917f9554bc7b276", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "8732705714d1b967", - "element_type": "hda", - "element_index": 46, - "element_identifier": "100037", - "object": { - "id": "17ac36cd25e09d75", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "9faac61c404df8d0", - "element_type": "hda", - "element_index": 47, - "element_identifier": "100038", - "object": { - "id": "fc587326c65e935c", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "eec6329884514299", - "element_type": "hda", - "element_index": 48, - "element_identifier": "100039", - "object": { - "id": "0f1c2717b9554064", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "0a7c50c76808cb45", - "element_type": "hda", - "element_index": 49, - "element_identifier": "10004", - "object": { - "id": "175262dbcbd9987f", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "1ff31d34983529f8", - "element_type": "hda", - "element_index": 50, - "element_identifier": "100040", - "object": { - "id": "7c3fb968df7e50f1", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "0af255ba4961216c", - "element_type": "hda", - "element_index": 51, - "element_identifier": "100041", - "object": { - "id": "cf83f2dcc9f7362b", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "5be07bbe21af2e4a", - "element_type": "hda", - "element_index": 52, - "element_identifier": "100042", - "object": { - "id": "4efb7c25e2bb7fa8", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "8ab1435b8102e81e", - "element_type": "hda", - "element_index": 53, - "element_identifier": "100043", - "object": { - "id": "defeea39a9509c16", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "842e3a8e9b827812", - "element_type": "hda", - "element_index": 54, - "element_identifier": "100044", - "object": { - "id": "5f5b885264688585", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "a5824a57e59503dc", - "element_type": "hda", - "element_index": 55, - "element_identifier": "100045", - "object": { - "id": "041689872363985f", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "616698141b35a61f", - "element_type": "hda", - "element_index": 56, - "element_identifier": "100046", - "object": { - "id": "2839cb63aea76bb4", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "d36010717df75f3d", - "element_type": "hda", - "element_index": 57, - "element_identifier": "100047", - "object": { - "id": "4236bdc00992172f", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "f5d1282ba4e61ea9", - "element_type": "hda", - "element_index": 58, - "element_identifier": "100048", - "object": { - "id": "bf8ded787d0c5b53", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - }, - { - "model_class": "DatasetCollectionElement", - "id": "8dc4bd112d29b27d", - "element_type": "hda", - "element_index": 59, - "element_identifier": "100049", - "object": { - "id": "023453c935463dba", - "model_class": "HistoryDatasetAssociation", - "state": "ok", - "hda_ldda": "hda", - "history_id": "e90facaf332c3038" - } - } -] diff --git a/client/src/components/providers/History/test/json/collectionContent.nested.json b/client/src/components/providers/History/test/json/collectionContent.nested.json deleted file mode 100644 index 59f5c7aa840..00000000000 --- a/client/src/components/providers/History/test/json/collectionContent.nested.json +++ /dev/null @@ -1,30 +0,0 @@ -[ - { - "element_identifier": "forward", - "object": { - "hda_ldda": "hda", - "history_id": "4ff6f47412c3e65e", - "state": "ok", - "model_class": "HistoryDatasetAssociation", - "id": "d5836ddca35af8fe" - }, - "id": "40876639881ca029", - "element_index": 0, - "element_type": "hda", - "model_class": "DatasetCollectionElement" - }, - { - "element_identifier": "reverse", - "object": { - "hda_ldda": "hda", - "history_id": "4ff6f47412c3e65e", - "state": "ok", - "model_class": "HistoryDatasetAssociation", - "id": "c1209dcf6a15c53b" - }, - "id": "d071e794759ab192", - "element_index": 1, - "element_type": "hda", - "model_class": "DatasetCollectionElement" - } -] diff --git a/client/src/components/providers/History/test/json/historyContent.json b/client/src/components/providers/History/test/json/historyContent.json deleted file mode 100644 index 21240416443..00000000000 --- a/client/src/components/providers/History/test/json/historyContent.json +++ /dev/null @@ -1,2797 +0,0 @@ -[ - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "9c019bbfa7322549", - "name": "Select random lines on data 108", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:14.598049", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1151.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "cffbf0b3-37f5-44d7-845c-daef7bab22a3", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-ee74a7a2152289f5", - "url": "/api/histories/2f94e8ae9edff68a/contents/ee74a7a2152289f5", - "peek": "
- +
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 165, - "visible": true, - "creating_job": "9c019bbfa7322549", - "update_time": "2020-07-02T17:14:35.084070", - "rerunnable": true, - "deleted": false, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/ee74a7a2152289f5/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "ee74a7a2152289f5" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "0fec89153aa8743d", - "name": "Select random lines on data 107", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:14.358836", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1150.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "e8b1cd57-9d0b-47d3-8dd4-8bb1515d5c01", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-65b05d9846015547", - "url": "/api/histories/2f94e8ae9edff68a/contents/65b05d9846015547", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 164, - "visible": false, - "creating_job": "0fec89153aa8743d", - "update_time": "2020-07-02T17:14:30.779839", - "rerunnable": true, - "deleted": false, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/65b05d9846015547/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "65b05d9846015547" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "182bab19390d12f7", - "name": "Select random lines on data 106", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:13.967109", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1149.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "0ff390c7-4c59-4076-84e7-a932c62679b0", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-bf99977736808be6", - "url": "/api/histories/2f94e8ae9edff68a/contents/bf99977736808be6", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 163, - "visible": false, - "creating_job": "182bab19390d12f7", - "update_time": "2020-07-02T17:14:30.801856", - "rerunnable": true, - "deleted": true, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/bf99977736808be6/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "bf99977736808be6" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "6df8ec04e60b3b26", - "name": "Select random lines on data 105", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:13.680997", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1148.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "eee66bc1-c344-4e6f-aa70-64c8e71086d2", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-9d5d229c62e3f271", - "url": "/api/histories/2f94e8ae9edff68a/contents/9d5d229c62e3f271", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 162, - "visible": true, - "creating_job": "6df8ec04e60b3b26", - "update_time": "2020-07-02T17:14:30.742075", - "rerunnable": true, - "deleted": false, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/9d5d229c62e3f271/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "9d5d229c62e3f271" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "3cdbf23a5a13103b", - "name": "Select random lines on data 104", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:13.540450", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1147.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "7b6b8d9a-bbd0-454c-900f-c2bdfc0b771e", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-e87d5b3bf14d5c06", - "url": "/api/histories/2f94e8ae9edff68a/contents/e87d5b3bf14d5c06", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 161, - "visible": true, - "creating_job": "3cdbf23a5a13103b", - "update_time": "2020-07-02T17:14:29.392503", - "rerunnable": true, - "deleted": false, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/e87d5b3bf14d5c06/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "e87d5b3bf14d5c06" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "ada85e82199e3fc1", - "name": "Select random lines on data 103", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:13.450379", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1146.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "7368bd7f-63d3-41c6-9869-7443dd36ab58", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-01611285b1b754d4", - "url": "/api/histories/2f94e8ae9edff68a/contents/01611285b1b754d4", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 160, - "visible": true, - "creating_job": "ada85e82199e3fc1", - "update_time": "2020-07-02T17:14:21.003319", - "rerunnable": true, - "deleted": false, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/01611285b1b754d4/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "01611285b1b754d4" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "e4b800ee43da817e", - "name": "Select random lines on data 102", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:13.156049", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1145.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "c0572def-614f-46f0-b0f1-c7c53330b068", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-21fb8edef9e06aee", - "url": "/api/histories/2f94e8ae9edff68a/contents/21fb8edef9e06aee", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 159, - "visible": true, - "creating_job": "e4b800ee43da817e", - "update_time": "2020-07-02T17:14:21.028645", - "rerunnable": true, - "deleted": false, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/21fb8edef9e06aee/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "21fb8edef9e06aee" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "13b397a1ea2c1dbf", - "name": "Select random lines on data 101", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:12.918074", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1144.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "01522715-eb0e-421d-8bc7-42a3785a8bbe", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-e2cd7fe532848756", - "url": "/api/histories/2f94e8ae9edff68a/contents/e2cd7fe532848756", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 158, - "visible": true, - "creating_job": "13b397a1ea2c1dbf", - "update_time": "2020-07-02T17:14:20.944971", - "rerunnable": true, - "deleted": false, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/e2cd7fe532848756/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "e2cd7fe532848756" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "169175a66f863459", - "name": "Select random lines on data 100", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:12.595760", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1143.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "ef597777-20ac-4d3e-a5d9-99e09fd38831", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-9d5bdb551832597a", - "url": "/api/histories/2f94e8ae9edff68a/contents/9d5bdb551832597a", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 157, - "visible": true, - "creating_job": "169175a66f863459", - "update_time": "2020-07-02T17:14:20.145028", - "rerunnable": true, - "deleted": false, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/9d5bdb551832597a/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "9d5bdb551832597a" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "43c04a4df15a1f7a", - "name": "Select random lines on data 99", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:12.417473", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1142.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "3f477ffd-7e06-4c60-85d3-11e1ee251ef3", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-0d7418ad8754e1ba", - "url": "/api/histories/2f94e8ae9edff68a/contents/0d7418ad8754e1ba", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 156, - "visible": true, - "creating_job": "43c04a4df15a1f7a", - "update_time": "2020-07-02T17:14:08.322766", - "rerunnable": true, - "deleted": false, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/0d7418ad8754e1ba/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "0d7418ad8754e1ba" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "78176dfc865c8302", - "name": "Select random lines on data 98", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:12.126872", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1141.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "719fa5b5-f898-45f3-a279-76329c6913b4", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-cc5fb41fa467c62f", - "url": "/api/histories/2f94e8ae9edff68a/contents/cc5fb41fa467c62f", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 155, - "visible": true, - "creating_job": "78176dfc865c8302", - "update_time": "2020-07-02T17:14:08.341521", - "rerunnable": true, - "deleted": false, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/cc5fb41fa467c62f/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "cc5fb41fa467c62f" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "7e5a6282b7150cce", - "name": "Select random lines on data 97", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:11.898501", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1140.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "9ec6c5fc-08da-44d7-8259-3179c95689bb", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-be164d1018ed2499", - "url": "/api/histories/2f94e8ae9edff68a/contents/be164d1018ed2499", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 154, - "visible": true, - "creating_job": "7e5a6282b7150cce", - "update_time": "2020-07-02T17:14:06.969005", - "rerunnable": true, - "deleted": false, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/be164d1018ed2499/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "be164d1018ed2499" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "fb1af791c00dd0e4", - "name": "Select random lines on data 96", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:11.636493", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1139.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "7904faba-9cfe-411c-bf5d-2e52b80c6d63", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-9c7fc3db9aebaa60", - "url": "/api/histories/2f94e8ae9edff68a/contents/9c7fc3db9aebaa60", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 153, - "visible": true, - "creating_job": "fb1af791c00dd0e4", - "update_time": "2020-07-02T17:14:08.355740", - "rerunnable": true, - "deleted": false, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/9c7fc3db9aebaa60/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "9c7fc3db9aebaa60" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "18c1ddee9d6509e0", - "name": "Select random lines on data 95", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:11.276238", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1138.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "023752c7-6fad-4eef-accf-cdf7a6624788", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-5583febb78070f6e", - "url": "/api/histories/2f94e8ae9edff68a/contents/5583febb78070f6e", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 152, - "visible": true, - "creating_job": "18c1ddee9d6509e0", - "update_time": "2020-07-02T17:13:55.258074", - "rerunnable": true, - "deleted": false, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/5583febb78070f6e/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "5583febb78070f6e" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "fae76ced2b7e97d7", - "name": "Select random lines on data 94", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:10.847345", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1137.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "70900478-12a9-43a1-ab04-3ca3c4445592", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-a3034dc0fe6363f2", - "url": "/api/histories/2f94e8ae9edff68a/contents/a3034dc0fe6363f2", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 151, - "visible": true, - "creating_job": "fae76ced2b7e97d7", - "update_time": "2020-07-02T17:13:55.210624", - "rerunnable": true, - "deleted": false, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/a3034dc0fe6363f2/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "a3034dc0fe6363f2" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "07630b166a59bdad", - "name": "Select random lines on data 93", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:10.630512", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1136.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "3703cf59-cecf-47a0-a96d-638f66ecf58d", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-e865c3fa6b646e8b", - "url": "/api/histories/2f94e8ae9edff68a/contents/e865c3fa6b646e8b", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 150, - "visible": true, - "creating_job": "07630b166a59bdad", - "update_time": "2020-07-02T17:13:55.253801", - "rerunnable": true, - "deleted": false, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/e865c3fa6b646e8b/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "e865c3fa6b646e8b" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "13c07ce0ed2cea74", - "name": "Select random lines on data 92", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:10.474227", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1135.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "bf91813f-e082-40cf-a0a8-85a9646eeb00", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-fc39b4da94a2e68f", - "url": "/api/histories/2f94e8ae9edff68a/contents/fc39b4da94a2e68f", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 149, - "visible": true, - "creating_job": "13c07ce0ed2cea74", - "update_time": "2020-07-02T17:13:54.932915", - "rerunnable": true, - "deleted": false, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/fc39b4da94a2e68f/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "fc39b4da94a2e68f" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "4a86c8de6f7e9376", - "name": "Select random lines on data 91", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:10.341217", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1134.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "12233e16-7128-4bdc-a708-12b29607823f", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-bb349edfde212365", - "url": "/api/histories/2f94e8ae9edff68a/contents/bb349edfde212365", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 148, - "visible": true, - "creating_job": "4a86c8de6f7e9376", - "update_time": "2020-07-02T17:13:44.549193", - "rerunnable": true, - "deleted": false, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/bb349edfde212365/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "bb349edfde212365" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "0ff195fdd529b966", - "name": "Select random lines on data 90", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:10.196285", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1133.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "44525832-3a54-4e36-be65-b584141846f8", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-129ca6c26a6931c9", - "url": "/api/histories/2f94e8ae9edff68a/contents/129ca6c26a6931c9", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 147, - "visible": false, - "creating_job": "0ff195fdd529b966", - "update_time": "2020-07-02T17:13:44.566149", - "rerunnable": true, - "deleted": false, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/129ca6c26a6931c9/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "129ca6c26a6931c9" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "80c7a7e8240dd658", - "name": "Select random lines on data 87", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:10.068164", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1132.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "a85c004f-c513-4e04-b8f8-a83dbf8a2975", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-8c4acef511ce6a13", - "url": "/api/histories/2f94e8ae9edff68a/contents/8c4acef511ce6a13", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 146, - "visible": true, - "creating_job": "80c7a7e8240dd658", - "update_time": "2020-07-02T17:13:44.501758", - "rerunnable": true, - "deleted": false, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/8c4acef511ce6a13/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "8c4acef511ce6a13" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "5786ce0546cbc07f", - "name": "Select random lines on data 86", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:09.895745", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1131.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "e2d0cbd2-db2e-45e9-8ddc-43f939ade175", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-cb39084e4b94595e", - "url": "/api/histories/2f94e8ae9edff68a/contents/cb39084e4b94595e", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 145, - "visible": true, - "creating_job": "5786ce0546cbc07f", - "update_time": "2020-07-02T17:13:44.540544", - "rerunnable": true, - "deleted": false, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/cb39084e4b94595e/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "cb39084e4b94595e" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "26d704460e4a4985", - "name": "Select random lines on data 85", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:09.779313", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1130.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "ecf8e87f-0512-4129-8acb-f1b22fc44af9", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-a54402b4d6323562", - "url": "/api/histories/2f94e8ae9edff68a/contents/a54402b4d6323562", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 144, - "visible": true, - "creating_job": "26d704460e4a4985", - "update_time": "2020-07-02T17:13:35.150171", - "rerunnable": true, - "deleted": false, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/a54402b4d6323562/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "a54402b4d6323562" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "dc72786b0d4875fb", - "name": "Select random lines on data 84", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:09.462394", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1129.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "575ca4f6-6e7c-4890-922b-462e6f4ba055", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-b0ac2b753662f808", - "url": "/api/histories/2f94e8ae9edff68a/contents/b0ac2b753662f808", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 143, - "visible": true, - "creating_job": "dc72786b0d4875fb", - "update_time": "2020-07-02T17:13:35.215070", - "rerunnable": true, - "deleted": false, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/b0ac2b753662f808/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "b0ac2b753662f808" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "303d4722059b63c2", - "name": "Select random lines on data 83", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:09.024308", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1128.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "660e243c-c794-43ba-b070-f6ffa67941e8", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-0bb53d1c0b73db00", - "url": "/api/histories/2f94e8ae9edff68a/contents/0bb53d1c0b73db00", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 142, - "visible": true, - "creating_job": "303d4722059b63c2", - "update_time": "2020-07-02T17:13:35.048004", - "rerunnable": true, - "deleted": false, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/0bb53d1c0b73db00/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "0bb53d1c0b73db00" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "e373ca7c82675833", - "name": "Select random lines on data 82", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:08.839549", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1127.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "d06c6b69-32e0-46a4-9f63-b5e6a6b7b8f9", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-c8d221932b740453", - "url": "/api/histories/2f94e8ae9edff68a/contents/c8d221932b740453", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 141, - "visible": true, - "creating_job": "e373ca7c82675833", - "update_time": "2020-07-02T17:13:34.966640", - "rerunnable": true, - "deleted": false, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/c8d221932b740453/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "c8d221932b740453" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "4969f1ebf8379add", - "name": "Select random lines on data 81", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:08.678261", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1126.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "ad3c7ddf-93d7-4102-8b65-6ee950c0e53e", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-fd5d271453b419bf", - "url": "/api/histories/2f94e8ae9edff68a/contents/fd5d271453b419bf", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 140, - "visible": true, - "creating_job": "4969f1ebf8379add", - "update_time": "2020-07-02T17:13:26.515907", - "rerunnable": true, - "deleted": false, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/fd5d271453b419bf/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "fd5d271453b419bf" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "871ce98d8c078309", - "name": "Select random lines on data 80", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:08.439740", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1125.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "85228002-5316-4408-8b30-d47e0687b47b", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-1b558b31eb840782", - "url": "/api/histories/2f94e8ae9edff68a/contents/1b558b31eb840782", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 139, - "visible": true, - "creating_job": "871ce98d8c078309", - "update_time": "2020-07-02T17:13:26.064511", - "rerunnable": true, - "deleted": false, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/1b558b31eb840782/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "1b558b31eb840782" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "1bca6b54cf56afed", - "name": "Select random lines on data 79", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:08.204846", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1124.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "e211cd1b-77d9-4b7d-b30c-7d4cd21b80dd", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-c1cba22c86ef1135", - "url": "/api/histories/2f94e8ae9edff68a/contents/c1cba22c86ef1135", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 138, - "visible": true, - "creating_job": "1bca6b54cf56afed", - "update_time": "2020-07-02T17:13:26.420395", - "rerunnable": true, - "deleted": false, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/c1cba22c86ef1135/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "c1cba22c86ef1135" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "8be3d18182a9e60b", - "name": "Select random lines on data 78", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:07.956424", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1123.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "5e3b5d07-2cb3-4360-afa9-35e939cae35e", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-d5974bc21ed0134c", - "url": "/api/histories/2f94e8ae9edff68a/contents/d5974bc21ed0134c", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 137, - "visible": true, - "creating_job": "8be3d18182a9e60b", - "update_time": "2020-07-02T17:13:26.380713", - "rerunnable": true, - "deleted": false, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/d5974bc21ed0134c/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "d5974bc21ed0134c" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "0375199fa16cf1e6", - "name": "Select random lines on data 77", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:07.838633", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1122.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "0262a34f-2f67-4f2a-afcd-1dafef6b8452", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-ef9838f9f223749f", - "url": "/api/histories/2f94e8ae9edff68a/contents/ef9838f9f223749f", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 136, - "visible": true, - "creating_job": "0375199fa16cf1e6", - "update_time": "2020-07-02T17:13:14.970221", - "rerunnable": true, - "deleted": false, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/ef9838f9f223749f/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "ef9838f9f223749f" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "b1da20f285d33353", - "name": "Select random lines on data 76", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:07.554284", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1121.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "217d8f4c-ab9f-44d4-bd48-1ef9ecf932cf", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-5f7733bbff4769c3", - "url": "/api/histories/2f94e8ae9edff68a/contents/5f7733bbff4769c3", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 135, - "visible": true, - "creating_job": "b1da20f285d33353", - "update_time": "2020-07-02T17:13:14.952092", - "rerunnable": true, - "deleted": false, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/5f7733bbff4769c3/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "5f7733bbff4769c3" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "a83adc160e0a77a0", - "name": "Select random lines on data 75", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:07.291121", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1120.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "f5f0dffa-4fe6-4bb9-9ddc-02fe75edb0f7", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-046c7f452ce7fd20", - "url": "/api/histories/2f94e8ae9edff68a/contents/046c7f452ce7fd20", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 134, - "visible": true, - "creating_job": "a83adc160e0a77a0", - "update_time": "2020-07-02T17:13:14.865652", - "rerunnable": true, - "deleted": false, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/046c7f452ce7fd20/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "046c7f452ce7fd20" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "3a437f8167ca4c87", - "name": "Select random lines on data 74", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:07.176345", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1119.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "82bd2e95-d89e-4e7a-aa98-58f96c004031", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-ff404b57ba36c247", - "url": "/api/histories/2f94e8ae9edff68a/contents/ff404b57ba36c247", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 133, - "visible": true, - "creating_job": "3a437f8167ca4c87", - "update_time": "2020-07-02T17:13:14.880696", - "rerunnable": true, - "deleted": false, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/ff404b57ba36c247/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "ff404b57ba36c247" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "39ac0e5728f31c3f", - "name": "Select random lines on data 73", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:07.015882", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1118.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "42146c48-a85e-4dc1-a3a3-1d8b34a56369", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-514bfe2c97b0d65e", - "url": "/api/histories/2f94e8ae9edff68a/contents/514bfe2c97b0d65e", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 132, - "visible": true, - "creating_job": "39ac0e5728f31c3f", - "update_time": "2020-07-02T17:13:04.669701", - "rerunnable": true, - "deleted": false, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/514bfe2c97b0d65e/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "514bfe2c97b0d65e" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "a87795113868c92b", - "name": "Select random lines on data 72", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:06.805947", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1117.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "9b0bce06-f1cc-4e6c-ba8c-08bd8b383b55", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-2bb5f5a4f02e625e", - "url": "/api/histories/2f94e8ae9edff68a/contents/2bb5f5a4f02e625e", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 131, - "visible": true, - "creating_job": "a87795113868c92b", - "update_time": "2020-07-02T17:13:04.593101", - "rerunnable": true, - "deleted": false, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/2bb5f5a4f02e625e/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "2bb5f5a4f02e625e" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "1fe8025a3eb5c482", - "name": "Select random lines on data 71", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:06.640136", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1116.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "3912edb6-4098-40c0-9897-2ace659cf519", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-fb5d33f2616836cb", - "url": "/api/histories/2f94e8ae9edff68a/contents/fb5d33f2616836cb", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 130, - "visible": true, - "creating_job": "1fe8025a3eb5c482", - "update_time": "2020-07-02T17:13:04.641115", - "rerunnable": true, - "deleted": false, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/fb5d33f2616836cb/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "fb5d33f2616836cb" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "6e01c0e0a145bde3", - "name": "Select random lines on data 70", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:06.390560", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1115.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "ea8b3418-b28c-471c-b0e9-776d71b5b981", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-e6c0d6c692fcb434", - "url": "/api/histories/2f94e8ae9edff68a/contents/e6c0d6c692fcb434", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 129, - "visible": true, - "creating_job": "6e01c0e0a145bde3", - "update_time": "2020-07-02T17:13:04.355401", - "rerunnable": true, - "deleted": false, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/e6c0d6c692fcb434/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "e6c0d6c692fcb434" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "be439a3589617e67", - "name": "Select random lines on data 69", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:06.151651", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1114.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "2dfe3fb3-43b8-4ff1-9bdc-622c48bd8dea", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-0c92c65fab74b1a4", - "url": "/api/histories/2f94e8ae9edff68a/contents/0c92c65fab74b1a4", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 128, - "visible": true, - "creating_job": "be439a3589617e67", - "update_time": "2020-07-02T17:12:55.245423", - "rerunnable": true, - "deleted": false, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/0c92c65fab74b1a4/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "0c92c65fab74b1a4" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "d216b0eb84f63cf4", - "name": "Select random lines on data 68", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:05.962884", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1113.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "e252492a-bcc7-431c-b34a-fdad068b1e0a", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-6bf3b9c89dac7d31", - "url": "/api/histories/2f94e8ae9edff68a/contents/6bf3b9c89dac7d31", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 127, - "visible": true, - "creating_job": "d216b0eb84f63cf4", - "update_time": "2020-07-02T17:12:55.242585", - "rerunnable": true, - "deleted": true, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/6bf3b9c89dac7d31/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "6bf3b9c89dac7d31" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "f7bbb5e66d40415a", - "name": "Select random lines on data 67", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:05.703961", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1112.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "ab55aa3e-d0b2-4996-a986-fdee374e19e3", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-4fe86edec13b16fc", - "url": "/api/histories/2f94e8ae9edff68a/contents/4fe86edec13b16fc", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 126, - "visible": true, - "creating_job": "f7bbb5e66d40415a", - "update_time": "2020-07-02T17:12:55.219388", - "rerunnable": true, - "deleted": false, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/4fe86edec13b16fc/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "4fe86edec13b16fc" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "dcf349068765fa56", - "name": "Select random lines on data 66", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:05.319558", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1111.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "93ab479e-b545-48ce-a9e2-d6ecebfd8ee4", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-93d8baed04ca4af1", - "url": "/api/histories/2f94e8ae9edff68a/contents/93d8baed04ca4af1", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 125, - "visible": true, - "creating_job": "dcf349068765fa56", - "update_time": "2020-07-02T17:12:55.201945", - "rerunnable": true, - "deleted": false, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/93d8baed04ca4af1/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "93d8baed04ca4af1" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "556237b7328da3af", - "name": "Select random lines on data 65", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:05.085542", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1110.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "6925b3a8-c31e-4180-ac51-c001568f2937", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-cf9547eabff54bee", - "url": "/api/histories/2f94e8ae9edff68a/contents/cf9547eabff54bee", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 124, - "visible": true, - "creating_job": "556237b7328da3af", - "update_time": "2020-07-02T17:12:45.807635", - "rerunnable": true, - "deleted": false, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/cf9547eabff54bee/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "cf9547eabff54bee" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "ed7fb4075bcdedec", - "name": "Select random lines on data 64", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:04.680743", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1109.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "abe20394-e9ad-4d4f-acb7-408bf7681a16", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-f70e420d6812c25f", - "url": "/api/histories/2f94e8ae9edff68a/contents/f70e420d6812c25f", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 123, - "visible": true, - "creating_job": "ed7fb4075bcdedec", - "update_time": "2020-07-02T17:12:45.746180", - "rerunnable": true, - "deleted": false, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/f70e420d6812c25f/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "f70e420d6812c25f" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "c1736909009d5cef", - "name": "Select random lines on data 63", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:03.908014", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1108.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "89b5a86f-6699-49fd-acad-2e968c16ad19", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-131a180e703d5f4c", - "url": "/api/histories/2f94e8ae9edff68a/contents/131a180e703d5f4c", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 122, - "visible": true, - "creating_job": "c1736909009d5cef", - "update_time": "2020-07-02T17:12:45.669314", - "rerunnable": true, - "deleted": false, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/131a180e703d5f4c/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "131a180e703d5f4c" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "5d2948302cdb9e75", - "name": "Select random lines on data 62", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:03.536610", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1107.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "98a90a2f-095d-4d3d-a396-1b873a27ced3", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-f23e895e293d0e9b", - "url": "/api/histories/2f94e8ae9edff68a/contents/f23e895e293d0e9b", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 121, - "visible": true, - "creating_job": "5d2948302cdb9e75", - "update_time": "2020-07-02T17:12:45.670944", - "rerunnable": true, - "deleted": false, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/f23e895e293d0e9b/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "f23e895e293d0e9b" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "a27b5c37ceef7a82", - "name": "Select random lines on data 61", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:03.286832", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1106.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "e2b38c6b-92c4-4bf5-9eea-21a26ff7e20d", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-014dd2f744a04c28", - "url": "/api/histories/2f94e8ae9edff68a/contents/014dd2f744a04c28", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 120, - "visible": true, - "creating_job": "a27b5c37ceef7a82", - "update_time": "2020-07-02T17:12:36.747722", - "rerunnable": true, - "deleted": false, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/014dd2f744a04c28/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "014dd2f744a04c28" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "d76cd3ffbd3d6b3a", - "name": "Select random lines on data 60", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:02.981696", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1105.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "25d49a2b-e698-4302-a95d-f79ebf875eb3", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-74040d11a5878de0", - "url": "/api/histories/2f94e8ae9edff68a/contents/74040d11a5878de0", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 119, - "visible": true, - "creating_job": "d76cd3ffbd3d6b3a", - "update_time": "2020-07-02T17:12:36.722427", - "rerunnable": true, - "deleted": false, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/74040d11a5878de0/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "74040d11a5878de0" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "05bf548021f41f6c", - "name": "Select random lines on data 59", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:02.595473", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1104.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "f574e108-11de-4446-a893-22039c1d59ad", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-055cc08a240db326", - "url": "/api/histories/2f94e8ae9edff68a/contents/055cc08a240db326", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 118, - "visible": true, - "creating_job": "05bf548021f41f6c", - "update_time": "2020-07-02T17:12:36.438954", - "rerunnable": true, - "deleted": false, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/055cc08a240db326/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "055cc08a240db326" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "c3e6313f0f678209", - "name": "Select random lines on data 58", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:02.244714", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1103.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "8bed0b33-ba35-46f7-9b28-cdd36e2b1887", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-8cd34530378e3e0c", - "url": "/api/histories/2f94e8ae9edff68a/contents/8cd34530378e3e0c", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 117, - "visible": true, - "creating_job": "c3e6313f0f678209", - "update_time": "2020-07-02T17:12:36.291137", - "rerunnable": true, - "deleted": false, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/8cd34530378e3e0c/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "8cd34530378e3e0c" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "ba195747b0d051d6", - "name": "Select random lines on data 57", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:01.955466", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1102.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "58ec487d-bbdc-43f7-9bdf-e6b0eff72b17", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-39c71ca2ebd30e0b", - "url": "/api/histories/2f94e8ae9edff68a/contents/39c71ca2ebd30e0b", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 116, - "visible": true, - "creating_job": "ba195747b0d051d6", - "update_time": "2020-07-02T17:12:26.178251", - "rerunnable": true, - "deleted": false, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/39c71ca2ebd30e0b/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "39c71ca2ebd30e0b" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "a96cf1e0fad42ad9", - "name": "Select random lines on data 56", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:01.696373", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1101.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "1ebcb32a-d067-417d-a686-ecc53042d756", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-fd71395d8c0f1155", - "url": "/api/histories/2f94e8ae9edff68a/contents/fd71395d8c0f1155", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 115, - "visible": true, - "creating_job": "a96cf1e0fad42ad9", - "update_time": "2020-07-02T17:12:26.293059", - "rerunnable": true, - "deleted": false, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/fd71395d8c0f1155/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "fd71395d8c0f1155" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "9105f12a25bf5d9e", - "name": "Select random lines on data 55", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:01.458797", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1100.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "1e45365d-20b2-4ec8-83d6-43581ffad771", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-d1e91c3db64140b0", - "url": "/api/histories/2f94e8ae9edff68a/contents/d1e91c3db64140b0", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 114, - "visible": true, - "creating_job": "9105f12a25bf5d9e", - "update_time": "2020-07-02T17:12:25.921361", - "rerunnable": true, - "deleted": false, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/d1e91c3db64140b0/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "d1e91c3db64140b0" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "e51a9291655ff7cd", - "name": "Select random lines on data 54", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:01.241452", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1099.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "b27dbd12-2ceb-44a1-997c-4ab968d48265", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-c0dd61e7648dddfe", - "url": "/api/histories/2f94e8ae9edff68a/contents/c0dd61e7648dddfe", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 113, - "visible": true, - "creating_job": "e51a9291655ff7cd", - "update_time": "2020-07-02T17:12:25.829743", - "rerunnable": true, - "deleted": false, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/c0dd61e7648dddfe/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "c0dd61e7648dddfe" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "4a3277a7f1f97c58", - "name": "Select random lines on data 4", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:01.016513", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1098.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "72804271-fab6-4ffe-a00f-fce78dd1d2ec", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-9bad2b56849bec8b", - "url": "/api/histories/2f94e8ae9edff68a/contents/9bad2b56849bec8b", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 112, - "visible": true, - "creating_job": "4a3277a7f1f97c58", - "update_time": "2020-07-02T17:12:15.459958", - "rerunnable": true, - "deleted": false, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/9bad2b56849bec8b/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "9bad2b56849bec8b" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "233d8b78eeff8302", - "name": "Select random lines on data 3", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:00.821573", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1097.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "ff0a7e87-ef17-4d21-8adf-e777ee9b552f", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-ac86fc9298249ba8", - "url": "/api/histories/2f94e8ae9edff68a/contents/ac86fc9298249ba8", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 111, - "visible": true, - "creating_job": "233d8b78eeff8302", - "update_time": "2020-07-02T17:12:15.528769", - "rerunnable": true, - "deleted": false, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/ac86fc9298249ba8/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "ac86fc9298249ba8" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "09ac4927b877e47e", - "name": "Select random lines on data 2", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:00.597906", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1096.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "7f4da894-7294-452d-a954-190a74ecb695", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-a6171316c00d613f", - "url": "/api/histories/2f94e8ae9edff68a/contents/a6171316c00d613f", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 110, - "visible": true, - "creating_job": "09ac4927b877e47e", - "update_time": "2020-07-02T17:12:15.165269", - "rerunnable": true, - "deleted": false, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/a6171316c00d613f/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "a6171316c00d613f" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "4e77bbc3b46ddd17", - "name": "Select random lines on data 1", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-07-02T17:12:00.358427", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1095.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "53e4f095-328f-4265-8e8b-6185620f63ac", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-11a7217485a18c95", - "url": "/api/histories/2f94e8ae9edff68a/contents/11a7217485a18c95", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 109, - "visible": true, - "creating_job": "4e77bbc3b46ddd17", - "update_time": "2020-07-02T17:12:15.170146", - "rerunnable": true, - "deleted": false, - "misc_info": "Kept 1 of 1 total lines.", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/11a7217485a18c95/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "11a7217485a18c95" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "a7f411be446bb522", - "name": "file_53.txt", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-06-30T21:29:40.588045", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1094.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "d0066a2c-0f40-424e-8bac-5cda9571d7b1", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-927a35d35d4f4860", - "url": "/api/histories/2f94e8ae9edff68a/contents/927a35d35d4f4860", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 108, - "visible": true, - "creating_job": "a7f411be446bb522", - "update_time": "2020-07-02T17:12:14.713730", - "rerunnable": false, - "deleted": false, - "misc_info": "uploaded txt file", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/927a35d35d4f4860/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "927a35d35d4f4860" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "cd3649f8f517008c", - "name": "file_52.txt", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-06-30T21:29:39.526953", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1093.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "b98773ec-d097-4484-b102-b9b006897204", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-d354ebeb1da9547c", - "url": "/api/histories/2f94e8ae9edff68a/contents/d354ebeb1da9547c", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 107, - "visible": true, - "creating_job": "cd3649f8f517008c", - "update_time": "2020-07-02T17:12:14.507095", - "rerunnable": false, - "deleted": false, - "misc_info": "uploaded txt file", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/d354ebeb1da9547c/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "d354ebeb1da9547c" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "f5da09edbdbab890", - "name": "file_51.txt", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-06-30T21:29:38.197428", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1092.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "5327143b-e55e-409f-9b6c-dcd988d08a5d", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-c031430486d68421", - "url": "/api/histories/2f94e8ae9edff68a/contents/c031430486d68421", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 106, - "visible": true, - "creating_job": "f5da09edbdbab890", - "update_time": "2020-07-02T17:12:14.129077", - "rerunnable": false, - "deleted": false, - "misc_info": "uploaded txt file", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/c031430486d68421/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "c031430486d68421" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "297a73e30b2956ff", - "name": "file_50.txt", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-06-30T21:29:37.429959", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1091.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "063d3763-17e7-43db-bbf3-48a0d2b3dd37", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-398f4c5cc135b0dd", - "url": "/api/histories/2f94e8ae9edff68a/contents/398f4c5cc135b0dd", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 105, - "visible": true, - "creating_job": "297a73e30b2956ff", - "update_time": "2020-07-02T17:12:13.854684", - "rerunnable": false, - "deleted": false, - "misc_info": "uploaded txt file", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/398f4c5cc135b0dd/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "398f4c5cc135b0dd" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "a8a5797f705e8237", - "name": "file_49.txt", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-06-30T21:29:37.104929", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1090.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "6e7e05a4-c13a-4372-9fe3-eae1fb87ab09", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-f309dd72bc16e39b", - "url": "/api/histories/2f94e8ae9edff68a/contents/f309dd72bc16e39b", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 104, - "visible": true, - "creating_job": "a8a5797f705e8237", - "update_time": "2020-07-02T17:12:13.586169", - "rerunnable": false, - "deleted": false, - "misc_info": "uploaded txt file", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/f309dd72bc16e39b/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "f309dd72bc16e39b" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "f7d88bdd14b12f30", - "name": "file_48.txt", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-06-30T21:29:35.390006", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1089.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "69414981-2dca-4e7c-941a-e9cacfbb5065", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-153c8b79e6a3fc75", - "url": "/api/histories/2f94e8ae9edff68a/contents/153c8b79e6a3fc75", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 103, - "visible": true, - "creating_job": "f7d88bdd14b12f30", - "update_time": "2020-07-02T17:12:13.493854", - "rerunnable": false, - "deleted": false, - "misc_info": "uploaded txt file", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/153c8b79e6a3fc75/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "153c8b79e6a3fc75" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "878d21f25ae61fc8", - "name": "file_47.txt", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-06-30T21:29:34.850394", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1088.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "1758b148-025f-4ac7-b742-df8cea33d6e0", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-73c35346fd188ccb", - "url": "/api/histories/2f94e8ae9edff68a/contents/73c35346fd188ccb", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 102, - "visible": true, - "creating_job": "878d21f25ae61fc8", - "update_time": "2020-07-02T17:12:13.226133", - "rerunnable": false, - "deleted": false, - "misc_info": "uploaded txt file", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/73c35346fd188ccb/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "73c35346fd188ccb" - }, - { - "model_class": "HistoryDatasetAssociation", - "type": "file", - "display_types": [], - "history_id": "2f94e8ae9edff68a", - "dataset_id": "15851351f4d86d24", - "name": "file_46.txt", - "annotation": null, - "file_ext": "txt", - "validated_state_message": null, - "resubmitted": false, - "genome_build": "?", - "accessible": true, - "create_time": "2020-06-30T21:29:33.864461", - "history_content_type": "dataset", - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1087.dat", - "misc_blurb": "1 line", - "data_type": "galaxy.datatypes.data.Text", - "purged": false, - "validated_state": "unknown", - "meta_files": [], - "uuid": "930912bd-ce0f-418a-b1e1-ba69e69e76cd", - "tags": [], - "created_from_basename": null, - "type_id": "dataset-09bd879dc9508532", - "url": "/api/histories/2f94e8ae9edff68a/contents/09bd879dc9508532", - "peek": "
1 2 3
", - "api_type": "file", - "extension": "txt", - "hid": 101, - "visible": true, - "creating_job": "15851351f4d86d24", - "update_time": "2020-07-02T17:12:13.023050", - "rerunnable": false, - "deleted": false, - "misc_info": "uploaded txt file", - "download_url": "/api/histories/2f94e8ae9edff68a/contents/09bd879dc9508532/display", - "state": "ok", - "file_size": 6, - "display_apps": [], - "hda_ldda": "hda", - "id": "09bd879dc9508532" - } -] \ No newline at end of file diff --git a/client/src/components/providers/History/test/json/historyContentWithCollection.json b/client/src/components/providers/History/test/json/historyContentWithCollection.json deleted file mode 100644 index ef8302a0043..00000000000 --- a/client/src/components/providers/History/test/json/historyContentWithCollection.json +++ /dev/null @@ -1,256 +0,0 @@ -[ - { - "element_count": 2, - "tags": [], - "id": "a799d38679e985db", - "name": "asdaaa", - "type": "collection", - "type_id": "dataset_collection-a799d38679e985db", - "contents_url": "/api/dataset_collections/a799d38679e985db/contents/03501d7626bd192f", - "create_time": "2020-06-24T15:34:06.848662", - "populated": true, - "populated_state": "ok", - "collection_id": 10, - "collection_type": "list", - "job_state_summary": { - "running": 0, - "resubmitted": 0, - "upload": 0, - "deleted": 0, - "error": 0, - "all_jobs": 0, - "waiting": 0, - "queued": 0, - "ok": 0, - "new": 0, - "failed": 0, - "paused": 0, - "deleted_new": 0 - }, - "populated_state_message": null, - "job_source_id": null, - "deleted": false, - "visible": true, - "hid": 22, - "update_time": "2020-06-24T15:34:06.848669", - "history_id": "63cd3858d057a6d1", - "history_content_type": "dataset_collection", - "job_source_type": null, - "url": "/api/histories/63cd3858d057a6d1/contents/dataset_collections/a799d38679e985db" - }, - { - "hda_ldda": "hda", - "tags": [], - "type": "file", - "resubmitted": false, - "validated_state_message": null, - "misc_info": "uploaded fastqsanger file", - "deleted": false, - "id": "ab50e8da6b53023e", - "type_id": "dataset-ab50e8da6b53023e", - "rerunnable": false, - "display_types": [], - "create_time": "2020-06-24T11:31:35.680953", - "uuid": "92efa8a4-92ab-4941-977a-7d653e95c250", - "data_type": "galaxy.datatypes.sequence.FastqSanger", - "state": "ok", - "download_url": "/api/histories/63cd3858d057a6d1/contents/ab50e8da6b53023e/display", - "model_class": "HistoryDatasetAssociation", - "dataset_id": "ee712460b9af5737", - "file_ext": "fastqsanger", - "meta_files": [], - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1030.dat", - "annotation": null, - "creating_job": "ee712460b9af5737", - "accessible": true, - "name": "abcdeff", - "created_from_basename": null, - "visible": true, - "hid": 8, - "update_time": "2020-09-23T00:23:52.834927", - "history_id": "63cd3858d057a6d1", - "misc_blurb": "207.5 MB", - "peek": "
@M01368:20:000000000-A46F5:1:1101:10294:8369/2<\/td><\/tr>
CTCCAGGCCCATAACACTTGGGGGTAGCTAAAGTGAACTGTATCCGACATCTGGTTCCTACTTCAGGGCCATAAAGCCTAAATAGCCCACACGTTCCCCTTAAATAAGACATCACGATGGATCACAGGTCTATCACCCTATTAACCACTCACGGGAGCTCTCCATGCATTTGGTATTTTCGTCTGGGGGGTATGCACGCGATAGCATTGCGAGACGCTGGAGCCGGAGCACCCTATGTCGCAGTAT<\/td><\/tr>
+<\/td><\/tr>
BBCCCFFCCCCFGGGGGGGGGGGGGGGHHHHHGHHHHHHHHHHHHGGGGGHHHHHHHHHFHGHHHHGGGGCHGHHHHEHFHFHHGHHHHGGGGHHHHHHHHHHFHHFHHGHHHHHGGHGGHHHHHHGHGGGHHHHHHHGFHHGFHHHGHHHHHGGFGGHGHHHHHHHHHHHGHHEHHHHHGHGHGHGGGFFFFFFFFFFFFFDFFFFFFFF;D=DDFFFA.AFFFAA;A.;.AAFFFBBEECDFEF<\/td><\/tr>
@M01368:20:000000000-A46F5:1:1101:10302:11041/2<\/td><\/tr><\/table>", - "history_content_type": "dataset", - "purged": false, - "display_apps": [], - "file_size": 217570404, - "genome_build": "?", - "validated_state": "unknown", - "extension": "fastqsanger", - "url": "/api/histories/63cd3858d057a6d1/contents/ab50e8da6b53023e", - "api_type": "file" - }, - { - "hda_ldda": "hda", - "tags": [], - "type": "file", - "resubmitted": false, - "validated_state_message": null, - "misc_info": "uploaded fastqsanger file", - "deleted": false, - "id": "ee712460b9af5737", - "type_id": "dataset-ee712460b9af5737", - "rerunnable": false, - "display_types": [], - "create_time": "2020-06-24T11:31:25.377836", - "uuid": "a8a88acf-7cd4-4acc-9bf0-23a917e62d94", - "data_type": "galaxy.datatypes.sequence.FastqSanger", - "state": "ok", - "download_url": "/api/histories/63cd3858d057a6d1/contents/ee712460b9af5737/display", - "model_class": "HistoryDatasetAssociation", - "dataset_id": "9d04922baebb355f", - "file_ext": "fastqsanger", - "meta_files": [], - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1029.dat", - "annotation": null, - "creating_job": "9d04922baebb355f", - "accessible": true, - "name": "M117C1-ch_1.fq.fastqsan", - "created_from_basename": null, - "visible": true, - "hid": 7, - "update_time": "2020-09-10T18:21:32.734118", - "history_id": "63cd3858d057a6d1", - "misc_blurb": "207.5 MB", - "peek": "
@M01368:20:000000000-A46F5:1:1101:10294:8369/1<\/td><\/tr>
TATTGAACGTAGGTGCGATAAATAATGGGATGAGGCAGGAATCAAAGACAGATACTGCGACATAGGGTGCTCCGGCTCCAGCGTCTCGCAATGCTATCGCGTGCATACCCCCCAGACGAAAATACCAAATGCATGGAGAGCTCCCGTGAGTGGTTAATAGGGTGATAGACCTGTGATCCATCGTGATGTCTTATTTAAGGGGAACGTGTGGGCTATTTAGGCTTTATGGCCCTGAAGTAGGAACCAGATGT<\/td><\/tr>
+<\/td><\/tr>
BBBBBFFFFBFFGGGGGGGGGGHGFGHHHGHGHHHGGGGGHHHHHHHGGFHHHGHHGHGGGGGHHHHHGGHHHGGGGGGGHHGGGGHHGGGGHHHGHHGGGGGGHHHHHHHGGGGHGGGGGGHGHGHFGHGHHHHHGHFDHHHHGGAEEEDGFGGHFGFHHHGGHHHFGGHHHHHHHHHHHGGAFBFGGGGGFGGGGGGGGGGGGGGGGGFDFFFFFFFFFFFFFFFFFFFFFFF/9:BFFFFF?AF.BF/<\/td><\/tr>
@M01368:20:000000000-A46F5:1:1101:10302:11041/1<\/td><\/tr><\/table>", - "history_content_type": "dataset", - "purged": false, - "display_apps": [], - "file_size": 217620050, - "genome_build": "?", - "validated_state": "unknown", - "extension": "fastqsanger", - "url": "/api/histories/63cd3858d057a6d1/contents/ee712460b9af5737", - "api_type": "file" - }, - { - "hda_ldda": "hda", - "tags": [], - "type": "file", - "resubmitted": false, - "validated_state_message": null, - "misc_info": "uploaded fastqsanger file", - "deleted": false, - "id": "85f94a3b50966dc9", - "type_id": "dataset-85f94a3b50966dc9", - "rerunnable": false, - "display_types": [], - "create_time": "2020-06-24T11:30:35.766010", - "uuid": "fad0ccfa-f164-45d6-b779-1b433178da13", - "data_type": "galaxy.datatypes.sequence.FastqSanger", - "state": "ok", - "download_url": "/api/histories/63cd3858d057a6d1/contents/85f94a3b50966dc9/display", - "model_class": "HistoryDatasetAssociation", - "dataset_id": "5733d8d6dc37a408", - "file_ext": "fastqsanger", - "meta_files": [], - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1026.dat", - "annotation": null, - "creating_job": "5733d8d6dc37a408", - "accessible": true, - "name": "M117-ch_2.fq.fastqsanger", - "created_from_basename": null, - "visible": true, - "hid": 4, - "update_time": "2020-06-24T11:30:47.520492", - "history_id": "63cd3858d057a6d1", - "misc_blurb": "326.1 MB", - "peek": "
@M01368:13:000000000-A41AR:1:1101:10488:10383/2<\/td><\/tr>
CTTCAGGGCCATAAAGCCTAAATAGCCCACACGTTCCCCTTAAATAAGACATCACGATGGATCACAGGTCTATCACCCTATTAACCACTCACGGGAGCTCTCCATGCATTTGGTATTTTCGTCTGGGGGGTATGCACGCGATAGCATTGCGAGACGCTGGAGCCGGAGCACCCTATGTCGCAGTATCTGTCTTTGATTCCTGCCTCATCCCATTATTTATCGCACCTACGTTCAATATTACAGGCGAAC<\/td><\/tr>
+<\/td><\/tr>
CCCCCFFFCCCFGGGGGGGGGGHHHHHHHGGGGHHHHHHHHHHGHGHHHHHHHHHGGHGGHHHHHHHHHHHGHHHHHHHHHHGHHHHGHHHHHGGGGGHHHFHHHHHEHHHHHHFHHHHHGHGHGHFEGGCFGGGGGGGGGGFGGEFFGGGGFFFFFFFFFFFFFF@DAEFEFFFFFFFFFAFFFFFFFFFFFFFEFFFFFFFFFF0BBFFEFFFFBFFF09..:;FFFDFFFBBBBFFF0BFBEFF@B<\/td><\/tr>
@M01368:13:000000000-A41AR:1:1101:10763:9363/2<\/td><\/tr><\/table>", - "history_content_type": "dataset", - "purged": false, - "display_apps": [], - "file_size": 341989137, - "genome_build": "?", - "validated_state": "unknown", - "extension": "fastqsanger", - "url": "/api/histories/63cd3858d057a6d1/contents/85f94a3b50966dc9", - "api_type": "file" - }, - { - "hda_ldda": "hda", - "tags": [], - "type": "file", - "resubmitted": false, - "validated_state_message": null, - "misc_info": "uploaded fastqsanger file", - "deleted": false, - "id": "5733d8d6dc37a408", - "type_id": "dataset-5733d8d6dc37a408", - "rerunnable": false, - "display_types": [], - "create_time": "2020-06-24T11:30:22.204488", - "uuid": "1fbccf35-ef7c-4e67-9e9e-b7878b1db264", - "data_type": "galaxy.datatypes.sequence.FastqSanger", - "state": "ok", - "download_url": "/api/histories/63cd3858d057a6d1/contents/5733d8d6dc37a408/display", - "model_class": "HistoryDatasetAssociation", - "dataset_id": "e41dd4fdd9a586ef", - "file_ext": "fastqsanger", - "meta_files": [], - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1025.dat", - "annotation": null, - "creating_job": "e41dd4fdd9a586ef", - "accessible": true, - "name": "M117-ch_1.fq.fastqsanger", - "created_from_basename": null, - "visible": true, - "hid": 3, - "update_time": "2020-06-24T11:30:32.534455", - "history_id": "63cd3858d057a6d1", - "misc_blurb": "325.9 MB", - "peek": "
@M01368:13:000000000-A41AR:1:1101:10488:10383/1<\/td><\/tr>
CACTTTAGTAAGTATGTTCGCCTGTAATATTGAACGTAGGTGCGATAAATAATGGGATGAGGCAGGAATCAAAGACAGATACTGCGACATAGGGTGCTCCGGCTCCAGCGTCTCGCAATGCTATCGCGTGCATACCCCCCAGACGAAAATACCAAATGCATGGAGAGCTCCCGTGAGTGGTTAATAGGGTGATAGACCTGTGATCCATCGTGATGTCTTATTTAAGGGGAACGTGTGGGCTATTTAGGCTT<\/td><\/tr>
+<\/td><\/tr>
CCCCCFFFFFFFGGGGGGGGGGGHHHHHHHHHGHHHHHHGHHHGGHGGHHHHHHHHHHGHHHGGGGGHHHHHHGHHHHHHHHHHHGGGGGHHHHHGGHHHGGGGGGHHHGFGGHHGGGGHHHHHHGGGGGGHHHHHHHGGGGHGGGGGGHHHHHHHHHHHHHHHHHGHHGHGGGGAEGGGGGGGGEGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGFFFFFFFFBFFFFFFFFFFFFFFFFFFFFFFFFFF<\/td><\/tr>
@M01368:13:000000000-A41AR:1:1101:10763:9363/1<\/td><\/tr><\/table>", - "history_content_type": "dataset", - "purged": false, - "display_apps": [], - "file_size": 341692613, - "genome_build": "?", - "validated_state": "unknown", - "extension": "fastqsanger", - "url": "/api/histories/63cd3858d057a6d1/contents/5733d8d6dc37a408", - "api_type": "file" - }, - { - "hda_ldda": "hda", - "tags": [], - "type": "file", - "resubmitted": false, - "validated_state_message": null, - "misc_info": "uploaded fastqsanger file", - "deleted": false, - "id": "e41dd4fdd9a586ef", - "type_id": "dataset-e41dd4fdd9a586ef", - "rerunnable": false, - "display_types": [], - "create_time": "2020-06-24T11:30:08.266197", - "uuid": "37c3aaf0-c47d-4398-9f82-7f4218fb4c2f", - "data_type": "galaxy.datatypes.sequence.FastqSanger", - "state": "ok", - "download_url": "/api/histories/63cd3858d057a6d1/contents/e41dd4fdd9a586ef/display", - "model_class": "HistoryDatasetAssociation", - "dataset_id": "03feb7df0a14654f", - "file_ext": "fastqsanger", - "meta_files": [], - "file_name": "/Users/mason/Projects/ohsu/galaxy-data-files/001/dataset_1024.dat", - "annotation": null, - "creating_job": "03feb7df0a14654f", - "accessible": true, - "name": "M117-bl_2.fq.fastqsanger", - "created_from_basename": null, - "visible": true, - "hid": 2, - "update_time": "2020-06-24T11:30:20.546660", - "history_id": "63cd3858d057a6d1", - "misc_blurb": "497.4 MB", - "peek": "" % ( - tool_shed, - name, - owner, - changeset_revision, - pir_str, - ) + message += f"" message += "
@M01368:28:000000000-A5KYY:1:1101:10045:12104/2<\/td><\/tr>
CCATAAAGCCTAAATAGCCCACACGTTCCCCTTAAATAAGACATCACGATGGATCACAGGTCTATCACCCTATTAACCACTCACGGGAGCTCTCCATGCATTTGGTATTTTCGTCTGGGGGGTATGCACGCGATAGCATTGCGAGACGCTGGAGCCGGAGCACCCTATGTCGCAGTATCTGTCTTTGATTCCTGCCTCATCCCATTATTTATCGCACCTACGTTCAATAT<\/td><\/tr>
+<\/td><\/tr>
?????<<<B<BBBBB<>CFFFFHEHHH?-CEFFB?ECC??EFFDDGDEHFFDD?A?C??;5AEAEGCFFDFBEDC-AFHHFFFHHD*<DDDCGHHHHHFHFFF@@+77D@=DEFHFFBBA=8.?CEECC????8EEEEECC?.8??AA;A?'8??E;;'82AC*8AECCEEEE2AEEE?EE:?AC::AE?AEE::*0:C?A?EC?C*:?E:/?A?848:?CECEEEEECE<\/td><\/tr>
@M01368:28:000000000-A5KYY:1:1101:10126:23848/2<\/td><\/tr><\/table>", - "history_content_type": "dataset", - "purged": false, - "display_apps": [], - "file_size": 521548589, - "genome_build": "?", - "validated_state": "unknown", - "extension": "fastqsanger", - "url": "/api/histories/63cd3858d057a6d1/contents/e41dd4fdd9a586ef", - "api_type": "file" - } -] \ No newline at end of file diff --git a/client/src/components/providers/History/test/providerTestHelpers.js b/client/src/components/providers/History/test/providerTestHelpers.js deleted file mode 100644 index 7b97fc520a0..00000000000 --- a/client/src/components/providers/History/test/providerTestHelpers.js +++ /dev/null @@ -1,43 +0,0 @@ -import { watchForChange } from "jest/helpers"; -import { defaultPayload } from "components/providers/History/ContentProvider"; - -/** - * Vue adds a bunch of setters and getters so we need to create our own comparison fn - */ -export const sameVueObj = (a, b) => { - return JSON.stringify(a) == JSON.stringify(b); -}; - -/** - * Detects a new emission of a "payload" a standardized object delivered by HistoryContentProvider - * and CollectionContentProvider, skips the default value that's emitted when it initialized - * - * @param {Object} config vm, propName, label, other watchForChange configs - * @return {Promise} promise that resolves with the changed payload - */ -export const payloadChange = async (config = {}) => { - const { vm, propName = "payload", label = "payload change", skipDefault = false, ...cfg } = config; - const result = await watchForChange({ vm, propName, label, ...cfg }); - - // can see how fast the response comes back by monitoring this result - // console.log("payloadChange result", JSON.stringify(result.newVal)); - // chopout the placeholder flag for the default - // eslint-disable-next-line no-unused-vars - const { placeholder, ...payload } = result.newVal; - if (skipDefault && sameVueObj(payload, defaultPayload)) { - return await payloadChange(config); - } - - return payload; -}; - -export const reportPayload = (payload, { indexKey = "hid", label = "payload" } = {}) => { - const { contents = [], startKeyIndex, ...resultFields } = payload; - const keys = contents.map((o, idx) => { - const thisKey = o[indexKey]; - const keyString = idx == startKeyIndex ? `${thisKey}*` : thisKey; - return keyString; - }); - const report = { keys, startKeyIndex, ...resultFields }; - console.log(label, report); -}; diff --git a/client/src/components/providers/History/test/testCollection.js b/client/src/components/providers/History/test/testCollection.js deleted file mode 100644 index 70c6d59899a..00000000000 --- a/client/src/components/providers/History/test/testCollection.js +++ /dev/null @@ -1,25 +0,0 @@ -// Doctored collection & children for unit tests -import rawCollection from "./json/DatasetCollection"; -import rawNestedCollection from "./json/DatasetCollection.nested.json"; -import rawCollectionContent from "./json/collectionContent.json"; -import rawNestedCollectionContent from "./json/collectionContent.nested.json"; - -// Sample collection for CCP tests - -const parent_url = "/foo"; - -export const testCollectionContent = rawCollectionContent.map((props) => ({ ...props, parent_url })); - -export const testCollection = Object.assign({}, rawCollection, { - contents_url: parent_url, - element_count: testCollectionContent.length, -}); - -export const testNestedCollection = Object.assign({}, rawNestedCollection, { - parent_url: parent_url, - contents_url: "/foo/bar", - element_count: rawNestedCollectionContent.length, -}); - -// a list of element_indexes handy for testing -export const indices = (list) => list.map(({ element_index, _id, parent_url }) => ({ element_index, _id, parent_url })); diff --git a/client/src/components/providers/History/test/testHistory.js b/client/src/components/providers/History/test/testHistory.js deleted file mode 100644 index 6371bc79005..00000000000 --- a/client/src/components/providers/History/test/testHistory.js +++ /dev/null @@ -1,29 +0,0 @@ -import { History } from "components/History/model"; -import { SearchParams } from "components/providers/History/SearchParams"; -import rawHistory from "./json/History.json"; -import rawHistoryContent from "./json/historyContent.json"; - -// doctor the sample content -export const testHistoryContent = rawHistoryContent.map((item) => { - item.history_id = rawHistory.id; - return item; -}); - -// doctor the sample history -export const testHistory = new History({ - ...rawHistory, - hid_counter: testHistoryContent[0].hid + 1, -}); - -// simplified filter simlates server-side complete set -export const serverContent = (filters = new SearchParams()) => { - return testHistoryContent.filter((o) => { - if (!filters.showDeleted && o.deleted) { - return false; - } - if (!filters.showHidden && !o.visible) { - return false; - } - return true; - }); -}; diff --git a/client/src/components/providers/InvocationStepProvider.js b/client/src/components/providers/InvocationStepProvider.js index 1d0808bdeff..b2352e4e888 100644 --- a/client/src/components/providers/InvocationStepProvider.js +++ b/client/src/components/providers/InvocationStepProvider.js @@ -1,11 +1,24 @@ -import { default as RxProviderMixin } from "./rxProviders"; -import { invocationStepMonitor } from "./monitors"; +import axios from "axios"; +import { getAppRoot } from "onload/loadConfig"; +import { SingleQueryProvider } from "components/providers/SingleQueryProvider"; +import { rethrowSimple } from "utils/simple-error"; -export default { - mixins: [RxProviderMixin], - methods: { - buildMonitor() { - return invocationStepMonitor(); - }, - }, +const TERMINAL_JOB_STATES = ["ok", "error", "deleted", "paused"]; + +async function getInvocation({ id }) { + try { + const { data } = await axios.get(`${getAppRoot()}api/invocations/any/steps/${id}`); + return data; + } catch (e) { + rethrowSimple(e); + } +} + +const stepIsTerminal = (step) => { + const isTerminal = + ["scheduled", "cancelled", "failed"].includes(step.state) && + step.jobs.every((job) => TERMINAL_JOB_STATES.includes(job.state)); + return isTerminal; }; + +export default SingleQueryProvider(getInvocation, stepIsTerminal); diff --git a/client/src/components/providers/JobProvider.js b/client/src/components/providers/JobProvider.js index 484432e3415..af5ddf42b43 100644 --- a/client/src/components/providers/JobProvider.js +++ b/client/src/components/providers/JobProvider.js @@ -2,6 +2,7 @@ import axios from "axios"; import { getAppRoot } from "onload/loadConfig"; import { SingleQueryProvider } from "components/providers/SingleQueryProvider"; import { rethrowSimple } from "utils/simple-error"; +import { stateIsTerminal } from "./utils"; async function jobDetails({ jobid }) { const url = `${getAppRoot()}api/jobs/${jobid}?full=True`; @@ -23,5 +24,5 @@ async function jobProblems({ jobid }) { } } -export const JobDetailsProvider = SingleQueryProvider(jobDetails); -export const JobProblemProvider = SingleQueryProvider(jobProblems); +export const JobDetailsProvider = SingleQueryProvider(jobDetails, stateIsTerminal); +export const JobProblemProvider = SingleQueryProvider(jobProblems, stateIsTerminal); diff --git a/client/src/components/providers/History/UserHistories/MockCurrentHistory.js b/client/src/components/providers/MockCurrentHistory.js similarity index 100% rename from client/src/components/providers/History/UserHistories/MockCurrentHistory.js rename to client/src/components/providers/MockCurrentHistory.js diff --git a/client/src/components/providers/SingleQueryProvider.js b/client/src/components/providers/SingleQueryProvider.js index 98b33727d57..8cdc310e5e6 100644 --- a/client/src/components/providers/SingleQueryProvider.js +++ b/client/src/components/providers/SingleQueryProvider.js @@ -1,4 +1,5 @@ import hash from "object-hash"; +import { LastQueue } from "utils/promise-queue"; /** * Builds a provider that gets its result from a single promise-based query function and @@ -6,60 +7,94 @@ import hash from "object-hash"; * * @param {Function} lookup async function that loads the result, parameters will be an object * whose properties are the attributes assigned to the provider component + * @param {Function} stopRefresh function that will be called with result of lookup. + * If function returns true refresh will be stopped. * @return {VueComponentOptions} Vue component options definition */ -export const SingleQueryProvider = (lookup) => { +export const SingleQueryProvider = (lookup, stopRefresh = (result) => false) => { const promiseCache = new Map(); - return { props: { useCache: { type: Boolean, - default: true, + default: false, + }, + autoRefresh: { + type: Boolean, + default: false, + }, + autoTime: { + type: Number, + default: 3000, }, }, data() { return { - result: undefined, - error: undefined, + result: null, + error: null, + timeoutId: null, }; }, + created() { + this.queue = new LastQueue(this.autoTime); + }, computed: { loading() { - return this.result === undefined; + return this.result === null; }, cacheKey() { return hash(this.$attrs || {}); }, }, mounted() { - let lookupPromise; - if (this.useCache) { - lookupPromise = promiseCache.get(this.cacheKey); - if (!lookupPromise) { - lookupPromise = lookup(this.$attrs); - promiseCache.set(this.cacheKey, lookupPromise); - } - } else { - lookupPromise = lookup(this.$attrs); + this.doQuery(); + }, + destroyed() { + if (this.timeoutId) { + clearTimeout(this.timeoutId); } - lookupPromise.then( - (result) => { - this.result = result; - }, - (err) => { - this.result = {}; - this.error = err; - this.$emit("error", err); - } - ); }, render() { - return this.$scopedSlots.default({ - loading: this.loading, - result: this.result, - error: this.error, - }); + return ( + this.$scopedSlots.default && + this.$scopedSlots.default({ + loading: this.loading, + result: this.result, + error: this.error, + }) + ); + }, + methods: { + doQuery() { + let lookupPromise; + if (this.useCache) { + lookupPromise = promiseCache.get(this.cacheKey); + if (!lookupPromise) { + lookupPromise = lookup(this.$attrs); + promiseCache.set(this.cacheKey, lookupPromise); + } + } else { + lookupPromise = this.queue.enqueue(lookup, this.$attrs); + } + lookupPromise.then( + (result) => { + this.result = result; + this.$emit("update:result", result); + this.error = null; + if (this.autoRefresh && !stopRefresh(result)) { + this.timeoutId = setTimeout(() => { + this.doQuery(); + }, this.autoTime); + } + }, + (err) => { + this.result = {}; + this.error = err; + this.$emit("error", err); + console.debug("Failed to fulfill promise.", err); + } + ); + }, }, }; }; diff --git a/client/src/components/providers/UrlDataProvider.js b/client/src/components/providers/UrlDataProvider.js index a7051ceef71..02cf92790f3 100644 --- a/client/src/components/providers/UrlDataProvider.js +++ b/client/src/components/providers/UrlDataProvider.js @@ -5,7 +5,9 @@ import { rethrowSimple } from "utils/simple-error"; async function urlData({ url }) { try { + console.debug("Requesting data from: ", url); const { data } = await axios.get(`${getAppRoot()}${url}`); + console.debug("Received response"); return data; } catch (e) { rethrowSimple(e); diff --git a/client/src/components/providers/History/UserHistories/UserHistories.js b/client/src/components/providers/UserHistories.js similarity index 100% rename from client/src/components/providers/History/UserHistories/UserHistories.js rename to client/src/components/providers/UserHistories.js diff --git a/client/src/components/providers/History/UserHistories/UserHistories.test.js b/client/src/components/providers/UserHistories.test.js similarity index 99% rename from client/src/components/providers/History/UserHistories/UserHistories.test.js rename to client/src/components/providers/UserHistories.test.js index f3dfa4c6264..23eb9cdb8be 100644 --- a/client/src/components/providers/History/UserHistories/UserHistories.test.js +++ b/client/src/components/providers/UserHistories.test.js @@ -35,7 +35,6 @@ import { } from "components/History/model/queries"; jest.mock("app"); -jest.mock("components/providers/History/caching"); jest.mock("components/History/model/queries"); const maxServerDelay = 60; diff --git a/client/src/components/providers/fetch.js b/client/src/components/providers/fetch.js deleted file mode 100644 index 185bc7d07cb..00000000000 --- a/client/src/components/providers/fetch.js +++ /dev/null @@ -1,25 +0,0 @@ -import { pipe } from "rxjs"; -import { map, mergeMap } from "rxjs/operators"; -import { ajax } from "rxjs/ajax"; -import { prependPath } from "utils/redirect"; - -export const fetchDatasetById = () => { - return pipe( - map((id) => prependPath(`/api/datasets/${id}`)), - mergeMap((url) => ajax.getJSON(url)) - ); -}; - -export const fetchDatasetCollectionById = () => { - return pipe( - map((id) => prependPath(`/api/dataset_collections/${id}?instance_type=history`)), - mergeMap((url) => ajax.getJSON(url)) - ); -}; - -export const fetchInvocationStepById = () => { - return pipe( - map((id) => prependPath(`/api/invocations/any/steps/${id}`)), - mergeMap((url) => ajax.getJSON(url)) - ); -}; diff --git a/client/src/components/providers/monitors/index.js b/client/src/components/providers/monitors/index.js deleted file mode 100644 index 1eccdcc0dc9..00000000000 --- a/client/src/components/providers/monitors/index.js +++ /dev/null @@ -1,10 +0,0 @@ -// Public exports -export { - buildHistoryMonitor, - datasetMonitor, - datasetCollectionMonitor, - getHistoryMonitor, - invocationStepMonitor, -} from "./monitors"; - -export { monitorHistoryUntilTrue } from "./monitorHistoriesUntilTrue"; diff --git a/client/src/components/providers/monitors/monitorHistoriesUntilTrue.js b/client/src/components/providers/monitors/monitorHistoriesUntilTrue.js deleted file mode 100644 index 7be014c8439..00000000000 --- a/client/src/components/providers/monitors/monitorHistoriesUntilTrue.js +++ /dev/null @@ -1,19 +0,0 @@ -import { concat } from "rxjs"; -import { share, take, takeWhile } from "rxjs/operators"; -import { getHistoryMonitor } from "./monitors"; - -export const monitorHistoryUntilTrue = (condition, historyId, monitorEvery = 3000) => { - // monitorHistory until condition is true, then fetch one last update - const historyMonitor$ = getHistoryMonitor(historyId, monitorEvery); - const primaryHistoryMonitor$ = historyMonitor$.pipe( - takeWhile(() => !condition()), - share() - ); - return concat( - primaryHistoryMonitor$, - historyMonitor$.pipe( - // get one more update after invocation is terminal - take(1) - ) - ); -}; diff --git a/client/src/components/providers/monitors/monitorHistoriesUntilTrue.test.js b/client/src/components/providers/monitors/monitorHistoriesUntilTrue.test.js deleted file mode 100644 index d151939d31e..00000000000 --- a/client/src/components/providers/monitors/monitorHistoriesUntilTrue.test.js +++ /dev/null @@ -1,47 +0,0 @@ -import { of } from "rxjs"; -import { ObserverSpy } from "@hirez_io/observer-spy"; -import { loadHistoryContents } from "components/providers/History/caching"; -import { monitorHistoryUntilTrue } from "./monitorHistoriesUntilTrue"; - -jest.mock("components/providers/History/caching"); - -// prettier-ignore -describe("monitorHistoryUntilTrue", () => { - let monitor$; - let spy; - const historyId = 1; - let stopMonitor; - const stopFn = () => stopMonitor == true; - let mockLoadHistoryContent; - let completed; - - beforeEach(async () => { - stopMonitor = false; - completed = false; - mockLoadHistoryContent = of(1, 2, 3); - loadHistoryContents.mockImplementation(() => () => mockLoadHistoryContent); - monitor$ = monitorHistoryUntilTrue(stopFn, historyId, 20); - }); - - test("that history subscription runs until condition is met, and then once more", async () => { - // spy on output of observable; - spy = new ObserverSpy(); - monitor$.subscribe(spy); - monitor$.subscribe({ - next: (val) => { - // stop once we reach 2 - if (val >= 2) { - stopMonitor = true; - } - }, - complete: () => { - completed = true; - } - }) - await spy.onComplete(); - - // We stop at 2, and then take one more value. - expect(spy.getValues()).toEqual([1, 2, 1]); - expect(completed).toBeTruthy(); - }); -}); diff --git a/client/src/components/providers/monitors/monitors.js b/client/src/components/providers/monitors/monitors.js deleted file mode 100644 index c946a287a92..00000000000 --- a/client/src/components/providers/monitors/monitors.js +++ /dev/null @@ -1,128 +0,0 @@ -import { of, race, pipe, concat, iif } from "rxjs"; -import { filter, map, mergeMap, switchMap, delay, share, repeat, take, takeWhile } from "rxjs/operators"; -import { cacheContent, monitorContentQuery, loadHistoryContents } from "components/providers/History/caching"; -import { fetchDatasetById, fetchDatasetCollectionById, fetchInvocationStepById } from "../fetch"; - -// prettier-ignore -export const datasetMonitor = (cfg = {}) => { - // delay gives the monitor a head-start so it can - // spin up a little before we decide to do an ajax call - const { spinUpDelay } = cfg; - - return pipe( - switchMap((id) => createContentMonitor(id, 'dataset', spinUpDelay)) - ); -}; - -export const datasetCollectionMonitor = (cfg = {}) => { - const { spinUpDelay } = cfg; - return pipe(switchMap((id) => createContentMonitor(id, "dataset_collection", spinUpDelay))); -}; - -const createContentMonitor = (id, contentType, spinUpDelay = 250) => { - let fetcher; - switch (contentType) { - case "dataset": - fetcher = fetchDatasetById; - break; - case "dataset_collection": - fetcher = fetchDatasetCollectionById; - break; - default: - console.error(`Can't create monitor for ${contentType}-${id}`); - } - return createMonitor(id, contentType, fetcher, spinUpDelay); -}; - -const buildPouchRequest = (id, contentType) => { - return { - selector: { - id: { $eq: id }, - history_content_type: contentType, - }, - }; -}; - -// prettier-ignore -const createMonitor = (id, contentType, fetcher, spinUpDelay = 250) => { - // build the pouchdb/mongo request, which is a selector - // and limits, offsets, etc - const request = buildPouchRequest(id, contentType); - - // retrieve changes from cache - const changes$ = of(request).pipe( - monitorContentQuery(), - // singleUpdateResult(), - filter(change => change.match && change.doc), - map(change => change.doc), - share() - ); - - // cache results that reflect non-deleted existing data in the cache - const existing$ = changes$.pipe( - filter(Boolean), - ); - - // load and cache dataset from server then switch over to the monitor - const fetchItem$ = of(id).pipe( - delay(spinUpDelay), - fetcher(), - mergeMap((rawJson) => cacheContent(rawJson, true)) - ); - - // let the monitor and the ajax call race, first one wins - const firstValue$ = race(fetchItem$, existing$).pipe(take(1)); - - return concat(firstValue$, changes$); -}; - -const TERMINAL_JOB_STATES = ["ok", "error", "deleted", "paused"]; -const stepIsTerminal = (step) => { - const isTerminal = - ["scheduled", "cancelled", "failed"].includes(step.state) && - step.jobs.every((job) => TERMINAL_JOB_STATES.includes(job.state)); - return isTerminal; -}; - -const createInvocationStepMonitor = (id, monitorEvery = 3000) => { - const initialFetch$ = of(id).pipe(fetchInvocationStepById()); - const pollingFetch$ = of(id).pipe( - delay(monitorEvery), - fetchInvocationStepById(), - repeat(), - // takeWhile cancels source on true, so also cancels repeat - takeWhile((val) => !stepIsTerminal(val), true) - ); - return initialFetch$.pipe( - // poll only if initial status is non-terminal. If polling, concat the initial result we already have - // with pollingFetch$ which emits after the delay - mergeMap((val) => iif(() => stepIsTerminal(val), of(val), concat(of(val), pollingFetch$))) - ); -}; - -export const invocationStepMonitor = (monitorEvery = 3000) => - pipe(switchMap((id) => createInvocationStepMonitor(id, monitorEvery))); - -// feed observables, keyed by history id. Keeps just one historyMonitor around. -export const historyFeeds = new Map(); - -export const getHistoryMonitor = (historyId, monitorEvery = 3000) => { - if (!historyFeeds.has(historyId)) { - historyFeeds.set(historyId, buildHistoryMonitor(historyId, monitorEvery)); - } - return historyFeeds.get(historyId); -}; - -// prettier-ignore -export const buildHistoryMonitor = (historyId, monitorEvery = 3000) => { - // temporary hack until we can subscribe to invocations and their outputs. - // set large windowSize around which to monitor (could we just monitor all updates?) - const windowSize = 100000; - return of([historyId, {showHidden: true, showVisible: true}, 1]).pipe( - // noInitial skips fetching exisiting datasets and only monitors - // for updated datastes after the last query (or now, if this is the first query) - loadHistoryContents({windowSize, noInitial: true}), - delay(monitorEvery), - repeat(), - share()); -}; diff --git a/client/src/components/providers/monitors/monitors.test.js b/client/src/components/providers/monitors/monitors.test.js deleted file mode 100644 index 4a3c434a08b..00000000000 --- a/client/src/components/providers/monitors/monitors.test.js +++ /dev/null @@ -1,42 +0,0 @@ -import { of } from "rxjs"; -import { ObserverSpy } from "@hirez_io/observer-spy"; -import { invocationStepMonitor } from "./monitors"; -import * as fetch from "../fetch"; - -describe("invocationStepMonitor", () => { - let monitor$; - let spy; - let fetchInvocationMock; - const invocationId = 1; - const invocationStepDataNew = { jobs: [{ state: "new" }], state: "scheduled" }; - const invocationStepDataRunning = { jobs: [{ state: "running" }], state: "scheduled" }; - const invocationStepDataOk = { jobs: [{ state: "ok" }], state: "scheduled" }; - - beforeEach(async () => { - fetchInvocationMock = jest.spyOn(fetch, "fetchInvocationStepById"); - spy = new ObserverSpy(); - }); - - test("invocationStepMonitor runs until step and job are terminal", async () => { - // mocks fetchInvocationStep in initialFetch$ - fetchInvocationMock.mockImplementationOnce(() => () => of(invocationStepDataNew)); - // mocks fetchInvocationStep in pollingFetch$ - fetchInvocationMock.mockImplementationOnce( - () => () => of(invocationStepDataRunning, invocationStepDataOk, invocationStepDataOk) - ); - monitor$ = of(invocationId).pipe(invocationStepMonitor(20)); - monitor$.subscribe(spy); - await spy.onComplete(); - expect(spy.getValues()).toEqual([invocationStepDataNew, invocationStepDataRunning, invocationStepDataOk]); - }); - test("invocationStepMonitor terminates immediately if step is terminal", async () => { - fetchInvocationMock.mockImplementationOnce(() => () => of(invocationStepDataOk)); - fetchInvocationMock.mockImplementationOnce( - () => () => of(invocationStepDataRunning, invocationStepDataOk, invocationStepDataOk, invocationStepDataOk) - ); - monitor$ = of(invocationId).pipe(invocationStepMonitor(20)); - monitor$.subscribe(spy); - await spy.onComplete(); - expect(spy.getValues()).toEqual([invocationStepDataOk]); - }); -}); diff --git a/client/src/components/providers/rxProviders.js b/client/src/components/providers/rxProviders.js deleted file mode 100644 index 534cfeee426..00000000000 --- a/client/src/components/providers/rxProviders.js +++ /dev/null @@ -1,46 +0,0 @@ -export default { - props: { - id: { type: String, required: true }, - }, - data() { - return { - item: undefined, - error: undefined, - }; - }, - computed: { - loading() { - return this.item === undefined && this.error === undefined; - }, - }, - methods: { - buildMonitor() { - throw new Error("define me"); - }, - }, - created() { - const id$ = this.watch$("id"); - const monitor$ = id$.pipe(this.buildMonitor()); - - this.listenTo(monitor$, { - next: (ds) => { - this.item = ds; - this.error = undefined; - }, - error: (err) => { - this.error = err.response.err_msg; - console.warn("error in content monitor", err); - }, - complete: () => { - console.log("I should only complete on destroy"); - }, - }); - }, - render() { - return this.$scopedSlots.default({ - loading: this.loading, - item: this.item, - error: this.error, - }); - }, -}; diff --git a/client/src/components/providers/History/test/json/Dataset.json b/client/src/components/providers/test/json/Dataset.json similarity index 100% rename from client/src/components/providers/History/test/json/Dataset.json rename to client/src/components/providers/test/json/Dataset.json diff --git a/client/src/components/providers/History/test/json/DatasetCollection.error.json b/client/src/components/providers/test/json/DatasetCollection.error.json similarity index 100% rename from client/src/components/providers/History/test/json/DatasetCollection.error.json rename to client/src/components/providers/test/json/DatasetCollection.error.json diff --git a/client/src/components/providers/History/test/json/DatasetCollection.heterogeneous.json b/client/src/components/providers/test/json/DatasetCollection.heterogeneous.json similarity index 100% rename from client/src/components/providers/History/test/json/DatasetCollection.heterogeneous.json rename to client/src/components/providers/test/json/DatasetCollection.heterogeneous.json diff --git a/client/src/components/providers/History/test/json/DatasetCollection.homogeneous.json b/client/src/components/providers/test/json/DatasetCollection.homogeneous.json similarity index 100% rename from client/src/components/providers/History/test/json/DatasetCollection.homogeneous.json rename to client/src/components/providers/test/json/DatasetCollection.homogeneous.json diff --git a/client/src/components/providers/History/test/json/DatasetCollection.json b/client/src/components/providers/test/json/DatasetCollection.json similarity index 100% rename from client/src/components/providers/History/test/json/DatasetCollection.json rename to client/src/components/providers/test/json/DatasetCollection.json diff --git a/client/src/components/providers/History/test/json/DatasetCollection.nested.json b/client/src/components/providers/test/json/DatasetCollection.nested.json similarity index 100% rename from client/src/components/providers/History/test/json/DatasetCollection.nested.json rename to client/src/components/providers/test/json/DatasetCollection.nested.json diff --git a/client/src/components/providers/History/test/json/DatasetCollection.processing.json b/client/src/components/providers/test/json/DatasetCollection.processing.json similarity index 100% rename from client/src/components/providers/History/test/json/DatasetCollection.processing.json rename to client/src/components/providers/test/json/DatasetCollection.processing.json diff --git a/client/src/components/providers/utils.js b/client/src/components/providers/utils.js new file mode 100644 index 00000000000..b06b25c4aa6 --- /dev/null +++ b/client/src/components/providers/utils.js @@ -0,0 +1,5 @@ +import JOB_STATES_MODEL from "mvc/history/job-states-model"; + +export function stateIsTerminal(result) { + return !JOB_STATES_MODEL.NON_TERMINAL_STATES.includes(result.state); +} diff --git a/client/src/entry/analysis/AnalysisRouter.js b/client/src/entry/analysis/AnalysisRouter.js index f3d687eda11..388ae22febd 100644 --- a/client/src/entry/analysis/AnalysisRouter.js +++ b/client/src/entry/analysis/AnalysisRouter.js @@ -17,40 +17,40 @@ import decodeUriComponent from "decode-uri-component"; import Router from "layout/router"; import ToolForm from "components/Tool/ToolForm"; import FormGeneric from "components/Form/FormGeneric"; -import Sharing from "components/Sharing/Sharing.vue"; -import UserPreferences from "components/User/UserPreferences.vue"; -import DatasetList from "components/Dataset/DatasetList.vue"; +import Sharing from "components/Sharing/Sharing"; +import UserPreferences from "components/User/UserPreferences"; +import DatasetList from "components/Dataset/DatasetList"; import { getUserPreferencesModel } from "components/User/UserPreferencesModel"; -import CustomBuilds from "components/User/CustomBuilds.vue"; +import CustomBuilds from "components/User/CustomBuilds"; import Tours from "mvc/tours"; import GridView from "mvc/grid/grid-view"; import GridShared from "mvc/grid/grid-shared"; -import JobDetails from "components/JobInformation/JobDetails.vue"; -import WorkflowImport from "components/Workflow/WorkflowImport.vue"; -import TrsImport from "components/Workflow/TrsImport.vue"; -import TrsSearch from "components/Workflow/TrsSearch.vue"; -import InteractiveTools from "components/InteractiveTools/InteractiveTools.vue"; -import WorkflowList from "components/Workflow/WorkflowList.vue"; -import CollectionEditView from "components/Collections/common/CollectionEditView.vue"; -import HistoryImport from "components/HistoryImport.vue"; +import JobDetails from "components/JobInformation/JobDetails"; +import WorkflowImport from "components/Workflow/WorkflowImport"; +import TrsImport from "components/Workflow/TrsImport"; +import TrsSearch from "components/Workflow/TrsSearch"; +import InteractiveTools from "components/InteractiveTools/InteractiveTools"; +import WorkflowList from "components/Workflow/WorkflowList"; +import CollectionEditView from "components/Collections/common/CollectionEditView"; +import HistoryImport from "components/HistoryImport"; import { HistoryExport } from "components/HistoryExport/index"; -import HistoryView from "components/HistoryView.vue"; -import WorkflowInvocationReport from "components/Workflow/InvocationReport.vue"; -import WorkflowRun from "components/Workflow/Run/WorkflowRun.vue"; +import HistoryView from "components/HistoryView"; +import WorkflowInvocationReport from "components/Workflow/InvocationReport"; +import WorkflowRun from "components/Workflow/Run/WorkflowRun"; import UserInvocations from "components/Workflow/UserInvocations"; -import ToolsView from "components/ToolsView/ToolsView.vue"; -import ToolsJson from "components/ToolsView/ToolsSchemaJson/ToolsJson.vue"; +import ToolsView from "components/ToolsView/ToolsView"; +import ToolsJson from "components/ToolsView/ToolsSchemaJson/ToolsJson"; import HistoryList from "mvc/history/history-list"; import VisualizationsList from "components/Visualizations/Index"; import QueryStringParsing from "utils/query-string-parsing"; import DatasetError from "components/DatasetInformation/DatasetError"; import DatasetAttributes from "components/DatasetInformation/DatasetAttributes"; -import Citations from "components/Citation/Citations.vue"; -import DisplayStructure from "components/DisplayStructured.vue"; +import Citations from "components/Citation/Citations"; +import DisplayStructure from "components/DisplayStructured"; import { CloudAuth } from "components/User/CloudAuth"; import { ExternalIdentities } from "components/User/ExternalIdentities"; -import Confirmation from "components/login/Confirmation.vue"; -import DatasetDetails from "components/Details/DatasetDetails.vue"; +import Confirmation from "components/login/Confirmation"; +import DatasetDetails from "components/DatasetInformation/DatasetDetails"; import Libraries from "components/Libraries"; import { mountVueComponent } from "utils/mountVueComponent"; diff --git a/client/src/entry/analysis/index.js b/client/src/entry/analysis/index.js index 8898b967012..f68c2772f21 100644 --- a/client/src/entry/analysis/index.js +++ b/client/src/entry/analysis/index.js @@ -5,7 +5,7 @@ import MvcHistoryPanel from "entry/panels/history-panel"; import Page from "layout/page"; // Vue adapter emulates current features of backbone history panel -import { HistoryPanelProxy } from "components/History"; +import { HistoryPanelProxy } from "components/History/adapters/HistoryPanelProxy"; import { isBetaHistoryOpen } from "components/History/adapters/betaToggle"; addInitialization((Galaxy, { options = {} }) => { diff --git a/client/src/entry/panels/history-panel.js b/client/src/entry/panels/history-panel.js index 6780cd37987..77a85e692ea 100644 --- a/client/src/entry/panels/history-panel.js +++ b/client/src/entry/panels/history-panel.js @@ -72,7 +72,7 @@ const HistoryPanel = Backbone.View.extend({ this.model = new Backbone.Model({ // define components - cls: "history-right-panel history-details", + cls: "history-right-panel details", title: _l("History"), buttons: panelHeaderButtons, }); diff --git a/client/src/mvc/annotation.js b/client/src/mvc/annotation.js index cc94408adb0..7382a2e597c 100644 --- a/client/src/mvc/annotation.js +++ b/client/src/mvc/annotation.js @@ -54,7 +54,7 @@ var AnnotationEditor = Backbone.View.extend(baseMVC.LoggableMixin) _l("Annotation"), "", // set up initial tags by adding as CSV to input vals (necc. to init select2) - '
', + '
', _.escape(annotation), "
", ].join(""); diff --git a/client/src/mvc/dataset/dataset-li-edit.js b/client/src/mvc/dataset/dataset-li-edit.js index 7539329a166..7931bddc5da 100644 --- a/client/src/mvc/dataset/dataset-li-edit.js +++ b/client/src/mvc/dataset/dataset-li-edit.js @@ -268,44 +268,27 @@ var DatasetListItemEdit = _super.extend( /** Render an icon-button or popupmenu of links based on the applicable visualizations */ _renderVisualizationsButton: function () { - //TODO: someday - lazyload visualizations const Galaxy = getGalaxyInstance(); - const visualizations = this.model.get("visualizations"); - if ( - !Galaxy.config.visualizations_visible || - this.model.isDeletedOrPurged() || - !this.hasUser || - !this.model.hasData() || - _.isEmpty(visualizations) - ) { - return null; - } - if (!_.isObject(visualizations[0])) { - this.warn("Visualizations have been switched off"); - return null; - } - if (visualizations.length >= 1) { - const dsid = this.model.get("element_id") || this.model.get("id"); - const url = getAppRoot() + "visualizations?dataset_id=" + dsid; - return faIconButton({ - title: _l("Visualize this data"), - href: url, - classes: "visualization-link", - faIcon: "fa-bar-chart-o", - onclick: (ev) => { - if (Galaxy.frame && Galaxy.frame.active) { - ev.preventDefault(); - Galaxy.frame.add({ url: url, title: "Visualization" }); - } else if (Galaxy.router) { - ev.preventDefault(); - Galaxy.router.push("visualizations", { - dataset_id: dsid, - }); - Galaxy.trigger("activate-hda", dsid); - } - }, - }); - } + const dsid = this.model.get("element_id") || this.model.get("id"); + const url = getAppRoot() + "visualizations?dataset_id=" + dsid; + return faIconButton({ + title: _l("Visualize this data"), + href: url, + classes: "visualization-link", + faIcon: "fa-bar-chart-o", + onclick: (ev) => { + if (Galaxy.frame && Galaxy.frame.active) { + ev.preventDefault(); + Galaxy.frame.add({ url: url, title: "Visualization" }); + } else if (Galaxy.router) { + ev.preventDefault(); + Galaxy.router.push("visualizations", { + dataset_id: dsid, + }); + Galaxy.trigger("activate-hda", dsid); + } + }, + }); }, /** Render the tags list/control */ diff --git a/client/src/mvc/dataset/dataset-model.js b/client/src/mvc/dataset/dataset-model.js index bc69f77b972..d968c3c7585 100644 --- a/client/src/mvc/dataset/dataset-model.js +++ b/client/src/mvc/dataset/dataset-model.js @@ -103,19 +103,25 @@ var DatasetAssociation = Backbone.Model.extend(BASE_MVC.LoggableMixin).extend( this.trigger("state:ready", currModel, newState, this.previous("state")); if (newState != "discarded") { if (newState === "ok") { - new Notification(`Job complete: ${this.get("name")}`, { - icon: "static/favicon.ico", - }); + // If Notifications are supported, send one. + if (window.Notification) { + new Notification(`Job complete: ${this.get("name")}`, { + icon: "static/favicon.ico", + }); + } if (TAB_UPDATES.is_hidden()) { TAB_UPDATES.hidden_count(hiddenupdates); hiddenupdates++; } } else if (newState == "error") { - new Notification(`Job failure: ${this.get("name")}`, { - icon: "static/erricon.ico", - }); - if (TAB_UPDATES.is_hidden() && Notification.permission == "granted") { - TAB_UPDATES.change_favicon("static/erricon.ico"); + // If Notifications are supported, send one. + if (window.Notification) { + new Notification(`Job failure: ${this.get("name")}`, { + icon: "static/erricon.ico", + }); + if (TAB_UPDATES.is_hidden() && Notification.permission == "granted") { + TAB_UPDATES.change_favicon("static/erricon.ico"); + } } } } diff --git a/client/src/mvc/history/history-view.js b/client/src/mvc/history/history-view.js index 7eae932a382..e566b3d8a67 100644 --- a/client/src/mvc/history/history-view.js +++ b/client/src/mvc/history/history-view.js @@ -521,14 +521,14 @@ var HistoryView = _super.extend( HistoryView.prototype.templates = (() => { var mainTemplate = () => `
-
+
    ',
    `; var controlsTemplate = BASE_MVC.wrapTemplate( [ - '
    ', + '
    ', '
    ', '
    <%- history.name %>
    ', "
    ", diff --git a/client/src/mvc/rules/rule-definitions.js b/client/src/mvc/rules/rule-definitions.js index dda2879ed47..0a69c965454 100644 --- a/client/src/mvc/rules/rule-definitions.js +++ b/client/src/mvc/rules/rule-definitions.js @@ -904,3 +904,5 @@ export default { RULES: RULES, MAPPING_TARGETS: MAPPING_TARGETS, }; + +export { RULES, MAPPING_TARGETS }; diff --git a/client/src/mvc/ui/ui-misc.js b/client/src/mvc/ui/ui-misc.js index d32c0cdc7da..f6a755145ab 100644 --- a/client/src/mvc/ui/ui-misc.js +++ b/client/src/mvc/ui/ui-misc.js @@ -86,8 +86,7 @@ export var Input = Backbone.View.extend({ input: "_onchange", }, value: function (new_val) { - new_val !== undefined && - this.model.set("value", new_val === null || typeof new_val === "string" ? new_val : ""); + new_val !== undefined && this.model.set("value", typeof new_val === "string" ? new_val : ""); return this.model.get("value"); }, render: function () { diff --git a/client/src/store/index.js b/client/src/store/index.js index b6b06ed2963..734d60ffb22 100644 --- a/client/src/store/index.js +++ b/client/src/store/index.js @@ -5,7 +5,8 @@ import Vue from "vue"; import Vuex from "vuex"; import createCache from "vuex-cache"; -import createPersistedState from "vuex-persistedstate"; +import VuexPersistence from "vuex-persist"; +import localForage from "localforage"; import config from "config"; @@ -35,19 +36,28 @@ import { syncVuextoGalaxy } from "./syncVuextoGalaxy"; Vue.use(Vuex); -const panelsState = createPersistedState({ - paths: ["panels"], +const galaxyStorage = localForage.createInstance({}); +galaxyStorage.config({ + driver: [localForage.INDEXEDDB, localForage.LOCALSTORAGE], + name: "galaxyIndexedDB", + version: 1.0, + storeName: "galaxyStore", +}); + +const panelsPersistence = new VuexPersistence({ + storage: galaxyStorage, + asyncStorage: true, + modules: ["panels"], }); export function createStore() { const storeConfig = { - plugins: [createCache(), panelsState], + plugins: [createCache(), panelsPersistence.plugin], modules: { user: userStore, config: configStore, betaHistory: betaHistoryStore, panels: panelStore, - // TODO: please namespace all store modules gridSearch: gridSearchStore, histories: historyStore, diff --git a/client/src/ui/search-input.js b/client/src/ui/search-input.js index 21c36f4e7dd..06acd5ce212 100644 --- a/client/src/ui/search-input.js +++ b/client/src/ui/search-input.js @@ -152,6 +152,18 @@ function searchInput(parentNode, options) { .popover("show"); }); } + // Hack to hide the advanced search popover when clicking outside + $("body").on("click", function (e) { + $('[data-toggle="advSearchPopover"]').each(function () { + if ( + !$(this).is(e.target) && + $(this).has(e.target).length === 0 && + $(".popover").has(e.target).length === 0 + ) { + $(this).popover("hide"); + } + }); + }); // .................................................................... loadingIndicator rendering // a button for clearing the search bar, placed on the right hand side diff --git a/client/src/utils/observable/index.js b/client/src/utils/observable/index.js index 8e26b1d8369..430e19c50ff 100644 --- a/client/src/utils/observable/index.js +++ b/client/src/utils/observable/index.js @@ -6,7 +6,6 @@ export { firstValueFrom } from "./firstValueFrom"; export { hydrate } from "./hydrate"; export { monitorBackboneModel } from "./monitorBackboneModel"; export { monitorXHR } from "./monitorXHR"; -export { needs } from "./needs"; export { nth } from "./nth"; export { shareButDie } from "./shareButDie"; export { show } from "./show"; diff --git a/client/src/utils/observable/needs.js b/client/src/utils/observable/needs.js deleted file mode 100644 index 3900a76f90e..00000000000 --- a/client/src/utils/observable/needs.js +++ /dev/null @@ -1,12 +0,0 @@ -// I'm actually unclear on why I needed this operator to make pouchdb -// observables work. This bears further investigation. To my knowledge -// withLatestFrom() does the same thing as this operator, yet this works and -// withLatestFrom() doesn't when used with pouchdb observable creation. - -import { zip, of, pipe } from "rxjs"; -import { concatMap } from "rxjs/operators"; - -// prettier-ignore -export const needs = (dep$) => pipe( - concatMap((src) => zip(of(src), dep$)) -); diff --git a/client/src/utils/promise-queue.js b/client/src/utils/promise-queue.js index 01a7e36db4d..bd6d9bcfff8 100644 --- a/client/src/utils/promise-queue.js +++ b/client/src/utils/promise-queue.js @@ -11,7 +11,7 @@ export class LastQueue { this.pendingPromise = false; } - async enqueue(action, ...args) { + async enqueue(action, args) { return new Promise((resolve, reject) => { this.nextPromise = { action, args, resolve, reject }; this.dequeue(); @@ -24,7 +24,7 @@ export class LastQueue { this.nextPromise = null; this.pendingPromise = true; try { - const payload = await item.action(...item.args); + const payload = await item.action(item.args); item.resolve(payload); } catch (e) { item.reject(e); diff --git a/client/yarn.lock b/client/yarn.lock index 34265f13732..a902a914e00 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -1167,10 +1167,10 @@ resolved "https://registry.yarnpkg.com/@fortawesome/vue-fontawesome/-/vue-fontawesome-2.0.6.tgz#87e691ed87f28f4667238573a29743f543a087f6" integrity sha512-V3vT3flY15AKbUS31aZOP12awQI3aAzkr2B1KnqcHLmwrmy51DW3pwyBczKdypV8QxBZ8U68Hl2XxK2nudTxpg== -"@galaxyproject/bootstrap-tour@^0.12.1": - version "0.12.1" - resolved "https://registry.yarnpkg.com/@galaxyproject/bootstrap-tour/-/bootstrap-tour-0.12.1.tgz#138ab9d993fda658220dea2662fc8e0d0c720577" - integrity sha512-fnDLpaQx/RFKLL9xtuaVz2RTCo4qQrWYrvFspu6h5ZObgFmTGLTQm3tjiZ0qWBV7Npmwt7X9mc8lUpSCwFPmQw== +"@galaxyproject/bootstrap-tour@0.12.2": + version "0.12.2" + resolved "https://registry.yarnpkg.com/@galaxyproject/bootstrap-tour/-/bootstrap-tour-0.12.2.tgz#9277e2dd976b8c778927c70d8180bb7a7090f009" + integrity sha512-iGmRhRhoq5vuhgVNig5/Hiu+86emgBDMtB12vo0ApAh9K+1lSnb8NLvfxDOOD7XlO9n4UMFJ5xnjSzeKCnOhqQ== dependencies: bootstrap ">=4.0.0-beta" jquery "^3.0.0" @@ -2020,42 +2020,6 @@ abbrev@1: resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== -abort-controller@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" - integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== - dependencies: - event-target-shim "^5.0.0" - -abstract-leveldown@^6.2.1: - version "6.3.0" - resolved "https://registry.yarnpkg.com/abstract-leveldown/-/abstract-leveldown-6.3.0.tgz#d25221d1e6612f820c35963ba4bd739928f6026a" - integrity sha512-TU5nlYgta8YrBMNpc9FwQzRbiXsj49gsALsXadbGHt9CROPzX5fB0rWDR5mtdpOOKa5XqRFpbj1QroPAoPzVjQ== - dependencies: - buffer "^5.5.0" - immediate "^3.2.3" - level-concat-iterator "~2.0.0" - level-supports "~1.0.0" - xtend "~4.0.0" - -abstract-leveldown@~2.7.1: - version "2.7.2" - resolved "https://registry.yarnpkg.com/abstract-leveldown/-/abstract-leveldown-2.7.2.tgz#87a44d7ebebc341d59665204834c8b7e0932cc93" - integrity sha512-+OVvxH2rHVEhWLdbudP6p0+dNMXu8JA1CbhP19T8paTYAcX7oJ4OVjT+ZUVpv7mITxXHqDMej+GdqXBmXkw09w== - dependencies: - xtend "~4.0.0" - -abstract-leveldown@~6.2.1, abstract-leveldown@~6.2.3: - version "6.2.3" - resolved "https://registry.yarnpkg.com/abstract-leveldown/-/abstract-leveldown-6.2.3.tgz#036543d87e3710f2528e47040bc3261b77a9a8eb" - integrity sha512-BsLm5vFMRUrrLeCcRc+G0t2qOaTzpoJQLOubq2XM72eNpjF5UdU5o/5NvlNhx95XHcAvcl8OMXr4mlg/fRgUXQ== - dependencies: - buffer "^5.5.0" - immediate "^3.2.3" - level-concat-iterator "~2.0.0" - level-supports "~1.0.0" - xtend "~4.0.0" - accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7: version "1.3.7" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" @@ -2264,11 +2228,6 @@ argparse@^2.0.1: resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== -argsarray@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/argsarray/-/argsarray-0.0.1.tgz#6e7207b4ecdb39b0af88303fa5ae22bda8df61cb" - integrity sha1-bnIHtOzbObCviDA/pa4ivajfYcs= - aria-query@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.0.0.tgz#210c21aaf469613ee8c9a62c7f86525e058db52c" @@ -2779,11 +2738,6 @@ buffer-equal@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-1.0.0.tgz#59616b498304d556abd466966b22eeda3eca5fbe" integrity sha1-WWFrSYME1Var1GaWayLu2j7KX74= -buffer-from@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" - integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== - buffer-from@^0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-0.1.2.tgz#15f4b9bcef012044df31142c14333caf6e0260d0" @@ -2799,7 +2753,7 @@ buffer-indexof@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.1.tgz#52fabcc6a606d1a00302802648ef68f639da268c" integrity sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g== -buffer@^5.5.0, buffer@^5.6.0, buffer@^5.7.0: +buffer@^5.7.0: version "5.7.1" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== @@ -3045,7 +2999,7 @@ cliui@^7.0.2: strip-ansi "^6.0.0" wrap-ansi "^7.0.0" -clone-buffer@1.0.0, clone-buffer@^1.0.0: +clone-buffer@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58" integrity sha1-4+JbIHrE5wGvch4staFnksrD3Fg= @@ -3603,13 +3557,6 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3: dependencies: ms "2.0.0" -debug@3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" - integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== - dependencies: - ms "2.0.0" - debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.2.0, debug@^4.3.2, debug@^4.3.3, debug@~4.3.1, debug@~4.3.2: version "4.3.3" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" @@ -3718,14 +3665,6 @@ default-resolution@^2.0.0: resolved "https://registry.yarnpkg.com/default-resolution/-/default-resolution-2.0.0.tgz#bcb82baa72ad79b426a76732f1a81ad6df26d684" integrity sha1-vLgrqnKtebQmp2cy8aga1t8m1oQ= -deferred-leveldown@~5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/deferred-leveldown/-/deferred-leveldown-5.3.0.tgz#27a997ad95408b61161aa69bd489b86c71b78058" - integrity sha512-a59VOT+oDy7vtAbLRCZwWgxu2BaCfd5Hk7wxJd48ei7I+nsg8Orlb9CLG0PMZienk9BSUKgeAqkO2+Lw+1+Ukw== - dependencies: - abstract-leveldown "~6.2.1" - inherits "^2.0.3" - define-lazy-prop@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" @@ -3933,11 +3872,6 @@ dot-case@^3.0.4: no-case "^3.0.4" tslib "^2.0.3" -double-ended-queue@2.1.0-0: - version "2.1.0-0" - resolved "https://registry.yarnpkg.com/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz#103d3527fd31528f40188130c841efdd78264e5c" - integrity sha1-ED01J/0xUo9AGIEwyEHv3XgmTlw= - duplexify@^3.6.0: version "3.7.1" resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309" @@ -4001,16 +3935,6 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= -encoding-down@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/encoding-down/-/encoding-down-6.3.0.tgz#b1c4eb0e1728c146ecaef8e32963c549e76d082b" - integrity sha512-QKrV0iKR6MZVJV08QY0wp1e7vF6QbhnbQhb07bwpEyuz4uZiZgPlEGdkCROuFkUwdxlFaiPIhjyarH1ee/3vhw== - dependencies: - abstract-leveldown "^6.2.1" - inherits "^2.0.3" - level-codec "^9.0.0" - level-errors "^2.0.0" - end-of-stream@^1.0.0, end-of-stream@^1.1.0: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" @@ -4018,13 +3942,6 @@ end-of-stream@^1.0.0, end-of-stream@^1.1.0: dependencies: once "^1.4.0" -end-stream@~0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/end-stream/-/end-stream-0.1.0.tgz#32003f3f438a2b0143168137f8fa6e9866c81ed5" - integrity sha1-MgA/P0OKKwFDFoE3+PpumGbIHtU= - dependencies: - write-stream "~0.4.3" - engine.io-parser@~5.0.0: version "5.0.3" resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.0.3.tgz#ca1f0d7b11e290b4bfda251803baea765ed89c09" @@ -4076,13 +3993,6 @@ envinfo@^7.7.3: resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.8.1.tgz#06377e3e5f4d379fea7ac592d5ad8927e0c4d475" integrity sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw== -errno@~0.1.1: - version "0.1.8" - resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f" - integrity sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A== - dependencies: - prr "~1.0.1" - error-ex@^1.2.0, error-ex@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" @@ -4371,11 +4281,6 @@ ev-emitter@^1.0.0: resolved "https://registry.yarnpkg.com/ev-emitter/-/ev-emitter-1.1.1.tgz#8f18b0ce5c76a5d18017f71c0a795c65b9138f2a" integrity sha512-ipiDYhdQSCZ4hSbX4rMW+XzNKMD1prg/sTvoVmSLkuQ1MVlwjJQQA+sW8tMYR3BLUr9KjodFV4pvzunvRhd33Q== -event-target-shim@^5.0.0: - version "5.0.1" - resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" - integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== - eventemitter3@^4.0.0: version "4.0.7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" @@ -4592,13 +4497,6 @@ fb-watchman@^2.0.0: dependencies: bser "2.1.1" -fetch-cookie@0.10.1: - version "0.10.1" - resolved "https://registry.yarnpkg.com/fetch-cookie/-/fetch-cookie-0.10.1.tgz#5ea88f3d36950543c87997c27ae2aeafb4b5c4d4" - integrity sha512-beB+VEd4cNeVG1PY+ee74+PkuCQnik78pgLi5Ah/7qdUfov8IctU0vLUbBT8/10Ma5GMBeI4wtxhGrEfKNYs2g== - dependencies: - tough-cookie "^2.3.3 || ^3.0.1 || ^4.0.0" - fflate@^0.4.8: version "0.4.8" resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.4.8.tgz#f90b82aefbd8ac174213abb338bd7ef848f0f5ae" @@ -4727,10 +4625,15 @@ flat-cache@^3.0.4: flatted "^3.1.0" rimraf "^3.0.2" -flatted@^3.0.5, flatted@^3.1.0, flatted@^3.2.4: - version "3.2.5" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.5.tgz#76c8584f4fc843db64702a6bd04ab7a8bd666da3" - integrity sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg== +flatted@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138" + integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA== + +flatted@^3.1.0, flatted@^3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.4.tgz#28d9969ea90661b5134259f312ab6aa7929ac5e2" + integrity sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw== flush-promises@^1.0.2: version "1.0.2" @@ -4746,9 +4649,9 @@ flush-write-stream@^1.0.2: readable-stream "^2.3.6" follow-redirects@^1.0.0, follow-redirects@^1.14.7: - version "1.14.7" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.7.tgz#2004c02eb9436eee9a21446a6477debf17e81685" - integrity sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ== + version "1.14.8" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.8.tgz#016996fb9a11a100566398b1c6839337d7bfa8fc" + integrity sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA== for-in@^1.0.1, for-in@^1.0.2: version "1.0.2" @@ -5318,11 +5221,6 @@ imask@^6.4.0: resolved "https://registry.yarnpkg.com/imask/-/imask-6.4.0.tgz#76e48e6bbf5306f7de058f8a1b90fd2282d446bc" integrity sha512-iXXgSQA6TJX/RSKivKoTYYoCc4ZmVseMcoDIxMrlW1JPpc8LU9FtCp3f80vg1eUQvmVfEQbh+vybFVDRt2o9TQ== -immediate@3.3.0, immediate@^3.2.3: - version "3.3.0" - resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.3.0.tgz#1aef225517836bcdf7f2a2de2600c79ff0269266" - integrity sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q== - immediate@~3.0.5: version "3.0.6" resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" @@ -5380,7 +5278,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3, inherits@~2.0.4: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3, inherits@~2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -6555,10 +6453,10 @@ karma-webpack@^5.0.0: minimatch "^3.0.4" webpack-merge "^4.1.5" -karma@^6.3.13: - version "6.3.13" - resolved "https://registry.yarnpkg.com/karma/-/karma-6.3.13.tgz#ff58622c4f913508dbf86506a5fc614be0e98137" - integrity sha512-64lbtHBnM/sG7XKvitSQVlImKYNugyjw4K2e8cZYrlZm0GwoWrfsrkzgS/KWY/r3+fVM/2xvT/JN6MyfdttxLw== +karma@^6.3.16: + version "6.3.16" + resolved "https://registry.yarnpkg.com/karma/-/karma-6.3.16.tgz#76d1a705fd1cf864ee5ed85270b572641e0958ef" + integrity sha512-nEU50jLvDe5yvXqkEJRf8IuvddUkOY2x5Xc4WXHz6dxINgGDrgD2uqQWeVrJs4hbfNaotn+HQ1LZJ4yOXrL7xQ== dependencies: body-parser "^1.19.0" braces "^3.0.2" @@ -6575,6 +6473,7 @@ karma@^6.3.13: log4js "^6.4.1" mime "^2.5.2" minimatch "^3.0.4" + mkdirp "^0.5.5" qjobs "^1.2.0" range-parser "^1.2.1" rimraf "^3.0.2" @@ -6647,95 +6546,6 @@ lead@^1.0.0: dependencies: flush-write-stream "^1.0.2" -level-codec@9.0.2, level-codec@^9.0.0: - version "9.0.2" - resolved "https://registry.yarnpkg.com/level-codec/-/level-codec-9.0.2.tgz#fd60df8c64786a80d44e63423096ffead63d8cbc" - integrity sha512-UyIwNb1lJBChJnGfjmO0OR+ezh2iVu1Kas3nvBS/BzGnx79dv6g7unpKIDNPMhfdTEGoc7mC8uAu51XEtX+FHQ== - dependencies: - buffer "^5.6.0" - -level-concat-iterator@~2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/level-concat-iterator/-/level-concat-iterator-2.0.1.tgz#1d1009cf108340252cb38c51f9727311193e6263" - integrity sha512-OTKKOqeav2QWcERMJR7IS9CUo1sHnke2C0gkSmcR7QuEtFNLLzHQAvnMw8ykvEcv0Qtkg0p7FOwP1v9e5Smdcw== - -level-errors@^2.0.0, level-errors@~2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/level-errors/-/level-errors-2.0.1.tgz#2132a677bf4e679ce029f517c2f17432800c05c8" - integrity sha512-UVprBJXite4gPS+3VznfgDSU8PTRuVX0NXwoWW50KLxd2yw4Y1t2JUR5In1itQnudZqRMT9DlAM3Q//9NCjCFw== - dependencies: - errno "~0.1.1" - -level-iterator-stream@~4.0.0: - version "4.0.2" - resolved "https://registry.yarnpkg.com/level-iterator-stream/-/level-iterator-stream-4.0.2.tgz#7ceba69b713b0d7e22fcc0d1f128ccdc8a24f79c" - integrity sha512-ZSthfEqzGSOMWoUGhTXdX9jv26d32XJuHz/5YnuHZzH6wldfWMOVwI9TBtKcya4BKTyTt3XVA0A3cF3q5CY30Q== - dependencies: - inherits "^2.0.4" - readable-stream "^3.4.0" - xtend "^4.0.2" - -level-js@^5.0.0: - version "5.0.2" - resolved "https://registry.yarnpkg.com/level-js/-/level-js-5.0.2.tgz#5e280b8f93abd9ef3a305b13faf0b5397c969b55" - integrity sha512-SnBIDo2pdO5VXh02ZmtAyPP6/+6YTJg2ibLtl9C34pWvmtMEmRTWpra+qO/hifkUtBTOtfx6S9vLDjBsBK4gRg== - dependencies: - abstract-leveldown "~6.2.3" - buffer "^5.5.0" - inherits "^2.0.3" - ltgt "^2.1.2" - -level-packager@^5.1.0: - version "5.1.1" - resolved "https://registry.yarnpkg.com/level-packager/-/level-packager-5.1.1.tgz#323ec842d6babe7336f70299c14df2e329c18939" - integrity sha512-HMwMaQPlTC1IlcwT3+swhqf/NUO+ZhXVz6TY1zZIIZlIR0YSn8GtAAWmIvKjNY16ZkEg/JcpAuQskxsXqC0yOQ== - dependencies: - encoding-down "^6.3.0" - levelup "^4.3.2" - -level-supports@~1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/level-supports/-/level-supports-1.0.1.tgz#2f530a596834c7301622521988e2c36bb77d122d" - integrity sha512-rXM7GYnW8gsl1vedTJIbzOrRv85c/2uCMpiiCzO2fndd06U/kUXEEU9evYn4zFggBOg36IsBW8LzqIpETwwQzg== - dependencies: - xtend "^4.0.2" - -level-write-stream@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/level-write-stream/-/level-write-stream-1.0.0.tgz#3f7fbb679a55137c0feb303dee766e12ee13c1dc" - integrity sha1-P3+7Z5pVE3wP6zA97nZuEu4Twdw= - dependencies: - end-stream "~0.1.0" - -level@6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/level/-/level-6.0.1.tgz#dc34c5edb81846a6de5079eac15706334b0d7cd6" - integrity sha512-psRSqJZCsC/irNhfHzrVZbmPYXDcEYhA5TVNwr+V92jF44rbf86hqGp8fiT702FyiArScYIlPSBTDUASCVNSpw== - dependencies: - level-js "^5.0.0" - level-packager "^5.1.0" - leveldown "^5.4.0" - -leveldown@5.6.0, leveldown@^5.4.0: - version "5.6.0" - resolved "https://registry.yarnpkg.com/leveldown/-/leveldown-5.6.0.tgz#16ba937bb2991c6094e13ac5a6898ee66d3eee98" - integrity sha512-iB8O/7Db9lPaITU1aA2txU/cBEXAt4vWwKQRrrWuS6XDgbP4QZGj9BL2aNbwb002atoQ/lIotJkfyzz+ygQnUQ== - dependencies: - abstract-leveldown "~6.2.1" - napi-macros "~2.0.0" - node-gyp-build "~4.1.0" - -levelup@4.4.0, levelup@^4.3.2: - version "4.4.0" - resolved "https://registry.yarnpkg.com/levelup/-/levelup-4.4.0.tgz#f89da3a228c38deb49c48f88a70fb71f01cafed6" - integrity sha512-94++VFO3qN95cM/d6eBXvd894oJE0w3cInq9USsyQzzoJxmiYzPAocNcuGCPGGjoXqDVJcr3C1jzt1TSjyaiLQ== - dependencies: - deferred-leveldown "~5.3.0" - level-errors "~2.0.0" - level-iterator-stream "~4.0.0" - level-supports "~1.0.0" - xtend "~4.0.0" - leven@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" @@ -6839,6 +6649,13 @@ loader-utils@^2.0.0: emojis-list "^3.0.0" json5 "^2.1.2" +localforage@^1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.10.0.tgz#5c465dc5f62b2807c3a84c0c6a1b1b3212781dd4" + integrity sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg== + dependencies: + lie "3.1.1" + locate-path@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" @@ -6926,7 +6743,7 @@ lodash.uniqby@4.5.0: lodash._baseiteratee "~4.7.0" lodash._baseuniq "~4.6.0" -lodash@^4.13.1, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.21, lodash@^4.7.0: +lodash@^4.13.1, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21, lodash@^4.7.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -6971,11 +6788,6 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" -ltgt@2.2.1, ltgt@^2.1.2, ltgt@~2.2.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/ltgt/-/ltgt-2.2.1.tgz#f35ca91c493f7b73da0e07495304f17b31f87ee5" - integrity sha1-81ypHEk/e3PaDgdJUwTxezH4fuU= - make-dir@^3.0.0, make-dir@^3.0.2, make-dir@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" @@ -7058,18 +6870,6 @@ media-typer@0.3.0: resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= -memdown@1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/memdown/-/memdown-1.4.1.tgz#b4e4e192174664ffbae41361aa500f3119efe215" - integrity sha1-tOThkhdGZP+65BNhqlAPMRnv4hU= - dependencies: - abstract-leveldown "~2.7.1" - functional-red-black-tree "^1.0.1" - immediate "^3.2.3" - inherits "~2.0.1" - ltgt "~2.2.0" - safe-buffer "~5.1.1" - memfs@^3.4.1: version "3.4.1" resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.4.1.tgz#b78092f466a0dce054d63d39275b24c71d3f1305" @@ -7272,11 +7072,6 @@ nanomatch@^1.2.9: snapdragon "^0.8.1" to-regex "^3.0.1" -napi-macros@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/napi-macros/-/napi-macros-2.0.0.tgz#2b6bae421e7b96eb687aa6c77a7858640670001b" - integrity sha512-A0xLykHtARfueITVDernsAWdtIMbOJgKgcluwENp3AlsKN/PloyO10HtmoqnFAQAcxPkgZN7wdfPfEd0zNGxbg== - natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -7316,11 +7111,6 @@ no-case@^3.0.4: lower-case "^2.0.2" tslib "^2.0.3" -node-fetch@2.6.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd" - integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA== - node-fetch@^2.6.1: version "2.6.7" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" @@ -7333,11 +7123,6 @@ node-forge@^1.2.0: resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.2.1.tgz#82794919071ef2eb5c509293325cec8afd0fd53c" integrity sha512-Fcvtbb+zBcZXbTTVwqGA5W+MKBj56UjVRevvchv5XrcyXbmNdesfZL37nlcWOfpgHhgmxApw3tQbTr4CqNmX4w== -node-gyp-build@~4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.1.1.tgz#d7270b5d86717068d114cc57fff352f96d745feb" - integrity sha512-dSq1xmcPDKPZ2EED2S6zw/b9NKsqzXRE6dVr8TVQnI3FJOTteUMuqF3Qqs6LZg+mLGYJWqQzMbIjMtJqTv87nQ== - node-int64@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" @@ -7447,12 +7232,17 @@ object-hash@^2.2.0: resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.2.0.tgz#5ad518581eefc443bd763472b8ff2e9c2c0d54a5" integrity sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw== -object-inspect@^1.11.0, object-inspect@^1.9.0: +object-inspect@^1.11.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.11.0.tgz#9dceb146cedd4148a0d9e51ab88d34cf509922b1" + integrity sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg== + +object-inspect@^1.9.0: version "1.12.0" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.0.tgz#6e2c120e868fd1fd18cb4f18c31741d0d6e776f0" integrity sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g== -object-is@^1.0.1, object-is@^1.1.4: +object-is@^1.0.1: version "1.1.5" resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac" integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw== @@ -7460,6 +7250,14 @@ object-is@^1.0.1, object-is@^1.1.4: call-bind "^1.0.2" define-properties "^1.1.3" +object-is@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.4.tgz#63d6c83c00a43f4cbc9434eb9757c8a5b8565068" + integrity sha512-1ZvAZ4wlF7IyPVOcE1Omikt7UpaFlOQq0HlSti+ZvDH3UiD2brwGMwDbyV43jao2bKJ+4+WdPJHSd7kgzKYVqg== + dependencies: + call-bind "^1.0.0" + define-properties "^1.1.3" + object-keys@^1.0.12, object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" @@ -8117,213 +7915,6 @@ postcss@^8.3.5, postcss@^8.4.5: picocolors "^1.0.0" source-map-js "^1.0.2" -pouchdb-abstract-mapreduce@7.2.2: - version "7.2.2" - resolved "https://registry.yarnpkg.com/pouchdb-abstract-mapreduce/-/pouchdb-abstract-mapreduce-7.2.2.tgz#dd1b10a83f8d24361dce9aaaab054614b39f766f" - integrity sha512-7HWN/2yV2JkwMnGnlp84lGvFtnm0Q55NiBUdbBcaT810+clCGKvhssBCrXnmwShD1SXTwT83aszsgiSfW+SnBA== - dependencies: - pouchdb-binary-utils "7.2.2" - pouchdb-collate "7.2.2" - pouchdb-collections "7.2.2" - pouchdb-errors "7.2.2" - pouchdb-fetch "7.2.2" - pouchdb-mapreduce-utils "7.2.2" - pouchdb-md5 "7.2.2" - pouchdb-utils "7.2.2" - -pouchdb-adapter-leveldb-core@7.2.2: - version "7.2.2" - resolved "https://registry.yarnpkg.com/pouchdb-adapter-leveldb-core/-/pouchdb-adapter-leveldb-core-7.2.2.tgz#e0aa6a476e2607d7ae89f4a803c9fba6e6d05a8a" - integrity sha512-K9UGf1Ivwe87mjrMqN+1D07tO/DfU7ariVDrGffuOjvl+3BcvUF25IWrxsBObd4iPOYCH7NVQWRpojhBgxULtQ== - dependencies: - argsarray "0.0.1" - buffer-from "1.1.1" - double-ended-queue "2.1.0-0" - levelup "4.4.0" - pouchdb-adapter-utils "7.2.2" - pouchdb-binary-utils "7.2.2" - pouchdb-collections "7.2.2" - pouchdb-errors "7.2.2" - pouchdb-json "7.2.2" - pouchdb-md5 "7.2.2" - pouchdb-merge "7.2.2" - pouchdb-utils "7.2.2" - sublevel-pouchdb "7.2.2" - through2 "3.0.2" - -pouchdb-adapter-memory@^7.2.2: - version "7.2.2" - resolved "https://registry.yarnpkg.com/pouchdb-adapter-memory/-/pouchdb-adapter-memory-7.2.2.tgz#c0ec2e87928d516ca9d1b5badc7269df6f95e5ea" - integrity sha512-9o+zdItPEq7rIrxdkUxgsLNaZkDJAGEqqoYgeYdrHidOCZnlhxhX3g7/R/HcpDKC513iEPqJWDJQSfeT6nVKkw== - dependencies: - memdown "1.4.1" - pouchdb-adapter-leveldb-core "7.2.2" - pouchdb-utils "7.2.2" - -pouchdb-adapter-utils@7.2.2: - version "7.2.2" - resolved "https://registry.yarnpkg.com/pouchdb-adapter-utils/-/pouchdb-adapter-utils-7.2.2.tgz#c64426447d9044ba31517a18500d6d2d28abd47d" - integrity sha512-2CzZkTyTyHZkr3ePiWFMTiD5+56lnembMjaTl8ohwegM0+hYhRyJux0biAZafVxgIL4gnCUC4w2xf6WVztzKdg== - dependencies: - pouchdb-binary-utils "7.2.2" - pouchdb-collections "7.2.2" - pouchdb-errors "7.2.2" - pouchdb-md5 "7.2.2" - pouchdb-merge "7.2.2" - pouchdb-utils "7.2.2" - -pouchdb-binary-utils@7.2.2: - version "7.2.2" - resolved "https://registry.yarnpkg.com/pouchdb-binary-utils/-/pouchdb-binary-utils-7.2.2.tgz#0690b348052c543b1e67f032f47092ca82bcb10e" - integrity sha512-shacxlmyHbUrNfE6FGYpfyAJx7Q0m91lDdEAaPoKZM3SzAmbtB1i+OaDNtYFztXjJl16yeudkDb3xOeokVL3Qw== - dependencies: - buffer-from "1.1.1" - -pouchdb-collate@7.2.2: - version "7.2.2" - resolved "https://registry.yarnpkg.com/pouchdb-collate/-/pouchdb-collate-7.2.2.tgz#fc261f5ef837c437e3445fb0abc3f125d982c37c" - integrity sha512-/SMY9GGasslknivWlCVwXMRMnQ8myKHs4WryQ5535nq1Wj/ehpqWloMwxEQGvZE1Sda3LOm7/5HwLTcB8Our+w== - -pouchdb-collections@7.2.2: - version "7.2.2" - resolved "https://registry.yarnpkg.com/pouchdb-collections/-/pouchdb-collections-7.2.2.tgz#aeed77f33322429e3f59d59ea233b48ff0e68572" - integrity sha512-6O9zyAYlp3UdtfneiMYuOCWdUCQNo2bgdjvNsMSacQX+3g8WvIoFQCYJjZZCpTttQGb+MHeRMr8m2U95lhJTew== - -pouchdb-debug@^7.2.1: - version "7.2.1" - resolved "https://registry.yarnpkg.com/pouchdb-debug/-/pouchdb-debug-7.2.1.tgz#f5f869f6113c12ccb97cddf5b0a32b6e0e67e961" - integrity sha512-eP3ht/AKavLF2RjTzBM6S9gaI2/apcW6xvaKRQhEdOfiANqerFuksFqHCal3aikVQuDO+cB/cw+a4RyJn/glBw== - dependencies: - debug "3.1.0" - -pouchdb-erase@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/pouchdb-erase/-/pouchdb-erase-1.0.2.tgz#cbdfdddf2dfcd814d0cad72221514237eeb2c151" - integrity sha1-y9/d3y382BTQytciIVFCN+6ywVE= - dependencies: - promise-nodify "^1.0.1" - -pouchdb-errors@7.2.2: - version "7.2.2" - resolved "https://registry.yarnpkg.com/pouchdb-errors/-/pouchdb-errors-7.2.2.tgz#80d811d65c766c9d20b755c6e6cc123f8c3c4792" - integrity sha512-6GQsiWc+7uPfgEHeavG+7wuzH3JZW29Dnrvz8eVbDFE50kVFxNDVm3EkYHskvo5isG7/IkOx7PV7RPTA3keG3g== - dependencies: - inherits "2.0.4" - -pouchdb-fetch@7.2.2: - version "7.2.2" - resolved "https://registry.yarnpkg.com/pouchdb-fetch/-/pouchdb-fetch-7.2.2.tgz#492791236d60c899d7e9973f9aca0d7b9cc02230" - integrity sha512-lUHmaG6U3zjdMkh8Vob9GvEiRGwJfXKE02aZfjiVQgew+9SLkuOxNw3y2q4d1B6mBd273y1k2Lm0IAziRNxQnA== - dependencies: - abort-controller "3.0.0" - fetch-cookie "0.10.1" - node-fetch "2.6.0" - -pouchdb-find@^7.2.2: - version "7.2.2" - resolved "https://registry.yarnpkg.com/pouchdb-find/-/pouchdb-find-7.2.2.tgz#1227afdd761812d508fe0794b3e904518a721089" - integrity sha512-BmFeFVQ0kHmDehvJxNZl9OmIztCjPlZlVSdpijuFbk/Fi1EFPU1BAv3kLC+6DhZuOqU/BCoaUBY9sn66pPY2ag== - dependencies: - pouchdb-abstract-mapreduce "7.2.2" - pouchdb-collate "7.2.2" - pouchdb-errors "7.2.2" - pouchdb-fetch "7.2.2" - pouchdb-md5 "7.2.2" - pouchdb-selector-core "7.2.2" - pouchdb-utils "7.2.2" - -pouchdb-json@7.2.2: - version "7.2.2" - resolved "https://registry.yarnpkg.com/pouchdb-json/-/pouchdb-json-7.2.2.tgz#b939be24b91a7322e9a24b8880a6e21514ec5e1f" - integrity sha512-3b2S2ynN+aoB7aCNyDZc/4c0IAdx/ir3nsHB+/RrKE9cM3QkQYbnnE3r/RvOD1Xvr6ji/KOCBie+Pz/6sxoaug== - dependencies: - vuvuzela "1.0.3" - -pouchdb-mapreduce-utils@7.2.2: - version "7.2.2" - resolved "https://registry.yarnpkg.com/pouchdb-mapreduce-utils/-/pouchdb-mapreduce-utils-7.2.2.tgz#13a46a3cc2a3f3b8e24861da26966904f2963146" - integrity sha512-rAllb73hIkU8rU2LJNbzlcj91KuulpwQu804/F6xF3fhZKC/4JQMClahk+N/+VATkpmLxp1zWmvmgdlwVU4HtQ== - dependencies: - argsarray "0.0.1" - inherits "2.0.4" - pouchdb-collections "7.2.2" - pouchdb-utils "7.2.2" - -pouchdb-md5@7.2.2: - version "7.2.2" - resolved "https://registry.yarnpkg.com/pouchdb-md5/-/pouchdb-md5-7.2.2.tgz#415401acc5a844112d765bd1fb4e5d9f38fb0838" - integrity sha512-c/RvLp2oSh8PLAWU5vFBnp6ejJABIdKqboZwRRUrWcfGDf+oyX8RgmJFlYlzMMOh4XQLUT1IoaDV8cwlsuryZw== - dependencies: - pouchdb-binary-utils "7.2.2" - spark-md5 "3.0.1" - -pouchdb-merge@7.2.2: - version "7.2.2" - resolved "https://registry.yarnpkg.com/pouchdb-merge/-/pouchdb-merge-7.2.2.tgz#940d85a2b532d6a93a6cab4b250f5648511bcc16" - integrity sha512-6yzKJfjIchBaS7Tusuk8280WJdESzFfQ0sb4jeMUNnrqs4Cx3b0DIEOYTRRD9EJDM+je7D3AZZ4AT0tFw8gb4A== - -pouchdb-promise@^6.1.2: - version "6.4.3" - resolved "https://registry.yarnpkg.com/pouchdb-promise/-/pouchdb-promise-6.4.3.tgz#74516f4acf74957b54debd0fb2c0e5b5a68ca7b3" - integrity sha512-ruJaSFXwzsxRHQfwNHjQfsj58LBOY1RzGzde4PM5CWINZwFjCQAhZwfMrch2o/0oZT6d+Xtt0HTWhq35p3b0qw== - dependencies: - lie "3.1.1" - -pouchdb-selector-core@7.2.2: - version "7.2.2" - resolved "https://registry.yarnpkg.com/pouchdb-selector-core/-/pouchdb-selector-core-7.2.2.tgz#264d7436a8c8ac3801f39960e79875ef7f3879a0" - integrity sha512-XYKCNv9oiNmSXV5+CgR9pkEkTFqxQGWplnVhO3W9P154H08lU0ZoNH02+uf+NjZ2kjse7Q1fxV4r401LEcGMMg== - dependencies: - pouchdb-collate "7.2.2" - pouchdb-utils "7.2.2" - -pouchdb-upsert@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/pouchdb-upsert/-/pouchdb-upsert-2.2.0.tgz#42b15e420848f3b294c35060589fdb51cf7f7f5f" - integrity sha1-QrFeQghI87KUw1BgWJ/bUc9/f18= - dependencies: - pouchdb-promise "^6.1.2" - -pouchdb-utils@7.2.2: - version "7.2.2" - resolved "https://registry.yarnpkg.com/pouchdb-utils/-/pouchdb-utils-7.2.2.tgz#c17c4788f1d052b0daf4ef8797bbc4aaa3945aa4" - integrity sha512-XmeM5ioB4KCfyB2MGZXu1Bb2xkElNwF1qG+zVFbQsKQij0zvepdOUfGuWvLRHxTOmt4muIuSOmWZObZa3NOgzQ== - dependencies: - argsarray "0.0.1" - clone-buffer "1.0.0" - immediate "3.3.0" - inherits "2.0.4" - pouchdb-collections "7.2.2" - pouchdb-errors "7.2.2" - pouchdb-md5 "7.2.2" - uuid "8.1.0" - -pouchdb@^7.2.2: - version "7.2.2" - resolved "https://registry.yarnpkg.com/pouchdb/-/pouchdb-7.2.2.tgz#fcae82862db527e4cf7576ed8549d1384961f364" - integrity sha512-5gf5nw5XH/2H/DJj8b0YkvG9fhA/4Jt6kL0Y8QjtztVjb1y4J19Rg4rG+fUbXu96gsUrlyIvZ3XfM0b4mogGmw== - dependencies: - abort-controller "3.0.0" - argsarray "0.0.1" - buffer-from "1.1.1" - clone-buffer "1.0.0" - double-ended-queue "2.1.0-0" - fetch-cookie "0.10.1" - immediate "3.3.0" - inherits "2.0.4" - level "6.0.1" - level-codec "9.0.2" - level-write-stream "1.0.0" - leveldown "5.6.0" - levelup "4.4.0" - ltgt "2.2.1" - node-fetch "2.6.0" - readable-stream "1.1.14" - spark-md5 "3.0.1" - through2 "3.0.2" - uuid "8.1.0" - vuvuzela "1.0.3" - prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -8368,9 +7959,9 @@ pretty@^2.0.0: js-beautify "^1.6.12" prismjs@^1.6.0: - version "1.26.0" - resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.26.0.tgz#16881b594828bb6b45296083a8cbab46b0accd47" - integrity sha512-HUoH9C5Z3jKkl3UunCyiD5jwk0+Hz0fIgQ2nbwU2Oo/ceuTAQAg+pPVnfdt2TJWRVLcxKh9iuoYDUSc8clb5UQ== + version "1.27.0" + resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.27.0.tgz#bb6ee3138a0b438a3653dd4d6ce0cc6510a45057" + integrity sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA== process-nextick-args@^2.0.0, process-nextick-args@~2.0.0: version "2.0.1" @@ -8382,11 +7973,6 @@ process@^0.11.10: resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= -promise-nodify@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/promise-nodify/-/promise-nodify-1.0.2.tgz#0d0fb143c33400b0061b47e581257557047d4c5a" - integrity sha1-DQ+xQ8M0ALAGG0flgSV1VwR9TFo= - prompts@^2.0.1, prompts@~2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" @@ -8421,11 +8007,6 @@ proxy-addr@~2.0.7: forwarded "0.2.0" ipaddr.js "1.9.1" -prr@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" - integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY= - pseudomap@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" @@ -8556,25 +8137,6 @@ read-pkg@^1.0.0: normalize-package-data "^2.3.2" path-type "^1.0.0" -readable-stream@1.1.14: - version "1.1.14" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" - integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk= - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.1" - isarray "0.0.1" - string_decoder "~0.10.x" - -"readable-stream@2 || 3", readable-stream@^3.0.6, readable-stream@^3.4.0, readable-stream@^3.5.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" - integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.5, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" @@ -8588,10 +8150,14 @@ readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.5, readable string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@~0.0.2: - version "0.0.4" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-0.0.4.tgz#f32d76e3fb863344a548d79923007173665b3b8d" - integrity sha1-8y124/uGM0SlSNeZIwBxc2ZbO40= +readable-stream@^3.0.6, readable-stream@^3.5.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" readdirp@~3.6.0: version "3.6.0" @@ -9139,11 +8705,6 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -shvl@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/shvl/-/shvl-2.0.3.tgz#eb4bd37644f5684bba1fc52c3010c96fb5e6afd1" - integrity sha512-V7C6S9Hlol6SzOJPnQ7qzOVEWUQImt3BNmmzh40wObhla3XOYMe4gGiYzLrJd5TFa+cI2f9LKIRJTTKZSTbWgw== - side-channel@^1.0.3, side-channel@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" @@ -9330,11 +8891,6 @@ sourcemap-codec@1.4.8: resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== -spark-md5@3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/spark-md5/-/spark-md5-3.0.1.tgz#83a0e255734f2ab4e5c466e5a2cfc9ba2aa2124d" - integrity sha512-0tF3AGSD1ppQeuffsLDIOWlKUd3lS92tFxcsrh5Pe3ZphhnoK+oXIBTzOAThZCiuINZLvpiLH/1VS1/ANEJVig== - sparkles@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/sparkles/-/sparkles-1.0.1.tgz#008db65edce6c50eec0c5e228e1945061dd0437c" @@ -9540,11 +9096,6 @@ string_decoder@^1.1.1: dependencies: safe-buffer "~5.2.0" -string_decoder@~0.10.x: - version "0.10.31" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" - integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= - string_decoder@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" @@ -9620,16 +9171,6 @@ stylehacks@^5.0.2: browserslist "^4.16.6" postcss-selector-parser "^6.0.4" -sublevel-pouchdb@7.2.2: - version "7.2.2" - resolved "https://registry.yarnpkg.com/sublevel-pouchdb/-/sublevel-pouchdb-7.2.2.tgz#49e46cd37883bf7ff5006d7c5b9bcc7bcc1f422f" - integrity sha512-y5uYgwKDgXVyPZceTDGWsSFAhpSddY29l9PJbXqMJLfREdPmQTY8InpatohlEfCXX7s1LGcrfYAhxPFZaJOLnQ== - dependencies: - inherits "2.0.4" - level-codec "9.0.2" - ltgt "2.2.1" - readable-stream "1.1.14" - supports-color@^5.3.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" @@ -9782,14 +9323,6 @@ through2-filter@^3.0.0: through2 "~2.0.0" xtend "~4.0.0" -through2@3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/through2/-/through2-3.0.2.tgz#99f88931cfc761ec7678b41d5d7336b5b6a07bf4" - integrity sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ== - dependencies: - inherits "^2.0.4" - readable-stream "2 || 3" - through2@^2.0.0, through2@^2.0.3, through2@~2.0.0: version "2.0.5" resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" @@ -9911,7 +9444,7 @@ toidentifier@1.0.1: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== -"tough-cookie@^2.3.3 || ^3.0.1 || ^4.0.0", tough-cookie@^4.0.0: +tough-cookie@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.0.0.tgz#d822234eeca882f991f0f908824ad2622ddbece4" integrity sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg== @@ -10150,9 +9683,9 @@ urix@^0.1.0: integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= url-parse@^1.4.3: - version "1.5.4" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.4.tgz#e4f645a7e2a0852cc8a66b14b292a3e9a11a97fd" - integrity sha512-ITeAByWWoqutFClc/lRZnFplgXgEZr3WJ6XngMM/N9DMIm4K8zXPCZ1Jdu0rERwO84w1WC5wkle2ubwTA4NTBg== + version "1.5.10" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1" + integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== dependencies: querystringify "^2.1.1" requires-port "^1.0.0" @@ -10191,11 +9724,6 @@ utrie@^1.0.2: dependencies: base64-arraybuffer "^1.0.2" -uuid@8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.1.0.tgz#6f1536eb43249f473abc6bd58ff983da1ca30d8d" - integrity sha512-CI18flHDznR0lq54xBycOVmphdCYnQLKn8abKn7PXUiKUGdEd+/l9LWNJmugXel4hXq7S+RMNl34ecyC9TntWg== - uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" @@ -10387,6 +9915,11 @@ vue-template-es2015-compiler@^1.5.3, vue-template-es2015-compiler@^1.9.0: resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825" integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw== +vue-virtual-scroll-list@^2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/vue-virtual-scroll-list/-/vue-virtual-scroll-list-2.3.3.tgz#290f0d04be814b93585f2ef232097b671141e5b1" + integrity sha512-heuwlZ+lEdcVSp66CpVXnyNdsiHl/XU1cvqJb0yUHGlLv6DGK8OX16FH8I3w/wKNXtHSPF9itdBBGJFrXkyHeg== + vue@^2.6.10, vue@^2.6.14: version "2.6.14" resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.14.tgz#e51aa5250250d569a3fbad3a8a5a687d6036e235" @@ -10412,32 +9945,19 @@ vuex-cache@^3.4.0: resolved "https://registry.yarnpkg.com/vuex-cache/-/vuex-cache-3.4.0.tgz#0aec66b7abb370b1a089f678681af8efe5a9289b" integrity sha512-C0HJvVTEdjn8gq0EXemcJCBgAyPLnms8VGz/kMakrpaF6XNwkZY5KvySkdBJSY6rp0eTdWMoHPVU2Oq+dK7WEQ== -vuex-persist@^3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/vuex-persist/-/vuex-persist-3.1.3.tgz#518c722a2ca3026bcee5732f99d24f75cee0f3b6" - integrity sha512-QWOpP4SxmJDC5Y1+0+Yl/F4n7z27syd1St/oP+IYCGe0X0GFio0Zan6kngZFufdIhJm+5dFGDo3VG5kdkCGeRQ== +vuex-persist@2.3: + version "2.3.0" + resolved "https://registry.yarnpkg.com/vuex-persist/-/vuex-persist-2.3.0.tgz#f3d21b6d631078667d6834f2f74442ee332ac281" + integrity sha512-0QPZQYgQ72SbXkdwctXGFQJhlRiySP0z9wHDBTlyHvNy3CneKvgyJ7Lfob8NUiYbaurJepaC+JFynwshp59FGA== dependencies: - deepmerge "^4.2.2" - flatted "^3.0.5" - -vuex-persistedstate@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/vuex-persistedstate/-/vuex-persistedstate-4.1.0.tgz#127165f85f5b4534fb3170a5d3a8be9811bd2a53" - integrity sha512-3SkEj4NqwM69ikJdFVw6gObeB0NHyspRYMYkR/EbhR0hbvAKyR5gksVhtAfY1UYuWUOCCA0QNGwv9pOwdj+XUQ== - dependencies: - deepmerge "^4.2.2" - shvl "^2.0.3" + flatted "^2.0.0" + lodash "^4.17.19" vuex@^3.6.2: version "3.6.2" resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.6.2.tgz#236bc086a870c3ae79946f107f16de59d5895e71" integrity sha512-ETW44IqCgBpVomy520DT5jf8n0zoCac+sxWnn+hMe/CzaSejb/eVw2YToiXYX+Ex/AuHHia28vWTq4goAexFbw== -vuvuzela@1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/vuvuzela/-/vuvuzela-1.0.3.tgz#3be145e58271c73ca55279dd851f12a682114b0b" - integrity sha1-O+FF5YJxxzylUnndhR8SpoIRSws= - w3c-hr-time@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd" @@ -10758,13 +10278,6 @@ write-file-atomic@^3.0.0: signal-exit "^3.0.2" typedarray-to-buffer "^3.1.5" -write-stream@~0.4.3: - version "0.4.3" - resolved "https://registry.yarnpkg.com/write-stream/-/write-stream-0.4.3.tgz#83cc8c0347d0af6057a93862b4e3ae01de5c81c1" - integrity sha1-g8yMA0fQr2BXqThitOOuAd5cgcE= - dependencies: - readable-stream "~0.0.2" - ws@^7.4.6: version "7.5.6" resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.6.tgz#e59fc509fb15ddfb65487ee9765c5a51dec5fe7b" @@ -10797,7 +10310,7 @@ xmlchars@^2.2.0: resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== -xtend@^4.0.2, xtend@~4.0.0, xtend@~4.0.1: +xtend@~4.0.0, xtend@~4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== diff --git a/config/galaxy.yml.interactivetools b/config/galaxy.yml.interactivetools index 48909a654c2..bb7f3621fe7 100644 --- a/config/galaxy.yml.interactivetools +++ b/config/galaxy.yml.interactivetools @@ -1,54 +1,19 @@ -uwsgi: - - http: localhost:8080 - - threads: 8 - - http-raw-body: True - - offload-threads: 8 - - master: true - - module: galaxy.webapps.galaxy.buildapp:uwsgi_app() - - interactivetools_map: database/interactivetools_map.sqlite - python-raw: scripts/interactivetools/key_type_token_mapping.py - route-host: ^([A-Za-z0-9]+(?:-[A-Za-z0-9]+)*)-([A-Za-z0-9]+(?:-[A-Za-z0-9]+)*)\.([A-Za-z0-9]+(?:-[A-Za-z0-9]+)*)\.(interactivetool\.localhost:8080)$ goto:itdomain - route-run: goto:itdomainend - route-label: itdomain - route-host: ^([A-Za-z0-9]+(?:-[A-Za-z0-9]+)*)-([A-Za-z0-9]+(?:-[A-Za-z0-9]+)*)\.([A-Za-z0-9]+(?:-[A-Za-z0-9]+)*)\.(interactivetool\.localhost:8080)$ rpcvar:TARGET_HOST rtt_key_type_token_mapper_cached $1 $3 $2 $4 $0 5 - route-if-not: empty:${TARGET_HOST} httpdumb:${TARGET_HOST} - route: .* break:404 Not Found - route-label: itdomainend - # Path-based is currently less functional and less tested than domain-based - route: ^(/interactivetool/access/)([A-Za-z0-9]+(?:-[A-Za-z0-9]+)*)/([A-Za-z0-9]+(?:-[A-Za-z0-9]+)*)/([A-Za-z0-9]+(?:-[A-Za-z0-9]+)*)(()|(/.*)*)$ goto:itpath - route-run: goto:itpathend - route-label: itpath - route: ^/interactivetool/access/([A-Za-z0-9]+(?:-[A-Za-z0-9]+)*)/([A-Za-z0-9]+(?:-[A-Za-z0-9]+)*)/([A-Za-z0-9]+(?:-[A-Za-z0-9]+)*)(()|/.*)$ rpcvar:TARGET_HOST rtt_key_type_token_mapper_cached $2 $1 $3 $4 $0 5 - route-if: empty:${TARGET_HOST} goto:itpathfail - route: ^/interactivetool/access/([A-Za-z0-9]+(?:-[A-Za-z0-9]+)*)/([A-Za-z0-9]+(?:-[A-Za-z0-9]+)*)/([A-Za-z0-9]+(?:-[A-Za-z0-9]+)*)((()|/.*)*)$ rewrite:$4 - route-if: empty:${PATH_INFO} addvar:PATH_INFO=/ - route-run: seturi:${PATH_INFO} - route-if-not: empty:${QUERY_STRING} seturi:${PATH_INFO}?${QUERY_STRING} - route-if-not: empty:${TARGET_HOST} httpdumb:${TARGET_HOST} - route-label: itpathfail - route: .* break:404 Not Found - route-label: itpathend - - +gravity: + gx_it_proxy: + enable: true + proxy_port: 4002 galaxy: interactivetools_enable: true # outputs_to_working_directory will provide you with a better level of isolation. It is highly recommended to set # this parameter with InteractiveTools. outputs_to_working_directory: true - interactivetools_prefix: interactivetool interactivetools_map: database/interactivetools_map.sqlite - # If you develop InteractiveTools locally and do not have a full FQDN you can - # use an arbritrary one, e.g. 'my-hostname' here, if you set this hostname in your - # job_conf.xml as well (see the corresponding comment). If running mac OS X, do not match the - # "http://host.docker.internal:8080" used at galaxy_infrastructure_url in the galaxy.yml file, - # and use an arbitrary name here instead. - # Please make sure that in the local development case you use https://localhost:8080 to access - # your Galaxy. http://my-hostname:8080 will not work. - # galaxy_infrastructure_url: http://my-hostname:8080 + # `galaxy_infrastructure_url` needs to be reachable from IT containers. + # For local development you can map arbitrary hostnames. See `job_conf.xml.interactivetools` + # for an example. + # In the local development case you should use the `http` protocol (e.g http://localhost:8080) to access + # your Galaxy, so saving notebooks doesn't fail due to invalid certificates. + galaxy_infrastructure_url: http://localhost:8080 + # Do not set the following 2 options if you are using an upstream proxy server like nginx + interactivetools_upstream_proxy: false + interactivetools_proxy_host: localhost:4002 diff --git a/config/job_conf.xml.interactivetools b/config/job_conf.xml.interactivetools index 5daf8f054e5..f422ca36799 100644 --- a/config/job_conf.xml.interactivetools +++ b/config/job_conf.xml.interactivetools @@ -6,7 +6,6 @@ - true @@ -21,21 +20,16 @@ If you develop interactive tools on your 'localhost' and don't have a proper domain name you need to tell all Docker containers a hostname where Galaxy is running. This can be done via the add-host parameter during the `docker run` command. - 'my-hostname' here is an arbritrary hostname that matches the IP address of your - Galaxy host. Make sure this hostname ('my-hostname') is also set in your galaxy.yml file, e.g. - `galaxy_infrastructure_url: http://my-hostname:8080`. - Note: XML does not allow double hyphens in comments, hence the line below has them escaped as - + 'localhost' here is an arbitrary hostname that matches the IP address of your + Galaxy host. Make sure this hostname ('localhost') is also set in your galaxy.yml file, e.g. + `galaxy_infrastructure_url: http://localhost:8080`. --> - - + --add-host localhost:host-gateway - docker_dispatch docker_local local - - diff --git a/config/plugins/visualizations/jqplot/jqplot_bar/src/script.js b/config/plugins/visualizations/jqplot/jqplot_bar/src/script.js index 99dbe854806..bec0b8b510a 100644 --- a/config/plugins/visualizations/jqplot/jqplot_bar/src/script.js +++ b/config/plugins/visualizations/jqplot/jqplot_bar/src/script.js @@ -3,7 +3,7 @@ import _ from "underscore"; import jqplot from 'jqplot-exported/jqplot'; import { LineRenderer } from 'jqplot-exported/LineRenderer'; import { BarRenderer } from 'jqplot-exported/plugins/BarRenderer'; -import { OHLCRenderer } from 'jqplot-exported/plugins/OHLCRenderer'; +import { OHLCRenderer } from 'jqplot-exported/plugins/OhlcRenderer'; import { EnhancedLegendRenderer } from 'jqplot-exported/plugins/EnhancedLegendRenderer'; import { getDomains, makeCategories, makeTickFormat, makeSeries, mapCategories } from "@galaxyproject/charts/lib/utilities/series"; diff --git a/config/plugins/webhooks/demo/tour_generator/__init__.py b/config/plugins/webhooks/demo/tour_generator/__init__.py index c132aff0501..098d2cb52f3 100644 --- a/config/plugins/webhooks/demo/tour_generator/__init__.py +++ b/config/plugins/webhooks/demo/tour_generator/__init__.py @@ -163,7 +163,7 @@ class TourGenerator: if name in test_inputs: hid = self._hids[name] dataset = self._test.inputs[name][0] - step["content"] = "Select dataset: {}: {}".format(hid, dataset) + step["content"] = f"Select dataset: {hid}: {dataset}" else: step["content"] = "Select a dataset" @@ -192,7 +192,7 @@ class TourGenerator: if case_id in self._data_inputs.keys(): hid = self._hids[case_id] dataset = self._test.inputs[tour_id][0] - step_msg = "Select dataset: %s: %s" % (hid, dataset) + step_msg = f"Select dataset: {hid}: {dataset}" else: case_params = ", ".join(self._test.inputs[tour_id]) step_msg = "Select parameter(s): " + "%s" % case_params diff --git a/config/plugins/webhooks/gtn/script.js b/config/plugins/webhooks/gtn/script.js index bcd2223ee86..b0ea7c9db9b 100644 --- a/config/plugins/webhooks/gtn/script.js +++ b/config/plugins/webhooks/gtn/script.js @@ -1,6 +1,8 @@ (function () { var gtnWebhookLoaded = false; var lastUpdate = 0; + var urlParams = new URLSearchParams(document.location.search); + var autoLoadTutorial = urlParams.get('autoload_gtn_tutorial') === null ? "" : urlParams.get('autoload_gtn_tutorial'); function removeOverlay() { const container = document.getElementById("gtn-container"); @@ -58,7 +60,7 @@ .then((response) => { if (!response.ok) { url = - "https://training.galaxyproject.org/training-material/?utm_source=webhook&utm_medium=noproxy&utm_campaign=gxy"; + `https://training.galaxyproject.org/training-material/${autoLoadTutorial}?utm_source=webhook&utm_medium=noproxy&utm_campaign=gxy`; message = ` Click to run unavailable. @@ -75,7 +77,7 @@ onloadscroll = storedLocation.split(" ")[0]; url = storedLocation.split(" ")[1]; } else { - url = "/training-material/?utm_source=webhook&utm_medium=proxy&utm_campaign=gxy"; + url = `/training-material/${autoLoadTutorial}?utm_source=webhook&utm_medium=proxy&utm_campaign=gxy`; } message = ""; } @@ -181,6 +183,9 @@ showOverlay(); } }); + if(autoLoadTutorial){ + clean.click(); + } }); // Remove the overlay on escape button click diff --git a/contrib/galaxy.debian-init b/contrib/galaxy.debian-init deleted file mode 100644 index 1de8ffc83fb..00000000000 --- a/contrib/galaxy.debian-init +++ /dev/null @@ -1,59 +0,0 @@ -#!/bin/bash - -# Author: James Casbon, 2009 - -### BEGIN INIT INFO -# Provides: galaxy -# Required-Start: $network $local_fs $mysql -# Required-Stop: -# Default-Start: 2 3 4 5 -# Default-Stop: 0 1 6 -# Short-Description: Galaxy -### END INIT INFO - -. /lib/lsb/init-functions - -USER="galaxy" -GROUP="nogroup" -GALAXY_DIR="/home/galaxy/galaxy_dist/" -# Galaxy releases >= 16.01 installs dependencies by default into a virtualenv in /.venv -# A simple way to activate this virtualenv is to use the python interpreter in /.venv -# See https://galaxyproject.org/news/2016-01-galaxy-release/ and -# https://github.com/galaxyproject/galaxy/blob/dev/doc/source/admin/framework_dependencies.rst -PYTHON="/home/galaxy/galaxy_dist/.venv/bin/python" -OPTS="./scripts/paster.py serve --log-file /home/galaxy/galaxy.log config/galaxy.ini" -PIDFILE="/var/run/galaxy.pid" - -case "${1:-''}" in - 'start') - log_daemon_msg "Starting Galaxy" - if start-stop-daemon --chuid $USER --group $GROUP --start --make-pidfile \ - --pidfile $PIDFILE --background --chdir $GALAXY_DIR --exec $PYTHON -- $OPTS; then - log_end_msg 0 - else - log_end_msg 1 - fi - - ;; - 'stop') - log_daemon_msg "Stopping Galaxy" - if start-stop-daemon --stop --pidfile $PIDFILE; then - log_end_msg 0 - else - log_end_msg 1 - fi - ;; - 'restart') - # restart commands here - $0 stop - $0 start - - ;; - *) # no parameter specified - echo "Usage: $SELF start|stop|restart|reload|force-reload|status" - exit 1 - ;; -esac - - - diff --git a/contrib/galaxy.solaris-smf.xml b/contrib/galaxy.solaris-smf.xml deleted file mode 100644 index c44d73d6e0b..00000000000 --- a/contrib/galaxy.solaris-smf.xml +++ /dev/null @@ -1,75 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/create_db.sh b/create_db.sh index 3c34dd6c3e5..690d0c6f8c8 100755 --- a/create_db.sh +++ b/create_db.sh @@ -1,5 +1,21 @@ #!/bin/sh +####### +# Use this script to verify the state of the Galaxy and Tool Shed Install +# database(s). If the database does not exist or is empty, it will be created +# and initialized. +# (Use create_toolshed_db.sh to create and initialize a new +# Tool Shed database.) +# +# To pass a galaxy config file, use `--galaxy-config` +# +# You may also override the galaxy database url and/or the +# tool shed install database url, as well as the database_template +# and database_encoding configuration options with env vars: +# GALAXY_CONFIG_OVERRIDE_DATABASE_CONNECTION=my-db-url ./create_db.sh +# GALAXY_INSTALL_CONFIG_OVERRIDE_DATABASE_CONNECTION=my-other-db-url ./create_db.sh +####### + cd "$(dirname "$0")" . ./scripts/common_startup_functions.sh diff --git a/create_toolshed_db.sh b/create_toolshed_db.sh new file mode 100755 index 00000000000..3d266a3c767 --- /dev/null +++ b/create_toolshed_db.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +####### +# Use this script to verify the state of the Tool Shed database. +# If the database does not exist or is empty, it will be created +# and initialized. +# (For Galaxy and Tool Shed Install databases, use create_db.sh). +####### + +cd "$(dirname "$0")" + +. ./scripts/common_startup_functions.sh + +setup_python + +python ./scripts/create_toolshed_db.py "$@" tool_shed diff --git a/doc/source/_static/22.01-colour.mp4 b/doc/source/_static/22.01-colour.mp4 new file mode 120000 index 00000000000..ac9575e09d3 --- /dev/null +++ b/doc/source/_static/22.01-colour.mp4 @@ -0,0 +1 @@ +../releases/images/22.01-colour.mp4 \ No newline at end of file diff --git a/doc/source/_static/22.01-export.mp4 b/doc/source/_static/22.01-export.mp4 new file mode 120000 index 00000000000..dc52335e958 --- /dev/null +++ b/doc/source/_static/22.01-export.mp4 @@ -0,0 +1 @@ +../releases/images/22.01-export.mp4 \ No newline at end of file diff --git a/doc/source/admin/_inc_proxy_serving_root.rst b/doc/source/admin/_inc_proxy_serving_root.rst index fe043fa0243..605118beef7 100644 --- a/doc/source/admin/_inc_proxy_serving_root.rst +++ b/doc/source/admin/_inc_proxy_serving_root.rst @@ -1,10 +1,11 @@ This configuration assumes that Galaxy will be the only site on your server using the given hostname (e.g. ``https://galaxy.example.org``). -Beginning with Galaxy Release 18.01, the default application server that Galaxy runs under is uWSGI. Because of this, -the native high performance uWSGI protocol should be used for communication between |PROXY| and Galaxy, rather -than HTTP. Legacy instructions for proxying via HTTP can be found in the `Galaxy Release 17.09 proxy documentation`_. +Beginning with Galaxy Release 22.01, the default application server that Galaxy runs under is Gunicorn. Because of this, +the HTTP protocol should be used for communication between |PROXY| and Galaxy, rather +than the uWSGI protocol used in earlier version of this document. +Legacy instructions for proxying via uWSGI can be found in the `Galaxy Release 21.09 proxy documentation`_. -Since |PROXY| is more efficient than uWSGI at serving static content, it is best to serve it directly, reducing the load +Since |PROXY| is more efficient than Gunicorn at serving static content, it is best to serve it directly, reducing the load on the Galaxy process and allowing for more effective compression (if enabled), caching, and pipelining. Directives to do so are included in the example below. diff --git a/doc/source/admin/apache.md b/doc/source/admin/apache.md index 40370e9ceec..f44c0e0352c 100644 --- a/doc/source/admin/apache.md +++ b/doc/source/admin/apache.md @@ -27,28 +27,21 @@ also available. ### Apache Proxy Prerequisites -Currently, the only recommended way to proxy Galaxy with Apache is using `mod_rewrite`, `mod_proxy`, and -`mod_proxy_uwsgi`. These modules must be enabled in the Apache config. The main proxy directives, `ProxyRequests` and -`ProxyVia` do **not** need to be enabled. +Currently, the only recommended way to proxy Galaxy with Apache is using `mod_rewrite` and `mod_proxy`. +These modules must be enabled in the Apache config. The main proxy directives, `ProxyRequests` and `ProxyVia` do **not** need to be enabled. Additionally, these directions are written for Apache 2.4+. Apache 2.4 for EL 6 can be obtained from the [CentOS SCLo -SIG Repo][sclo-sig-repo]. Otherwise, your system package manager's version of Apache should be suitable. On EL, you will -need to enable the [EPEL][epel] repository to obtain the `mod_proxy_uwsgi` package. +SIG Repo][sclo-sig-repo]. Otherwise, your system package manager's version of Apache should be suitable. -```eval_rst -.. caution:: ``mod_uwsgi`` is not the same module as ``mod_proxy_uwsgi``. The former is the old and unsupported module. - Be sure that you have installed ``mod_proxy_uwsgi``. -``` - -Ensure that the `mod_headers`, `mod_rewrite`, `mod_proxy`, and `mod_proxy_uwsgi` modules are loaded. Although not +Ensure that the `mod_headers`, `mod_rewrite` and `mod_proxy`modules are loaded. Although not required, the configuration examples also use `mod_deflate` and `mod_expires` for increased client/server performance, so these should also be enabled. On Debian you can install the necessary packages and enable the modules this with the following: ```shell-session -# apt-get install apache2 libapache2-mod-proxy-uwsgi -# a2enmod headers deflate expires rewrite proxy proxy_uwsgi +# apt-get install apache2 +$ a2enmod headers deflate expires rewrite proxy Enabling module headers. Considering dependency filter for deflate: Module filter already enabled @@ -56,9 +49,6 @@ Module deflate already enabled Enabling module expires. Enabling module rewrite. Enabling module proxy. -Considering dependency proxy for proxy_uwsgi: -Module proxy already enabled -Enabling module proxy_uwsgi. To activate the new configuration, you need to run: service apache2 restart ``` @@ -66,12 +56,10 @@ To activate the new configuration, you need to run: And on EL: ```shell-session -# yum install httpd mod_proxy_uwsgi -# echo "LoadModule proxy_uwsgi_module modules/mod_proxy_uwsgi.so" > /etc/httpd/conf.modules.d/10-proxy-uwsgi.conf +# yum install httpd ``` [sclo-sig-repo]: https://wiki.centos.org/SpecialInterestGroup/SCLo/CollectionsList -[epel]: https://fedoraproject.org/wiki/EPEL ## Basic configuration @@ -83,7 +71,7 @@ And on EL: ```eval_rst .. include:: _inc_proxy_serving_root.rst -.. _Galaxy Release 17.09 Proxy Documentation: https://docs.galaxyproject.org/en/release_17.09/admin/special_topics/apache.html +.. _Galaxy Release 21.09 Proxy Documentation: https://docs.galaxyproject.org/en/release_21.09/admin/apache.html ``` The following configuration is not exhaustive, only the portions most relevant to serving Galaxy are shown, these should @@ -138,15 +126,15 @@ SSLStaplingCache shmcb:/var/run/ocsp(128000) # Galaxy needs to know that this is https for generating URLs - RequestHeader set X-URL-SCHEME "%{REQUEST_SCHEME}e" + RequestHeader set "X-Forwarded-Proto" expr=%{REQUEST_SCHEME} # allow up to 3 minutes for Galaxy to respond to slow requests before timing out ProxyTimeout 180 - # proxy all requests not matching other locations to uWSGI - ProxyPass / unix:///srv/galaxy/var/uwsgi.sock|uwsgi:// - # or uWSGI on a TCP socket - #ProxyPass / uwsgi://127.0.0.1:4001/ + # proxy all requests not matching other locations to Gunicorn + ProxyPass / unix:/srv/galaxy/var/gunicorn.sock|http://localhost/ + # or Gunicorn on a TCP socket + # ProxyPass / http://localhost:4001/ # serve framework static content RewriteEngine On @@ -170,8 +158,7 @@ SSLStaplingCache shmcb:/var/run/ocsp(128000) ``` Be sure to set `galaxy_root` to the path to your copy of Galaxy and modify the value of `ProxyPass /` to match your -uWSGI socket path. With the default configuration, uWSGI will bind to a random TCP socket, so you will need to set it to -a fixed value as described in the [Scaling and Load Balancing](scaling.md) documentation. If using a UNIX domain +Gunicorn socket path. With the default configuration, Gunicorn will bind to a TCP socket, so you will need Gunicorn to bind to a UNIX domain socket as described in the [Scaling and Load Balancing](scaling.md) documentation. If using a UNIX domain socket, be sure to pay particular attention to the discussion of users and permissions. ### Additional Notes @@ -183,8 +170,8 @@ socket, be sure to pay particular attention to the discussion of users and permi disable it by commenting its `` or preventing its inclusion (under Debian this is done by removing its symlink from `/etc/apache2/sites-enabled`). - `ProxyTimeout` can be adjusted as appropriate for your site. This is the amount of time allowed for communication - between Apache and uWSGI to block while waiting for a response from Galaxy, and is useful for holding client (browser) - connections while uWSGI is restarting Galaxy subprocesses or Galaxy is performing a slow operation. + between Apache and Gunicorn to block while waiting for a response from Galaxy, and is useful for holding client (browser) + connections while Gunicorn is restarting Galaxy subprocesses or Galaxy is performing a slow operation. - If your Apache server is set up to use `mod_security`, you may need to modify the value of the `SecRequestBodyLimit`. The default value on some systems will limit uploads to only a few kilobytes. - Some Galaxy URLs contain encoded slashes (%2F) in the path and Apache will not serve these URLs by default, which is @@ -195,6 +182,7 @@ socket, be sure to pay particular attention to the discussion of users and permi - If the proxy works but you are getting 404 errors for Galaxy's static content, be sure that the user that Apache runs as has access to Galaxy's `static/` directory (and all its parent directories) on the filesystem. You can test this on the command line with e.g. `sudo -u www-data ls /srv/galaxy/server/static`. +- A sample configuration for this setup is available [here](https://github.com/mvdbeek/galaxy_doc_examples/tree/main/apache). ### Serving Galaxy at a URL Prefix @@ -207,10 +195,10 @@ previous section: ```apache #... - # proxy all requests not matching other locations to uWSGI - ProxyPass /galaxy unix:///srv/galaxy/var/uwsgi.sock|uwsgi:// - # or uWSGI on a TCP socket - #ProxyPass /galaxy uwsgi://127.0.0.1:4001/ + # proxy all requests not matching other locations to Gunicorn + ProxyPass /galaxy unix:///srv/galaxy/var/gunicorn.sock|http://localhost/galaxy + # or Gunicorn on a TCP socket + #ProxyPass /galaxy http://127.0.0.1:4001/galaxy # serve framework static content RewriteEngine On @@ -221,16 +209,18 @@ previous section: ``` 2. The Galaxy application needs to be aware that it is running with a prefix (for generating URLs in dynamic pages). - This is accomplished by configuring uWSGI (the `uwsgi` section in `config/galaxy.yml`) like so and restarting Galaxy: + This is accomplished by configuring Galaxy and Gunicorn in your `config/galaxy.yml` file like so and restarting Galaxy: ```yaml - uwsgi: - #... - socket: /srv/galaxy/var/uwsgi.sock - mount: /galaxy=galaxy.webapps.galaxy.buildapp:uwsgi_app() - manage-script-name: true - # `module` MUST NOT be set when `mount` is in use - #module: galaxy.webapps.galaxy.buildapp:uwsgi_app() + gravity: + gunicorn: + # ... + bind: /srv/galaxy/var/gunicorn.sock + gunicorn_extra_args: '--forwarded-allow-ips="*"' + galaxy: + # ... + galaxy_url_prefix: /galaxy + # ... ``` ```eval_rst @@ -239,8 +229,7 @@ previous section: overrides the automatic setting. If you have this option set, unset it unless you know what you're doing. ``` - Be sure to consult the [Scaling and Load Balancing](scaling.md) documentation, other options unrelated to proxying - should also be set in the `uwsgi` section of the config. + Be sure to consult the [Scaling and Load Balancing](scaling.md) documentation. ## Advanced Configuration Topics diff --git a/doc/source/admin/cluster.md b/doc/source/admin/cluster.md index 7f9060a9cbe..37c0e047756 100644 --- a/doc/source/admin/cluster.md +++ b/doc/source/admin/cluster.md @@ -330,7 +330,12 @@ Since this is a complex problem, the current solution does have some caveats: ### Configuration -You'll need to ensure that all datasets are stored on the filesystem such that they are readable by all users that will use Galaxy: either made readable by a group, or world-readable. If using a group, set your `umask(1)` to `027` or for world-readable, use `022` Setting the umask assumes your underlying filesystem uses POSIX permissions, so if this is not the case, your environment changes may be different. For uWSGI setups (which are the default since release 19.01) the default umask is set in the `uwsgi:` section of `galaxy.yml` . +You'll need to ensure that all datasets are stored on the filesystem such that they are readable by all users that will use Galaxy: either made readable by a group, or world-readable. If using a group, set your `umask(1)` to `027` or for world-readable, use `022` Setting the umask assumes your underlying filesystem uses POSIX permissions, so if this is not the case, your environment changes may be different. For [gravity](https://github.com/galaxyproject/gravity/) setups (which are the default since release 22.01) the default umask can be set in the `gravity:` section of `galaxy.yml`: + +```yaml +gravity: + umask: 027 +``` The directory specified in `new_file_path` in the Galaxy config should be world-writable, cluster-accessible (via the same absolute path) and have its sticky bit (+t) set. This directory should also be cleaned regularly using a script or program as is appropriate for your site, since temporary files created here may not always be cleaned up under certain conditions. diff --git a/doc/source/admin/config.rst b/doc/source/admin/config.rst index dd628554a2d..0d95c1595a2 100644 --- a/doc/source/admin/config.rst +++ b/doc/source/admin/config.rst @@ -93,22 +93,17 @@ Configuration Basics ---------------------------- - Edit ``config/galaxy.yml`` (copy it from ``config/galaxy.yml.sample`` if it does not exist) to make configuration - changes. This is a `uWSGI YAML configuration file`_ and should contain two sections, one named ``uwsgi`` for uWSGI and - one named ``galaxy`` for Galaxy. + changes. This is a `YAML configuration file`_ and should contain one or two sections, one named ``gravity`` for + process management via `Gravity`_ (optional) and one named ``galaxy`` for Galaxy (required). - - The default port for the Galaxy web server is ``8080``, and it only binds to localhost by default. To configure - uWSGI to listen on all available network addresses, set ``http`` to ``0.0.0.0:`` (e.g. ``http: - 0.0.0.0:8080``). - - Some uWSGI options are required for uWSGI to run Galaxy properly and will be added to the ``uwsgi`` command line - by ``run.sh`` if not specified in ``galaxy.yml``. - - uWSGI has a `large number of options`_. The Galaxy documentation refers to some of them, but many additional - advanced deployment scenarios are available. + - The `gravity` section contain several important options about how to + launch Galaxy web workers and job handlers, which are documented in the + :doc:`Scaling and Load Balancing ` documentation. - Run Galaxy with ``sh run.sh`` - Use a web browser and go to the address you configured in ``galaxy.yml`` (defaults to http://localhost:8080/) -.. _uWSGI YAML configuration file: https://uwsgi-docs.readthedocs.io/en/latest/Configuration.html -.. _large number of options: https://uwsgi-docs.readthedocs.io/en/latest/Options.html +.. _Gravity: https://github.com/galaxyproject/gravity ---------------------------- Configuration Options diff --git a/doc/source/admin/config_logging.rst b/doc/source/admin/config_logging.rst index db3e9fc5073..e5730056d33 100644 --- a/doc/source/admin/config_logging.rst +++ b/doc/source/admin/config_logging.rst @@ -10,18 +10,9 @@ There are two ways in which you can configure logging for Galaxy servers: 2. More complex configuration using the Python :mod:`logging` module's :func:`logging.config.dictConfig` or :func:`logging.config.fileConfig`. By default, Galaxy logs all messages to standard output at the ``DEBUG`` logging level, unless the ``--daemon`` argument -is passed to ``run.sh``, in which case, output is logged to the file ``galaxy.log`` in the current directory. +is passed to ``run.sh``, in which case, output is logged to a location configured in `gravity`_. -The way in which you configure logging depends on whether you are using a YAML or INI configuration file, and also on -whether you are using the uWSGI application server, or Python Paste. Galaxy servers that were created starting with -Galaxy Release 18.01 or later use a YAML configuration file with uWSGI. Galaxy servers that were created with 17.09 or -older use an INI configuration file, and Python Paste by default, but they could be configured to run under uWSGI (and -this was the recommendation for production servers). If you upgrade a pre-18.01 server running under Paste to 18.01 or -later but do not convert your INI config (``galaxy.ini``) to a YAML config (``galaxy.yml``), the INI config and Paste -will still be used. - -uWSGI, Paste, and related terminology are explained in detail in the :doc:`Scaling and Load Balancing ` -documentation. +Gravity and related terminology are explained in detail in the :doc:`Scaling and Load Balancing ` documentation. Basic Configuration ---------------------------- @@ -32,9 +23,8 @@ is controlled by the ``log_level`` configuration option. If not set, Galaxy logs all messages at the ``DEBUG`` level (versions prior to 18.01 defaulted to ``INFO`` if unset, but the default config file shipped with ``log_level`` explicitly set to ``DEBUG`` for development purposes). -Galaxy logs all messages to standard output by default if running in the foreground. If running in the background (``sh -run.sh --daemon``) under uWSGI, the log is written to ``galaxy.log`` in the current directory. If running in the -background under Paste, the log is written to ``paster.log``. +Galaxy logs all messages to standard output by default if running in the foreground. If running in the background, the +log is written to a location configured in `gravity`_. **Setting the log level:** @@ -45,15 +35,6 @@ In ``galaxy.yml``, set ``log_level``: galaxy: log_level: LEVEL - -Or if using ``galaxy.ini``: - -.. code-block:: ini - - [app:main] - log_level = LEVEL - - Where ``LEVEL`` is one of the `logging levels`_ documented in the :mod:`logging` module. **Logging to a file:** @@ -68,7 +49,7 @@ To change the log file name or location, use the ``$GALAXY_LOG`` environment var Advanced Configuration ---------------------------- -For more useful and manageable logging when running Galaxy with forking application stacks (e.g. uWSGI) where multiple +For more useful and manageable logging when running Galaxy with forking application stacks where multiple Galaxy server processes are forked after the Galaxy application is loaded, some extensions to standard Python :mod:`logging` are available in Galaxy: @@ -98,145 +79,5 @@ The default as of this Galaxy release can be found (in Python syntax) in the .. include:: config_logging_default_yaml.rst -Split Logfiles -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Using ``run.sh --daemon`` causes Galaxy to log to ``galaxy.log``, but this is done using uWSGI's logging functionality -and does not allow for splitting logging in to multiple files. The following logging definition uses the -``filename_template`` custom handler configuration option to split logging in to the files ``galaxy_web_0.log`` (the -combined messages of all web workers) and ``galaxy_job-handlers_N.log`` where ``N`` is the instance ID of the server -process in its pool (aka the mule's position in its farm argument). In addition to the split file logging, the combined -output is also still logged to standard error. - -.. code-block:: yaml - - galaxy: - logging: - filters: - stack: - (): galaxy.web.stack.application_stack_log_filter - formatters: - stack: - (): galaxy.web.stack.application_stack_log_formatter - handlers: - console: - class: logging.StreamHandler - filters: - - stack - formatter: stack - level: DEBUG - stream: ext://sys.stderr - files: - class: logging.FileHandler - filters: - - stack - level: DEBUG - formatter: stack - filename: galaxy_default.log - filename_template: galaxy_{pool_name}_{server_id}.log - loggers: - galaxy: - handlers: - - console - - files - level: DEBUG - propagate: 0 - qualname: galaxy - paste.httpserver.ThreadPool: - level: WARN - qualname: paste.httpserver.ThreadPool - routes.middleware: - level: WARN - qualname: routes.middleware - root: - handlers: - - console - - files - level: INFO - version: 1 - -The list of available template facts for use in ``filename_template`` with all Galaxy application server types, and -their values under the various possible :doc:`server deployment scenarios ` are given below: - -+-------------------+-----------------------------------------------------------------------------------------------+ -| Fact | Application server | -+-------------------+-------------------------------+-------------------------------+-------------------------------+ -| | Paste/webless | uWSGI web worker | uWSGI mule | -+===================+===============================+===============================+===============================+ -| ``server_name`` | ``NAME`` for | ``main``, but can be modified with ``server_name`` in | -| | ``[server:]`` in | ``galaxy.yml`` | -| | ``galaxy.ini`` | | -+-------------------+-------------------------------+-------------------------------+-------------------------------+ -| ``server_id`` | ``None`` | 1-indexed worker ID | 1-indexed mule ID | -+-------------------+-------------------------------+-------------------------------+-------------------------------+ -| ``pool_name`` | ``None`` | ``web`` | Mule's farm name | -+-------------------+-------------------------------+-------------------------------+-------------------------------+ -| ``instance_id`` | ``None`` | Same as ``server_id`` | Mule's 1-indexed position in | -| | | | its defined farm | -+-------------------+-------------------------------+-------------------------------+-------------------------------+ -| ``fqdn`` | Fully-qualified domain name of the host on which Galaxy is running | -+-------------------+-----------------------------------------------------------------------------------------------+ -| ``hostname`` | "Short" hostname (with domain portion stripped) of the host on which Galaxy is running | -+-------------------+-----------------------------------------------------------------------------------------------+ - -The log message format can be customized by adding a (or modifying the default) formatter as in the following (partial) -example. Note that in addition to the new format, the value of ``formatter`` in the ``console`` handler has been -changed: - -.. code-block:: yaml - - galaxy: - logging: - formatters: - myformat: - (): '%(name)s %(levelname)s %(asctime)s [p:%(process)s,w:%(worker_id)s,m:%(mule_id)s] [%(threadName)s] %(message)s' - handlers: - console: - class: logging.StreamHandler - filters: - - stack - formatter: myformat - level: DEBUG - stream: ext://sys.stderr - -INI -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -With an INI Galaxy configuration, it is possible to use Python's :func:`logging.config.fileConfig` configuration method for -advanced logging configuration. For example: - -.. code-block:: ini - - [loggers] - keys = root, galaxy - - [handlers] - keys = console - - [formatters] - keys = generic - - [logger_root] - level = INFO - handlers = console - - [logger_galaxy] - level = DEBUG - handlers = console - qualname = galaxy - propagate = 0 - - [handler_console] - class = StreamHandler - args = (sys.stderr,) - level = DEBUG - formatter = generic - - [formatter_generic] - format = %(name)s %(levelname)-5.5s %(asctime)s [p:%(process)s,w:%(worker_id)s,m:%(mule_id)s] [%(threadName)s] %(message)s - -While Galaxy's custom log format fields can be used (as seen in the example), the ``filename_template`` handler -configuration extension is only available in the YAML format configuration file. - .. _logging levels: https://docs.python.org/library/logging.html#logging-levels .. _fileConfig file format: https://docs.python.org/library/logging.config.html#configuration-file-format diff --git a/doc/source/admin/framework_dependencies.rst b/doc/source/admin/framework_dependencies.rst index 8d7bb7aafcb..383a6dbc5b9 100644 --- a/doc/source/admin/framework_dependencies.rst +++ b/doc/source/admin/framework_dependencies.rst @@ -77,15 +77,6 @@ source code has been cloned to ``/srv/galaxy/server``. $ . /srv/galaxy/venv/bin/activate (venv)$ -Next, in ``galaxy.yml``, set the ``virtualenv`` option in the ``uwsgi`` section to point to your new virtualenv: - -.. code-block:: yaml - - --- - uwsgi: - #... - virtualenv: /srv/galaxy/venv - Install dependencies ^^^^^^^^^^^^^^^^^^^^ @@ -190,89 +181,26 @@ Galaxy can create a virtualenv using the adapted virtualenv package. Once a vali .. _conda-forge: https://conda-forge.org/ .. _Bioconda: https://bioconda.github.io/ -uWSGI -^^^^^ - -``run.sh`` should automatically set ``--virtualenv`` on uWSGI's command line. However, you can override this using the -``virtualenv`` option in the ``uwsgi`` section of ``galaxy.yml`` as described in the `Managing dependencies manually`_ -section. - -Unpinned dependencies -^^^^^^^^^^^^^^^^^^^^^ - -.. danger:: - - Unpinned dependencies may be useful for development but should not be used in production. Please do not install - unpinned dependencies unless you know what you're doing. While the :doc:`Galaxy Committers ` - will do their best to keep dependencies updated, they cannot provide support for problems arising from unpinned - dependencies. - -Galaxy's dependencies can be installed either "pinned" (they will be installed at exact versions specified for your -Galaxy release) or "unpinned" (the latest versions of all dependencies will be installed unless there are known -incompatibilities with new versions). By default, the release branches of Galaxy use pinned versions for three reasons: - -1. Using pinned versions insures that the prebuilt wheels will be installed, and no - compilation will be necessary. - -2. Galaxy releases are tested with the pinned versions and this allows us to give as much assurance as possible that the - pinned versions will work with the given Galaxy release (especially as time progresses and newer dependency versions - are released while the Galaxy release receives fewer updates. - -3. Pinning furthers Galaxy's goal of reproducibility as differing dependency versions could result in non-reproducible - behavior. - -If you would like to install unpinned versions of Galaxy's dependencies, install dependencies using the `unpinned -requirements file`_, and then instruct Galaxy to start without attempting to fetch wheels: - -.. code-block:: console - - (venv)$ pip install -r lib/galaxy/dependencies/requirements.txt - (venv)$ deactivate - $ sh run.sh --no-create-venv --skip-wheels - -You may be able to save yourself some compiling by adding the argument ``--index-url -https://wheels.galaxyproject.org/simple/`` to ``pip install``, but it is possible to install all of Galaxy's -dependencies directly from PyPI_. - -.. _unpinned requirements file: https://github.com/galaxyproject/galaxy/blob/dev/lib/galaxy/dependencies/requirements.txt -.. _PyPI: https://pypi.org - Adding additional Galaxy dependencies ------------------------------------- -New packages can be added to Galaxy, or the versions of existing packages can be updated, using `pipenv`_ and `Starforge`_, Galaxy's Docker-based build system. +New packages can be added to Galaxy, or the versions of existing packages can be updated, using `poetry`_ and `Starforge Recipes`_, Galaxy's Docker-based build system. -The process is still under development and will be streamlined and automated over time. For the time being, please use -the following process to add new packages and have their wheels built: +The process is still under development and will be streamlined and automated over time. +If wheels exist on PyPI for all supported platforms and python versions you can skip +to step 3 in the process below. -1. Install `Starforge`_ (e.g. with ``pip install starforge`` or ``python setup.py install`` from the source). You will - also need to have Docker installed on your system. +1. Clone https://github.com/galaxyproject/starforge-recipes/ and add or edit the wheel you would like to build under the wheels/ directory. -2. Obtain `wheels.yml`_ (this file will most likely be moved in to Galaxy in the future) and add/modify the wheel - definition. +2. Submit a pull request to `Starforge Recipes`_. -3. Use ``starforge wheel --wheels-config=wheels.yml `` to build the wheel. If the wheel includes C - extensions, you will probably want to also use the ``--no-qemu`` flag to prevent Starforge from attempting to build - on Mac OS X using QEMU/KVM. +3. Add the new dependency to the `[tool.poetry.dependencies]` (or to `[tool.poetry.dev-dependencies]` if only needed for Galaxy development) section of `pyproject.toml` . +4. Run `make update-dependencies` to update the requirements file in `lib/galaxy/dependencies`_. +5. Submit a pull request to Galaxy with your changes. -4. If the wheel build is successful, submit a pull request to `Starforge`_ with your changes to `wheels.yml`_. - -5. A :doc:`Galaxy Committers group ` member will need to trigger an automated build of the wheel - changes in your pull request. Galaxy's Jenkins_ service will build these changes using Starforge. - -6. If the pull request is merged, submit a pull request to Galaxy modifying the files in `lib/galaxy/dependencies`_ as - appropriate. - -You may attempt to skip directly to step 4 and let the Starforge wheel PR builder build your wheels for you. This is -especially useful if you are simply updating an existing wheel's version. However, if you are adding a new C extension -wheel that is not simple to build, you may need to go through many iterations of updating the PR and having a -:doc:`Galaxy Committers group ` member triggering builds -before wheels are successfully built. You can avoid this cycle by performing -steps 1-3 locally. - -.. _pipenv: https://pipenv.readthedocs.io/ -.. _Starforge: https://github.com/galaxyproject/starforge/ +.. _poetry: https://python-poetry.org/ +.. _Starforge Recipes: https://github.com/galaxyproject/starforge-recipes/ .. _Pull Request #4891: https://github.com/galaxyproject/galaxy/pull/4891 .. _wheels.yml: https://github.com/galaxyproject/starforge/blob/master/wheels/build/wheels.yml .. _Jenkins: https://jenkins.galaxyproject.org/ diff --git a/doc/source/admin/galaxy_options.rst b/doc/source/admin/galaxy_options.rst index 1db75278308..6855a2167f0 100644 --- a/doc/source/admin/galaxy_options.rst +++ b/doc/source/admin/galaxy_options.rst @@ -1707,13 +1707,25 @@ :Type: bool +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``interactivetools_upstream_proxy`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:Description: + Set this to false to redirect users of Interactive tools directly + to the Interactive tools proxy. `interactivetools_upstream_proxy` + should only be set to false in development. +:Default: ``true`` +:Type: bool + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ``interactivetools_proxy_host`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :Description: - Proxy host - assumed to just be hosted on the same hostname and - port as Galaxy by default. + Hostname and port of Interactive tools proxy. It is assumed to be + hosted on the same hostname and port as Galaxy by default. :Default: ``None`` :Type: str @@ -1918,13 +1930,12 @@ :Description: URL (with schema http/https) of the Galaxy instance as accessible - within your local network - if specified used as a default by - pulsar file staging and Jupyter Docker container for communicating + within your local network. This URL is used as a default by pulsar + file staging and Interactive Tool containers for communicating back with Galaxy via the API. - If you are attempting to set up GIEs on Mac OS X with Docker - Desktop for Mac and your Galaxy instance runs on port 8080 this - should be 'http://host.docker.internal:8080'. For more details - see https://docs.docker.com/docker-for-mac/networking/ + If you plan to run Interactive Tools make sure the docker + container can reach this URL. For more details see + `job_conf.xml.interactivetools`. :Default: ``http://localhost:8080`` :Type: str @@ -2310,6 +2321,18 @@ :Type: str +~~~~~~~~~~~~~~~~~~~~ +``tus_upload_store`` +~~~~~~~~~~~~~~~~~~~~ + +:Description: + The upload store is a temporary directory in which files uploaded + by the tus middleware or server will be placed. Defaults to + new_file_path if not set. +:Default: ``None`` +:Type: str + + ~~~~~~~~~~~~~~~~~~~~~ ``chunk_upload_size`` ~~~~~~~~~~~~~~~~~~~~~ @@ -2670,11 +2693,6 @@ use_printdebug. It also causes the files used by PBS/SGE (submission script, output, and error) to remain on disk after the job is complete. - In addition, this will set uWSGI's `honour-stdin` option to - `true`; thus, preventing uWSGI from remapping stdin to `/dev/null` - and enabling debugging with tools like pdb. To keep uWSGI's - default setting, set `honor-stdin` to `false` in the `uwsgi` - section of this configuration file. :Default: ``false`` :Type: bool @@ -2721,9 +2739,7 @@ are responsible for preparing/submitting and collecting/finishing jobs, and which can cause job errors if not shut down cleanly. If using supervisord, consider also increasing the value of - `stopwaitsecs`. If using job handler mules, consider also setting - the `mule-reload-mercy` uWSGI option. See the Galaxy Admin - Documentation for more. + `stopwaitsecs`. See the Galaxy Admin Documentation for more. :Default: ``30`` :Type: int @@ -2747,8 +2763,7 @@ :Description: Control the period (in seconds) between dumps. Use -1 to disable. Regardless of this setting, if use_heartbeat is enabled, you can - send a Galaxy process (unless running with uWSGI) SIGUSR1 (`kill - -USR1`) to force a dump. + send a Galaxy process SIGUSR1 (`kill -USR1`) to force a dump. :Default: ``20`` :Type: int @@ -4782,4 +4797,14 @@ :Type: str +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``display_builtin_converters`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:Description: + Display built-in converters in the tool panel. +:Default: ``true`` +:Type: bool + + diff --git a/doc/source/admin/index.rst b/doc/source/admin/index.rst index 79e79215658..efeebf35776 100644 --- a/doc/source/admin/index.rst +++ b/doc/source/admin/index.rst @@ -24,3 +24,4 @@ This documentation is in the midst of being ported and unified based on resource reports useful_scripts options + migrating_to_gunicorn diff --git a/doc/source/admin/jobs.md b/doc/source/admin/jobs.md index 936d7e093e4..513fd5dc713 100644 --- a/doc/source/admin/jobs.md +++ b/doc/source/admin/jobs.md @@ -128,25 +128,25 @@ tool using a ```` tag in the ```` section. ### Running jobs on a Kubernetes cluster via Pulsar -In order to dispatch jobs to a Kubernetes (K8s) cluster via Pulsar, -Pulsar implements a "two-container" architecture per pod, where +In order to dispatch jobs to a Kubernetes (K8s) cluster via Pulsar, +Pulsar implements a "two-container" architecture per pod, where one container stages job execution environment (`pulsar-container`) and another container encompasses tool's executables (`tool-container`). -Note that this architecture is experimental and under active development, +Note that this architecture is experimental and under active development, it is increasingly improved and it will soon be production-grade ready. -In order to setup Galaxy to use the "two-container" architecture, you may -take the following steps: +In order to setup Galaxy to use the "two-container" architecture, you may +take the following steps: 1. In the `galaxy.yml` set the following attributes: - `job_config_file: job_conf.yml` -- Appropriately configure `galaxy_infrastructure_url`; for example, -set it as the following on macOS: -`galaxy_infrastructure_url: 'http://host.docker.internal:$UWSGI_PORT'` +- Appropriately configure `galaxy_infrastructure_url`; for example, +set it as the following on macOS: +`galaxy_infrastructure_url: 'http://host.docker.internal:$GALAXY_WEB_PORT'` -2. In the `job_conf.yml` set the following runners and execution attributes appropriately: +2. In the `job_conf.yml` set the following runners and execution attributes appropriately: ```yaml runners: @@ -361,7 +361,6 @@ The following example assumes the existence of a job destination with ids `short ... ``` - With these if place, the following `default_runner` rule function will route all tools with id containing `mothur` to the `long_pbs` destination defined `jobs_conf.xml` and all other tools to the `short_pbs` destination: ```python diff --git a/doc/source/admin/migrating_to_gunicorn.md b/doc/source/admin/migrating_to_gunicorn.md new file mode 100644 index 00000000000..f5ed9566f40 --- /dev/null +++ b/doc/source/admin/migrating_to_gunicorn.md @@ -0,0 +1,45 @@ +# Migrating from uWSGI to Gunicorn and FastAPI + +## Why do we migrate away from uWSGI + +Starting with Galaxy release 22.01 the default application webserver is [Gunicorn](https://gunicorn.org/), which replaces [uWSGI](https://uwsgi-docs.readthedocs.io/en/latest/). We will remove support for running Galaxy under uWSGI in Galaxy release 22.05. +We have made this move because uWSGI support for newer Python versions has been problematic and because +we have modernized the API portion of our backend. The Galaxy web application is now an ASGI application +driven by [FastAPI](https://fastapi.tiangolo.com/) and [Starlette](https://www.starlette.io/). +This will enable numerous important features in the future that would not be possible (or would be more complicated to set up) +otherwise. Importantly, [FastAPI's documentation](https://fastapi.tiangolo.com/tutorial/) is very good and +should increase developer productivity. + +The main advantage for our users today is that Galaxy hosts documentation for its API as an [OpenAPI](https://www.openapis.org/) +document that is directly generated from Galaxy's source code, and so will never be out of date or differ +from the actual implementation. +Note that not all API endpoints have been modernized as of yet, this is an ongoing process. + +To see the API documentation for your Galaxy server you can go to `/api/docs`. +Another immediate advantage is that parameters to all incoming API requests are validated against a [Pydantic](https://pydantic-docs.helpmanual.io/) schema. +This will lead to fewer subtle bugs and make it easier to interface with Galaxy's API. + +In the future we are going to use Websockets and asynchronous programming to increase the reactivity +of the Galaxy user interface. This would not be as simple under uWSGI. + +## How do I upgrade my instance to use Gunicorn + +If you are using `run.sh` to start Galaxy and have not set up job handling via uWSGI mules, +you do not need to do anything to start Galaxy under Gunicorn. + +If you are using uWSGI mules, please read the [Scaling and Load Balancing documentation](scaling.md). +You will most likely want to set up the **Gunicorn + Webless** strategy. + +If you are using an upstream proxy server such as NGINX or Apache and you've been +using the uWSGI protocol, you need to replace `uwsgi_pass` and `mod_proxy_uwsgi`. +You can find detailed documentation in the [NGINX](nginx.md) and [Apache](apache.md) +documentation. + +If you are managing your Galaxy server via Ansible, you can continue to run Galaxy +under uWSGI while we are working on updating the Ansible roles for installing Galaxy. +We will update instructions here and on the [Galaxy Training Network](https://training.galaxyproject.org/). + +If you are starting Web and Job Handlers using an external process manager like +systemd or supervisor, please read the [Scaling and Load Balancing documentation](scaling.md). +In particular, you can use [gravity](https://github.com/galaxyproject/gravity) to generate supervisor configuration files +that you can either use directly or as a basis to update your existing configuration. diff --git a/doc/source/admin/nginx.md b/doc/source/admin/nginx.md index 1a8e8c1f774..e9e45926ce9 100644 --- a/doc/source/admin/nginx.md +++ b/doc/source/admin/nginx.md @@ -28,9 +28,7 @@ Instructions for [proxying with Apache](apache.md) are also available. ### NGINX Proxy Prerequisities -If you plan to use nginx to handle your file uploads, you will (most likely) not be able to use your package manager's -version of nginx. The [Receiving Files With NGINX](#receiving-files-with-nginx) section explains this in detail and -provides some options for installing *nginx + upload module* packages maintained by the Galaxy Committers Team. +If you are **not** planning to use the recommended [tus.io method](#receiving-files-via-the-tus-protocol) to handle file uploads but want to use nginx to handle uploads, you will (most likely) not be able to use your package manager's version of nginx. The [Receiving Files With NGINX](#receiving-files-with-nginx) section explains this in detail and provides some options for installing *nginx + upload module* packages maintained by the Galaxy Committers Team. Otherwise, your system package manager's version of nginx should be suitable. Under Debian, the [nginx-light][nginx-light] package contains all the necessary modules used in this guide. On EL, the [EPEL][epel] @@ -49,11 +47,9 @@ version of nginx is suitable. ```eval_rst .. include:: _inc_proxy_serving_root.rst -.. _Galaxy Release 17.09 Proxy Documentation: https://docs.galaxyproject.org/en/release_17.09/admin/special_topics/nginx.html +.. _Galaxy Release 21.09 Proxy Documentation: https://docs.galaxyproject.org/en/release_21.09/admin/nginx.html ``` -uWSGI protocol support is built in to nginx, so (unlike Apache) no extra modules or recompiling should be required. - The following configuration is not exhaustive, only the portions most relevant to serving Galaxy are shown, these should be incorporated with your existing/default nginx config as is appropriate for your server. Notably, the nginx package you installed most likely has a multi-file config layout. If you are not already familiar with that layout and where @@ -75,7 +71,7 @@ http { gzip_buffers 16 8k; # allow up to 3 minutes for Galaxy to respond to slow requests before timing out - uwsgi_read_timeout 180; + proxy_read_timeout 180; # maximum file upload size client_max_body_size 10g; @@ -122,11 +118,13 @@ http { # Enable HSTS add_header Strict-Transport-Security "max-age=15552000; includeSubdomains"; - # proxy all requests not matching other locations to uWSGI + # proxy all requests not matching other locations to Gunicorn location / { - uwsgi_pass unix:///srv/galaxy/var/uwsgi.sock; - uwsgi_param UWSGI_SCHEME $scheme; - include uwsgi_params; + proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_pass http://unix:/srv/galaxy/var/gunicorn.sock; } # serve framework static content @@ -152,9 +150,8 @@ http { } ``` -Be sure to set `$galaxy_root` to the path to your copy of Galaxy and modify the value of `uwsgi_pass` to match your -uWSGI socket path. With the default configuration, uWSGI will bind to a random TCP socket, so you will need to set it to -a fixed value as described in the [Scaling and Load Balancing](scaling.md) documentation. If using a UNIX domain +Be sure to set `$galaxy_root` to the path to your copy of Galaxy and modify the value of `proxy_pass` to match your +Gunicorn socket path. With the default configuration, gunicorn will bind to a TCP socket, so you will need to Gunicorn to bind to a UNIX domain socket as described in the [Scaling and Load Balancing](scaling.md) documentation. If using a UNIX domain socket, be sure to pay particular attention to the discussion of users and permissions. ### Additional Notes @@ -165,9 +162,9 @@ socket, be sure to pay particular attention to the discussion of users and permi - If your existing nginx configuration contains a line or included config file defining a default server, be sure to disable it by commenting its `server {}` or preventing its inclusion (under Debian this is done by removing its symlink from `/etc/nginx/sites-enabled`). -- `uwsgi_read_timeout` can be adjusted as appropriate for your site. This is the amount of time allowed for - communication between nginx and uWSGI to block while waiting for a response from Galaxy, and is useful for holding - client (browser) connections while uWSGI is restarting Galaxy subprocesses or Galaxy is performing a slow operation. +- `proxy_read_timeout` can be adjusted as appropriate for your site. This is the amount of time allowed for + communication between nginx and Gunicorn to block while waiting for a response from Galaxy, and is useful for holding + client (browser) connections while Gunicorn is restarting Galaxy subprocesses or Galaxy is performing a slow operation. - The parameter `client_max_body_size` specifies the maximum upload size that can be handled by POST requests through nginx. You should set this to the largest file size that you wish to allow for upload and that could be reasonably handled by your site. It defaults to 1MB, so it will need to be increased if you are dealing with genome sized @@ -191,11 +188,13 @@ previous section: ```nginx #... - # proxy all requests not matching other locations to uWSGI + # proxy all requests not matching other locations to Gunicorn location /galaxy { - uwsgi_pass unix:///srv/galaxy/var/uwsgi.sock - uwsgi_param UWSGI_SCHEME $scheme; - include uwsgi_params; + proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_pass http://unix:/srv/galaxy/var/gunicorn.sock:/galaxy; } # serve framework static content @@ -211,16 +210,19 @@ previous section: ``` 2. The Galaxy application needs to be aware that it is running with a prefix (for generating URLs in dynamic pages). - This is accomplished by configuring uWSGI (the `uwsgi` section in `config/galaxy.yml`) like so and restarting Galaxy: + This is accomplished by configuring Galaxy in your `config/galaxy.yml` file like so and restarting Galaxy: ```yaml - uwsgi: - #... - socket: /srv/galaxy/var/uwsgi.sock - mount: /galaxy=galaxy.webapps.galaxy.buildapp:uwsgi_app() - manage-script-name: true - # `module` MUST NOT be set when `mount` is in use - #module: galaxy.webapps.galaxy.buildapp:uwsgi_app() + gravity: + gunicorn: + # ... + bind: /srv/galaxy/var/gunicorn.sock + gunicorn_extra_args: '--forwarded-allow-ips="*"' + # ... + galaxy: + # ... + galaxy_url_prefix: /galaxy + # ... ``` ```eval_rst @@ -229,8 +231,7 @@ previous section: overrides the automatic setting. If you have this option set, unset it unless you know what you're doing. ``` - Be sure to consult the [Scaling and Load Balancing](scaling.md) documentation, other options unrelated to proxying - should also be set in the `uwsgi` section of the config. + Be sure to consult the [Scaling and Load Balancing](scaling.md) documentation. ## Advanced Configuration Topics @@ -488,8 +489,6 @@ subject](http://galacticengineer.blogspot.com/2015/06/exposing-galaxy-reports-vi After successfully following the blog post, Galaxy reports should be available at e.g. `https://galaxy.example.org/reports`. To secure this page to only Galaxy administrators, adjust your nginx config accordingly: -**TODO:** This is not valid for the uWSGI proxy method and needs to be updated. -nate 2018-01-11 - ```nginx location /reports { #... diff --git a/doc/source/admin/python.md b/doc/source/admin/python.md index c323c96b9fc..23870548c8e 100644 --- a/doc/source/admin/python.md +++ b/doc/source/admin/python.md @@ -1,6 +1,6 @@ # Supported Python versions -Galaxy's core functionality is currently supported on Python **>=3.6** . +Galaxy's core functionality is currently supported on Python **>=3.7** . If Galaxy complains about the version of Python you are using: diff --git a/doc/source/admin/reports.rst b/doc/source/admin/reports.rst index 91d10028d6a..2ce8843f375 100644 --- a/doc/source/admin/reports.rst +++ b/doc/source/admin/reports.rst @@ -41,7 +41,7 @@ following (and more): Configuration ---------------------------- -- Configure ``config/reports.yml`` in the same manner as your main galaxy instance (i.e., same database connection, but different web server port). This is a uWSGI YAML configuration file and should contain a ``reports`` section with app-specific configuration (options described below). +- Configure ``config/reports.yml`` in the same manner as your main galaxy instance (i.e., same database connection, but different web server port). This is a YAML configuration file and should contain a ``reports`` section with app-specific configuration (options described below). - The default port for the reports application is ``9001``, and like Galaxy it only binds to localhost by default. - ``database_connection`` should match the value used in your Galaxy configuration @@ -49,7 +49,7 @@ Configuration experimental support for MySQL is available but SQLite is not supported at all. -- Run reports in a uWSGI server with ``sh run_reports.sh`` +- Run reports in a Gunicorn server with ``sh run_reports.sh`` - Use a web browser and go to the address you configured in ``reports.yml`` (defaults to http://localhost:9001/) - If you'd like the report tool to persist between sessions then use ``sh run_reports.sh --daemon`` to run it as a background process. As with diff --git a/doc/source/admin/scaling.md b/doc/source/admin/scaling.md index 26a6d41442e..550f0bbcd22 100644 --- a/doc/source/admin/scaling.md +++ b/doc/source/admin/scaling.md @@ -9,9 +9,8 @@ more thorough explanation of this problem and why you will almost surely want to configuration if running for more than a small handful of users, see the [production configuration](production.md) page. -Just to be clear: increasing the values of `threadpool_workers` in `galaxy.yml` or the number of plugin workers in -`job_conf.xml` will not make you Galaxy server much more responsive. The key to scaling Galaxy is the ability to run -*multiple* Galaxy servers which co-operatively work on the same database. +Just to be clear: Increasing the number of plugin workers in `job_conf.xml` will not make your Galaxy server much more responsive. +The key to scaling Galaxy is the ability to run *multiple* Galaxy servers which co-operatively work on the same database. ## Terminology @@ -22,40 +21,21 @@ Just to be clear: increasing the values of `threadpool_workers` in `galaxy.yml` mapped to a handler tag such that all executions of that tool are handled by the tagged handler(s). * **default** - Any handlers without defined tags - aka "untagged handlers" - will handle executions of all tools not mapped to a specific handler ID or tag. -* **[uWSGI][uwsgi]** - Powerful application server written in C that implements the HTTP and Python WSGI protocols - * **[Mules][uwsgi-mules]** - uWSGI processes started after the main application (Galaxy) that can run separate code - and receive messages from uWSGI web workers - * **[Zerg Mode][uwsgi-zerg-mode]** - uWSGI configuration where multiple copies of the same application can be started - simultaneously in order to maintain availability during application restarts -* **Webless Galaxy application** - The Galaxy application run as a standalone Python application with no web/WSGI server - -[uwsgi]: https://uwsgi-docs.readthedocs.io/ -[uwsgi-mules]: https://uwsgi-docs.readthedocs.io/en/latest/Mules.html -[uwsgi-zerg-mode]: https://uwsgi-docs.readthedocs.io/en/latest/Zerg.html +* **Webless Galaxy application** - The Galaxy application run as a standalone Python application with no web/ASGI server ## Application Servers It is possible to run the Galaxy server in many different ways, including under different web application frameworks, or -as a standalone server with no web stack. For most of its modern life, prior to the 18.01 release, Galaxy (by default) -used the [Python Paste][paste] web stack, and ran in a single process. +as a standalone server with no web stack. Prior to the 18.01 release, Galaxy (by default) +used the [Python Paste][paste] web stack, and ran in a single process. Between the 18.01 release and the 22.01 release +[uWSGI](https://uwsgi-docs.readthedocs.io/) was used as the default application server. Starting with the 22.01 release the default application server is +[Gunicorn][Gunicorn]. For information about uWSGI in the Galaxy context please consult the version of this document that is +appropriate to your Galaxy release. -Beginning with Galaxy release 18.01, the default application server for new installations of Galaxy is [uWSGI][uwsgi]. -Prior to 18.01, it was possible (and indeed, recommended for production Galaxy servers) to run Galaxy under uWSGI, but -it was necessary to install and configure uWSGI separately from Galaxy. uWSGI is now provided with Galaxy as a Python -Wheel and installed in to its virtualenv, as described in detail in the [Framework -Dependencies](framework_dependencies) documentation. - -uWSGI has numerous benefits over Python Paste for our purposes: - -* Written in C and designed to be high performance -* Easily runs multiple processes by increasing `processes` config option -* Load balances multiple processes internally rather than requiring load balancing in the proxy server -* Offload engine for serving static content -* Speaks high performance native protocol between uWSGI and proxy server -* Can speak HTTP and HTTPS protocols without proxy server -* Incredibly featureful, supports a wide array of deployment scenarios -* Supports WebSockets, which enable Galaxy Interactive Environments out-of-the-box without a proxy server or Node.js +Gunicorn is able to serve ASGI applications. Galaxy can act as an ASGI web application since release 21.01, +and we will drop support for being run as a WSGI application via uWSGI in Galaxy release 22.05. +[Gunicorn]: https://gunicorn.org/ [paste]: https://paste.readthedocs.io/ ## Deployment Options @@ -64,61 +44,26 @@ There are multiple deployment strategies for the Galaxy application that you can the configuration of the infrastructure on which you are deploying. In all cases, all Galaxy job features such as [running on a cluster](cluster.md) are supported. -Although uWSGI implements nearly all the features that were previously the responsibility of an upstream proxy server, -at this time, it is still recomended to place a proxy server in front of uWSGI and utilize it for all of its traditional +Although Gunicorn implements many features that were previously the responsibility of an upstream proxy server, +it is recommended to place a proxy server in front of Gunicorn and utilize it for all of its traditional roles (serving static content, serving dataset downloads, etc.) as described in the [production configuration](production.md) documentation. -When using uWSGI with a proxy server, it is recommended that you use the native high performance uWSGI protocol -(supported by both [Apache](apache.md) and [nginx](nginx.md)) between uWSGI and the -proxy server, rather than HTTP. +### Gunicorn with jobs handled by web workers (default configuration) -### uWSGI with jobs handled by web workers (default configuration) - -Referred to in this documentation as the **uWSGI all-in-one** strategy. +Referred to in this documentation as the **all-in-one** strategy. * Job handlers and web workers are the same processes and cannot be separated -* The web worker that receives the job request from the UI/API will be the job handler for that job +* A random web worker will be the job handler for that job -Under this strategy, jobs will be handled by uWSGI web workers. Having web processes handle jobs will negatively impact +Under this strategy, jobs will be handled by Gunicorn workers. Having web processes handle jobs will negatively impact UI/API performance. -This is the default out-of-the-box configuration as of Galaxy Release 18.01 and will be deprecated in Galaxy Release 21.09 -in favor of running separate processes for handling web requests (gunicorn serving a fastAPI instance), for handling jobs and -workflows (using the current webless job handling code) and Celery workers for running long-running and/or CPU-intensive tasks. +This is the default out-of-the-box configuration. -### uWSGI for web serving with Mules as job handlers +### Gunicorn for web serving and Webless Galaxy applications as job handlers -Referred to in this documentation as the **uWSGI + Mules** strategy. - -* Job handlers run as children of the uWSGI process -* Jobs are dispatched from web workers to job handlers via native *mule messaging* -* Jobs can only be dispatched to mules on the same host -* Trivially easy to enable (disabled by default for simplicity reasons) - -Under this strategy, job handling is offloaded to dedicated non-web-serving processes that are started and stopped -directly by the master uWSGI process. As a benefit of using mule messaging, only job handlers that are alive will be -selected to run jobs. - -This was the recommended deployment strategy until Galaxy release 21.01. This deployment strategy will be supported -until Galaxy release 21.05 and will be deprecated in Galaxy release 21.09. - -```eval_rst -.. important:: - - If using **Zerg Mode** or running more than one uWSGI *master* process, do not use **uWSGI + Mules**. Doing so can - can cause jobs to be executed by mutiple handlers when recovering unassigned jobs at Galaxy server startup. - - Multiple master processes is a rare configuration and is typically only used in the case of load balancing the web - application across multiple hosts. Note that multiple master proceses is not the same thing as the ``processess`` - uWSGI configuration option, which is perfectly safe to set when using job handler mules. - - For these scenarios, **uWSGI + Webless** is the recommended deployment strategy. -``` - -### uWSGI for web serving and Webless Galaxy applications as job handlers - -Referred to in this documentation as the **uWSGI + Webless** strategy. +Referred to in this documentation as the **Gunicorn + Webless** strategy. * Job handlers are started as standalone Python applications with no web stack * Jobs are dispatched from web workers to job handlers via the Galaxy database @@ -126,73 +71,52 @@ Referred to in this documentation as the **uWSGI + Webless** strategy. * Additional job handlers can be added dynamically without reconfiguring/restarting Galaxy (19.01 or later) * The recommended deployment strategy for production Galaxy instances -Like mules, under this strategy, job handling is offloaded to dedicated non-web-serving processes, but those processes -are [managed by the administrator](#starting-and-stopping). - By default, handler assignment will occur using the **Database Transaction Isolation** or **Database SKIP LOCKED** -methods (see below). However, if the database used does not support this mechanism (in practice this should only apply -to sqlite before version 3.25, which is not at all recommended for production Galaxy server) a handler is randomly -assigned by the web worker when the job is submitted via the UI/API, meaning that jobs may be assigned to dead handlers. +methods (see below). -This is the recommended deployment strategy when **Zerg Mode** is used, for Galaxy servers that run web servers and -job handlers **on different hosts**, and for deployments where dynamic handler addition is desired. +### uWSGI for web serving with Mules as job handlers -Beginning with Galaxy release 19.01, it is also possible to use a combination of both **uWSGI + Mules** and **uWSGI + -Webless**, referred to in the documentation as **uWSGI + Hybrid**. +This strategy is deprecated and will be removed in Galaxy release 22.05. +You can consult the documentation for older versions of Galaxy for details. -### Legacy - -Other deployment strategies were commonly used in older versions of Galaxy and can be found in previous versions of the -documentation, but these are deprecated and should no longer be used. +If you're upgrading Galaxy to release 22.01 or newer we recommend you set up the +**Gunicorn + Webless** strategy above. ## Job Handler Assignment Methods -Job handler assignment methods configurable with the `assign_with` +Job handler assignment methods are configurable with the `assign_with` attribute on the `` tag in `job_conf.xml`. The available methods are: -- **Database Transaction Isolation** (`db-transaction-isolation`, new in 19.01) - Jobs are assigned a handler by handlers selecting the unassigned +* **Database Transaction Isolation** (`db-transaction-isolation`, new in 19.01) - Jobs are assigned a handler by handlers selecting the unassigned job from the database using SQL transaction isolation, which uses database locks to guarantee that only one handler can select a given job. This occurs by the web worker that receives the tool execution request (via the UI or API) setting a new job's 'handler' column in the database to the configured tag/default (or `_default_` if no tag/default is configured). Handlers "listen" for jobs by selecting jobs from the database that match the handler tag(s) for which - they are configured. This is the default if no handlers are defined, or handlers are defined but no assign_with attribute is set - on the `handlers` tag and *Database SKIP LOCKED* is not available and no uwsgi-mule-messaging is set up. + they are configured. `db-transaction-isolation` is the default assignment method if no handlers are defined, + or handlers are defined but no `assign_with` attribute is set on the `handlers` tag and *Database SKIP LOCKED* is not available. -- **Database SKIP LOCKED** (`db-skip-locked`, new in 19.01) - Jobs are assigned a handler by handlers selecting the unassigned job from +* **Database SKIP LOCKED** (`db-skip-locked`, new in 19.01) - Jobs are assigned a handler by handlers selecting the unassigned job from the database using `SELECT ... FOR UPDATE SKIP LOCKED` on databases that support this query (see the next section for details). This occurs via the same process as *Database Transaction Isolation*, the only difference is the way in - which handlers query the database. This is the default if no handlers are defined, or handlers are defined but no assign_with attribute is set - on the `handlers` tag and *Database SKIP LOCKED* is available and no uwsgi-mule-messaging is set up. + which handlers query the database. This is the default if no handlers are defined, or handlers are defined but no `assign_with` attribute is set + on the `handlers` tag and *Database SKIP LOCKED* is available. -- **Database Self Assignment** (`db-self`) - Like *In-memory Self Assignment* but assignment occurs by setting a new job's 'handler' +* **Database Self Assignment** (`db-self`) - Like *In-memory Self Assignment* but assignment occurs by setting a new job's 'handler' column in the database to the process that created the job at the time it is created. Additionally, if a tool is configured to use a specific handler (ID or tag), that handler is assigned (tags by *Database Preassignment*). This is - the default if no handlers are defined and no `job-handlers` uWSGI Farm is present and the database does not support - *Database SKIP LOCKED* or *Database Transaction Isolation*. + the default fallback if no handlers are defined and the database does not support *Database SKIP LOCKED* or *Database Transaction Isolation*. -- **In-memory Self Assignment** (`mem-self`) - Jobs are assigned to the web worker that received the tool execution request from the +* **In-memory Self Assignment** (`mem-self`) - Jobs are assigned to the web worker that received the tool execution request from the user via an internal in-memory queue. If a tool is configured to use a specific handler, that configuration is ignored; the process that creates the job *always* handles it. This can be slightly faster than **Database Self Assignment** but only makes sense in single process environments without dedicated job handlers. This option - supercedes the former `track_jobs_in_database` option in `galaxy.yml` and corresponds to setting that option to - `false`. Will be removed from Galaxy in release 21.09. + supersedes the former `track_jobs_in_database` option in `galaxy.yml` and corresponds to setting that option to + `false`. -- **Database Preassignment** (`db-preassign`) - Jobs are assigned a handler by selecting one at random from the configured tag or default +* **Database Preassignment** (`db-preassign`) - Jobs are assigned a handler by selecting one at random from the configured tag or default handlers at the time the job is created. This occurs by the web worker that receives the tool execution request (via the UI or API) setting a new job's 'handler' column in the database to the randomly chose handler ID (hence - "preassignment"). This is the default if handlers are defined and no `job-handlers` uWSGI Farm is present - and the database does not support *Database SKIP LOCKED* or *Database Transaction Isolation*. - -- **uWSGI Mule Messaging** (`uwsgi-mule-message`) - Jobs are assigned a handler via uWSGI mule messaging. A mule in the `job-handlers` (for - default/untagged tool-to-handler mappings) or `job-handlers.` farm will receive the message and assign itself. - This the default if a `job-handlers` uWSGI Farm is present and no handlers are configured. - Will be removed in Galaxy release 21.09. - -In the event that both a `job-handlers` uWSGI Farm is present and handlers are configured, the default is *uWSGI Mule -Messaging* followed by *Database SKIP LOCKED* or *Database Transaction Isolation* or *Database Preassignment*, depending on which method -is supported by the database in use. At present, only *uWSGI Mule Messaging* is capable of deferring handler -assignment to a later method (which would occur in the event that a tool is configured to use a tag for which there is -not a matching farm). + "preassignment"). This is the default only if handlers are defined and the database does not support *Database SKIP LOCKED* or *Database Transaction Isolation*. In all cases, if a tool is configured to use a specific handler (by ID, not tag), configured assignment methods are ignored and that handler is directly assigned in the job's 'handler' column at job creation time. @@ -203,33 +127,24 @@ assignment method. ### Choosing an Assignment Method Prior to Galaxy 19.01, the most common deployment strategies (e.g. **uWSGI + Webless**) assigned handlers using what is -now (since 19.01) referred to as *Database Preassignment*. Although still the default in many cases (until the new -methods mature), preassignment has a few drawbacks: +now (since 19.01) referred to as *Database Preassignment*. Although still a fallback option when the database +does not support *Database SKIP LOCKED* or *Database Transaction Isolation*, preassignment has a few drawbacks: -- Web workers do not have a way to know whether a particular handler is alive when assigning that handler -- Jobs are not load balanced across handlers -- Changing the number of handlers requires changing `job_conf.xml` and restarting *all* Galaxy processes +* Web workers do not have a way to know whether a particular handler is alive when assigning that handler +* Jobs are not load balanced across handlers +* Changing the number of handlers requires changing `job_conf.xml` and restarting *all* Galaxy processes -The new "database locking" methods (*Database SKIP LOCKED* and *Database Transaction Isolation*) were created to solve -these issues. The preferred method between the two new options is *Database SKIP LOCKED*, but it requires PostgreSQL 9.5 -or newer, MySQL 8.0 or newer (untested), or MariaDB 10.3 or newer (untested). If using an older database version, use -*Database Transaction Isolation* instead. A detailed explanation of these database locking methods in PostgreSQL can be +The "database locking" methods (*Database SKIP LOCKED* and *Database Transaction Isolation*) were created to solve +these issues. The preferred method between the two options is *Database SKIP LOCKED*, but it requires PostgreSQL 9.5 +or newer, SQLite 3.25 or newer or MySQL 8.0 or newer (untested), or MariaDB 10.3 or newer (untested). +If using an older database version, use *Database Transaction Isolation* instead. A detailed explanation of these database locking methods in PostgreSQL can be found in the excellent [What is SKIP LOCKED for in PostgreSQL 9.5?][2ndquadrant-skip-locked] entry on the [2ndQuadrant PostgreSQL Blog][2ndquadrant-blog]. -The preferred method depends on your deployment strategy: +The preferred assignment method is *Database SKIP LOCKED* or *Database Transaction Isolation*. -- **uWSGI + Mules** - Either *Database SKIP LOCKED* or *Database Transaction Isolation* is preferred. -- **uWSGI + Webless** - Either *Database SKIP LOCKED* or *Database Transaction Isolation* is preferred. -- **uWSGI + Hybrid** - Either *Database SKIP LOCKED* or *Database Transaction Isolation* is preferred. If your mule and - webless handlers are in non-overlapping pools (i.e. tags, or untagged), you can alternatively use both *uWSGI Mule - Messaging* followed by either *Database SKIP LOCKED* or *Database Transaction Isolation*. If pools overlap, using - *uWSGI Mule Messaging* would prevent any non-mule handlers in that pool from being assigned jobs. - -Handlers (as well as assignment methods) are not configurable when using **uWSGI all-in-one**. - -[2ndquadrant-skip-locked]: https://blog.2ndquadrant.com/what-is-select-skip-locked-for-in-postgresql-9-5/ -[2ndquadrant-blog]: https://blog.2ndquadrant.com/ +[2ndquadrant-skip-locked]: https://www.2ndquadrant.com/en/blog/what-is-select-skip-locked-for-in-postgresql-9-5/ +[2ndquadrant-blog]: https://www.2ndquadrant.com/en/blog/ ```eval_rst .. _scaling-configuration: @@ -237,160 +152,126 @@ Handlers (as well as assignment methods) are not configurable when using **uWSGI ## Configuration -### uWSGI +### Gunicorn -Although this document goes in to significant detail about uWSGI configuration, many more options are available, as well -as additional documentation about options described here. Consult the uWSGI documentation for more: +We will only outline a few of Gunicorn's options, consult the [Gunicorn documentation](https://docs.gunicorn.org/en/latest/settings.html) for more. -* [Configuring uWSGI](https://uwsgi-docs.readthedocs.io/en/latest/Configuration.html) -* [uWSGI Options](https://uwsgi-docs.readthedocs.io/en/latest/Options.html) -* [Quickstart for Python/WSGI applications](https://uwsgi-docs.readthedocs.io/en/latest/WSGIquickstart.html) +Note that by default Galaxy will use [gravity](https://github.com/galaxyproject/gravity/) to create a [supervisor](http://supervisord.org/) configuration in Gravity's state directory. +Gravity's state directory is located in `database/gravity`, and you can find the generated supervisor configuration in +`database/gravity/supervisor`. The location of the state directory can be controlled using the `--state-dir` argument +of `galaxyctl`, or using the `GRAVITY_STATE_DIR` environment variable. +Configuration values for the supervisor configuration are read from the `gravity` section of your `galaxy.yml` file. +This is the preferred and out-of-the box way of configuring Gunicorn for serving Galaxy. +If you are not using `./run.sh` for starting Galaxy or you would like to use another process manager, +all the Gunicorn configuration values can also directly be set on the command line. -Configuration is performed in the `uwsgi` section of `galaxy.yml`. You will find that the default, if copied from -`galaxy.yml.sample`, is commented out. The default configuration options are provided to uWSGI on the command line by -Galaxy's `run.sh` script. +Configuration is performed in the `gravity` section of `galaxy.yml`. You will find that the default, if copied from +`galaxy.yml.sample`, is commented out. The default configuration options are provided to Gunicorn on the command line by +using `gravity` within the `run.sh` script. -Galaxy releases prior to 18.01 (or upgraded-to-18.01+ servers which have not migrated their configuration to the YAML -format) used an INI-format configuration file, `galaxy.ini`. +After making changes to the `gravity` section, you always need to activate Galaxy's virtualenv and run `galaxyctl update`, +or one of the `start`, `stop`, `restart`, and `graceful` subcommands of the `galaxyctl` command, which will run the `update` command internally. -Note that uWSGI's YAML parser is hand-coded and not actually conformant to the YAML standard. Specifically: +#### Common Gunicorn configuration -* Multiple identical keys with unique values can exist in the same dictionary/hash, as with `hook-master-start` in the - example below. -* Quoting values (with single or double quotes) is unncessary since the parser treats all values as strings. The parser - does not correctly handle these quote characters, resulting in invalid values. - -If using `galaxy.ini`, the option names and values are the same but in INI format, for example: - -```ini -[uwsgi] -processes = 4 -socket = 127.0.0.1:4001 -... -``` - -#### Configuration common to all uWSGI deployment styles - -In `galaxy.yml`, define a `uwsgi` section. Shown below are the options common to all deployment strategies: +In `galaxy.yml`, define a `gravity` section. Shown below are the options common to all deployment strategies: ```yaml -uwsgi: +gravity: + app_server: gunicorn + gunicorn: + # listening options + bind: '127.0.0.1:8080' + # performance options + workers: 1 + # Other options that will be passed to gunicorn + gunicorn_extra_args: - # required in order to start the galaxy application - module: galaxy.webapps.galaxy.buildapp:uwsgi_app() - virtualenv: .venv - pythonpath: lib - - # performance options - master: true - enable-threads: true - processes: 2 - threads: 4 - offload-threads: 1 - - # fix up signal handling - die-on-term: true - hook-master-start: unix_signal:2 gracefully_kill_them_all - hook-master-start: unix_signal:15 gracefully_kill_them_all - - # listening options - - # job handling options ``` -Some of these options warrant explanation: +Some of these options deserve explanation: -* `master`: Instructs uWSGI to first start a master process manager and then fork web workers, mules, http servers (if - enabled), and any others from the master. This is required for certain operational modes such as daemonization, but - can interfere with the use of `+` to shut down Galaxy when running in the foreground on the command line, and - so is not enabled by default (except when `run.sh --daemon` is used). Its use is strongly recommended for all - production deployments. -* `processes`: Controls the number of Galaxy application processes uWSGI will spawn. Increased web performance can be - attained by increasing this value. -* `threads`: Controls the number of web worker threads each application process will spawn. -* `offload-threads`: uWSGI can use a dedicated threadpool for serving static content and handling internal routing, - setting this value automatically enables such offloading. - -Additional options are explained in the [uWSGI Minutiae](#uwsgi-minutiae) below. +* `workers`: Controls the number of Galaxy application processes Gunicorn will spawn. Increased web performance can be + attained by increasing this value. If Gunicorn is the only application on the server, a good starting value is the number of CPUs * 2 + 1. +4-12 workers should be able to handle hundreds if not thousands of requests per second. +* `gunicorn_extra_args`: You can specify additional arguments to pass to gunicorn here. Note that the performance option values given above are just examples and should be tuned per your specific needs. However, as given, they are a good place to start. -Due to the Python GIL, increasing the value of `threads` has diminishing returns on web performance while increasing the -memory footprint of each application process. Increasing it is most useful on servers experiencing a high amount of IO -waiting, but the greatest performance gain comes from increasing `processes` as appropriate for the hardware on which -Galaxy is running. - #### Listening and proxy options **With a proxy server:** -To use the native uWSGI protocol, set the `socket` option: +To use a socket for the communication between the proxy and Gunicorn, set the `bind` option to a path: ```yaml - # listening options - socket: /srv/galaxy/var/uwsgi.sock +gravity: + app_server: gunicorn + gunicorn: + # listening options + bind: 'unix:/srv/galaxy/var/gunicorn.sock' + gunicorn_extra_args: '--forwarded-allow-ips="*"' ``` Here we've used a UNIX domain socket because there's less overhead than a TCP socket and it can be secured by filesystem -permissions, but you can also listen on a port: +permissions. Note that we've added `--forwarded-allow-ips="*"` to ensure that the domain socket is trusted as a source from which to proxy headers. + +You can also listen on a port: ```yaml - # listening options - socket: 127.0.0.1:4001 + app_server: gunicorn + gunicorn: + # listening options + bind: '127.0.0.1:4001' ``` +If you are listening on a port do not set `--forwarded-allow-ips="*"`. + The choice of port 4001 is arbitrary, but in both cases, the socket location must match whatever socket the proxy server is configured to communicate with. If using a UNIX domain socket, be sure that the proxy server's user has read/write permission on the socket. Because Galaxy and the proxy server most likely run as different users, this is not likely to be the case by default. One common solution is to add the proxy server's user to the Galaxy user's primary group. -uWSGI's `chmod-socket` option can also help here. +Gunicorn's `umask` option can also help here. You can consult the Galaxy documentation for [Apache](apache.md) or [nginx](nginx.md) for help with the proxy-side configuration. -By setting the `socket` option, `run.sh` will no longer automatically serve Galaxy via HTTP (since it is assumed that -you are setting a socket to serve Galaxy via a proxy server). If you wish to continue serving HTTP directly with uWSGI -while `socket` is set, you can use the `http` option as shown in the directions below. +By setting the `bind` option to a socket, `run.sh` will no longer automatically serve Galaxy via HTTP (since it is assumed that +you are setting a socket to serve Galaxy via a proxy server). If you wish to continue serving HTTP directly with Gunicorn +while using a socket, you can add an additional `--bind` argument via the `gunicorn_extra_args` option: -**Without a proxy server** or with a proxy server that does not speak the uWSGI native protocol: +```yaml +gravity: + app_server: gunicorn + gunicorn: + # listening options + bind: 'unix:/srv/galaxy/var/gunicorn.sock' + gunicorn_extra_args: '--forwarded-allow-ips="*" --bind 127.0.0.1:8080' +``` -uWSGI can be configured to serve HTTP and/or HTTPS directly: +Note that this should only be used for debugging purposes due to `--forwarded-allow-ips="*"`. + +**Without a proxy server**: + +It is strongly recommended to use a proxy server. + +Gunicorn can be configured to serve HTTPS directly: ```yaml # listening options - http: :8080 - https: :8443,server.crt,server.key - static-map: /static=static + gunicorn: + # listening options + bind: '0.0.0.0:443' + keyfile: server.key + certfile: server.crt ``` +See [Gunicorn's SSL documentation](https://docs.gunicorn.org/en/latest/settings.html#ssl) for more details. + To bind to ports < 1024 (e.g. if you want to bind to the standard HTTP/HTTPS ports 80/443), you must bind as the `root` -user and drop privileges to the Galaxy user with a configuration such as: - -```yaml - # listening options - shared-socket: :80 - shared-socket: :443,server.crt,server.key - http: =0 - https: =1 - uid: galaxy - gid: galaxy - static-map: /static=static -``` - -To redirect HTTP traffic to the HTTPS port rather than serving Galaxy over HTTP, change `http: =0` in the above example -to `http-to-https: =0`. - -Because `run.sh` performs setup steps, **it should not be run as `root`**. Instead, you can run uWSGI directly as root -with: - -```console -# cd /srv/galaxy/server -# ./.venv/bin/uwsgi --yaml config/galaxy.yml -``` - -You can run the startup-time setup steps as the galaxy user after upgrading Galaxy with `sh -./scripts/common_startup.sh`. +user and drop privileges to the Galaxy user. However you are strongly encouraged to setup a proxy server +as described in the [production configuration](production.md) documentation. ### Job Handling @@ -406,11 +287,23 @@ You can run the startup-time setup steps as the galaxy user after upgrading Gala using an ``UPDATE`` query in the database and is only recommended for advanced Galaxy administrators. ``` -#### uWSGI all-in-one job handling +#### all-in-one job handling Ensure that no `` section exists in your `job_conf.xml` (or no `job_conf.xml` exists at all) and start Galaxy normally. No additional configuration is required. To increase the number of web workers/job handlers, increase the -value of `processes`. Jobs will be handled according to rules outlined above in [Job Handler Assignment Methods](#job-handler-assignment-methods). +value of `workers` in the gunicorn section of your galaxy.yml file: + +```yaml +gravity: + ... + gunicorn: + ... + # performance options + workers: 4 + ... +``` + +Jobs will be handled according to rules outlined above in [Job Handler Assignment Methods](#job-handler-assignment-methods). ```eval_rst .. note:: @@ -420,79 +313,6 @@ value of `processes`. Jobs will be handled according to rules outlined above in handling, choose another deployment strategy. ``` -#### uWSGI + Mule job handling - -Ensure that no `` section exists in your `job_conf.xml` (or no `job_conf.xml` exists at all) and add the -following to the `uwsgi` section of `galaxy.yml` to start a single job handler mule: - -```yaml - # job handling options - mule: lib/galaxy/main.py - farm: job-handlers:1 -``` - -Then start Galaxy normally. To add additional mule handlers, add additional `mule` options and add their ID(s), comma -separated, to the `job-handlers` farm. For example, 3 handlers are defined like so: - -```yaml - # job handling options - mule: lib/galaxy/main.py - mule: lib/galaxy/main.py - mule: lib/galaxy/main.py - farm: job-handlers:1,2,3 -``` - -By default, a job will be handled by whatever mule currently has the lock on the mule message queue. After receiving a -message, it will release the lock, giving other mules a chance to handle future jobs. Jobs can be explicitly mapped to -specific mules as described in the [Galaxy Job Configuration](./jobs.md) documentation -by using the handler IDs `main.job-handlers.N`, where `N` is the mule's position in the farm, starting at 1 and incrementing for each mule in the -farm (this is not necessarily the mule ID, but it will be if you only define one farm and you add mules to that farm in -sequential order). Each worker that you wish to explicitly map jobs to should be defined in the `` section -of `job_conf.xml`. *Do not* define a default handler. - -For example, to have the 2nd mule in the three-mule job-handlers farm shown above handle the `test1` tool, you would set -the following in `job_conf.xml` (irrelevant options are not shown): - -```xml - - - - - - - - -``` - -It is also possible to create separate pools for handler tags: the farm name for tags is simply `job-handlers.`. -In the following example, all jobs will be handled by mules 1 and 2, except for executions of tool ``test1``: those jobs -will be handled by mule 3. - -The `uwsgi` section in `galaxy.yml`: - -```yaml - # job handling options - mule: lib/galaxy/main.py - mule: lib/galaxy/main.py - mule: lib/galaxy/main.py - farm: job-handlers:1,2 - farm: job-handlers.special:3 -``` - -In `job_conf.xml`: - -``` - - - - - -``` - -#### uWSGI + Webless job handling - -Beginning with Galaxy release 19.01, webless handlers do not always need to be defined in `job_conf.xml`. - ##### Statically defined handlers In the `` section in `job_conf.xml`, define the webless handlers you plan to start. Tools can be mapped @@ -525,8 +345,7 @@ to specific handlers, or to handler tags, as in the following example: ``handler3`` will *only* handle executions of tool ``test1``. ``` -`run.sh` will start the uWSGI process(es), but you will need to start the webless handler processes yourself. This is -done on the command line like so: +`run.sh` will start the gunicorn and job handler process(es), but if you are not using `run.sh` or the generated supervisor setup you will need to start the webless handler processes yourself. This is done on the command line like so: ```console $ cd /srv/galaxy/server @@ -535,9 +354,6 @@ $ ./scripts/galaxy-main -c config/galaxy.yml --server-name handler1 --daemonize $ ./scripts/galaxy-main -c config/galaxy.yml --server-name handler2 --daemonize ``` -However, a better option to managing processes by hand is to use a process manager as documented in the [Starting and -Stopping](#starting-and-stopping) section. - #### Dynamically defined handlers In order to define handlers dynamically, you must be using one of the new "database locking" handler assignment methods @@ -553,234 +369,151 @@ as explained in [Job Handler Assignment Methods](#job-handler-assignment-methods ``` -As with statically defined handlers, `run.sh` will start the uWSGI process(es), but you will need to start the webless -handler processes yourself. This is done on the command line like so (note the addition of the `--attach-to-pool` -option): +Note that we have defined a `` section without any `` entries, +and we have explicitly configured the assignment method with `assign_with="db-skip-locked"`. + +To let gravity know how many webless handler processes should be started set the number of processes to start in the `gravity:` section of galaxy.yml: + +```yaml +gravity: + handlers: + handler: + processes: 3 + pools: + - job-handler + - workflow-scheduler + special: + pools: + - job-handler.special +``` + +In this example 4 processes will be started in total: +3 processes will act as job handler and workflow scheduler, and one process will be dedicated to handling jobs for the `special` tag only. With the `job_conf.xml` configuration above these would be jobs created by the `test1` tool. +You can omit the `pools` argument, this will then default to: + +```yaml + ... + pools: + - job-handler + - workflow-scheduler + ... +``` + +If you omit the `processes` argument this will default to a single process. +You can further customize the handler names using the `name_template` section, +for a complete example see [this gravity test case](https://github.com/galaxyproject/gravity/blob/a00df1c671fdc01e3fbc42f64a851a45a37a1870/tests/test_process_manager.py#L28). + +If you are not using dynamic handlers please omit the `handlers` entry completely, as these will +otherwise be idle and not handle jobs or workflows. + +As with statically defined handlers, `run.sh` will start the process(es), but if you are not using `run.sh` or the generated supervisor config you will need to start the webless handler processes yourself. This is done on the command line like so (note the addition of the `--attach-to-pool` option): ```console $ cd /srv/galaxy/server -$ ./scripts/galaxy-main -c config/galaxy.yml --server-name handler0 --attach-to-pool job-handlers --daemonize -$ ./scripts/galaxy-main -c config/galaxy.yml --server-name handler1 --attach-to-pool job-handlers.special --daemonize -$ ./scripts/galaxy-main -c config/galaxy.yml --server-name handler2 --attach-to-pool job-handlers --attach-to-pool job-handlers.special --daemonize +./scripts/galaxy-main -c config/galaxy.yml --server-name handler_0 --attach-to-pool job-handlers --attach-to-pool workflow-scheduler --daemonize +./scripts/galaxy-main -c config/galaxy.yml --server-name handler_1 --attach-to-pool job-handlers --attach-to-pool workflow-scheduler --daemonize +./scripts/galaxy-main -c config/galaxy.yml --server-name handler_3 --attach-to-pool job-handlers --attach-to-pool workflow-scheduler --daemonize +./scripts/galaxy-main -c config/galaxy.yml --server-name special_0 --attach-to-pool job-handlers.special --daemonize ``` In this example: -- `handler0` and `handler2` will handle tool executions that are not explicitly mapped to handlers -- `handler1` and `handler2` will handle tool executions that are mapped to the `special` handler tag +* `handler_0`, `handler_1` and `handler2` will handle tool executions that are not explicitly mapped to handlers +* `special_0` will handle tool executions that are mapped to the `special` handler tag -#### uWSGI + Hybrid job handling +## gravity & galaxyctl -Follow the process for both **uWSGI + Mules** and **uWSGI + Webless**: +[Gravity](https://github.com/galaxyproject/gravity) is a management tool for Galaxy servers, +and is installed when you set up Galaxy. -1. Define Mules as in **uWSGI + Mules** -2. Define Webless handlers in `` as in **uWSGI + Webless** (when using static handlers) -3. Configure Webless handlers to start as in **uWSGI + Webless** +It provides two executables, `galaxyctl`, which is used to manage the starting, stopping, and logging of Galaxy's various processes, and `galaxy`, which can be used to run a Galaxy server in the foreground. -### uWSGI Minutiae +These commands are available from within Galaxy's virtualenv, or you can install them globally. +If you have used the standard installation method for Galaxy by running `./run.sh` or executing +`./scripts/common_startup.sh`, a default directory has been configured in which gravity stores +its state. If you have installed Galaxy or gravity by another means, you can use the `--state-dir` argument +or the `GRAVITY_STATE_DIR` environment variable to control the state directory. If a state dir is not specified, it defaults to `~/.config/galaxy-gravity`. +In the following sections we assume you have correctly set up gravity and can use the `galaxyctl` command. -**Threads** +## Logging and daemonization -Although `enable-threads` was explicitly set in our example, in reality, as long as any value is set for the `threads` -option, `enable-threads` is set implicitly. This option enables the Python GIL and application threads (threads started -by Galaxy itself for various non-web tasks), which Galaxy uses extensively. Setting it explicitly, however, is harmless -and can prevent strange difficult-to-debug situations if `threads` is accidentally unset. +When running `./run.sh` the log output is shown on screen, and ^ctrl-c will stop all processes. +Galaxy can be started in the background by running `./run.sh --daemon`. -**Worker/Mule shutdown/reload mercy** - -By default, uWSGI will wait up to 60 seconds for web workers and mules to terminate. This is generally safe for -servicing web requests, but some parts of Galaxy's job preparation/submission and collection/finishing operations can -take quite a bit of time to complete and are not entirely reentrant: job errors or state inconsistencies can occur if -interrupted (although every effort has been made to minimize such possibilities). By default, Galaxy will wait up to 30 -seconds for the threads allocated for these operations to terminate after instructing them to shut down. You can change -this behavior by increasing the value of `monitor_thread_join_timeout` in the `galaxy` section of `galaxy.yml`. The -maximum amount of time that Galaxy will take to shut down job runner workers is `monitor_thread_join_timeout * -runner_plugin_count` since each plugin is shut down sequentially (`runner_plugin_count` is the number of ``s in -your `job_conf.xml`). - -Thus you should set the appropriate uWSGI `*-restart-mercy` option to a value higher than the maximum job runner worker -shutdown time. If using **uWSGI all-in-one**, set `worker-reload-mercy`, and if using **uWSGI + Mule job handling**, set -`mule-reload-mercy` (both in the `uwsgi` section of `galaxy.yml`). - -**Signals** - -The signal handling options (`die-on-term` and `hook-master-start` with `unix_signal` values) are not required but, if -set, will override [uWSGI's unconventional signal handling](https://uwsgi-docs.readthedocs.io/en/latest/Management.html) -and cause `SIGTERM` to kill the server rather than restart it, and the uWSGI master process to gracefully shut down its -web workers and job handler mules (i.e. the various Galaxy application processes) when it receives a `SIGINT` (signal 2) -or `SIGTERM` (signal 15) signal (e.g. from `kill(1)` or `+`). When shutting down gracefully, uWSGI will wait 60 -seconds (by default, but this can be changed with the `reload-mercy`, `worker-reload-mercy` and `mule-reload-mercy` -options) for child processes to die before forcefully killing them with `SIGKILL` (signal 9). Alternatively, you may -prefer to have it shut down gracefully on `SIGTERM` but forcefully on `SIGINT` (forceful shutdown by uWSGI is still -slightly cleaner than `kill -9` of the master process since it can attempt to release sockets cleanly) or vice-versa, -which you can do by setting one of the signals to call `kill_them_all` rather than `gracefully_kill_them_all`: - -```yaml - # fix up signal handling - die-on-term: true - hook-master-start: unix_signal:2 kill_them_all - hook-master-start: unix_signal:15 gracefully_kill_them_all -``` - -More details on the `unix_signal` hook can be found in [uWSGI Issue #849](https://github.com/unbit/uwsgi/issues/849). - -**Logging and daemonization** - -It's possible to configure uWSGI to log to a file with the `logto` or `logto2` options (when running in the foreground, -the default), but more advanced logging options that split log files for each process are possible and described in the -Galaxy [Logging Configuration documentation](config_logging) - -When running as a daemon with `run.sh --daemon`, output is logged to `galaxy.log` and the pid is written to -`galaxy.pid`. These can be controlled with the `daemonize` and `pidfile` arguments (their `daemonize2` and `pidfile2` -counterparts wait until after the application successfully loads to open and write the files). If you set a `daemonize*` -option, you should not use the `--daemon` argument to `run.sh` (or not use `run.sh` at all, and start Galaxy directly -with `uwsgi` on the command line). - -**External uwsgi binary:** - -It is still possible to run Galaxy using an external copy of uWSGI (for example, installed from APT under -Debian/Ubuntu). This was the recommended installation method in the past. To use an external uWSGI, you'll need simply -need to start with uWSGI directly, rather than using the `run.sh` script. Once you have configured Galaxy/uWSGI, you can -start it with: +Alternatively, you can control Galaxy using the `galaxyctl` command provided by gravity. +After activating Galaxy's virtual environment you can start Galaxy in the background +using `galaxyctl start`: ```console -$ cd /srv/galaxy/server -$ uwsgi --yaml config/galaxy.yml +$ galaxyctl start +celery STARTING +celery-beat STARTING +gunicorn STARTING +Log files are in /Users/mvandenb/src/doc_test/database/gravity/log ``` -When installing uWSGI, be sure to install the Python plugin, as this is not always contained in the same package (such -as when installing from APT under Debian/Ubuntu). With the APT packages, you will also need to add `--plugin python` to -the command line (or `plugin: python` to the `uwsgi` section of `galaxy.yml`). +All process logs can be seen with `galaxyctl follow`: -**Other options** +```console +$ galaxyctl follow +==> /Users/mvandenb/src/doc_test/database/gravity/log/gunicorn.log <== +galaxy.webapps.galaxy.buildapp DEBUG 2022-02-17 16:31:33,344 [pN:main,p:91480,tN:MainThread] Prior to webapp return, Galaxy thread is alive. +galaxy.webapps.galaxy.buildapp DEBUG 2022-02-17 16:31:33,345 [pN:main,p:91480,tN:MainThread] Prior to webapp return, Galaxy thread is alive. +galaxy.webapps.galaxy.buildapp DEBUG 2022-02-17 16:31:33,345 [pN:main,p:91480,tN:MainThread] Prior to webapp return, Galaxy thread is alive. +galaxy.webapps.galaxy.buildapp DEBUG 2022-02-17 16:31:33,345 [pN:main,p:91480,tN:MainThread] Prior to webapp return, Galaxy thread is alive. +galaxy.webapps.galaxy.buildapp DEBUG 2022-02-17 16:31:33,345 [pN:main,p:91480,tN:MainThread] Prior to webapp return, Galaxy thread is alive. +galaxy.webapps.galaxy.buildapp DEBUG 2022-02-17 16:31:33,346 [pN:main,p:91480,tN:MainThread] Prior to webapp return, Galaxy thread is alive. +galaxy.webapps.galaxy.buildapp DEBUG 2022-02-17 16:31:33,346 [pN:main,p:91480,tN:MainThread] Prior to webapp return, Galaxy thread is alive. +[2022-02-17 16:31:34 +0100] [91480] [INFO] Started server process [91480] +[2022-02-17 16:31:34 +0100] [91480] [INFO] Waiting for application startup. +[2022-02-17 16:31:34 +0100] [91480] [INFO] Application startup complete. -The `py-call-osafterfork` option may only be needed when mule messaging is in use, but it [seems to have no negative -effect](https://github.com/unbit/uwsgi/issues/643) and may solve other situations with starting a complex threaded -Python application. +==> /Users/mvandenb/src/doc_test/database/gravity/log/celery.log <== +[2022-02-17 16:31:27,008: DEBUG/MainProcess] ^-- substep ok +[2022-02-17 16:31:27,009: DEBUG/MainProcess] | Consumer: Starting Events +[2022-02-17 16:31:27,009: DEBUG/MainProcess] ^-- substep ok +[2022-02-17 16:31:27,009: DEBUG/MainProcess] | Consumer: Starting Tasks +[2022-02-17 16:31:27,035: DEBUG/MainProcess] ^-- substep ok +[2022-02-17 16:31:27,035: DEBUG/MainProcess] | Consumer: Starting Heart +[2022-02-17 16:31:27,035: DEBUG/MainProcess] ^-- substep ok +[2022-02-17 16:31:27,035: DEBUG/MainProcess] | Consumer: Starting event loop +[2022-02-17 16:31:27,035: INFO/MainProcess] celery@MacBook-Pro-2.local ready. +[2022-02-17 16:31:27,035: DEBUG/MainProcess] basic.qos: prefetch_count->8 -The `chdir` option is useful if you are not using `run.sh`, to be able to call `uwsgi` from anywhere without having to -`cd` to the Galaxy directory first. +==> /Users/mvandenb/src/doc_test/database/gravity/log/celery-beat.log <== +[2022-02-17 16:29:04,023: DEBUG/MainProcess] beat: Ticking with max interval->5.00 minutes +[2022-02-17 16:29:04,024: DEBUG/MainProcess] beat: Waking up in 5.00 minutes. +No Galaxy config file found, running from current working directory: /Users/mvandenb/src/doc_test +[2022-02-17 16:31:26,818: DEBUG/MainProcess] Setting default socket timeout to 30 +[2022-02-17 16:31:26,819: INFO/MainProcess] beat: Starting... +[2022-02-17 16:31:26,824: DEBUG/MainProcess] Current schedule: + + +[2022-02-17 16:31:26,824: DEBUG/MainProcess] beat: Ticking with max interval->5.00 minutes +[2022-02-17 16:31:26,825: DEBUG/MainProcess] beat: Waking up in 5.00 minutes. +``` -**Monitoring** - -The [uwsgitop](https://github.com/xrmx/uwsgitop) tool uses uWSGI's stats server (the `stats` option, which is configured -to listen on a socket or port in the same manner as the `socket` option) to report on the health and performance of the -web workers. It can be installed with `pip install uwsgitop`. This tool can be useful for determing whether a worker is -stuck, or seeing the throughput of traffic on your site. +More advanced logging options are described in the Galaxy [Logging Configuration documentation](config_logging). ## Starting and Stopping -If you are using the **uWSGI + Webless job handlers** deployment strategy or want to run your Galaxy server as a -persistent service, you can control it through a process manager. The current recommended process manager is -[Supervisord](http://supervisord.org/). If you are comfortable with systemd and are running a relatively modern Linux -distribution, you can also configure Galaxy as a service directly in systemd. +If you want to run your Galaxy server as a persistent service, you can include the `galaxy` script from Galaxy's +virtualenv in the configuration of your process manager (e.g. systemd). You can then continue using the `galaxyctl` command as usual +to start/stop/restart Galaxy or follow the logs. -### Supervisord +### Transparent restarts -You can use a supervisord config file like the following or be inspired by [this -example](https://github.com/galaxyproject/galaxy/blob/dev/contrib/galaxy_supervisor.conf). Be sure to `supervisord -update` or `supervisord reread && supervisord restart` whenever you make configuration changes. - -```ini -[program:web] -command = /srv/galaxy/venv/bin/uwsgi --yaml /srv/galaxy/config/galaxy.yml -directory = /srv/galaxy/server -umask = 022 -autostart = true -autorestart = true -startsecs = 10 -stopwaitsecs = 65 -user = galaxy -numprocs = 1 -stopsignal = INT -``` - -This configuration defines a "program" named `web` which represents our Galaxy uWSGI frontend. You'll notice that we've -set a command, a directory, a umask, all of which you should be familiar with. Additionally we've specified that the -process should `autostart` on boot, and `autorestart` if it ever crashes. We specify `startsecs` to say "the process -must stay up for this long before we consider it OK. If the process crashes sooner than that (e.g. bad changes you've -made to your local installation) supervisord will try again a couple of times to restart the process before giving up -and marking it as failed. This is one of the many ways supervisord is much friendly for managing these sorts of tasks. - -The value of `stopwaitsecs` should be at least as large as the smallest value of uWSGI's `reload-mercy`, -`worker-reload-mercy`, and `mule-reload-mercy` options, all of which default to `60`. - -If using the **uWSGI + Webless** strategy, you'll need to addtionally define job handlers to start. There's no simple -way to activate a virtualenv when using supervisor, but you can simulate the effects by setting `$PATH` and -`$VIRTUAL_ENV`: - -```ini -[program:handler] -command = /srv/galaxy/venv/bin/python ./scripts/galaxy-main -c /srv/galaxy/config/galaxy.yml --server-name=handler%(process_num)s --pid-file=/srv/galaxy/var/handler%(process_num)s.pid --log-file=/srv/galaxy/log/handler%(process_num)s.log -directory = /srv/galaxy/server -process_name = handler%(process_num)s -numprocs = 3 -umask = 022 -autostart = true -autorestart = true -startsecs = 15 -stopwaitsecs = 35 -user = galaxy -environment = VIRTUAL_ENV="/srv/galaxy/venv",PATH="/srv/galaxy/venv/bin:%(ENV_PATH)s" -``` - -This is similar to the "web" definition above, however, you'll notice that we use `%(process_num)s`. That's a variable -substitution in the `command` and `process_name` fields. We've set `numprocs = 3`, which says to launch three handler -processes. Supervisord will loop over `0..numprocs` and launch `handler0`, `handler1`, and `handler2` processes -automatically for us, templating out the command string so each handler receives a different log file and name. - -The value of `stopwaitsecs` should be at least as large as `monitor_thread_join_timeout * runner_plugin_count`, which -is `30` in the default configuration (`monitor_thread_join_timeout` is a Galaxy configuration option and -`runner_plugin_count` is the number of ``s in your `job_conf.xml`). - -Lastly, collect the tasks defined above into a single group. If you are not using webless handlers this is as simple as: - -```ini -[group:galaxy] -programs = web -``` - -With webless handlers, it is: - -```ini -[group:galaxy] -programs = web, handler -``` - -This will let us manage these tasks more globally with the `supervisorctl` command line tool: - -```console -# supervisorctl status -galaxy:handler0 RUNNING pid 7275, uptime 16:32:17 -galaxy:handler1 RUNNING pid 7276, uptime 16:32:17 -galaxy:handler2 RUNNING pid 7277, uptime 16:32:17 -galaxy:web RUNNING pid 7299, uptime 16:32:16 -``` - -This command shows us the status of our jobs, and we can easily restart all of the processes at once by naming the -group. Familiar commands like start and stop are also available. - -```console -# supervisorctl restart galaxy: -galaxy:handler0: stopped -galaxy:handler1: stopped -galaxy:handler2: stopped -galaxy:web: stopped -galaxy:web: started -galaxy:handler0: started -galaxy:handler1: started -galaxy:handler2: started -``` +For zero-downtime restarts use the command `galaxyctl graceful`. ### Systemd -Sample **uWSGI + Webless** strategy for systemd. More information on systemd.service environment -settings can be found in the [documentation](http://0pointer.de/public/systemd-man/systemd.exec.html#Environment=) - +This is a sample config for systemd. More information on systemd.service environment +settings can be found in the [documentation](https://www.freedesktop.org/software/systemd/man/systemd.exec.html#Environment=) +The filename follows the pattern `.service`. In this case we will use `galaxy.service` ```ini [Unit] -Description=Galaxy web handler +Description=Galaxy processes After=network.target After=time-sync.target @@ -792,50 +525,15 @@ Group=galaxy Restart=on-abort WorkingDirectory=/srv/galaxy/server TimeoutStartSec=10 -ExecStart=/srv/galaxy/venv/bin/uwsgi --yaml /srv/galaxy/config/galaxy.yml +ExecStart=/srv/galaxy/venv/bin/galaxy --state-dir /srv/galaxy/database/gravity Environment=VIRTUAL_ENV=/srv/galaxy/venv PATH=/srv/galaxy/venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin [Install] WantedBy=multi-user.target ``` -For multiple handlers, the service file needs to be a [template unit](https://www.freedesktop.org/software/systemd/man/systemd.unit.html#Description>) - the filename follows the syntax -`@.service` and all instances of `%I` in the service file are replaced with the `` +We can now enable and start the the Galaxy services with systemd: -```ini -[Unit] -Description=Galaxy job handlers -After=network.target -After=time-sync.target - -[Service] -PermissionsStartOnly=true -Type=simple -User=galaxy -Group=galaxy -Restart=on-abort -WorkingDirectory=/srv/galaxy/server -TimeoutStartSec=10 -ExecStart=/srv/galaxy/venv/bin/python ./scripts/galaxy-main -c /srv/galaxy/config/galaxy.yml --server-name=handler%I --log-file=/srv/galaxy/log/handler%I.log -#Environment=VIRTUAL_ENV=/srv/galaxy/venv PATH=/srv/galaxy/venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin -[Install] -WantedBy=multi-user.target -``` - -We can now enable and start the web services with systemd ```console -# systemctl enable galaxy-web -Created symlink from /etc/systemd/system/multi-user.target.wants/galaxy-web.service to /etc/systemd/system/galaxy-web.service. -# systemctl enable galaxy-handler@{0..3} -Created symlink from /etc/systemd/system/multi-user.target.wants/galaxy-handler@0.service to /etc/systemd/system/galaxy-handler@.service. -Created symlink from /etc/systemd/system/multi-user.target.wants/galaxy-handler@1.service to /etc/systemd/system/galaxy-handler@.service. -Created symlink from /etc/systemd/system/multi-user.target.wants/galaxy-handler@2.service to /etc/systemd/system/galaxy-handler@.service. -Created symlink from /etc/systemd/system/multi-user.target.wants/galaxy-handler@3.service to /etc/systemd/system/galaxy-handler@.service. -# systemctl start galaxy-handler@{0..3} -# systemctl start galaxy-web +# systemctl enable galaxy +Created symlink from /etc/systemd/system/multi-user.target.wants/galaxy.service to /etc/systemd/system/galaxy.service. ``` - -### Transparent Restart - Zerg Mode - -The standard uWSGI operation mode allows you to restart the Galaxy application while blocking client connections. Zerg Mode does away with the waiting by running a special Zerg Pool process, and connecting Zergling workers (aka Galaxy application processes) to the pool. As long as at least one is connected, requests can be served. - -See the [GCC2017 Admin Training session](https://github.com/galaxyproject/dagobah-training/blob/2017-montpellier/sessions/10-uwsgi/ex2-zerg-mode.md) on how to set this up. diff --git a/doc/source/admin/special_topics/index.rst b/doc/source/admin/special_topics/index.rst index 2f904d28fa5..834a686d052 100644 --- a/doc/source/admin/special_topics/index.rst +++ b/doc/source/admin/special_topics/index.rst @@ -7,7 +7,6 @@ Special Topics ftp interactivetools - interactive_environments mulled_containers grt gtn diff --git a/doc/source/admin/special_topics/interactive_environments.png b/doc/source/admin/special_topics/interactive_environments.png deleted file mode 100644 index 49a31646fd8..00000000000 Binary files a/doc/source/admin/special_topics/interactive_environments.png and /dev/null differ diff --git a/doc/source/admin/special_topics/interactive_environments.rst b/doc/source/admin/special_topics/interactive_environments.rst deleted file mode 100644 index c8d87b9d615..00000000000 --- a/doc/source/admin/special_topics/interactive_environments.rst +++ /dev/null @@ -1,300 +0,0 @@ -Galaxy Interactive Environments -=============================== - -A GIE is a Docker container, launched by Galaxy, proxied by Galaxy, with some -extra sugar inside the container to allow users to interact easily with their -Galaxy histories. - -How GIEs Work -------------- - -A GIE is primarily composed of a Docker container, and the Galaxy visualization -component. Galaxy visualization plugins are rendered using Mako templates and -Mako templates in turn can run Python code. GIEs build upon visualization plugins, -adding features to allow for container management and proxying. This Python code -in the Mako templates is used to launch the Docker container within which a GIE -runs. Once this container is launched, we notify a proxy built into Galaxy which -helps coordinate a 1:1 mapping of users and their docker containers. - -Here's a simple diagram recapping the above: - -.. image:: interactive_environments.png - -Deploying GIEs --------------- - -Deploying GIEs is not a trivial operation. They have complex interactions with -numerous services, you'll need to be a fairly competent SysAdmin to debug all -of the possible problems that can occur during deployment. After the initial -hurdle, most find that GIEs require little to no maintenance. - -An `Ansible `__ role for installing and managing GIEs -can be found on -`Github `__ -and `Ansible Galaxy `__. - -Setting up the Proxy -^^^^^^^^^^^^^^^^^^^^ - -The Galaxy IE Proxy is a NodeJS+Sqlite3 application. The NodeJS that is -installed by default into the Galaxy Virtualenv is suitable for an execution -environment for the Galaxy IE Proxy. - -- Note that if you have NodeJS installed under Ubuntu, it often installs to - ``/usr/bin/nodejs``, whereas ``npm`` expects it to be ``/usr/bin/node``. You - may need to create that symlink yourself. - -Once Node and npm are ready to go, you'll need to install the dependencies - -.. code-block:: console - - $ cd $GALAXY_ROOT/lib/galaxy/web/proxy/js - $ npm install - -Running ``node lib/main.js --help`` should produce some useful help text - -.. code-block:: console - - Usage: main [options] - - Options: - - -h, --help output usage information - -V, --version output the version number - --ip Public-facing IP of the proxy - --port Public-facing port of the proxy - --cookie Cookie proving authentication - --sessions Routes file to monitor - --verbose - -There are two ways to handle actually running the proxy. The first is to have -Galaxy automatically launches the proxy as needed. This is the default configuration -as of 2014. Alternately, the proxy can be stated manually or via a system such as -Supervisord. Assuming that the ``$GALAXY_ROOT`` environment variable refers to the location of -the Galaxy installation, the command for launching the proxy is: - -.. code-block:: console - - $ node $GALAXY_ROOT/lib/galaxy/web/proxy/js/lib/main.js --ip 0.0.0.0 \ - --port 8800 --sessions $GALAXY_ROOT/database/session_map.sqlite \ - --cookie galaxysession --verbose - -And this can be configured in your supervisord config by adding: - -.. code-block:: console - - - [program:galaxy_nodejs_proxy] - directory = GALAXY_ROOT - command = GALAXY_ROOT/lib/galaxy/web/proxy/js/lib/main.js --sessions database/session_map.sqlite --ip 0.0.0.0 --port 8800 - autostart = true - autorestart = unexpected - user = GALAXY_USER - startsecs = 5 - redirect_stderr = true - -where ``GALAXY_ROOT`` is the location of your Galaxy installation and ``GALAXY_USER`` is the username of the user that -Galaxy runs as. - -Configuring the Proxy -^^^^^^^^^^^^^^^^^^^^^ - -Configuration is all managed in ``galaxy.yml``. The default arguments used -for the proxy are: - -.. code-block:: yaml - - dynamic_proxy_manage: true - dynamic_proxy_session_map: database/session_map.sqlite - dynamic_proxy_bind_port: 8800 - dynamic_proxy_bind_ip: 0.0.0.0 - dynamic_proxy_debug: true - -As you can see most of these variables map directly to the command line -arguments to the NodeJS script. There are a few extra parameters which will -be needed if you run Galaxy behind an upstream proxy like nginx or -Apache: - -.. code-block:: yaml - - dynamic_proxy_external_proxy: true - dynamic_proxy_prefix: gie_proxy - -The first option says that you have Galaxy and the Galaxy NodeJS proxy wrapped -in an upstream proxy like Apache or NGINX. This will cause Galaxy to connect -users to the same port as Galaxy is being served on (so 80/443), rather than -directing them to port 8800. - -The second option is closely entertwined with the first option. When Galaxy is -accessed, it sets a cookie called ``galaxysession``. This cookie generally cannot be sent with requests -to different domains and different ports, so Galaxy and the dynamic proxy must -be accessible on the same port and protocol. In addition, the cookie is only -accessible to URLs that share the same prefix as the Galaxy URL. For example, -if you're running Galaxy under a URL like ``https://f.q.d.n/galaxy/``, the cookie -is only accessible to URLs that look like ``https://f.q.d.n/galaxy/*``. The -second (``dynamic_proxy_prefix``) option sets the URL path that's used to -differentiate requests that should go through the proxy to those that should go -to Galaxy. You will need to add special upstream proxy configuration to handle -this, and you'll need to use the same ``dynamic_proxy_prefix`` in your -``galaxy.yml`` that you use in your URL routes. - -In the examples below, we assume that your Galaxy installation is available -at a URL such as ``https://f.q.d.n/galaxy``. If instead it is available at a -URL like ``https://f.q.d.n``, remove the ``/galaxy`` prefix from the examples. -For example ``/galaxy/gie_proxy`` would become ``/gie_proxy``. Remember that -``gie_proxy`` is the value you use for the ``dynamic_proxy_prefix`` option. If -you use a different value in that option you should change the examples -accordingly. - -**Apache** - -.. code-block:: apache - - # Project Jupyter specific. Other IEs may require their own routes. - ProxyPass /galaxy/gie_proxy/jupyter/ipython/api/kernels ws://localhost:8800/galaxy/gie_proxy/jupyter/ipython/api/kernels - - # Global GIE configuration - ProxyPass /galaxy/gie_proxy http://localhost:8800/galaxy/gie_proxy - ProxyPassReverse /galaxy/gie_proxy http://localhost:8800/galaxy/gie_proxy - - # Normal Galaxy configuration - ProxyPass /galaxy http://localhost:8000/galaxy - ProxyPassReverse /galaxy http://localhost:8000/galaxy - -Please note you will need to be using apache2.4 with ``mod_proxy_wstunnel``. - -**Nginx** - -.. code-block:: nginx - - # Global GIE configuration - location /galaxy/gie_proxy { - proxy_pass http://localhost:8800/galaxy/gie_proxy; - proxy_redirect off; - } - - # Project Jupyter specific. Other IEs may require their own routes. - location ~ ^/galaxy/gie_proxy/jupyter/(?[^/]+)/api/kernels(?.*?)$ { - proxy_pass http://localhost:8800/galaxy/gie_proxy/jupyter/$nbtype/api/kernels$rest; - proxy_redirect off; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - } - -If you proxy static content, you may find the following rule useful for -proxying to GIE and other visualization plugin static content. - -.. code-block:: nginx - - location ~ ^/static/plugins/(?.+?)/(?.+?)/static/(?.*?)$ { - alias /path/to/galaxy-dist/config/plugins/$plug_type/$vis_name/static/$static_file; - } - -Docker on Another Host -^^^^^^^^^^^^^^^^^^^^^^ - -You might want to run your IEs on a host different to the one that hosts your -Galaxy webserver, since IEs on the same host as the webserver compete for -resources with that webserver and introduce some security considerations which -could be mitigated by moving containers to a separate host. This feature has -been available since 15.07 and is used in production at the University of -Freiburg and on usegalaxy.org. - -First you need to configure a second host to be Docker enabled. In the -following we call this host ``gx-docker`` You need to start the Docker daemon -and bind it to a TCP port, not to a Unix socket as is the default. For example -you can start the daemon with - -.. code-block:: console - - $ docker -H 0.0.0.0:4243 -d - -On your client, the Galaxy webserver, you can now install a Docker client. This -can also be done on older systems like Scientific-Linux, CentOS 6, which do not -have Docker support by default. The client just talks to the Docker daemon on -host ``gx-docker``, and does not run anything itself, locally. You can test -your configuration for example by starting busybox from your client on the -Docker host with - -.. code-block:: console - - $ docker -H tcp://gx-docker:4243 run -it busybox sh - -So far so good! Note, however, that unless restricted by a firewall, this mode -of operation is insecure, as any client could connect and run containers on -``gx-docker``. If this is a concern at your site, follow the instructions in -the Docker documentation to `Protect the Docker daemon socket -`__. - -Now we need to configure Galaxy to use our new Docker host -to start the Interactive Environments. For that we need to edit the Jupyter GIE -configuration, ``jupyter.ini`` to use our custom docker host - -.. code-block:: ini - - [main] - - [docker] - command = docker -H tcp://gx-docker:4243 {docker_args} - docker_hostname = gx-docker - -Please adapt your ``command`` as needed. - -The Jupyter GIE supports getting and fetching Galaxy history datasets entirely -through the Galaxy API so it is not necessary to share a filesystem with -``gx-docker``. However, other GIE plugins may still require this. - -For those GIE plugins, we need to configure a share mount point between the -Docker host and Galaxy. Unfortunately, this can not be a NFS mount. Docker does -not like NFS yet. You could for example use a sshfs mount with the following -script - -.. code-block:: bash - - if mount | grep ^gx-docker:/var/tmp/gx-docker; then - echo "/var/tmp/gx-docker already mounted." - else - sshfs gx-docker:/var/tmp/gx-docker /var/tmp/gx-docker - echo 'Mounting ...' - fi - -This will let Galaxy and the Docker host share temporary files. - -Docker Engine Swarm Mode -^^^^^^^^^^^^^^^^^^^^^^^^ - -As of Docker Engine version 1.12, Docker Engine can be configured to provide a -cluster of Docker Engines in a configuration known as *Docker Engine swarm -mode*. This replaces the previous and similarly named *Docker Swarm* -clustering solution, which is not compatible with swarm mode. - -`The Docker Engine swarm mode documentation -`__ fully explains the differences, but -the major difference is that whereas under Docker Swarm one could run commands -on the swarm with ``docker run``, Docker Engine swarm mode requires one to -create persistent services with ``docker service create`` and to remove those -services once no longer in use with ``docker service rm``. - -Galaxy supports both Docker Engine swarm mode and the legacy Docker Swarm -system. Legacy Docker Swarm is supported without any special configuration, -because the containers are still run with ``docker run`` as before. To support -Docker Engine swarm mode, additional configuration is required. Begin by -editing your GIE plugin's ini configuration file (e.g. ``jupyter.ini``) and set -the ``docker_connect_port`` in addition to any other -relevant options. Unless you are using a non-standard Docker image, the correct -value for ``docker_connect_port`` should be suggested to you in the sample -configuration file: - -.. code-block:: ini - - [docker] - docker_connect_port = 8888 - -Note that your Galaxy server does not need to be a member of the swarm itself. -It can use the method outlined above in the `Docker on Another Host`_ section -to connect as a client to a Docker daemon acting as a swarm mode manager. - -Once configured, you should see that your GIE containers are started and run as -services, which you can inspect using the ``docker service ls`` command and -other ``docker service`` subcommands. diff --git a/doc/source/admin/special_topics/interactive_environments.rst b/doc/source/admin/special_topics/interactive_environments.rst new file mode 120000 index 00000000000..454bb916148 --- /dev/null +++ b/doc/source/admin/special_topics/interactive_environments.rst @@ -0,0 +1 @@ +../../dev/interactive_environments.rst \ No newline at end of file diff --git a/doc/source/admin/special_topics/interactive_environments.svg b/doc/source/admin/special_topics/interactive_environments.svg deleted file mode 100644 index 55c81e4c55f..00000000000 --- a/doc/source/admin/special_topics/interactive_environments.svg +++ /dev/null @@ -1,2536 +0,0 @@ - - - - - Galaxy Jupyter Graphic - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - Galaxy Jupyter Graphic - - - Björn Grüning, Torsten Houwaart, Helena Rasche - - - en_US - - - - - - - - - - - - - - Launch Jupyternotebook - - - IE Entry Point - - Configure Docker ContainerRun Docker Container - authenticate with Passwordload default notebook - call - - - - - - - - - - jupyter_galaxy_notebook.ipynb - - - config.yaml - - History_ID- API_Key- Password - - - Jupyter + scipy stack installed - - Docker Container - - Jupyter Webservice - While connection active - - - - - - - Galaxy - - - - - - - - diff --git a/doc/source/admin/special_topics/interactivetools.rst b/doc/source/admin/special_topics/interactivetools.rst index 3e1e70e93a4..1bea5ac37ca 100644 --- a/doc/source/admin/special_topics/interactivetools.rst +++ b/doc/source/admin/special_topics/interactivetools.rst @@ -1,3 +1,5 @@ +.. _interactivetools: + Galaxy InteractiveTools ======================= @@ -42,52 +44,74 @@ Some important benefits of using Galaxy InteractiveTools Server-side configuration of Galaxy InteractiveTools ---------------------------------------------------- +For production deployments and additional considerations please see the `Galaxy Interactive Tools Tutorial `__. + The ``galaxy.yml`` file will need to be populated as seen in ``config/galaxy.yml.interactivetools``. -Galaxy InteractiveTool routing relies on wildcard subdomain routes. For users -who manage their own DNS, you can set the appropriate A records to redirect -``*.interactivetool.yourdomain``, following format seen below. +Galaxy InteractiveTool routing relies on wildcard subdomain routes and a proxy server that forwards requests to a running container. +For users who manage their own DNS, you can set the appropriate A records to redirect +``*.interactivetool.yourdomain``. -It's not recommended for production, but for a quick local deployment -``localhost.blankenberglab.org`` is a domain record provided by Dan Blankenberg -(a Galaxy contributor and the architect of ITs) configured with the appropriate -wildcard redirect to 127.0.0.1, which you can use that in place of -```` to resolve to your local machine. +`gravity` will automatically start the needed proxy server. -In the ``uwsgi:`` section: +The following configuration is only recommended for local testing, as users will directly connect to the InteractiveTool Proxy. +In a production setup an upstream proxy should route requests to the proxy via the ``*.interactivetool.yourdomain`` subdomain. + +Set these values in `galaxy.yml`: .. code-block:: yaml - http-raw-body: true - # master: true - - interactivetools_map: database/interactivetools_map.sqlite - python-raw: scripts/interactivetools/key_type_token_mapping.py - route-host: ^([A-Za-z0-9]+(?:-[A-Za-z0-9]+)*)-([A-Za-z0-9]+(?:-[A-Za-z0-9]+)*)\.([A-Za-z0-9]+(?:-[A-Za-z0-9]+)*)\.(interactivetool\.:8080)$ goto:interactivetool - route-run: goto:endendend - route-label: interactivetool - route-host: ^([A-Za-z0-9]+(?:-[A-Za-z0-9]+)*)-([A-Za-z0-9]+(?:-[A-Za-z0-9]+)*)\.([A-Za-z0-9]+(?:-[A-Za-z0-9]+)*)\.(interactivetool\.:8080)$ rpcvar:TARGET_HOST rtt_key_type_token_mapper_cached $1 $3 $2 $4 $0 5 - route-if-not: empty:${TARGET_HOST} httpdumb:${TARGET_HOST} - route: .* break:404 Not Found - route-label: endendend + gravity: + # ... + gx_it_proxy: + enable: true + proxy_port: 4002 + galaxy: + # ... + interactivetools_enable: true + interactivetools_map: database/interactivetools_map.sqlite + galaxy_infrastructure_url: http://localhost:8080 + # Do not set the following 2 options if you are using an upstream proxy server like nginx + interactivetools_upstream_proxy: false + interactivetools_proxy_host: localhost:4002 + # ... -In the ``galaxy:`` section: +If you do want to use nginx as an upstream proxy server you can use the following server section to route requests to the InteractiveTool proxy: -.. code-block:: yaml +.. code-block:: nginx - interactivetools_enable: true - interactivetools_map: database/interactivetools_map.sqlite - galaxy_infrastructure_url: http://.org:8080 + server { + # Listen on port 443 + listen *:443 ssl; + # Match all requests for the interactive tools subdomain + server_name *.interactivetool.localhost; + + # Proxy all requests to the GIE Proxy application + location / { + proxy_redirect off; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_pass http://localhost:4002; + } + } -The admin should modify the ``route-host`` and ``interactivetools_prefix`` to match their preferred configuration. +Note that this nginx example uses https, so you need to have a wildcard certificate for your domain, +and you need to adjust ``galaxy_infrastructure_url`` as appropriate. You will most likely also want +to replace localhost with your server domain. +You will also need to enable a docker destination in the job_conf.xml file. An example ``job_conf.xml`` file as seen in ``config/job_conf.xml.interactivetools``: .. code-block:: xml + + @@ -96,11 +120,23 @@ An example ``job_conf.xml`` file as seen in ``config/job_conf.xml.interactivetoo true + $galaxy_root:ro,$tool_directory:ro,$job_directory:rw,$working_directory:rw,$default_file_path:ro false bridge true true + true + + + --add-host localhost:host-gateway docker_dispatch @@ -108,10 +144,11 @@ An example ``job_conf.xml`` file as seen in ``config/job_conf.xml.interactivetoo local - + -Alternatively to the local job runner, InteractiveTools have been enabled for the condor job runner, e.g.: +InteractiveTools have been enabled for the Condor, Slurm, Pulsar and Kuberneters job runner. +A destination configuration for Condor may look like this: .. code-block:: xml diff --git a/doc/source/admin/special_topics/performance_tracking.rst b/doc/source/admin/special_topics/performance_tracking.rst index e500cca698b..e6a420a7fae 100644 --- a/doc/source/admin/special_topics/performance_tracking.rst +++ b/doc/source/admin/special_topics/performance_tracking.rst @@ -2,44 +2,22 @@ Galaxy Performance Tracking =========================== Tracking performance over time and identifying slow queries in your Galaxy can be an extremely important thing to do, especially for larger Galaxies. +[A more in-depth tutorial is available at the Galaxy Training Network](https://training.galaxyproject.org/training-material/topics/admin/tutorials/monitoring/tutorial.html). -Most performance tracking requires sending metrics to a metrics collection server such as `Graphite `__ or `StatsD `__. This document assumes you have already deployed a metrics server. +Most performance tracking requires sending metrics to a metrics collection server such as `StatsD `__. This document assumes you have already deployed StatsD. -uWSGI +Gunicorn ----- -There is some built-in uWSGI support for performance logging. You can send uWSGI's internal metrics to a carbon (Graphite) server by setting the carbon option in your galaxy.yml: +There is some built-in `Gunicorn `__ support for performance logging. You can send Gunicorn's internal metrics to a StatsD server by setting the `--statsd-host` and `--statsd-prefix` command line options for Gunicorn in the `gravity` section of `galaxy.yml`: .. code-block:: yaml - uwsgi: - socket: ... - carbon: 127.0.0.1:2003 - -Or a StatsD server via: - -.. code-block:: yaml - - uwsgi: - socket: ... - stats-push: statsd:127.0.0.1:8125 - -The `official documentation `__ contains further information on uWSGI and stats servers. In the `uWSGI Stats Server `__ documentation, you can see an example of the sort of information that you will be able to collect. Note that you will need to make sure that the statsd pusher plugin is activated in your uWSGI servers. - -Alternatively, you can use `gxadmin `__ to generate data ready to load in an InfluxDB database. In this case, you will need to add the stats option to your galaxy.yml: - -.. code-block:: yaml - - uwsgi: - socket: ... - stats: 127.0.0.1:9191 - -And then run gxadmin like this: - - -.. code-block:: bash - - gxadmin uwsgi stats_influx 127.0.0.1:9191 + gravity: + ... + gunicorn: + gunicorn_extra_args: `--statsd-host 127.0.0.1:8125 --statsd-prefix=gunicorn` + ... API / Route Timing Statistics ----------------------------- diff --git a/doc/source/admin/special_topics/vault.md b/doc/source/admin/special_topics/vault.md index 6dee3563ec5..2c104eec37b 100644 --- a/doc/source/admin/special_topics/vault.md +++ b/doc/source/admin/special_topics/vault.md @@ -85,6 +85,7 @@ Fernet key, use the following Python code: ```python from cryptography.fernet import Fernet Fernet.generate_key().decode('utf-8') +``` If multiple encryption keys are defined, only the first key is used to encrypt secrets. The remaining keys are tried in turn during decryption. This is useful for key rotation. We recommend periodically generating a new fernet key and rotating old keys. However, before removing an old key, make sure that any data encrypted using that old key is no longer diff --git a/doc/source/dev/index.rst b/doc/source/dev/index.rst index 5741b5488e6..afe39081eee 100644 --- a/doc/source/dev/index.rst +++ b/doc/source/dev/index.rst @@ -23,6 +23,5 @@ A multi-hour long video playlist covering these slides can be found at debugging_tests debugging_galaxy debugging_galaxy_slurm - starting_galaxy file_upload translating diff --git a/doc/source/dev/interactive_environments.rst b/doc/source/dev/interactive_environments.rst index 99d8f190f60..e7eb5462657 100644 --- a/doc/source/dev/interactive_environments.rst +++ b/doc/source/dev/interactive_environments.rst @@ -1,548 +1,5 @@ -Build an Interactive Environment -================================ +Interactive Environments +========================= -Unfortunately building a Galaxy Interactive Environment (GIE) isn't completely straightforward, and it's -certainly not as simple as picking out an existing container and plugging it -in. Here we'll go through build a "Hello, World" GIE which just displays a file -from a user's history. - -Directory Layout ----------------- - -The GIE directory layout looks identical to that of normal visualization -plugins, for those familiar with developing those - -.. code-block:: console - - - $ tree config/plugins/interactive_environments/jupyter/ - config/plugins/interactive_environments/jupyter/ - ├── config - │ ├── allowed_images.yml.sample - │ ├── jupyter.ini.sample - │ └── jupyter.xml - ├── static - │ └── js - │ └── jupyter.js - └── templates - └── jupyter.mako - -We'll use the variable ``{gie}`` to stand in for the name of your GIE. It -should match ``[a-z]+``, like ``jupyter`` or ``rstudio``. Here you can see the -``config/`` directory with a ``{gie}.ini.sample`` providing docker and image -configuration, ``allowed_images.yml.sample`` specifies acceptable images, -and then ``{gie}.xml`` which outlines that it is a GIE. - -The static directory can hold resources such as JavaScript and CSS files. If -you are actively developing a GIE, you'll need to restart Galaxy after adding -any resources to that file, before they can be accessed in the browser. - -Lastly, and most importantly, there's the templates folder. This normally just -contains ``{gie}.mako``, however the Jupyter file needs an extra template file. - -First Steps, Configuration --------------------------- - -We will name our GIE "helloworld", but you are free to name your's differently. -We'll first need to create the directory structure and set up our -configuration - -.. code-block:: console - - $ mkdir -p $GALAXY_ROOT/config/plugins/interactive_environments/helloworld/{config,static,templates} - $ cd $GALAXY_ROOT/config/plugins/interactive_environments/helloworld/ - -Next, you'll need to create the GIE plugin XML file ``config/helloworld.xml`` - -.. code-block:: xml - - - - - - - - HistoryDatasetAssociation - - - tabular.Tabular - data.Text - dataset_id - - - - dataset_id - - - helloworld.mako - - -Once this is done, we can set up our INI file, ``config/helloworld.ini.sample`` which controls docker interaction - -.. code-block:: ini - - [main] - # Unused - - [docker] - # Command to execute docker. For example `sudo docker` or `docker-lxc`. - #command = docker {docker_args} - - # The image argument was moved to "allowed_images.yml.sample" - - # Additional arguments that are passed to the `docker run` command. - #command_inject = --sig-proxy=true -e DEBUG=false - - # URL to access the Galaxy API with from the spawn Docker container, if empty - # this falls back to galaxy.ini's galaxy_infrastructure_url and finally to the - # Docker host of the spawned container if that is also not set. - #galaxy_url = - - # The Docker hostname. It can be useful to run the Docker daemon on a different - # host than Galaxy. - #docker_hostname = localhost - - # Try to set the temp directory to world execute - this can fix the issue - # where 'sudo docker' is not able to mount the folder otherwise. - # "finalize namespace chdir to /import permission denied" - #wx_tempdir = False - - # Overwrite the IE temp directory. This can be useful if you regular tempdir is - # located on an NFS share, which does not work well as Docker volume. In this case - # you can have a shared sshfs share which you can use as temporary directory to - # share data between the IE and Galaxy. - #docker_galaxy_temp_dir = None - - # If your Docker container exposes more then one port, Galaxy needs to know to - # which ports it needs to connect. With this option you can specify the port number - # inside your container to which Galaxy should connect the UI. - #docker_connect_port = None - - # Set the following value to false if Docker volumes between Galaxy server and Docker - # container cannot or should not be used. - #use_volumes = True - -You'll then need to create the GIE plugin YML file ``allowed_images.yml.sample`` -to specify allowed images -.. code-block:: - - # This file lists acceptable images to allow running. - # - # This allows you, the admin, to create multiple flavours - # for your users to run. E.g. maybe you need a geosciences flavour, - # you can create the image based on our default image and add the - # appropriate `apt-get/pip install` statements. - --- - - - image: hello-ie - description: | - hello-ie is for getting you up to speed with - IE development. - -We've named our image ``hello-ie``, we'll get to creating that in a minute. - -Mako Templates --------------- - -Mako templates are very easy to use, and they allow significantly more -flexibility than many other templating languages. It's because of this -flexibility (and ability to write plain python code in them) that GIEs were -possible to develop easily. - -In our ``templates/helloworld.mako``, we'll add the following - -.. code-block:: html+mako - - <%namespace name="ie" file="ie.mako" /> - -This line says to inherit from the ``ie.mako`` file that's available in -``$GALAXY_ROOT/config/plugins/interactive_environments/common/templates/ie.mako``. -Next we'll add the following - -.. code-block:: html+mako - - <% - # Sets ID and sets up a lot of other variables - ie_request.load_deploy_config() - - # Define a volume that will be mounted into the container. - # This is a useful way to provide access to large files in the container, - # if the user knows ahead of time that they will need it. - user_file = ie_request.volume( - hda.file_name, '/import/file.dat', mode='ro') - - # Launch the IE. This builds and runs the docker command in the background. - ie_request.launch( - volumes=[user_file], - env_override={ - 'custom': '42' - } - ) - - # Only once the container is launched can we template our URLs. The ie_request - # doesn't have all of the information needed until the container is running. - url = ie_request.url_template('${PROXY_URL}/helloworld/') - %> - -That mako snippet loaded the configuration from the INI files, launched the -docker container, and then built a URL to the correct endpoint, through the -Galaxy NodeJS proxy. Additionally we've set an environment variable named ``CUSTOM`` -with the value ``42`` to be passed to the container, and we've attached the dataset the -user selected (available in ``hda``) to the container as a read-only volume. - -We'll continue appending to our ``helloworld.mako`` the HTML code that's actually displayed to the user, when this template is rendered - -.. code-block:: html+mako - - - - - ${ ie.load_default_js() } - - ${ ie.load_default_app() } - - - - -
    -
    - - - -We've glossed over some of the features of this file, but most IEs do a significant amount -of "magic" in the top half of the mako template. For instance, the Jupyter notebook: - -- If the user is trying to run the Jupyter GIE Visualization on an existing notebook in their history, then that gets loaded into the docker container via the temp directory and set as the default notebook -- Otherwise a default notebook is built for the user. - -The RStudio notebook: - -- generates a random password and configures the image to use this password -- Copies in an RData file if the user has loaded one -- sets some custom environment variables. - - -Connecting the User to the Container via Javascript ---------------------------------------------------- - -With the mako template above finished, if you were to load this in your -browser, not a lot would happen because we haven't built the hello-ie image, -and we haven't used Javascript to connect the user with the container. In the -tail end of the template, we set a variable ``notebook_access_url``. These are -partially a legacy of how things used to be done and you're welcome to clean up -your code according to your desires. Galaxy's NodeJS proxy handles the -authentication of users, so you don't have to worry about it, and can just -assume that only the correct user will have access to a given notebook. - -In the ``static/`` directory, we generally create a ``js/`` directory below that, -and create a ``main.js`` file in there. -That file will have a function, ``load_notebook`` which will check if the GIE is available, -and when it is, display it to the user. - -We start by writing the load notebook function, which is pretty generic - -.. code-block:: javascript - - // Globals - var IES = window.IES; - // Load an interactive environment (IE) from a remote URL - // @param {String} notebook_access_url: the URL embeded in the page and loaded - function load_notebook(notebook_access_url){ - // When the page has completely loaded... - // Test if we can access the GIE, and if so, execute the function - // to load the GIE for the user. - IES.test_ie_availability(notebook_access_url, function(){ - IES.append_notebook(notebook_access_url); - }); - } - -This function will display a spinner to the user to indicate process, make multiple requests -to ``notebook_access_url`` and display the GIE to the user in an iframe. That MUST return a 200 OK for the ``append_notebook`` function to ever be called. 302s do not count! - -Historically, the GIE process involved a complex dance of: - -- generating a random password in the mako template -- setting it as a javascript variable -- passing it to the docker container -- once the container was available, have the javascript automatically log a - user in (something browsers try to prevent since that's otherwise an XSS - vulnerability.) -- hope everything worked - -You may wish to look at the Jupyter and RStudio GIEs for examples of the complex things that can be done at every step. - -The GIE Container ------------------ - -We'll build a simple container that just displays the dataset a user has -selected to them. Remember when we attached a volume to the container? We'll -make use of that now. - -GIE Containers (often) consist of: - -- Dockerfile -- NGINX Proxy configuration -- A custom startup script/entrypoint -- A script to monitor traffic and kill unused containers - -We have to monitor the container's traffic and kill off unused containers, -because no one is watching them. The user launches the container in Galaxy, and -Galaxy immediately forgets the container exists. Thus, we say that if a -container has no connections to TCP connections to itself, then it should -commit suicide by killing the root process. - -Here's an example ``Dockerfile`` for our helloworld container - -.. code-block:: dockerfile - - FROM alpine - # These environment variables are passed from Galaxy to the container - # and help you enable connectivity to Galaxy from within the container. - # This means your user can import/export data from/to Galaxy. - ENV DEBIAN_FRONTEND=noninteractive \ - API_KEY=none \ - DEBUG=false \ - PROXY_PREFIX=none \ - GALAXY_URL=none \ - GALAXY_WEB_PORT=10000 \ - HISTORY_ID=none \ - REMOTE_HOST=none \ - DOCKER_PORT=none \ - CORS_ORIGIN-none - - RUN apk update && \ - apk add \ - wget procps nginx python py2-pip net-tools nginx git patch - - # Our very important scripts. Make sure you've run `chmod +x startup.sh - # monitor_traffic.sh` outside of the container! - ADD ./startup.sh /startup.sh - ADD ./monitor_traffic.sh /monitor_traffic.sh - - # /import will be the universal mount-point - # The Galaxy instance can copy in data that needs to be present to the - # container - RUN mkdir -p /import /web/helloworld /run/nginx - - # Nginx configuration - COPY ./proxy.conf /proxy.conf - COPY ./index.html /web/helloworld/ - RUN chmod ugo+r /web/helloworld/index.html - - VOLUME ["/import"] - WORKDIR /import/ - - # EXTREMELY IMPORTANT! You must expose a SINGLE port on your container. - EXPOSE 80 - CMD /startup.sh - -If you have questions on this, please feel free to contact us on IRC -(`irc.freenode.net#galaxyproject `__). - -The proxy configuration is interesting, here we'll point NGINX to reverse proxy -a service running on ``:8000`` inside the container. That port will be hosting -a python process which serves up the directory contents of ``/import``, i.e. -the file the user selected which was mounted as a volume into -``/import/file.dat`` - -.. code-block:: nginx - - events { - worker_connections 1024; - } - - - http { - include mime.types; - default_type application/octet-stream; - - sendfile on; - keepalive_timeout 65; - - server { - listen 80; - server_name localhost; - access_log /var/log/nginx/localhost.access.log; - - root /web/; - - location PROXY_PREFIX/ { - alias /web/; - } - - rewrite ^(.*)/helloworld/(.*\.dat)$ PROXY_PREFIX/helloworld/dir/$2; - - location PROXY_PREFIX/helloworld/dir/ { - proxy_buffering off; - proxy_pass http://127.0.0.1:8000/; - proxy_redirect http://127.0.0.1:8000/ PROXY_PREFIX/helloworld/dir/; - } - } - } - -Below is our ``index.html`` file - -.. code-block:: html - - - - - -

    Welcome to Hello-World IE

    -

    - There is one service running, a directory - listing. (We originally had more but the github repository - for the flask app we were using disappeared.) -

    - - - -And here we'll run that service in our ``startup.sh`` file - -.. code-block:: bash - - #!/bin/bash - # First, replace the PROXY_PREFIX value in /proxy.conf with the value from - # the environment variable. - sed -i "s|PROXY_PREFIX|${PROXY_PREFIX}|" /proxy.conf; - # Then copy into the default location for ubuntu+nginx - cp /proxy.conf /etc/nginx/sites-enabled/default; - - # Here you would normally start whatever service you want to start. In our - # example we start a simple directory listing service on port 8000 - cd /import/ && python -mSimpleHTTPServer & - - # Launch traffic monitor which will automatically kill the container if - # traffic stops - /monitor_traffic.sh & - # And finally launch nginx in foreground mode. This will make debugging - # easier as logs will be available from `docker logs ...` - nginx -g 'daemon off;' - -Lastly, our ``monitor_traffic.sh`` file is often re-used between containers, the only adjustment being the port that is looked at - -.. code-block:: bash - - #!/bin/bash - while true; do - sleep 60 - if [ `netstat -t | grep -v CLOSE_WAIT | grep ':80' | wc -l` -lt 3 ] - then - pkill nginx - fi - done - -With those files, ``monitor_traffic.sh``, ``Dockerfile``, ``startup.sh``, and ``proxy.conf``, you should be able to build your ``hello-ie`` container - -.. code-block:: bash - - $ cd hello-ie - $ docker build -t hello-ie . - -Now, if everything went smoothly, you should be able to restart Galaxy and try out your new GIE on a tabular or text file! - -Debugging ---------- - -When you launch your new GIE in Galaxy, your Galaxy logs should show something like the following: - -.. code-block:: console - - Starting docker container for IE helloworld with command [docker run --sig-proxy=true -e DEBUG=false -e "GALAXY_URL=http://localhost/galaxy/" -e "CORS_ORIGIN=http://localhost" -e "GALAXY_WEB_PORT=8000" -e "HISTORY_ID=f2db41e1fa331b3e" -e "CUSTOM=42" -e "GALAXY_PASTER_PORT=8000" -e "PROXY_PREFIX=/galaxy/gie_proxy" -e "API_KEY=1712364174a0ff79b34e9a78fee3ca1c" -e "REMOTE_HOST=127.0.0.1" -e "USER_EMAIL=hxr@local.host" -d -P -v "/home/hxr/work/galaxy/database/tmp/tmp5HaqZy:/import/" -v "/home/hxr/work/galaxy/database/files/000/dataset_68.dat:/import/file.dat:ro" hello-ie] - -Here's the docker command written out in a more readable manner: - -.. code-block:: console - - $ docker run --sig-proxy=true \ - -d -P \ - -e "API_KEY=1712364174a0ff79b34e9a78fee3ca1c" \ - -e "CORS_ORIGIN=http://localhost" \ - -e "CUSTOM=42" \ - -e "DEBUG=false" \ - -e "GALAXY_PASTER_PORT=8000" \ - -e "GALAXY_URL=http://localhost/galaxy/" \ - -e "GALAXY_WEB_PORT=8000" \ - -e "HISTORY_ID=f2db41e1fa331b3e" \ - -e "PROXY_PREFIX=/galaxy/gie_proxy" \ - -e "REMOTE_HOST=127.0.0.1" \ - -e "USER_EMAIL=hxr@local.host" \ - -v "/home/hxr/work/galaxy/database/tmp/tmp5HaqZy:/import/" \ - -v "/home/hxr/work/galaxy/database/files/000/dataset_68.dat:/import/file.dat:ro" \ - hello-ie - -As you can see, a LOT is going on! We'll break it down further: - -- ``-d`` runs the container in daemon mode, it launches and the client - submitting the container finished -- ``-P`` randomly assigns an unused port to the container for each ``EXPOSE``d - port from our ``Dockerfile``. This is why you must expose a port, and only - one port. -- A large number of environment variables are set: - - - The user's API key is provided, allowing you to access datasets and - submit jobs on their behalf. If you have an environment like - Jupyter/RStudio, it is **highly recommended** that you provide some magic - by which the user can use their API key without embedding it in the - notebook. If you do embed it somehow in a document that gets saved to - their history, anyone can impersonate that user if they get a hold of it. - In the Jupyter GIE we have a variable that just runs - ``os.environ.get('API_KEY')`` to avoid embedding it in their notebook. - - A CORS Origin is provided for very strict servers, but it may be easier - to simply void CORS requirements within the nginx proxy in your - container. - - Custom variables specified in your ``launch()`` command are available - - A ``DEBUG`` environment variable should be used to help admins debug - existing containers. You should use it to increase logging, not cleanup - temporary files, and so on. - - ``GALAXY_PASTER_PORT`` (deprecated) and ``GALAXY_WEB_PORT`` are the raw - port that Galaxy is listening on. You can use this to help decide how to - talk to Galaxy. - - ``GALAXY_URL`` is the URL that Galaxy should be accessible at. For - various reasons this may not be true. We recommend looking at our - implementation of `galaxy.py - `__ - which is a small utility script to provide API access to Galaxy to get - and fetch data, based on those environment variables. - - The ``HISTORY_ID`` of the current history the user is on is provided. In - the Jupyter/RStudio containers, we provide a dead simple method for users - to download datasets from their current history which will be visible to - them on the right hand side of their screen. - - A ``PROXY_PREFIX`` is provided which should be used in the nginx conf. - - ``REMOTE_HOST`` is another component used to test for a possible Galaxy - access path - - The user's email is made available, very convenient for webservices like - Entrez which require the user's email address. You can pre-fill it out - for them, making their life easier. - - Two volumes are mounted, one a temporary directory from Galaxy (rw), and one - the dataset the user selected (ro). - -- and finally the image is specified. - -Most of this information is usually required to build friendly, easy-to-use -GIEs. One of the strong points of GIEs is their magic interaction with Galaxy. -Here we've mounted a volume read-only, but in real life you may wish to provide -connectivity like Jupyter and RStudio provide, allowing the user to load -datasets on demand for interactive analysis, and then to store analysis -artifacts (and a log of what was done inside the container, à la Jupyter's -"notebooks") back to their current history. - -If everything went well, at this point you should see a directory listing show up: - -.. image:: interactive_environments_success.png - -If you find yourself encountering difficulties, the "Hello, World" IE is -available in a `GitHub repo `__, and there are people on the IRC channel who can help debug. +Galaxy Interactive Environments are deprecated and were the precursors to the current :doc:`Galaxy Interactive Tools <../admin/special_topics/interactivetools>`. +You can find information about Galaxy Interactive Environments in `older versions `__ of this document. diff --git a/doc/source/dev/interactive_environments_success.png b/doc/source/dev/interactive_environments_success.png deleted file mode 100644 index 2f881909379..00000000000 Binary files a/doc/source/dev/interactive_environments_success.png and /dev/null differ diff --git a/doc/source/dev/starting_galaxy.md b/doc/source/dev/starting_galaxy.md deleted file mode 100644 index 2f8c483f45e..00000000000 --- a/doc/source/dev/starting_galaxy.md +++ /dev/null @@ -1,95 +0,0 @@ -# Starting Galaxy - -## What happens when you start Galaxy? - -Galaxy is started by running ./run.sh script. The following call graph documents what happens during Galaxy startup process. Please note, Galaxy is a WSGI app, so in principle all Python WSGI servers can run the Galaxy app (Paste, Gunicorn and uWSGI are known to work and have some level of support). This document separates web server specific section (e.g. Paste) from Galaxy specific section (FYI, Galaxy uses uWSGI by default). - -## Paste - -* ./galaxy/scripts/paster.py - * Add ./galaxy/lib to system path - * Check Python version - * version >= 3.5 -> OK - * version = 2.7 -> Deprecated - * No other version is supported - * serve.run() - -* ./galaxy/lib/galaxy/util/pastescript/serve.py (The Python Paste package contains Python modules that help in implementing WSGI middleware) - * serve.run() - * Parse Python command line arguments - * Extract the command name from command line arguments (e.g., 'serve') - * Get the Command object for command name, then invoke it (call Command's run() method) - * Command.run() - * Setup default options, validate command line arguments, then call ServeCommand's command() method - * ServeCommand.command() - * Conditionally, stop the daemon - * If a config file is required, make sure a config file is provided on the command line arguments. - * Possible subcommands: start, stop, restart, status (or None) - * Call loadserver() method in ./galaxy/lib/galaxy/util/pastescript/loadwsgi.py - * Calls loadobj() -> calls LoaderContext's create() -> calls _Server's invoke() - * Call loadapp() method in ./galaxy/lib/galaxy/util/pastescript/loadwsgi.py - * Calls loadobj() -> calls LoaderContext's create() -> calls _App's invoke() -> calls fix_call() on app_factory (in ./galaxy/lib/galaxy/webapps/galaxy/buildapp.py) - -## Galaxy - -* app_factory: - * Create UniverseApplication (in ./galaxy/lib/galaxy/webapps/galaxy/buildapp.py) - * Create GalaxyAppConfiguration instance from config file specified on the command line, then check for errors - * Configure logging (in ./galaxy/lib/galaxy/config/__init__.py) - * Create ExecutionTimerFactory - * Which has a GalaxyStatsdClient field - * Configure FluentTraceLogger - * Create the application stack (uWSGi, Paste, or Webless) - * App registers post fork functions to start application stack threads - * Create GalaxyQueueWorker instance (a worker for Galaxy's queues, used for dispatching control tasks. Sqlite instance) - * Configure tool shed registry - * Configure object store. build_object_store_from_config() (in ./galaxy/lib/galaxy/objectstore/__init__.py) calls objectstore_class.from_xml(), calls clazz.parse_xml() - * Configure models in _configure_models() - * Calls create_or_verify_database() in ./galaxy/libl/galaxy/model/migrate/check.py - * If a new database, calls migrate_from_scratch() in ./galaxy/libl/galaxy/model/migrate/check.py - * Calls mapping.init() in ./galaxy/libl/galaxy/model/mapping.py (Details of how data objects are mapped to relational DB tables). - * Calls init_models_from_config in in ./galaxy/libl/galaxy/model/mapping.py - * Configure security - * Set GalaxyTagHandler (in ./galaxy/lib/galaxy/model/tags.py) - * Handles CRUD operations for tagging datasets, workflows, visualizations, etc - * Set DatasetCollectionManager (in ./galaxy/lib/galaxy/managers/collections.py) - * Configure HistoryManager - * Configure HistoryDatasetAssociation (HDA) Manager - * Create WorkflowsManager - * Handles CRUD operations related to workflows - * Configure DependencyResolverView (a RESTful interface to galaxy.tool_util.deps.DependencyResolver) - * Configure TestDataResolver - * Configure LibraryManager (Data libraries under Shared Data) - * Configure FolderManager (Folders are folders in a data library) - * Configure DynamicToolManager (Tools that are loaded from DB instead of disk files) - * Configure DataProviderRegistry - * Configure JobMetrics manager - * Configure ErrorReport plugins - * Configure job management configuration - * Setup a tool cache - * Watch various config files for immediate reload - * Configure Data Manager - * Configure UpdateRespositoyManager - * Load proprietary datatype convertors and display applications - * Configure VisualizationRegistry, ToursRegistry, and WebHookRegistry - * Load security policy and quota management - * Configure heartbeat for thread profiling - * If configured, create Sentry client - * App registers post fork functions to start Sentry client thread - * Create JobManager - * App registers post fork functions to start application job manager thread - * Job manager thread starts monitor threads for JobHandlerQueue and JobHandlerStopQueue (2 threads) - * JobManager has a JobHandler field, which manages the preparation, running, tracking, and finishing of jobs - * JobHandler has a job dispatcher, which launches underlying job runners - * DefaultJobDispatcher calls app's JobConfiguration's get_job_runner_plugins() to create LocalJobRunner instances - * The default number of LocalJobRuner instances is 4 (4 threads) - * JobHandler also has queues for starting and stopping jobs (unless track_jobs_in_database is True) - * Create WorkflowSchedulingManager - * Starts workflow request monitor thread in __start_request_monitor() method (1 thread) - * Configure InteractiveToolManager - * Create database heartbeat instance - * App registers post fork functions to start application DB heartbeat thread (1 thread) - * Create CommunityWebApplication (in ./galaxy/lib/galaxy/webapps/galaxy/buildapp.py) - * CommunityWebApplication: A WSGI application that maps requests to objects using routes and to methods on those objects - * Map all requests to controllers/actions - * Wrap web app in some middleware and return webapp diff --git a/doc/source/releases/22.01.rst b/doc/source/releases/22.01.rst new file mode 100644 index 00000000000..730bbdd92ff --- /dev/null +++ b/doc/source/releases/22.01.rst @@ -0,0 +1,971 @@ + +.. to_doc + +22.01 +=============================== + +Enhancements +------------------------------- + +* Swap to Webpack5 + `Pull Request 11710`_ +* Remote / Late evaluation of tools + `Pull Request 12459`_ +* File sources for gdrive, gcs, onedata, basespace + (thanks to `@nuwang `__). + `Pull Request 12500`_ +* Add support for caching the Singularity cached images directory + `Pull Request 12524`_ +* Various extensions and improvements for asserts + (thanks to `@bernt-matthias `__). + `Pull Request 12528`_ +* New interactive tool - iSEE + (thanks to `@neoformit `__). + `Pull Request 12650`_ +* Resumable file uploads with tus.io + `Pull Request 12656`_ +* Make directory path editable in remote files + (thanks to `@OlegZharkov `__). + `Pull Request 12759`_ +* Display tool source in tool form + `Pull Request 12811`_ +* Modify history contents/near endpoint to support new history scroller + `Pull Request 12892`_ +* Vault abstraction for Galaxy + (thanks to `@nuwang `__). + `Pull Request 12940`_ +* Sort icon on List Collection Creator + (thanks to `@assuntad23 `__). + `Pull Request 12945`_ +* Provide more context for lint context + (thanks to `@bernt-matthias `__). + `Pull Request 12978`_ +* Add mulled-hash command to galaxy-tool-util + (thanks to `@simonbray `__). + `Pull Request 13061`_ +* Add parquet to csv datatype converter + `Pull Request 13078`_ +* XML macros: add named yields, tokenized macros, tokens for attributes, and fix replacement of toplevel yield + (thanks to `@bernt-matthias `__). + `Pull Request 13152`_ +* Enable setting SQLALCHEMY_WARN_20 + `Pull Request 13171`_ +* Add new datatype GRIB + (thanks to `@annefou `__). + `Pull Request 13175`_ +* Use Gravity to run Galaxy + `Pull Request 13224`_ +* Add datatype iml_dataset subclassing html + (thanks to `@knutwa-ext `__). + `Pull Request 12099`_ +* Mark sniffer for fastqillumina as not implemented + (thanks to `@bernt-matthias `__). + `Pull Request 12685`_ +* Create sniffer for GRO file type + (thanks to `@simonbray `__). + `Pull Request 12885`_ +* Agp datatype + `Pull Request 12891`_ +* Type updates from @mr-c - part 2 + (thanks to `@common-workflow-lab `__). + `Pull Request 12899`_ +* 3rd round of type updates to support CWL import + (thanks to `@common-workflow-lab `__). + `Pull Request 12913`_ +* Tighten type ignores + (thanks to `@common-workflow-lab `__). + `Pull Request 12965`_ +* Remove is_multi_byte + (thanks to `@bernt-matthias `__). + `Pull Request 12996`_ +* Add ludwig model datatype + (thanks to `@qiagu `__). + `Pull Request 13073`_ +* Add ampvis2 datatype + (thanks to `@bernt-matthias `__). + `Pull Request 13083`_ +* Add Neper and Gmsh datatypes + (thanks to `@jj-umn `__). + `Pull Request 13087`_ +* Add description of inpcrd and prmtop + (thanks to `@thepineapplepirate `__). + `Pull Request 13126`_ +* Add compression converter + (thanks to `@gallardoalba `__). + `Pull Request 13223`_ +* Change display_in_upload to true for PHAR datatype + (thanks to `@simonbray `__). + `Pull Request 13238`_ +* Add gz as datatype + (thanks to `@astrovsky01 `__). + `Pull Request 13271`_ +* Limit required element validation to safe elements + `Pull Request 13299`_ +* Add phyloseq datatype + (thanks to `@bernt-matthias `__). + `Pull Request 13462`_ +* Allow any format in export tool + (thanks to `@astrovsky01 `__). + `Pull Request 13151`_ +* Add bioconductor as valid xref for tools + (thanks to `@bgruening `__). + `Pull Request 13268`_ +* Test cases to verify only admins can import workflows from paths. + `Pull Request 12513`_ +* Remove deprecated workflow run path through the API. + `Pull Request 12516`_ +* Type updates + (thanks to `@common-workflow-lab `__). + `Pull Request 12868`_ +* Allow workflows to automatically filter inputs by tag + (thanks to `@astrovsky01 `__). + `Pull Request 12930`_ +* Enable macros for workflow scheduler config + (thanks to `@bernt-matthias `__). + `Pull Request 13167`_ +* Remove history service worker + `Pull Request 12510`_ +* Implement fastAPI prefix handling, run selenium tests against prefix + `Pull Request 13103`_ +* Hide form fields and button upon successful submission of bug report + `Pull Request 13237`_ +* Fix toolbox cache + `Pull Request 13333`_ +* Limit maximum number of discovered files per job + `Pull Request 13170`_ +* Fixes for HTML sanitization + (thanks to `@abretaud `__). + `Pull Request 13059`_ +* Make admin user list sortable by last login + (thanks to `@cat-bro `__). + `Pull Request 13209`_ +* Support "show all running" in admin job interface + `Pull Request 13215`_ +* Add config variable for setting tus upload directory + `Pull Request 13313`_ +* Document gunicorn and nginx + `Pull Request 13436`_ +* Update interactive tool docs for Gunicorn / dropping uWSGI + `Pull Request 13470`_ +* Add Migrating from uWSGI to Gunicorn and FastAPI document + `Pull Request 13480`_ +* Support ``--preload`` and worker_id for server processes + `Pull Request 13514`_ +* Separated data PVC + (thanks to `@almahmoud `__). + `Pull Request 12345`_ +* Revised download() so parallel get() is utilized. Small refactor. + (thanks to `@kxk302 `__). + `Pull Request 12522`_ +* More linting for requirements + (thanks to `@bernt-matthias `__). + `Pull Request 12527`_ +* Preserve important dataset metadata when importing/exporting histories. + `Pull Request 12537`_ +* Modify description of the "database" label + `Pull Request 12540`_ +* Add Oleg Zharkov to committers group + `Pull Request 12544`_ +* Add David López to committers group + `Pull Request 12545`_ +* Update container_resolvers_conf.xml.sample + (thanks to `@gmauro `__). + `Pull Request 12560`_ +* Add test for dataset HTML sanitization interface + `Pull Request 12573`_ +* FastAPI: migrate history contents routes + (thanks to `@davelopez `__). + `Pull Request 12578`_ +* Update localization directive to trim whitepsace around text content. + `Pull Request 12580`_ +* Added more caching for github actions which use tox and venv + (thanks to `@nuwang `__). + `Pull Request 12632`_ +* More typing for API tests (especially test_workflows). + `Pull Request 12636`_ +* Better model store abstractions. + `Pull Request 12638`_ +* More Python unit test typing fixes. + `Pull Request 12647`_ +* Annotate more tests as requiring Github to be online. + `Pull Request 12649`_ +* Change logged-in-as to use username + (thanks to `@hexylena `__). + `Pull Request 12652`_ +* Add buttons to expand and collapse history dataset display in pages + `Pull Request 12660`_ +* Formalize dependency of galaxy-data package on galaxy.files module. + `Pull Request 12665`_ +* Check types of modules without type annotations in the their signatures on new modules. + `Pull Request 12691`_ +* Rev Python tooling to latest mypy release. + `Pull Request 12695`_ +* Assorted random typing fixes. + `Pull Request 12696`_ +* Add Tag validation in API requests + (thanks to `@davelopez `__). + `Pull Request 12697`_ +* Framework support for integration tests requiring celery. + `Pull Request 12700`_ +* Allow passing Pydantic models as arguments to Celery tasks. + `Pull Request 12701`_ +* Discourage the use of true/falsevalue for boolean test parameters + (thanks to `@bernt-matthias `__). + `Pull Request 12706`_ +* Extend debugging test instructions for GHA + `Pull Request 12707`_ +* Split integration tests more evenly + `Pull Request 12711`_ +* Tool linter: simple test for test params absent from inputs + (thanks to `@bernt-matthias `__). + `Pull Request 12723`_ +* Add test for invoking a workflow on a new autocreated history + (thanks to `@nsoranzo `__). + `Pull Request 12773`_ +* More Python type fixes from deferred data branch. + `Pull Request 12778`_ +* Type fixes for HDAs and LDDAs manager. + `Pull Request 12780`_ +* FastAPI: migrate Dataset Collections API + (thanks to `@davelopez `__). + `Pull Request 12781`_ +* Type fixes for various visualization backend Python files. + `Pull Request 12783`_ +* Add various include/exclude patterns for github workflows + `Pull Request 12787`_ +* Force strict channel priority when building images with mulled + (thanks to `@rhpvorderman `__). + `Pull Request 12788`_ +* More type fixes for various pieces of managers code. + `Pull Request 12801`_ +* Beta history menu reorg, display fixes. + `Pull Request 12802`_ +* More type fixes for managers code. + `Pull Request 12812`_ +* Fixes and test for PDF generation. + `Pull Request 12813`_ +* Upgrade FastAPI to v0.70.0 + (thanks to `@davelopez `__). + `Pull Request 12836`_ +* Stronger typing of upload code. + `Pull Request 12845`_ +* Sync Selenium GitHub workflows + (thanks to `@nsoranzo `__). + `Pull Request 12849`_ +* Update all Python dependencies + (thanks to `@nsoranzo `__). + `Pull Request 12858`_ +* Drop legacy uploadpost + `Pull Request 12861`_ +* Vuefy color input form element + (thanks to `@OlegZharkov `__). + `Pull Request 12867`_ +* Show error codes when running mypy + (thanks to `@nsoranzo `__). + `Pull Request 12894`_ +* Update cwltool pin to 3.1.20211107152837 + (thanks to `@nsoranzo `__). + `Pull Request 12895`_ +* Increase directory check specificity, of particular use with AnVIL + (thanks to `@luke-c-sargent `__). + `Pull Request 12902`_ +* Create codeql-analysis.yml + (thanks to `@common-workflow-lab `__). + `Pull Request 12908`_ +* Improved Upload Provenance and Correctness + `Pull Request 12912`_ +* Update Python dependencies + (thanks to `@galaxybot `__). + `Pull Request 12916`_ +* Lint for option without text or value + (thanks to `@bernt-matthias `__). + `Pull Request 12921`_ +* FastAPI: migrate datasets API + (thanks to `@davelopez `__). + `Pull Request 12924`_ +* Vuefy Number and Slider Form inputs + (thanks to `@OlegZharkov `__). + `Pull Request 12937`_ +* Allow labeling of subworkflow outputs in parent workflow + `Pull Request 12939`_ +* Linting: check for test expecting failure with outputs + (thanks to `@bernt-matthias `__). + `Pull Request 12960`_ +* Add CWL conformance API tests + (thanks to `@nsoranzo `__). + `Pull Request 12966`_ +* Allow unit testing tool init to specify paths. + (thanks to `@common-workflow-lab `__). + `Pull Request 12969`_ +* Lint for inputs with redundant name + (thanks to `@bernt-matthias `__). + `Pull Request 12974`_ +* Support running celery when cwd is not galaxy_root by setting $GALAXY_ROOT_DIR + `Pull Request 12977`_ +* Split `run_tool_raw()` out of `BaseDatasetPopulator.run_tool()` + (thanks to `@nsoranzo `__). + `Pull Request 12994`_ +* Move ``galaxy_tasks`` decorator to ``galaxy.celery`` + `Pull Request 13000`_ +* Show display applications together with visualizations in center panel + `Pull Request 13018`_ +* Update Python dependencies + (thanks to `@galaxybot `__). + `Pull Request 13029`_ +* Explicit singularity cache + (thanks to `@rhpvorderman `__). + `Pull Request 13032`_ +* Add "diff-format" Makefile target + (thanks to `@common-workflow-lab `__). + `Pull Request 13034`_ +* Drop support for Python 3.6, add Python 3.10 + (thanks to `@nsoranzo `__). + `Pull Request 13035`_ +* Allow param_value filter to refer to parameters with multiple values + (thanks to `@bernt-matthias `__). + `Pull Request 13039`_ +* Add support for CWL `CommandLineTool` and `ExpressionTool` to `DynamicToolManager.create_tool()` + (thanks to `@nsoranzo `__). + `Pull Request 13041`_ +* Update Python dependencies + (thanks to `@galaxybot `__). + `Pull Request 13051`_ +* Allow admin access to datasets API + (thanks to `@davelopez `__). + `Pull Request 13060`_ +* Drop non-actionable warnings from LibraryPermissionsWarning + `Pull Request 13063`_ +* Beta History: homogeneous collection indication + (thanks to `@davelopez `__). + `Pull Request 13068`_ +* Add documentation for test/param/metadata in xsd + (thanks to `@bernt-matthias `__). + `Pull Request 13082`_ +* Update Python dependencies + (thanks to `@galaxybot `__). + `Pull Request 13086`_ +* Add tox environment and GitHub workflow for reports startup test + (thanks to `@nsoranzo `__). + `Pull Request 13088`_ +* Allow duplicated static options + (thanks to `@bernt-matthias `__). + `Pull Request 13094`_ +* Beta History: additional small fixes at collection level + (thanks to `@davelopez `__). + `Pull Request 13097`_ +* Add Back button in file source navigation and ftp helper text + (thanks to `@itisAliRH `__). + `Pull Request 13104`_ +* Add job reports for node types and execution times + (thanks to `@gregvonkuster `__). + `Pull Request 13121`_ +* Initial typing for model discover module. + `Pull Request 13123`_ +* Remove Oleg from committers list + `Pull Request 13128`_ +* Remove Dave C. from committers list + `Pull Request 13129`_ +* Update Python dependencies + (thanks to `@galaxybot `__). + `Pull Request 13135`_ +* Fix MetadataValidator + (thanks to `@bernt-matthias `__). + `Pull Request 13139`_ +* Update DatasetError.vue + `Pull Request 13148`_ +* Assorted Data Libraries fixes + (thanks to `@davelopez `__). + `Pull Request 13159`_ +* Provide even more context to the lint context + (thanks to `@bernt-matthias `__). + `Pull Request 13186`_ +* Skip non-Selenium tests if only lib/galaxy_test/selenium is touched + `Pull Request 13227`_ +* Add documentation for output action attributes + (thanks to `@bernt-matthias `__). + `Pull Request 13236`_ +* Add openPBS cli support, rebuild CLI test images + `Pull Request 13251`_ +* Add missing collection types to xsd doc + (thanks to `@bernt-matthias `__). + `Pull Request 13267`_ +* Update dependencies + `Pull Request 13273`_ +* Only create HDAH entries for undeleted HDAs + (thanks to `@bernt-matthias `__). + `Pull Request 13309`_ +* Add version.json for exposing extra api information about docker container + (thanks to `@nuwang `__). + `Pull Request 13433`_ +* Document gunicorn and apache + `Pull Request 13439`_ +* Update jobs and cluster documentation + `Pull Request 13440`_ +* Update performance tracking for Gunicorn + `Pull Request 13441`_ +* Drop interactive environment build instructions from docs + `Pull Request 13442`_ +* Create config package, move app-related functionality to app + `Pull Request 13461`_ +* Fix pgcleanup.py for datasets stored by UUID + `Pull Request 13474`_ +* Pin gravity to 0.10.0 + `Pull Request 13483`_ +* Include gravity in sample config + `Pull Request 13531`_ +* FastAPI: Histories API migration + (thanks to `@davelopez `__). + `Pull Request 12386`_ +* Vue component dangling bracket style fix (prettier upgrade) + `Pull Request 12428`_ +* Remove unused relationships from model + `Pull Request 12468`_ +* Remove workflow API hooks for tool shed. + `Pull Request 12512`_ +* Vueify Form Recursion + `Pull Request 12518`_ +* FastAPI: Add request wrapper for `SessionRequestContext` + (thanks to `@davelopez `__). + `Pull Request 12531`_ +* Update SQLAlchemy to 1.4.25 + `Pull Request 12538`_ +* Establish unittest utils for galaxy-{data,objectstore} packages. + `Pull Request 12546`_ +* Move API services to their own package + (thanks to `@davelopez `__). + `Pull Request 12548`_ +* Establish galaxy-app unit tests. + `Pull Request 12549`_ +* Establish galaxy-job-execution unit test directory. + `Pull Request 12550`_ +* Establish galaxy-job-metrics unit test directory. + `Pull Request 12551`_ +* Fix up small issues around packaging of galaxy-tool-util code. + `Pull Request 12552`_ +* Move more webapps unit tests in test/unit/webapps. + `Pull Request 12554`_ +* Share a tool_util unittest method via packing to fix up unit test dependencies. + `Pull Request 12555`_ +* Better path handling in app unit tests for packaging. + `Pull Request 12556`_ +* Selenium abstractions for dataset information. + `Pull Request 12569`_ +* TS model: declarative mapping + prep for SQLAlchemy 2.0 + `Pull Request 12666`_ +* Remove unused script and ``history_delete`` controller endpoint + `Pull Request 12668`_ +* Remove ui-table backbone component + `Pull Request 12671`_ +* Libraries (legacy) backbone entrypoint and component removal + `Pull Request 12674`_ +* Ignore type in unused function + `Pull Request 12694`_ +* Misc. fixes for SQLAlchemy 2.0 + `Pull Request 12698`_ +* Upgrade to lagom 1.4.1 + (thanks to `@meadsteve `__). + `Pull Request 12703`_ +* Remove `galaxy.util.getfullargspec` + (thanks to `@bernt-matthias `__). + `Pull Request 12755`_ +* Convert install_model to declarative mapping + `Pull Request 12756`_ +* Mapping tests: restructure, refactor, decouple session and engine, improve + `Pull Request 12765`_ +* New pyupgrade cleanups + (thanks to `@nsoranzo `__). + `Pull Request 12774`_ +* Refactor workflow bookmark + (thanks to `@OlegZharkov `__). + `Pull Request 12810`_ +* Replace boolean input parameter with Vue component in forms + `Pull Request 12820`_ +* Replace hidden field input element with Vue + `Pull Request 12841`_ +* Refactor datasets API + (thanks to `@davelopez `__). + `Pull Request 12847`_ +* Vuefy library folder details modal + (thanks to `@OlegZharkov `__). + `Pull Request 12886`_ +* Remove chart utilities and artifacts from code base + `Pull Request 12890`_ +* Vuefy DirectoryUri form input field + (thanks to `@OlegZharkov `__). + `Pull Request 12898`_ +* Simplify Augustus datatype sniffer + (thanks to `@nsoranzo `__). + `Pull Request 12905`_ +* Modify logic in history_contents/near to accommodate scroller + `Pull Request 12914`_ +* FastAPI: Add response wrapper for `SessionRequestContext` + (thanks to `@davelopez `__). + `Pull Request 12929`_ +* Eliminate a lot of while: True loops in datatype code + `Pull Request 12942`_ +* Small tweaks to facilitate deferred dataset implementation. + `Pull Request 12944`_ +* Modifications to next hid_counter generation + `Pull Request 12948`_ +* Don't bind model's MetaData to engine + `Pull Request 12949`_ +* Vuefy library dataset + (thanks to `@OlegZharkov `__). + `Pull Request 12950`_ +* Assorted mapping cleanup + `Pull Request 12951`_ +* Move `wait_for_invocation_and_jobs()` method to `BaseWorkflowPopulator` + (thanks to `@nsoranzo `__). + `Pull Request 12954`_ +* Avoid setting response headers through `trans` in datatypes + (thanks to `@davelopez `__). + `Pull Request 12956`_ +* Use ``lazy="joined"`` instead of ``lazy=False`` + `Pull Request 12962`_ +* Minor type fixes + `Pull Request 12963`_ +* Add type hints to JobHandlerQueue + `Pull Request 12976`_ +* Fix flaky legacy API tests (hopefully) + (thanks to `@davelopez `__). + `Pull Request 12980`_ +* Create select() construct explicitly + `Pull Request 12983`_ +* Remove unused dataset conversion code + `Pull Request 12991`_ +* Remove unnecessary list creations + (thanks to `@nsoranzo `__). + `Pull Request 13002`_ +* Don't use triple-quoted strings as comments + (thanks to `@nsoranzo `__). + `Pull Request 13007`_ +* Misc fixes - round 4 + (thanks to `@nsoranzo `__). + `Pull Request 13026`_ +* Refactor uploadbox / partially remove jQuery + `Pull Request 13047`_ +* Initial refactoring to separate History rendering and provider components + `Pull Request 13049`_ +* Log missing version file as warning. + `Pull Request 13062`_ +* Importing math.inf no longer needs try/except + `Pull Request 13065`_ +* Avoid imports of history provider internals + `Pull Request 13098`_ +* Drop tool migration scripts and related code + `Pull Request 13099`_ +* Remove duplicated pydantic models + (thanks to `@davelopez `__). + `Pull Request 13125`_ +* Remove migrate tools-related code + `Pull Request 13163`_ +* Replace backref with back_populates + `Pull Request 13165`_ +* Prevent a future regression + `Pull Request 13218`_ +* Remove `jquery.complexify` from everywhere + (thanks to `@itisAliRH `__). + `Pull Request 13228`_ +* Replace custom tour ids in forms with regular DOM identifiers + `Pull Request 13252`_ +* Drop pkg_resources + `Pull Request 13454`_ + + +Fixes +------------------------------- + +* Large peptide IDs + (thanks to `@reid-wagner `__). + `Pull Request 13261`_ +* Try to unit test more of DictParser in Viz component. + `Pull Request 13385`_ +* Fix jqplot_bar import, adjust plugin hash saving + `Pull Request 13493`_ +* Allow trailing semicolon in last column of gff3 + (thanks to `@bernt-matthias `__). + `Pull Request 12673`_ +* Fix gff-version header sniffing + (thanks to `@nsoranzo `__). + `Pull Request 12676`_ +* Fix gff and scidx parsing + (thanks to `@bernt-matthias `__). + `Pull Request 12687`_ +* Version bump galaxy tool util and converter dependency fix + `Pull Request 12844`_ +* Downgrade bowtie requirement for CONVERTER_fasta_to_bowtie_color_index + (thanks to `@nsoranzo `__). + `Pull Request 12851`_ +* Fix constructive_solid_geometry.py for non_utf8_error + (thanks to `@jj-umn `__). + `Pull Request 13057`_ +* Show yaml files in preview + `Pull Request 13176`_ +* Enable display of BAM data without sequence info + (thanks to `@wm75 `__). + `Pull Request 13219`_ +* Fix containerized NCBI Datasets Genomes data source tool + `Pull Request 12915`_ +* Update interactivetool_pyiron.xml + (thanks to `@gmauro `__). + `Pull Request 12990`_ +* Add dynamic step to FormNumber range slider + (thanks to `@davelopez `__). + `Pull Request 13233`_ +* FormNumber: notify value change on slider change + (thanks to `@davelopez `__). + `Pull Request 13260`_ +* Always display digest in name for refgenie assets. + `Pull Request 13403`_ +* Fix `UnboundLocalError` in sort collection tool + (thanks to `@davelopez `__). + `Pull Request 13429`_ +* Misc fixes - round 2 + (thanks to `@nsoranzo `__). + `Pull Request 12982`_ +* Fix workflow connections for outputs that are not displayed in uploads + `Pull Request 13174`_ +* Sync tool identifier string with tool version in workflow editor + `Pull Request 13185`_ +* Allow mapping of collection outputs to parameter inputs + `Pull Request 13189`_ +* Fix tool id update when updating tool shed tools in editor + `Pull Request 13196`_ +* Ensure tool_id/content_id is updated when refactoring workflows + (thanks to `@simonbray `__). + `Pull Request 13231`_ +* Fix construction of SelectToolParameter default value + `Pull Request 13293`_ +* Return actual tool id instead of specified tool id in workflow step + `Pull Request 13536`_ +* RxJS major version upgrade, client unit testing enhancements. + `Pull Request 12686`_ +* Polyfill overhaul, fixes. + `Pull Request 12889`_ +* Fix publicPath and asset handling when using webpack-dev-server for HMR + `Pull Request 13155`_ +* Bump markdown-it from 12.2.0 to 12.3.2 in /client + `Pull Request 13205`_ +* Bump engine.io from 4.1.1 to 4.1.2 in /client + `Pull Request 13206`_ +* Bump follow-redirects from 1.14.4 to 1.14.7 in /client + `Pull Request 13207`_ +* Bump log4js from 6.3.0 to 6.4.0 in /client + `Pull Request 13225`_ +* Bump nanoid from 3.1.29 to 3.2.0 in /client + `Pull Request 13226`_ +* Guard against instance of GenomeIndexToolData + `Pull Request 12523`_ +* Fix job runner shutdown + `Pull Request 12744`_ +* Fix parsing of yml job conf file in galaxy.dependencies + (thanks to `@cat-bro `__). + `Pull Request 12893`_ +* Rev pulsar-galaxy-lib. + `Pull Request 12896`_ +* Don't pass all Galaxy environment variables to jobs + (thanks to `@nsoranzo `__). + `Pull Request 13011`_ +* Unit test fixes + (thanks to `@nsoranzo `__). + `Pull Request 13033`_ +* Fix re-run with rerun_remap_job_id hiding collection + `Pull Request 13157`_ +* Fix univa job runner setup without nativeSpecification + (thanks to `@Smithsonian `__). + `Pull Request 13301`_ +* Make __link_file_check more robust + `Pull Request 13316`_ +* Don't record version command as part of command line + `Pull Request 13317`_ +* Fix resuming job when job has optional data parameters + `Pull Request 13325`_ +* Allow UNIVA destinations without runtime and memory max given in the nativespec + (thanks to `@bernt-matthias `__). + `Pull Request 13343`_ +* Fix header content-length, add test for send_file + `Pull Request 13120`_ +* Add option for disabling tool shed datatypes + `Pull Request 13250`_ +* Drop OpenID post authentication protocols + `Pull Request 13281`_ +* Restore Metadata size limit + `Pull Request 13315`_ +* Drop uWSGI from scaling documentation + `Pull Request 13381`_ +* Reimplement collection order test, add wrong order, add `expect_test_failure` attribute + (thanks to `@bernt-matthias `__). + `Pull Request 10304`_ +* Fix test_sharing_valid_by_id selenium test + (thanks to `@OlegZharkov `__). + `Pull Request 12547`_ +* Update and correct sample object store config file + `Pull Request 12571`_ +* Fix ``user_library_import_symlink_allowlist`` option + (thanks to `@scholtalbers `__). + `Pull Request 12608`_ +* Fix selenium populators to respect the json parameter. + `Pull Request 12626`_ +* Fix how beta history panel decides when downloads are available. + `Pull Request 12642`_ +* Typing fixes for various unit tests. + `Pull Request 12644`_ +* Fix database display for beta history panel. + `Pull Request 12646`_ +* Fix deprecation warnings from the `threading` module + (thanks to `@nsoranzo `__). + `Pull Request 12659`_ +* Join thread that hosts asyncio loop for embedded uvicorn + `Pull Request 12710`_ +* Removing unusable Name field from Collections upload + (thanks to `@assuntad23 `__). + `Pull Request 12736`_ +* Removing last character of pair name for Paired List Collections if it's a . or _ + (thanks to `@assuntad23 `__). + `Pull Request 12739`_ +* Avoid double '/' when generating fully qualified API URLs in API tests + (thanks to `@davelopez `__). + `Pull Request 12791`_ +* Create new history for anonymous API users via history/current_histor… + `Pull Request 12829`_ +* Avoid console error when cloning newly created nodes + `Pull Request 12831`_ +* GxITs: don't always add trailing slash to entry point url + (thanks to `@abretaud `__). + `Pull Request 12833`_ +* fix typo in bookmarking + (thanks to `@OlegZharkov `__). + `Pull Request 12838`_ +* Fix bookmarking workflow twice + (thanks to `@OlegZharkov `__). + `Pull Request 12840`_ +* Temporary downgrade FastAPI to v0.68.2 + (thanks to `@davelopez `__). + `Pull Request 12856`_ +* Fix startup with edam + (thanks to `@abretaud `__). + `Pull Request 12860`_ +* Fixes for packages + (thanks to `@nsoranzo `__). + `Pull Request 12883`_ +* Fix `parse_input_sources()` method to return a list + (thanks to `@nsoranzo `__). + `Pull Request 12888`_ +* Consider a history not new once there are any items in it + `Pull Request 12897`_ +* Bump tensorflow from 2.5.1 to 2.5.2 in /lib/galaxy/dependencies + `Pull Request 12900`_ +* Sync package deps with `pyproject.toml` + (thanks to `@nsoranzo `__). + `Pull Request 12904`_ +* Fix menu overflow in right panel + `Pull Request 12906`_ +* Tweak docs for container_resolver_* options + `Pull Request 12910`_ +* Fix installation of tools with biotools id + `Pull Request 12919`_ +* Fix linter for select inputs + (thanks to `@bernt-matthias `__). + `Pull Request 12920`_ +* Fix OpenAPI docs not rendering + (thanks to `@davelopez `__). + `Pull Request 12923`_ +* Fix default value of `runners` in `job_conf_dict` + (thanks to `@nsoranzo `__). + `Pull Request 12926`_ +* Provide 2 types of scoped_session in DI (initially "type of session parameter should be scoped_session") + `Pull Request 12931`_ +* Move the `titleonly` option to input element component + `Pull Request 12932`_ +* fix value_in_data_table validator for multiple selects + (thanks to `@bernt-matthias `__). + `Pull Request 12936`_ +* Update link to "Python for Biologists" page. + (thanks to `@neoformit `__). + `Pull Request 12946`_ +* Try cimg/node:14 to fix circleci js builds + `Pull Request 12947`_ +* Misc fixes + (thanks to `@nsoranzo `__). + `Pull Request 12972`_ +* BugFix: Update social-auth-core library version to fix OIDC problem + (thanks to `@IslamHeggy `__). + `Pull Request 12973`_ +* Fix Rerun in the new form + (thanks to `@OlegZharkov `__). + `Pull Request 12984`_ +* Show error message when history id is invalid + (thanks to `@OlegZharkov `__). + `Pull Request 12985`_ +* Reconstruct separate scoped session for ToolShedRepositoryCache + `Pull Request 12989`_ +* Misc fixes - round 3 + (thanks to `@nsoranzo `__). + `Pull Request 12993`_ +* Bump python-ldap from 3.2.0 to 3.4.0 in /lib/galaxy/dependencies + `Pull Request 13003`_ +* Fix known marcos typo + `Pull Request 13009`_ +* Refactor Sentry middleware/client changes for Tool Shed + `Pull Request 13010`_ +* Drop stream_template_mako function, replace with fill_template_mako + `Pull Request 13017`_ +* Copy HDCA member items to new history when copying history + `Pull Request 13021`_ +* Make dataset display route sessionless as well as anonymous + `Pull Request 13024`_ +* Trigger visualization update on chart initialization + `Pull Request 13025`_ +* Fix running test/integration/test_remote_files.py directly. + `Pull Request 13038`_ +* Misc fixes - round 5 + (thanks to `@nsoranzo `__). + `Pull Request 13044`_ +* Fix dynamic optional + multiple select parameters: allow to select no option + (thanks to `@bernt-matthias `__). + `Pull Request 13048`_ +* Add tools to IMPLICITLY_REQUIRED_TOOL_FILES in tools/__init__.py + (thanks to `@cat-bro `__). + `Pull Request 13050`_ +* Only check profile version in Galaxy + `Pull Request 13055`_ +* Fix private history publishing + (thanks to `@davelopez `__). + `Pull Request 13056`_ +* Improve dataset search help + (thanks to `@wm75 `__). + `Pull Request 13058`_ +* Change default `file_path` variable for galaxy reports + (thanks to `@quacksawbones `__). + `Pull Request 13067`_ +* Drop old sqlalchemy patterns that break reports app + `Pull Request 13069`_ +* Add iRODS resource name in calls to irods + (thanks to `@kxk302 `__). + `Pull Request 13072`_ +* Fix Cloud object store + (thanks to `@bioflowy `__). + `Pull Request 13074`_ +* Use yield_per option to limit amount of ORM objects loaded into memory + `Pull Request 13076`_ +* Fix issue with uploading cached file to irods + (thanks to `@kxk302 `__). + `Pull Request 13080`_ +* Fix tag attribute parsing on input element + (thanks to `@astrovsky01 `__). + `Pull Request 13081`_ +* Fix dataset details button when using prefix + `Pull Request 13092`_ +* Fix center panel reload when clicking help button + `Pull Request 13093`_ +* Limit possible size of info field, read only 100 MB of stdio + `Pull Request 13096`_ +* Disable cleanup thread for cloud object store in set metadata + (thanks to `@bioflowy `__). + `Pull Request 13102`_ +* Prevent select all in FilesDialog when no entries and select empty resource + (thanks to `@itisAliRH `__). + `Pull Request 13107`_ +* Fix selenium test via dropping redundant test case + `Pull Request 13112`_ +* Delay iframe creation for news until clicked + `Pull Request 13116`_ +* Webhooks robustification for news and gtn + `Pull Request 13118`_ +* Fix routing for tool execution from gtn webhook + `Pull Request 13130`_ +* Fix history import for datasets with tags + `Pull Request 13134`_ +* Add more required dependencies for installed Galaxy + `Pull Request 13143`_ +* Path resolution fixes for installed Galaxy + `Pull Request 13147`_ +* Limit lifetime of repository cache + `Pull Request 13154`_ +* converting forward and reverse filters to reactive Vue array + (thanks to `@assuntad23 `__). + `Pull Request 13166`_ +* Fix stdio parsing + (thanks to `@bernt-matthias `__). + `Pull Request 13178`_ +* Catch exceptions when job.user is None + (thanks to `@rhpvorderman `__). + `Pull Request 13179`_ +* Fix redirect to login page when login is required to perform operations + `Pull Request 13188`_ +* Fix workflow invocation name overflow + (thanks to `@davelopez `__). + `Pull Request 13193`_ +* Fix metadata source access in tool shed + `Pull Request 13202`_ +* Fix error getting toolshed repo metadata when no repo found + (thanks to `@simonbray `__). + `Pull Request 13203`_ +* Bugfix for allowing +'s in tool urls + `Pull Request 13204`_ +* Fix test_deserialize_* unit tests + (thanks to `@nsoranzo `__). + `Pull Request 13208`_ +* Fix hide collections in batch + (thanks to `@davelopez `__). + `Pull Request 13214`_ +* Handle unique tag name constraints + `Pull Request 13216`_ +* Ensure that jQuery updates drop target locations during dragging + `Pull Request 13217`_ +* Fix adding new files via tarball upload + `Pull Request 13230`_ +* Initialize error attribute in input model for workflows + `Pull Request 13246`_ +* Fix imported sub-workflows cannot be edited + (thanks to `@davelopez `__). + `Pull Request 13254`_ +* Fix expect_test_failure for expect_num_outputs + (thanks to `@bernt-matthias `__). + `Pull Request 13265`_ +* Fix gravity requirement for Poetry + (thanks to `@nsoranzo `__). + `Pull Request 13278`_ +* Fix dataset details and drag and drop + `Pull Request 13290`_ +* Fix stopping ITs on slurm + `Pull Request 13304`_ +* Ensure Notification API is available prior to trying to use it + `Pull Request 13322`_ +* Fix toolbox handling of spaces in old tools + `Pull Request 13347`_ +* Fix macro parsing + (thanks to `@bernt-matthias `__). + `Pull Request 13373`_ +* Clarify that history exports are not kept forever + `Pull Request 13389`_ +* Fix history search help popover not closing on click outside + (thanks to `@davelopez `__). + `Pull Request 13390`_ +* Ensure $GRAVITY_STATE_DIR is present in venv activate script any time run.sh/common_startup.sh is called normally + `Pull Request 13392`_ +* Use a2wsgi to serve WSGI app + `Pull Request 13396`_ +* Dataset annotations are optional and should not be required + `Pull Request 13420`_ +* Fix repository uploads with remove_repo_files_not_in_tar + `Pull Request 13425`_ +* Wrap reports app in static, fix tool shed entry point + `Pull Request 13430`_ +* enabling Reset button for Rule Builder tab + (thanks to `@assuntad23 `__). + `Pull Request 13443`_ +* Initial fix for quay.io repo query issue + `Pull Request 13468`_ +* Add profile version to ethercalc + `Pull Request 13469`_ +* Backport and fix webapps tests + `Pull Request 13478`_ +* Use timeout instead of interval when polling, optionally terminate polling + `Pull Request 13485`_ +* Pin corresponding playbook repo to sync with Galaxy release + (thanks to `@nuwang `__). + `Pull Request 13497`_ + +.. include:: 22.01_prs.rst + diff --git a/doc/source/releases/22.01_announce.rst b/doc/source/releases/22.01_announce.rst new file mode 100644 index 00000000000..26dd3a9621d --- /dev/null +++ b/doc/source/releases/22.01_announce.rst @@ -0,0 +1,125 @@ + +=========================================================== +January 2022 Galaxy Release (v 22.01) +=========================================================== + +.. include:: _header.rst + +Highlights +=========================================================== + +**Galaxy starts as FastAPI application by default** + Starting Galaxy via ``run.sh`` will use the new `gravity process manager + `__. + The new configuration uses `gunicorn `__ and `FastAPI `__ to drive Galaxy's web process and starts job handler and `Celery `__ processes automatically. + For more details and instructions please consult the `Migrating to Gunicorn documentation `__. + (`Pull Request 13224`_). + +**User Preferences can be encrypted in Galaxy Vault** + Galaxy can now be configured to store secrets in an external vault, which is useful for secure handling and centralization of secrets management. + In particular, information fields in the "Manage information" section of the user profile, such as dropbox keys, can be configured to be encrypted + at rest in a vault (Hashicorp, Custos or database) instead of being stored as plain text in the user preferences table. For detailed information on + configuration, refer to the `vault section `__ of the admin documentation. + (`Pull Request 12940`_). + +**Resumable uploads via tus.io, improved upload provenance** + Galaxy now supports chunked and resumable uploads via the `tus protocol `__. + This is more performant and easier to setup than the current nginx-based + solutions. For detailed configuration instructions, see the `documentation `__. + Galaxy now displays all manipulations that were performed during an upload, + such as newline conversions, converting spaces to tabs or dataset grooming. + (`Pull Request 12656`_, `Pull Request 12861`_, `Pull Request 13313`_, `Pull Request 12912`_). + +Also check out the `22.01 user release notes <22.01_announce_user.html>`__ + +Get Galaxy +========== + +The code lives at `GitHub `__ and you should have `Git `__ to obtain it. + +To get a new Galaxy repository run: + .. code-block:: shell + + $ git clone -b release_22.01 https://github.com/galaxyproject/galaxy.git + +To update an existing Galaxy repository run: + .. code-block:: shell + + $ git fetch origin && git checkout release_22.01 && git pull --ff-only origin release_22.01 + +See the `community hub `__ for additional details on source code locations. + +Deprecation Notices +=========================================================== +PostgreSQL 9.x is now EOL and is no longer supported. Please upgrade to PostgreSQL 10 or higher. +Python 3.6 is officially EOL and Galaxy no longer tests or maintains compatibility. +This release will be the last Galaxy release that can run under Paste or uWSGI. +Please consult the `Migrating to Gunicorn documentation `__ for . +instructions on how to upgrade. + +Configuration Changes +===================== + +Added +----- + +The following configuration options are new + +config/galaxy.yml.sample:galaxy +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- mapping.galaxy.mapping.enable_tool_source_display +- mapping.galaxy.mapping.galaxy_url_prefix +- mapping.galaxy.mapping.load_tool_shed_datatypes +- mapping.galaxy.mapping.max_discovered_files +- mapping.galaxy.mapping.maximum_upload_file_size +- mapping.galaxy.mapping.tool_evaluation_strategy +- mapping.galaxy.mapping.tus_upload_store +- mapping.galaxy.mapping.vault_config_file + + +Changed +------- + +The following configuration options have been changed + +config/galaxy.yml.sample:galaxy +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- mapping.reports.mapping.file_path.default has changed from + + :: + + database/files + + to + + :: + + database/objects + + + + +Removed +------- + +The following configuration options have been completely removed + +config/galaxy.yml.sample:galaxy +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- mapping.galaxy.mapping.check_migrate_tools +- mapping.galaxy.mapping.enable_openid +- mapping.galaxy.mapping.force_beta_workflow_scheduled_for_collections +- mapping.galaxy.mapping.force_beta_workflow_scheduled_min_steps +- mapping.galaxy.mapping.openid_consumer_cache_path + + +Release Notes +=========================================================== + +.. include:: 22.01.rst + :start-after: announce_start + +.. include:: _thanks.rst diff --git a/doc/source/releases/22.01_announce_user.rst b/doc/source/releases/22.01_announce_user.rst new file mode 100644 index 00000000000..c598d9c8743 --- /dev/null +++ b/doc/source/releases/22.01_announce_user.rst @@ -0,0 +1,155 @@ + +=========================================================== +January 2022 Galaxy Release (v 22.01) +=========================================================== + +.. include:: _header.rst + +Highlights +=========================================================== + +**New Colour Selector** + +Do you use tools which require colour inputs, like Circos? Previously we had a restricted colour input which gave you a very limited palette. Now, you have complete freedom of choice with the modern colour selector. + +.. raw:: html + + + + +**Improved File Export** + +If you've been exporting files from Galaxy lately, you've probably seen the amazing new remote file source export, which allows you to export files to FTP, Dropbox, and other locations. + +.. raw:: html + + + +**Improved File Uploads** + +Previously, Galaxy servers used a variety of methods to let you upload large files easily, including some servers which required FTP for large files. We have replaced this with a new upload method, which will be enabled on all usegalaxy servers soon. This will make file uploads significantly smoother, and will be more tolerant of network failures and interruptions! You do not need to make any changes. See `Pull Request 12656`_ for more information. + +**Beta History: Collection Improvements** + +If you've been trying out the beta history (which will be the default history next release!), it has been updated to indicate whether collections are homogeneous or heterogeneous. This will help you see more easily if you've accidentally included an incorrect dataset. + +.. figure:: https://user-images.githubusercontent.com/46503462/146426341-d16d07d8-164b-40ef-976a-52e73d94bfc9.png + :alt: Screenshot of a history named collection tests history using the new beta history panel. Several collections are shown below, some labelled "A list of 6 txt datasets" and some labelled "a list of 3 datasets", without a datatype shown. + + +New Visualizations +=========================================================== + +.. visualizations + +Thanks to `@neoformit `__, a new R shiny app "iSEE" has been added, which provides a UI for visualization of single-cell RNA datasets. See `Pull Request 12650`_ for more information + +.. figure:: https://user-images.githubusercontent.com/42562517/136135157-40a46a31-3553-49b3-a975-b518db1374b2.png + :alt: screenshot of the iSEE app, on the left is a colourful scatterplot with dots grouped, labelled reduced dimension plot. On the right is a plot labelled feature assay plot with a number of violin plots. Both plots feature controls at the bottom for modifying the visualisation's parameters. + +New Datatypes +=========================================================== + +.. datatypes +* Add datatype iml_dataset subclassing html + (thanks to `@knutwa-ext `__). + `Pull Request 12099`_ +* Create sniffer for GRO file type + (thanks to `@simonbray `__). + `Pull Request 12885`_ +* Agp datatype + `Pull Request 12891`_ +* Add ludwig model datatype + (thanks to `@qiagu `__). + `Pull Request 13073`_ +* Add ampvis2 datatype + (thanks to `@bernt-matthias `__). + `Pull Request 13083`_ +* Add Neper and Gmsh datatypes + (thanks to `@jj-umn `__). + `Pull Request 13087`_ +* Add new datatype GRIB + (thanks to `@annefou `__). + `Pull Request 13175`_ +* Show yaml files in preview + `Pull Request 13176`_ +* Enable display of BAM data without sequence info + (thanks to `@wm75 `__). + `Pull Request 13219`_ +* Add gz as datatype + (thanks to `@astrovsky01 `__). + `Pull Request 13271`_ +* Add phyloseq datatype + (thanks to `@bernt-matthias `__). + `Pull Request 13462`_ + +Builtin Tool Updates +=========================================================== + +.. tools +* New interactive tool - iSEE + (thanks to `@neoformit `__). + `Pull Request 12650`_ +* Ignore type in unused function + `Pull Request 12694`_ +* Merge 21.09 -> dev + `Pull Request 12747`_ +* Set profile version of ncbi_datasets so it can run containerized + `Pull Request 12819`_ +* Downgrade bowtie requirement for CONVERTER_fasta_to_bowtie_color_index + (thanks to `@nsoranzo `__). + `Pull Request 12851`_ +* Fix containerized NCBI Datasets Genomes data source tool + `Pull Request 12915`_ +* Tighten type ignores + (thanks to `@common-workflow-lab `__). + `Pull Request 12965`_ +* Update interactivetool_pyiron.xml + (thanks to `@gmauro `__). + `Pull Request 12990`_ +* Don't use triple-quoted strings as comments + (thanks to `@nsoranzo `__). + `Pull Request 13007`_ +* Add parquet to csv datatype converter + `Pull Request 13078`_ +* Drop tool migration scripts and related code + `Pull Request 13099`_ +* Allow any format in export tool + (thanks to `@astrovsky01 `__). + `Pull Request 13151`_ +* Add dynamic step to FormNumber range slider + (thanks to `@davelopez `__). + `Pull Request 13233`_ +* FormNumber: notify value change on slider change + (thanks to `@davelopez `__). + `Pull Request 13260`_ +* Add bioconductor as valid xref for tools + (thanks to `@bgruening `__). + `Pull Request 13268`_ + +Release Testing Team +=========================================================== + +A special thanks to the release testing team for testing many of the new features and reporting many bugs: + +- `Keith Suderman `_ +- `Delphine Lariviere `_ +- `Jennifer Hillman-Jackson `_ +- `Natalie Kucher `_ +- `Alex Ostrovsky `_ +- `Enis Afgan `_ + +Release Notes +=========================================================== + +User facing release notes compiled by `Helena Rasche `_. + +Please see the `full release notes <22.01_announce.html>`_ for more details. + +.. include:: 22.01_prs.rst + +.. include:: _thanks.rst diff --git a/doc/source/releases/22.01_prs.rst b/doc/source/releases/22.01_prs.rst new file mode 100644 index 00000000000..3d1391e7324 --- /dev/null +++ b/doc/source/releases/22.01_prs.rst @@ -0,0 +1,425 @@ + +.. github_links +.. _Pull Request 10304: https://github.com/galaxyproject/galaxy/pull/10304 +.. _Pull Request 11710: https://github.com/galaxyproject/galaxy/pull/11710 +.. _Pull Request 12099: https://github.com/galaxyproject/galaxy/pull/12099 +.. _Pull Request 12345: https://github.com/galaxyproject/galaxy/pull/12345 +.. _Pull Request 12386: https://github.com/galaxyproject/galaxy/pull/12386 +.. _Pull Request 12428: https://github.com/galaxyproject/galaxy/pull/12428 +.. _Pull Request 12459: https://github.com/galaxyproject/galaxy/pull/12459 +.. _Pull Request 12468: https://github.com/galaxyproject/galaxy/pull/12468 +.. _Pull Request 12500: https://github.com/galaxyproject/galaxy/pull/12500 +.. _Pull Request 12510: https://github.com/galaxyproject/galaxy/pull/12510 +.. _Pull Request 12512: https://github.com/galaxyproject/galaxy/pull/12512 +.. _Pull Request 12513: https://github.com/galaxyproject/galaxy/pull/12513 +.. _Pull Request 12516: https://github.com/galaxyproject/galaxy/pull/12516 +.. _Pull Request 12518: https://github.com/galaxyproject/galaxy/pull/12518 +.. _Pull Request 12522: https://github.com/galaxyproject/galaxy/pull/12522 +.. _Pull Request 12523: https://github.com/galaxyproject/galaxy/pull/12523 +.. _Pull Request 12524: https://github.com/galaxyproject/galaxy/pull/12524 +.. _Pull Request 12527: https://github.com/galaxyproject/galaxy/pull/12527 +.. _Pull Request 12528: https://github.com/galaxyproject/galaxy/pull/12528 +.. _Pull Request 12531: https://github.com/galaxyproject/galaxy/pull/12531 +.. _Pull Request 12537: https://github.com/galaxyproject/galaxy/pull/12537 +.. _Pull Request 12538: https://github.com/galaxyproject/galaxy/pull/12538 +.. _Pull Request 12540: https://github.com/galaxyproject/galaxy/pull/12540 +.. _Pull Request 12544: https://github.com/galaxyproject/galaxy/pull/12544 +.. _Pull Request 12545: https://github.com/galaxyproject/galaxy/pull/12545 +.. _Pull Request 12546: https://github.com/galaxyproject/galaxy/pull/12546 +.. _Pull Request 12547: https://github.com/galaxyproject/galaxy/pull/12547 +.. _Pull Request 12548: https://github.com/galaxyproject/galaxy/pull/12548 +.. _Pull Request 12549: https://github.com/galaxyproject/galaxy/pull/12549 +.. _Pull Request 12550: https://github.com/galaxyproject/galaxy/pull/12550 +.. _Pull Request 12551: https://github.com/galaxyproject/galaxy/pull/12551 +.. _Pull Request 12552: https://github.com/galaxyproject/galaxy/pull/12552 +.. _Pull Request 12554: https://github.com/galaxyproject/galaxy/pull/12554 +.. _Pull Request 12555: https://github.com/galaxyproject/galaxy/pull/12555 +.. _Pull Request 12556: https://github.com/galaxyproject/galaxy/pull/12556 +.. _Pull Request 12560: https://github.com/galaxyproject/galaxy/pull/12560 +.. _Pull Request 12569: https://github.com/galaxyproject/galaxy/pull/12569 +.. _Pull Request 12571: https://github.com/galaxyproject/galaxy/pull/12571 +.. _Pull Request 12573: https://github.com/galaxyproject/galaxy/pull/12573 +.. _Pull Request 12578: https://github.com/galaxyproject/galaxy/pull/12578 +.. _Pull Request 12580: https://github.com/galaxyproject/galaxy/pull/12580 +.. _Pull Request 12608: https://github.com/galaxyproject/galaxy/pull/12608 +.. _Pull Request 12626: https://github.com/galaxyproject/galaxy/pull/12626 +.. _Pull Request 12632: https://github.com/galaxyproject/galaxy/pull/12632 +.. _Pull Request 12636: https://github.com/galaxyproject/galaxy/pull/12636 +.. _Pull Request 12638: https://github.com/galaxyproject/galaxy/pull/12638 +.. _Pull Request 12642: https://github.com/galaxyproject/galaxy/pull/12642 +.. _Pull Request 12644: https://github.com/galaxyproject/galaxy/pull/12644 +.. _Pull Request 12646: https://github.com/galaxyproject/galaxy/pull/12646 +.. _Pull Request 12647: https://github.com/galaxyproject/galaxy/pull/12647 +.. _Pull Request 12649: https://github.com/galaxyproject/galaxy/pull/12649 +.. _Pull Request 12650: https://github.com/galaxyproject/galaxy/pull/12650 +.. _Pull Request 12652: https://github.com/galaxyproject/galaxy/pull/12652 +.. _Pull Request 12656: https://github.com/galaxyproject/galaxy/pull/12656 +.. _Pull Request 12659: https://github.com/galaxyproject/galaxy/pull/12659 +.. _Pull Request 12660: https://github.com/galaxyproject/galaxy/pull/12660 +.. _Pull Request 12664: https://github.com/galaxyproject/galaxy/pull/12664 +.. _Pull Request 12665: https://github.com/galaxyproject/galaxy/pull/12665 +.. _Pull Request 12666: https://github.com/galaxyproject/galaxy/pull/12666 +.. _Pull Request 12668: https://github.com/galaxyproject/galaxy/pull/12668 +.. _Pull Request 12671: https://github.com/galaxyproject/galaxy/pull/12671 +.. _Pull Request 12673: https://github.com/galaxyproject/galaxy/pull/12673 +.. _Pull Request 12674: https://github.com/galaxyproject/galaxy/pull/12674 +.. _Pull Request 12676: https://github.com/galaxyproject/galaxy/pull/12676 +.. _Pull Request 12685: https://github.com/galaxyproject/galaxy/pull/12685 +.. _Pull Request 12686: https://github.com/galaxyproject/galaxy/pull/12686 +.. _Pull Request 12687: https://github.com/galaxyproject/galaxy/pull/12687 +.. _Pull Request 12691: https://github.com/galaxyproject/galaxy/pull/12691 +.. _Pull Request 12694: https://github.com/galaxyproject/galaxy/pull/12694 +.. _Pull Request 12695: https://github.com/galaxyproject/galaxy/pull/12695 +.. _Pull Request 12696: https://github.com/galaxyproject/galaxy/pull/12696 +.. _Pull Request 12697: https://github.com/galaxyproject/galaxy/pull/12697 +.. _Pull Request 12698: https://github.com/galaxyproject/galaxy/pull/12698 +.. _Pull Request 12700: https://github.com/galaxyproject/galaxy/pull/12700 +.. _Pull Request 12701: https://github.com/galaxyproject/galaxy/pull/12701 +.. _Pull Request 12703: https://github.com/galaxyproject/galaxy/pull/12703 +.. _Pull Request 12706: https://github.com/galaxyproject/galaxy/pull/12706 +.. _Pull Request 12707: https://github.com/galaxyproject/galaxy/pull/12707 +.. _Pull Request 12710: https://github.com/galaxyproject/galaxy/pull/12710 +.. _Pull Request 12711: https://github.com/galaxyproject/galaxy/pull/12711 +.. _Pull Request 12723: https://github.com/galaxyproject/galaxy/pull/12723 +.. _Pull Request 12736: https://github.com/galaxyproject/galaxy/pull/12736 +.. _Pull Request 12739: https://github.com/galaxyproject/galaxy/pull/12739 +.. _Pull Request 12744: https://github.com/galaxyproject/galaxy/pull/12744 +.. _Pull Request 12747: https://github.com/galaxyproject/galaxy/pull/12747 +.. _Pull Request 12752: https://github.com/galaxyproject/galaxy/pull/12752 +.. _Pull Request 12754: https://github.com/galaxyproject/galaxy/pull/12754 +.. _Pull Request 12755: https://github.com/galaxyproject/galaxy/pull/12755 +.. _Pull Request 12756: https://github.com/galaxyproject/galaxy/pull/12756 +.. _Pull Request 12759: https://github.com/galaxyproject/galaxy/pull/12759 +.. _Pull Request 12765: https://github.com/galaxyproject/galaxy/pull/12765 +.. _Pull Request 12773: https://github.com/galaxyproject/galaxy/pull/12773 +.. _Pull Request 12774: https://github.com/galaxyproject/galaxy/pull/12774 +.. _Pull Request 12778: https://github.com/galaxyproject/galaxy/pull/12778 +.. _Pull Request 12780: https://github.com/galaxyproject/galaxy/pull/12780 +.. _Pull Request 12781: https://github.com/galaxyproject/galaxy/pull/12781 +.. _Pull Request 12783: https://github.com/galaxyproject/galaxy/pull/12783 +.. _Pull Request 12787: https://github.com/galaxyproject/galaxy/pull/12787 +.. _Pull Request 12788: https://github.com/galaxyproject/galaxy/pull/12788 +.. _Pull Request 12791: https://github.com/galaxyproject/galaxy/pull/12791 +.. _Pull Request 12797: https://github.com/galaxyproject/galaxy/pull/12797 +.. _Pull Request 12801: https://github.com/galaxyproject/galaxy/pull/12801 +.. _Pull Request 12802: https://github.com/galaxyproject/galaxy/pull/12802 +.. _Pull Request 12810: https://github.com/galaxyproject/galaxy/pull/12810 +.. _Pull Request 12811: https://github.com/galaxyproject/galaxy/pull/12811 +.. _Pull Request 12812: https://github.com/galaxyproject/galaxy/pull/12812 +.. _Pull Request 12813: https://github.com/galaxyproject/galaxy/pull/12813 +.. _Pull Request 12819: https://github.com/galaxyproject/galaxy/pull/12819 +.. _Pull Request 12820: https://github.com/galaxyproject/galaxy/pull/12820 +.. _Pull Request 12826: https://github.com/galaxyproject/galaxy/pull/12826 +.. _Pull Request 12829: https://github.com/galaxyproject/galaxy/pull/12829 +.. _Pull Request 12831: https://github.com/galaxyproject/galaxy/pull/12831 +.. _Pull Request 12832: https://github.com/galaxyproject/galaxy/pull/12832 +.. _Pull Request 12833: https://github.com/galaxyproject/galaxy/pull/12833 +.. _Pull Request 12836: https://github.com/galaxyproject/galaxy/pull/12836 +.. _Pull Request 12838: https://github.com/galaxyproject/galaxy/pull/12838 +.. _Pull Request 12840: https://github.com/galaxyproject/galaxy/pull/12840 +.. _Pull Request 12841: https://github.com/galaxyproject/galaxy/pull/12841 +.. _Pull Request 12843: https://github.com/galaxyproject/galaxy/pull/12843 +.. _Pull Request 12844: https://github.com/galaxyproject/galaxy/pull/12844 +.. _Pull Request 12845: https://github.com/galaxyproject/galaxy/pull/12845 +.. _Pull Request 12847: https://github.com/galaxyproject/galaxy/pull/12847 +.. _Pull Request 12849: https://github.com/galaxyproject/galaxy/pull/12849 +.. _Pull Request 12851: https://github.com/galaxyproject/galaxy/pull/12851 +.. _Pull Request 12856: https://github.com/galaxyproject/galaxy/pull/12856 +.. _Pull Request 12858: https://github.com/galaxyproject/galaxy/pull/12858 +.. _Pull Request 12860: https://github.com/galaxyproject/galaxy/pull/12860 +.. _Pull Request 12861: https://github.com/galaxyproject/galaxy/pull/12861 +.. _Pull Request 12867: https://github.com/galaxyproject/galaxy/pull/12867 +.. _Pull Request 12868: https://github.com/galaxyproject/galaxy/pull/12868 +.. _Pull Request 12883: https://github.com/galaxyproject/galaxy/pull/12883 +.. _Pull Request 12885: https://github.com/galaxyproject/galaxy/pull/12885 +.. _Pull Request 12886: https://github.com/galaxyproject/galaxy/pull/12886 +.. _Pull Request 12888: https://github.com/galaxyproject/galaxy/pull/12888 +.. _Pull Request 12889: https://github.com/galaxyproject/galaxy/pull/12889 +.. _Pull Request 12890: https://github.com/galaxyproject/galaxy/pull/12890 +.. _Pull Request 12891: https://github.com/galaxyproject/galaxy/pull/12891 +.. _Pull Request 12892: https://github.com/galaxyproject/galaxy/pull/12892 +.. _Pull Request 12893: https://github.com/galaxyproject/galaxy/pull/12893 +.. _Pull Request 12894: https://github.com/galaxyproject/galaxy/pull/12894 +.. _Pull Request 12895: https://github.com/galaxyproject/galaxy/pull/12895 +.. _Pull Request 12896: https://github.com/galaxyproject/galaxy/pull/12896 +.. _Pull Request 12897: https://github.com/galaxyproject/galaxy/pull/12897 +.. _Pull Request 12898: https://github.com/galaxyproject/galaxy/pull/12898 +.. _Pull Request 12899: https://github.com/galaxyproject/galaxy/pull/12899 +.. _Pull Request 12900: https://github.com/galaxyproject/galaxy/pull/12900 +.. _Pull Request 12902: https://github.com/galaxyproject/galaxy/pull/12902 +.. _Pull Request 12904: https://github.com/galaxyproject/galaxy/pull/12904 +.. _Pull Request 12905: https://github.com/galaxyproject/galaxy/pull/12905 +.. _Pull Request 12906: https://github.com/galaxyproject/galaxy/pull/12906 +.. _Pull Request 12908: https://github.com/galaxyproject/galaxy/pull/12908 +.. _Pull Request 12910: https://github.com/galaxyproject/galaxy/pull/12910 +.. _Pull Request 12912: https://github.com/galaxyproject/galaxy/pull/12912 +.. _Pull Request 12913: https://github.com/galaxyproject/galaxy/pull/12913 +.. _Pull Request 12914: https://github.com/galaxyproject/galaxy/pull/12914 +.. _Pull Request 12915: https://github.com/galaxyproject/galaxy/pull/12915 +.. _Pull Request 12916: https://github.com/galaxyproject/galaxy/pull/12916 +.. _Pull Request 12919: https://github.com/galaxyproject/galaxy/pull/12919 +.. _Pull Request 12920: https://github.com/galaxyproject/galaxy/pull/12920 +.. _Pull Request 12921: https://github.com/galaxyproject/galaxy/pull/12921 +.. _Pull Request 12923: https://github.com/galaxyproject/galaxy/pull/12923 +.. _Pull Request 12924: https://github.com/galaxyproject/galaxy/pull/12924 +.. _Pull Request 12926: https://github.com/galaxyproject/galaxy/pull/12926 +.. _Pull Request 12929: https://github.com/galaxyproject/galaxy/pull/12929 +.. _Pull Request 12930: https://github.com/galaxyproject/galaxy/pull/12930 +.. _Pull Request 12931: https://github.com/galaxyproject/galaxy/pull/12931 +.. _Pull Request 12932: https://github.com/galaxyproject/galaxy/pull/12932 +.. _Pull Request 12935: https://github.com/galaxyproject/galaxy/pull/12935 +.. _Pull Request 12936: https://github.com/galaxyproject/galaxy/pull/12936 +.. _Pull Request 12937: https://github.com/galaxyproject/galaxy/pull/12937 +.. _Pull Request 12939: https://github.com/galaxyproject/galaxy/pull/12939 +.. _Pull Request 12940: https://github.com/galaxyproject/galaxy/pull/12940 +.. _Pull Request 12942: https://github.com/galaxyproject/galaxy/pull/12942 +.. _Pull Request 12944: https://github.com/galaxyproject/galaxy/pull/12944 +.. _Pull Request 12945: https://github.com/galaxyproject/galaxy/pull/12945 +.. _Pull Request 12946: https://github.com/galaxyproject/galaxy/pull/12946 +.. _Pull Request 12947: https://github.com/galaxyproject/galaxy/pull/12947 +.. _Pull Request 12948: https://github.com/galaxyproject/galaxy/pull/12948 +.. _Pull Request 12949: https://github.com/galaxyproject/galaxy/pull/12949 +.. _Pull Request 12950: https://github.com/galaxyproject/galaxy/pull/12950 +.. _Pull Request 12951: https://github.com/galaxyproject/galaxy/pull/12951 +.. _Pull Request 12954: https://github.com/galaxyproject/galaxy/pull/12954 +.. _Pull Request 12956: https://github.com/galaxyproject/galaxy/pull/12956 +.. _Pull Request 12960: https://github.com/galaxyproject/galaxy/pull/12960 +.. _Pull Request 12962: https://github.com/galaxyproject/galaxy/pull/12962 +.. _Pull Request 12963: https://github.com/galaxyproject/galaxy/pull/12963 +.. _Pull Request 12965: https://github.com/galaxyproject/galaxy/pull/12965 +.. _Pull Request 12966: https://github.com/galaxyproject/galaxy/pull/12966 +.. _Pull Request 12968: https://github.com/galaxyproject/galaxy/pull/12968 +.. _Pull Request 12969: https://github.com/galaxyproject/galaxy/pull/12969 +.. _Pull Request 12972: https://github.com/galaxyproject/galaxy/pull/12972 +.. _Pull Request 12973: https://github.com/galaxyproject/galaxy/pull/12973 +.. _Pull Request 12974: https://github.com/galaxyproject/galaxy/pull/12974 +.. _Pull Request 12976: https://github.com/galaxyproject/galaxy/pull/12976 +.. _Pull Request 12977: https://github.com/galaxyproject/galaxy/pull/12977 +.. _Pull Request 12978: https://github.com/galaxyproject/galaxy/pull/12978 +.. _Pull Request 12980: https://github.com/galaxyproject/galaxy/pull/12980 +.. _Pull Request 12982: https://github.com/galaxyproject/galaxy/pull/12982 +.. _Pull Request 12983: https://github.com/galaxyproject/galaxy/pull/12983 +.. _Pull Request 12984: https://github.com/galaxyproject/galaxy/pull/12984 +.. _Pull Request 12985: https://github.com/galaxyproject/galaxy/pull/12985 +.. _Pull Request 12989: https://github.com/galaxyproject/galaxy/pull/12989 +.. _Pull Request 12990: https://github.com/galaxyproject/galaxy/pull/12990 +.. _Pull Request 12991: https://github.com/galaxyproject/galaxy/pull/12991 +.. _Pull Request 12993: https://github.com/galaxyproject/galaxy/pull/12993 +.. _Pull Request 12994: https://github.com/galaxyproject/galaxy/pull/12994 +.. _Pull Request 12996: https://github.com/galaxyproject/galaxy/pull/12996 +.. _Pull Request 13000: https://github.com/galaxyproject/galaxy/pull/13000 +.. _Pull Request 13002: https://github.com/galaxyproject/galaxy/pull/13002 +.. _Pull Request 13003: https://github.com/galaxyproject/galaxy/pull/13003 +.. _Pull Request 13007: https://github.com/galaxyproject/galaxy/pull/13007 +.. _Pull Request 13009: https://github.com/galaxyproject/galaxy/pull/13009 +.. _Pull Request 13010: https://github.com/galaxyproject/galaxy/pull/13010 +.. _Pull Request 13011: https://github.com/galaxyproject/galaxy/pull/13011 +.. _Pull Request 13013: https://github.com/galaxyproject/galaxy/pull/13013 +.. _Pull Request 13017: https://github.com/galaxyproject/galaxy/pull/13017 +.. _Pull Request 13018: https://github.com/galaxyproject/galaxy/pull/13018 +.. _Pull Request 13021: https://github.com/galaxyproject/galaxy/pull/13021 +.. _Pull Request 13024: https://github.com/galaxyproject/galaxy/pull/13024 +.. _Pull Request 13025: https://github.com/galaxyproject/galaxy/pull/13025 +.. _Pull Request 13026: https://github.com/galaxyproject/galaxy/pull/13026 +.. _Pull Request 13029: https://github.com/galaxyproject/galaxy/pull/13029 +.. _Pull Request 13032: https://github.com/galaxyproject/galaxy/pull/13032 +.. _Pull Request 13033: https://github.com/galaxyproject/galaxy/pull/13033 +.. _Pull Request 13034: https://github.com/galaxyproject/galaxy/pull/13034 +.. _Pull Request 13035: https://github.com/galaxyproject/galaxy/pull/13035 +.. _Pull Request 13036: https://github.com/galaxyproject/galaxy/pull/13036 +.. _Pull Request 13038: https://github.com/galaxyproject/galaxy/pull/13038 +.. _Pull Request 13039: https://github.com/galaxyproject/galaxy/pull/13039 +.. _Pull Request 13041: https://github.com/galaxyproject/galaxy/pull/13041 +.. _Pull Request 13044: https://github.com/galaxyproject/galaxy/pull/13044 +.. _Pull Request 13047: https://github.com/galaxyproject/galaxy/pull/13047 +.. _Pull Request 13048: https://github.com/galaxyproject/galaxy/pull/13048 +.. _Pull Request 13049: https://github.com/galaxyproject/galaxy/pull/13049 +.. _Pull Request 13050: https://github.com/galaxyproject/galaxy/pull/13050 +.. _Pull Request 13051: https://github.com/galaxyproject/galaxy/pull/13051 +.. _Pull Request 13055: https://github.com/galaxyproject/galaxy/pull/13055 +.. _Pull Request 13056: https://github.com/galaxyproject/galaxy/pull/13056 +.. _Pull Request 13057: https://github.com/galaxyproject/galaxy/pull/13057 +.. _Pull Request 13058: https://github.com/galaxyproject/galaxy/pull/13058 +.. _Pull Request 13059: https://github.com/galaxyproject/galaxy/pull/13059 +.. _Pull Request 13060: https://github.com/galaxyproject/galaxy/pull/13060 +.. _Pull Request 13061: https://github.com/galaxyproject/galaxy/pull/13061 +.. _Pull Request 13062: https://github.com/galaxyproject/galaxy/pull/13062 +.. _Pull Request 13063: https://github.com/galaxyproject/galaxy/pull/13063 +.. _Pull Request 13065: https://github.com/galaxyproject/galaxy/pull/13065 +.. _Pull Request 13067: https://github.com/galaxyproject/galaxy/pull/13067 +.. _Pull Request 13068: https://github.com/galaxyproject/galaxy/pull/13068 +.. _Pull Request 13069: https://github.com/galaxyproject/galaxy/pull/13069 +.. _Pull Request 13072: https://github.com/galaxyproject/galaxy/pull/13072 +.. _Pull Request 13073: https://github.com/galaxyproject/galaxy/pull/13073 +.. _Pull Request 13074: https://github.com/galaxyproject/galaxy/pull/13074 +.. _Pull Request 13076: https://github.com/galaxyproject/galaxy/pull/13076 +.. _Pull Request 13078: https://github.com/galaxyproject/galaxy/pull/13078 +.. _Pull Request 13080: https://github.com/galaxyproject/galaxy/pull/13080 +.. _Pull Request 13081: https://github.com/galaxyproject/galaxy/pull/13081 +.. _Pull Request 13082: https://github.com/galaxyproject/galaxy/pull/13082 +.. _Pull Request 13083: https://github.com/galaxyproject/galaxy/pull/13083 +.. _Pull Request 13086: https://github.com/galaxyproject/galaxy/pull/13086 +.. _Pull Request 13087: https://github.com/galaxyproject/galaxy/pull/13087 +.. _Pull Request 13088: https://github.com/galaxyproject/galaxy/pull/13088 +.. _Pull Request 13092: https://github.com/galaxyproject/galaxy/pull/13092 +.. _Pull Request 13093: https://github.com/galaxyproject/galaxy/pull/13093 +.. _Pull Request 13094: https://github.com/galaxyproject/galaxy/pull/13094 +.. _Pull Request 13096: https://github.com/galaxyproject/galaxy/pull/13096 +.. _Pull Request 13097: https://github.com/galaxyproject/galaxy/pull/13097 +.. _Pull Request 13098: https://github.com/galaxyproject/galaxy/pull/13098 +.. _Pull Request 13099: https://github.com/galaxyproject/galaxy/pull/13099 +.. _Pull Request 13102: https://github.com/galaxyproject/galaxy/pull/13102 +.. _Pull Request 13103: https://github.com/galaxyproject/galaxy/pull/13103 +.. _Pull Request 13104: https://github.com/galaxyproject/galaxy/pull/13104 +.. _Pull Request 13107: https://github.com/galaxyproject/galaxy/pull/13107 +.. _Pull Request 13112: https://github.com/galaxyproject/galaxy/pull/13112 +.. _Pull Request 13114: https://github.com/galaxyproject/galaxy/pull/13114 +.. _Pull Request 13116: https://github.com/galaxyproject/galaxy/pull/13116 +.. _Pull Request 13118: https://github.com/galaxyproject/galaxy/pull/13118 +.. _Pull Request 13120: https://github.com/galaxyproject/galaxy/pull/13120 +.. _Pull Request 13121: https://github.com/galaxyproject/galaxy/pull/13121 +.. _Pull Request 13123: https://github.com/galaxyproject/galaxy/pull/13123 +.. _Pull Request 13124: https://github.com/galaxyproject/galaxy/pull/13124 +.. _Pull Request 13125: https://github.com/galaxyproject/galaxy/pull/13125 +.. _Pull Request 13126: https://github.com/galaxyproject/galaxy/pull/13126 +.. _Pull Request 13127: https://github.com/galaxyproject/galaxy/pull/13127 +.. _Pull Request 13128: https://github.com/galaxyproject/galaxy/pull/13128 +.. _Pull Request 13129: https://github.com/galaxyproject/galaxy/pull/13129 +.. _Pull Request 13130: https://github.com/galaxyproject/galaxy/pull/13130 +.. _Pull Request 13134: https://github.com/galaxyproject/galaxy/pull/13134 +.. _Pull Request 13135: https://github.com/galaxyproject/galaxy/pull/13135 +.. _Pull Request 13137: https://github.com/galaxyproject/galaxy/pull/13137 +.. _Pull Request 13139: https://github.com/galaxyproject/galaxy/pull/13139 +.. _Pull Request 13142: https://github.com/galaxyproject/galaxy/pull/13142 +.. _Pull Request 13143: https://github.com/galaxyproject/galaxy/pull/13143 +.. _Pull Request 13147: https://github.com/galaxyproject/galaxy/pull/13147 +.. _Pull Request 13148: https://github.com/galaxyproject/galaxy/pull/13148 +.. _Pull Request 13151: https://github.com/galaxyproject/galaxy/pull/13151 +.. _Pull Request 13152: https://github.com/galaxyproject/galaxy/pull/13152 +.. _Pull Request 13154: https://github.com/galaxyproject/galaxy/pull/13154 +.. _Pull Request 13155: https://github.com/galaxyproject/galaxy/pull/13155 +.. _Pull Request 13156: https://github.com/galaxyproject/galaxy/pull/13156 +.. _Pull Request 13157: https://github.com/galaxyproject/galaxy/pull/13157 +.. _Pull Request 13159: https://github.com/galaxyproject/galaxy/pull/13159 +.. _Pull Request 13162: https://github.com/galaxyproject/galaxy/pull/13162 +.. _Pull Request 13163: https://github.com/galaxyproject/galaxy/pull/13163 +.. _Pull Request 13165: https://github.com/galaxyproject/galaxy/pull/13165 +.. _Pull Request 13166: https://github.com/galaxyproject/galaxy/pull/13166 +.. _Pull Request 13167: https://github.com/galaxyproject/galaxy/pull/13167 +.. _Pull Request 13170: https://github.com/galaxyproject/galaxy/pull/13170 +.. _Pull Request 13171: https://github.com/galaxyproject/galaxy/pull/13171 +.. _Pull Request 13174: https://github.com/galaxyproject/galaxy/pull/13174 +.. _Pull Request 13175: https://github.com/galaxyproject/galaxy/pull/13175 +.. _Pull Request 13176: https://github.com/galaxyproject/galaxy/pull/13176 +.. _Pull Request 13178: https://github.com/galaxyproject/galaxy/pull/13178 +.. _Pull Request 13179: https://github.com/galaxyproject/galaxy/pull/13179 +.. _Pull Request 13182: https://github.com/galaxyproject/galaxy/pull/13182 +.. _Pull Request 13185: https://github.com/galaxyproject/galaxy/pull/13185 +.. _Pull Request 13186: https://github.com/galaxyproject/galaxy/pull/13186 +.. _Pull Request 13188: https://github.com/galaxyproject/galaxy/pull/13188 +.. _Pull Request 13189: https://github.com/galaxyproject/galaxy/pull/13189 +.. _Pull Request 13193: https://github.com/galaxyproject/galaxy/pull/13193 +.. _Pull Request 13196: https://github.com/galaxyproject/galaxy/pull/13196 +.. _Pull Request 13202: https://github.com/galaxyproject/galaxy/pull/13202 +.. _Pull Request 13203: https://github.com/galaxyproject/galaxy/pull/13203 +.. _Pull Request 13204: https://github.com/galaxyproject/galaxy/pull/13204 +.. _Pull Request 13205: https://github.com/galaxyproject/galaxy/pull/13205 +.. _Pull Request 13206: https://github.com/galaxyproject/galaxy/pull/13206 +.. _Pull Request 13207: https://github.com/galaxyproject/galaxy/pull/13207 +.. _Pull Request 13208: https://github.com/galaxyproject/galaxy/pull/13208 +.. _Pull Request 13209: https://github.com/galaxyproject/galaxy/pull/13209 +.. _Pull Request 13214: https://github.com/galaxyproject/galaxy/pull/13214 +.. _Pull Request 13215: https://github.com/galaxyproject/galaxy/pull/13215 +.. _Pull Request 13216: https://github.com/galaxyproject/galaxy/pull/13216 +.. _Pull Request 13217: https://github.com/galaxyproject/galaxy/pull/13217 +.. _Pull Request 13218: https://github.com/galaxyproject/galaxy/pull/13218 +.. _Pull Request 13219: https://github.com/galaxyproject/galaxy/pull/13219 +.. _Pull Request 13223: https://github.com/galaxyproject/galaxy/pull/13223 +.. _Pull Request 13224: https://github.com/galaxyproject/galaxy/pull/13224 +.. _Pull Request 13225: https://github.com/galaxyproject/galaxy/pull/13225 +.. _Pull Request 13226: https://github.com/galaxyproject/galaxy/pull/13226 +.. _Pull Request 13227: https://github.com/galaxyproject/galaxy/pull/13227 +.. _Pull Request 13228: https://github.com/galaxyproject/galaxy/pull/13228 +.. _Pull Request 13230: https://github.com/galaxyproject/galaxy/pull/13230 +.. _Pull Request 13231: https://github.com/galaxyproject/galaxy/pull/13231 +.. _Pull Request 13233: https://github.com/galaxyproject/galaxy/pull/13233 +.. _Pull Request 13236: https://github.com/galaxyproject/galaxy/pull/13236 +.. _Pull Request 13237: https://github.com/galaxyproject/galaxy/pull/13237 +.. _Pull Request 13238: https://github.com/galaxyproject/galaxy/pull/13238 +.. _Pull Request 13246: https://github.com/galaxyproject/galaxy/pull/13246 +.. _Pull Request 13249: https://github.com/galaxyproject/galaxy/pull/13249 +.. _Pull Request 13250: https://github.com/galaxyproject/galaxy/pull/13250 +.. _Pull Request 13251: https://github.com/galaxyproject/galaxy/pull/13251 +.. _Pull Request 13252: https://github.com/galaxyproject/galaxy/pull/13252 +.. _Pull Request 13254: https://github.com/galaxyproject/galaxy/pull/13254 +.. _Pull Request 13260: https://github.com/galaxyproject/galaxy/pull/13260 +.. _Pull Request 13261: https://github.com/galaxyproject/galaxy/pull/13261 +.. _Pull Request 13265: https://github.com/galaxyproject/galaxy/pull/13265 +.. _Pull Request 13267: https://github.com/galaxyproject/galaxy/pull/13267 +.. _Pull Request 13268: https://github.com/galaxyproject/galaxy/pull/13268 +.. _Pull Request 13271: https://github.com/galaxyproject/galaxy/pull/13271 +.. _Pull Request 13273: https://github.com/galaxyproject/galaxy/pull/13273 +.. _Pull Request 13275: https://github.com/galaxyproject/galaxy/pull/13275 +.. _Pull Request 13278: https://github.com/galaxyproject/galaxy/pull/13278 +.. _Pull Request 13281: https://github.com/galaxyproject/galaxy/pull/13281 +.. _Pull Request 13285: https://github.com/galaxyproject/galaxy/pull/13285 +.. _Pull Request 13290: https://github.com/galaxyproject/galaxy/pull/13290 +.. _Pull Request 13292: https://github.com/galaxyproject/galaxy/pull/13292 +.. _Pull Request 13293: https://github.com/galaxyproject/galaxy/pull/13293 +.. _Pull Request 13299: https://github.com/galaxyproject/galaxy/pull/13299 +.. _Pull Request 13301: https://github.com/galaxyproject/galaxy/pull/13301 +.. _Pull Request 13304: https://github.com/galaxyproject/galaxy/pull/13304 +.. _Pull Request 13309: https://github.com/galaxyproject/galaxy/pull/13309 +.. _Pull Request 13313: https://github.com/galaxyproject/galaxy/pull/13313 +.. _Pull Request 13315: https://github.com/galaxyproject/galaxy/pull/13315 +.. _Pull Request 13316: https://github.com/galaxyproject/galaxy/pull/13316 +.. _Pull Request 13317: https://github.com/galaxyproject/galaxy/pull/13317 +.. _Pull Request 13322: https://github.com/galaxyproject/galaxy/pull/13322 +.. _Pull Request 13324: https://github.com/galaxyproject/galaxy/pull/13324 +.. _Pull Request 13325: https://github.com/galaxyproject/galaxy/pull/13325 +.. _Pull Request 13330: https://github.com/galaxyproject/galaxy/pull/13330 +.. _Pull Request 13333: https://github.com/galaxyproject/galaxy/pull/13333 +.. _Pull Request 13343: https://github.com/galaxyproject/galaxy/pull/13343 +.. _Pull Request 13347: https://github.com/galaxyproject/galaxy/pull/13347 +.. _Pull Request 13363: https://github.com/galaxyproject/galaxy/pull/13363 +.. _Pull Request 13373: https://github.com/galaxyproject/galaxy/pull/13373 +.. _Pull Request 13377: https://github.com/galaxyproject/galaxy/pull/13377 +.. _Pull Request 13381: https://github.com/galaxyproject/galaxy/pull/13381 +.. _Pull Request 13385: https://github.com/galaxyproject/galaxy/pull/13385 +.. _Pull Request 13389: https://github.com/galaxyproject/galaxy/pull/13389 +.. _Pull Request 13390: https://github.com/galaxyproject/galaxy/pull/13390 +.. _Pull Request 13392: https://github.com/galaxyproject/galaxy/pull/13392 +.. _Pull Request 13396: https://github.com/galaxyproject/galaxy/pull/13396 +.. _Pull Request 13403: https://github.com/galaxyproject/galaxy/pull/13403 +.. _Pull Request 13407: https://github.com/galaxyproject/galaxy/pull/13407 +.. _Pull Request 13417: https://github.com/galaxyproject/galaxy/pull/13417 +.. _Pull Request 13420: https://github.com/galaxyproject/galaxy/pull/13420 +.. _Pull Request 13425: https://github.com/galaxyproject/galaxy/pull/13425 +.. _Pull Request 13429: https://github.com/galaxyproject/galaxy/pull/13429 +.. _Pull Request 13430: https://github.com/galaxyproject/galaxy/pull/13430 +.. _Pull Request 13432: https://github.com/galaxyproject/galaxy/pull/13432 +.. _Pull Request 13433: https://github.com/galaxyproject/galaxy/pull/13433 +.. _Pull Request 13436: https://github.com/galaxyproject/galaxy/pull/13436 +.. _Pull Request 13439: https://github.com/galaxyproject/galaxy/pull/13439 +.. _Pull Request 13440: https://github.com/galaxyproject/galaxy/pull/13440 +.. _Pull Request 13441: https://github.com/galaxyproject/galaxy/pull/13441 +.. _Pull Request 13442: https://github.com/galaxyproject/galaxy/pull/13442 +.. _Pull Request 13443: https://github.com/galaxyproject/galaxy/pull/13443 +.. _Pull Request 13454: https://github.com/galaxyproject/galaxy/pull/13454 +.. _Pull Request 13461: https://github.com/galaxyproject/galaxy/pull/13461 +.. _Pull Request 13462: https://github.com/galaxyproject/galaxy/pull/13462 +.. _Pull Request 13468: https://github.com/galaxyproject/galaxy/pull/13468 +.. _Pull Request 13469: https://github.com/galaxyproject/galaxy/pull/13469 +.. _Pull Request 13470: https://github.com/galaxyproject/galaxy/pull/13470 +.. _Pull Request 13474: https://github.com/galaxyproject/galaxy/pull/13474 +.. _Pull Request 13478: https://github.com/galaxyproject/galaxy/pull/13478 +.. _Pull Request 13480: https://github.com/galaxyproject/galaxy/pull/13480 +.. _Pull Request 13483: https://github.com/galaxyproject/galaxy/pull/13483 +.. _Pull Request 13484: https://github.com/galaxyproject/galaxy/pull/13484 +.. _Pull Request 13485: https://github.com/galaxyproject/galaxy/pull/13485 +.. _Pull Request 13491: https://github.com/galaxyproject/galaxy/pull/13491 +.. _Pull Request 13493: https://github.com/galaxyproject/galaxy/pull/13493 +.. _Pull Request 13497: https://github.com/galaxyproject/galaxy/pull/13497 +.. _Pull Request 13514: https://github.com/galaxyproject/galaxy/pull/13514 +.. _Pull Request 13531: https://github.com/galaxyproject/galaxy/pull/13531 +.. _Pull Request 13536: https://github.com/galaxyproject/galaxy/pull/13536 diff --git a/doc/source/releases/22.05_announce.rst b/doc/source/releases/22.05_announce.rst new file mode 100644 index 00000000000..1997de3e400 --- /dev/null +++ b/doc/source/releases/22.05_announce.rst @@ -0,0 +1,12 @@ + +:orphan: + +=========================================================== +May 2022 Galaxy Release (v 22.05) +=========================================================== + + +Schedule +=========================================================== + * Planned Freeze Date: 2022-05-02 + * Planned Release Date: 2022-05-23 diff --git a/doc/source/releases/images/22.01-colour.mp4 b/doc/source/releases/images/22.01-colour.mp4 new file mode 100644 index 00000000000..b8ca5236894 Binary files /dev/null and b/doc/source/releases/images/22.01-colour.mp4 differ diff --git a/doc/source/releases/images/22.01-export.mp4 b/doc/source/releases/images/22.01-export.mp4 new file mode 100644 index 00000000000..e832b0a2948 Binary files /dev/null and b/doc/source/releases/images/22.01-export.mp4 differ diff --git a/doc/source/releases/index.rst b/doc/source/releases/index.rst index ce5f17b1faf..da1e2921287 100644 --- a/doc/source/releases/index.rst +++ b/doc/source/releases/index.rst @@ -4,6 +4,7 @@ Releases .. toctree:: :maxdepth: 1 + 22.01_announce_user 21.09_announce_user 21.05_announce_user 21.01_announce_user diff --git a/lib/galaxy/app.py b/lib/galaxy/app.py index f0647767282..2d9a519e597 100644 --- a/lib/galaxy/app.py +++ b/lib/galaxy/app.py @@ -1,6 +1,10 @@ +import collections +import errno import logging +import os import signal import sys +import threading import time from typing import ( Any, @@ -9,6 +13,9 @@ from typing import ( Tuple, ) +from beaker.cache import CacheManager +from beaker.util import parse_cache_config_options + import galaxy.model import galaxy.model.security import galaxy.queues @@ -17,13 +24,16 @@ from galaxy import ( auth, config, jobs, + tools, ) from galaxy.config_watchers import ConfigWatchers from galaxy.containers import build_container_interfaces from galaxy.datatypes.registry import Registry from galaxy.files import ConfiguredFileSources from galaxy.job_metrics import JobMetrics +from galaxy.jobs.manager import JobManager from galaxy.managers.api_keys import ApiKeyManager +from galaxy.managers.citations import CitationsManager from galaxy.managers.collections import DatasetCollectionManager from galaxy.managers.folders import FolderManager from galaxy.managers.hdas import HDAManager @@ -40,14 +50,26 @@ from galaxy.managers.workflows import ( WorkflowContentsManager, WorkflowsManager, ) +from galaxy.model import ( + custom_types, + mapping, +) from galaxy.model.base import SharedModelMapping from galaxy.model.database_heartbeat import DatabaseHeartbeat +from galaxy.model.database_utils import ( + database_exists, + is_one_database, +) from galaxy.model.mapping import GalaxyModelMapping +from galaxy.model.migrations import verify_databases +from galaxy.model.orm.engine_factory import build_engine from galaxy.model.scoped_session import ( galaxy_scoped_session, install_model_scoped_session, ) from galaxy.model.tags import GalaxyTagHandler +from galaxy.model.tool_shed_install import mapping as install_mapping +from galaxy.objectstore import build_object_store_from_config from galaxy.queue_worker import ( GalaxyQueueWorker, send_local_control_task, @@ -56,21 +78,28 @@ from galaxy.quota import ( get_quota_agent, QuotaAgent, ) +from galaxy.schema.fields import BaseDatabaseIdField from galaxy.security.idencoding import IdEncodingHelper from galaxy.security.vault import ( Vault, VaultFactory, ) +from galaxy.tool_shed import tool_shed_registry from galaxy.tool_shed.galaxy_install.installed_repository_manager import InstalledRepositoryManager from galaxy.tool_shed.galaxy_install.update_repository_manager import UpdateRepositoryManager +from galaxy.tool_util.deps import containers +from galaxy.tool_util.deps.dependencies import AppInfo from galaxy.tool_util.deps.views import DependencyResolversView from galaxy.tool_util.verify.test_data import TestDataResolver +from galaxy.tools.biotools import get_galaxy_biotools_metadata_source from galaxy.tools.cache import ( ToolCache, ToolShedRepositoryCache, ) +from galaxy.tools.data import ToolDataTableManager from galaxy.tools.data_manager.manager import DataManagers from galaxy.tools.error_reports import ErrorReports +from galaxy.tools.search import ToolBoxSearch from galaxy.tools.special_tools import load_lib_tools from galaxy.tours import ( build_tours_registry, @@ -79,8 +108,10 @@ from galaxy.tours import ( from galaxy.util import ( ExecutionTimer, heartbeat, + listify, StructuredExecutionTimer, ) +from galaxy.util.dbkeys import GenomeBuilds from galaxy.util.task import IntervalTask from galaxy.visualization.data_providers.registry import DataProviderRegistry from galaxy.visualization.genomes import Genomes @@ -92,6 +123,7 @@ from galaxy.web_stack import ( ApplicationStack, ) from galaxy.webhooks import WebhooksRegistry +from galaxy.workflow import scheduling_manager from galaxy.workflow.trs_proxy import TrsProxy from .di import Container from .structured_app import ( @@ -153,10 +185,247 @@ class SentryClientMixin: self.application_stack.register_postfork_function(postfork_sentry_client) -class MinimalGalaxyApplication(BasicSharedApp, config.ConfiguresGalaxyMixin, HaltableContainer, SentryClientMixin): +class ConfiguresGalaxyMixin: + """Shared code for configuring Galaxy-like app objects.""" + + config: config.GalaxyAppConfiguration + tool_cache: ToolCache + job_config: jobs.JobConfiguration + toolbox: tools.ToolBox + toolbox_search: ToolBoxSearch + container_finder: containers.ContainerFinder + + def _configure_genome_builds(self, data_table_name="__dbkeys__", load_old_style=True): + self.genome_builds = GenomeBuilds(self, data_table_name=data_table_name, load_old_style=load_old_style) + + def wait_for_toolbox_reload(self, old_toolbox): + timer = ExecutionTimer() + log.debug("Waiting for toolbox reload") + # Wait till toolbox reload has been triggered (or more than 60 seconds have passed) + while timer.elapsed < 60: + if self.toolbox.has_reloaded(old_toolbox): + log.debug("Finished waiting for toolbox reload %s", timer) + break + time.sleep(0.1) + else: + log.warning("Waiting for toolbox reload timed out after 60 seconds") + + def _configure_tool_config_files(self): + if self.config.shed_tool_config_file not in self.config.tool_configs: + self.config.tool_configs.append(self.config.shed_tool_config_file) + # The value of migrated_tools_config is the file reserved for containing only those tools that have been + # eliminated from the distribution and moved to the tool shed. If migration checking is disabled, only add it if + # it exists (since this may be an existing deployment where migrations were previously run). + if ( + os.path.exists(self.config.migrated_tools_config) + and self.config.migrated_tools_config not in self.config.tool_configs + ): + self.config.tool_configs.append(self.config.migrated_tools_config) + + def _configure_toolbox(self): + if not isinstance(self, BasicSharedApp): + raise Exception("Must inherit from BasicSharedApp") + + self.citations_manager = CitationsManager(self) + self.biotools_metadata_source = get_galaxy_biotools_metadata_source(self.config) + + self.dynamic_tools_manager = DynamicToolManager(self) + self._toolbox_lock = threading.RLock() + self.toolbox = tools.ToolBox(self.config.tool_configs, self.config.tool_path, self) + galaxy_root_dir = os.path.abspath(self.config.root) + file_path = os.path.abspath(self.config.file_path) + app_info = AppInfo( + galaxy_root_dir=galaxy_root_dir, + default_file_path=file_path, + tool_data_path=self.config.tool_data_path, + shed_tool_data_path=self.config.shed_tool_data_path, + outputs_to_working_directory=self.config.outputs_to_working_directory, + container_image_cache_path=self.config.container_image_cache_path, + library_import_dir=self.config.library_import_dir, + enable_mulled_containers=self.config.enable_mulled_containers, + container_resolvers_config_file=self.config.container_resolvers_config_file, + container_resolvers_config_dict=self.config.container_resolvers, + involucro_path=self.config.involucro_path, + involucro_auto_init=self.config.involucro_auto_init, + mulled_channels=self.config.mulled_channels, + ) + mulled_resolution_cache = None + if self.config.mulled_resolution_cache_type: + cache_opts = { + "cache.type": self.config.mulled_resolution_cache_type, + "cache.data_dir": self.config.mulled_resolution_cache_data_dir, + "cache.lock_dir": self.config.mulled_resolution_cache_lock_dir, + } + mulled_resolution_cache = CacheManager(**parse_cache_config_options(cache_opts)).get_cache( + "mulled_resolution" + ) + self.container_finder = containers.ContainerFinder(app_info, mulled_resolution_cache=mulled_resolution_cache) + self._set_enabled_container_types() + index_help = getattr(self.config, "index_tool_help", True) + self.toolbox_search = ToolBoxSearch( + self.toolbox, index_dir=self.config.tool_search_index_dir, index_help=index_help + ) + + def reindex_tool_search(self): + # Call this when tools are added or removed. + self.toolbox_search.build_index(tool_cache=self.tool_cache) + self.tool_cache.reset_status() + + def _set_enabled_container_types(self): + container_types_to_destinations = collections.defaultdict(list) + for destinations in self.job_config.destinations.values(): + for destination in destinations: + for enabled_container_type in self.container_finder._enabled_container_types(destination.params): + container_types_to_destinations[enabled_container_type].append(destination) + self.toolbox.dependency_manager.set_enabled_container_types(container_types_to_destinations) + self.toolbox.dependency_manager.resolver_classes.update( + self.container_finder.default_container_registry.resolver_classes + ) + self.toolbox.dependency_manager.dependency_resolvers.extend( + self.container_finder.default_container_registry.container_resolvers + ) + + def _configure_tool_data_tables(self, from_shed_config): + # Initialize tool data tables using the config defined by self.config.tool_data_table_config_path. + self.tool_data_tables = ToolDataTableManager( + tool_data_path=self.config.tool_data_path, + config_filename=self.config.tool_data_table_config_path, + other_config_dict=self.config, + ) + # Load additional entries defined by self.config.shed_tool_data_table_config into tool data tables. + try: + self.tool_data_tables.load_from_config_file( + config_filename=self.config.shed_tool_data_table_config, + tool_data_path=self.tool_data_tables.tool_data_path, + from_shed_config=from_shed_config, + ) + except OSError as exc: + # Missing shed_tool_data_table_config is okay if it's the default + if exc.errno != errno.ENOENT or self.config.is_set("shed_tool_data_table_config"): + raise + + def _configure_datatypes_registry(self, installed_repository_manager=None): + # Create an empty datatypes registry. + self.datatypes_registry = Registry(self.config) + if installed_repository_manager and self.config.load_tool_shed_datatypes: + # Load proprietary datatypes defined in datatypes_conf.xml files in all installed tool shed repositories. We + # load proprietary datatypes before datatypes in the distribution because Galaxy's default sniffers include some + # generic sniffers (eg text,xml) which catch anything, so it's impossible for proprietary sniffers to be used. + # However, if there is a conflict (2 datatypes with the same extension) between a proprietary datatype and a datatype + # in the Galaxy distribution, the datatype in the Galaxy distribution will take precedence. If there is a conflict + # between 2 proprietary datatypes, the datatype from the repository that was installed earliest will take precedence. + installed_repository_manager.load_proprietary_datatypes() + # Load the data types in the Galaxy distribution, which are defined in self.config.datatypes_config. + datatypes_configs = self.config.datatypes_config + for datatypes_config in listify(datatypes_configs): + # Setting override=False would make earlier files would take + # precedence - but then they wouldn't override tool shed + # datatypes. + self.datatypes_registry.load_datatypes(self.config.root, datatypes_config, override=True) + + def _configure_object_store(self, **kwds): + self.object_store = build_object_store_from_config(self.config, **kwds) + + def _configure_security(self): + self.security = IdEncodingHelper(id_secret=self.config.id_secret) + BaseDatabaseIdField.security = self.security + + def _configure_tool_shed_registry(self): + # Set up the tool sheds registry + if os.path.isfile(self.config.tool_sheds_config_file): + self.tool_shed_registry = tool_shed_registry.Registry(self.config.tool_sheds_config_file) + else: + self.tool_shed_registry = tool_shed_registry.Registry() + + def _configure_engines(self, db_url, install_db_url, combined_install_database): + trace_logger = getattr(self, "trace_logger", None) + engine = build_engine( + db_url, + self.config.database_engine_options, + self.config.database_query_profiling_proxy, + trace_logger, + self.config.slow_query_log_threshold, + self.config.thread_local_log, + self.config.database_log_query_counts, + ) + install_engine = None + if not combined_install_database: + install_engine = build_engine(install_db_url, self.config.install_database_engine_options) + return engine, install_engine + + def _configure_models(self, check_migrate_databases=False, config_file=None): + """Preconditions: object_store must be set on self.""" + # TODO this block doesn't seem to belong in this method + if getattr(self.config, "max_metadata_value_size", None): + custom_types.MAX_METADATA_VALUE_SIZE = self.config.max_metadata_value_size + + db_url = self.config.database_connection + install_db_url = self.config.install_database_connection + combined_install_database = is_one_database(db_url, install_db_url) + engine, install_engine = self._configure_engines(db_url, install_db_url, combined_install_database) + + if self.config.database_wait: + self._wait_for_database(db_url) + + if check_migrate_databases: + self._verify_databases(engine, install_engine, combined_install_database) + + self.model = mapping.configure_model_mapping( + self.config.file_path, + self.object_store, + self.config.use_pbkdf2, + engine, + combined_install_database, + self.config.thread_local_log, + ) + + if combined_install_database: + log.info("Install database targeting Galaxy's database configuration.") # TODO this message is ambiguous + self.install_model = self.model + else: + self.install_model = install_mapping.configure_model_mapping(install_engine) + log.info(f"Install database using its own connection {install_db_url}") + + def _verify_databases(self, engine, install_engine, combined_install_database): + install_template, install_encoding = None, None + if not combined_install_database: # Otherwise these options are not used. + install_template = getattr(self.config, "install_database_template", None) + install_encoding = getattr(self.config, "install_database_encoding", None) + + verify_databases( + engine, + self.config.database_template, + self.config.database_encoding, + install_engine, + install_template, + install_encoding, + self.config.database_auto_migrate, + ) + + def _configure_signal_handlers(self, handlers): + for sig, handler in handlers.items(): + signal.signal(sig, handler) + + def _wait_for_database(self, url): + attempts = self.config.database_wait_attempts + pause = self.config.database_wait_sleep + for i in range(1, attempts): + try: + database_exists(url) + break + except Exception: + log.info("Waiting for database: attempt %d of %d" % (i, attempts)) + time.sleep(pause) + + @property + def tool_dependency_dir(self): + return self.toolbox.dependency_manager.default_base_path + + +class MinimalGalaxyApplication(BasicSharedApp, ConfiguresGalaxyMixin, HaltableContainer, SentryClientMixin): """Encapsulates the state of a minimal Galaxy application""" - def __init__(self, fsmon=False, configure_logging=True, **kwargs) -> None: + def __init__(self, fsmon=False, **kwargs) -> None: super().__init__() self.haltables = [ ("object store", self._shutdown_object_store), @@ -171,12 +440,9 @@ class MinimalGalaxyApplication(BasicSharedApp, config.ConfiguresGalaxyMixin, Hal log.debug("python path is: %s", ", ".join(sys.path)) self.name = "galaxy" self.is_webapp = False - self.new_installation = False # Read config file and check for errors self.config: Any = self._register_singleton(config.Configuration, config.Configuration(**kwargs)) self.config.check() - if configure_logging: - config.configure_logging(self.config) self._configure_object_store(fsmon=True) config_file = kwargs.get("global_conf", {}).get("__file__", None) if config_file: @@ -208,7 +474,7 @@ class MinimalGalaxyApplication(BasicSharedApp, config.ConfiguresGalaxyMixin, Hal class GalaxyManagerApplication(MinimalManagerApp, MinimalGalaxyApplication): """Extends the MinimalGalaxyApplication with most managers that are not tied to a web or job handling context.""" - def __init__(self, **kwargs): + def __init__(self, configure_logging=True, **kwargs): super().__init__(**kwargs) self._register_singleton(MinimalManagerApp, self) self.execution_timer_factory = self._register_singleton( @@ -216,6 +482,8 @@ class GalaxyManagerApplication(MinimalManagerApp, MinimalGalaxyApplication): ) self.configure_fluent_log() self.application_stack = self._register_singleton(ApplicationStack, application_stack_instance(app=self)) + if configure_logging: + config.configure_logging(self.config, self.application_stack.facts) # Initialize job metrics manager, needs to be in place before # config so per-destination modifications can be made. self.job_metrics = self._register_singleton( @@ -238,8 +506,6 @@ class GalaxyManagerApplication(MinimalManagerApp, MinimalGalaxyApplication): self.library_manager = self._register_singleton(LibraryManager) self.library_datasets_manager = self._register_singleton(LibraryDatasetsManager) self.role_manager = self._register_singleton(RoleManager) - from galaxy.jobs.manager import JobManager - self.job_manager = self._register_singleton(JobManager) # ConfiguredFileSources @@ -336,6 +602,9 @@ class UniverseApplication(StructuredApp, GalaxyManagerApplication): self.datatypes_registry.load_external_metadata_tool(self.toolbox) # Load history import/export tools. load_lib_tools(self.toolbox) + # Load built-in converters + if self.config.display_builtin_converters: + self.toolbox.load_builtin_converters() self.toolbox.persist_cache(register_postfork=True) # visualizations registry: associates resources with visualizations, controls how to render self.visualizations_registry = self._register_singleton( @@ -398,13 +667,8 @@ class UniverseApplication(StructuredApp, GalaxyManagerApplication): self.haltables.append(("HistoryAuditTablePruneTask", self.prune_history_audit_task.shutdown)) # Start the job manager self.application_stack.register_postfork_function(self.job_manager.start) - # If app is not job handler but uses mule messaging. - # Can be removed when removing mule support. - self.job_manager._check_jobs_at_startup() self.proxy_manager = ProxyManager(self.config) - from galaxy.workflow import scheduling_manager - # Must be initialized after job_config. self.workflow_scheduling_manager = scheduling_manager.WorkflowSchedulingManager(self) @@ -441,6 +705,7 @@ class UniverseApplication(StructuredApp, GalaxyManagerApplication): self.server_starttime = int(time.time()) # used for cachebusting # Limit lifetime of tool shed repository cache to app startup self.tool_shed_repository_cache = None + self.api_spec = None log.info(f"Galaxy app startup finished {startup_timer}") def _shutdown_queue_worker(self): diff --git a/lib/galaxy/app_unittest_utils/celery_helper.py b/lib/galaxy/app_unittest_utils/celery_helper.py deleted file mode 100644 index d70e0eafb94..00000000000 --- a/lib/galaxy/app_unittest_utils/celery_helper.py +++ /dev/null @@ -1,21 +0,0 @@ -from functools import wraps - - -def rebind_container_to_task(app): - import galaxy.app - - galaxy.app.app = app - from galaxy.celery import ( - CELERY_TASKS, - tasks, - ) - - def magic_bind_dynamic(func): - return wraps(func)(app.magic_partial(func, shared=None)) - - for task in CELERY_TASKS: - task_fn = getattr(tasks, task, None) - if task_fn: - task_fn = getattr(task_fn, "__wrapped__", task_fn) - container_bound_task = magic_bind_dynamic(task_fn) - setattr(tasks, task, container_bound_task) diff --git a/lib/galaxy/app_unittest_utils/galaxy_mock.py b/lib/galaxy/app_unittest_utils/galaxy_mock.py index 53ece3ff6fa..e161011c273 100644 --- a/lib/galaxy/app_unittest_utils/galaxy_mock.py +++ b/lib/galaxy/app_unittest_utils/galaxy_mock.py @@ -11,6 +11,7 @@ from galaxy import ( quota, ) from galaxy.auth import AuthManager +from galaxy.celery import set_thread_app from galaxy.config import CommonConfigurationMixin from galaxy.jobs.manager import NoopManager from galaxy.managers.users import UserManager @@ -34,7 +35,6 @@ from galaxy.util import StructuredExecutionTimer from galaxy.util.bunch import Bunch from galaxy.util.dbkeys import GenomeBuilds from galaxy.web_stack import ApplicationStack -from .celery_helper import rebind_container_to_task # ============================================================================= @@ -85,9 +85,7 @@ class MockApp(di.Container, GalaxyDataTestApp): self.tag_handler = tags.GalaxyTagHandler(self.model.context) self[tags.GalaxyTagHandler] = self.tag_handler self.quota_agent = quota.DatabaseQuotaAgent(self.model) - self.job_config = Bunch( - dynamic_params=None, destinations={}, use_messaging=False, assign_handler=lambda *args, **kwargs: None - ) + self.job_config = Bunch(dynamic_params=None, destinations={}, assign_handler=lambda *args, **kwargs: None) self.tool_data_tables = ToolDataTableManager(tool_data_path=self.config.tool_data_path) self.dataset_collections_service = None self.container_finder = NullContainerFinder() @@ -103,7 +101,7 @@ class MockApp(di.Container, GalaxyDataTestApp): self.interactivetool_manager = Bunch(create_interactivetool=lambda *args, **kwargs: None) self.is_job_handler = False self.biotools_metadata_source = None - rebind_container_to_task(self) + set_thread_app(self) def url_for(*args, **kwds): return "/mock/url" diff --git a/lib/galaxy/authnz/custos_authnz.py b/lib/galaxy/authnz/custos_authnz.py index f3aa897a7c1..9d0db4d452a 100644 --- a/lib/galaxy/authnz/custos_authnz.py +++ b/lib/galaxy/authnz/custos_authnz.py @@ -22,6 +22,7 @@ from galaxy.model import ( CustosAuthnzToken, User, ) +from galaxy.model.orm.util import add_object_to_object_session from ..authnz import IdentityProvider log = logging.getLogger(__name__) @@ -186,7 +187,6 @@ class CustosAuthnz(IdentityProvider): trans.app.user_manager.send_activation_email(trans, email, username) custos_authnz_token = CustosAuthnzToken( - user=user, external_user_id=user_id, provider=self.config["provider"], access_token=access_token, @@ -195,6 +195,8 @@ class CustosAuthnz(IdentityProvider): expiration_time=expiration_time, refresh_expiration_time=refresh_expiration_time, ) + add_object_to_object_session(custos_authnz_token, user) + custos_authnz_token.user = user trans.sa_session.add(user) trans.sa_session.add(custos_authnz_token) @@ -275,7 +277,7 @@ class CustosAuthnz(IdentityProvider): trans.set_cookie("", name=NONCE_COOKIE_NAME, age=-1) nonce_cookie_hash = self._hash_nonce(nonce_cookie) if nonce_hash != nonce_cookie_hash: - raise Exception("Nonce mismatch!") + raise Exception("Nonce mismatch. Check that configured redirect_uri matches the URL you are using.") def _load_config_for_cilogon(self): # Set cilogon endpoints diff --git a/lib/galaxy/celery/__init__.py b/lib/galaxy/celery/__init__.py index 6aa5a4c8535..24fdfe3b952 100644 --- a/lib/galaxy/celery/__init__.py +++ b/lib/galaxy/celery/__init__.py @@ -3,16 +3,21 @@ from functools import ( lru_cache, wraps, ) +from threading import local +from typing import ( + Any, + Dict, +) from celery import ( Celery, shared_task, ) from kombu import serialization -from lagom import magic_bind_to_container from galaxy.config import Configuration from galaxy.main_config import find_config +from galaxy.util import ExecutionTimer from galaxy.util.custom_logging import get_logger from galaxy.util.properties import load_app_properties from ._serialization import ( @@ -22,16 +27,39 @@ from ._serialization import ( log = get_logger(__name__) +MAIN_TASK_MODULE = "galaxy.celery.tasks" +TASKS_MODULES = [MAIN_TASK_MODULE] +PYDANTIC_AWARE_SERIALIZER_NAME = "pydantic-aware-json" + +APP_LOCAL = local() + +serialization.register( + PYDANTIC_AWARE_SERIALIZER_NAME, encoder=schema_dumps, decoder=schema_loads, content_type="application/json" +) + + +def set_thread_app(app): + APP_LOCAL.app = app + + +def get_galaxy_app(): + try: + return APP_LOCAL.app + except AttributeError: + import galaxy.app + + if galaxy.app.app: + return galaxy.app.app + return build_app() + @lru_cache(maxsize=1) -def get_galaxy_app(): - import galaxy.app - - if galaxy.app.app: - return galaxy.app.app +def build_app(): kwargs = get_app_properties() if kwargs: kwargs["check_migrate_databases"] = False + import galaxy.app + galaxy_app = galaxy.app.GalaxyManagerApplication(configure_logging=False, **kwargs) return galaxy_app @@ -63,7 +91,13 @@ def get_config(): def get_broker(): config = get_config() if config: - return config.amqp_internal_connection + return config.celery_broker or config.amqp_internal_connection + + +def get_backend(): + config = get_config() + if config: + return config.celery_backend def get_history_audit_table_prune_interval(): @@ -75,7 +109,15 @@ def get_history_audit_table_prune_interval(): broker = get_broker() -celery_app = Celery("galaxy", broker=broker, include=["galaxy.celery.tasks"]) +backend = get_backend() +celery_app_kwd: Dict[str, Any] = { + "broker": broker, + "include": TASKS_MODULES, +} +if backend: + celery_app_kwd["backend"] = backend + +celery_app = Celery("galaxy", **celery_app_kwd) prune_interval = get_history_audit_table_prune_interval() if prune_interval > 0: celery_app.conf.beat_schedule = { @@ -87,28 +129,33 @@ if prune_interval > 0: celery_app.conf.timezone = "UTC" -CELERY_TASKS = [] -PYDANTIC_AWARE_SERIALIER_NAME = "pydantic-aware-json" - - -serialization.register( - PYDANTIC_AWARE_SERIALIER_NAME, encoder=schema_dumps, decoder=schema_loads, content_type="application/json" -) - - -def galaxy_task(*args, **celery_task_kwd): +def galaxy_task(*args, action=None, **celery_task_kwd): if "serializer" not in celery_task_kwd: - celery_task_kwd["serializer"] = PYDANTIC_AWARE_SERIALIER_NAME + celery_task_kwd["serializer"] = PYDANTIC_AWARE_SERIALIZER_NAME def decorate(func): - CELERY_TASKS.append(func.__name__) - @shared_task(**celery_task_kwd) @wraps(func) def wrapper(*args, **kwds): app = get_galaxy_app() assert app - return magic_bind_to_container(app)(func)(*args, **kwds) + desc = func.__name__ + if action is not None: + desc += f" to {action}" + + try: + timer = app.execution_timer_factory.get_timer("internals.tasks.{func.__name__}", desc) + except AttributeError: + timer = ExecutionTimer() + + try: + rval = app.magic_partial(func)(*args, **kwds) + message = f"Successfully executed Celery task {desc} {timer}" + log.info(message) + return rval + except Exception: + log.warning(f"Celery task execution failed for {desc} {timer}") + raise return wrapper diff --git a/lib/galaxy/celery/tasks.py b/lib/galaxy/celery/tasks.py index 8914cfaf193..181e00e05eb 100644 --- a/lib/galaxy/celery/tasks.py +++ b/lib/galaxy/celery/tasks.py @@ -1,17 +1,16 @@ from galaxy import model from galaxy.celery import galaxy_task -from galaxy.jobs.manager import JobManager from galaxy.managers.hdas import HDAManager from galaxy.managers.lddas import LDDAManager +from galaxy.managers.model_stores import ModelStoreManager from galaxy.model.scoped_session import galaxy_scoped_session -from galaxy.structured_app import MinimalManagerApp -from galaxy.util import ExecutionTimer +from galaxy.schema.tasks import SetupHistoryExportJob from galaxy.util.custom_logging import get_logger log = get_logger(__name__) -@galaxy_task(ignore_result=True) +@galaxy_task(ignore_result=True, action="recalcuate a user's disk usage") def recalculate_user_disk_usage(session: galaxy_scoped_session, user_id=None): if user_id: user = session.query(model.User).get(user_id) @@ -24,13 +23,13 @@ def recalculate_user_disk_usage(session: galaxy_scoped_session, user_id=None): log.error("Recalculate user disk usage task received without user_id.") -@galaxy_task(ignore_result=True) +@galaxy_task(ignore_result=True, action="purge a history dataset") def purge_hda(hda_manager: HDAManager, hda_id): hda = hda_manager.by_id(hda_id) hda_manager._purge(hda) -@galaxy_task +@galaxy_task(action="set dataset association metadata") def set_metadata( hda_manager: HDAManager, ldda_manager: LDDAManager, dataset_id, model_class="HistoryDatasetAssociation" ): @@ -41,29 +40,15 @@ def set_metadata( dataset.datatype.set_meta(dataset) -@galaxy_task(ignore_result=True) +@galaxy_task(ignore_result=True, action="setting up export history job") def export_history( - app: MinimalManagerApp, - sa_session: galaxy_scoped_session, - job_manager: JobManager, - store_directory: str, - history_id: int, - job_id: int, - include_hidden=False, - include_deleted=False, + model_store_manager: ModelStoreManager, + request: SetupHistoryExportJob, ): - history = sa_session.query(model.History).get(history_id) - with model.store.DirectoryModelExportStore(store_directory, app=app, export_files="symlink") as export_store: - export_store.export_history(history, include_hidden=include_hidden, include_deleted=include_deleted) - job = sa_session.query(model.Job).filter_by(id=job_id).one() - job.state = model.Job.states.NEW - sa_session.flush() - job_manager.enqueue(job) + model_store_manager.setup_history_export_job(request) -@galaxy_task +@galaxy_task(action="pruning history audit table") def prune_history_audit_table(sa_session: galaxy_scoped_session): """Prune ever growing history_audit table.""" - timer = ExecutionTimer() model.HistoryAudit.prune(sa_session) - log.debug(f"Successfully pruned history_audit table {timer}") diff --git a/lib/galaxy/config/__init__.py b/lib/galaxy/config/__init__.py index 15ac7dd8d16..dac440e47b6 100644 --- a/lib/galaxy/config/__init__.py +++ b/lib/galaxy/config/__init__.py @@ -3,21 +3,18 @@ Universe configuration builder. """ # absolute_import needed for tool_shed package. -import collections import configparser -import errno import ipaddress +import locale import logging import logging.config import os import re -import signal import socket import string import sys import tempfile import threading -import time from datetime import timedelta from typing import ( Any, @@ -28,39 +25,28 @@ from typing import ( Optional, Set, SupportsInt, - TYPE_CHECKING, TypeVar, Union, ) import yaml -from beaker.cache import CacheManager -from beaker.util import parse_cache_config_options from galaxy.config.schema import AppSchema from galaxy.containers import parse_containers_config from galaxy.exceptions import ConfigurationError -from galaxy.model import mapping -from galaxy.model.database_utils import database_exists -from galaxy.model.tool_shed_install.migrate.check import create_or_verify_database as tsi_create_or_verify_database -from galaxy.schema.fields import BaseDatabaseIdField -from galaxy.structured_app import BasicSharedApp from galaxy.util import ( - ExecutionTimer, listify, string_as_bool, unicodify, ) from galaxy.util.custom_logging import LOGLV_TRACE -from galaxy.util.dbkeys import GenomeBuilds from galaxy.util.dynamic import HasDynamicProperties +from galaxy.util.facts import get_facts from galaxy.util.properties import ( find_config_file, read_properties_from_file, running_from_source, ) -from galaxy.web.formatting import expand_pretty_datetime_format -from galaxy.web_stack import get_stack_facts from ..version import ( VERSION_MAJOR, VERSION_MINOR, @@ -72,19 +58,16 @@ except ImportError: # Python < 3.9 from importlib_resources import files # type: ignore[no-redef] -if TYPE_CHECKING: - from galaxy.jobs import JobConfiguration - from galaxy.tool_util.deps.containers import ContainerFinder - from galaxy.tools import ToolBox - from galaxy.tools.cache import ToolCache - from galaxy.tools.search import ToolBoxSearch - log = logging.getLogger(__name__) +DEFAULT_LOCALE_FORMAT = "%a %b %e %H:%M:%S %Y" +ISO_DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S" + GALAXY_APP_NAME = "galaxy" GALAXY_SCHEMAS_PATH = files("galaxy.config") / "schemas" GALAXY_CONFIG_SCHEMA_PATH = GALAXY_SCHEMAS_PATH / "config_schema.yml" -UWSGI_SCHEMA_PATH = GALAXY_SCHEMAS_PATH / "uwsgi_schema.yml" +REPORTS_CONFIG_SCHEMA_PATH = GALAXY_SCHEMAS_PATH / "reports_config_schema.yml" +TOOL_SHED_CONFIG_SCHEMA_PATH = GALAXY_SCHEMAS_PATH / "tool_shed_config_schema.yml" LOGGING_CONFIG_DEFAULT: Dict[str, Any] = { "disable_existing_loggers": False, "version": 1, @@ -141,10 +124,76 @@ LOGGING_CONFIG_DEFAULT: Dict[str, Any] = { """Default value for logging configuration, passed to :func:`logging.config.dictConfig`""" +def configure_logging(config, facts=None): + """Allow some basic logging configuration to be read from ini file. + + This should be able to consume either a galaxy.config.Configuration object + or a simple dictionary of configuration variables. + """ + facts = facts or get_facts(config=config) + # Get root logger + logging.addLevelName(LOGLV_TRACE, "TRACE") + # PasteScript will have already configured the logger if the + # 'loggers' section was found in the config file, otherwise we do + # some simple setup using the 'log_*' values from the config. + parser = getattr(config, "global_conf_parser", None) + if parser: + paste_configures_logging = config.global_conf_parser.has_section("loggers") + else: + paste_configures_logging = False + auto_configure_logging = not paste_configures_logging and string_as_bool( + config.get("auto_configure_logging", "True") + ) + if auto_configure_logging: + logging_conf = config.get("logging", None) + if logging_conf is None: + # if using the default logging config, honor the log_level setting + logging_conf = LOGGING_CONFIG_DEFAULT + if config.get("log_level", "DEBUG") != "DEBUG": + logging_conf["handlers"]["console"]["level"] = config.get("log_level", "DEBUG") + # configure logging with logging dict in config, template *FileHandler handler filenames with the `filename_template` option + for name, conf in logging_conf.get("handlers", {}).items(): + if ( + conf["class"].startswith("logging.") + and conf["class"].endswith("FileHandler") + and "filename_template" in conf + ): + conf["filename"] = conf.pop("filename_template").format(**facts) + logging_conf["handlers"][name] = conf + logging.config.dictConfig(logging_conf) + + def find_root(kwargs): return os.path.abspath(kwargs.get("root_dir", ".")) +def expand_pretty_datetime_format(value): + """ + + >>> expand_pretty_datetime_format("%H:%M:%S %Z") + '%H:%M:%S %Z' + >>> locale_format = expand_pretty_datetime_format("$locale (UTC)") + >>> import locale + >>> expected_format = '%s (UTC)' % locale.nl_langinfo(locale.D_T_FMT) + >>> locale_format == expected_format + True + >>> expand_pretty_datetime_format("$iso8601") + '%Y-%m-%d %H:%M:%S' + """ + locale_format = None + try: + locale_format = locale.nl_langinfo(locale.D_T_FMT) + except AttributeError: # nl_langinfo not available + pass + if not locale_format: + locale_format = DEFAULT_LOCALE_FORMAT + stock_formats = dict( + locale=locale_format, + iso8601=ISO_DATETIME_FORMAT, + ) + return string.Template(value).safe_substitute(**stock_formats) + + OptStr = TypeVar("OptStr", None, str) @@ -644,6 +693,7 @@ class GalaxyAppConfiguration(BaseAppConfiguration, CommonConfigurationMixin): involucro_path: str mulled_channels: List[str] nginx_upload_store: str + tus_upload_store: str pretty_datetime_format: str visualization_plugins_directory: str galaxy_infrastructure_url: str @@ -723,7 +773,6 @@ class GalaxyAppConfiguration(BaseAppConfiguration, CommonConfigurationMixin): db_path = self._in_data_dir("universe.sqlite") self.database_connection = f"sqlite:///{db_path}?isolation_level=IMMEDIATE" self.database_engine_options = get_database_engine_options(kwargs) - self.database_create_tables = string_as_bool(kwargs.get("database_create_tables", "True")) self.database_encoding = kwargs.get("database_encoding") # Create new databases with this encoding self.thread_local_log = None if self.enable_per_request_sql_debugging: @@ -910,6 +959,9 @@ class GalaxyAppConfiguration(BaseAppConfiguration, CommonConfigurationMixin): if self.nginx_upload_store: self.nginx_upload_store = os.path.abspath(self.nginx_upload_store) + if self.tus_upload_store: + self.tus_upload_store = os.path.abspath(self.tus_upload_store) + self.object_store = kwargs.get("object_store", "disk") self.object_store_check_old_style = string_as_bool(kwargs.get("object_store_check_old_style", False)) self.object_store_cache_path = self._in_root_dir( @@ -1118,14 +1170,7 @@ class GalaxyAppConfiguration(BaseAppConfiguration, CommonConfigurationMixin): {"GALAXY_WEB_PORT": port} ) if "UWSGI_PORT" in self.galaxy_infrastructure_url: - import uwsgi - - http = unicodify(uwsgi.opt["http"]) - host, port = http.split(":", 1) - assert port, "galaxy_infrastructure_url depends on dynamic PORT determination but port unknown" - self.galaxy_infrastructure_url = string.Template(self.galaxy_infrastructure_url).safe_substitute( - {"UWSGI_PORT": port} - ) + raise Exception("UWSGI_PORT is not supported anymore") def reload_sanitize_allowlist(self, explicit=True): self.sanitize_allowlist = [] @@ -1153,6 +1198,7 @@ class GalaxyAppConfiguration(BaseAppConfiguration, CommonConfigurationMixin): self.managed_config_dir, self.new_file_path, self.nginx_upload_store, + self.tus_upload_store, self.object_store_cache_path, self.template_cache_path, self.tool_data_path, @@ -1237,308 +1283,3 @@ def get_database_engine_options(kwargs, model_prefix=""): value = conversions[key](value) rval[key] = value return rval - - -def get_database_url(config): - db_url = config.database_connection - return db_url - - -def init_models_from_config(config, map_install_models=False, object_store=None, trace_logger=None): - db_url = get_database_url(config) - model = mapping.init( - config.file_path, - db_url, - config.database_engine_options, - map_install_models=map_install_models, - database_query_profiling_proxy=config.database_query_profiling_proxy, - object_store=object_store, - trace_logger=trace_logger, - use_pbkdf2=config.get_bool("use_pbkdf2", True), - slow_query_log_threshold=config.slow_query_log_threshold, - thread_local_log=config.thread_local_log, - log_query_counts=config.database_log_query_counts, - ) - return model - - -def configure_logging(config): - """Allow some basic logging configuration to be read from ini file. - - This should be able to consume either a galaxy.config.Configuration object - or a simple dictionary of configuration variables. - """ - # Get root logger - logging.addLevelName(LOGLV_TRACE, "TRACE") - # PasteScript will have already configured the logger if the - # 'loggers' section was found in the config file, otherwise we do - # some simple setup using the 'log_*' values from the config. - parser = getattr(config, "global_conf_parser", None) - if parser: - paste_configures_logging = config.global_conf_parser.has_section("loggers") - else: - paste_configures_logging = False - auto_configure_logging = not paste_configures_logging and string_as_bool( - config.get("auto_configure_logging", "True") - ) - if auto_configure_logging: - logging_conf = config.get("logging", None) - if logging_conf is None: - # if using the default logging config, honor the log_level setting - logging_conf = LOGGING_CONFIG_DEFAULT - if config.get("log_level", "DEBUG") != "DEBUG": - logging_conf["handlers"]["console"]["level"] = config.get("log_level", "DEBUG") - # configure logging with logging dict in config, template *FileHandler handler filenames with the `filename_template` option - for name, conf in logging_conf.get("handlers", {}).items(): - if ( - conf["class"].startswith("logging.") - and conf["class"].endswith("FileHandler") - and "filename_template" in conf - ): - conf["filename"] = conf.pop("filename_template").format(**get_stack_facts(config=config)) - logging_conf["handlers"][name] = conf - logging.config.dictConfig(logging_conf) - - -class ConfiguresGalaxyMixin: - """Shared code for configuring Galaxy-like app objects.""" - - config: GalaxyAppConfiguration - tool_cache: "ToolCache" - job_config: "JobConfiguration" - toolbox: "ToolBox" - toolbox_search: "ToolBoxSearch" - container_finder: "ContainerFinder" - - def _configure_genome_builds(self, data_table_name="__dbkeys__", load_old_style=True): - self.genome_builds = GenomeBuilds(self, data_table_name=data_table_name, load_old_style=load_old_style) - - def wait_for_toolbox_reload(self, old_toolbox): - timer = ExecutionTimer() - log.debug("Waiting for toolbox reload") - # Wait till toolbox reload has been triggered (or more than 60 seconds have passed) - while timer.elapsed < 60: - if self.toolbox.has_reloaded(old_toolbox): - log.debug("Finished waiting for toolbox reload %s", timer) - break - time.sleep(0.1) - else: - log.warning("Waiting for toolbox reload timed out after 60 seconds") - - def _configure_tool_config_files(self): - if self.config.shed_tool_config_file not in self.config.tool_configs: - self.config.tool_configs.append(self.config.shed_tool_config_file) - # The value of migrated_tools_config is the file reserved for containing only those tools that have been - # eliminated from the distribution and moved to the tool shed. If migration checking is disabled, only add it if - # it exists (since this may be an existing deployment where migrations were previously run). - if ( - os.path.exists(self.config.migrated_tools_config) - and self.config.migrated_tools_config not in self.config.tool_configs - ): - self.config.tool_configs.append(self.config.migrated_tools_config) - - def _configure_toolbox(self): - import galaxy.tools.search - from galaxy import tools - from galaxy.managers.citations import CitationsManager - from galaxy.tool_util.deps import containers - from galaxy.tool_util.deps.dependencies import AppInfo - from galaxy.tools.biotools import get_galaxy_biotools_metadata_source - - if not isinstance(self, BasicSharedApp): - raise Exception("Must inherit from BasicSharedApp") - - self.citations_manager = CitationsManager(self) - self.biotools_metadata_source = get_galaxy_biotools_metadata_source(self.config) - - from galaxy.managers.tools import DynamicToolManager - - self.dynamic_tools_manager = DynamicToolManager(self) - self._toolbox_lock = threading.RLock() - self.toolbox = tools.ToolBox(self.config.tool_configs, self.config.tool_path, self) - galaxy_root_dir = os.path.abspath(self.config.root) - file_path = os.path.abspath(self.config.file_path) - app_info = AppInfo( - galaxy_root_dir=galaxy_root_dir, - default_file_path=file_path, - tool_data_path=self.config.tool_data_path, - shed_tool_data_path=self.config.shed_tool_data_path, - outputs_to_working_directory=self.config.outputs_to_working_directory, - container_image_cache_path=self.config.container_image_cache_path, - library_import_dir=self.config.library_import_dir, - enable_mulled_containers=self.config.enable_mulled_containers, - container_resolvers_config_file=self.config.container_resolvers_config_file, - container_resolvers_config_dict=self.config.container_resolvers, - involucro_path=self.config.involucro_path, - involucro_auto_init=self.config.involucro_auto_init, - mulled_channels=self.config.mulled_channels, - ) - mulled_resolution_cache = None - if self.config.mulled_resolution_cache_type: - cache_opts = { - "cache.type": self.config.mulled_resolution_cache_type, - "cache.data_dir": self.config.mulled_resolution_cache_data_dir, - "cache.lock_dir": self.config.mulled_resolution_cache_lock_dir, - } - mulled_resolution_cache = CacheManager(**parse_cache_config_options(cache_opts)).get_cache( - "mulled_resolution" - ) - self.container_finder = containers.ContainerFinder(app_info, mulled_resolution_cache=mulled_resolution_cache) - self._set_enabled_container_types() - index_help = getattr(self.config, "index_tool_help", True) - self.toolbox_search = galaxy.tools.search.ToolBoxSearch( - self.toolbox, index_dir=self.config.tool_search_index_dir, index_help=index_help - ) - - def reindex_tool_search(self): - # Call this when tools are added or removed. - self.toolbox_search.build_index(tool_cache=self.tool_cache) - self.tool_cache.reset_status() - - def _set_enabled_container_types(self): - container_types_to_destinations = collections.defaultdict(list) - for destinations in self.job_config.destinations.values(): - for destination in destinations: - for enabled_container_type in self.container_finder._enabled_container_types(destination.params): - container_types_to_destinations[enabled_container_type].append(destination) - self.toolbox.dependency_manager.set_enabled_container_types(container_types_to_destinations) - self.toolbox.dependency_manager.resolver_classes.update( - self.container_finder.default_container_registry.resolver_classes - ) - self.toolbox.dependency_manager.dependency_resolvers.extend( - self.container_finder.default_container_registry.container_resolvers - ) - - def _configure_tool_data_tables(self, from_shed_config): - from galaxy.tools.data import ToolDataTableManager - - # Initialize tool data tables using the config defined by self.config.tool_data_table_config_path. - self.tool_data_tables = ToolDataTableManager( - tool_data_path=self.config.tool_data_path, - config_filename=self.config.tool_data_table_config_path, - other_config_dict=self.config, - ) - # Load additional entries defined by self.config.shed_tool_data_table_config into tool data tables. - try: - self.tool_data_tables.load_from_config_file( - config_filename=self.config.shed_tool_data_table_config, - tool_data_path=self.tool_data_tables.tool_data_path, - from_shed_config=from_shed_config, - ) - except OSError as exc: - # Missing shed_tool_data_table_config is okay if it's the default - if exc.errno != errno.ENOENT or self.config.is_set("shed_tool_data_table_config"): - raise - - def _configure_datatypes_registry(self, installed_repository_manager=None): - from galaxy.datatypes import registry - - # Create an empty datatypes registry. - self.datatypes_registry = registry.Registry(self.config) - if installed_repository_manager and self.config.load_tool_shed_datatypes: - # Load proprietary datatypes defined in datatypes_conf.xml files in all installed tool shed repositories. We - # load proprietary datatypes before datatypes in the distribution because Galaxy's default sniffers include some - # generic sniffers (eg text,xml) which catch anything, so it's impossible for proprietary sniffers to be used. - # However, if there is a conflict (2 datatypes with the same extension) between a proprietary datatype and a datatype - # in the Galaxy distribution, the datatype in the Galaxy distribution will take precedence. If there is a conflict - # between 2 proprietary datatypes, the datatype from the repository that was installed earliest will take precedence. - installed_repository_manager.load_proprietary_datatypes() - # Load the data types in the Galaxy distribution, which are defined in self.config.datatypes_config. - datatypes_configs = self.config.datatypes_config - for datatypes_config in listify(datatypes_configs): - # Setting override=False would make earlier files would take - # precedence - but then they wouldn't override tool shed - # datatypes. - self.datatypes_registry.load_datatypes(self.config.root, datatypes_config, override=True) - - def _configure_object_store(self, **kwds): - from galaxy.objectstore import build_object_store_from_config - - self.object_store = build_object_store_from_config(self.config, **kwds) - - def _configure_security(self): - from galaxy.security import idencoding - - self.security = idencoding.IdEncodingHelper(id_secret=self.config.id_secret) - BaseDatabaseIdField.security = self.security - - def _configure_tool_shed_registry(self): - import galaxy.tool_shed.tool_shed_registry - - # Set up the tool sheds registry - if os.path.isfile(self.config.tool_sheds_config_file): - self.tool_shed_registry = galaxy.tool_shed.tool_shed_registry.Registry(self.config.tool_sheds_config_file) - else: - self.tool_shed_registry = galaxy.tool_shed.tool_shed_registry.Registry() - - def _configure_models(self, check_migrate_databases=False, config_file=None): - """Preconditions: object_store must be set on self.""" - db_url = get_database_url(self.config) - install_db_url = self.config.install_database_connection - # TODO: Consider more aggressive check here that this is not the same - # database file under the hood. - combined_install_database = not (install_db_url and install_db_url != db_url) - install_db_url = install_db_url or db_url - install_database_options = ( - self.config.database_engine_options - if combined_install_database - else self.config.install_database_engine_options - ) - - if self.config.database_wait: - self._wait_for_database(db_url) - - if getattr(self.config, "max_metadata_value_size", None): - from galaxy.model import custom_types - - custom_types.MAX_METADATA_VALUE_SIZE = self.config.max_metadata_value_size - - if check_migrate_databases: - # Initialize database / check for appropriate schema version. # If this - # is a new installation, we'll restrict the tool migration messaging. - from galaxy.model.migrate.check import create_or_verify_database - - create_or_verify_database( - db_url, - config_file, - self.config.database_engine_options, - app=self, - map_install_models=combined_install_database, - ) - if not combined_install_database: - tsi_create_or_verify_database(install_db_url, install_database_options, app=self) - - self.model = init_models_from_config( - self.config, - map_install_models=combined_install_database, - object_store=self.object_store, - trace_logger=getattr(self, "trace_logger", None), - ) - if combined_install_database: - log.info("Install database targetting Galaxy's database configuration.") - self.install_model = self.model - else: - from galaxy.model.tool_shed_install import mapping as install_mapping - - install_db_url = self.config.install_database_connection - log.info(f"Install database using its own connection {install_db_url}") - self.install_model = install_mapping.init(install_db_url, install_database_options) - - def _configure_signal_handlers(self, handlers): - for sig, handler in handlers.items(): - signal.signal(sig, handler) - - def _wait_for_database(self, url): - attempts = self.config.database_wait_attempts - pause = self.config.database_wait_sleep - for i in range(1, attempts): - try: - database_exists(url) - break - except Exception: - log.info("Waiting for database: attempt %d of %d" % (i, attempts)) - time.sleep(pause) - - @property - def tool_dependency_dir(self): - return self.toolbox.dependency_manager.default_base_path diff --git a/lib/galaxy/config/config_manage.py b/lib/galaxy/config/config_manage.py index 8f508a1688d..1c90a18ca94 100644 --- a/lib/galaxy/config/config_manage.py +++ b/lib/galaxy/config/config_manage.py @@ -2,7 +2,6 @@ import argparse import math import os import shutil -import string import sys import tempfile from io import StringIO @@ -13,10 +12,14 @@ from typing import ( NamedTuple, ) -import requests import yaml from boltons.iterutils import remap +try: + from gravity.util import settings_to_sample +except ImportError: + settings_to_sample = None + try: from pykwalify.core import Core except ImportError: @@ -28,12 +31,12 @@ if __name__ == "__main__": from galaxy.config import ( GALAXY_CONFIG_SCHEMA_PATH, - UWSGI_SCHEMA_PATH, + REPORTS_CONFIG_SCHEMA_PATH, + TOOL_SHED_CONFIG_SCHEMA_PATH, ) from galaxy.config.schema import ( AppSchema, OPTION_DEFAULTS, - Schema, ) from galaxy.util import safe_makedirs from galaxy.util.properties import nice_config_parser @@ -58,214 +61,6 @@ YAML_COMMENT_WRAPPER = TextWrapper( RST_DESCRIPTION_WRAPPER = TextWrapper( initial_indent=" ", subsequent_indent=" ", break_long_words=False, break_on_hyphens=False ) - -UWSGI_OPTIONS = dict( - [ - ( - "http", - { - "desc": """The address and port on which to listen. By default, only listen to localhost ($app_name will not be accessible over the network). Use ':$default_port' to listen on all available network interfaces.""", - "default": "127.0.0.1:$default_port", - "type": "str", - }, - ), - ( - "buffer-size", - { - "desc": """By default uWSGI allocates a very small buffer (4096 bytes) for the headers of each request. If you start receiving "invalid request block size" in your logs, it could mean you need a bigger buffer. We recommend at least 16384.""", - "default": 16384, - "type": "int", - }, - ), - ( - "processes", - { - "desc": """Number of web server (worker) processes to fork after the application has loaded. If this is set to greater than 1, thunder-lock likely should be enabled below.""", - "default": 1, - "type": "int", - }, - ), - ( - "threads", - { - "desc": """Number of threads for each web server process.""", - "default": 4, - "type": "int", - }, - ), - ( - "offload-threads", - { - "desc": """Number of threads for serving static content and handling internal routing requests.""", - "default": 2, - "type": "int", - }, - ), - ( - "static-map.1", - { - "key": "static-map", - "desc": """Mapping to serve static content.""", - "default": "/static=static", - "type": "str", - }, - ), - ( - "static-map.2", - { - "key": "static-map", - "desc": """Mapping to serve the favicon.""", - "default": "/favicon.ico=static/favicon.ico", - "type": "str", - }, - ), - ( - "static-safe", - { - "key": "static-safe", - "desc": """Allow serving certain assets out of `client`. Most modern Galaxy interfaces bundle all of this, but some older pages still serve these via symlink, requiring this rule.""", - "default": "client/src/assets", - "type": "str", - }, - ), - ( - "master", - { - "desc": """Enable the master process manager. Disabled by default for maximum compatibility with CTRL+C, but should be enabled for use with --daemon and/or production deployments.""", - "default": False, - "type": "bool", - }, - ), - ( - "virtualenv", - { - "desc": """Path to the application's Python virtual environment. If using Conda for Galaxy's framework dependencies (not tools!), do not set this.""", - "default": ".venv", - "type": "str", - }, - ), - ( - "pythonpath", - { - "desc": """Path to the application's Python library.""", - "default": "lib", - "type": "str", - }, - ), - ( - "module", - { - "desc": """The entry point which returns the web application (e.g. Galaxy, Reports, etc.) that you are loading.""", - "default": "$uwsgi_module", - "type": "str", - }, - ), - ( - "#mount", - { - "desc": """Mount the web application (e.g. Galaxy, Reports, etc.) at the given URL prefix. Cannot be used together with 'module:' above.""", - "default": "/galaxy=$uwsgi_module", - "type": "str", - }, - ), - ( - "manage-script-name", - { - "desc": """Make uWSGI rewrite PATH_INFO and SCRIPT_NAME according to mount-points. Set this to true if a URL prefix is used.""", - "default": False, - "type": "bool", - }, - ), - ( - "thunder-lock", - { - "desc": """It is usually a good idea to set this to ``true`` if processes is greater than 1.""", - "default": False, - "type": "bool", - }, - ), - ( - "die-on-term", - { - "desc": """Cause uWSGI to respect the traditional behavior of dying on SIGTERM (its default is to brutally reload workers)""", - "default": True, - "type": "bool", - }, - ), - ( - "hook-master-start.1", - { - "key": "hook-master-start", - "desc": """Cause uWSGI to gracefully reload workers and mules upon receipt of SIGINT (its default is to brutally kill workers)""", - "default": "unix_signal:2 gracefully_kill_them_all", - "type": "str", - }, - ), - ( - "hook-master-start.2", - { - "key": "hook-master-start", - "desc": """Cause uWSGI to gracefully reload workers and mules upon receipt of SIGTERM (its default is to brutally kill workers)""", - "default": "unix_signal:15 gracefully_kill_them_all", - "type": "str", - }, - ), - ( - "py-call-osafterfork", - { - "desc": """Feature necessary for proper mule signal handling on Python versions below 3.7.2. The default is set to false to prevent a runtime error under Python 3.7.2 and newer (see https://github.com/unbit/uwsgi/issues/1978).""", - "default": False, - "type": "bool", - }, - ), - ( - "enable-threads", - { - "desc": """Ensure application threads will run if `threads` is unset.""", - "default": True, - "type": "bool", - }, - ), - ( - "umask", - { - "desc": """uWSGI default umask. On some systems uWSGI has a default umask of 000, for Galaxy a somewhat safer default is chosen. If Galaxy submits jobs as real user then all users needs to be able to read the files, i.e. the umask needs to be '022' or the Galaxy users need to be in the same group as the Galaxy system user""", - "default": "027", - "type": "str", - }, - ), - # ('route-uri', { - # 'default': '^/proxy/ goto:proxy' - # }), - # ('route', { - # 'default': '.* last:' - # }), - # ('route-label', { - # 'default': 'proxy' - # }), - # ('route-run', { - # 'default': 'rpcvar:TARGET_HOST galaxy_dynamic_proxy_mapper ${HTTP_HOST} ${cookie[galaxysession]}' - # }), - # ('route-run', { - # 'default': "['log:Proxy ${HTTP_HOST} to ${TARGET_HOST}', 'httpdumb:${TARGET_HOST}']", - # }), - # ('http-raw-body', { - # 'default': True - # }), - ] -) - -SHED_ONLY_UWSGI_OPTIONS = [ - ( - "cron", - { - "desc": """Task for rebuilding Toolshed search indexes using the uWSGI cron-like interface.""", - "default": "0 -1 -1 -1 -1 python scripts/tool_shed/build_ts_whoosh_index.py -c config/tool_shed.yml --config-section tool_shed", - "type": "str", - }, - ) -] - DROP_OPTION_VALUE = object() @@ -393,7 +188,6 @@ class App(NamedTuple): expected_app_factories: List[str] destination: str schema_path: str - uwsgi_module: str @property def app_name(self): @@ -417,28 +211,23 @@ class OptionValue(NamedTuple): GALAXY_APP = App( ["universe_wsgi.ini", "config/galaxy.ini"], "8080", - [ - "galaxy.web.buildapp:app_factory" - ], # TODO: Galaxy could call factory a few different things and they'd all be fine. + ["galaxy.web.buildapp:app_factory"], "config/galaxy.yml", str(GALAXY_CONFIG_SCHEMA_PATH), - "galaxy.webapps.galaxy.buildapp:uwsgi_app()", ) SHED_APP = App( ["tool_shed_wsgi.ini", "config/tool_shed.ini"], "9009", ["tool_shed.webapp.buildapp:app_factory"], "config/tool_shed.yml", - "lib/tool_shed/webapp/config_schema.yml", - "tool_shed.webapp.buildapp:uwsgi_app()", + str(TOOL_SHED_CONFIG_SCHEMA_PATH), ) REPORTS_APP = App( ["reports_wsgi.ini", "config/reports.ini"], "9001", ["galaxy.webapps.reports.buildapp:app_factory"], "config/reports.yml", - "lib/galaxy/webapps/reports/config_schema.yml", - "galaxy.webapps.reports.buildapp:uwsgi_app()", + str(REPORTS_CONFIG_SCHEMA_PATH), ) APPS = {"galaxy": GALAXY_APP, "tool_shed": SHED_APP, "reports": REPORTS_APP} @@ -504,42 +293,6 @@ def _write_option_rst(args, rst, key, heading_level, option_value): rst.write("\n\n") -def _build_uwsgi_schema(args, app_desc): - req = requests.get("https://raw.githubusercontent.com/unbit/uwsgi-docs/master/Options.rst") - rst_options = req.text - last_line = None - current_opt = None - - options = {} - option = None - for line in rst_options.splitlines(): - line = line.strip() - dots = "*" * len(line) - if line and (line == dots): - current_opt = last_line - option = { - "type": "any", - } - options[current_opt] = option - - if line.startswith("``parser``"): - parser = line.split(":", 1)[1].strip() - if parser == "uwsgi_opt_set_int": - option["type"] = "int" - # TODO: disptch on parser... - elif line.startswith("``help``"): - option["desc"] = line.split(":", 1)[1] - - last_line = line - schema = { - "type": "map", - "desc": "uwsgi definition, see https://uwsgi-docs.readthedocs.io/en/latest/Options.html", - "mapping": options, - } - contents = ordered_dump(schema) - _write_to_file(args, contents, UWSGI_SCHEMA_PATH) - - def _find_config(args, app_desc): path = os.path.join(args.galaxy_root, app_desc.destination) if not os.path.exists(path): @@ -640,53 +393,7 @@ def _run_conversion(args, app_desc): sys.exit(1) p = nice_config_parser(ini_config) - server_section = None - filters = {} - for section in p.sections(): - if section.startswith("server:"): - if server_section: - _warn(EXTRA_SERVER_MESSAGE % (server_section, section)) - else: - server_section = section - - if section.startswith("filter:"): - filter_name = section[len("filter:") :] - filter_type = p.get(section, "use") - if filter_type is None: - MISSING_FILTER_TYPE_MESSAGE - message = EXTRA_SERVER_MESSAGE % section - _warn(message) - continue - - if filter_type == "egg:PasteDeploy#prefix": - prefix = p.get(section, "prefix") - filters[filter_name] = PrefixFilter(filter_name, prefix) - elif filter_type == "egg:Paste#gzip": - filters[filter_name] = GzipFilter(filter_name) - else: - message = UNHANDLED_FILTER_TYPE_MESSAGE % (filter_type, section) - _warn(message) - continue - - if not server_section: - _warn("No server section found, using default uwsgi server definition.") - server_config = {} - else: - server_config = dict(p.items(server_section)) - app_items = _find_app_options_from_config_parser(p) - applied_filters = [] - if filters: - for key, value in app_items.items(): - if key == "filter-with": - if value in filters: - applied_filters.append(filters[value]) - else: - _warn(f"Unknown filter found [{value}], exiting...") - sys.exit(1) - - uwsgi_dict = _server_paste_to_uwsgi(app_desc, server_config, applied_filters) - app_dict = {} schema = app_desc.schema for key, value in app_items.items(): @@ -713,7 +420,6 @@ def _run_conversion(args, app_desc): app_dict[key] = option_value f = StringIO() - _write_section(args, f, "uwsgi", uwsgi_dict, uwsgi_hack=True) _write_section(args, f, app_desc.app_name, app_dict) destination = os.path.join(args.galaxy_root, app_desc.destination) _replace_file(args, f, app_desc, ini_config, destination) @@ -734,32 +440,17 @@ def _replace_file(args, f, app_desc, from_path, to_path): def _build_sample_yaml(args, app_desc): - if app_desc.app_name in ["tool_shed"]: - UWSGI_OPTIONS.update(SHED_ONLY_UWSGI_OPTIONS) schema = app_desc.schema f = StringIO() - for value in UWSGI_OPTIONS.values(): - for field in ["desc", "default"]: - if field not in value: - continue - field_value = value[field] - if not isinstance(field_value, str): - continue - - new_field_value = string.Template(field_value).safe_substitute( - **{ - "default_port": str(app_desc.default_port), - "app_name": app_desc.app_name, - "uwsgi_module": app_desc.uwsgi_module, - } - ) - value[field] = new_field_value description = getattr(schema, "description", None) if description: description = description.lstrip() as_comment = "\n".join(f"# {line}" for line in description.split("\n")) + "\n" f.write(as_comment) - _write_sample_section(args, f, "uwsgi", Schema(UWSGI_OPTIONS), as_comment=False, uwsgi_hack=True) + if app_desc.app_name == "galaxy": + if settings_to_sample is None: + raise Exception("Please install gravity to rebuild the sample config") + f.write(settings_to_sample()) _write_sample_section(args, f, app_desc.app_name, schema) destination = os.path.join(args.galaxy_root, app_desc.sample_destination) _write_to_file(args, f, destination) @@ -789,28 +480,27 @@ def _order_load_path(path): return raw_config -def _write_sample_section(args, f, section_header, schema, as_comment=True, uwsgi_hack=False): +def _write_sample_section(args, f, section_header, schema, as_comment=True): _write_header(f, section_header) for key, value in schema.app_schema.items(): default = None if "default" not in value else value["default"] option = schema.get_app_option(key) option_value = OptionValue(key, default, option) - # support uWSGI "dumb YAML parser" (unbit/uwsgi#863) key = option.get("key", key) - _write_option(args, f, key, option_value, as_comment=as_comment, uwsgi_hack=uwsgi_hack) + _write_option(args, f, key, option_value, as_comment=as_comment) -def _write_section(args, f, section_header, section_dict, uwsgi_hack=False): +def _write_section(args, f, section_header, section_dict): _write_header(f, section_header) for key, option_value in section_dict.items(): - _write_option(args, f, key, option_value, uwsgi_hack=uwsgi_hack) + _write_option(args, f, key, option_value) def _write_header(f, section_header): f.write(f"{section_header}:\n\n") -def _write_option(args, f, key, option_value, as_comment=False, uwsgi_hack=False): +def _write_option(args, f, key, option_value, as_comment=False): option, value = _parse_option_value(option_value) desc = _get_option_desc(option) comment = "" @@ -819,12 +509,7 @@ def _write_option(args, f, key, option_value, as_comment=False, uwsgi_hack=False # for double newlines which are replaced with a single newline. comment += "\n".join("\n".join(YAML_COMMENT_WRAPPER.wrap(_)) for _ in desc.split("\n\n")) + "\n" as_comment_str = "#" if as_comment else "" - if uwsgi_hack: - if option.get("type", "str") == "bool": - value = str(value).lower() - key_val_str = f"{key}: {value}" - else: - key_val_str = yaml.dump({key: value}, width=math.inf).lstrip("{").rstrip("\n}") + key_val_str = yaml.dump({key: value}, width=math.inf).lstrip("{").rstrip("\n}") lines = f"{comment}{as_comment_str}{key_val_str}" lines_idented = "\n".join(f" {line}" for line in lines.split("\n")) f.write(f"{lines_idented}\n\n") @@ -847,38 +532,6 @@ def _parse_option_value(option_value): return option, value -def _server_paste_to_uwsgi(app_desc, server_config, applied_filters): - uwsgi_dict = {} - port = server_config.get("port", app_desc.default_port) - host = server_config.get("host", "127.0.0.1") - - if server_config.get("use", "egg:Paste#http") != "egg:Paste#http": - raise Exception("Unhandled paste server 'use' value [%s], file must be manually migrate.") - - uwsgi_dict["http"] = f"{host}:{port}" - # default changing from 10 to 8 - uwsgi_dict["threads"] = int(server_config.get("threadpool_workers", 8)) - # required for static... - uwsgi_dict["http-raw-body"] = True - uwsgi_dict["offload-threads"] = 8 - - # Handle paste filters during conversion. - prefix = None - for applied_filter in applied_filters: - if isinstance(applied_filter, PrefixFilter): - prefix = applied_filter.prefix - break - elif isinstance(applied_filter, GzipFilter): - uwsgi_dict["http-auto-gzip"] = True - - if prefix: - uwsgi_dict["mount"] = f"{prefix}={app_desc.uwsgi_module}" - uwsgi_dict["manage-script-name"] = True - else: - uwsgi_dict["module"] = app_desc.uwsgi_module - return uwsgi_dict - - def _warn(message): print(f"WARNING: {message}") @@ -897,7 +550,6 @@ ACTIONS = { "build_sample_yaml": _build_sample_yaml, "validate": _validate, "lint": _lint, - "build_uwsgi_yaml": _build_uwsgi_schema, "build_rst": _to_rst, } diff --git a/lib/galaxy/config/sample/datatypes_conf.xml.sample b/lib/galaxy/config/sample/datatypes_conf.xml.sample index c712f074ace..bfd3d07a683 100644 --- a/lib/galaxy/config/sample/datatypes_conf.xml.sample +++ b/lib/galaxy/config/sample/datatypes_conf.xml.sample @@ -647,6 +647,7 @@ + diff --git a/lib/galaxy/config/sample/galaxy.yml.sample b/lib/galaxy/config/sample/galaxy.yml.sample index 61115cf8373..3281d5233a0 100644 --- a/lib/galaxy/config/sample/galaxy.yml.sample +++ b/lib/galaxy/config/sample/galaxy.yml.sample @@ -16,98 +16,125 @@ # # Config hackers are encouraged to check there before asking for help. # -uwsgi: +# Configuration for Gravity process manager. +# ``uwsgi:`` section will be ignored if Galaxy is started via Gravity commands (e.g ``./run.sh``, ``galaxy`` or ``galaxyctl``). +gravity: - # The address and port on which to listen. By default, only listen to - # localhost (galaxy will not be accessible over the network). Use - # ':8080' to listen on all available network interfaces. - http: 127.0.0.1:8080 + # Specify Galaxy's root directory. + # Gravity will attempt to find the root directory, but you can set the directory explicitly with this option. + # galaxy_root: - # By default uWSGI allocates a very small buffer (4096 bytes) for the - # headers of each request. If you start receiving "invalid request - # block size" in your logs, it could mean you need a bigger buffer. We - # recommend at least 16384. - buffer-size: 16384 + # Set to a directory that should contain log files for the processes controlled by Gravity. + # If not specified defaults to ``/logs``. + # log_dir: - # Number of web server (worker) processes to fork after the - # application has loaded. If this is set to greater than 1, - # thunder-lock likely should be enabled below. - processes: 1 + # Set to Galaxy's virtualenv directory. + # If not specified, Gravity assumes all processes are on PATH. + # virtualenv: - # Number of threads for each web server process. - threads: 4 + # Select the application server. + # ``gunicorn`` is the default application server. + # ``unicornherder`` is a production-oriented manager for (G)unicorn servers that automates zero-downtime Galaxy server restarts, + # similar to uWSGI Zerg Mode used in the past. + # Valid options are: gunicorn, unicornherder + # app_server: gunicorn - # Number of threads for serving static content and handling internal - # routing requests. - offload-threads: 2 + # Override the default instance name. + # this is hidden from you when running a single instance. + # instance_name: _default_ - # Mapping to serve static content. - static-map: /static=static + # Configuration for Gunicorn. + gunicorn: - # Mapping to serve the favicon. - static-map: /favicon.ico=static/favicon.ico + # The socket to bind. A string of the form: ``HOST``, ``HOST:PORT``, ``unix:PATH``, ``fd://FD``. An IP is a valid HOST. + # bind: localhost:8080 - # Allow serving certain assets out of `client`. Most modern Galaxy - # interfaces bundle all of this, but some older pages still serve - # these via symlink, requiring this rule. - static-safe: client/src/assets + # Controls the number of Galaxy application processes Gunicorn will spawn. + # Increased web performance can be attained by increasing this value. + # If Gunicorn is the only application on the server, a good starting value is the number of CPUs * 2 + 1. + # 4-12 workers should be able to handle hundreds if not thousands of requests per second. + # workers: 1 - # Enable the master process manager. Disabled by default for maximum - # compatibility with CTRL+C, but should be enabled for use with - # --daemon and/or production deployments. - master: false + # Gunicorn workers silent for more than this many seconds are killed and restarted. + # Value is a positive number or 0. Setting it to 0 has the effect of infinite timeouts by disabling timeouts for all workers entirely. + # If you disable the ``preload`` option workers need to have finished booting within the timeout. + # timeout: 300 - # Path to the application's Python virtual environment. If using Conda - # for Galaxy's framework dependencies (not tools!), do not set this. - virtualenv: .venv + # Extra arguments to pass to Gunicorn command line. + # extra_args: - # Path to the application's Python library. - pythonpath: lib + # Use Gunicorn's --preload option to fork workers after loading the Galaxy Application. + # Consumes less memory when multiple processes are configured. + # preload: true - # The entry point which returns the web application (e.g. Galaxy, - # Reports, etc.) that you are loading. - module: galaxy.webapps.galaxy.buildapp:uwsgi_app() + # Configuration for Celery Processes. + celery: - # Mount the web application (e.g. Galaxy, Reports, etc.) at the given - # URL prefix. Cannot be used together with 'module:' above. - #mount: /galaxy=galaxy.webapps.galaxy.buildapp:uwsgi_app() + # Number of Celery Workers to start. + # concurrency: 2 - # Make uWSGI rewrite PATH_INFO and SCRIPT_NAME according to - # mount-points. Set this to true if a URL prefix is used. - manage-script-name: false + # Log Level to use for Celery Worker. + # Valid options are: DEBUG, INFO, WARNING, ERROR + # loglevel: DEBUG - # It is usually a good idea to set this to ``true`` if processes is - # greater than 1. - thunder-lock: false + # Extra arguments to pass to Celery command line. + # extra_args: - # Cause uWSGI to respect the traditional behavior of dying on SIGTERM - # (its default is to brutally reload workers) - die-on-term: true + # Configuration for gx-it-proxy. + gx_it_proxy: - # Cause uWSGI to gracefully reload workers and mules upon receipt of - # SIGINT (its default is to brutally kill workers) - hook-master-start: unix_signal:2 gracefully_kill_them_all + # Set to true to start gx-it-proxy + # enable: false - # Cause uWSGI to gracefully reload workers and mules upon receipt of - # SIGTERM (its default is to brutally kill workers) - hook-master-start: unix_signal:15 gracefully_kill_them_all + # Public-facing IP of the proxy + # ip: localhost - # Feature necessary for proper mule signal handling on Python versions - # below 3.7.2. The default is set to false to prevent a runtime error - # under Python 3.7.2 and newer (see - # https://github.com/unbit/uwsgi/issues/1978). - py-call-osafterfork: false + # Public-facing port of the proxy + # port: 4002 - # Ensure application threads will run if `threads` is unset. - enable-threads: true + # Routes file to monitor. + # Should be set to the same path as ``interactivetools_map`` in the ``galaxy:`` section. + # sessions: database/interactivetools_map.sqlite - # uWSGI default umask. On some systems uWSGI has a default umask of - # 000, for Galaxy a somewhat safer default is chosen. If Galaxy - # submits jobs as real user then all users needs to be able to read - # the files, i.e. the umask needs to be '022' or the Galaxy users need - # to be in the same group as the Galaxy system user - umask: 027 + # Include verbose messages in gx-it-proxy + # verbose: true + # Forward all requests to IP. + # This is an advanced option that is only needed when proxying to remote interactive tool container that cannot be reached through the local network. + # forward_ip: + + # Forward all requests to port. + # This is an advanced option that is only needed when proxying to remote interactive tool container that cannot be reached through the local network. + # forward_port: + + # Rewrite location blocks with proxy port. + # This is an advanced option that is only needed when proxying to remote interactive tool container that cannot be reached through the local network. + # reverse_proxy: false + + # Configuration for tusd server (https://github.com/tus/tusd). + # The ``tusd`` binary must be installed manually and made available on PATH (e.g in galaxy's .venv/bin directory). + tusd: + + # Enable tusd server. + # If enabled, you also need to set up your proxy as outlined in https://docs.galaxyproject.org/en/latest/admin/nginx.html#receiving-files-via-the-tus-protocol. + # enable: false + + # Host to bind the tusd server to + # host: localhost + + # Port to bind the tusd server to + # port: 1080 + + # Directory to store uploads in. + # Must match ``tus_upload_store`` setting in ``galaxy:`` section. + # upload_dir: + + # Extra arguments to pass to tusd command line. + # extra_args: + + # Configure dynamic handlers in this section. + # See https://docs.galaxyproject.org/en/latest/admin/scaling.html#dynamically-defined-handlers for details. + # handlers: {} galaxy: # The directory that will be prepended to relative paths in options @@ -921,8 +948,13 @@ galaxy: # Enable InteractiveTools. #interactivetools_enable: false - # Proxy host - assumed to just be hosted on the same hostname and port - # as Galaxy by default. + # Set this to false to redirect users of Interactive tools directly to + # the Interactive tools proxy. `interactivetools_upstream_proxy` + # should only be set to false in development. + #interactivetools_upstream_proxy: true + + # Hostname and port of Interactive tools proxy. It is assumed to be + # hosted on the same hostname and port as Galaxy by default. #interactivetools_proxy_host: null # Base path for interactive tools running at a subpath without a @@ -1007,13 +1039,12 @@ galaxy: #galaxy_url_prefix: / # URL (with schema http/https) of the Galaxy instance as accessible - # within your local network - if specified used as a default by pulsar - # file staging and Jupyter Docker container for communicating back + # within your local network. This URL is used as a default by pulsar + # file staging and Interactive Tool containers for communicating back # with Galaxy via the API. - # If you are attempting to set up GIEs on Mac OS X with Docker Desktop - # for Mac and your Galaxy instance runs on port 8080 this should be - # 'http://host.docker.internal:8080'. For more details see - # https://docs.docker.com/docker-for-mac/networking/ + # If you plan to run Interactive Tools make sure the docker container + # can reach this URL. For more details see + # `job_conf.xml.interactivetools`. #galaxy_infrastructure_url: http://localhost:8080 # If the above URL cannot be determined ahead of time in dynamic @@ -1178,6 +1209,11 @@ galaxy: # documentation for the corresponding nginx configuration. #nginx_upload_job_files_path: null + # The upload store is a temporary directory in which files uploaded by + # the tus middleware or server will be placed. Defaults to + # new_file_path if not set. + #tus_upload_store: null + # Galaxy can upload user files in chunks without using nginx. Enable # the chunk uploader by specifying a chunk size larger than 0. The # chunk size is specified in bytes (default: 10MB). @@ -1336,11 +1372,6 @@ galaxy: # use_printdebug. It also causes the files used by PBS/SGE # (submission script, output, and error) to remain on disk after the # job is complete. - # In addition, this will set uWSGI's `honour-stdin` option to `true`; - # thus, preventing uWSGI from remapping stdin to `/dev/null` and - # enabling debugging with tools like pdb. To keep uWSGI's default - # setting, set `honor-stdin` to `false` in the `uwsgi` section of this - # configuration file. #debug: false # Check for WSGI compliance. @@ -1359,9 +1390,7 @@ galaxy: # responsible for preparing/submitting and collecting/finishing jobs, # and which can cause job errors if not shut down cleanly. If using # supervisord, consider also increasing the value of `stopwaitsecs`. - # If using job handler mules, consider also setting the - # `mule-reload-mercy` uWSGI option. See the Galaxy Admin Documentation - # for more. + # See the Galaxy Admin Documentation for more. #monitor_thread_join_timeout: 30 # Write thread status periodically to 'heartbeat.log', (careful, uses @@ -1371,8 +1400,7 @@ galaxy: # Control the period (in seconds) between dumps. Use -1 to disable. # Regardless of this setting, if use_heartbeat is enabled, you can - # send a Galaxy process (unless running with uWSGI) SIGUSR1 (`kill - # -USR1`) to force a dump. + # send a Galaxy process SIGUSR1 (`kill -USR1`) to force a dump. #heartbeat_interval: 20 # Heartbeat log filename. Can accept the template variables @@ -2339,3 +2367,6 @@ galaxy: # . #vault_config_file: vault_conf.yml + # Display built-in converters in the tool panel. + #display_builtin_converters: true + diff --git a/lib/galaxy/config/sample/galaxy_vscode.ini b/lib/galaxy/config/sample/galaxy_vscode.ini deleted file mode 100644 index 2aef4983e43..00000000000 --- a/lib/galaxy/config/sample/galaxy_vscode.ini +++ /dev/null @@ -1,30 +0,0 @@ -[server:main] - -# The internal HTTP server to use. Currently only Paste is provided. -use = egg:Paste#http - -# The port on which to listen. -port = 8080 - -# Define the gzip filter. -[filter:gzip] -use = egg:Paste#gzip - -# Define the proxy-prefix filter. -[filter:proxy-prefix] -use = egg:PasteDeploy#prefix - -[app:main] - -# The factory for the WSGI application. This should not be changed. -paste.app_factory = galaxy.web.buildapp:app_factory - -# The URL of the page to display in Galaxy's middle pane when loaded. -welcome_url = /static/welcome.html - -# Serve static content, which must be enabled if you're not serving it via a proxy server. -static_enabled = True -static_cache_time = 360 -static_dir = static/ -static_robots_txt = static/robots.txt - diff --git a/lib/galaxy/config/sample/job_conf.xml.sample_advanced b/lib/galaxy/config/sample/job_conf.xml.sample_advanced index 8cc36cc0536..76f35416fed 100644 --- a/lib/galaxy/config/sample/job_conf.xml.sample_advanced +++ b/lib/galaxy/config/sample/job_conf.xml.sample_advanced @@ -396,7 +396,6 @@ - `mem-self` - In-memory Self Assignment - `db-self`- Database Self Assignment - `db-preassign` - Database Preassignment - - `uwsgi-mule-message` - uWSGI Mule Messaging - `db-transaction-isolation` - Database Transaction Isolation - `db-skip-locked` - Database SKIP LOCKED diff --git a/lib/galaxy/config/sample/reports.yml.sample b/lib/galaxy/config/sample/reports.yml.sample index 1805c55f73e..1e1a75ed13a 100644 --- a/lib/galaxy/config/sample/reports.yml.sample +++ b/lib/galaxy/config/sample/reports.yml.sample @@ -1,95 +1,3 @@ -uwsgi: - - # The address and port on which to listen. By default, only listen to - # localhost (reports will not be accessible over the network). Use - # ':9001' to listen on all available network interfaces. - http: 127.0.0.1:9001 - - # By default uWSGI allocates a very small buffer (4096 bytes) for the - # headers of each request. If you start receiving "invalid request - # block size" in your logs, it could mean you need a bigger buffer. We - # recommend at least 16384. - buffer-size: 16384 - - # Number of web server (worker) processes to fork after the - # application has loaded. If this is set to greater than 1, - # thunder-lock likely should be enabled below. - processes: 1 - - # Number of threads for each web server process. - threads: 4 - - # Number of threads for serving static content and handling internal - # routing requests. - offload-threads: 2 - - # Mapping to serve static content. - static-map: /static=static - - # Mapping to serve the favicon. - static-map: /favicon.ico=static/favicon.ico - - # Allow serving certain assets out of `client`. Most modern Galaxy - # interfaces bundle all of this, but some older pages still serve - # these via symlink, requiring this rule. - static-safe: client/src/assets - - # Enable the master process manager. Disabled by default for maximum - # compatibility with CTRL+C, but should be enabled for use with - # --daemon and/or production deployments. - master: false - - # Path to the application's Python virtual environment. If using Conda - # for Galaxy's framework dependencies (not tools!), do not set this. - virtualenv: .venv - - # Path to the application's Python library. - pythonpath: lib - - # The entry point which returns the web application (e.g. Galaxy, - # Reports, etc.) that you are loading. - module: galaxy.webapps.reports.buildapp:uwsgi_app() - - # Mount the web application (e.g. Galaxy, Reports, etc.) at the given - # URL prefix. Cannot be used together with 'module:' above. - #mount: /galaxy=galaxy.webapps.reports.buildapp:uwsgi_app() - - # Make uWSGI rewrite PATH_INFO and SCRIPT_NAME according to - # mount-points. Set this to true if a URL prefix is used. - manage-script-name: false - - # It is usually a good idea to set this to ``true`` if processes is - # greater than 1. - thunder-lock: false - - # Cause uWSGI to respect the traditional behavior of dying on SIGTERM - # (its default is to brutally reload workers) - die-on-term: true - - # Cause uWSGI to gracefully reload workers and mules upon receipt of - # SIGINT (its default is to brutally kill workers) - hook-master-start: unix_signal:2 gracefully_kill_them_all - - # Cause uWSGI to gracefully reload workers and mules upon receipt of - # SIGTERM (its default is to brutally kill workers) - hook-master-start: unix_signal:15 gracefully_kill_them_all - - # Feature necessary for proper mule signal handling on Python versions - # below 3.7.2. The default is set to false to prevent a runtime error - # under Python 3.7.2 and newer (see - # https://github.com/unbit/uwsgi/issues/1978). - py-call-osafterfork: false - - # Ensure application threads will run if `threads` is unset. - enable-threads: true - - # uWSGI default umask. On some systems uWSGI has a default umask of - # 000, for Galaxy a somewhat safer default is chosen. If Galaxy - # submits jobs as real user then all users needs to be able to read - # the files, i.e. the umask needs to be '022' or the Galaxy users need - # to be in the same group as the Galaxy system user - umask: 027 - reports: # Verbosity of console log messages. Acceptable values can be found diff --git a/lib/galaxy/config/sample/tool_shed.yml.sample b/lib/galaxy/config/sample/tool_shed.yml.sample index 3b099773899..a4fe9607080 100644 --- a/lib/galaxy/config/sample/tool_shed.yml.sample +++ b/lib/galaxy/config/sample/tool_shed.yml.sample @@ -1,99 +1,3 @@ -uwsgi: - - # The address and port on which to listen. By default, only listen to - # localhost (tool_shed will not be accessible over the network). Use - # ':9009' to listen on all available network interfaces. - http: 127.0.0.1:9009 - - # By default uWSGI allocates a very small buffer (4096 bytes) for the - # headers of each request. If you start receiving "invalid request - # block size" in your logs, it could mean you need a bigger buffer. We - # recommend at least 16384. - buffer-size: 16384 - - # Number of web server (worker) processes to fork after the - # application has loaded. If this is set to greater than 1, - # thunder-lock likely should be enabled below. - processes: 1 - - # Number of threads for each web server process. - threads: 4 - - # Number of threads for serving static content and handling internal - # routing requests. - offload-threads: 2 - - # Mapping to serve static content. - static-map: /static=static - - # Mapping to serve the favicon. - static-map: /favicon.ico=static/favicon.ico - - # Allow serving certain assets out of `client`. Most modern Galaxy - # interfaces bundle all of this, but some older pages still serve - # these via symlink, requiring this rule. - static-safe: client/src/assets - - # Enable the master process manager. Disabled by default for maximum - # compatibility with CTRL+C, but should be enabled for use with - # --daemon and/or production deployments. - master: false - - # Path to the application's Python virtual environment. If using Conda - # for Galaxy's framework dependencies (not tools!), do not set this. - virtualenv: .venv - - # Path to the application's Python library. - pythonpath: lib - - # The entry point which returns the web application (e.g. Galaxy, - # Reports, etc.) that you are loading. - module: tool_shed.webapp.buildapp:uwsgi_app() - - # Mount the web application (e.g. Galaxy, Reports, etc.) at the given - # URL prefix. Cannot be used together with 'module:' above. - #mount: /galaxy=tool_shed.webapp.buildapp:uwsgi_app() - - # Make uWSGI rewrite PATH_INFO and SCRIPT_NAME according to - # mount-points. Set this to true if a URL prefix is used. - manage-script-name: false - - # It is usually a good idea to set this to ``true`` if processes is - # greater than 1. - thunder-lock: false - - # Cause uWSGI to respect the traditional behavior of dying on SIGTERM - # (its default is to brutally reload workers) - die-on-term: true - - # Cause uWSGI to gracefully reload workers and mules upon receipt of - # SIGINT (its default is to brutally kill workers) - hook-master-start: unix_signal:2 gracefully_kill_them_all - - # Cause uWSGI to gracefully reload workers and mules upon receipt of - # SIGTERM (its default is to brutally kill workers) - hook-master-start: unix_signal:15 gracefully_kill_them_all - - # Feature necessary for proper mule signal handling on Python versions - # below 3.7.2. The default is set to false to prevent a runtime error - # under Python 3.7.2 and newer (see - # https://github.com/unbit/uwsgi/issues/1978). - py-call-osafterfork: false - - # Ensure application threads will run if `threads` is unset. - enable-threads: true - - # uWSGI default umask. On some systems uWSGI has a default umask of - # 000, for Galaxy a somewhat safer default is chosen. If Galaxy - # submits jobs as real user then all users needs to be able to read - # the files, i.e. the umask needs to be '022' or the Galaxy users need - # to be in the same group as the Galaxy system user - umask: 027 - - # Task for rebuilding Toolshed search indexes using the uWSGI - # cron-like interface. - cron: 0 -1 -1 -1 -1 python scripts/tool_shed/build_ts_whoosh_index.py -c config/tool_shed.yml --config-section tool_shed - tool_shed: # Verbosity of console log messages. Acceptable values can be found diff --git a/lib/galaxy/config/sample/workflow_schedulers_conf.xml.sample b/lib/galaxy/config/sample/workflow_schedulers_conf.xml.sample index 1c83136f870..5905ee34c6e 100644 --- a/lib/galaxy/config/sample/workflow_schedulers_conf.xml.sample +++ b/lib/galaxy/config/sample/workflow_schedulers_conf.xml.sample @@ -18,19 +18,7 @@ in job_conf.xml). The options here are the same as is documented for in - job_conf.xml.sample_advanced with two exceptions: - - - If a uWSGI farm named `workflow-schedulers` is present, it will be - preferred, followed by `job-handlers`. If any untagged handlers are - defined in this configuration they are eligible to schedule workflows - in addition to any matching mules. - - If uWSGI farms are present, the default assignment method is - `db-preassign` rather than `uwsgi-mule-message`, because `db-preassign` - is deterministic. All workflows scheduled in a single history will be - assigned to the same handler, ensuring they are scheduled serially - (preventing their outputs from being interleaved in the history). You - can override this by explicitly setting - `assign_with="uwsgi-mule-message"`. + job_conf.xml.sample_advanced --> initialize with latest version and return - 2) Database older than migration support --> fail and require manual update - 3) Database at state where migrate support introduced --> add version control information but make no changes (might still require manual update) - 4) Database versioned but out of date --> fail with informative message, user must run "sh manage_db.sh upgrade" - """ - # Create the base database if it doesn't yet exist. - engine_options = engine_options or {} - new_database = not database_exists(url) - if new_database: - template = app and getattr(app.config, "database_template", None) - encoding = app and getattr(app.config, "database_encoding", None) - create_kwds = {} - - message = f"Creating database for URI [{url}]" - if template: - message += f" from template [{template}]" - create_kwds["template"] = template - if encoding: - message += f" with encoding [{encoding}]" - create_kwds["encoding"] = encoding - log.info(message) - create_database(url, **create_kwds) - - # Create engine and metadata - engine = create_engine(url, **engine_options) - - def migrate(): - try: - # Declare the database to be under a repository's version control - db_schema = schema.ControlledSchema.create(engine, migrate_repository) - except Exception: - # The database is already under version control - db_schema = schema.ControlledSchema(engine, migrate_repository) - # Apply all scripts to get to current version - migrate_to_current_version(engine, db_schema) - - def migrate_from_scratch(): - if not os.environ.get("GALAXY_TEST_FORCE_DATABASE_MIGRATION"): - log.info("Creating new database from scratch, skipping migrations") - current_version = migrate_repository.version().version - mapping.init(file_path="/tmp", url=url, map_install_models=map_install_models, create_tables=True) - schema.ControlledSchema.create(engine, migrate_repository, version=current_version) - db_schema = schema.ControlledSchema(engine, migrate_repository) - assert db_schema.version == current_version - migrate() - if app: - # skips the tool migration process. - app.new_installation = True - - meta = MetaData(bind=engine) - if new_database: - migrate_from_scratch() - return - elif app and getattr(app.config, "database_auto_migrate", False): - migrate() - return - - # Try to load dataset table - try: - Table("dataset", meta, autoload=True) - except NoSuchTableError: - # No 'dataset' table means a completely uninitialized database. - log.info("No database, initializing") - migrate_from_scratch() - return - try: - hda_table = Table("history_dataset_association", meta, autoload=True) - except NoSuchTableError: - raise Exception( - "Your database is older than hg revision 1464:c7acaa1bb88f and will need to be updated manually" - ) - # There is a 'history_dataset_association' table, so we (hopefully) have - # version 1 of the database, but without the migrate_version table. This - # happens if the user has a build from right before migration was added. - # Verify that this is true, if it is any older they'll have to update - # manually - if "copied_from_history_dataset_association_id" not in hda_table.c: - # The 'copied_from_history_dataset_association_id' column was added in - # rev 1464:c7acaa1bb88f. This is the oldest revision we currently do - # automated versioning for, so stop here - raise Exception( - "Your database is older than hg revision 1464:c7acaa1bb88f and will need to be updated manually" - ) - # At revision 1464:c7acaa1bb88f or greater (database version 1), make sure - # that the db has version information. This is the trickiest case -- we - # have a database but no version control, and are assuming it is a certain - # version. If the user has postion version 1 changes this could cause - # problems - try: - Table("migrate_version", meta, autoload=True) - except NoSuchTableError: - # The database exists but is not yet under migrate version control, so init with version 1 - log.info("Adding version control to existing database") - try: - Table("metadata_file", meta, autoload=True) - schema.ControlledSchema.create(engine, migrate_repository, version=2) - except NoSuchTableError: - schema.ControlledSchema.create(engine, migrate_repository, version=1) - # Verify that the code and the DB are in sync - db_schema = schema.ControlledSchema(engine, migrate_repository) - if migrate_repository.versions.latest != db_schema.version: - config_arg = "" - if ( - galaxy_config_file - and os.path.abspath(os.path.join(os.getcwd(), "config", "galaxy.ini")) != galaxy_config_file - ): - config_arg = f" -c {galaxy_config_file.replace(os.path.abspath(os.getcwd()), '.')}" - expect_msg = "Your database has version '%d' but this code expects version '%d'" % ( - db_schema.version, - migrate_repository.versions.latest, - ) - instructions = "" - if db_schema.version > migrate_repository.versions.latest: - instructions = "To downgrade the database schema you have to checkout the Galaxy version that you were running previously. " - cmd_msg = "sh manage_db.sh%s downgrade %d" % (config_arg, migrate_repository.versions.latest) - else: - cmd_msg = f"sh manage_db.sh{config_arg} upgrade" - backup_msg = f"Please backup your database and then migrate the database schema by running '{cmd_msg}'." - allow_future_database = os.environ.get("GALAXY_ALLOW_FUTURE_DATABASE", False) - if db_schema.version > migrate_repository.versions.latest and allow_future_database: - log.warning( - "WARNING: Database is from the future, but GALAXY_ALLOW_FUTURE_DATABASE is set, so Galaxy will continue to start." - ) - else: - raise Exception(f"{expect_msg}. {instructions}{backup_msg}") - else: - log.info("At database version %d" % db_schema.version) - - -def migrate_to_current_version(engine, schema): - # Changes to get to current version - try: - changeset = schema.changeset(None) - except Exception as e: - log.error(f"Problem determining migration changeset for engine [{engine}]") - raise e - for ver, change in changeset: - nextver = ver + changeset.step - log.info(f"Migrating {ver} -> {nextver}... ") - old_stdout = sys.stdout - - class FakeStdout: - def __init__(self): - self.buffer = [] - - def write(self, s): - self.buffer.append(s) - - def flush(self): - pass - - sys.stdout = FakeStdout() - try: - schema.runchange(ver, change, changeset.step) - finally: - for message in "".join(sys.stdout.buffer).split("\n"): - log.info(message) - sys.stdout = old_stdout diff --git a/lib/galaxy/model/migrate/migrate.cfg b/lib/galaxy/model/migrate/migrate.cfg deleted file mode 100644 index 3fd7400ff42..00000000000 --- a/lib/galaxy/model/migrate/migrate.cfg +++ /dev/null @@ -1,20 +0,0 @@ -[db_settings] -# Used to identify which repository this database is versioned under. -# You can use the name of your project. -repository_id=Galaxy - -# The name of the database table used to track the schema version. -# This name shouldn't already be used by your project. -# If this is changed once a database is under version control, you'll need to -# change the table name in each database too. -version_table=migrate_version - -# When committing a change script, Migrate will attempt to generate the -# sql for all supported databases; normally, if one of them fails - probably -# because you don't have that database installed - it is ignored and the -# commit continues, perhaps ending successfully. -# Databases in this list MUST compile successfully during a commit, or the -# entire commit will fail. List the databases your application will actually -# be using to ensure your updates to that database work properly. -# This must be a list; example: ['postgres','sqlite'] -required_dbs=[] diff --git a/lib/galaxy/model/migrate/versions/0001_initial_tables.py b/lib/galaxy/model/migrate/versions/0001_initial_tables.py deleted file mode 100644 index f9f4fcf116d..00000000000 --- a/lib/galaxy/model/migrate/versions/0001_initial_tables.py +++ /dev/null @@ -1,283 +0,0 @@ -import datetime -import logging - -from sqlalchemy import ( - Boolean, - Column, - DateTime, - ForeignKey, - Integer, - MetaData, - Numeric, - String, - Table, - TEXT, -) - -# Need our custom types, but don't import anything else from model -from galaxy.model.custom_types import ( - JSONType, - MetadataType, - TrimmedString, -) - -log = logging.getLogger(__name__) -now = datetime.datetime.utcnow -metadata = MetaData() - -# Tables as of changeset 1464:c7acaa1bb88f -User_table = Table( - "galaxy_user", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("email", TrimmedString(255), nullable=False), - Column("password", TrimmedString(40), nullable=False), - Column("external", Boolean, default=False), -) - -History_table = Table( - "history", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, index=True, default=now, onupdate=now), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True), - Column("name", TrimmedString(255)), - Column("hid_counter", Integer, default=1), - Column("deleted", Boolean, index=True, default=False), - Column("purged", Boolean, index=True, default=False), - Column("genome_build", TrimmedString(40)), -) - -HistoryDatasetAssociation_table = Table( - "history_dataset_association", - metadata, - Column("id", Integer, primary_key=True), - Column("history_id", Integer, ForeignKey("history.id"), index=True), - Column("dataset_id", Integer, ForeignKey("dataset.id"), index=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column( - "copied_from_history_dataset_association_id", - Integer, - ForeignKey("history_dataset_association.id"), - nullable=True, - ), - Column("hid", Integer), - Column("name", TrimmedString(255)), - Column("info", TrimmedString(255)), - Column("blurb", TrimmedString(255)), - Column("peek", TEXT), - Column("extension", TrimmedString(64)), - Column("metadata", MetadataType, key="_metadata"), - Column("parent_id", Integer, ForeignKey("history_dataset_association.id"), nullable=True), - Column("designation", TrimmedString(255)), - Column("deleted", Boolean, index=True, default=False), - Column("visible", Boolean), -) - -Dataset_table = Table( - "dataset", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, index=True, default=now, onupdate=now), - Column("state", TrimmedString(64)), - Column("deleted", Boolean, index=True, default=False), - Column("purged", Boolean, index=True, default=False), - Column("purgable", Boolean, default=True), - Column("external_filename", TEXT), - Column("_extra_files_path", TEXT), - Column("file_size", Numeric(15, 0)), -) - -ImplicitlyConvertedDatasetAssociation_table = Table( - "implicitly_converted_dataset_association", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("hda_id", Integer, ForeignKey("history_dataset_association.id"), index=True, nullable=True), - Column("hda_parent_id", Integer, ForeignKey("history_dataset_association.id"), index=True), - Column("deleted", Boolean, index=True, default=False), - Column("metadata_safe", Boolean, index=True, default=True), - Column("type", TrimmedString(255)), -) - -ValidationError_table = Table( - "validation_error", - metadata, - Column("id", Integer, primary_key=True), - Column("dataset_id", Integer, ForeignKey("history_dataset_association.id"), index=True), - Column("message", TrimmedString(255)), - Column("err_type", TrimmedString(64)), - Column("attributes", TEXT), -) - -Job_table = Table( - "job", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("history_id", Integer, ForeignKey("history.id"), index=True), - Column("tool_id", String(255)), - Column("tool_version", TEXT, default="1.0.0"), - Column("state", String(64)), - Column("info", TrimmedString(255)), - Column("command_line", TEXT), - Column("param_filename", String(1024)), - Column("runner_name", String(255)), - Column("stdout", TEXT), - Column("stderr", TEXT), - Column("traceback", TEXT), - Column("session_id", Integer, ForeignKey("galaxy_session.id"), index=True, nullable=True), - Column("job_runner_name", String(255)), - Column("job_runner_external_id", String(255)), -) - -JobParameter_table = Table( - "job_parameter", - metadata, - Column("id", Integer, primary_key=True), - Column("job_id", Integer, ForeignKey("job.id"), index=True), - Column("name", String(255)), - Column("value", TEXT), -) - -JobToInputDatasetAssociation_table = Table( - "job_to_input_dataset", - metadata, - Column("id", Integer, primary_key=True), - Column("job_id", Integer, ForeignKey("job.id"), index=True), - Column("dataset_id", Integer, ForeignKey("history_dataset_association.id"), index=True), - Column("name", String(255)), -) - -JobToOutputDatasetAssociation_table = Table( - "job_to_output_dataset", - metadata, - Column("id", Integer, primary_key=True), - Column("job_id", Integer, ForeignKey("job.id"), index=True), - Column("dataset_id", Integer, ForeignKey("history_dataset_association.id"), index=True), - Column("name", String(255)), -) - -Event_table = Table( - "event", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("history_id", Integer, ForeignKey("history.id"), index=True, nullable=True), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True, nullable=True), - Column("message", TrimmedString(1024)), - Column("session_id", Integer, ForeignKey("galaxy_session.id"), index=True, nullable=True), - Column("tool_id", String(255)), -) - -GalaxySession_table = Table( - "galaxy_session", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True, nullable=True), - Column("remote_host", String(255)), - Column("remote_addr", String(255)), - Column("referer", TEXT), - Column("current_history_id", Integer, ForeignKey("history.id"), nullable=True), - Column("session_key", TrimmedString(255), index=True, unique=True), - Column("is_valid", Boolean, default=False), - Column("prev_session_id", Integer), -) - -GalaxySessionToHistoryAssociation_table = Table( - "galaxy_session_to_history", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("session_id", Integer, ForeignKey("galaxy_session.id"), index=True), - Column("history_id", Integer, ForeignKey("history.id"), index=True), -) - -StoredWorkflow_table = Table( - "stored_workflow", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True, nullable=False), - Column( - "latest_workflow_id", - Integer, - ForeignKey("workflow.id", use_alter=True, name="stored_workflow_latest_workflow_id_fk"), - index=True, - ), - Column("name", TEXT), - Column("deleted", Boolean, default=False), -) - -Workflow_table = Table( - "workflow", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("stored_workflow_id", Integer, ForeignKey("stored_workflow.id"), index=True, nullable=False), - Column("name", TEXT), - Column("has_cycles", Boolean), - Column("has_errors", Boolean), -) - -WorkflowStep_table = Table( - "workflow_step", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("workflow_id", Integer, ForeignKey("workflow.id"), index=True, nullable=False), - Column("type", String(64)), - Column("tool_id", TEXT), - Column("tool_version", TEXT), - Column("tool_inputs", JSONType), - Column("tool_errors", JSONType), - Column("position", JSONType), - Column("config", JSONType), - Column("order_index", Integer), -) - -WorkflowStepConnection_table = Table( - "workflow_step_connection", - metadata, - Column("id", Integer, primary_key=True), - Column("output_step_id", Integer, ForeignKey("workflow_step.id"), index=True), - Column("input_step_id", Integer, ForeignKey("workflow_step.id"), index=True), - Column("output_name", TEXT), - Column("input_name", TEXT), -) - -StoredWorkflowUserShareAssociation_table = Table( - "stored_workflow_user_share_connection", - metadata, - Column("id", Integer, primary_key=True), - Column("stored_workflow_id", Integer, ForeignKey("stored_workflow.id"), index=True), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True), -) - -StoredWorkflowMenuEntry_table = Table( - "stored_workflow_menu_entry", - metadata, - Column("id", Integer, primary_key=True), - Column("stored_workflow_id", Integer, ForeignKey("stored_workflow.id"), index=True), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True), - Column("order_index", Integer), -) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.create_all() diff --git a/lib/galaxy/model/migrate/versions/0002_metadata_file_table.py b/lib/galaxy/model/migrate/versions/0002_metadata_file_table.py deleted file mode 100644 index b61fe4bf580..00000000000 --- a/lib/galaxy/model/migrate/versions/0002_metadata_file_table.py +++ /dev/null @@ -1,53 +0,0 @@ -""" -""" - -import datetime -import logging - -from sqlalchemy import ( - Boolean, - Column, - DateTime, - ForeignKey, - Integer, - MetaData, - Table, - TEXT, -) - -from galaxy.model.migrate.versions.util import ( - create_table, - drop_table, -) - -log = logging.getLogger(__name__) -now = datetime.datetime.utcnow -metadata = MetaData() - -# New table in changeset 1568:0b022adfdc34 -MetadataFile_table = Table( - "metadata_file", - metadata, - Column("id", Integer, primary_key=True), - Column("name", TEXT), - Column("hda_id", Integer, ForeignKey("history_dataset_association.id"), index=True, nullable=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, index=True, default=now, onupdate=now), - Column("deleted", Boolean, index=True, default=False), - Column("purged", Boolean, index=True, default=False), -) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - create_table(MetadataFile_table) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - drop_table(MetadataFile_table) diff --git a/lib/galaxy/model/migrate/versions/0003_security_and_libraries.py b/lib/galaxy/model/migrate/versions/0003_security_and_libraries.py deleted file mode 100644 index 27b09337bca..00000000000 --- a/lib/galaxy/model/migrate/versions/0003_security_and_libraries.py +++ /dev/null @@ -1,745 +0,0 @@ -""" -""" - -import datetime -import logging - -from migrate import ForeignKeyConstraint -from sqlalchemy import ( - Boolean, - Column, - DateTime, - ForeignKey, - Index, - Integer, - MetaData, - String, - Table, - TEXT, -) - -from galaxy.model.custom_types import ( - JSONType, - MetadataType, - TrimmedString, -) -from galaxy.model.migrate.versions.util import ( - add_column, - add_index, - drop_column, - drop_index, - drop_table, - engine_false, - localtimestamp, - nextval, -) - -log = logging.getLogger(__name__) -now = datetime.datetime.utcnow -metadata = MetaData() - -# New tables as of changeset 2341:5498ac35eedd -Group_table = Table( - "galaxy_group", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("name", String(255), index=True, unique=True), - Column("deleted", Boolean, index=True, default=False), -) - -UserGroupAssociation_table = Table( - "user_group_association", - metadata, - Column("id", Integer, primary_key=True), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True), - Column("group_id", Integer, ForeignKey("galaxy_group.id"), index=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), -) - -UserRoleAssociation_table = Table( - "user_role_association", - metadata, - Column("id", Integer, primary_key=True), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True), - Column("role_id", Integer, ForeignKey("role.id"), index=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), -) - -GroupRoleAssociation_table = Table( - "group_role_association", - metadata, - Column("id", Integer, primary_key=True), - Column("group_id", Integer, ForeignKey("galaxy_group.id"), index=True), - Column("role_id", Integer, ForeignKey("role.id"), index=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), -) - -Role_table = Table( - "role", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("name", String(255), index=True, unique=True), - Column("description", TEXT), - Column("type", String(40), index=True), - Column("deleted", Boolean, index=True, default=False), -) - -DatasetPermissions_table = Table( - "dataset_permissions", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("action", TEXT), - Column("dataset_id", Integer, ForeignKey("dataset.id"), index=True), - Column("role_id", Integer, ForeignKey("role.id"), index=True), -) - -LibraryPermissions_table = Table( - "library_permissions", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("action", TEXT), - Column("library_id", Integer, ForeignKey("library.id"), nullable=True, index=True), - Column("role_id", Integer, ForeignKey("role.id"), index=True), -) - -LibraryFolderPermissions_table = Table( - "library_folder_permissions", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("action", TEXT), - Column("library_folder_id", Integer, ForeignKey("library_folder.id"), nullable=True, index=True), - Column("role_id", Integer, ForeignKey("role.id"), index=True), -) - -LibraryDatasetPermissions_table = Table( - "library_dataset_permissions", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("action", TEXT), - Column("library_dataset_id", Integer, ForeignKey("library_dataset.id"), nullable=True, index=True), - Column("role_id", Integer, ForeignKey("role.id"), index=True), -) - -LibraryDatasetDatasetAssociationPermissions_table = Table( - "library_dataset_dataset_association_permissions", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("action", TEXT), - Column( - "library_dataset_dataset_association_id", - Integer, - ForeignKey("library_dataset_dataset_association.id"), - nullable=True, - ), - Column("role_id", Integer, ForeignKey("role.id"), index=True), -) -Index( - "ix_lddap_library_dataset_dataset_association_id", - LibraryDatasetDatasetAssociationPermissions_table.c.library_dataset_dataset_association_id, -) - -LibraryItemInfoPermissions_table = Table( - "library_item_info_permissions", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("action", TEXT), - Column("library_item_info_id", Integer, ForeignKey("library_item_info.id"), nullable=True, index=True), - Column("role_id", Integer, ForeignKey("role.id"), index=True), -) - -LibraryItemInfoTemplatePermissions_table = Table( - "library_item_info_template_permissions", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("action", TEXT), - Column("library_item_info_template_id", Integer, ForeignKey("library_item_info_template.id"), nullable=True), - Column("role_id", Integer, ForeignKey("role.id"), index=True), -) -Index( - "ix_liitp_library_item_info_template_id", LibraryItemInfoTemplatePermissions_table.c.library_item_info_template_id -) - -DefaultUserPermissions_table = Table( - "default_user_permissions", - metadata, - Column("id", Integer, primary_key=True), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True), - Column("action", TEXT), - Column("role_id", Integer, ForeignKey("role.id"), index=True), -) - -DefaultHistoryPermissions_table = Table( - "default_history_permissions", - metadata, - Column("id", Integer, primary_key=True), - Column("history_id", Integer, ForeignKey("history.id"), index=True), - Column("action", TEXT), - Column("role_id", Integer, ForeignKey("role.id"), index=True), -) - -LibraryDataset_table = Table( - "library_dataset", - metadata, - Column("id", Integer, primary_key=True), - Column( - "library_dataset_dataset_association_id", - Integer, - ForeignKey( - "library_dataset_dataset_association.id", use_alter=True, name="library_dataset_dataset_association_id_fk" - ), - nullable=True, - index=True, - ), # current version of dataset, if null, there is not a current version selected - Column("folder_id", Integer, ForeignKey("library_folder.id"), index=True), - Column("order_id", Integer), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column( - "name", TrimmedString(255), key="_name" - ), # when not None/null this will supercede display in library (but not when imported into user's history?) - Column( - "info", TrimmedString(255), key="_info" - ), # when not None/null this will supercede display in library (but not when imported into user's history?) - Column("deleted", Boolean, index=True, default=False), -) - -LibraryDatasetDatasetAssociation_table = Table( - "library_dataset_dataset_association", - metadata, - Column("id", Integer, primary_key=True), - Column("library_dataset_id", Integer, ForeignKey("library_dataset.id"), index=True), - Column("dataset_id", Integer, ForeignKey("dataset.id"), index=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column( - "copied_from_history_dataset_association_id", - Integer, - ForeignKey( - "history_dataset_association.id", use_alter=True, name="history_dataset_association_dataset_id_fkey" - ), - nullable=True, - ), - Column( - "copied_from_library_dataset_dataset_association_id", - Integer, - ForeignKey( - "library_dataset_dataset_association.id", use_alter=True, name="library_dataset_dataset_association_id_fkey" - ), - nullable=True, - ), - Column("name", TrimmedString(255)), - Column("info", TrimmedString(255)), - Column("blurb", TrimmedString(255)), - Column("peek", TEXT), - Column("extension", TrimmedString(64)), - Column("metadata", MetadataType, key="_metadata"), - Column("parent_id", Integer, ForeignKey("library_dataset_dataset_association.id"), nullable=True), - Column("designation", TrimmedString(255)), - Column("deleted", Boolean, index=True, default=False), - Column("visible", Boolean), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True), - Column("message", TrimmedString(255)), -) - -Library_table = Table( - "library", - metadata, - Column("id", Integer, primary_key=True), - Column("root_folder_id", Integer, ForeignKey("library_folder.id"), index=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("name", String(255), index=True), - Column("deleted", Boolean, index=True, default=False), - Column("purged", Boolean, index=True, default=False), - Column("description", TEXT), -) - -LibraryFolder_table = Table( - "library_folder", - metadata, - Column("id", Integer, primary_key=True), - Column("parent_id", Integer, ForeignKey("library_folder.id"), nullable=True, index=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("name", TEXT), - Column("description", TEXT), - Column("order_id", Integer), - Column("item_count", Integer), - Column("deleted", Boolean, index=True, default=False), - Column("purged", Boolean, index=True, default=False), - Column("genome_build", TrimmedString(40)), -) - -LibraryItemInfoTemplateElement_table = Table( - "library_item_info_template_element", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("optional", Boolean, index=True, default=True), - Column("deleted", Boolean, index=True, default=False), - Column("name", TEXT), - Column("description", TEXT), - Column("type", TEXT, default="string"), - Column("order_id", Integer), - Column("options", JSONType), - Column("library_item_info_template_id", Integer, ForeignKey("library_item_info_template.id")), -) -Index("ix_liite_library_item_info_template_id", LibraryItemInfoTemplateElement_table.c.library_item_info_template_id) - -LibraryItemInfoTemplate_table = Table( - "library_item_info_template", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("optional", Boolean, index=True, default=True), - Column("deleted", Boolean, index=True, default=False), - Column("name", TEXT), - Column("description", TEXT), - Column("item_count", Integer, default=0), -) - -LibraryInfoTemplateAssociation_table = Table( - "library_info_template_association", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("library_id", Integer, ForeignKey("library.id"), nullable=True, index=True), - Column("library_item_info_template_id", Integer, ForeignKey("library_item_info_template.id")), -) -Index("ix_lita_library_item_info_template_id", LibraryInfoTemplateAssociation_table.c.library_item_info_template_id) - -LibraryFolderInfoTemplateAssociation_table = Table( - "library_folder_info_template_association", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("library_folder_id", Integer, ForeignKey("library_folder.id"), nullable=True, index=True), - Column("library_item_info_template_id", Integer, ForeignKey("library_item_info_template.id")), -) -Index( - "ix_lfita_library_item_info_template_id", LibraryFolderInfoTemplateAssociation_table.c.library_item_info_template_id -) - -LibraryDatasetInfoTemplateAssociation_table = Table( - "library_dataset_info_template_association", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("library_dataset_id", Integer, ForeignKey("library_dataset.id"), nullable=True, index=True), - Column("library_item_info_template_id", Integer, ForeignKey("library_item_info_template.id")), -) -Index( - "ix_ldita_library_item_info_template_id", - LibraryDatasetInfoTemplateAssociation_table.c.library_item_info_template_id, -) - -LibraryDatasetDatasetInfoTemplateAssociation_table = Table( - "library_dataset_dataset_info_template_association", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column( - "library_dataset_dataset_association_id", - Integer, - ForeignKey("library_dataset_dataset_association.id"), - nullable=True, - ), - Column("library_item_info_template_id", Integer, ForeignKey("library_item_info_template.id")), -) -Index( - "ix_lddita_library_dataset_dataset_association_id", - LibraryDatasetDatasetInfoTemplateAssociation_table.c.library_dataset_dataset_association_id, -) -Index( - "ix_lddita_library_item_info_template_id", - LibraryDatasetDatasetInfoTemplateAssociation_table.c.library_item_info_template_id, -) - -LibraryItemInfoElement_table = Table( - "library_item_info_element", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("contents", JSONType), - Column("library_item_info_id", Integer, ForeignKey("library_item_info.id"), index=True), - Column("library_item_info_template_element_id", Integer, ForeignKey("library_item_info_template_element.id")), -) -Index( - "ix_liie_library_item_info_template_element_id", - LibraryItemInfoElement_table.c.library_item_info_template_element_id, -) - -LibraryItemInfo_table = Table( - "library_item_info", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("deleted", Boolean, index=True, default=False), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), nullable=True, index=True), - Column( - "library_item_info_template_id", Integer, ForeignKey("library_item_info_template.id"), nullable=True, index=True - ), -) - -LibraryInfoAssociation_table = Table( - "library_info_association", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("library_id", Integer, ForeignKey("library.id"), nullable=True, index=True), - Column("library_item_info_id", Integer, ForeignKey("library_item_info.id"), index=True), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), nullable=True, index=True), -) - -LibraryFolderInfoAssociation_table = Table( - "library_folder_info_association", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("library_folder_id", Integer, ForeignKey("library_folder.id"), nullable=True, index=True), - Column("library_item_info_id", Integer, ForeignKey("library_item_info.id"), index=True), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), nullable=True, index=True), -) - -LibraryDatasetInfoAssociation_table = Table( - "library_dataset_info_association", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("library_dataset_id", Integer, ForeignKey("library_dataset.id"), nullable=True, index=True), - Column("library_item_info_id", Integer, ForeignKey("library_item_info.id"), index=True), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), nullable=True, index=True), -) - -LibraryDatasetDatasetInfoAssociation_table = Table( - "library_dataset_dataset_info_association", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column( - "library_dataset_dataset_association_id", - Integer, - ForeignKey("library_dataset_dataset_association.id"), - nullable=True, - ), - Column("library_item_info_id", Integer, ForeignKey("library_item_info.id")), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), nullable=True, index=True), -) -Index( - "ix_lddia_library_dataset_dataset_association_id", - LibraryDatasetDatasetInfoAssociation_table.c.library_dataset_dataset_association_id, -) -Index("ix_lddia_library_item_info_id", LibraryDatasetDatasetInfoAssociation_table.c.library_item_info_id) - -JobExternalOutputMetadata_table = Table( - "job_external_output_metadata", - metadata, - Column("id", Integer, primary_key=True), - Column("job_id", Integer, ForeignKey("job.id"), index=True), - Column( - "history_dataset_association_id", - Integer, - ForeignKey("history_dataset_association.id"), - index=True, - nullable=True, - ), - Column( - "library_dataset_dataset_association_id", - Integer, - ForeignKey("library_dataset_dataset_association.id"), - nullable=True, - ), - Column("filename_in", String(255)), - Column("filename_out", String(255)), - Column("filename_results_code", String(255)), - Column("filename_kwds", String(255)), - Column("job_runner_external_pid", String(255)), -) -Index( - "ix_jeom_library_dataset_dataset_association_id", - JobExternalOutputMetadata_table.c.library_dataset_dataset_association_id, -) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - # Add 2 new columns to the galaxy_user table - User_table = Table("galaxy_user", metadata, autoload=True) - col = Column("deleted", Boolean, index=True, default=False) - add_column(col, User_table, metadata, index_name="ix_galaxy_user_deleted") - col = Column("purged", Boolean, index=True, default=False) - add_column(col, User_table, metadata, index_name="ix_galaxy_user_purged") - # Add 1 new column to the history_dataset_association table - HistoryDatasetAssociation_table = Table("history_dataset_association", metadata, autoload=True) - col = Column("copied_from_library_dataset_dataset_association_id", Integer, nullable=True) - add_column(col, HistoryDatasetAssociation_table, metadata) - # Add 1 new column to the metadata_file table - MetadataFile_table = Table("metadata_file", metadata, autoload=True) - col = Column("lda_id", Integer, index=True, nullable=True) - add_column(col, MetadataFile_table, metadata, index_name="ix_metadata_file_lda_id") - # Add 1 new column to the stored_workflow table - changeset 2328 - StoredWorkflow_table = Table( - "stored_workflow", - metadata, - Column( - "latest_workflow_id", - Integer, - ForeignKey("workflow.id", use_alter=True, name="stored_workflow_latest_workflow_id_fk"), - index=True, - ), - autoload=True, - extend_existing=True, - ) - col = Column("importable", Boolean, default=False) - add_column(col, StoredWorkflow_table, metadata) - # Create an index on the Job.state column - changeset 2192 - add_index("ix_job_state", "job", "state", metadata) - # Add all of the new tables above - metadata.create_all() - # Add 1 foreign key constraint to the history_dataset_association table - LibraryDatasetDatasetAssociation_table = Table("library_dataset_dataset_association", metadata, autoload=True) - try: - cons = ForeignKeyConstraint( - [HistoryDatasetAssociation_table.c.copied_from_library_dataset_dataset_association_id], - [LibraryDatasetDatasetAssociation_table.c.id], - name="history_dataset_association_copied_from_library_dataset_da_fkey", - ) - # Create the constraint - cons.create() - except Exception: - log.exception( - "Adding foreign key constraint 'history_dataset_association_copied_from_library_dataset_da_fkey' to table 'history_dataset_association' failed." - ) - # Add 1 foreign key constraint to the metadata_file table - LibraryDatasetDatasetAssociation_table = Table("library_dataset_dataset_association", metadata, autoload=True) - if migrate_engine.name != "sqlite": - # Sqlite can't alter table add foreign key. - try: - cons = ForeignKeyConstraint( - [MetadataFile_table.c.lda_id], - [LibraryDatasetDatasetAssociation_table.c.id], - name="metadata_file_lda_id_fkey", - ) - # Create the constraint - cons.create() - except Exception: - log.exception("Adding foreign key constraint 'metadata_file_lda_id_fkey' to table 'metadata_file' failed.") - # Make sure we have at least 1 user - cmd = "SELECT * FROM galaxy_user;" - users = migrate_engine.execute(cmd).fetchall() - if users: - cmd = "SELECT * FROM role;" - roles = migrate_engine.execute(cmd).fetchall() - if not roles: - # Create private roles for each user - pass 1 - cmd = ( - "INSERT INTO role " - + "SELECT %s AS id," - + "%s AS create_time," - + "%s AS update_time," - + "email AS name," - + "email AS description," - + "'private' As type," - + "%s AS deleted " - + "FROM galaxy_user " - + "ORDER BY id;" - ) - cmd = cmd % ( - nextval(migrate_engine, "role"), - localtimestamp(migrate_engine), - localtimestamp(migrate_engine), - engine_false(migrate_engine), - ) - migrate_engine.execute(cmd) - # Create private roles for each user - pass 2 - if migrate_engine.name in ["postgres", "postgresql", "sqlite"]: - cmd = "UPDATE role SET description = 'Private role for ' || description;" - elif migrate_engine.name == "mysql": - cmd = "UPDATE role SET description = CONCAT( 'Private role for ', description );" - migrate_engine.execute(cmd) - # Create private roles for each user - pass 3 - cmd = ( - "INSERT INTO user_role_association " - + "SELECT %s AS id," - + "galaxy_user.id AS user_id," - + "role.id AS role_id," - + "%s AS create_time," - + "%s AS update_time " - + "FROM galaxy_user, role " - + "WHERE galaxy_user.email = role.name " - + "ORDER BY galaxy_user.id;" - ) - cmd = cmd % ( - nextval(migrate_engine, "user_role_association"), - localtimestamp(migrate_engine), - localtimestamp(migrate_engine), - ) - migrate_engine.execute(cmd) - # Create default permissions for each user - cmd = ( - "INSERT INTO default_user_permissions " - + "SELECT %s AS id," - + "galaxy_user.id AS user_id," - + "'manage permissions' AS action," - + "user_role_association.role_id AS role_id " - + "FROM galaxy_user " - + "JOIN user_role_association ON user_role_association.user_id = galaxy_user.id " - + "ORDER BY galaxy_user.id;" - ) - cmd = cmd % nextval(migrate_engine, "default_user_permissions") - migrate_engine.execute(cmd) - # Create default history permissions for each active history associated with a user - - cmd = ( - "INSERT INTO default_history_permissions " - + "SELECT %s AS id," - + "history.id AS history_id," - + "'manage permissions' AS action," - + "user_role_association.role_id AS role_id " - + "FROM history " - + "JOIN user_role_association ON user_role_association.user_id = history.user_id " - + "WHERE history.purged = %s AND history.user_id IS NOT NULL;" - ) - cmd = cmd % (nextval(migrate_engine, "default_history_permissions"), engine_false(migrate_engine)) - migrate_engine.execute(cmd) - # Create "manage permissions" dataset_permissions for all activate-able datasets - cmd = ( - "INSERT INTO dataset_permissions " - + "SELECT %s AS id," - + "%s AS create_time," - + "%s AS update_time," - + "'manage permissions' AS action," - + "history_dataset_association.dataset_id AS dataset_id," - + "user_role_association.role_id AS role_id " - + "FROM history " - + "JOIN history_dataset_association ON history_dataset_association.history_id = history.id " - + "JOIN dataset ON history_dataset_association.dataset_id = dataset.id " - + "JOIN user_role_association ON user_role_association.user_id = history.user_id " - + "WHERE dataset.purged = %s AND history.user_id IS NOT NULL;" - ) - cmd = cmd % ( - nextval(migrate_engine, "dataset_permissions"), - localtimestamp(migrate_engine), - localtimestamp(migrate_engine), - engine_false(migrate_engine), - ) - migrate_engine.execute(cmd) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - # NOTE: all new data added in the upgrade method is eliminated here via table drops - # Drop 1 foreign key constraint from the metadata_file table - MetadataFile_table = Table("metadata_file", metadata, autoload=True) - LibraryDatasetDatasetAssociation_table = Table("library_dataset_dataset_association", metadata, autoload=True) - try: - cons = ForeignKeyConstraint( - [MetadataFile_table.c.lda_id], - [LibraryDatasetDatasetAssociation_table.c.id], - name="metadata_file_lda_id_fkey", - ) - # Drop the constraint - cons.drop() - except Exception: - log.exception("Dropping foreign key constraint 'metadata_file_lda_id_fkey' from table 'metadata_file' failed.") - # Drop 1 foreign key constraint from the history_dataset_association table - HistoryDatasetAssociation_table = Table("history_dataset_association", metadata, autoload=True) - LibraryDatasetDatasetAssociation_table = Table("library_dataset_dataset_association", metadata, autoload=True) - try: - cons = ForeignKeyConstraint( - [HistoryDatasetAssociation_table.c.copied_from_library_dataset_dataset_association_id], - [LibraryDatasetDatasetAssociation_table.c.id], - name="history_dataset_association_copied_from_library_dataset_da_fkey", - ) - # Drop the constraint - cons.drop() - except Exception: - log.exception( - "Dropping foreign key constraint 'history_dataset_association_copied_from_library_dataset_da_fkey' from table 'history_dataset_association' failed." - ) - # Drop all of the new tables above - TABLES = [ - UserGroupAssociation_table, - UserRoleAssociation_table, - GroupRoleAssociation_table, - Group_table, - DatasetPermissions_table, - LibraryPermissions_table, - LibraryFolderPermissions_table, - LibraryDatasetPermissions_table, - LibraryDatasetDatasetAssociationPermissions_table, - LibraryItemInfoPermissions_table, - LibraryItemInfoTemplatePermissions_table, - DefaultUserPermissions_table, - DefaultHistoryPermissions_table, - Role_table, - LibraryDatasetDatasetInfoAssociation_table, - LibraryDataset_table, - LibraryDatasetDatasetAssociation_table, - LibraryDatasetDatasetInfoTemplateAssociation_table, - JobExternalOutputMetadata_table, - Library_table, - LibraryFolder_table, - LibraryItemInfoTemplateElement_table, - LibraryInfoTemplateAssociation_table, - LibraryFolderInfoTemplateAssociation_table, - LibraryDatasetInfoTemplateAssociation_table, - LibraryInfoAssociation_table, - LibraryFolderInfoAssociation_table, - LibraryDatasetInfoAssociation_table, - LibraryItemInfoElement_table, - LibraryItemInfo_table, - LibraryItemInfoTemplate_table, - ] - for table in TABLES: - drop_table(table) - # Drop the index on the Job.state column - changeset 2192 - drop_index("ix_job_state", "job", "state", metadata) - # Drop 1 column from the stored_workflow table - changeset 2328 - drop_column("importable", "stored_workflow", metadata) - # Drop 1 column from the metadata_file table - drop_column("lda_id", "metadata_file", metadata) - # Drop 1 column from the history_dataset_association table - drop_column("copied_from_library_dataset_dataset_association_id", HistoryDatasetAssociation_table) - # Drop 2 columns from the galaxy_user table - User_table = Table("galaxy_user", metadata, autoload=True) - drop_column("deleted", User_table) - drop_column("purged", User_table) diff --git a/lib/galaxy/model/migrate/versions/0004_indexes_and_defaults.py b/lib/galaxy/model/migrate/versions/0004_indexes_and_defaults.py deleted file mode 100644 index a3c3d72e17b..00000000000 --- a/lib/galaxy/model/migrate/versions/0004_indexes_and_defaults.py +++ /dev/null @@ -1,50 +0,0 @@ -""" -""" - -import logging - -from sqlalchemy import ( - MetaData, - Table, -) - -from galaxy.model.migrate.versions.util import ( - add_index, - engine_false, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - User_table = Table("galaxy_user", metadata, autoload=True) - # The next add_index() calls are not needed any more after commit - # 7ee93c0995123b0f357abd649326295dfa06766c , but harmless - add_index("ix_galaxy_user_deleted", User_table, "deleted") - add_index("ix_galaxy_user_purged", User_table, "purged") - # Set the default data in the galaxy_user table, but only for null values - cmd = f"UPDATE galaxy_user SET deleted = {engine_false(migrate_engine)} WHERE deleted is null" - try: - migrate_engine.execute(cmd) - except Exception: - log.exception("Setting default data for galaxy_user.deleted column failed.") - cmd = f"UPDATE galaxy_user SET purged = {engine_false(migrate_engine)} WHERE purged is null" - try: - migrate_engine.execute(cmd) - except Exception: - log.exception("Setting default data for galaxy_user.purged column failed.") - add_index( - "ix_hda_copied_from_library_dataset_dataset_association_id", - "history_dataset_association", - "copied_from_library_dataset_dataset_association_id", - metadata, - ) - - -def downgrade(migrate_engine): - pass diff --git a/lib/galaxy/model/migrate/versions/0005_cleanup_datasets_fix.py b/lib/galaxy/model/migrate/versions/0005_cleanup_datasets_fix.py deleted file mode 100644 index 82ef1ddc832..00000000000 --- a/lib/galaxy/model/migrate/versions/0005_cleanup_datasets_fix.py +++ /dev/null @@ -1,9 +0,0 @@ -"""No-op cleanup for existing datasets.""" - - -def upgrade(migrate_engine): - print(__doc__) - - -def downgrade(migrate_engine): - pass diff --git a/lib/galaxy/model/migrate/versions/0006_change_qual_datatype.py b/lib/galaxy/model/migrate/versions/0006_change_qual_datatype.py deleted file mode 100644 index d73bb40c1d2..00000000000 --- a/lib/galaxy/model/migrate/versions/0006_change_qual_datatype.py +++ /dev/null @@ -1,33 +0,0 @@ -""" -This migration script changes certain values in the history_dataset_association.extension -column, specifically 'qual' is changed to be 'qual454'. -""" - -import logging - -from sqlalchemy import MetaData - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - # Set the default data in the galaxy_user table, but only for null values - cmd = "UPDATE history_dataset_association SET extension = 'qual454' WHERE extension = 'qual' and peek like '>%%'" - try: - migrate_engine.execute(cmd) - except Exception: - log.exception("Resetting extension qual to qual454 in history_dataset_association failed.") - cmd = "UPDATE history_dataset_association SET extension = 'qualsolexa' WHERE extension = 'qual' and peek not like '>%%'" - try: - migrate_engine.execute(cmd) - except Exception: - log.exception("Resetting extension qual to qualsolexa in history_dataset_association failed.") - - -def downgrade(migrate_engine): - pass diff --git a/lib/galaxy/model/migrate/versions/0007_sharing_histories.py b/lib/galaxy/model/migrate/versions/0007_sharing_histories.py deleted file mode 100644 index fdfd555a57c..00000000000 --- a/lib/galaxy/model/migrate/versions/0007_sharing_histories.py +++ /dev/null @@ -1,52 +0,0 @@ -""" -This migration script creates the new history_user_share_association table, and adds -a new boolean type column to the history table. This provides support for sharing -histories in the same way that workflows are shared. -""" - -import logging - -from sqlalchemy import ( - Boolean, - Column, - ForeignKey, - Integer, - MetaData, - Table, -) - -from galaxy.model.migrate.versions.util import ( - add_column, - create_table, - drop_column, - drop_table, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - -HistoryUserShareAssociation_table = Table( - "history_user_share_association", - metadata, - Column("id", Integer, primary_key=True), - Column("history_id", Integer, ForeignKey("history.id"), index=True), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True), -) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - create_table(HistoryUserShareAssociation_table) - col = Column("importable", Boolean, index=True, default=False) - add_column(col, "history", metadata, index_name="ix_history_importable") - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - drop_column("importable", "history", metadata) - drop_table(HistoryUserShareAssociation_table) diff --git a/lib/galaxy/model/migrate/versions/0008_galaxy_forms.py b/lib/galaxy/model/migrate/versions/0008_galaxy_forms.py deleted file mode 100644 index f7b3fb91487..00000000000 --- a/lib/galaxy/model/migrate/versions/0008_galaxy_forms.py +++ /dev/null @@ -1,166 +0,0 @@ -""" -This migration script adds the following new tables for supporting Galaxy forms: -1) form_definition_current -2) form_definition -3) form_values -4) request_type -5) request -6) sample -7) sample_state -8) sample_event -""" - -import datetime -import logging - -from sqlalchemy import ( - Boolean, - Column, - DateTime, - ForeignKey, - Integer, - MetaData, - Table, - TEXT, -) - -from galaxy.model.custom_types import ( - JSONType, - TrimmedString, -) -from galaxy.model.migrate.versions.util import ( - create_table, - drop_table, -) - -log = logging.getLogger(__name__) -now = datetime.datetime.utcnow -metadata = MetaData() - -FormDefinition_table = Table( - "form_definition", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("name", TrimmedString(255), nullable=False), - Column("desc", TEXT), - Column( - "form_definition_current_id", - Integer, - ForeignKey("form_definition_current.id", use_alter=True), - index=True, - nullable=False, - ), - Column("fields", JSONType), -) - -FormDefinitionCurrent_table = Table( - "form_definition_current", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("latest_form_id", Integer, ForeignKey("form_definition.id"), index=True), - Column("deleted", Boolean, index=True, default=False), -) - -FormValues_table = Table( - "form_values", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("form_definition_id", Integer, ForeignKey("form_definition.id"), index=True), - Column("content", JSONType), -) - -RequestType_table = Table( - "request_type", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("name", TrimmedString(255), nullable=False), - Column("desc", TEXT), - Column("request_form_id", Integer, ForeignKey("form_definition.id"), index=True), - Column("sample_form_id", Integer, ForeignKey("form_definition.id"), index=True), -) - -Request_table = Table( - "request", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("name", TrimmedString(255), nullable=False), - Column("desc", TEXT), - Column("form_values_id", Integer, ForeignKey("form_values.id"), index=True), - Column("request_type_id", Integer, ForeignKey("request_type.id"), index=True), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True), - Column("library_id", Integer, ForeignKey("library.id"), index=True), - Column("deleted", Boolean, index=True, default=False), -) - -Sample_table = Table( - "sample", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("name", TrimmedString(255), nullable=False), - Column("desc", TEXT), - Column("form_values_id", Integer, ForeignKey("form_values.id"), index=True), - Column("request_id", Integer, ForeignKey("request.id"), index=True), - Column("deleted", Boolean, index=True, default=False), -) - -SampleState_table = Table( - "sample_state", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("name", TrimmedString(255), nullable=False), - Column("desc", TEXT), - Column("request_type_id", Integer, ForeignKey("request_type.id"), index=True), -) - -SampleEvent_table = Table( - "sample_event", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("sample_id", Integer, ForeignKey("sample.id"), index=True), - Column("sample_state_id", Integer, ForeignKey("sample_state.id"), index=True), - Column("comment", TEXT), -) - -TABLES = [ - FormDefinition_table, - FormDefinitionCurrent_table, - FormValues_table, - RequestType_table, - Request_table, - Sample_table, - SampleState_table, - SampleEvent_table, -] - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - for table in TABLES: - create_table(table) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - for table in reversed(TABLES): - drop_table(table) diff --git a/lib/galaxy/model/migrate/versions/0009_request_table.py b/lib/galaxy/model/migrate/versions/0009_request_table.py deleted file mode 100644 index 2c26a82b27a..00000000000 --- a/lib/galaxy/model/migrate/versions/0009_request_table.py +++ /dev/null @@ -1,44 +0,0 @@ -""" -This migration script adds a new column to 2 tables: -1) a new boolean type column named 'submitted' to the 'request' table -2) a new string type column named 'bar_code' to the 'sample' table -""" - -import logging - -from sqlalchemy import ( - Boolean, - Column, - MetaData, -) - -from galaxy.model.custom_types import TrimmedString -from galaxy.model.migrate.versions.util import ( - add_column, - drop_column, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - col = Column("submitted", Boolean, default=False) - add_column(col, "request", metadata) - - col = Column("bar_code", TrimmedString(255), index=True) - add_column(col, "sample", metadata, index_name="ix_sample_bar_code") - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - drop_column("bar_code", "sample", metadata) - # SQLAlchemy Migrate has a bug when dropping a boolean column in SQLite - if migrate_engine.name != "sqlite": - drop_column("submitted", "request", metadata) diff --git a/lib/galaxy/model/migrate/versions/0010_hda_display_at_authz_table.py b/lib/galaxy/model/migrate/versions/0010_hda_display_at_authz_table.py deleted file mode 100644 index 8f3675a2e10..00000000000 --- a/lib/galaxy/model/migrate/versions/0010_hda_display_at_authz_table.py +++ /dev/null @@ -1,59 +0,0 @@ -""" -This migration script adds the history_dataset_association_display_at_authorization table, -which allows 'private' datasets to be displayed at external sites without making them public. - -If using mysql, this script will display the following error, which is corrected in the next -migration script: -history_dataset_association_display_at_authorization table failed: (OperationalError) -(1059, "Identifier name 'ix_history_dataset_association_display_at_authorization_update_time' -is too long -""" - -import datetime -import logging - -from sqlalchemy import ( - Column, - DateTime, - ForeignKey, - Integer, - MetaData, - Table, -) - -from galaxy.model.custom_types import TrimmedString - -log = logging.getLogger(__name__) -now = datetime.datetime.utcnow -metadata = MetaData() - -HistoryDatasetAssociationDisplayAtAuthorization_table = Table( - "history_dataset_association_display_at_authorization", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, index=True, default=now, onupdate=now), - Column("history_dataset_association_id", Integer, ForeignKey("history_dataset_association.id"), index=True), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True), - Column("site", TrimmedString(255)), -) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - try: - HistoryDatasetAssociationDisplayAtAuthorization_table.create() - except Exception: - log.exception("Creating history_dataset_association_display_at_authorization table failed.") - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - # Load existing tables - metadata.reflect() - try: - HistoryDatasetAssociationDisplayAtAuthorization_table.drop() - except Exception: - log.exception("Dropping history_dataset_association_display_at_authorization table failed.") diff --git a/lib/galaxy/model/migrate/versions/0011_v0010_mysql_index_fix.py b/lib/galaxy/model/migrate/versions/0011_v0010_mysql_index_fix.py deleted file mode 100644 index 397c878a35f..00000000000 --- a/lib/galaxy/model/migrate/versions/0011_v0010_mysql_index_fix.py +++ /dev/null @@ -1,47 +0,0 @@ -""" -This script fixes a problem introduced in the previous migration script -0010_hda_display_at_authz_table.py . MySQL has a name length limit and -thus the index "ix_hdadaa_history_dataset_association_id" has to be -manually created. -""" - -import datetime -import logging - -from sqlalchemy import MetaData - -from galaxy.model.migrate.versions.util import ( - add_index, - drop_index, -) - -log = logging.getLogger(__name__) -now = datetime.datetime.utcnow -metadata = MetaData() - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - if migrate_engine.name == "mysql": - add_index( - "ix_hdadaa_history_dataset_association_id", - "history_dataset_association_display_at_authorization", - "history_dataset_association_id", - metadata, - ) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - if migrate_engine.name == "mysql": - drop_index( - "ix_hdadaa_history_dataset_association_id", - "history_dataset_association_display_at_authorization", - "history_dataset_association_id", - metadata, - ) diff --git a/lib/galaxy/model/migrate/versions/0012_user_address.py b/lib/galaxy/model/migrate/versions/0012_user_address.py deleted file mode 100644 index 06bda287d47..00000000000 --- a/lib/galaxy/model/migrate/versions/0012_user_address.py +++ /dev/null @@ -1,76 +0,0 @@ -""" -This script adds a new user_address table that is currently only used with sample requests, where -a user can select from a list of his addresses to associate with the request. This script also -drops the request.submitted column which was boolean and replaces it with a request.state column -which is a string, allowing for more flexibility with request states. -""" - -import datetime -import logging - -from sqlalchemy import ( - Boolean, - Column, - DateTime, - ForeignKey, - Integer, - MetaData, - Table, - TEXT, -) - -from galaxy.model.custom_types import TrimmedString -from galaxy.model.migrate.versions.util import ( - add_column, - create_table, - drop_column, -) - -log = logging.getLogger(__name__) -now = datetime.datetime.utcnow -metadata = MetaData() - -UserAddress_table = Table( - "user_address", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True), - Column("desc", TEXT), - Column("name", TrimmedString(255), nullable=False), - Column("institution", TrimmedString(255)), - Column("address", TrimmedString(255), nullable=False), - Column("city", TrimmedString(255), nullable=False), - Column("state", TrimmedString(255), nullable=False), - Column("postal_code", TrimmedString(255), nullable=False), - Column("country", TrimmedString(255), nullable=False), - Column("phone", TrimmedString(255)), - Column("deleted", Boolean, index=True, default=False), - Column("purged", Boolean, index=True, default=False), -) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - # Add all of the new tables above - create_table(UserAddress_table) - # Add 1 column to the request_type table - col = Column("deleted", Boolean, index=True, default=False) - add_column(col, "request_type", metadata, index_name="ix_request_type_deleted") - - # Delete the submitted column - # This fails for sqlite, so skip the drop -- no conflicts in the future - Request_table = Table("request", metadata, autoload=True) - # SQLAlchemy Migrate has a bug when dropping a boolean column in SQLite - if migrate_engine.name != "sqlite": - drop_column("submitted", Request_table) - col = Column("state", TrimmedString(255), index=True) - add_column(col, Request_table, metadata, index_name="ix_request_state") - - -def downgrade(migrate_engine): - pass diff --git a/lib/galaxy/model/migrate/versions/0013_change_lib_item_templates_to_forms.py b/lib/galaxy/model/migrate/versions/0013_change_lib_item_templates_to_forms.py deleted file mode 100644 index 57b6440e573..00000000000 --- a/lib/galaxy/model/migrate/versions/0013_change_lib_item_templates_to_forms.py +++ /dev/null @@ -1,106 +0,0 @@ -""" -This migration script eliminates all of the tables that were used for the 1st version of the -library templates where template fields and contents were each stored as a separate table row -in various library item tables. All of these tables are dropped in this script, eliminating all -existing template data. A total of 14 existing tables are dropped. - -We're now basing library templates on forms, so field contents are -stored as a jsonified list in the form_values table. This script introduces the following 3 -new association tables: -1) library_info_association -2) library_folder_info_association -3) library_dataset_dataset_info_association -""" - -import logging - -from sqlalchemy import ( - Column, - ForeignKey, - Integer, - MetaData, - Table, -) - -from galaxy.model.migrate.versions.util import ( - create_table, - drop_table, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - -OLD_TABLE_NAMES = [ - "library_item_info_permissions", - "library_item_info_template_permissions", - "library_item_info_element", - "library_item_info_template_element", - "library_info_template_association", - "library_folder_info_template_association", - "library_dataset_info_template_association", - "library_dataset_dataset_info_template_association", - "library_info_association", - "library_folder_info_association", - "library_dataset_info_association", - "library_dataset_dataset_info_association", - "library_item_info", - "library_item_info_template", -] - -LibraryInfoAssociation_table = Table( - "library_info_association", - metadata, - Column("id", Integer, primary_key=True), - Column("library_id", Integer, ForeignKey("library.id"), index=True), - Column("form_definition_id", Integer, ForeignKey("form_definition.id"), index=True), - Column("form_values_id", Integer, ForeignKey("form_values.id"), index=True), -) - -LibraryFolderInfoAssociation_table = Table( - "library_folder_info_association", - metadata, - Column("id", Integer, primary_key=True), - Column("library_folder_id", Integer, ForeignKey("library_folder.id"), nullable=True, index=True), - Column("form_definition_id", Integer, ForeignKey("form_definition.id"), index=True), - Column("form_values_id", Integer, ForeignKey("form_values.id"), index=True), -) - -LibraryDatasetDatasetInfoAssociation_table = Table( - "library_dataset_dataset_info_association", - metadata, - Column("id", Integer, primary_key=True), - Column( - "library_dataset_dataset_association_id", - Integer, - ForeignKey("library_dataset_dataset_association.id"), - nullable=True, - index=True, - ), - Column("form_definition_id", Integer, ForeignKey("form_definition.id"), index=True), - Column("form_values_id", Integer, ForeignKey("form_values.id"), index=True), -) - -NEW_TABLES = [ - LibraryInfoAssociation_table, - LibraryFolderInfoAssociation_table, - LibraryDatasetDatasetInfoAssociation_table, -] - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - # Drop all of the original library_item_info tables - # NOTE: all existing library item into template data is eliminated here via table drops - for table_name in OLD_TABLE_NAMES: - drop_table(table_name, metadata) - - # Create all new tables above - for table in NEW_TABLES: - create_table(table) - - -def downgrade(migrate_engine): - pass diff --git a/lib/galaxy/model/migrate/versions/0014_pages.py b/lib/galaxy/model/migrate/versions/0014_pages.py deleted file mode 100644 index dd454ae228b..00000000000 --- a/lib/galaxy/model/migrate/versions/0014_pages.py +++ /dev/null @@ -1,81 +0,0 @@ -""" -Migration script to add support for "Pages". - 1) Creates Page and PageRevision tables - 2) Adds username column to User table -""" - -import datetime -import logging - -from sqlalchemy import ( - Column, - DateTime, - ForeignKey, - Index, - Integer, - MetaData, - String, - Table, - TEXT, -) - -from galaxy.model.migrate.versions.util import ( - add_column, - create_table, - drop_column, - drop_table, -) - -now = datetime.datetime.utcnow -log = logging.getLogger(__name__) -metadata = MetaData() - -Page_table = Table( - "page", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True, nullable=False), - Column( - "latest_revision_id", - Integer, - ForeignKey("page_revision.id", use_alter=True, name="page_latest_revision_id_fk"), - index=True, - ), - Column("title", TEXT), - Column("slug", TEXT), - Index("ix_page_slug", "slug", unique=True, mysql_length=200), -) - -PageRevision_table = Table( - "page_revision", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("page_id", Integer, ForeignKey("page.id"), index=True, nullable=False), - Column("title", TEXT), - Column("content", TEXT), -) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - create_table(Page_table) - create_table(PageRevision_table) - - col = Column("username", String(255), index=True, unique=True, default=False) - add_column(col, "galaxy_user", metadata, index_name="ix_galaxy_user_username", unique_name="username") - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - drop_column("username", "galaxy_user", metadata) - drop_table(PageRevision_table) - drop_table(Page_table) diff --git a/lib/galaxy/model/migrate/versions/0015_tagging.py b/lib/galaxy/model/migrate/versions/0015_tagging.py deleted file mode 100644 index 835635bea03..00000000000 --- a/lib/galaxy/model/migrate/versions/0015_tagging.py +++ /dev/null @@ -1,86 +0,0 @@ -""" -This migration script adds the tables necessary to support tagging of histories, -datasets, and history-dataset associations (user views of datasets). -""" - -import logging - -from migrate import UniqueConstraint -from sqlalchemy import ( - Column, - ForeignKey, - Integer, - MetaData, - Table, -) - -from galaxy.model.custom_types import TrimmedString -from galaxy.model.migrate.versions.util import ( - create_table, - drop_table, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - -# New tables to support tagging of histories, datasets, and history-dataset associations. -Tag_table = Table( - "tag", - metadata, - Column("id", Integer, primary_key=True), - Column("type", Integer), - Column("parent_id", Integer, ForeignKey("tag.id")), - Column("name", TrimmedString(255)), - UniqueConstraint("name"), -) - -HistoryTagAssociation_table = Table( - "history_tag_association", - metadata, - Column("history_id", Integer, ForeignKey("history.id"), index=True), - Column("tag_id", Integer, ForeignKey("tag.id"), index=True), - Column("user_tname", TrimmedString(255), index=True), - Column("value", TrimmedString(255), index=True), - Column("user_value", TrimmedString(255), index=True), -) - -DatasetTagAssociation_table = Table( - "dataset_tag_association", - metadata, - Column("dataset_id", Integer, ForeignKey("dataset.id"), index=True), - Column("tag_id", Integer, ForeignKey("tag.id"), index=True), - Column("user_tname", TrimmedString(255), index=True), - Column("value", TrimmedString(255), index=True), - Column("user_value", TrimmedString(255), index=True), -) - -HistoryDatasetAssociationTagAssociation_table = Table( - "history_dataset_association_tag_association", - metadata, - Column("history_dataset_association_id", Integer, ForeignKey("history_dataset_association.id"), index=True), - Column("tag_id", Integer, ForeignKey("tag.id"), index=True), - Column("user_tname", TrimmedString(255), index=True), - Column("value", TrimmedString(255), index=True), - Column("user_value", TrimmedString(255), index=True), -) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - create_table(Tag_table) - create_table(HistoryTagAssociation_table) - create_table(DatasetTagAssociation_table) - create_table(HistoryDatasetAssociationTagAssociation_table) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - drop_table(HistoryDatasetAssociationTagAssociation_table) - drop_table(DatasetTagAssociation_table) - drop_table(HistoryTagAssociation_table) - drop_table(Tag_table) diff --git a/lib/galaxy/model/migrate/versions/0016_v0015_mysql_index_fix.py b/lib/galaxy/model/migrate/versions/0016_v0015_mysql_index_fix.py deleted file mode 100644 index ff5b0cfc9b8..00000000000 --- a/lib/galaxy/model/migrate/versions/0016_v0015_mysql_index_fix.py +++ /dev/null @@ -1,53 +0,0 @@ -""" -This script was used to fix a problem introduced in 0015_tagging.py. MySQL has a -name length limit and thus the index "ix_hda_ta_history_dataset_association_id" -had to be manually created. - -This is now fixed in SQLAlchemy Migrate. -""" - -import logging - -from sqlalchemy import ( - MetaData, - Table, -) - -from galaxy.model.migrate.versions.util import ( - add_index, - drop_index, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - HistoryDatasetAssociationTagAssociation_table = Table( - "history_dataset_association_tag_association", metadata, autoload=True - ) - if not any( - [_.name for _ in index.columns] == ["history_dataset_association_id"] - for index in HistoryDatasetAssociationTagAssociation_table.indexes - ): - add_index( - "ix_hda_ta_history_dataset_association_id", - HistoryDatasetAssociationTagAssociation_table, - "history_dataset_association_id", - ) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - drop_index( - "ix_hda_ta_history_dataset_association_id", - "history_dataset_association_tag_association", - "history_dataset_association_id", - metadata, - ) diff --git a/lib/galaxy/model/migrate/versions/0017_library_item_indexes.py b/lib/galaxy/model/migrate/versions/0017_library_item_indexes.py deleted file mode 100644 index 7a29439fd97..00000000000 --- a/lib/galaxy/model/migrate/versions/0017_library_item_indexes.py +++ /dev/null @@ -1,35 +0,0 @@ -""" -This script adds 3 indexes to table columns: library_folder.name, -library_dataset.name, library_dataset_dataset_association.name. -""" - -import logging - -from sqlalchemy import MetaData - -from galaxy.model.migrate.versions.util import ( - add_index, - drop_index, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - add_index("ix_library_folder_name", "library_folder", "name", metadata) - add_index("ix_library_dataset_dataset_association_name", "library_dataset_dataset_association", "name", metadata) - add_index("ix_library_dataset_name", "library_dataset", "name", metadata) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - drop_index("ix_library_dataset_name", "library_dataset", "name", metadata) - drop_index("ix_library_dataset_dataset_association_name", "library_dataset_dataset_association", "name", metadata) - drop_index("ix_library_folder_name", "library_folder", "name", metadata) diff --git a/lib/galaxy/model/migrate/versions/0018_ordered_tags_and_page_tags.py b/lib/galaxy/model/migrate/versions/0018_ordered_tags_and_page_tags.py deleted file mode 100644 index f64c84ff1b0..00000000000 --- a/lib/galaxy/model/migrate/versions/0018_ordered_tags_and_page_tags.py +++ /dev/null @@ -1,127 +0,0 @@ -""" -This migration script provides support for (a) ordering tags by recency and -(b) tagging pages. This script deletes all existing tags. -""" - -import logging - -from sqlalchemy import ( - Column, - ForeignKey, - Index, - Integer, - MetaData, - Table, -) -from sqlalchemy.exc import OperationalError - -# Need our custom types, but don't import anything else from model -from galaxy.model.custom_types import TrimmedString - -log = logging.getLogger(__name__) -metadata = MetaData() - -HistoryTagAssociation_table = Table( - "history_tag_association", - metadata, - Column("id", Integer, primary_key=True), - Column("history_id", Integer, ForeignKey("history.id"), index=True), - Column("tag_id", Integer, ForeignKey("tag.id"), index=True), - Column("user_tname", TrimmedString(255), index=True), - Column("value", TrimmedString(255), index=True), - Column("user_value", TrimmedString(255), index=True), -) - -DatasetTagAssociation_table = Table( - "dataset_tag_association", - metadata, - Column("id", Integer, primary_key=True), - Column("dataset_id", Integer, ForeignKey("dataset.id"), index=True), - Column("tag_id", Integer, ForeignKey("tag.id"), index=True), - Column("user_tname", TrimmedString(255), index=True), - Column("value", TrimmedString(255), index=True), - Column("user_value", TrimmedString(255), index=True), -) - -HistoryDatasetAssociationTagAssociation_table = Table( - "history_dataset_association_tag_association", - metadata, - Column("id", Integer, primary_key=True), - Column("history_dataset_association_id", Integer, ForeignKey("history_dataset_association.id"), index=True), - Column("tag_id", Integer, ForeignKey("tag.id"), index=True), - Column("user_tname", TrimmedString(255), index=True), - Column("value", TrimmedString(255), index=True), - Column("user_value", TrimmedString(255), index=True), -) - -PageTagAssociation_table = Table( - "page_tag_association", - metadata, - Column("id", Integer, primary_key=True), - Column("page_id", Integer, ForeignKey("page.id"), index=True), - Column("tag_id", Integer, ForeignKey("tag.id"), index=True), - Column("user_tname", TrimmedString(255), index=True), - Column("value", TrimmedString(255), index=True), - Column("user_value", TrimmedString(255), index=True), -) - - -def upgrade(migrate_engine): - metadata.bind = migrate_engine - print(__doc__) - metadata.reflect() - - # - # Recreate tables. - # - try: - HistoryTagAssociation_table.drop() - HistoryTagAssociation_table.create() - except Exception: - log.exception("Recreating history_tag_association table failed.") - - try: - DatasetTagAssociation_table.drop() - DatasetTagAssociation_table.create() - except Exception: - log.exception("Recreating dataset_tag_association table failed.") - - try: - HistoryDatasetAssociationTagAssociation_table.drop() - HistoryDatasetAssociationTagAssociation_table.create() - except OperationalError as e: - # Handle error that results from and index name that is too long; this occurs - # in MySQL. - if str(e).find("CREATE INDEX") != -1: - # Manually create index. - i = Index( - "ix_hda_ta_history_dataset_association_id", - HistoryDatasetAssociationTagAssociation_table.c.history_dataset_association_id, - ) - try: - i.create() - except Exception: - log.exception( - "Adding index 'ix_hda_ta_history_dataset_association_id' to table 'history_dataset_association_tag_association' table failed." - ) - except Exception: - log.exception("Recreating history_dataset_association_tag_association table failed.") - - # Create page_tag_association table. - try: - PageTagAssociation_table.create() - except Exception: - log.exception("Creating page_tag_association table failed.") - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - # No need to downgrade other tagging tables. They work fine with verision 16 code. - - # Drop page_tag_association table. - try: - PageTagAssociation_table.drop() - except Exception: - log.exception("Dropping page_tag_association table failed.") diff --git a/lib/galaxy/model/migrate/versions/0019_request_library_folder.py b/lib/galaxy/model/migrate/versions/0019_request_library_folder.py deleted file mode 100644 index 98937561d62..00000000000 --- a/lib/galaxy/model/migrate/versions/0019_request_library_folder.py +++ /dev/null @@ -1,44 +0,0 @@ -""" -This script creates a request.folder_id column which is a foreign -key to the library_folder table. This also adds a 'type' and 'layout' column -to the form_definition table. -""" - -import logging - -from sqlalchemy import ( - Column, - ForeignKey, - Integer, - MetaData, - Table, -) - -from galaxy.model.custom_types import ( - JSONType, - TrimmedString, -) -from galaxy.model.migrate.versions.util import add_column - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - # Create the folder_id column - col = Column("folder_id", Integer, ForeignKey("library_folder.id"), index=True) - add_column(col, "request", metadata, index_name="ix_request_folder_id") - # Create the type column in form_definition - FormDefinition_table = Table("form_definition", metadata, autoload=True) - col = Column("type", TrimmedString(255), index=True) - add_column(col, FormDefinition_table, metadata, index_name="ix_form_definition_type") - col = Column("layout", JSONType) - add_column(col, FormDefinition_table, metadata) - - -def downgrade(migrate_engine): - pass diff --git a/lib/galaxy/model/migrate/versions/0020_library_upload_job.py b/lib/galaxy/model/migrate/versions/0020_library_upload_job.py deleted file mode 100644 index 340c3f60245..00000000000 --- a/lib/galaxy/model/migrate/versions/0020_library_upload_job.py +++ /dev/null @@ -1,71 +0,0 @@ -""" -This script creates a job_to_output_library_dataset table for allowing library -uploads to run as regular jobs. To support this, a library_folder_id column is -added to the job table, and library_folder/output_library_datasets relations -are added to the Job object. An index is also added to the dataset.state -column. -""" - -import logging - -from sqlalchemy import ( - Column, - ForeignKey, - Integer, - MetaData, - String, - Table, -) - -from galaxy.model.migrate.versions.util import ( - add_column, - add_index, - create_table, - drop_column, - drop_index, - drop_table, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - -JobToOutputLibraryDataset_table = Table( - "job_to_output_library_dataset", - metadata, - Column("id", Integer, primary_key=True), - Column("job_id", Integer, ForeignKey("job.id"), index=True), - Column("ldda_id", Integer, ForeignKey("library_dataset_dataset_association.id"), index=True), - Column("name", String(255)), -) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - # Create the job_to_output_library_dataset table - create_table(JobToOutputLibraryDataset_table) - - # Create the library_folder_id column - col = Column( - "library_folder_id", Integer, ForeignKey("library_folder.id", name="job_library_folder_id_fk"), index=True - ) - add_column(col, "job", metadata, index_name="ix_job_library_folder_id") - - # Create the ix_dataset_state index - add_index("ix_dataset_state", "dataset", "state", metadata) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - # Drop the ix_dataset_state index - drop_index("ix_dataset_state", "dataset", "state", metadata) - - # Drop the library_folder_id column - drop_column("library_folder_id", "job", metadata) - - # Drop the job_to_output_library_dataset table - drop_table(JobToOutputLibraryDataset_table) diff --git a/lib/galaxy/model/migrate/versions/0021_user_prefs.py b/lib/galaxy/model/migrate/versions/0021_user_prefs.py deleted file mode 100644 index 02b52b2d53f..00000000000 --- a/lib/galaxy/model/migrate/versions/0021_user_prefs.py +++ /dev/null @@ -1,47 +0,0 @@ -""" -This migration script adds a user preferences table to Galaxy. -""" - -import logging - -from sqlalchemy import ( - Column, - ForeignKey, - Integer, - MetaData, - Table, - Unicode, -) - -from galaxy.model.migrate.versions.util import ( - create_table, - drop_table, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - -# New table to support user preferences. -UserPreference_table = Table( - "user_preference", - metadata, - Column("id", Integer, primary_key=True), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True), - Column("name", Unicode(255), index=True), - Column("value", Unicode(1024)), -) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - create_table(UserPreference_table) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - drop_table(UserPreference_table) diff --git a/lib/galaxy/model/migrate/versions/0022_visualization_tables.py b/lib/galaxy/model/migrate/versions/0022_visualization_tables.py deleted file mode 100644 index 03b06635e6c..00000000000 --- a/lib/galaxy/model/migrate/versions/0022_visualization_tables.py +++ /dev/null @@ -1,70 +0,0 @@ -""" -Migration script to add support for storing visualizations. - 1) Creates Visualization and VisualizationRevision tables -""" - -import datetime -import logging - -from sqlalchemy import ( - Column, - DateTime, - ForeignKey, - Integer, - MetaData, - Table, - TEXT, -) - -now = datetime.datetime.utcnow -log = logging.getLogger(__name__) -metadata = MetaData() - -Visualization_table = Table( - "visualization", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True, nullable=False), - Column( - "latest_revision_id", - Integer, - ForeignKey("visualization_revision.id", use_alter=True, name="visualization_latest_revision_id_fk"), - index=True, - ), - Column("title", TEXT), - Column("type", TEXT), -) - -VisualizationRevision_table = Table( - "visualization_revision", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("visualization_id", Integer, ForeignKey("visualization.id"), index=True, nullable=False), - Column("title", TEXT), - Column("config", TEXT), -) - - -def upgrade(migrate_engine): - metadata.bind = migrate_engine - print(__doc__) - metadata.reflect() - try: - Visualization_table.create() - except Exception: - log.exception("Could not create page table") - try: - VisualizationRevision_table.create() - except Exception: - log.exception("Could not create page_revision table") - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - Visualization_table.drop() - VisualizationRevision_table.drop() diff --git a/lib/galaxy/model/migrate/versions/0023_page_published_and_deleted_columns.py b/lib/galaxy/model/migrate/versions/0023_page_published_and_deleted_columns.py deleted file mode 100644 index c2c09605e2d..00000000000 --- a/lib/galaxy/model/migrate/versions/0023_page_published_and_deleted_columns.py +++ /dev/null @@ -1,42 +0,0 @@ -""" -Migration script to add columns for tracking whether pages are deleted and -publicly accessible. -""" - -import logging - -from sqlalchemy import ( - Boolean, - Column, - MetaData, - Table, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - metadata.bind = migrate_engine - - print(__doc__) - metadata.reflect() - - Page_table = Table("page", metadata, autoload=True) - - c = Column("published", Boolean, index=True, default=False) - c.create(Page_table, index_name="ix_page_published") - assert c is Page_table.c.published - - c = Column("deleted", Boolean, index=True, default=False) - c.create(Page_table, index_name="ix_page_deleted") - assert c is Page_table.c.deleted - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - Page_table = Table("page", metadata, autoload=True) - Page_table.c.published.drop() - Page_table.c.deleted.drop() diff --git a/lib/galaxy/model/migrate/versions/0024_page_slug_unique_constraint.py b/lib/galaxy/model/migrate/versions/0024_page_slug_unique_constraint.py deleted file mode 100644 index c69d563444e..00000000000 --- a/lib/galaxy/model/migrate/versions/0024_page_slug_unique_constraint.py +++ /dev/null @@ -1,39 +0,0 @@ -""" -Remove unique constraint from page slugs to allow creating a page with -the same slug as a deleted page. -""" - -import logging - -from sqlalchemy import ( - MetaData, - Table, -) - -from galaxy.model.migrate.versions.util import ( - add_index, - drop_index, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - Page_table = Table("page", metadata, autoload=True) - try: - # Sqlite doesn't support .alter, so we need to drop an recreate - drop_index("ix_page_slug", Page_table, "slug") - - add_index("ix_page_slug", Page_table, "slug", unique=False) - except Exception: - # Mysql doesn't have a named index, but alter should work - Page_table.c.slug.alter(unique=False) - - -def downgrade(migrate_engine): - pass diff --git a/lib/galaxy/model/migrate/versions/0025_user_info.py b/lib/galaxy/model/migrate/versions/0025_user_info.py deleted file mode 100644 index 72c537cc9cd..00000000000 --- a/lib/galaxy/model/migrate/versions/0025_user_info.py +++ /dev/null @@ -1,36 +0,0 @@ -""" -This script adds a foreign key to the form_values table in the galaxy_user table -""" - -import logging - -from sqlalchemy import ( - Column, - ForeignKey, - Integer, - MetaData, -) - -from galaxy.model.migrate.versions.util import ( - add_column, - drop_column, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - col = Column("form_values_id", Integer, ForeignKey("form_values.id", name="user_form_values_id_fk"), index=True) - add_column(col, "galaxy_user", metadata, index_name="ix_galaxy_user_form_values_id") - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - drop_column("form_values_id", "galaxy_user", metadata) diff --git a/lib/galaxy/model/migrate/versions/0026_cloud_tables.py b/lib/galaxy/model/migrate/versions/0026_cloud_tables.py deleted file mode 100644 index ee9440bf887..00000000000 --- a/lib/galaxy/model/migrate/versions/0026_cloud_tables.py +++ /dev/null @@ -1,183 +0,0 @@ -""" -This script adds tables needed for Galaxy cloud functionality. -""" - -import datetime -import logging - -from sqlalchemy import ( - Boolean, - Column, - DateTime, - ForeignKey, - Integer, - MetaData, - Table, - TEXT, -) - -from galaxy.model.migrate.versions.util import ( - create_table, - drop_table, -) - -now = datetime.datetime.utcnow -log = logging.getLogger(__name__) -metadata = MetaData() - -CloudImage_table = Table( - "cloud_image", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("provider_type", TEXT), - Column("image_id", TEXT, nullable=False), - Column("manifest", TEXT), - Column("state", TEXT), - Column("architecture", TEXT), - Column("deleted", Boolean, default=False), -) - -""" UserConfiguredInstance (UCI) table """ -UCI_table = Table( - "cloud_uci", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True, nullable=False), - Column("credentials_id", Integer, ForeignKey("cloud_user_credentials.id"), index=True), - Column("key_pair_name", TEXT), - Column("key_pair_material", TEXT), - Column("name", TEXT), - Column("state", TEXT), - Column("error", TEXT), - Column("total_size", Integer), - Column("launch_time", DateTime), - Column("deleted", Boolean, default=False), -) - -CloudInstance_table = Table( - "cloud_instance", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("launch_time", DateTime), - Column("stop_time", DateTime), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True, nullable=False), - Column("uci_id", Integer, ForeignKey("cloud_uci.id"), index=True), - Column("type", TEXT), - Column("reservation_id", TEXT), - Column("instance_id", TEXT), - Column("mi_id", Integer, ForeignKey("cloud_image.id"), index=True), - Column("state", TEXT), - Column("error", TEXT), - Column("public_dns", TEXT), - Column("private_dns", TEXT), - Column("security_group", TEXT), - Column("availability_zone", TEXT), -) - -CloudStore_table = Table( - "cloud_store", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("attach_time", DateTime), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True, nullable=False), - Column("uci_id", Integer, ForeignKey("cloud_uci.id"), index=True, nullable=False), - Column("volume_id", TEXT), - Column("size", Integer, nullable=False), - Column("availability_zone", TEXT), - Column("inst_id", Integer, ForeignKey("cloud_instance.id")), - Column("status", TEXT), - Column("device", TEXT), - Column("space_consumed", Integer), - Column("error", TEXT), - Column("deleted", Boolean, default=False), -) - -CloudSnapshot_table = Table( - "cloud_snapshot", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True, nullable=False), - Column("uci_id", Integer, ForeignKey("cloud_uci.id"), index=True), - Column("store_id", Integer, ForeignKey("cloud_store.id"), index=True, nullable=False), - Column("snapshot_id", TEXT), - Column("status", TEXT), - Column("description", TEXT), - Column("error", TEXT), - Column("deleted", Boolean, default=False), -) - -CloudUserCredentials_table = Table( - "cloud_user_credentials", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True, nullable=False), - Column("provider_id", Integer, ForeignKey("cloud_provider.id"), index=True, nullable=False), - Column("name", TEXT), - Column("access_key", TEXT), - Column("secret_key", TEXT), - Column("deleted", Boolean, default=False), -) - -CloudProvider_table = Table( - "cloud_provider", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True, nullable=False), - Column("type", TEXT, nullable=False), - Column("name", TEXT), - Column("region_connection", TEXT), - Column("region_name", TEXT), - Column("region_endpoint", TEXT), - Column("is_secure", Boolean), - Column("host", TEXT), - Column("port", Integer), - Column("proxy", TEXT), - Column("proxy_port", TEXT), - Column("proxy_user", TEXT), - Column("proxy_pass", TEXT), - Column("debug", Integer), - Column("https_connection_factory", TEXT), - Column("path", TEXT), - Column("deleted", Boolean, default=False), -) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - create_table(CloudProvider_table) - create_table(CloudUserCredentials_table) - create_table(CloudImage_table) - create_table(UCI_table) - create_table(CloudInstance_table) - create_table(CloudStore_table) - create_table(CloudSnapshot_table) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - drop_table(CloudSnapshot_table) - drop_table(CloudStore_table) - drop_table(CloudInstance_table) - drop_table(UCI_table) - drop_table(CloudImage_table) - drop_table(CloudUserCredentials_table) - drop_table(CloudProvider_table) diff --git a/lib/galaxy/model/migrate/versions/0027_request_events.py b/lib/galaxy/model/migrate/versions/0027_request_events.py deleted file mode 100644 index 4cb033b64e5..00000000000 --- a/lib/galaxy/model/migrate/versions/0027_request_events.py +++ /dev/null @@ -1,72 +0,0 @@ -""" -This migration script adds the request_event table and -removes the state field in the request table -""" - -import datetime -import logging - -from sqlalchemy import ( - Column, - DateTime, - ForeignKey, - Integer, - MetaData, - Table, - TEXT, -) - -from galaxy.model.custom_types import TrimmedString -from galaxy.model.migrate.versions.util import ( - create_table, - drop_column, - localtimestamp, - nextval, -) - -log = logging.getLogger(__name__) -now = datetime.datetime.utcnow -metadata = MetaData() - -RequestEvent_table = Table( - "request_event", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("request_id", Integer, ForeignKey("request.id"), index=True), - Column("state", TrimmedString(255), index=True), - Column("comment", TEXT), -) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - create_table(RequestEvent_table) - # move the current state of all existing requests to the request_event table - cmd = ( - "INSERT INTO request_event " - + "SELECT %s AS id," - + "%s AS create_time," - + "%s AS update_time," - + "request.id AS request_id," - + "request.state AS state," - + "'%s' AS comment " - + "FROM request;" - ) - cmd = cmd % ( - nextval(migrate_engine, "request_event"), - localtimestamp(migrate_engine), - localtimestamp(migrate_engine), - "Imported from request table", - ) - migrate_engine.execute(cmd) - - drop_column("state", "request", metadata) - - -def downgrade(migrate_engine): - pass diff --git a/lib/galaxy/model/migrate/versions/0028_external_metadata_file_override.py b/lib/galaxy/model/migrate/versions/0028_external_metadata_file_override.py deleted file mode 100644 index db6fcd0c702..00000000000 --- a/lib/galaxy/model/migrate/versions/0028_external_metadata_file_override.py +++ /dev/null @@ -1,37 +0,0 @@ -""" -This script adds the filename_override_metadata column to the JobExternalOutputMetadata table, -allowing existing metadata files to be written when using external metadata and a cluster -set up with read-only access to database/files -""" - -import logging - -from sqlalchemy import ( - Column, - MetaData, - String, -) - -from galaxy.model.migrate.versions.util import ( - add_column, - drop_column, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - col = Column("filename_override_metadata", String(255)) - add_column(col, "job_external_output_metadata", metadata) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - drop_column("filename_override_metadata", "job_external_output_metadata", metadata) diff --git a/lib/galaxy/model/migrate/versions/0029_user_actions.py b/lib/galaxy/model/migrate/versions/0029_user_actions.py deleted file mode 100644 index 4848fbd41a6..00000000000 --- a/lib/galaxy/model/migrate/versions/0029_user_actions.py +++ /dev/null @@ -1,52 +0,0 @@ -""" -This migration script adds a user actions table to Galaxy. -""" - -import datetime -import logging - -from sqlalchemy import ( - Column, - DateTime, - ForeignKey, - Integer, - MetaData, - Table, - Unicode, -) - -now = datetime.datetime.utcnow -log = logging.getLogger(__name__) -metadata = MetaData() - -# New table to store user actions. -UserAction_table = Table( - "user_action", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True), - Column("session_id", Integer, ForeignKey("galaxy_session.id"), index=True), - Column("action", Unicode(255)), - Column("context", Unicode(512)), - Column("params", Unicode(1024)), -) - - -def upgrade(migrate_engine): - metadata.bind = migrate_engine - print(__doc__) - metadata.reflect() - try: - UserAction_table.create() - except Exception: - log.exception("Creating user_action table failed.") - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - try: - UserAction_table.drop() - except Exception: - log.exception("Dropping user_action table failed.") diff --git a/lib/galaxy/model/migrate/versions/0030_history_slug_column.py b/lib/galaxy/model/migrate/versions/0030_history_slug_column.py deleted file mode 100644 index 09df4baeef9..00000000000 --- a/lib/galaxy/model/migrate/versions/0030_history_slug_column.py +++ /dev/null @@ -1,41 +0,0 @@ -""" -Migration script to add column for a history slug. -""" - -import logging - -from sqlalchemy import ( - Column, - MetaData, - Table, - TEXT, -) - -from galaxy.model.migrate.versions.util import ( - add_column, - add_index, - drop_column, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - History_table = Table("history", metadata, autoload=True) - c = Column("slug", TEXT) - add_column(c, History_table, metadata) - # Index needs to be added separately because MySQL cannot index a TEXT/BLOB - # column without specifying mysql_length - add_index("ix_history_slug", History_table, "slug") - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - drop_column("slug", "history", metadata) diff --git a/lib/galaxy/model/migrate/versions/0031_community_and_workflow_tags.py b/lib/galaxy/model/migrate/versions/0031_community_and_workflow_tags.py deleted file mode 100644 index 03ed15d4348..00000000000 --- a/lib/galaxy/model/migrate/versions/0031_community_and_workflow_tags.py +++ /dev/null @@ -1,113 +0,0 @@ -""" -Migration script to (a) add and populate necessary columns for doing community tagging of histories, datasets, and pages and \ -(b) add table for doing individual and community tagging of workflows. -""" - -import logging - -from sqlalchemy import ( - Column, - ForeignKey, - Integer, - MetaData, - Table, - Unicode, -) - -from galaxy.model.migrate.versions.util import ( - add_column, - create_table, - drop_column, - drop_table, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - -StoredWorkflowTagAssociation_table = Table( - "stored_workflow_tag_association", - metadata, - Column("id", Integer, primary_key=True), - Column("stored_workflow_id", Integer, ForeignKey("stored_workflow.id"), index=True), - Column("tag_id", Integer, ForeignKey("tag.id"), index=True), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True), - Column("user_tname", Unicode(255), index=True), - Column("value", Unicode(255), index=True), - Column("user_value", Unicode(255), index=True), -) - -WorkflowTagAssociation_table = Table( - "workflow_tag_association", - metadata, - Column("id", Integer, primary_key=True), - Column("workflow_id", Integer, ForeignKey("workflow.id"), index=True), - Column("tag_id", Integer, ForeignKey("tag.id"), index=True), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True), - Column("user_tname", Unicode(255), index=True), - Column("value", Unicode(255), index=True), - Column("user_value", Unicode(255), index=True), -) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - # Create user_id column in history_tag_association table. - c = Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True) - add_column(c, "history_tag_association", metadata, index_name="ix_history_tag_association_user_id") - - # Populate column so that user_id is the id of the user who owns the history (and, up to now, was the only person able to tag the history). - migrate_engine.execute( - "UPDATE history_tag_association SET user_id=( SELECT user_id FROM history WHERE history_tag_association.history_id = history.id )" - ) - - # Create user_id column in history_dataset_association_tag_association table. - c = Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True) - add_column( - c, - "history_dataset_association_tag_association", - metadata, - index_name="ix_history_dataset_association_tag_association_user_id", - ) - - # Populate column so that user_id is the id of the user who owns the history_dataset_association (and, up to now, was the only person able to tag the page). - migrate_engine.execute( - "UPDATE history_dataset_association_tag_association SET user_id=( SELECT history.user_id FROM history, history_dataset_association WHERE history_dataset_association.history_id = history.id AND history_dataset_association.id = history_dataset_association_tag_association.history_dataset_association_id)" - ) - - # Create user_id column in page_tag_association table. - c = Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True) - add_column(c, "page_tag_association", metadata, index_name="ix_page_tag_association_user_id") - - # Populate column so that user_id is the id of the user who owns the page (and, up to now, was the only person able to tag the page). - migrate_engine.execute( - "UPDATE page_tag_association SET user_id=( SELECT user_id FROM page WHERE page_tag_association.page_id = page.id )" - ) - - # Create stored_workflow_tag_association table. - create_table(StoredWorkflowTagAssociation_table) - - # Create workflow_tag_association table. - create_table(WorkflowTagAssociation_table) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - # Drop workflow_tag_association table. - drop_table(WorkflowTagAssociation_table) - - # Drop stored_workflow_tag_association table. - drop_table(StoredWorkflowTagAssociation_table) - - # Drop user_id column from page_tag_association table. - drop_column("user_id", "page_tag_association", metadata) - - # Drop user_id column from history_dataset_association_tag_association table. - drop_column("user_id", "history_dataset_association_tag_association", metadata) - - # Drop user_id column from history_tag_association table. - drop_column("user_id", "history_tag_association", metadata) diff --git a/lib/galaxy/model/migrate/versions/0032_stored_workflow_slug_column.py b/lib/galaxy/model/migrate/versions/0032_stored_workflow_slug_column.py deleted file mode 100644 index 579bd9bbf48..00000000000 --- a/lib/galaxy/model/migrate/versions/0032_stored_workflow_slug_column.py +++ /dev/null @@ -1,41 +0,0 @@ -""" -Migration script to add slug column for stored workflow. -""" - -import logging - -from sqlalchemy import ( - Column, - MetaData, - Table, - TEXT, -) - -from galaxy.model.migrate.versions.util import ( - add_column, - add_index, - drop_column, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - StoredWorkflow_table = Table("stored_workflow", metadata, autoload=True) - c = Column("slug", TEXT) - add_column(c, StoredWorkflow_table, metadata) - # Index needs to be added separately because MySQL cannot index a TEXT/BLOB - # column without specifying mysql_length - add_index("ix_stored_workflow_slug", StoredWorkflow_table, "slug") - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - drop_column("slug", "stored_workflow", metadata) diff --git a/lib/galaxy/model/migrate/versions/0033_published_cols_for_histories_and_workflows.py b/lib/galaxy/model/migrate/versions/0033_published_cols_for_histories_and_workflows.py deleted file mode 100644 index 3e965229466..00000000000 --- a/lib/galaxy/model/migrate/versions/0033_published_cols_for_histories_and_workflows.py +++ /dev/null @@ -1,76 +0,0 @@ -""" -Migration script to add necessary columns for distinguishing between viewing/importing and publishing histories, \ -workflows, and pages. Script adds published column to histories and workflows and importable column to pages. -""" - -import logging - -from sqlalchemy import ( - Boolean, - Column, - Index, - MetaData, - Table, -) - -from galaxy.model.migrate.versions.util import ( - add_column, - drop_column, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - # Create published column in history table. - History_table = Table("history", metadata, autoload=True) - c = Column("published", Boolean, index=True) - add_column(c, History_table, metadata, index_name="ix_history_published") - if migrate_engine.name != "sqlite": - # Create index for published column in history table. - try: - i = Index("ix_history_published", History_table.c.published) - i.create() - except Exception: - # Mysql doesn't have a named index, but alter should work - History_table.c.published.alter(unique=False) - - # Create published column in stored workflows table. - StoredWorkflow_table = Table("stored_workflow", metadata, autoload=True) - c = Column("published", Boolean, index=True) - add_column(c, StoredWorkflow_table, metadata, index_name="ix_stored_workflow_published") - if migrate_engine.name != "sqlite": - # Create index for published column in stored workflows table. - try: - i = Index("ix_stored_workflow_published", StoredWorkflow_table.c.published) - i.create() - except Exception: - # Mysql doesn't have a named index, but alter should work - StoredWorkflow_table.c.published.alter(unique=False) - - # Create importable column in page table. - Page_table = Table("page", metadata, autoload=True) - c = Column("importable", Boolean, index=True) - add_column(c, Page_table, metadata, index_name="ix_page_importable") - if migrate_engine.name != "sqlite": - # Create index for importable column in page table. - try: - i = Index("ix_page_importable", Page_table.c.importable) - i.create() - except Exception: - # Mysql doesn't have a named index, but alter should work - Page_table.c.importable.alter(unique=False) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - drop_column("published", "history", metadata) - drop_column("published", "stored_workflow", metadata) - drop_column("importable", "page", metadata) diff --git a/lib/galaxy/model/migrate/versions/0034_page_user_share_association.py b/lib/galaxy/model/migrate/versions/0034_page_user_share_association.py deleted file mode 100644 index fa8b3fb3c07..00000000000 --- a/lib/galaxy/model/migrate/versions/0034_page_user_share_association.py +++ /dev/null @@ -1,47 +0,0 @@ -""" -Migration script to create a table for page-user share association. -""" - -import logging - -from sqlalchemy import ( - Column, - ForeignKey, - Integer, - MetaData, - Table, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - -PageUserShareAssociation_table = Table( - "page_user_share_association", - metadata, - Column("id", Integer, primary_key=True), - Column("page_id", Integer, ForeignKey("page.id"), index=True), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True), -) - - -def upgrade(migrate_engine): - metadata.bind = migrate_engine - print(__doc__) - metadata.reflect() - - # Create stored_workflow_tag_association table. - try: - PageUserShareAssociation_table.create() - except Exception: - log.exception("Creating page_user_share_association table failed.") - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - # Drop workflow_tag_association table. - try: - PageUserShareAssociation_table.drop() - except Exception: - log.exception("Dropping page_user_share_association table failed.") diff --git a/lib/galaxy/model/migrate/versions/0035_item_annotations_and_workflow_step_tags.py b/lib/galaxy/model/migrate/versions/0035_item_annotations_and_workflow_step_tags.py deleted file mode 100644 index 35b6fe37214..00000000000 --- a/lib/galaxy/model/migrate/versions/0035_item_annotations_and_workflow_step_tags.py +++ /dev/null @@ -1,105 +0,0 @@ -""" -Migration script to (a) create tables for annotating objects and (b) create tags for workflow steps. -""" - -import logging - -from sqlalchemy import ( - Column, - ForeignKey, - Index, - Integer, - MetaData, - Table, - TEXT, - Unicode, -) - -from galaxy.model.migrate.versions.util import ( - create_table, - drop_table, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - -# Annotation tables. - -HistoryAnnotationAssociation_table = Table( - "history_annotation_association", - metadata, - Column("id", Integer, primary_key=True), - Column("history_id", Integer, ForeignKey("history.id"), index=True), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True), - Column("annotation", TEXT), - Index("ix_history_anno_assoc_annotation", "annotation", mysql_length=200), -) - -HistoryDatasetAssociationAnnotationAssociation_table = Table( - "history_dataset_association_annotation_association", - metadata, - Column("id", Integer, primary_key=True), - Column("history_dataset_association_id", Integer, ForeignKey("history_dataset_association.id"), index=True), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True), - Column("annotation", TEXT), - Index("ix_history_dataset_anno_assoc_annotation", "annotation", mysql_length=200), -) - -StoredWorkflowAnnotationAssociation_table = Table( - "stored_workflow_annotation_association", - metadata, - Column("id", Integer, primary_key=True), - Column("stored_workflow_id", Integer, ForeignKey("stored_workflow.id"), index=True), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True), - Column("annotation", TEXT), - Index("ix_stored_workflow_ann_assoc_annotation", "annotation", mysql_length=200), -) - -WorkflowStepAnnotationAssociation_table = Table( - "workflow_step_annotation_association", - metadata, - Column("id", Integer, primary_key=True), - Column("workflow_step_id", Integer, ForeignKey("workflow_step.id"), index=True), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True), - Column("annotation", TEXT), - Index("ix_workflow_step_ann_assoc_annotation", "annotation", mysql_length=200), -) - -# Tagging tables. - -WorkflowStepTagAssociation_table = Table( - "workflow_step_tag_association", - metadata, - Column("id", Integer, primary_key=True), - Column("workflow_step_id", Integer, ForeignKey("workflow_step.id"), index=True), - Column("tag_id", Integer, ForeignKey("tag.id"), index=True), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True), - Column("user_tname", Unicode(255), index=True), - Column("value", Unicode(255), index=True), - Column("user_value", Unicode(255), index=True), -) - -TABLES = [ - HistoryAnnotationAssociation_table, - HistoryDatasetAssociationAnnotationAssociation_table, - StoredWorkflowAnnotationAssociation_table, - WorkflowStepAnnotationAssociation_table, - WorkflowStepTagAssociation_table, -] - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - for table in TABLES: - create_table(table) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - for table in TABLES: - drop_table(table) diff --git a/lib/galaxy/model/migrate/versions/0036_add_deleted_column_to_library_template_assoc_tables.py b/lib/galaxy/model/migrate/versions/0036_add_deleted_column_to_library_template_assoc_tables.py deleted file mode 100644 index 7fc398a4ea2..00000000000 --- a/lib/galaxy/model/migrate/versions/0036_add_deleted_column_to_library_template_assoc_tables.py +++ /dev/null @@ -1,69 +0,0 @@ -""" -Migration script to add a deleted column to the following tables: -library_info_association, library_folder_info_association, library_dataset_dataset_info_association. -""" - -import logging - -from sqlalchemy import ( - Boolean, - Column, - MetaData, - Table, -) - -from galaxy.model.migrate.versions.util import engine_false - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - try: - LibraryInfoAssociation_table = Table("library_info_association", metadata, autoload=True) - c = Column("deleted", Boolean, index=True, default=False) - c.create(LibraryInfoAssociation_table, index_name="ix_library_info_association_deleted") - assert c is LibraryInfoAssociation_table.c.deleted - except Exception: - log.exception("Adding column 'deleted' to 'library_info_association' table failed.") - cmd = f"UPDATE library_info_association SET deleted = {engine_false(migrate_engine)}" - try: - migrate_engine.execute(cmd) - except Exception: - log.exception("deleted to false in library_info_association failed.") - try: - LibraryFolderInfoAssociation_table = Table("library_folder_info_association", metadata, autoload=True) - c = Column("deleted", Boolean, index=True, default=False) - c.create(LibraryFolderInfoAssociation_table, index_name="ix_library_folder_info_association_deleted") - assert c is LibraryFolderInfoAssociation_table.c.deleted - except Exception: - log.exception("Adding column 'deleted' to 'library_folder_info_association' table failed.") - cmd = f"UPDATE library_folder_info_association SET deleted = {engine_false(migrate_engine)}" - try: - migrate_engine.execute(cmd) - except Exception: - log.exception("deleted to false in library_folder_info_association failed.") - try: - LibraryDatasetDatasetInfoAssociation_table = Table( - "library_dataset_dataset_info_association", metadata, autoload=True - ) - c = Column("deleted", Boolean, index=True, default=False) - c.create( - LibraryDatasetDatasetInfoAssociation_table, index_name="ix_library_dataset_dataset_info_association_deleted" - ) - assert c is LibraryDatasetDatasetInfoAssociation_table.c.deleted - except Exception: - log.exception("Adding column 'deleted' to 'library_dataset_dataset_info_association' table failed.") - cmd = f"UPDATE library_dataset_dataset_info_association SET deleted = {engine_false(migrate_engine)}" - try: - migrate_engine.execute(cmd) - except Exception: - log.exception("deleted to false in library_dataset_dataset_info_association failed.") - - -def downgrade(migrate_engine): - pass diff --git a/lib/galaxy/model/migrate/versions/0037_samples_library.py b/lib/galaxy/model/migrate/versions/0037_samples_library.py deleted file mode 100644 index ec536e88e2f..00000000000 --- a/lib/galaxy/model/migrate/versions/0037_samples_library.py +++ /dev/null @@ -1,78 +0,0 @@ -""" -This migration script removes the library_id & folder_id fields in the 'request' table and -adds the same to the 'sample' table. This also adds a 'datatx' column to request_type table -to store the sequencer login information. Finally, this adds a 'dataset_files' column to -the sample table. -""" - -import datetime -import logging - -from sqlalchemy import ( - Column, - ForeignKey, - Integer, - MetaData, - Table, -) - -from galaxy.model.custom_types import JSONType -from galaxy.model.migrate.versions.util import ( - add_column, - drop_column, -) - -log = logging.getLogger(__name__) -now = datetime.datetime.utcnow -metadata = MetaData() - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - # Add the datatx_info column in 'request_type' table - col = Column("datatx_info", JSONType) - add_column(col, "request_type", metadata) - - # Delete the library_id column in 'request' table - Request_table = Table("request", metadata, autoload=True) - # TODO: Dropping a column used in a foreign key fails in MySQL, need to remove the FK first. - drop_column("library_id", Request_table) - - # Delete the folder_id column in 'request' table - # TODO: Dropping a column used in a foreign key fails in MySQL, need to remove the FK first. - drop_column("folder_id", Request_table) - - # Add the dataset_files column in 'sample' table - Sample_table = Table("sample", metadata, autoload=True) - col = Column("dataset_files", JSONType) - add_column(col, Sample_table, metadata) - - # Add the library_id column in 'sample' table - col = Column("library_id", Integer, ForeignKey("library.id"), index=True) - add_column(col, Sample_table, metadata, index_name="ix_sample_library_id") - - # Add the library_id column in 'sample' table - col = Column("folder_id", Integer, ForeignKey("library_folder.id"), index=True) - add_column(col, Sample_table, metadata, index_name="ix_sample_library_folder_id") - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - Sample_table = Table("sample", metadata, autoload=True) - drop_column("folder_id", Sample_table) - drop_column("library_id", Sample_table) - drop_column("dataset_files", Sample_table) - - Request_table = Table("request", metadata, autoload=True) - col = Column("folder_id", Integer, ForeignKey("library_folder.id"), index=True) - add_column(col, Request_table, metadata, index_name="ix_request_folder_id") - - col = Column("library_id", Integer, ForeignKey("library.id"), index=True) - add_column(col, Request_table, metadata, index_name="ix_request_library_id") - - drop_column("datatx_info", "request_type", metadata) diff --git a/lib/galaxy/model/migrate/versions/0038_add_inheritable_column_to_library_template_assoc_tables.py b/lib/galaxy/model/migrate/versions/0038_add_inheritable_column_to_library_template_assoc_tables.py deleted file mode 100644 index 1e0d4ada2b5..00000000000 --- a/lib/galaxy/model/migrate/versions/0038_add_inheritable_column_to_library_template_assoc_tables.py +++ /dev/null @@ -1,87 +0,0 @@ -""" -Migration script to add an inheritable column to the following tables: -library_info_association, library_folder_info_association. -Also, in case of sqlite check if the previous migration script deleted the -request table and if so, restore the table. -""" - -import datetime -import logging - -from sqlalchemy import ( - Boolean, - Column, - DateTime, - ForeignKey, - Integer, - MetaData, - Table, - TEXT, -) - -from galaxy.model.custom_types import TrimmedString -from galaxy.model.migrate.versions.util import engine_false - -now = datetime.datetime.utcnow -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - - # In case of sqlite, check if the previous migration script deleted the - # request table and if so, restore the table. - if migrate_engine.name == "sqlite": - if not migrate_engine.has_table("request"): - # load the tables referenced in foreign keys - metadata.reflect(only=["form_values", "request_type", "galaxy_user"]) - # create a temporary table - Request_table = Table( - "request", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("name", TrimmedString(255), nullable=False), - Column("desc", TEXT), - Column("form_values_id", Integer, ForeignKey("form_values.id"), index=True), - Column("request_type_id", Integer, ForeignKey("request_type.id"), index=True), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True), - Column("deleted", Boolean, index=True, default=False), - ) - try: - Request_table.create() - except Exception: - log.exception("Creating request table failed.") - - metadata.reflect() - try: - LibraryInfoAssociation_table = Table("library_info_association", metadata, autoload=True) - c = Column("inheritable", Boolean, index=True, default=False) - c.create(LibraryInfoAssociation_table, index_name="ix_library_info_association_inheritable") - assert c is LibraryInfoAssociation_table.c.inheritable - except Exception: - log.exception("Adding column 'inheritable' to 'library_info_association' table failed.") - cmd = f"UPDATE library_info_association SET inheritable = {engine_false(migrate_engine)}" - try: - migrate_engine.execute(cmd) - except Exception: - log.exception("Setting value of column inheritable to false in library_info_association failed.") - try: - LibraryFolderInfoAssociation_table = Table("library_folder_info_association", metadata, autoload=True) - c = Column("inheritable", Boolean, index=True, default=False) - c.create(LibraryFolderInfoAssociation_table, index_name="ix_library_folder_info_association_inheritable") - assert c is LibraryFolderInfoAssociation_table.c.inheritable - except Exception: - log.exception("Adding column 'inheritable' to 'library_folder_info_association' table failed.") - cmd = f"UPDATE library_folder_info_association SET inheritable = {engine_false(migrate_engine)}" - try: - migrate_engine.execute(cmd) - except Exception: - log.exception("Setting value of column inheritable to false in library_folder_info_association failed.") - - -def downgrade(migrate_engine): - pass diff --git a/lib/galaxy/model/migrate/versions/0039_add_synopsis_column_to_library_table.py b/lib/galaxy/model/migrate/versions/0039_add_synopsis_column_to_library_table.py deleted file mode 100644 index 469987c29db..00000000000 --- a/lib/galaxy/model/migrate/versions/0039_add_synopsis_column_to_library_table.py +++ /dev/null @@ -1,38 +0,0 @@ -""" -Migration script to add a synopsis column to the library table. -""" - -import logging - -from sqlalchemy import ( - Column, - MetaData, - Table, - TEXT, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - try: - Library_table = Table("library", metadata, autoload=True) - c = Column("synopsis", TEXT) - c.create(Library_table) - assert c is Library_table.c.synopsis - except Exception: - log.exception("Adding column 'synopsis' to 'library' table failed.") - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - try: - Library_table = Table("library", metadata, autoload=True) - Library_table.c.synopsis.drop() - except Exception: - log.exception("Dropping column 'synopsis' from 'library' table failed") diff --git a/lib/galaxy/model/migrate/versions/0040_page_annotations.py b/lib/galaxy/model/migrate/versions/0040_page_annotations.py deleted file mode 100644 index 35b12b27a3c..00000000000 --- a/lib/galaxy/model/migrate/versions/0040_page_annotations.py +++ /dev/null @@ -1,48 +0,0 @@ -""" -Migration script to (a) create tables for annotating pages. -""" - -import logging - -from sqlalchemy import ( - Column, - ForeignKey, - Index, - Integer, - MetaData, - Table, - TEXT, -) - -from galaxy.model.migrate.versions.util import ( - create_table, - drop_table, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - -PageAnnotationAssociation_table = Table( - "page_annotation_association", - metadata, - Column("id", Integer, primary_key=True), - Column("page_id", Integer, ForeignKey("page.id"), index=True), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True), - Column("annotation", TEXT), - Index("ix_page_annotation_association_annotation", "annotation", mysql_length=200), -) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - create_table(PageAnnotationAssociation_table) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - drop_table(PageAnnotationAssociation_table) diff --git a/lib/galaxy/model/migrate/versions/0041_workflow_invocation.py b/lib/galaxy/model/migrate/versions/0041_workflow_invocation.py deleted file mode 100644 index a30d56d5375..00000000000 --- a/lib/galaxy/model/migrate/versions/0041_workflow_invocation.py +++ /dev/null @@ -1,63 +0,0 @@ -""" -Migration script to create tables for tracking workflow invocations. -""" - -import datetime -import logging - -from sqlalchemy import ( - Column, - DateTime, - ForeignKey, - Integer, - MetaData, - Table, -) - -from galaxy.model.migrate.versions.util import ( - create_table, - drop_table, -) - -log = logging.getLogger(__name__) -now = datetime.datetime.utcnow -metadata = MetaData() - -WorkflowInvocation_table = Table( - "workflow_invocation", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("workflow_id", Integer, ForeignKey("workflow.id"), index=True, nullable=False), -) - -WorkflowInvocationStep_table = Table( - "workflow_invocation_step", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("workflow_invocation_id", Integer, ForeignKey("workflow_invocation.id"), index=True, nullable=False), - Column("workflow_step_id", Integer, ForeignKey("workflow_step.id"), index=True, nullable=False), - Column("job_id", Integer, ForeignKey("job.id"), index=True, nullable=False), -) - -tables = [WorkflowInvocation_table, WorkflowInvocationStep_table] - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - for table in tables: - create_table(table) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - for table in reversed(tables): - drop_table(table) diff --git a/lib/galaxy/model/migrate/versions/0042_workflow_invocation_fix.py b/lib/galaxy/model/migrate/versions/0042_workflow_invocation_fix.py deleted file mode 100644 index 0164a00746c..00000000000 --- a/lib/galaxy/model/migrate/versions/0042_workflow_invocation_fix.py +++ /dev/null @@ -1,64 +0,0 @@ -""" -Drop and readd workflow invocation tables, allowing null jobs -""" - -import datetime -import logging - -from sqlalchemy import ( - Column, - DateTime, - ForeignKey, - Integer, - MetaData, - Table, -) - -from galaxy.model.migrate.versions.util import ( - create_table, - drop_table, -) - -log = logging.getLogger(__name__) -now = datetime.datetime.utcnow -metadata = MetaData() - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - # 1) Drop - for table_name in ["workflow_invocation_step", "workflow_invocation"]: - t = Table(table_name, metadata, autoload=True) - drop_table(t) - metadata.remove(t) - - # 2) Re-add - WorkflowInvocation_table = Table( - "workflow_invocation", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("workflow_id", Integer, ForeignKey("workflow.id"), index=True, nullable=False), - ) - - WorkflowInvocationStep_table = Table( - "workflow_invocation_step", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("workflow_invocation_id", Integer, ForeignKey("workflow_invocation.id"), index=True, nullable=False), - Column("workflow_step_id", Integer, ForeignKey("workflow_step.id"), index=True, nullable=False), - Column("job_id", Integer, ForeignKey("job.id"), index=True, nullable=True), - ) - - for table in [WorkflowInvocation_table, WorkflowInvocationStep_table]: - create_table(table) - - -def downgrade(migrate_engine): - pass diff --git a/lib/galaxy/model/migrate/versions/0043_visualization_sharing_tagging_annotating.py b/lib/galaxy/model/migrate/versions/0043_visualization_sharing_tagging_annotating.py deleted file mode 100644 index 833371fb269..00000000000 --- a/lib/galaxy/model/migrate/versions/0043_visualization_sharing_tagging_annotating.py +++ /dev/null @@ -1,129 +0,0 @@ -""" -Migration script to create tables and columns for sharing visualizations. -""" - -import logging - -from sqlalchemy import ( - Boolean, - Column, - ForeignKey, - Index, - Integer, - MetaData, - Table, - TEXT, - Unicode, -) - -from galaxy.model.migrate.versions.util import ( - add_column, - add_index, - create_table, - drop_column, - drop_table, - engine_false, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - -# Sharing visualizations. - -VisualizationUserShareAssociation_table = Table( - "visualization_user_share_association", - metadata, - Column("id", Integer, primary_key=True), - Column("visualization_id", Integer, ForeignKey("visualization.id"), index=True), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True), -) - -# Tagging visualizations. - -VisualizationTagAssociation_table = Table( - "visualization_tag_association", - metadata, - Column("id", Integer, primary_key=True), - Column("visualization_id", Integer, ForeignKey("visualization.id"), index=True), - Column("tag_id", Integer, ForeignKey("tag.id"), index=True), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True), - Column("user_tname", Unicode(255), index=True), - Column("value", Unicode(255), index=True), - Column("user_value", Unicode(255), index=True), -) - -# Annotating visualizations. - -VisualizationAnnotationAssociation_table = Table( - "visualization_annotation_association", - metadata, - Column("id", Integer, primary_key=True), - Column("visualization_id", Integer, ForeignKey("visualization.id"), index=True), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True), - Column("annotation", TEXT), - Index("ix_visualization_annotation_association_annotation", "annotation", mysql_length=200), -) - -TABLES = [ - VisualizationUserShareAssociation_table, - VisualizationTagAssociation_table, - VisualizationAnnotationAssociation_table, -] - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - for table in TABLES: - create_table(table) - - # Add columns & create indices for supporting sharing to visualization table. - Visualization_table = Table("visualization", metadata, autoload=True) - deleted_column = Column("deleted", Boolean, default=False, index=True) - add_column(deleted_column, Visualization_table, metadata, index_name="ix_visualization_deleted") - try: - # Fill column with default value. - cmd = f"UPDATE visualization SET deleted = {engine_false(migrate_engine)}" - migrate_engine.execute(cmd) - except Exception: - log.exception("Updating column 'deleted' of table 'visualization' failed.") - - importable_column = Column("importable", Boolean, default=False, index=True) - add_column(importable_column, Visualization_table, metadata, index_name="ix_visualization_importable") - try: - # Fill column with default value. - cmd = f"UPDATE visualization SET importable = {engine_false(migrate_engine)}" - migrate_engine.execute(cmd) - except Exception: - log.exception("Updating column 'importable' of table 'visualization' failed.") - - slug_column = Column("slug", TEXT) - add_column(slug_column, Visualization_table, metadata) - # Index needs to be added separately because MySQL cannot index a TEXT/BLOB - # column without specifying mysql_length - add_index("ix_visualization_slug", Visualization_table, "slug") - - published_column = Column("published", Boolean, index=True) - add_column(published_column, Visualization_table, metadata, index_name="ix_visualization_published") - try: - # Fill column with default value. - cmd = f"UPDATE visualization SET published = {engine_false(migrate_engine)}" - migrate_engine.execute(cmd) - except Exception: - log.exception("Updating column 'published' of table 'visualization' failed.") - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - Visualization_table = Table("visualization", metadata, autoload=True) - drop_column("deleted", Visualization_table) - drop_column("importable", Visualization_table) - drop_column("slug", Visualization_table) - drop_column("published", Visualization_table) - - for table in TABLES: - drop_table(table) diff --git a/lib/galaxy/model/migrate/versions/0044_add_notify_column_to_request_table.py b/lib/galaxy/model/migrate/versions/0044_add_notify_column_to_request_table.py deleted file mode 100644 index f9257ec9e8f..00000000000 --- a/lib/galaxy/model/migrate/versions/0044_add_notify_column_to_request_table.py +++ /dev/null @@ -1,35 +0,0 @@ -""" -Migration script to add a notify column to the request table. -""" - -import logging - -from sqlalchemy import ( - Boolean, - Column, - MetaData, -) - -from galaxy.model.migrate.versions.util import ( - add_column, - drop_column, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - c = Column("notify", Boolean, default=False) - add_column(c, "request", metadata) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - drop_column("notify", "request", metadata) diff --git a/lib/galaxy/model/migrate/versions/0045_request_type_permissions_table.py b/lib/galaxy/model/migrate/versions/0045_request_type_permissions_table.py deleted file mode 100644 index e32db165e51..00000000000 --- a/lib/galaxy/model/migrate/versions/0045_request_type_permissions_table.py +++ /dev/null @@ -1,51 +0,0 @@ -""" -Migration script to add the request_type_permissions table. -""" - -import datetime -import logging - -from sqlalchemy import ( - Column, - DateTime, - ForeignKey, - Integer, - MetaData, - Table, - TEXT, -) - -now = datetime.datetime.utcnow -log = logging.getLogger(__name__) -metadata = MetaData() - -RequestTypePermissions_table = Table( - "request_type_permissions", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("action", TEXT), - Column("request_type_id", Integer, ForeignKey("request_type.id"), nullable=True, index=True), - Column("role_id", Integer, ForeignKey("role.id"), index=True), -) - - -def upgrade(migrate_engine): - metadata.bind = migrate_engine - print(__doc__) - metadata.reflect() - try: - RequestTypePermissions_table.create() - except Exception: - log.exception("Creating request_type_permissions table failed.") - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - try: - RequestTypePermissions_table = Table("request_type_permissions", metadata, autoload=True) - RequestTypePermissions_table.drop() - except Exception: - log.exception("Dropping 'request_type_permissions' table failed.") diff --git a/lib/galaxy/model/migrate/versions/0046_post_job_actions.py b/lib/galaxy/model/migrate/versions/0046_post_job_actions.py deleted file mode 100644 index c2dc6fa0b4e..00000000000 --- a/lib/galaxy/model/migrate/versions/0046_post_job_actions.py +++ /dev/null @@ -1,46 +0,0 @@ -""" -Migration script to create tables for handling post-job actions. -""" - -import logging - -from sqlalchemy import ( - Column, - ForeignKey, - Integer, - MetaData, - String, - Table, -) - -from galaxy.model.custom_types import JSONType -from galaxy.model.migrate.versions.util import ( - create_table, - drop_table, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - -PostJobAction_table = Table( - "post_job_action", - metadata, - Column("id", Integer, primary_key=True), - Column("workflow_step_id", Integer, ForeignKey("workflow_step.id"), index=True, nullable=False), - Column("action_type", String(255), nullable=False), - Column("output_name", String(255), nullable=True), - Column("action_arguments", JSONType, nullable=True), -) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - create_table(PostJobAction_table) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - drop_table(PostJobAction_table) diff --git a/lib/galaxy/model/migrate/versions/0047_job_table_user_id_column.py b/lib/galaxy/model/migrate/versions/0047_job_table_user_id_column.py deleted file mode 100644 index b24b89dbc60..00000000000 --- a/lib/galaxy/model/migrate/versions/0047_job_table_user_id_column.py +++ /dev/null @@ -1,61 +0,0 @@ -""" -Add a user_id column to the job table. -""" - -import logging - -from sqlalchemy import ( - Column, - ForeignKey, - Integer, - MetaData, - Table, -) - -from galaxy.model.migrate.versions.util import ( - add_column, - drop_column, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - Job_table = Table("job", metadata, autoload=True) - col = Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True, nullable=True) - add_column(col, Job_table, metadata, index_name="ix_job_user_id") - try: - cmd = ( - "SELECT job.id AS galaxy_job_id, " - + "galaxy_session.user_id AS galaxy_user_id " - + "FROM job " - + "JOIN galaxy_session ON job.session_id = galaxy_session.id;" - ) - job_users = migrate_engine.execute(cmd).fetchall() - print("Updating user_id column in job table for ", len(job_users), " rows...") - print("") - update_count = 0 - for row in job_users: - if row.galaxy_user_id: - cmd = "UPDATE job SET user_id = %d WHERE id = %d" % (int(row.galaxy_user_id), int(row.galaxy_job_id)) - update_count += 1 - migrate_engine.execute(cmd) - print("Updated column 'user_id' for ", update_count, " rows of table 'job'.") - print( - len(job_users) - update_count, " rows have no user_id since the value was NULL in the galaxy_session table." - ) - print("") - except Exception: - log.exception("Updating column 'user_id' of table 'job' failed.") - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - drop_column("user_id", "job", metadata) diff --git a/lib/galaxy/model/migrate/versions/0048_dataset_instance_state_column.py b/lib/galaxy/model/migrate/versions/0048_dataset_instance_state_column.py deleted file mode 100644 index 5187ce14bc4..00000000000 --- a/lib/galaxy/model/migrate/versions/0048_dataset_instance_state_column.py +++ /dev/null @@ -1,40 +0,0 @@ -""" -Add a state column to the history_dataset_association and library_dataset_dataset_association table. -""" - -import logging - -from sqlalchemy import ( - Column, - MetaData, -) - -from galaxy.model.custom_types import TrimmedString -from galaxy.model.migrate.versions.util import ( - add_column, - drop_column, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - -DATASET_INSTANCE_TABLE_NAMES = ["history_dataset_association", "library_dataset_dataset_association"] - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - for table_name in DATASET_INSTANCE_TABLE_NAMES: - col = Column("state", TrimmedString(64), index=True, nullable=True) - index_name = f"ix_{table_name}_state" - add_column(col, table_name, metadata, index_name=index_name) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - for table_name in DATASET_INSTANCE_TABLE_NAMES: - drop_column("state", table_name, metadata) diff --git a/lib/galaxy/model/migrate/versions/0049_api_keys_table.py b/lib/galaxy/model/migrate/versions/0049_api_keys_table.py deleted file mode 100644 index 5bde68d9201..00000000000 --- a/lib/galaxy/model/migrate/versions/0049_api_keys_table.py +++ /dev/null @@ -1,50 +0,0 @@ -""" -Migration script to add the api_keys table. -""" - -import datetime -import logging - -from sqlalchemy import ( - Column, - DateTime, - ForeignKey, - Integer, - MetaData, - Table, -) - -from galaxy.model.custom_types import TrimmedString - -now = datetime.datetime.utcnow -log = logging.getLogger(__name__) -metadata = MetaData() - -APIKeys_table = Table( - "api_keys", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True), - Column("key", TrimmedString(32), index=True, unique=True), -) - - -def upgrade(migrate_engine): - metadata.bind = migrate_engine - print(__doc__) - metadata.reflect() - try: - APIKeys_table.create() - except Exception: - log.exception("Creating api_keys table failed.") - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - # Load existing tables - metadata.reflect() - try: - APIKeys_table.drop() - except Exception: - log.exception("Dropping api_keys table failed.") diff --git a/lib/galaxy/model/migrate/versions/0050_drop_cloud_tables.py b/lib/galaxy/model/migrate/versions/0050_drop_cloud_tables.py deleted file mode 100644 index 3b7abf2ebfd..00000000000 --- a/lib/galaxy/model/migrate/versions/0050_drop_cloud_tables.py +++ /dev/null @@ -1,183 +0,0 @@ -""" -This script drops tables that were associated with the old Galaxy Cloud functionality. -""" - -import datetime -import logging - -from sqlalchemy import ( - Boolean, - Column, - DateTime, - ForeignKey, - Integer, - MetaData, - Table, - TEXT, -) - -from galaxy.model.migrate.versions.util import ( - create_table, - drop_table, -) - -now = datetime.datetime.utcnow -log = logging.getLogger(__name__) -metadata = MetaData() - -CloudImage_table = Table( - "cloud_image", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("provider_type", TEXT), - Column("image_id", TEXT, nullable=False), - Column("manifest", TEXT), - Column("state", TEXT), - Column("architecture", TEXT), - Column("deleted", Boolean, default=False), -) - -""" UserConfiguredInstance (UCI) table """ -UCI_table = Table( - "cloud_uci", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True, nullable=False), - Column("credentials_id", Integer, ForeignKey("cloud_user_credentials.id"), index=True), - Column("key_pair_name", TEXT), - Column("key_pair_material", TEXT), - Column("name", TEXT), - Column("state", TEXT), - Column("error", TEXT), - Column("total_size", Integer), - Column("launch_time", DateTime), - Column("deleted", Boolean, default=False), -) - -CloudInstance_table = Table( - "cloud_instance", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("launch_time", DateTime), - Column("stop_time", DateTime), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True, nullable=False), - Column("uci_id", Integer, ForeignKey("cloud_uci.id"), index=True), - Column("type", TEXT), - Column("reservation_id", TEXT), - Column("instance_id", TEXT), - Column("mi_id", Integer, ForeignKey("cloud_image.id"), index=True), - Column("state", TEXT), - Column("error", TEXT), - Column("public_dns", TEXT), - Column("private_dns", TEXT), - Column("security_group", TEXT), - Column("availability_zone", TEXT), -) - -CloudStore_table = Table( - "cloud_store", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("attach_time", DateTime), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True, nullable=False), - Column("uci_id", Integer, ForeignKey("cloud_uci.id"), index=True, nullable=False), - Column("volume_id", TEXT), - Column("size", Integer, nullable=False), - Column("availability_zone", TEXT), - Column("inst_id", Integer, ForeignKey("cloud_instance.id")), - Column("status", TEXT), - Column("device", TEXT), - Column("space_consumed", Integer), - Column("error", TEXT), - Column("deleted", Boolean, default=False), -) - -CloudSnapshot_table = Table( - "cloud_snapshot", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True, nullable=False), - Column("uci_id", Integer, ForeignKey("cloud_uci.id"), index=True), - Column("store_id", Integer, ForeignKey("cloud_store.id"), index=True, nullable=False), - Column("snapshot_id", TEXT), - Column("status", TEXT), - Column("description", TEXT), - Column("error", TEXT), - Column("deleted", Boolean, default=False), -) - -CloudUserCredentials_table = Table( - "cloud_user_credentials", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True, nullable=False), - Column("provider_id", Integer, ForeignKey("cloud_provider.id"), index=True, nullable=False), - Column("name", TEXT), - Column("access_key", TEXT), - Column("secret_key", TEXT), - Column("deleted", Boolean, default=False), -) - -CloudProvider_table = Table( - "cloud_provider", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True, nullable=False), - Column("type", TEXT, nullable=False), - Column("name", TEXT), - Column("region_connection", TEXT), - Column("region_name", TEXT), - Column("region_endpoint", TEXT), - Column("is_secure", Boolean), - Column("host", TEXT), - Column("port", Integer), - Column("proxy", TEXT), - Column("proxy_port", TEXT), - Column("proxy_user", TEXT), - Column("proxy_pass", TEXT), - Column("debug", Integer), - Column("https_connection_factory", TEXT), - Column("path", TEXT), - Column("deleted", Boolean, default=False), -) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - drop_table(CloudSnapshot_table) - drop_table(CloudStore_table) - drop_table(CloudInstance_table) - drop_table(UCI_table) - drop_table(CloudImage_table) - drop_table(CloudUserCredentials_table) - drop_table(CloudProvider_table) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - create_table(CloudProvider_table) - create_table(CloudUserCredentials_table) - create_table(CloudImage_table) - create_table(UCI_table) - create_table(CloudInstance_table) - create_table(CloudStore_table) - create_table(CloudSnapshot_table) diff --git a/lib/galaxy/model/migrate/versions/0051_imported_col_for_jobs_table.py b/lib/galaxy/model/migrate/versions/0051_imported_col_for_jobs_table.py deleted file mode 100644 index 410e78d2dca..00000000000 --- a/lib/galaxy/model/migrate/versions/0051_imported_col_for_jobs_table.py +++ /dev/null @@ -1,43 +0,0 @@ -""" -Migration script to add imported column for jobs table. -""" - -import logging - -from sqlalchemy import ( - Boolean, - Column, - MetaData, - Table, -) - -from galaxy.model.migrate.versions.util import ( - add_column, - drop_column, - engine_false, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - # Create and initialize imported column in job table. - Jobs_table = Table("job", metadata, autoload=True) - c = Column("imported", Boolean, default=False, index=True) - add_column(c, Jobs_table, metadata, index_name="ix_job_imported") - try: - migrate_engine.execute(f"UPDATE job SET imported={engine_false(migrate_engine)}") - except Exception: - log.exception("Updating column 'imported' of table 'job' failed.") - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - drop_column("imported", "job", metadata) diff --git a/lib/galaxy/model/migrate/versions/0052_sample_dataset_table.py b/lib/galaxy/model/migrate/versions/0052_sample_dataset_table.py deleted file mode 100644 index c597856ad91..00000000000 --- a/lib/galaxy/model/migrate/versions/0052_sample_dataset_table.py +++ /dev/null @@ -1,97 +0,0 @@ -""" -Migration script to add the sample_dataset table and remove the 'dataset_files' column -from the 'sample' table -""" - -import datetime -import logging -from json import loads - -from sqlalchemy import ( - Column, - DateTime, - ForeignKey, - Integer, - MetaData, - Table, - TEXT, -) -from sqlalchemy.exc import NoSuchTableError - -from galaxy.model.custom_types import TrimmedString -from galaxy.model.migrate.versions.util import ( - localtimestamp, - nextval, -) - -now = datetime.datetime.utcnow -log = logging.getLogger(__name__) -metadata = MetaData() - - -SampleDataset_table = Table( - "sample_dataset", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("sample_id", Integer, ForeignKey("sample.id"), index=True), - Column("name", TrimmedString(255), nullable=False), - Column("file_path", TrimmedString(255), nullable=False), - Column("status", TrimmedString(255), nullable=False), - Column("error_msg", TEXT), - Column("size", TrimmedString(255)), -) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - try: - SampleDataset_table.create() - except Exception: - log.exception("Creating sample_dataset table failed.") - - cmd = "SELECT id, dataset_files FROM sample" - result = migrate_engine.execute(cmd) - for r in result: - sample_id = r[0] - if r[1]: - dataset_files = loads(r[1]) - for df in dataset_files: - if isinstance(df, dict): - cmd = "INSERT INTO sample_dataset VALUES (%s, %s, %s, %s, '%s', '%s', '%s', '%s', '%s')" - cmd = cmd % ( - nextval(migrate_engine, "sample_dataset"), - localtimestamp(migrate_engine), - localtimestamp(migrate_engine), - str(sample_id), - df.get("name", ""), - df.get("filepath", ""), - df.get("status", "").replace('"', "").replace("'", ""), - "", - df.get("size", "") - .replace('"', "") - .replace("'", "") - .replace(df.get("filepath", ""), "") - .strip(), - ) - migrate_engine.execute(cmd) - - # Delete the dataset_files column in the Sample table - try: - Sample_table = Table("sample", metadata, autoload=True) - except NoSuchTableError: - Sample_table = None - log.debug("Failed loading table sample") - if Sample_table is not None: - try: - Sample_table.c.dataset_files.drop() - except Exception: - log.exception("Deleting column 'dataset_files' from the 'sample' table failed.") - - -def downgrade(migrate_engine): - pass diff --git a/lib/galaxy/model/migrate/versions/0053_item_ratings.py b/lib/galaxy/model/migrate/versions/0053_item_ratings.py deleted file mode 100644 index c3fa9b91b6a..00000000000 --- a/lib/galaxy/model/migrate/versions/0053_item_ratings.py +++ /dev/null @@ -1,107 +0,0 @@ -""" -Migration script to create tables for rating histories, datasets, workflows, pages, and visualizations. -""" - -import logging - -from sqlalchemy import ( - Column, - ForeignKey, - Integer, - MetaData, - Table, -) - -from galaxy.model.migrate.versions.util import ( - add_index, - create_table, - drop_table, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - -# Rating tables. -HistoryRatingAssociation_table = Table( - "history_rating_association", - metadata, - Column("id", Integer, primary_key=True), - Column("history_id", Integer, ForeignKey("history.id"), index=True), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True), - Column("rating", Integer, index=True), -) - -HistoryDatasetAssociationRatingAssociation_table = Table( - "history_dataset_association_rating_association", - metadata, - Column("id", Integer, primary_key=True), - Column("history_dataset_association_id", Integer, ForeignKey("history_dataset_association.id"), index=True), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True), - Column("rating", Integer, index=True), -) - -StoredWorkflowRatingAssociation_table = Table( - "stored_workflow_rating_association", - metadata, - Column("id", Integer, primary_key=True), - Column("stored_workflow_id", Integer, ForeignKey("stored_workflow.id"), index=True), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True), - Column("rating", Integer, index=True), -) - -PageRatingAssociation_table = Table( - "page_rating_association", - metadata, - Column("id", Integer, primary_key=True), - Column("page_id", Integer, ForeignKey("page.id"), index=True), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True), - Column("rating", Integer, index=True), -) - -VisualizationRatingAssociation_table = Table( - "visualization_rating_association", - metadata, - Column("id", Integer, primary_key=True), - Column("visualization_id", Integer, ForeignKey("visualization.id"), index=True), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True), - Column("rating", Integer, index=True), -) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - create_table(HistoryRatingAssociation_table) - - # Create history_dataset_association_rating_association table. - try: - HistoryDatasetAssociationRatingAssociation_table.create() - except Exception as e: - # MySQL cannot handle long index names; when we see this error, create the index name manually. - if migrate_engine.name == "mysql" and str(e).lower().find( - "identifier name 'ix_history_dataset_association_rating_association_history_dataset_association_id' is too long" - ): - add_index( - "ix_hda_rating_association_hda_id", - HistoryDatasetAssociationRatingAssociation_table, - "history_dataset_association_id", - ) - else: - log.exception("Creating history_dataset_association_rating_association table failed.") - - create_table(StoredWorkflowRatingAssociation_table) - create_table(PageRatingAssociation_table) - create_table(VisualizationRatingAssociation_table) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - drop_table(VisualizationRatingAssociation_table) - drop_table(PageRatingAssociation_table) - drop_table(StoredWorkflowRatingAssociation_table) - drop_table(HistoryDatasetAssociationRatingAssociation_table) - drop_table(HistoryRatingAssociation_table) diff --git a/lib/galaxy/model/migrate/versions/0054_visualization_dbkey.py b/lib/galaxy/model/migrate/versions/0054_visualization_dbkey.py deleted file mode 100644 index a5c42e4e8df..00000000000 --- a/lib/galaxy/model/migrate/versions/0054_visualization_dbkey.py +++ /dev/null @@ -1,61 +0,0 @@ -""" -Migration script to add dbkey column for visualization. -""" - -import logging -from json import loads - -from sqlalchemy import ( - Column, - MetaData, - Table, - TEXT, -) - -from galaxy.model.migrate.versions.util import ( - add_column, - add_index, - drop_column, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - Visualization_table = Table("visualization", metadata, autoload=True) - Visualization_revision_table = Table("visualization_revision", metadata, autoload=True) - - # Create dbkey columns. - x = Column("dbkey", TEXT) - add_column(x, Visualization_table, metadata) - y = Column("dbkey", TEXT) - add_column(y, Visualization_revision_table, metadata) - # Indexes need to be added separately because MySQL cannot index a TEXT/BLOB - # column without specifying mysql_length - add_index("ix_visualization_dbkey", Visualization_table, "dbkey") - add_index("ix_visualization_revision_dbkey", Visualization_revision_table, "dbkey") - - all_viz = migrate_engine.execute( - "SELECT visualization.id as viz_id, visualization_revision.id as viz_rev_id, visualization_revision.config FROM visualization_revision \ - LEFT JOIN visualization ON visualization.id=visualization_revision.visualization_id" - ) - for viz in all_viz: - viz_id = viz["viz_id"] - viz_rev_id = viz["viz_rev_id"] - if viz[Visualization_revision_table.c.config]: - dbkey = loads(viz[Visualization_revision_table.c.config]).get("dbkey", "").replace("'", "\\'") - migrate_engine.execute(f"UPDATE visualization_revision SET dbkey='{dbkey}' WHERE id={viz_rev_id}") - migrate_engine.execute(f"UPDATE visualization SET dbkey='{dbkey}' WHERE id={viz_id}") - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - drop_column("dbkey", "visualization", metadata) - drop_column("dbkey", "visualization_revision", metadata) diff --git a/lib/galaxy/model/migrate/versions/0055_add_pja_assoc_for_jobs.py b/lib/galaxy/model/migrate/versions/0055_add_pja_assoc_for_jobs.py deleted file mode 100644 index ae05437922d..00000000000 --- a/lib/galaxy/model/migrate/versions/0055_add_pja_assoc_for_jobs.py +++ /dev/null @@ -1,44 +0,0 @@ -""" -Migration script to add the post_job_action_association table. -""" - -import logging - -from sqlalchemy import ( - Column, - ForeignKey, - Integer, - MetaData, - Table, -) - -from galaxy.model.migrate.versions.util import ( - create_table, - drop_table, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - -PostJobActionAssociation_table = Table( - "post_job_action_association", - metadata, - Column("id", Integer, primary_key=True), - Column("post_job_action_id", Integer, ForeignKey("post_job_action.id"), index=True, nullable=False), - Column("job_id", Integer, ForeignKey("job.id"), index=True, nullable=False), -) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - create_table(PostJobActionAssociation_table) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - drop_table(PostJobActionAssociation_table) diff --git a/lib/galaxy/model/migrate/versions/0056_workflow_outputs.py b/lib/galaxy/model/migrate/versions/0056_workflow_outputs.py deleted file mode 100644 index 08de99ff700..00000000000 --- a/lib/galaxy/model/migrate/versions/0056_workflow_outputs.py +++ /dev/null @@ -1,43 +0,0 @@ -""" -Migration script to create tables for adding explicit workflow outputs. -""" - -import logging - -from sqlalchemy import ( - Column, - ForeignKey, - Integer, - MetaData, - String, - Table, -) - -from galaxy.model.migrate.versions.util import ( - create_table, - drop_table, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - -WorkflowOutput_table = Table( - "workflow_output", - metadata, - Column("id", Integer, primary_key=True), - Column("workflow_step_id", Integer, ForeignKey("workflow_step.id"), index=True, nullable=False), - Column("output_name", String(255), nullable=True), -) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - create_table(WorkflowOutput_table) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - drop_table(WorkflowOutput_table) diff --git a/lib/galaxy/model/migrate/versions/0057_request_notify.py b/lib/galaxy/model/migrate/versions/0057_request_notify.py deleted file mode 100644 index 3a134efc7fd..00000000000 --- a/lib/galaxy/model/migrate/versions/0057_request_notify.py +++ /dev/null @@ -1,59 +0,0 @@ -""" -Migration script to modify the 'notify' field in the 'request' table from a boolean -to a JSONType -""" - -import logging -from json import dumps - -from sqlalchemy import ( - Boolean, - Column, - MetaData, - Table, -) - -from galaxy.model.custom_types import JSONType -from galaxy.model.migrate.versions.util import ( - add_column, - drop_column, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - Request_table = Table("request", metadata, autoload=True) - - # create the column again as JSONType - col = Column("notification", JSONType) - add_column(col, Request_table, metadata) - - cmd = "SELECT id, user_id, notify FROM request" - result = migrate_engine.execute(cmd) - for r in result: - id = int(r[0]) - notify_new = dict(email=[], sample_states=[], body="", subject="") - cmd = "UPDATE request SET notification='%s' WHERE id=%i" % (dumps(notify_new), id) - migrate_engine.execute(cmd) - - # SQLAlchemy Migrate has a bug when dropping a boolean column in SQLite - if migrate_engine.name != "sqlite": - drop_column("notify", Request_table) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - Request_table = Table("request", metadata, autoload=True) - if migrate_engine.name != "sqlite": - c = Column("notify", Boolean, default=False) - add_column(c, Request_table, metadata) - - drop_column("notification", Request_table) diff --git a/lib/galaxy/model/migrate/versions/0058_history_import_export.py b/lib/galaxy/model/migrate/versions/0058_history_import_export.py deleted file mode 100644 index 669111d2589..00000000000 --- a/lib/galaxy/model/migrate/versions/0058_history_import_export.py +++ /dev/null @@ -1,56 +0,0 @@ -""" -Migration script to create table for exporting histories to archives. -""" - -import logging - -from sqlalchemy import ( - Boolean, - Column, - ForeignKey, - Integer, - MetaData, - Table, - TEXT, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - -# Table to add. - -JobExportHistoryArchive_table = Table( - "job_export_history_archive", - metadata, - Column("id", Integer, primary_key=True), - Column("job_id", Integer, ForeignKey("job.id"), index=True), - Column("history_id", Integer, ForeignKey("history.id"), index=True), - Column("dataset_id", Integer, ForeignKey("dataset.id"), index=True), - Column("compressed", Boolean, index=True, default=False), - Column("history_attrs_filename", TEXT), - Column("datasets_attrs_filename", TEXT), - Column("jobs_attrs_filename", TEXT), -) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - # Create job_export_history_archive table. - try: - JobExportHistoryArchive_table.create() - except Exception: - log.exception("Creating job_export_history_archive table failed.") - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - # Drop job_export_history_archive table. - try: - JobExportHistoryArchive_table.drop() - except Exception: - log.exception("Dropping job_export_history_archive table failed.") diff --git a/lib/galaxy/model/migrate/versions/0059_sample_dataset_file_path.py b/lib/galaxy/model/migrate/versions/0059_sample_dataset_file_path.py deleted file mode 100644 index f112668990d..00000000000 --- a/lib/galaxy/model/migrate/versions/0059_sample_dataset_file_path.py +++ /dev/null @@ -1,56 +0,0 @@ -""" -Migration script to modify the 'file_path' field type in 'sample_dataset' table -to 'TEXT' so that it can support large file paths exceeding 255 characters -""" - -import logging - -from sqlalchemy import ( - Column, - MetaData, - Table, - TEXT, -) -from sqlalchemy.exc import NoSuchTableError - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - metadata.bind = migrate_engine - print(__doc__) - metadata.reflect() - try: - SampleDataset_table = Table("sample_dataset", metadata, autoload=True) - except NoSuchTableError: - SampleDataset_table = None - log.debug("Failed loading table 'sample_dataset'") - - if SampleDataset_table is not None: - cmd = "SELECT id, file_path FROM sample_dataset" - result = migrate_engine.execute(cmd) - filepath_dict = {} - for r in result: - id = int(r[0]) - filepath_dict[id] = r[1] - # remove the 'file_path' column - try: - SampleDataset_table.c.file_path.drop() - except Exception: - log.exception("Deleting column 'file_path' from the 'sample_dataset' table failed.") - # create the column again - try: - col = Column("file_path", TEXT) - col.create(SampleDataset_table) - assert col is SampleDataset_table.c.file_path - except Exception: - log.exception("Creating column 'file_path' in the 'sample_dataset' table failed.") - - for id, file_path in filepath_dict.items(): - cmd = "update sample_dataset set file_path='%s' where id=%i" % (file_path, id) - migrate_engine.execute(cmd) - - -def downgrade(migrate_engine): - pass diff --git a/lib/galaxy/model/migrate/versions/0060_history_archive_import.py b/lib/galaxy/model/migrate/versions/0060_history_archive_import.py deleted file mode 100644 index 11bb16030e8..00000000000 --- a/lib/galaxy/model/migrate/versions/0060_history_archive_import.py +++ /dev/null @@ -1,79 +0,0 @@ -""" -Migration script to create column and table for importing histories from -file archives. -""" - -import logging - -from sqlalchemy import ( - Boolean, - Column, - ForeignKey, - Integer, - MetaData, - Table, - TEXT, -) - -from galaxy.model.migrate.versions.util import engine_false - -log = logging.getLogger(__name__) -metadata = MetaData() - -# Columns to add. - -importing_col = Column("importing", Boolean, index=True, default=False) -ldda_parent_col = Column("ldda_parent_id", Integer, ForeignKey("library_dataset_dataset_association.id"), index=True) - -# Table to add. - -JobImportHistoryArchive_table = Table( - "job_import_history_archive", - metadata, - Column("id", Integer, primary_key=True), - Column("job_id", Integer, ForeignKey("job.id"), index=True), - Column("history_id", Integer, ForeignKey("history.id"), index=True), - Column("archive_dir", TEXT), -) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - # Add column to history table and initialize. - try: - History_table = Table("history", metadata, autoload=True) - importing_col.create(History_table, index_name="ix_history_importing") - assert importing_col is History_table.c.importing - - # Initialize column to false. - migrate_engine.execute(f"UPDATE history SET importing={engine_false(migrate_engine)}") - except Exception: - log.exception("Adding column 'importing' to history table failed.") - - # Create job_import_history_archive table. - try: - JobImportHistoryArchive_table.create() - except Exception: - log.exception("Creating job_import_history_archive table failed.") - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - # Drop 'importing' column from history table. - try: - History_table = Table("history", metadata, autoload=True) - importing_col = History_table.c.importing - importing_col.drop() - except Exception: - log.exception("Dropping column 'importing' from history table failed.") - - # Drop job_import_history_archive table. - try: - JobImportHistoryArchive_table.drop() - except Exception: - log.exception("Dropping job_import_history_archive table failed.") diff --git a/lib/galaxy/model/migrate/versions/0061_tasks.py b/lib/galaxy/model/migrate/versions/0061_tasks.py deleted file mode 100644 index c4523f223fe..00000000000 --- a/lib/galaxy/model/migrate/versions/0061_tasks.py +++ /dev/null @@ -1,61 +0,0 @@ -""" -Migration script to create tables task management. -""" - -import datetime -import logging - -from sqlalchemy import ( - Column, - DateTime, - ForeignKey, - Integer, - MetaData, - String, - Table, - TEXT, -) - -from galaxy.model.migrate.versions.util import ( - create_table, - drop_table, -) - -log = logging.getLogger(__name__) -now = datetime.datetime.utcnow -metadata = MetaData() - -Task_table = Table( - "task", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("execution_time", DateTime), - Column("update_time", DateTime, default=now, onupdate=now), - Column("state", String(64), index=True), - Column("command_line", TEXT), - Column("param_filename", String(1024)), - Column("runner_name", String(255)), - Column("stdout", TEXT), - Column("stderr", TEXT), - Column("traceback", TEXT), - Column("job_id", Integer, ForeignKey("job.id"), index=True, nullable=False), - Column("part_file", String(1024)), - Column("task_runner_name", String(255)), - Column("task_runner_external_id", String(255)), -) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - create_table(Task_table) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - drop_table(Task_table) diff --git a/lib/galaxy/model/migrate/versions/0062_user_openid_table.py b/lib/galaxy/model/migrate/versions/0062_user_openid_table.py deleted file mode 100644 index 130ffd57661..00000000000 --- a/lib/galaxy/model/migrate/versions/0062_user_openid_table.py +++ /dev/null @@ -1,69 +0,0 @@ -""" -Migration script to create table for associating sessions and users with -OpenIDs. -""" - -import datetime -import logging - -from sqlalchemy import ( - Column, - DateTime, - ForeignKey, - Index, - Integer, - MetaData, - Table, - TEXT, -) - -now = datetime.datetime.utcnow -log = logging.getLogger(__name__) -metadata = MetaData() - -# Table to add - -UserOpenID_table = Table( - "galaxy_user_openid", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, index=True, default=now, onupdate=now), - Column("session_id", Integer, ForeignKey("galaxy_session.id"), index=True), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True), - Column("openid", TEXT), -) - - -def upgrade(migrate_engine): - metadata.bind = migrate_engine - print(__doc__) - metadata.reflect() - - # Create galaxy_user_openid table - try: - UserOpenID_table.create() - except Exception: - log.exception("Creating galaxy_user_openid table failed.") - - ix_name = "ix_galaxy_user_openid_openid" - if migrate_engine.name == "mysql": - i = "ALTER TABLE galaxy_user_openid ADD UNIQUE INDEX ( openid( 255 ) )" - migrate_engine.execute(i) - else: - i = Index(ix_name, UserOpenID_table.c.openid, unique=True) - try: - i.create() - except Exception: - log.exception("Adding index '%s' failed.", ix_name) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - # Drop galaxy_user_openid table - try: - UserOpenID_table.drop() - except Exception: - log.exception("Dropping galaxy_user_openid table failed.") diff --git a/lib/galaxy/model/migrate/versions/0063_sequencer_table.py b/lib/galaxy/model/migrate/versions/0063_sequencer_table.py deleted file mode 100644 index 0cc2489a2e2..00000000000 --- a/lib/galaxy/model/migrate/versions/0063_sequencer_table.py +++ /dev/null @@ -1,61 +0,0 @@ -""" -Migration script to create a new 'sequencer' table -""" - -import datetime -import logging - -from sqlalchemy import ( - Boolean, - Column, - DateTime, - ForeignKey, - Integer, - MetaData, - Table, - TEXT, -) - -from galaxy.model.custom_types import TrimmedString - -now = datetime.datetime.utcnow -log = logging.getLogger(__name__) -metadata = MetaData() - -# Table to add -Sequencer_table = Table( - "sequencer", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("name", TrimmedString(255), nullable=False), - Column("description", TEXT), - Column("sequencer_type_id", TrimmedString(255), nullable=False), - Column("version", TrimmedString(255)), - Column("form_definition_id", Integer, ForeignKey("form_definition.id"), index=True), - Column("form_values_id", Integer, ForeignKey("form_values.id"), index=True), - Column("deleted", Boolean, index=True, default=False), -) - - -def upgrade(migrate_engine): - metadata.bind = migrate_engine - print(__doc__) - metadata.reflect() - # create the sequencer table - try: - Sequencer_table.create() - except Exception: - log.exception("Creating 'sequencer' table failed.") - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - # delete sequencer table - try: - Sequencer_table = Table("sequencer", metadata, autoload=True) - Sequencer_table.drop() - except Exception: - log.exception("Deleting 'sequencer' table failed.") diff --git a/lib/galaxy/model/migrate/versions/0064_add_run_and_sample_run_association_tables.py b/lib/galaxy/model/migrate/versions/0064_add_run_and_sample_run_association_tables.py deleted file mode 100644 index e94c790ef64..00000000000 --- a/lib/galaxy/model/migrate/versions/0064_add_run_and_sample_run_association_tables.py +++ /dev/null @@ -1,71 +0,0 @@ -""" -Migration script to add the run and sample_run_association tables. -""" - -import datetime -import logging - -from sqlalchemy import ( - Boolean, - Column, - DateTime, - ForeignKey, - Integer, - MetaData, - Table, -) - -from galaxy.model.migrate.versions.util import ( - create_table, - drop_table, -) - -now = datetime.datetime.utcnow -log = logging.getLogger(__name__) -metadata = MetaData() - -Run_table = Table( - "run", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("form_definition_id", Integer, ForeignKey("form_definition.id"), index=True), - Column("form_values_id", Integer, ForeignKey("form_values.id"), index=True), - Column("deleted", Boolean, index=True, default=False), -) - -RequestTypeRunAssociation_table = Table( - "request_type_run_association", - metadata, - Column("id", Integer, primary_key=True), - Column("request_type_id", Integer, ForeignKey("request_type.id"), index=True, nullable=False), - Column("run_id", Integer, ForeignKey("run.id"), index=True, nullable=False), -) - -SampleRunAssociation_table = Table( - "sample_run_association", - metadata, - Column("id", Integer, primary_key=True), - Column("sample_id", Integer, ForeignKey("sample.id"), index=True, nullable=False), - Column("run_id", Integer, ForeignKey("run.id"), index=True, nullable=False), -) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - create_table(Run_table) - create_table(RequestTypeRunAssociation_table) - create_table(SampleRunAssociation_table) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - drop_table(SampleRunAssociation_table) - drop_table(RequestTypeRunAssociation_table) - drop_table(Run_table) diff --git a/lib/galaxy/model/migrate/versions/0065_add_name_to_form_fields_and_values.py b/lib/galaxy/model/migrate/versions/0065_add_name_to_form_fields_and_values.py deleted file mode 100644 index 7ff1d99aacc..00000000000 --- a/lib/galaxy/model/migrate/versions/0065_add_name_to_form_fields_and_values.py +++ /dev/null @@ -1,136 +0,0 @@ -""" -Migration script to add 'name' attribute to the JSON dict which describes -a form definition field and the form values in the database. In the 'form_values' -table, the 'content' column is now a JSON dict instead of a list. -""" - -import logging -from json import ( - dumps, - loads, -) - -from sqlalchemy import ( - MetaData, - Table, -) - -from galaxy.model.custom_types import _sniffnfix_pg9_hex - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - Table("form_definition", metadata, autoload=True) - Table("form_values", metadata, autoload=True) - - def get_value(lst, index): - try: - return str(lst[index]).replace("'", "''") - except IndexError: - return "" - - # Go through the entire table and add a 'name' attribute for each field - # in the list of fields for each form definition - cmd = "SELECT f.id, f.fields FROM form_definition AS f" - result = migrate_engine.execute(cmd) - for row in result: - form_definition_id = row[0] - fields = str(row[1]) - if not fields.strip(): - continue - fields_list = loads(_sniffnfix_pg9_hex(fields)) - if len(fields_list): - for index, field in enumerate(fields_list): - field["name"] = "field_%i" % index - field["helptext"] = field["helptext"].replace("'", "''").replace('"', "") - field["label"] = field["label"].replace("'", "''") - fields_json = dumps(fields_list) - if migrate_engine.name == "mysql": - cmd = "UPDATE form_definition AS f SET f.fields='%s' WHERE f.id=%i" % (fields_json, form_definition_id) - else: - cmd = "UPDATE form_definition SET fields='%s' WHERE id=%i" % (fields_json, form_definition_id) - migrate_engine.execute(cmd) - # replace the values list in the content field of the form_values table with a name:value dict - cmd = ( - "SELECT form_values.id, form_values.content, form_definition.fields" - " FROM form_values, form_definition" - " WHERE form_values.form_definition_id=form_definition.id" - " ORDER BY form_values.id ASC" - ) - result = migrate_engine.execute(cmd) - for row in result: - form_values_id = int(row[0]) - if not str(row[1]).strip(): - continue - row1 = str(row[1]).replace("\n", "").replace("\r", "") - values_list = loads(str(row1).strip()) - if not str(row[2]).strip(): - continue - fields_list = loads(str(row[2]).strip()) - if fields_list and isinstance(values_list, list): - values_dict = {} - for field_index, field in enumerate(fields_list): - field_name = field["name"] - values_dict[field_name] = get_value(values_list, field_index) - cmd = "UPDATE form_values SET content='%s' WHERE id=%i" % (dumps(values_dict), form_values_id) - migrate_engine.execute(cmd) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - Table("form_definition", metadata, autoload=True) - Table("form_values", metadata, autoload=True) - # remove the name attribute in the content column JSON dict in the form_values table - # and restore it to a list of values - cmd = ( - "SELECT form_values.id, form_values.content, form_definition.fields" - " FROM form_values, form_definition" - " WHERE form_values.form_definition_id=form_definition.id" - " ORDER BY form_values.id ASC" - ) - result = migrate_engine.execute(cmd) - for row in result: - form_values_id = int(row[0]) - if not str(row[1]).strip(): - continue - values_dict = loads(str(row[1])) - if not str(row[2]).strip(): - continue - fields_list = loads(str(row[2])) - if fields_list: - values_list = [] - for field in fields_list: - field_name = field["name"] - field_value = values_dict[field_name] - values_list.append(field_value) - cmd = "UPDATE form_values SET content='%s' WHERE id=%i" % (dumps(values_list), form_values_id) - migrate_engine.execute(cmd) - # remove name attribute from the field column of the form_definition table - cmd = "SELECT f.id, f.fields FROM form_definition AS f" - result = migrate_engine.execute(cmd) - for row in result: - form_definition_id = row[0] - fields = str(row[1]) - if not fields.strip(): - continue - fields_list = loads(_sniffnfix_pg9_hex(fields)) - if len(fields_list): - for field in fields_list: - if "name" in field: - del field["name"] - if migrate_engine.name == "mysql": - cmd = "UPDATE form_definition AS f SET f.fields='%s' WHERE f.id=%i" % ( - dumps(fields_list), - form_definition_id, - ) - else: - cmd = "UPDATE form_definition SET fields='%s' WHERE id=%i" % (dumps(fields_list), form_definition_id) - migrate_engine.execute(cmd) diff --git a/lib/galaxy/model/migrate/versions/0066_deferred_job_and_transfer_job_tables.py b/lib/galaxy/model/migrate/versions/0066_deferred_job_and_transfer_job_tables.py deleted file mode 100644 index 4e1ec88f454..00000000000 --- a/lib/galaxy/model/migrate/versions/0066_deferred_job_and_transfer_job_tables.py +++ /dev/null @@ -1,81 +0,0 @@ -""" -Migration script to create table for storing deferred job and managed transfer -information. -""" - -import datetime -import logging - -from sqlalchemy import ( - Column, - DateTime, - Integer, - MetaData, - String, - Table, -) - -from galaxy.model.custom_types import JSONType - -now = datetime.datetime.utcnow -log = logging.getLogger(__name__) -metadata = MetaData() - -# Table to add - -DeferredJob_table = Table( - "deferred_job", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("state", String(64), index=True), - Column("plugin", String(128), index=True), - Column("params", JSONType), -) - -TransferJob_table = Table( - "transfer_job", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("state", String(64), index=True), - Column("path", String(1024)), - Column("params", JSONType), -) - - -def upgrade(migrate_engine): - metadata.bind = migrate_engine - print(__doc__) - metadata.reflect() - - # Create deferred_job table - try: - DeferredJob_table.create() - except Exception: - log.exception("Creating deferred_job table failed.") - - # Create transfer_job table - try: - TransferJob_table.create() - except Exception: - log.exception("Creating transfer_job table failed.") - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - # Drop deferred_job table - try: - DeferredJob_table.drop() - except Exception: - log.exception("Dropping deferred_job table failed.") - - # Drop transfer_job table - try: - TransferJob_table.drop() - except Exception: - log.exception("Dropping transfer_job table failed.") diff --git a/lib/galaxy/model/migrate/versions/0067_populate_sequencer_table.py b/lib/galaxy/model/migrate/versions/0067_populate_sequencer_table.py deleted file mode 100644 index 4ed8cfa7c30..00000000000 --- a/lib/galaxy/model/migrate/versions/0067_populate_sequencer_table.py +++ /dev/null @@ -1,284 +0,0 @@ -""" -Migration script to populate the 'sequencer' table and it is populated using unique -entries in the 'datatx_info' column in the 'request_type' table. It also deletes the 'datatx_info' -column in the 'request_type' table and adds a foreign key to the 'sequencer' table. The -actual contents of the datatx_info column are stored as form_values. -""" - -import logging -from json import ( - dumps, - loads, -) - -from sqlalchemy import ( - Column, - ForeignKey, - Integer, - MetaData, - Table, -) - -from galaxy.model.custom_types import JSONType -from galaxy.model.migrate.versions.util import ( - add_column, - drop_column, - engine_false, - localtimestamp, - nextval, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def get_latest_id(migrate_engine, table): - result = migrate_engine.execute(f"select id from {table} order by id desc") - row = result.fetchone() - if row: - return row[0] - else: - raise Exception(f"Unable to get the latest id in the {table} table.") - - -def create_sequencer_form_definition(migrate_engine): - """ - Create a new form_definition containing 5 fields (host, username, password, - data_dir & rename_datasets) which described the existing datatx_info json - dict in the request_type table - """ - # create new form_definition_current in the db - cmd = "INSERT INTO form_definition_current VALUES ( {}, {}, {}, {}, {} )".format( - nextval(migrate_engine, "form_definition_current"), - localtimestamp(migrate_engine), - localtimestamp(migrate_engine), - "NULL", - engine_false(migrate_engine), - ) - migrate_engine.execute(cmd) - # get this form_definition_current id - form_definition_current_id = get_latest_id(migrate_engine, "form_definition_current") - # create new form_definition in the db - form_definition_name = "Generic sequencer form" - form_definition_desc = "" - form_definition_fields = [] - fields = [ - ("Host", "TextField"), - ("User name", "TextField"), - ("Password", "PasswordField"), - ("Data directory", "TextField"), - ] - for index, (label, field_type) in enumerate(fields): - form_definition_fields.append( - { - "name": "field_%i" % index, - "label": label, - "helptext": "", - "visible": True, - "required": False, - "type": field_type, - "selectlist": [], - "layout": "none", - "default": "", - } - ) - form_definition_fields.append( - { - "name": "field_%i" % len(fields), - "label": "Prepend the experiment name and sample name to the dataset name?", - "helptext": "Galaxy datasets are renamed by prepending the experiment name and sample name to the dataset name, ensuring dataset names remain unique in Galaxy even when multiple datasets have the same name on the sequencer.", - "visible": True, - "required": False, - "type": "SelectField", - "selectlist": [ - "Do not rename", - "Preprend sample name", - "Prepend experiment name", - "Prepend experiment and sample name", - ], - "layout": "none", - "default": "", - } - ) - form_definition_type = "Sequencer Information Form" - form_definition_layout = dumps("[]") - cmd = "INSERT INTO form_definition VALUES ( %s, %s, %s, '%s', '%s', %s, '%s', '%s', '%s' )" - cmd = cmd % ( - nextval(migrate_engine, "form_definition"), - localtimestamp(migrate_engine), - localtimestamp(migrate_engine), - form_definition_name, - form_definition_desc, - form_definition_current_id, - dumps(form_definition_fields), - form_definition_type, - form_definition_layout, - ) - migrate_engine.execute(cmd) - # get this form_definition id - form_definition_id = get_latest_id(migrate_engine, "form_definition") - # update the form_definition_id column in form_definition_current - cmd = "UPDATE form_definition_current SET latest_form_id=%i WHERE id=%i" % ( - form_definition_id, - form_definition_current_id, - ) - migrate_engine.execute(cmd) - return form_definition_id - - -def get_sequencer_id(migrate_engine, sequencer_info): - """Get the sequencer id corresponding to the sequencer information""" - # Check if there is any existing sequencer which have the same sequencer - # information fields & values - cmd = "SELECT sequencer.id, form_values.content FROM sequencer, form_values WHERE sequencer.form_values_id=form_values.id" - result = migrate_engine.execute(cmd) - for row in result: - sequencer_id = row[0] - values = str(row[1]) - if not values.strip(): - continue - values = loads(values) - # proceed only if sequencer_info is a valid list - if values and isinstance(values, dict): - if ( - sequencer_info.get("host", "") == values.get("field_0", "") - and sequencer_info.get("username", "") == values.get("field_1", "") - and sequencer_info.get("password", "") == values.get("field_2", "") - and sequencer_info.get("data_dir", "") == values.get("field_3", "") - and sequencer_info.get("rename_dataset", "") == values.get("field_4", "") - ): - return sequencer_id - return None - - -def add_sequencer(migrate_engine, sequencer_index, sequencer_form_definition_id, sequencer_info): - """Adds a new sequencer to the sequencer table along with its form values.""" - # Create a new form values record with the supplied sequencer information - values = dumps( - { - "field_0": sequencer_info.get("host", ""), - "field_1": sequencer_info.get("username", ""), - "field_2": sequencer_info.get("password", ""), - "field_3": sequencer_info.get("data_dir", ""), - "field_4": sequencer_info.get("rename_dataset", ""), - } - ) - cmd = "INSERT INTO form_values VALUES ( {}, {}, {}, {}, '{}' )".format( - nextval(migrate_engine, "form_values"), - localtimestamp(migrate_engine), - localtimestamp(migrate_engine), - sequencer_form_definition_id, - values, - ) - migrate_engine.execute(cmd) - sequencer_form_values_id = get_latest_id(migrate_engine, "form_values") - # Create a new sequencer record with reference to the form value created above. - name = "Sequencer_%i" % sequencer_index - desc = "" - version = "" - sequencer_type_id = "simple_unknown_sequencer" - cmd = "INSERT INTO sequencer VALUES ( {}, {}, {}, '{}', '{}', '{}', '{}', {}, {}, {} )".format( - nextval(migrate_engine, "sequencer"), - localtimestamp(migrate_engine), - localtimestamp(migrate_engine), - name, - desc, - sequencer_type_id, - version, - sequencer_form_definition_id, - sequencer_form_values_id, - engine_false(migrate_engine), - ) - migrate_engine.execute(cmd) - return get_latest_id(migrate_engine, "sequencer") - - -def update_sequencer_id_in_request_type(migrate_engine, request_type_id, sequencer_id): - """Update the foreign key to the sequencer table in the request_type table""" - cmd = "UPDATE request_type SET sequencer_id=%i WHERE id=%i" % (sequencer_id, request_type_id) - migrate_engine.execute(cmd) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - RequestType_table = Table("request_type", metadata, autoload=True) - # create foreign key field to the sequencer table in the request_type table - col = Column("sequencer_id", Integer, ForeignKey("sequencer.id"), nullable=True) - add_column(col, RequestType_table, metadata) - # copy the sequencer information contained in the 'datatx_info' column - # of the request_type table to the form values referenced in the sequencer table - cmd = "SELECT id, name, datatx_info FROM request_type ORDER BY id ASC" - result = migrate_engine.execute(cmd) - results_list = result.fetchall() - # Proceed only if request_types exists - if len(results_list): - # In this migration script the all the contents of the datatx_info are stored as form_values - # with a pointer to the sequencer table. This way the sequencer information can be customized - # by the admin and is no longer restricted to host, username, password, data directory. - # For the existing request_types in the database, we add a new form_definition - # with these 4 fields. Then we populate the sequencer table with unique datatx_info - # column from the existing request_types. - sequencer_form_definition_id = create_sequencer_form_definition(migrate_engine) - sequencer_index = 1 - for row in results_list: - request_type_id = row[0] - sequencer_info = str(row[2]) # datatx_info column - # skip if sequencer_info is empty - if not sequencer_info.strip() or sequencer_info in ["None", "null"]: - continue - sequencer_info = loads(sequencer_info.strip()) - # proceed only if sequencer_info is a valid dict - if sequencer_info and isinstance(sequencer_info, dict): - # check if this sequencer has already been added to the sequencer table - sequencer_id = get_sequencer_id(migrate_engine, sequencer_info) - if not sequencer_id: - # add to the sequencer table - sequencer_id = add_sequencer( - migrate_engine, sequencer_index, sequencer_form_definition_id, sequencer_info - ) - # now update the sequencer_id column in request_type table - update_sequencer_id_in_request_type(migrate_engine, request_type_id, sequencer_id) - sequencer_index = sequencer_index + 1 - - # Finally delete the 'datatx_info' column from the request_type table - drop_column("datatx_info", RequestType_table) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - RequestType_table = Table("request_type", metadata, autoload=True) - # create the 'datatx_info' column - col = Column("datatx_info", JSONType) - add_column(col, RequestType_table, metadata) - # restore the datatx_info column data in the request_type table with data from - # the sequencer and the form_values table - cmd = ( - "SELECT request_type.id, form_values.content " - + " FROM request_type, sequencer, form_values " - + " WHERE request_type.sequencer_id=sequencer.id AND sequencer.form_values_id=form_values.id " - + " ORDER BY request_type.id ASC" - ) - result = migrate_engine.execute(cmd) - for row in result: - request_type_id = row[0] - seq_values = loads(str(row[1])) - # create the datatx_info json dict - datatx_info = dumps( - dict( - host=seq_values.get("field_0", ""), - username=seq_values.get("field_1", ""), - password=seq_values.get("field_2", ""), - data_dir=seq_values.get("field_3", ""), - rename_dataset=seq_values.get("field_4", ""), - ) - ) - # update the column - cmd = "UPDATE request_type SET datatx_info='%s' WHERE id=%i" % (datatx_info, request_type_id) - migrate_engine.execute(cmd) - # delete foreign key field to the sequencer table in the request_type table - drop_column("sequencer_id", RequestType_table) diff --git a/lib/galaxy/model/migrate/versions/0068_rename_sequencer_to_external_services.py b/lib/galaxy/model/migrate/versions/0068_rename_sequencer_to_external_services.py deleted file mode 100644 index cef6baa7ca3..00000000000 --- a/lib/galaxy/model/migrate/versions/0068_rename_sequencer_to_external_services.py +++ /dev/null @@ -1,168 +0,0 @@ -""" -This migration script renames the sequencer table to 'external_service' table and -creates a association table, 'request_type_external_service_association' and -populates it. The 'sequencer_id' foreign_key from the 'request_type' table is removed. -The 'sequencer_type_id' column is renamed to 'external_service_type_id' in the renamed -table 'external_service'. Finally, adds a foreign key to the external_service table in the -sample_dataset table and populates it. -""" - -import datetime -import logging - -from sqlalchemy import ( - Column, - ForeignKey, - Integer, - MetaData, - Table, -) - -from galaxy.model.custom_types import TrimmedString -from galaxy.model.migrate.versions.util import ( - add_column, - create_table, - drop_column, - drop_table, - nextval, -) - -now = datetime.datetime.utcnow -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - # rename 'sequencer' table to 'external_service' - Sequencer_table = Table("sequencer", metadata, autoload=True) - Sequencer_table.rename("external_service") - - # if running PostgreSQL, rename the primary key sequence too - if migrate_engine.name in ["postgres", "postgresql"]: - cmd = "ALTER SEQUENCE sequencer_id_seq RENAME TO external_service_id_seq" - migrate_engine.execute(cmd) - - # Add 'external_services_id' column to 'sample_dataset' table - SampleDataset_table = Table("sample_dataset", metadata, autoload=True) - col = Column( - "external_service_id", - Integer, - ForeignKey("external_service.id", name="sample_dataset_external_services_id_fk"), - index=True, - ) - add_column(col, SampleDataset_table, metadata, index_name="ix_sample_dataset_external_service_id") - - # populate the column - cmd = ( - "SELECT sample_dataset.id, request_type.sequencer_id " - + " FROM sample_dataset, sample, request, request_type " - + " WHERE sample.id=sample_dataset.sample_id and request.id=sample.request_id and request.request_type_id=request_type.id " - + " ORDER BY sample_dataset.id" - ) - try: - result = migrate_engine.execute(cmd) - for r in result: - sample_dataset_id = int(r[0]) - sequencer_id = int(r[1]) - cmd = "UPDATE sample_dataset SET external_service_id='%i' where id=%i" % (sequencer_id, sample_dataset_id) - migrate_engine.execute(cmd) - except Exception: - log.exception("Exception executing SQL command: %s", cmd) - - # rename 'sequencer_type_id' column to 'external_service_type_id' in the table 'external_service' - # create the column as 'external_service_type_id' - ExternalServices_table = Table("external_service", metadata, autoload=True) - col = Column("external_service_type_id", TrimmedString(255)) - add_column(col, ExternalServices_table, metadata) - - # populate this new column - cmd = "UPDATE external_service SET external_service_type_id=sequencer_type_id" - migrate_engine.execute(cmd) - - # remove the 'sequencer_type_id' column - drop_column("sequencer_type_id", ExternalServices_table) - - # create 'request_type_external_service_association' table - RequestTypeExternalServiceAssociation_table = Table( - "request_type_external_service_association", - metadata, - Column("id", Integer, primary_key=True), - Column("request_type_id", Integer, ForeignKey("request_type.id"), index=True), - Column("external_service_id", Integer, ForeignKey("external_service.id"), index=True), - ) - create_table(RequestTypeExternalServiceAssociation_table) - - # populate 'request_type_external_service_association' table - cmd = "SELECT id, sequencer_id FROM request_type ORDER BY id ASC" - result = migrate_engine.execute(cmd) - results_list = result.fetchall() - # Proceed only if request_types exists - for row in results_list: - request_type_id = row[0] - sequencer_id = row[1] - if not sequencer_id: - sequencer_id = "null" - cmd = "INSERT INTO request_type_external_service_association VALUES ( {}, {}, {} )".format( - nextval(migrate_engine, "request_type_external_service_association"), request_type_id, sequencer_id - ) - migrate_engine.execute(cmd) - - # TODO: Dropping a column used in a foreign key fails in MySQL, need to remove the FK first. - drop_column("sequencer_id", "request_type", metadata) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - # SQLite does not always update foreign key constraints when the target - # table is renamed, so we start with the table rename. - # rename the 'external_service' table to 'sequencer' - ExternalServices_table = Table("external_service", metadata, autoload=True) - ExternalServices_table.rename("sequencer") - - # if running PostgreSQL, rename the primary key sequence too - if migrate_engine.name in ["postgres", "postgresql"]: - cmd = "ALTER SEQUENCE external_service_id_seq RENAME TO sequencer_id_seq" - migrate_engine.execute(cmd) - - # create the 'sequencer_id' column in the 'request_type' table - col = Column("sequencer_id", Integer, ForeignKey("sequencer.id"), nullable=True) - add_column(col, "request_type", metadata) - - # populate 'sequencer_id' column in the 'request_type' table from the - # 'request_type_external_service_association' table - cmd = "SELECT request_type_id, external_service_id FROM request_type_external_service_association ORDER BY id ASC" - result = migrate_engine.execute(cmd) - results_list = result.fetchall() - for row in results_list: - request_type_id = row[0] - external_service_id = row[1] - cmd = "UPDATE request_type SET sequencer_id=%i WHERE id=%i" % (external_service_id, request_type_id) - migrate_engine.execute(cmd) - - # remove the 'request_type_external_service_association' table - RequestTypeExternalServiceAssociation_table = Table( - "request_type_external_service_association", metadata, autoload=True - ) - drop_table(RequestTypeExternalServiceAssociation_table) - - # rename 'external_service_type_id' column to 'sequencer_type_id' in the table 'sequencer' - # create the column 'sequencer_type_id' - Sequencer_table = Table("sequencer", metadata, autoload=True) - col = Column("sequencer_type_id", TrimmedString(255)) # should also have nullable=False - add_column(col, Sequencer_table, metadata) - - # populate this new column - cmd = "UPDATE sequencer SET sequencer_type_id=external_service_type_id" - migrate_engine.execute(cmd) - - # remove the 'external_service_type_id' column - drop_column("external_service_type_id", Sequencer_table) - - # drop the 'external_service_id' column in the 'sample_dataset' table - drop_column("external_service_id", "sample_dataset", metadata) diff --git a/lib/galaxy/model/migrate/versions/0069_rename_sequencer_form_type.py b/lib/galaxy/model/migrate/versions/0069_rename_sequencer_form_type.py deleted file mode 100644 index a005987cabc..00000000000 --- a/lib/galaxy/model/migrate/versions/0069_rename_sequencer_form_type.py +++ /dev/null @@ -1,29 +0,0 @@ -""" -Migration script to rename the sequencer information form type to external service information form -""" - -import logging - -from sqlalchemy import MetaData - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - metadata.bind = migrate_engine - print(__doc__) - metadata.reflect() - current_form_type = "Sequencer Information Form" - new_form_type = "External Service Information Form" - cmd = f"update form_definition set type='{new_form_type}' where type='{current_form_type}'" - migrate_engine.execute(cmd) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - new_form_type = "Sequencer Information Form" - current_form_type = "External Service Information Form" - cmd = f"update form_definition set type='{new_form_type}' where type='{current_form_type}'" - migrate_engine.execute(cmd) diff --git a/lib/galaxy/model/migrate/versions/0070_add_info_column_to_deferred_job_table.py b/lib/galaxy/model/migrate/versions/0070_add_info_column_to_deferred_job_table.py deleted file mode 100644 index 64826809bb5..00000000000 --- a/lib/galaxy/model/migrate/versions/0070_add_info_column_to_deferred_job_table.py +++ /dev/null @@ -1,38 +0,0 @@ -""" -Migration script to add 'info' column to the transfer_job table. -""" - -import logging - -from sqlalchemy import ( - Column, - MetaData, - Table, - TEXT, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - metadata.bind = migrate_engine - print(__doc__) - metadata.reflect() - try: - TransferJob_table = Table("transfer_job", metadata, autoload=True) - c = Column("info", TEXT) - c.create(TransferJob_table) - assert c is TransferJob_table.c.info - except Exception: - log.exception("Adding info column to transfer_job table failed.") - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - try: - TransferJob_table = Table("transfer_job", metadata, autoload=True) - TransferJob_table.c.info.drop() - except Exception: - log.exception("Dropping info column from transfer_job table failed.") diff --git a/lib/galaxy/model/migrate/versions/0071_add_history_and_workflow_to_sample.py b/lib/galaxy/model/migrate/versions/0071_add_history_and_workflow_to_sample.py deleted file mode 100644 index 97b11977d80..00000000000 --- a/lib/galaxy/model/migrate/versions/0071_add_history_and_workflow_to_sample.py +++ /dev/null @@ -1,44 +0,0 @@ -""" -Migration script to add 'workflow' and 'history' columns for a sample. -""" - -import logging - -from sqlalchemy import ( - Column, - ForeignKey, - Integer, - MetaData, - Table, -) - -from galaxy.model.custom_types import JSONType -from galaxy.model.migrate.versions.util import ( - add_column, - drop_column, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - Sample_table = Table("sample", metadata, autoload=True) - c1 = Column("workflow", JSONType, nullable=True) - add_column(c1, Sample_table, metadata) - - c2 = Column("history_id", Integer, ForeignKey("history.id"), nullable=True) - add_column(c2, Sample_table, metadata) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - Sample_table = Table("sample", metadata, autoload=True) - drop_column("workflow", Sample_table) - drop_column("history_id", Sample_table) diff --git a/lib/galaxy/model/migrate/versions/0072_add_pid_and_socket_columns_to_transfer_job_table.py b/lib/galaxy/model/migrate/versions/0072_add_pid_and_socket_columns_to_transfer_job_table.py deleted file mode 100644 index d7b5c7904f9..00000000000 --- a/lib/galaxy/model/migrate/versions/0072_add_pid_and_socket_columns_to_transfer_job_table.py +++ /dev/null @@ -1,42 +0,0 @@ -""" -Migration script to add 'pid' and 'socket' columns to the transfer_job table. -""" - -import logging - -from sqlalchemy import ( - Column, - Integer, - MetaData, - Table, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - metadata.bind = migrate_engine - print(__doc__) - metadata.reflect() - try: - TransferJob_table = Table("transfer_job", metadata, autoload=True) - c = Column("pid", Integer) - c.create(TransferJob_table) - assert c is TransferJob_table.c.pid - c = Column("socket", Integer) - c.create(TransferJob_table) - assert c is TransferJob_table.c.socket - except Exception: - log.exception("Adding columns to transfer_job table failed.") - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - try: - TransferJob_table = Table("transfer_job", metadata, autoload=True) - TransferJob_table.c.pid.drop() - TransferJob_table.c.socket.drop() - except Exception: - log.exception("Dropping columns from transfer_job table failed.") diff --git a/lib/galaxy/model/migrate/versions/0073_add_ldda_to_implicit_conversion_table.py b/lib/galaxy/model/migrate/versions/0073_add_ldda_to_implicit_conversion_table.py deleted file mode 100644 index ebc8f3f50c5..00000000000 --- a/lib/galaxy/model/migrate/versions/0073_add_ldda_to_implicit_conversion_table.py +++ /dev/null @@ -1,43 +0,0 @@ -""" -Migration script to add 'ldda_parent_id' column to the implicitly_converted_dataset_association table. -""" - -import logging - -from sqlalchemy import ( - Column, - ForeignKey, - Integer, - MetaData, -) - -from galaxy.model.migrate.versions.util import ( - add_column, - drop_column, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - c = Column( - "ldda_parent_id", Integer, ForeignKey("library_dataset_dataset_association.id"), index=True, nullable=True - ) - add_column( - c, - "implicitly_converted_dataset_association", - metadata, - index_name="ix_implicitly_converted_dataset_assoc_ldda_parent_id", - ) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - drop_column("ldda_parent_id", "implicitly_converted_dataset_association", metadata) diff --git a/lib/galaxy/model/migrate/versions/0074_add_purged_column_to_library_dataset_table.py b/lib/galaxy/model/migrate/versions/0074_add_purged_column_to_library_dataset_table.py deleted file mode 100644 index 9284f3a179f..00000000000 --- a/lib/galaxy/model/migrate/versions/0074_add_purged_column_to_library_dataset_table.py +++ /dev/null @@ -1,60 +0,0 @@ -""" -Migration script to add 'purged' column to the library_dataset table. -""" - -import logging - -from sqlalchemy import ( - Boolean, - Column, - MetaData, -) - -from galaxy.model.migrate.versions.util import ( - add_column, - drop_column, - engine_false, - engine_true, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - c = Column("purged", Boolean, index=True, default=False) - add_column(c, "library_dataset", metadata, index_name="ix_library_dataset_purged") - # Update the purged flag to the default False - cmd = f"UPDATE library_dataset SET purged = {engine_false(migrate_engine)};" - try: - migrate_engine.execute(cmd) - except Exception: - log.exception("Setting default data for library_dataset.purged column failed.") - - # Update the purged flag for those LibraryDatasets whose purged flag should be True. This happens - # when the LibraryDataset has no active LibraryDatasetDatasetAssociations. - cmd = f"SELECT * FROM library_dataset WHERE deleted = {engine_true(migrate_engine)};" - deleted_lds = migrate_engine.execute(cmd).fetchall() - for row in deleted_lds: - cmd = ( - "SELECT * FROM library_dataset_dataset_association WHERE library_dataset_id = %d AND library_dataset_dataset_association.deleted = %s;" - % (int(row.id), engine_false(migrate_engine)) - ) - active_lddas = migrate_engine.execute(cmd).fetchall() - if not active_lddas: - print("Updating purged column to True for LibraryDataset id : ", int(row.id)) - cmd = "UPDATE library_dataset SET purged = %s WHERE id = %d;" % (engine_true(migrate_engine), int(row.id)) - migrate_engine.execute(cmd) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - # SQLAlchemy Migrate has a bug when dropping a boolean column in SQLite - if migrate_engine.name != "sqlite": - drop_column("purged", "library_dataset", metadata) diff --git a/lib/galaxy/model/migrate/versions/0075_add_subindex_column_to_run_table.py b/lib/galaxy/model/migrate/versions/0075_add_subindex_column_to_run_table.py deleted file mode 100644 index 536682a913d..00000000000 --- a/lib/galaxy/model/migrate/versions/0075_add_subindex_column_to_run_table.py +++ /dev/null @@ -1,39 +0,0 @@ -""" -Migration script to add a 'subindex' column to the run table. -""" - -import logging - -from sqlalchemy import ( - Column, - MetaData, - Table, -) - -from galaxy.model.custom_types import TrimmedString - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - metadata.bind = migrate_engine - print(__doc__) - metadata.reflect() - try: - Run_table = Table("run", metadata, autoload=True) - c = Column("subindex", TrimmedString(255), index=True) - c.create(Run_table, index_name="ix_run_subindex") - assert c is Run_table.c.subindex - except Exception: - log.exception("Adding the subindex column to the run table failed.") - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - try: - Run_table = Table("run", metadata, autoload=True) - Run_table.c.subindex.drop() - except Exception: - log.exception("Dropping the subindex column from run table failed.") diff --git a/lib/galaxy/model/migrate/versions/0076_fix_form_values_data_corruption.py b/lib/galaxy/model/migrate/versions/0076_fix_form_values_data_corruption.py deleted file mode 100644 index f0d43923041..00000000000 --- a/lib/galaxy/model/migrate/versions/0076_fix_form_values_data_corruption.py +++ /dev/null @@ -1,98 +0,0 @@ -""" -This migration script fixes the data corruption caused in the form_values -table (content json field) by migrate script 65. -""" - -import logging -from json import ( - dumps, - loads, -) - -from sqlalchemy import MetaData - -from galaxy.model.custom_types import _sniffnfix_pg9_hex - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - metadata.bind = migrate_engine - print(__doc__) - metadata.reflect() - cmd = ( - "SELECT form_values.id as id, form_values.content as field_values, form_definition.fields as fdfields " - + " FROM form_definition, form_values " - + " WHERE form_values.form_definition_id=form_definition.id " - + " ORDER BY form_values.id" - ) - result = migrate_engine.execute(cmd) - corrupted_rows = 0 - for row in result: - # first check if loading the dict from the json succeeds - # if that fails, it means that the content field is corrupted. - try: - field_values_dict = loads(_sniffnfix_pg9_hex(str(row["field_values"]))) - except Exception: - corrupted_rows = corrupted_rows + 1 - # content field is corrupted - fields_list = loads(_sniffnfix_pg9_hex(str(row["fdfields"]))) - field_values_str = _sniffnfix_pg9_hex(str(row["field_values"])) - try: - # Encoding errors? Just to be safe. - print(f"Attempting to fix row {row['id']}") - print(f"Prior to replacement: {field_values_str}") - except Exception: - pass - field_values_dict = {} - # look for each field name in the values and extract its value (string) - for index in range(len(fields_list)): - field = fields_list[index] - field_name_key = f"\"{field['name']}\": \"" - field_index = field_values_str.find(field_name_key) - if field_index == -1: - # if the field name is not present the field values dict then - # inform the admin that this form values cannot be fixed - print( - "The 'content' field of row 'id' %i does not have the field '%s' in the 'form_values' table and could not be fixed by this migration script." - % (int(field["id"]), field["name"]) - ) - else: - # check if this is the last field - if index == len(fields_list) - 1: - # since this is the last field, the value string lies between the - # field name and the '"}' string at the end, hence len(field_values_str) - 2 - value = field_values_str[field_index + len(field_name_key) : len(field_values_str) - 2] - else: - # if this is not the last field then the value string lies between - # this field name and the next field name - next_field = fields_list[index + 1] - next_field_index = field_values_str.find(f"\", \"{next_field['name']}\": \"") - value = field_values_str[field_index + len(field_name_key) : next_field_index] - # clean up the value string, escape the required quoutes and newline characters - value = ( - value.replace("'", "''") - .replace('"', '\\\\"') - .replace("\r", "\\\\r") - .replace("\n", "\\\\n") - .replace("\t", "\\\\t") - ) - # add to the new values dict - field_values_dict[field["name"]] = value - # update the db - json_values = dumps(field_values_dict) - cmd = "UPDATE form_values SET content='%s' WHERE id=%i" % (json_values, int(row["id"])) - migrate_engine.execute(cmd) - try: - print(f"Post replacement: {json_values}") - except Exception: - pass - if corrupted_rows: - print("Fixed %i corrupted rows." % corrupted_rows) - else: - print("No corrupted rows found.") - - -def downgrade(migrate_engine): - pass diff --git a/lib/galaxy/model/migrate/versions/0077_create_tool_tag_association_table.py b/lib/galaxy/model/migrate/versions/0077_create_tool_tag_association_table.py deleted file mode 100644 index 58763437c8c..00000000000 --- a/lib/galaxy/model/migrate/versions/0077_create_tool_tag_association_table.py +++ /dev/null @@ -1,55 +0,0 @@ -""" -Migration script to create table for storing tool tag associations. -""" - -import logging - -from sqlalchemy import ( - Column, - ForeignKey, - Integer, - MetaData, - Table, -) - -from galaxy.model.custom_types import TrimmedString - -log = logging.getLogger(__name__) -metadata = MetaData() - -# Table to add - -ToolTagAssociation_table = Table( - "tool_tag_association", - metadata, - Column("id", Integer, primary_key=True), - Column("tool_id", TrimmedString(255), index=True), - Column("tag_id", Integer, ForeignKey("tag.id"), index=True), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True), - Column("user_tname", TrimmedString(255), index=True), - Column("value", TrimmedString(255), index=True), - Column("user_value", TrimmedString(255), index=True), -) - - -def upgrade(migrate_engine): - metadata.bind = migrate_engine - print(__doc__) - metadata.reflect() - - # Create tool_tag_association table - try: - ToolTagAssociation_table.create() - except Exception: - log.exception("Creating tool_tag_association table failed.") - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - # Drop tool_tag_association table - try: - ToolTagAssociation_table.drop() - except Exception: - log.exception("Dropping tool_tag_association table failed.") diff --git a/lib/galaxy/model/migrate/versions/0078_add_columns_for_disk_usage_accounting.py b/lib/galaxy/model/migrate/versions/0078_add_columns_for_disk_usage_accounting.py deleted file mode 100644 index cab4ea2bf6f..00000000000 --- a/lib/galaxy/model/migrate/versions/0078_add_columns_for_disk_usage_accounting.py +++ /dev/null @@ -1,60 +0,0 @@ -""" -Migration script to add 'total_size' column to the dataset table, 'purged' -column to the HDA table, and 'disk_usage' column to the User and GalaxySession -tables. -""" - -import logging - -from sqlalchemy import ( - Boolean, - Column, - MetaData, - Numeric, - Table, -) - -from galaxy.model.migrate.versions.util import ( - add_column, - drop_column, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - c = Column("total_size", Numeric(15, 0)) - add_column(c, "dataset", metadata) - - HistoryDatasetAssociation_table = Table("history_dataset_association", metadata, autoload=True) - c = Column("purged", Boolean, index=True, default=False) - add_column(c, HistoryDatasetAssociation_table, metadata, index_name="ix_history_dataset_association_purged") - try: - migrate_engine.execute(HistoryDatasetAssociation_table.update().values(purged=False)) - except Exception: - log.exception("Updating column 'purged' of table 'history_dataset_association' failed.") - - c = Column("disk_usage", Numeric(15, 0), index=True) - add_column(c, "galaxy_user", metadata, index_name="ix_galaxy_user_disk_usage") - - c = Column("disk_usage", Numeric(15, 0), index=True) - add_column(c, "galaxy_session", metadata, index_name="ix_galaxy_session_disk_usage") - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - drop_column("disk_usage", "galaxy_session", metadata) - drop_column("disk_usage", "galaxy_user", metadata) - - # SQLAlchemy Migrate has a bug when dropping a boolean column in SQLite - if migrate_engine.name != "sqlite": - drop_column("purged", "history_dataset_association", metadata) - - drop_column("total_size", "dataset", metadata) diff --git a/lib/galaxy/model/migrate/versions/0079_input_library_to_job_table.py b/lib/galaxy/model/migrate/versions/0079_input_library_to_job_table.py deleted file mode 100644 index 5ddfb97d613..00000000000 --- a/lib/galaxy/model/migrate/versions/0079_input_library_to_job_table.py +++ /dev/null @@ -1,50 +0,0 @@ -""" -Migration script to add the job_to_input_library_dataset table. -""" - -import logging - -from sqlalchemy import ( - Column, - ForeignKey, - Integer, - MetaData, - String, - Table, -) - -log = logging.getLogger(__name__) - -metadata = MetaData() - -JobToInputLibraryDatasetAssociation_table = Table( - "job_to_input_library_dataset", - metadata, - Column("id", Integer, primary_key=True), - Column("job_id", Integer, ForeignKey("job.id"), index=True), - Column("ldda_id", Integer, ForeignKey("library_dataset_dataset_association.id"), index=True), - Column("name", String(255)), -) - - -def upgrade(migrate_engine): - metadata.bind = migrate_engine - print(__doc__) - metadata.reflect() - - # Create the job_to_input_library_dataset table - try: - JobToInputLibraryDatasetAssociation_table.create() - except Exception: - log.exception("Creating job_to_input_library_dataset table failed.") - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - # Drop the job_to_input_library_dataset table - try: - JobToInputLibraryDatasetAssociation_table.drop() - except Exception: - log.exception("Dropping job_to_input_library_dataset table failed.") diff --git a/lib/galaxy/model/migrate/versions/0080_quota_tables.py b/lib/galaxy/model/migrate/versions/0080_quota_tables.py deleted file mode 100644 index 0973ea2ea0c..00000000000 --- a/lib/galaxy/model/migrate/versions/0080_quota_tables.py +++ /dev/null @@ -1,127 +0,0 @@ -""" -Migration script to create tables for disk quotas. -""" - -import datetime -import logging - -from sqlalchemy import ( - BigInteger, - Boolean, - Column, - DateTime, - ForeignKey, - Integer, - MetaData, - String, - Table, - TEXT, -) - -now = datetime.datetime.utcnow -log = logging.getLogger(__name__) -metadata = MetaData() - -# Tables to add - -Quota_table = Table( - "quota", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("name", String(255), index=True, unique=True), - Column("description", TEXT), - Column("bytes", BigInteger), - Column("operation", String(8)), - Column("deleted", Boolean, index=True, default=False), -) - -UserQuotaAssociation_table = Table( - "user_quota_association", - metadata, - Column("id", Integer, primary_key=True), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True), - Column("quota_id", Integer, ForeignKey("quota.id"), index=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), -) - -GroupQuotaAssociation_table = Table( - "group_quota_association", - metadata, - Column("id", Integer, primary_key=True), - Column("group_id", Integer, ForeignKey("galaxy_group.id"), index=True), - Column("quota_id", Integer, ForeignKey("quota.id"), index=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), -) - -DefaultQuotaAssociation_table = Table( - "default_quota_association", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("type", String(32), index=True, unique=True), - Column("quota_id", Integer, ForeignKey("quota.id"), index=True), -) - - -def upgrade(migrate_engine): - metadata.bind = migrate_engine - print(__doc__) - metadata.reflect() - - # Create quota table - try: - Quota_table.create() - except Exception: - log.exception("Creating quota table failed.") - - # Create user_quota_association table - try: - UserQuotaAssociation_table.create() - except Exception: - log.exception("Creating user_quota_association table failed.") - - # Create group_quota_association table - try: - GroupQuotaAssociation_table.create() - except Exception: - log.exception("Creating group_quota_association table failed.") - - # Create default_quota_association table - try: - DefaultQuotaAssociation_table.create() - except Exception: - log.exception("Creating default_quota_association table failed.") - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - # Drop default_quota_association table - try: - DefaultQuotaAssociation_table.drop() - except Exception: - log.exception("Dropping default_quota_association table failed.") - - # Drop group_quota_association table - try: - GroupQuotaAssociation_table.drop() - except Exception: - log.exception("Dropping group_quota_association table failed.") - - # Drop user_quota_association table - try: - UserQuotaAssociation_table.drop() - except Exception: - log.exception("Dropping user_quota_association table failed.") - - # Drop quota table - try: - Quota_table.drop() - except Exception: - log.exception("Dropping quota table failed.") diff --git a/lib/galaxy/model/migrate/versions/0081_add_tool_version_to_hda_ldda.py b/lib/galaxy/model/migrate/versions/0081_add_tool_version_to_hda_ldda.py deleted file mode 100644 index 040f7e62967..00000000000 --- a/lib/galaxy/model/migrate/versions/0081_add_tool_version_to_hda_ldda.py +++ /dev/null @@ -1,47 +0,0 @@ -""" -Migration script to add a 'tool_version' column to the hda/ldda tables. -""" - -import logging - -from sqlalchemy import ( - Column, - MetaData, - Table, - TEXT, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - metadata.bind = migrate_engine - print(__doc__) - metadata.reflect() - try: - hda_table = Table("history_dataset_association", metadata, autoload=True) - c = Column("tool_version", TEXT) - c.create(hda_table) - assert c is hda_table.c.tool_version - - ldda_table = Table("library_dataset_dataset_association", metadata, autoload=True) - c = Column("tool_version", TEXT) - c.create(ldda_table) - assert c is ldda_table.c.tool_version - - except Exception: - log.exception("Adding the tool_version column to the hda/ldda tables failed.") - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - try: - hda_table = Table("history_dataset_association", metadata, autoload=True) - hda_table.c.tool_version.drop() - - ldda_table = Table("library_dataset_dataset_association", metadata, autoload=True) - ldda_table.c.tool_version.drop() - except Exception: - log.exception("Dropping the tool_version column from hda/ldda table failed.") diff --git a/lib/galaxy/model/migrate/versions/0082_add_tool_shed_repository_table.py b/lib/galaxy/model/migrate/versions/0082_add_tool_shed_repository_table.py deleted file mode 100644 index 52acd68fd13..00000000000 --- a/lib/galaxy/model/migrate/versions/0082_add_tool_shed_repository_table.py +++ /dev/null @@ -1,56 +0,0 @@ -""" -Migration script to add the tool_shed_repository table. -""" - -import datetime -import logging - -from sqlalchemy import ( - Boolean, - Column, - DateTime, - Integer, - MetaData, - Table, - TEXT, -) - -from galaxy.model.custom_types import TrimmedString -from galaxy.model.migrate.versions.util import ( - create_table, - drop_table, -) - -log = logging.getLogger(__name__) -now = datetime.datetime.utcnow -metadata = MetaData() - -# New table to store information about cloned tool shed repositories. -ToolShedRepository_table = Table( - "tool_shed_repository", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("tool_shed", TrimmedString(255), index=True), - Column("name", TrimmedString(255), index=True), - Column("description", TEXT), - Column("owner", TrimmedString(255), index=True), - Column("changeset_revision", TrimmedString(255), index=True), - Column("deleted", Boolean, index=True, default=False), -) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - create_table(ToolShedRepository_table) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - drop_table(ToolShedRepository_table) diff --git a/lib/galaxy/model/migrate/versions/0083_add_prepare_files_to_task.py b/lib/galaxy/model/migrate/versions/0083_add_prepare_files_to_task.py deleted file mode 100644 index d37b17c6829..00000000000 --- a/lib/galaxy/model/migrate/versions/0083_add_prepare_files_to_task.py +++ /dev/null @@ -1,49 +0,0 @@ -""" -Migration script to add 'prepare_input_files_cmd' column to the task table and to rename a column. -""" - -import logging - -from sqlalchemy import ( - Column, - MetaData, - String, - Table, - TEXT, -) - -from galaxy.model.migrate.versions.util import ( - add_column, - drop_column, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - task_table = Table("task", metadata, autoload=True) - c = Column("prepare_input_files_cmd", TEXT, nullable=True) - add_column(c, task_table, metadata) - - c = Column("working_directory", String(1024), nullable=True) - add_column(c, task_table, metadata) - - # remove the 'part_file' column - nobody used tasks before this, so no data needs to be migrated - drop_column("part_file", task_table) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - task_table = Table("task", metadata, autoload=True) - c = Column("part_file", String(1024), nullable=True) - add_column(c, task_table, metadata) - - drop_column("working_directory", task_table) - drop_column("prepare_input_files_cmd", task_table) diff --git a/lib/galaxy/model/migrate/versions/0084_add_ldda_id_to_implicit_conversion_table.py b/lib/galaxy/model/migrate/versions/0084_add_ldda_id_to_implicit_conversion_table.py deleted file mode 100644 index 770e715c7bb..00000000000 --- a/lib/galaxy/model/migrate/versions/0084_add_ldda_id_to_implicit_conversion_table.py +++ /dev/null @@ -1,38 +0,0 @@ -""" -Migration script to add 'ldda_id' column to the implicitly_converted_dataset_association table. -""" - -import logging - -from sqlalchemy import ( - Column, - ForeignKey, - Integer, - MetaData, -) - -from galaxy.model.migrate.versions.util import ( - add_column, - drop_column, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - c = Column("ldda_id", Integer, ForeignKey("library_dataset_dataset_association.id"), index=True, nullable=True) - add_column( - c, "implicitly_converted_dataset_association", metadata, index_name="ix_implicitly_converted_ds_assoc_ldda_id" - ) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - drop_column("ldda_id", "implicitly_converted_dataset_association", metadata) diff --git a/lib/galaxy/model/migrate/versions/0085_add_task_info.py b/lib/galaxy/model/migrate/versions/0085_add_task_info.py deleted file mode 100644 index 289188cb6e4..00000000000 --- a/lib/galaxy/model/migrate/versions/0085_add_task_info.py +++ /dev/null @@ -1,39 +0,0 @@ -""" -Migration script to add 'info' column to the task table. -""" - -import logging - -from sqlalchemy import ( - Column, - MetaData, - Table, -) - -from galaxy.model.custom_types import TrimmedString - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - metadata.bind = migrate_engine - print(__doc__) - metadata.reflect() - try: - task_table = Table("task", metadata, autoload=True) - c = Column("info", TrimmedString(255), nullable=True) - c.create(task_table) - assert c is task_table.c.info - except Exception: - log.exception("Adding info column to task table failed.") - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - try: - task_table = Table("task", metadata, autoload=True) - task_table.c.info.drop() - except Exception: - log.exception("Dropping info column from task table failed.") diff --git a/lib/galaxy/model/migrate/versions/0086_add_tool_shed_repository_table_columns.py b/lib/galaxy/model/migrate/versions/0086_add_tool_shed_repository_table_columns.py deleted file mode 100644 index 168e21d9d42..00000000000 --- a/lib/galaxy/model/migrate/versions/0086_add_tool_shed_repository_table_columns.py +++ /dev/null @@ -1,56 +0,0 @@ -""" -Migration script to add the metadata, update_available and includes_datatypes columns to the tool_shed_repository table. -""" - -import logging - -from sqlalchemy import ( - Boolean, - Column, - MetaData, - Table, -) - -from galaxy.model.custom_types import JSONType -from galaxy.model.migrate.versions.util import ( - add_column, - drop_column, - engine_false, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - ToolShedRepository_table = Table("tool_shed_repository", metadata, autoload=True) - c = Column("metadata", JSONType, nullable=True) - add_column(c, ToolShedRepository_table, metadata) - c = Column("includes_datatypes", Boolean, index=True, default=False) - add_column(c, ToolShedRepository_table, metadata, index_name="ix_tool_shed_repository_includes_datatypes") - try: - migrate_engine.execute(f"UPDATE tool_shed_repository SET includes_datatypes={engine_false(migrate_engine)}") - except Exception: - log.exception("Updating column 'includes_datatypes' of table 'tool_shed_repository' failed.") - c = Column("update_available", Boolean, default=False) - add_column(c, ToolShedRepository_table, metadata) - try: - migrate_engine.execute(f"UPDATE tool_shed_repository SET update_available={engine_false(migrate_engine)}") - except Exception: - log.exception("Updating column 'update_available' of table 'tool_shed_repository' failed.") - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - ToolShedRepository_table = Table("tool_shed_repository", metadata, autoload=True) - drop_column("metadata", ToolShedRepository_table) - # SQLAlchemy Migrate has a bug when dropping a boolean column in SQLite - if migrate_engine.name != "sqlite": - drop_column("includes_datatypes", ToolShedRepository_table) - drop_column("update_available", ToolShedRepository_table) diff --git a/lib/galaxy/model/migrate/versions/0087_tool_id_guid_map_table.py b/lib/galaxy/model/migrate/versions/0087_tool_id_guid_map_table.py deleted file mode 100644 index 056faf991f4..00000000000 --- a/lib/galaxy/model/migrate/versions/0087_tool_id_guid_map_table.py +++ /dev/null @@ -1,57 +0,0 @@ -""" -Migration script to create the tool_id_guid_map table. -""" - -import datetime -import logging - -from sqlalchemy import ( - Column, - DateTime, - Index, - Integer, - MetaData, - String, - Table, - TEXT, -) - -from galaxy.model.custom_types import TrimmedString -from galaxy.model.migrate.versions.util import ( - create_table, - drop_table, -) - -log = logging.getLogger(__name__) -now = datetime.datetime.utcnow -metadata = MetaData() - -ToolIdGuidMap_table = Table( - "tool_id_guid_map", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("tool_id", String(255)), - Column("tool_version", TEXT), - Column("tool_shed", TrimmedString(255)), - Column("repository_owner", TrimmedString(255)), - Column("repository_name", TrimmedString(255)), - Column("guid", TEXT), - Index("ix_tool_id_guid_map_guid", "guid", unique=True, mysql_length=200), -) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - create_table(ToolIdGuidMap_table) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - drop_table(ToolIdGuidMap_table) diff --git a/lib/galaxy/model/migrate/versions/0088_add_installed_changeset_revison_column.py b/lib/galaxy/model/migrate/versions/0088_add_installed_changeset_revison_column.py deleted file mode 100644 index 499e08d1ab0..00000000000 --- a/lib/galaxy/model/migrate/versions/0088_add_installed_changeset_revison_column.py +++ /dev/null @@ -1,59 +0,0 @@ -""" -Migration script to add the installed_changeset_revision column to the tool_shed_repository table. -""" - -import logging - -from sqlalchemy import ( - Column, - MetaData, -) - -from galaxy.model.custom_types import TrimmedString -from galaxy.model.migrate.versions.util import ( - add_column, - drop_column, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - col = Column("installed_changeset_revision", TrimmedString(255)) - add_column(col, "tool_shed_repository", metadata) - # Update each row by setting the value of installed_changeset_revison to be the value of changeset_revision. - # This will be problematic if the value of changeset_revision was updated to something other than the value - # that it was when the repository was installed (because the install path determined in real time will attempt to - # find the repository using the updated changeset_revison instead of the required installed_changeset_revision), - # but at the time this script was written, this scenario is extremely unlikely. - cmd = ( - "SELECT id AS id, " - + "installed_changeset_revision AS installed_changeset_revision, " - + "changeset_revision AS changeset_revision " - + "FROM tool_shed_repository;" - ) - tool_shed_repositories = migrate_engine.execute(cmd).fetchall() - update_count = 0 - for row in tool_shed_repositories: - cmd = ( - "UPDATE tool_shed_repository " - + f"SET installed_changeset_revision = '{row.changeset_revision}' " - + f"WHERE changeset_revision = '{row.changeset_revision}';" - ) - migrate_engine.execute(cmd) - update_count += 1 - print( - "Updated the installed_changeset_revision column for ", update_count, " rows in the tool_shed_repository table." - ) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - drop_column("installed_changeset_revision", "tool_shed_repository", metadata) diff --git a/lib/galaxy/model/migrate/versions/0089_add_object_store_id_columns.py b/lib/galaxy/model/migrate/versions/0089_add_object_store_id_columns.py deleted file mode 100644 index 4b276f2215b..00000000000 --- a/lib/galaxy/model/migrate/versions/0089_add_object_store_id_columns.py +++ /dev/null @@ -1,43 +0,0 @@ -""" -Migration script to add 'object_store_id' column to various tables -""" - -import logging - -from sqlalchemy import ( - Column, - MetaData, - Table, -) - -from galaxy.model.custom_types import TrimmedString - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - for t_name in ("dataset", "job", "metadata_file"): - t = Table(t_name, metadata, autoload=True) - c = Column("object_store_id", TrimmedString(255), index=True) - try: - c.create(t, index_name=f"ix_{t_name}_object_store_id") - assert c is t.c.object_store_id - except Exception: - log.exception("Adding object_store_id column to %s table failed.", t_name) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - for t_name in ("dataset", "job", "metadata_file"): - t = Table(t_name, metadata, autoload=True) - try: - t.c.object_store_id.drop() - except Exception: - log.exception("Dropping object_store_id column from %s table failed.", t_name) diff --git a/lib/galaxy/model/migrate/versions/0090_add_tool_shed_repository_table_columns.py b/lib/galaxy/model/migrate/versions/0090_add_tool_shed_repository_table_columns.py deleted file mode 100644 index b50fcd2fa56..00000000000 --- a/lib/galaxy/model/migrate/versions/0090_add_tool_shed_repository_table_columns.py +++ /dev/null @@ -1,52 +0,0 @@ -""" -Migration script to add the uninstalled and dist_to_shed columns to the tool_shed_repository table. -""" - -import logging - -from sqlalchemy import ( - Boolean, - Column, - MetaData, - Table, -) - -from galaxy.model.migrate.versions.util import ( - add_column, - drop_column, - engine_false, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - ToolShedRepository_table = Table("tool_shed_repository", metadata, autoload=True) - c = Column("uninstalled", Boolean, default=False) - add_column(c, ToolShedRepository_table, metadata) - try: - migrate_engine.execute(f"UPDATE tool_shed_repository SET uninstalled={engine_false(migrate_engine)}") - except Exception: - log.exception("Updating column 'uninstalled' of table 'tool_shed_repository' failed.") - c = Column("dist_to_shed", Boolean, default=False) - add_column(c, ToolShedRepository_table, metadata) - try: - migrate_engine.execute(f"UPDATE tool_shed_repository SET dist_to_shed={engine_false(migrate_engine)}") - except Exception: - log.exception("Updating column 'dist_to_shed' of table 'tool_shed_repository' failed.") - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - ToolShedRepository_table = Table("tool_shed_repository", metadata, autoload=True) - # SQLAlchemy Migrate has a bug when dropping a boolean column in SQLite - if migrate_engine.name != "sqlite": - drop_column("uninstalled", ToolShedRepository_table) - drop_column("dist_to_shed", ToolShedRepository_table) diff --git a/lib/galaxy/model/migrate/versions/0091_add_tool_version_tables.py b/lib/galaxy/model/migrate/versions/0091_add_tool_version_tables.py deleted file mode 100644 index b0c2626dbc2..00000000000 --- a/lib/galaxy/model/migrate/versions/0091_add_tool_version_tables.py +++ /dev/null @@ -1,128 +0,0 @@ -""" -Migration script to create the tool_version and tool_version_association tables and drop the tool_id_guid_map table. -""" - -import datetime -import logging -from json import loads - -from sqlalchemy import ( - Column, - DateTime, - ForeignKey, - Index, - Integer, - MetaData, - String, - Table, - TEXT, -) - -from galaxy.model.custom_types import ( - _sniffnfix_pg9_hex, - TrimmedString, -) -from galaxy.model.migrate.versions.util import ( - localtimestamp, - nextval, -) - -log = logging.getLogger(__name__) -now = datetime.datetime.utcnow -metadata = MetaData() - - -ToolVersion_table = Table( - "tool_version", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("tool_id", String(255)), - Column("tool_shed_repository_id", Integer, ForeignKey("tool_shed_repository.id"), index=True, nullable=True), -) - -ToolVersionAssociation_table = Table( - "tool_version_association", - metadata, - Column("id", Integer, primary_key=True), - Column("tool_id", Integer, ForeignKey("tool_version.id"), index=True, nullable=False), - Column("parent_id", Integer, ForeignKey("tool_version.id"), index=True, nullable=False), -) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - # Create the tables. - try: - ToolVersion_table.create() - except Exception: - log.exception("Creating tool_version table failed.") - try: - ToolVersionAssociation_table.create() - except Exception: - log.exception("Creating tool_version_association table failed.") - # Populate the tool table with tools included in installed tool shed repositories. - cmd = "SELECT id, metadata FROM tool_shed_repository" - result = migrate_engine.execute(cmd) - count = 0 - for row in result: - if row[1]: - tool_shed_repository_id = row[0] - repository_metadata = loads(_sniffnfix_pg9_hex(str(row[1]))) - # Create a new row in the tool table for each tool included in repository. We will NOT - # handle tool_version_associaions because we do not have the information we need to do so. - tools = repository_metadata.get("tools", []) - for tool_dict in tools: - cmd = "INSERT INTO tool_version VALUES (%s, %s, %s, '%s', %s)" % ( - nextval(migrate_engine, "tool_version"), - localtimestamp(migrate_engine), - localtimestamp(migrate_engine), - tool_dict["guid"], - tool_shed_repository_id, - ) - migrate_engine.execute(cmd) - count += 1 - print("Added %d rows to the new tool_version table." % count) - # Drop the tool_id_guid_map table since the 2 new tables render it unnecessary. - ToolIdGuidMap_table = Table("tool_id_guid_map", metadata, autoload=True) - try: - ToolIdGuidMap_table.drop() - except Exception: - log.exception("Dropping tool_id_guid_map table failed.") - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - - ToolIdGuidMap_table = Table( - "tool_id_guid_map", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("tool_id", String(255)), - Column("tool_version", TEXT), - Column("tool_shed", TrimmedString(255)), - Column("repository_owner", TrimmedString(255)), - Column("repository_name", TrimmedString(255)), - Column("guid", TEXT), - Index("ix_tool_id_guid_map_guid", "guid", unique=True, mysql_length=200), - ) - - metadata.reflect() - try: - ToolVersionAssociation_table.drop() - except Exception: - log.exception("Dropping tool_version_association table failed.") - try: - ToolVersion_table.drop() - except Exception: - log.exception("Dropping tool_version table failed.") - try: - ToolIdGuidMap_table.create() - except Exception: - log.exception("Creating tool_id_guid_map table failed.") diff --git a/lib/galaxy/model/migrate/versions/0092_add_migrate_tools_table.py b/lib/galaxy/model/migrate/versions/0092_add_migrate_tools_table.py deleted file mode 100644 index 9fdb919349a..00000000000 --- a/lib/galaxy/model/migrate/versions/0092_add_migrate_tools_table.py +++ /dev/null @@ -1,50 +0,0 @@ -""" -Migration script to create the migrate_tools table. -""" - -import logging - -from sqlalchemy import ( - Column, - Integer, - MetaData, - Table, - TEXT, -) - -from galaxy.model.custom_types import TrimmedString -from galaxy.model.migrate.versions.util import ( - create_table, - drop_table, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - -MigrateTools_table = Table( - "migrate_tools", - metadata, - Column("repository_id", TrimmedString(255)), - Column("repository_path", TEXT), - Column("version", Integer), -) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - create_table(MigrateTools_table) - try: - cmd = "INSERT INTO migrate_tools VALUES ('GalaxyTools', 'lib/galaxy/tool_shed/migrate', %d)" % 1 - migrate_engine.execute(cmd) - except Exception: - log.exception("Inserting into table 'migrate_tools' failed.") - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - drop_table(MigrateTools_table) diff --git a/lib/galaxy/model/migrate/versions/0093_add_job_params_col.py b/lib/galaxy/model/migrate/versions/0093_add_job_params_col.py deleted file mode 100644 index ffd2936845d..00000000000 --- a/lib/galaxy/model/migrate/versions/0093_add_job_params_col.py +++ /dev/null @@ -1,48 +0,0 @@ -""" -Migration script to add a 'params' column to the 'job' table. -""" - -import logging - -from sqlalchemy import ( - Column, - MetaData, - Table, -) - -# Need our custom types, but don't import anything else from model -from galaxy.model.custom_types import TrimmedString - -log = logging.getLogger(__name__) -metadata = MetaData() - -# Column to add. -params_col = Column("params", TrimmedString(255), index=True) - - -def upgrade(migrate_engine): - metadata.bind = migrate_engine - print(__doc__) - metadata.reflect() - - # Add column to Job table. - try: - Job_table = Table("job", metadata, autoload=True) - params_col.create(Job_table, index_name="ix_job_params") - assert params_col is Job_table.c.params - - except Exception: - log.exception("Adding column 'params' to job table failed.") - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - # Drop column from Job table. - try: - Job_table = Table("job", metadata, autoload=True) - params_col = Job_table.c.params - params_col.drop() - except Exception: - log.exception("Dropping column 'params' from job table failed.") diff --git a/lib/galaxy/model/migrate/versions/0094_add_job_handler_col.py b/lib/galaxy/model/migrate/versions/0094_add_job_handler_col.py deleted file mode 100644 index 1654d678901..00000000000 --- a/lib/galaxy/model/migrate/versions/0094_add_job_handler_col.py +++ /dev/null @@ -1,37 +0,0 @@ -""" -Migration script to add a 'handler' column to the 'job' table. -""" - -import logging - -from sqlalchemy import ( - Column, - MetaData, -) - -from galaxy.model.custom_types import TrimmedString -from galaxy.model.migrate.versions.util import ( - add_column, - drop_column, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - -# Column to add. -handler_col = Column("handler", TrimmedString(255), index=True) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - add_column(handler_col, "job", metadata, index_name="ix_job_handler") - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - drop_column("handler", "job", metadata) diff --git a/lib/galaxy/model/migrate/versions/0095_hda_subsets.py b/lib/galaxy/model/migrate/versions/0095_hda_subsets.py deleted file mode 100644 index f6a4c6a4395..00000000000 --- a/lib/galaxy/model/migrate/versions/0095_hda_subsets.py +++ /dev/null @@ -1,61 +0,0 @@ -""" -Migration script to create table for tracking history_dataset_association subsets. -""" - -import logging - -from sqlalchemy import ( - Column, - ForeignKey, - Index, - Integer, - MetaData, - Table, - Unicode, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - -# Table to add. - -HistoryDatasetAssociationSubset_table = Table( - "history_dataset_association_subset", - metadata, - Column("id", Integer, primary_key=True), - Column("history_dataset_association_id", Integer, ForeignKey("history_dataset_association.id")), - Column("history_dataset_association_subset_id", Integer, ForeignKey("history_dataset_association.id")), - Column("location", Unicode(255), index=True), -) - - -def upgrade(migrate_engine): - metadata.bind = migrate_engine - print(__doc__) - metadata.reflect() - - # Create history_dataset_association_subset. - try: - HistoryDatasetAssociationSubset_table.create() - except Exception: - log.exception("Creating history_dataset_association_subset table failed.") - - # Manually create indexes because they are too long for MySQL databases. - i1 = Index("ix_hda_id", HistoryDatasetAssociationSubset_table.c.history_dataset_association_id) - i2 = Index("ix_hda_subset_id", HistoryDatasetAssociationSubset_table.c.history_dataset_association_subset_id) - try: - i1.create() - i2.create() - except Exception: - log.exception("Adding indices to table 'history_dataset_association_subset' table failed.") - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - # Drop history_dataset_association_subset table. - try: - HistoryDatasetAssociationSubset_table.drop() - except Exception: - log.exception("Dropping history_dataset_association_subset table failed.") diff --git a/lib/galaxy/model/migrate/versions/0096_openid_provider.py b/lib/galaxy/model/migrate/versions/0096_openid_provider.py deleted file mode 100644 index 2a19e269073..00000000000 --- a/lib/galaxy/model/migrate/versions/0096_openid_provider.py +++ /dev/null @@ -1,48 +0,0 @@ -""" -Migration script to add column to openid table for provider. -Remove any OpenID entries with nonunique GenomeSpace Identifier -""" - -import logging - -from sqlalchemy import ( - Column, - MetaData, - Table, -) - -from galaxy.model.custom_types import TrimmedString - -log = logging.getLogger(__name__) -BAD_IDENTIFIER = "https://identity.genomespace.org/identityServer/xrd.jsp" -metadata = MetaData() - - -def upgrade(migrate_engine): - metadata.bind = migrate_engine - print(__doc__) - metadata.reflect() - - try: - OpenID_table = Table("galaxy_user_openid", metadata, autoload=True) - c = Column("provider", TrimmedString(255)) - c.create(OpenID_table) - assert c is OpenID_table.c.provider - except Exception: - log.exception("Adding provider column to galaxy_user_openid table failed.") - - try: - cmd = f"DELETE FROM galaxy_user_openid WHERE openid='{BAD_IDENTIFIER}'" - migrate_engine.execute(cmd) - except Exception: - log.exception("Deleting bad Identifiers from galaxy_user_openid failed.") - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - try: - OpenID_table = Table("galaxy_user_openid", metadata, autoload=True) - OpenID_table.c.provider.drop() - except Exception: - log.exception("Dropping provider column from galaxy_user_openid table failed.") diff --git a/lib/galaxy/model/migrate/versions/0097_add_ctx_rev_column.py b/lib/galaxy/model/migrate/versions/0097_add_ctx_rev_column.py deleted file mode 100644 index 81ff20f8f5b..00000000000 --- a/lib/galaxy/model/migrate/versions/0097_add_ctx_rev_column.py +++ /dev/null @@ -1,35 +0,0 @@ -""" -Migration script to add the ctx_rev column to the tool_shed_repository table. -""" - -import logging - -from sqlalchemy import ( - Column, - MetaData, -) - -from galaxy.model.custom_types import TrimmedString -from galaxy.model.migrate.versions.util import ( - add_column, - drop_column, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - col = Column("ctx_rev", TrimmedString(10)) - add_column(col, "tool_shed_repository", metadata) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - drop_column("ctx_rev", "tool_shed_repository", metadata) diff --git a/lib/galaxy/model/migrate/versions/0098_genome_index_tool_data_table.py b/lib/galaxy/model/migrate/versions/0098_genome_index_tool_data_table.py deleted file mode 100644 index 5bae2518624..00000000000 --- a/lib/galaxy/model/migrate/versions/0098_genome_index_tool_data_table.py +++ /dev/null @@ -1,55 +0,0 @@ -""" -Migration script to create the genome_index_tool_data table. -""" - -import datetime -import logging - -from sqlalchemy import ( - Column, - DateTime, - ForeignKey, - Integer, - MetaData, - String, - Table, -) - -from galaxy.model.migrate.versions.util import ( - create_table, - drop_table, -) - -log = logging.getLogger(__name__) -now = datetime.datetime.utcnow -metadata = MetaData() - -GenomeIndexToolData_table = Table( - "genome_index_tool_data", - metadata, - Column("id", Integer, primary_key=True), - Column("job_id", Integer, ForeignKey("job.id"), index=True), - Column("dataset_id", Integer, ForeignKey("dataset.id"), index=True), - Column("deferred_job_id", Integer, ForeignKey("deferred_job.id"), index=True), - Column("transfer_job_id", Integer, ForeignKey("transfer_job.id"), index=True), - Column("fasta_path", String(255)), - Column("created_time", DateTime, default=now), - Column("modified_time", DateTime, default=now, onupdate=now), - Column("indexer", String(64)), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True), -) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - create_table(GenomeIndexToolData_table) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - drop_table(GenomeIndexToolData_table) diff --git a/lib/galaxy/model/migrate/versions/0099_add_tool_dependency_table.py b/lib/galaxy/model/migrate/versions/0099_add_tool_dependency_table.py deleted file mode 100644 index b8b6ef35c61..00000000000 --- a/lib/galaxy/model/migrate/versions/0099_add_tool_dependency_table.py +++ /dev/null @@ -1,56 +0,0 @@ -""" -Migration script to add the tool_dependency table. -""" - -import datetime -import logging - -from sqlalchemy import ( - Boolean, - Column, - DateTime, - ForeignKey, - Integer, - MetaData, - Table, -) - -from galaxy.model.custom_types import TrimmedString -from galaxy.model.migrate.versions.util import ( - create_table, - drop_table, -) - -log = logging.getLogger(__name__) -now = datetime.datetime.utcnow -metadata = MetaData() - -# New table to store information about cloned tool shed repositories. -ToolDependency_table = Table( - "tool_dependency", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("tool_shed_repository_id", Integer, ForeignKey("tool_shed_repository.id"), index=True, nullable=False), - Column("installed_changeset_revision", TrimmedString(255)), - Column("name", TrimmedString(255)), - Column("version", TrimmedString(40)), - Column("type", TrimmedString(40)), - Column("uninstalled", Boolean, default=False), -) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - create_table(ToolDependency_table) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - drop_table(ToolDependency_table) diff --git a/lib/galaxy/model/migrate/versions/0100_alter_tool_dependency_table_version_column.py b/lib/galaxy/model/migrate/versions/0100_alter_tool_dependency_table_version_column.py deleted file mode 100644 index ea0a274b430..00000000000 --- a/lib/galaxy/model/migrate/versions/0100_alter_tool_dependency_table_version_column.py +++ /dev/null @@ -1,46 +0,0 @@ -""" -Migration script to alter the type of the tool_dependency.version column from TrimmedString(40) to Text. -""" - -import logging - -from sqlalchemy import ( - MetaData, - Table, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - Table("tool_dependency", metadata, autoload=True) - # Change the tool_dependency table's version column from TrimmedString to Text. - if migrate_engine.name in ["postgres", "postgresql"]: - cmd = "ALTER TABLE tool_dependency ALTER COLUMN version TYPE Text;" - elif migrate_engine.name == "mysql": - cmd = "ALTER TABLE tool_dependency MODIFY COLUMN version Text;" - else: - # We don't have to do anything for sqlite tables. From the sqlite documentation at http://sqlite.org/datatype3.html: - # 1.0 Storage Classes and Datatypes - # Each value stored in an SQLite database (or manipulated by the database engine) has one of the following storage classes: - # NULL. The value is a NULL value. - # INTEGER. The value is a signed integer, stored in 1, 2, 3, 4, 6, or 8 bytes depending on the magnitude of the value. - # REAL. The value is a floating point value, stored as an 8-byte IEEE floating point number. - # TEXT. The value is a text string, stored using the database encoding (UTF-8, UTF-16BE or UTF-16LE). - # BLOB. The value is a blob of data, stored exactly as it was input. - cmd = None - if cmd: - try: - migrate_engine.execute(cmd) - except Exception: - log.exception("Altering tool_dependency.version column from TrimmedString(40) to Text failed.") - - -def downgrade(migrate_engine): - # Not necessary to change column type Text to TrimmedString(40). - pass diff --git a/lib/galaxy/model/migrate/versions/0101_drop_installed_changeset_revision_column.py b/lib/galaxy/model/migrate/versions/0101_drop_installed_changeset_revision_column.py deleted file mode 100644 index 353ae12bb59..00000000000 --- a/lib/galaxy/model/migrate/versions/0101_drop_installed_changeset_revision_column.py +++ /dev/null @@ -1,35 +0,0 @@ -""" -Migration script to drop the installed_changeset_revision column from the tool_dependency table. -""" - -import logging - -from sqlalchemy import ( - Column, - MetaData, -) - -from galaxy.model.custom_types import TrimmedString -from galaxy.model.migrate.versions.util import ( - add_column, - drop_column, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - drop_column("installed_changeset_revision", "tool_dependency", metadata) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - c = Column("installed_changeset_revision", TrimmedString(255)) - add_column(c, "tool_dependency", metadata) diff --git a/lib/galaxy/model/migrate/versions/0102_add_tool_dependency_status_columns.py b/lib/galaxy/model/migrate/versions/0102_add_tool_dependency_status_columns.py deleted file mode 100644 index 3dd65fd7a8c..00000000000 --- a/lib/galaxy/model/migrate/versions/0102_add_tool_dependency_status_columns.py +++ /dev/null @@ -1,56 +0,0 @@ -""" -Migration script to add status and error_message columns to the tool_dependency table and drop the uninstalled column from the tool_dependency table. -""" - -import logging - -from sqlalchemy import ( - Boolean, - Column, - MetaData, - Table, - TEXT, -) - -from galaxy.model.custom_types import TrimmedString -from galaxy.model.migrate.versions.util import ( - add_column, - drop_column, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - ToolDependency_table = Table("tool_dependency", metadata, autoload=True) - if migrate_engine.name == "sqlite": - col = Column("status", TrimmedString(255)) - else: - col = Column("status", TrimmedString(255), nullable=False) - add_column(col, ToolDependency_table, metadata) - - col = Column("error_message", TEXT) - add_column(col, ToolDependency_table, metadata) - - # SQLAlchemy Migrate has a bug when dropping a boolean column in SQLite - # TODO move to alembic. - if migrate_engine.name != "sqlite": - drop_column("uninstalled", ToolDependency_table) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - ToolDependency_table = Table("tool_dependency", metadata, autoload=True) - if migrate_engine.name != "sqlite": - col = Column("uninstalled", Boolean, default=False) - add_column(col, ToolDependency_table, metadata) - - drop_column("error_message", ToolDependency_table) - drop_column("status", ToolDependency_table) diff --git a/lib/galaxy/model/migrate/versions/0103_add_tool_shed_repository_status_columns.py b/lib/galaxy/model/migrate/versions/0103_add_tool_shed_repository_status_columns.py deleted file mode 100644 index e98c41bc9c7..00000000000 --- a/lib/galaxy/model/migrate/versions/0103_add_tool_shed_repository_status_columns.py +++ /dev/null @@ -1,69 +0,0 @@ -"""Migration script to add status and error_message columns to the tool_shed_repository table.""" - -import logging - -from sqlalchemy import ( - Column, - MetaData, - Table, - TEXT, -) - -# Need our custom types, but don't import anything else from model -from galaxy.model.custom_types import TrimmedString - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - metadata.bind = migrate_engine - print(__doc__) - metadata.reflect() - ToolShedRepository_table = Table("tool_shed_repository", metadata, autoload=True) - # Add the status column to the tool_shed_repository table. - col = Column("status", TrimmedString(255)) - try: - col.create(ToolShedRepository_table) - assert col is ToolShedRepository_table.c.status - except Exception: - log.exception("Adding status column to the tool_shed_repository table failed.") - # Add the error_message column to the tool_shed_repository table. - col = Column("error_message", TEXT) - try: - col.create(ToolShedRepository_table) - assert col is ToolShedRepository_table.c.error_message - except Exception: - log.exception("Adding error_message column to the tool_shed_repository table failed.") - # Update the status column value for tool_shed_repositories to the default value 'Installed'. - cmd = "UPDATE tool_shed_repository SET status = 'Installed';" - try: - migrate_engine.execute(cmd) - except Exception: - log.exception("Exception executing SQL command: %s", cmd) - # Update the status column for tool_shed_repositories that have been uninstalled. - cmd = "UPDATE tool_shed_repository SET status = 'Uninstalled' WHERE uninstalled;" - try: - migrate_engine.execute(cmd) - except Exception: - log.exception("Exception executing SQL command: %s", cmd) - # Update the status column for tool_shed_repositories that have been deactivated. - cmd = "UPDATE tool_shed_repository SET status = 'Deactivated' where deleted and not uninstalled;" - try: - migrate_engine.execute(cmd) - except Exception: - log.exception("Exception executing SQL command: %s", cmd) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - ToolShedRepository_table = Table("tool_shed_repository", metadata, autoload=True) - try: - ToolShedRepository_table.c.status.drop() - except Exception: - log.exception("Dropping column status from the tool_shed_repository table failed.") - try: - ToolShedRepository_table.c.error_message.drop() - except Exception: - log.exception("Dropping column error_message from the tool_shed_repository table failed.") diff --git a/lib/galaxy/model/migrate/versions/0104_update_genome_downloader_job_parameters.py b/lib/galaxy/model/migrate/versions/0104_update_genome_downloader_job_parameters.py deleted file mode 100644 index 59d23193c4e..00000000000 --- a/lib/galaxy/model/migrate/versions/0104_update_genome_downloader_job_parameters.py +++ /dev/null @@ -1,11 +0,0 @@ -""" -Migration script to update the deferred job parameters for liftover transfer jobs. -""" - - -def upgrade(migrate_engine): - print(__doc__) - - -def downgrade(migrate_engine): - pass diff --git a/lib/galaxy/model/migrate/versions/0105_add_cleanup_event_table.py b/lib/galaxy/model/migrate/versions/0105_add_cleanup_event_table.py deleted file mode 100644 index 578969f5dd3..00000000000 --- a/lib/galaxy/model/migrate/versions/0105_add_cleanup_event_table.py +++ /dev/null @@ -1,148 +0,0 @@ -""" -Migration script to add the cleanup_event* tables. -""" - -import datetime -import logging - -from sqlalchemy import ( - Column, - DateTime, - ForeignKey, - Integer, - MetaData, - Table, -) - -from galaxy.model.custom_types import TrimmedString - -log = logging.getLogger(__name__) -now = datetime.datetime.utcnow -metadata = MetaData() - -# New table to log cleanup events -CleanupEvent_table = Table( - "cleanup_event", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("message", TrimmedString(1024)), -) - -CleanupEventDatasetAssociation_table = Table( - "cleanup_event_dataset_association", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("cleanup_event_id", Integer, ForeignKey("cleanup_event.id"), index=True, nullable=True), - Column("dataset_id", Integer, ForeignKey("dataset.id"), index=True), -) - -CleanupEventMetadataFileAssociation_table = Table( - "cleanup_event_metadata_file_association", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("cleanup_event_id", Integer, ForeignKey("cleanup_event.id"), index=True, nullable=True), - Column("metadata_file_id", Integer, ForeignKey("metadata_file.id"), index=True), -) - -CleanupEventHistoryAssociation_table = Table( - "cleanup_event_history_association", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("cleanup_event_id", Integer, ForeignKey("cleanup_event.id"), index=True, nullable=True), - Column("history_id", Integer, ForeignKey("history.id"), index=True), -) - -CleanupEventHistoryDatasetAssociationAssociation_table = Table( - "cleanup_event_hda_association", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("cleanup_event_id", Integer, ForeignKey("cleanup_event.id"), index=True, nullable=True), - Column("hda_id", Integer, ForeignKey("history_dataset_association.id"), index=True), -) - -CleanupEventLibraryAssociation_table = Table( - "cleanup_event_library_association", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("cleanup_event_id", Integer, ForeignKey("cleanup_event.id"), index=True, nullable=True), - Column("library_id", Integer, ForeignKey("library.id"), index=True), -) - -CleanupEventLibraryFolderAssociation_table = Table( - "cleanup_event_library_folder_association", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("cleanup_event_id", Integer, ForeignKey("cleanup_event.id"), index=True, nullable=True), - Column("library_folder_id", Integer, ForeignKey("library_folder.id"), index=True), -) - -CleanupEventLibraryDatasetAssociation_table = Table( - "cleanup_event_library_dataset_association", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("cleanup_event_id", Integer, ForeignKey("cleanup_event.id"), index=True, nullable=True), - Column("library_dataset_id", Integer, ForeignKey("library_dataset.id"), index=True), -) - -CleanupEventLibraryDatasetDatasetAssociationAssociation_table = Table( - "cleanup_event_ldda_association", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("cleanup_event_id", Integer, ForeignKey("cleanup_event.id"), index=True, nullable=True), - Column("ldda_id", Integer, ForeignKey("library_dataset_dataset_association.id"), index=True), -) - -CleanupEventImplicitlyConvertedDatasetAssociationAssociation_table = Table( - "cleanup_event_icda_association", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("cleanup_event_id", Integer, ForeignKey("cleanup_event.id"), index=True, nullable=True), - Column("icda_id", Integer, ForeignKey("implicitly_converted_dataset_association.id"), index=True), -) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - try: - CleanupEvent_table.create() - CleanupEventDatasetAssociation_table.create() - CleanupEventMetadataFileAssociation_table.create() - CleanupEventHistoryAssociation_table.create() - CleanupEventHistoryDatasetAssociationAssociation_table.create() - CleanupEventLibraryAssociation_table.create() - CleanupEventLibraryFolderAssociation_table.create() - CleanupEventLibraryDatasetAssociation_table.create() - CleanupEventLibraryDatasetDatasetAssociationAssociation_table.create() - CleanupEventImplicitlyConvertedDatasetAssociationAssociation_table.create() - except Exception: - log.exception("Creating table failed.") - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - try: - CleanupEventImplicitlyConvertedDatasetAssociationAssociation_table.drop() - CleanupEventLibraryDatasetDatasetAssociationAssociation_table.drop() - CleanupEventLibraryDatasetAssociation_table.drop() - CleanupEventLibraryFolderAssociation_table.drop() - CleanupEventLibraryAssociation_table.drop() - CleanupEventHistoryDatasetAssociationAssociation_table.drop() - CleanupEventHistoryAssociation_table.drop() - CleanupEventMetadataFileAssociation_table.drop() - CleanupEventDatasetAssociation_table.drop() - CleanupEvent_table.drop() - except Exception: - log.exception("Dropping table failed.") diff --git a/lib/galaxy/model/migrate/versions/0106_add_missing_indexes.py b/lib/galaxy/model/migrate/versions/0106_add_missing_indexes.py deleted file mode 100644 index 817b44d531f..00000000000 --- a/lib/galaxy/model/migrate/versions/0106_add_missing_indexes.py +++ /dev/null @@ -1,70 +0,0 @@ -""" -Migration script to create missing indexes. Adding new columns to existing tables via SQLAlchemy does not create the index, even if the column definition includes index=True. -""" - -import logging - -from sqlalchemy import MetaData - -from galaxy.model.migrate.versions.util import ( - add_index, - drop_index, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - -indexes = ( - ("ix_metadata_file_lda_id", "metadata_file", "lda_id"), # 0003 - ("ix_history_importable", "history", "importable"), # 0007 - ("ix_sample_bar_code", "sample", "bar_code"), # 0009 - ("ix_request_type_deleted", "request_type", "deleted"), # 0012 - ("ix_galaxy_user_username", "galaxy_user", "username"), # 0014 - ("ix_form_definition_type", "form_definition", "type"), # 0019 - ("ix_form_definition_layout", "form_definition", "layout"), # 0019 - ("ix_job_library_folder_id", "job", "library_folder_id"), # 0020 - ("ix_page_published", "page", "published"), # 0023 - ("ix_page_deleted", "page", "deleted"), # 0023 - ("ix_galaxy_user_form_values_id", "galaxy_user", "form_values_id"), # 0025 - ("ix_lia_deleted", "library_info_association", "deleted"), # 0036 - ("ix_lfia_deleted", "library_folder_info_association", "deleted"), # 0036 - ("ix_lddia_deleted", "library_dataset_dataset_info_association", "deleted"), # 0036 - ("ix_sample_library_id", "sample", "library_id"), # 0037 - ("ix_sample_folder_id", "sample", "folder_id"), # 0037 - ("ix_lia_inheritable", "library_info_association", "inheritable"), # 0038 - ("ix_lfia_inheritable", "library_folder_info_association", "inheritable"), # 0038 - ("ix_job_imported", "job", "imported"), # 0051 - ("ix_request_notification", "request", "notification"), # 0057 - ("ix_sd_external_service_id", "sample_dataset", "external_service_id"), # 0068 - ("ix_icda_ldda_parent_id", "implicitly_converted_dataset_association", "ldda_parent_id"), # 0073 - ("ix_library_dataset_purged", "library_dataset", "purged"), # 0074 - ("ix_run_subindex", "run", "subindex"), # 0075 - ("ix_history_dataset_association_purged", "history_dataset_association", "purged"), # 0078 - ("ix_galaxy_user_disk_usage", "galaxy_user", "disk_usage"), # 0078 - ("ix_galaxy_session_disk_usage", "galaxy_session", "disk_usage"), # 0078 - ("ix_icda_ldda_id", "implicitly_converted_dataset_association", "ldda_id"), # 0084 - ("ix_tsr_includes_datatypes", "tool_shed_repository", "includes_datatypes"), # 0086 - ("ix_dataset_object_store_id", "dataset", "object_store_id"), # 0089 - ("ix_job_object_store_id", "job", "object_store_id"), # 0089 - ("ix_metadata_file_object_store_id", "metadata_file", "object_store_id"), # 0089 - ("ix_job_handler", "job", "handler"), # 0094 - ("ix_galaxy_user_email", "galaxy_user", "email"), # 0106 -) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - for ix, table, col in indexes: - add_index(ix, table, col, metadata) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - # TODO: Dropping a column used in a foreign key fails in MySQL, need to remove the FK first. - for ix, table, col in indexes: - drop_index(ix, table, col, metadata) diff --git a/lib/galaxy/model/migrate/versions/0107_add_exit_code_to_job_and_task.py b/lib/galaxy/model/migrate/versions/0107_add_exit_code_to_job_and_task.py deleted file mode 100644 index deb279fd518..00000000000 --- a/lib/galaxy/model/migrate/versions/0107_add_exit_code_to_job_and_task.py +++ /dev/null @@ -1,63 +0,0 @@ -""" -Add the exit_code column to the Job and Task tables. -""" - -import logging - -from sqlalchemy import ( - Column, - Integer, - MetaData, - Table, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - -# There was a bug when only one column was used for both tables, -# so create separate columns. -exit_code_job_col = Column("exit_code", Integer, nullable=True) -exit_code_task_col = Column("exit_code", Integer, nullable=True) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - # Add the exit_code column to the Job table. - try: - job_table = Table("job", metadata, autoload=True) - exit_code_job_col.create(job_table) - assert exit_code_job_col is job_table.c.exit_code - except Exception: - log.exception("Adding column 'exit_code' to job table failed.") - - # Add the exit_code column to the Task table. - try: - task_table = Table("task", metadata, autoload=True) - exit_code_task_col.create(task_table) - assert exit_code_task_col is task_table.c.exit_code - except Exception: - log.exception("Adding column 'exit_code' to task table failed.") - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - # Drop the Job table's exit_code column. - try: - job_table = Table("job", metadata, autoload=True) - exit_code_col = job_table.c.exit_code - exit_code_col.drop() - except Exception: - log.exception("Dropping 'exit_code' column from job table failed.") - - # Drop the Job table's exit_code column. - try: - task_table = Table("task", metadata, autoload=True) - exit_code_col = task_table.c.exit_code - exit_code_col.drop() - except Exception: - log.exception("Dropping 'exit_code' column from task table failed.") diff --git a/lib/galaxy/model/migrate/versions/0108_add_extended_metadata.py b/lib/galaxy/model/migrate/versions/0108_add_extended_metadata.py deleted file mode 100644 index 8307074061d..00000000000 --- a/lib/galaxy/model/migrate/versions/0108_add_extended_metadata.py +++ /dev/null @@ -1,69 +0,0 @@ -""" -Add the ExtendedMetadata and ExtendedMetadataIndex tables -""" - -import logging - -from sqlalchemy import ( - Column, - ForeignKey, - Integer, - MetaData, - String, - Table, - TEXT, -) - -from galaxy.model.custom_types import JSONType -from galaxy.model.migrate.versions.util import ( - add_column, - create_table, - drop_column, - drop_table, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - -ExtendedMetadata_table = Table( - "extended_metadata", metadata, Column("id", Integer, primary_key=True), Column("data", JSONType) -) - -ExtendedMetadataIndex_table = Table( - "extended_metadata_index", - metadata, - Column("id", Integer, primary_key=True), - Column( - "extended_metadata_id", - Integer, - ForeignKey("extended_metadata.id", onupdate="CASCADE", ondelete="CASCADE"), - index=True, - ), - Column("path", String(255)), - Column("value", TEXT), -) - -TABLES = [ExtendedMetadata_table, ExtendedMetadataIndex_table] - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - for table in TABLES: - create_table(table) - extended_metadata_ldda_col = Column( - "extended_metadata_id", Integer, ForeignKey("extended_metadata.id"), nullable=True - ) - add_column(extended_metadata_ldda_col, "library_dataset_dataset_association", metadata) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - # TODO: Dropping a column used in a foreign key fails in MySQL, need to remove the FK first. - drop_column("extended_metadata_id", "library_dataset_dataset_association", metadata) - for table in reversed(TABLES): - drop_table(table) diff --git a/lib/galaxy/model/migrate/versions/0109_add_repository_dependency_tables.py b/lib/galaxy/model/migrate/versions/0109_add_repository_dependency_tables.py deleted file mode 100644 index 8bd63e6ba1d..00000000000 --- a/lib/galaxy/model/migrate/versions/0109_add_repository_dependency_tables.py +++ /dev/null @@ -1,60 +0,0 @@ -""" -Migration script to add the repository_dependency and repository_repository_dependency_association tables. -""" - -import datetime -import logging - -from sqlalchemy import ( - Column, - DateTime, - ForeignKey, - Integer, - MetaData, - Table, -) - -from galaxy.model.migrate.versions.util import ( - create_table, - drop_table, -) - -log = logging.getLogger(__name__) -now = datetime.datetime.utcnow -metadata = MetaData() - -RepositoryDependency_table = Table( - "repository_dependency", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("tool_shed_repository_id", Integer, ForeignKey("tool_shed_repository.id"), index=True, nullable=False), -) - -RepositoryRepositoryDependencyAssociation_table = Table( - "repository_repository_dependency_association", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("tool_shed_repository_id", Integer, ForeignKey("tool_shed_repository.id"), index=True), - Column("repository_dependency_id", Integer, ForeignKey("repository_dependency.id"), index=True), -) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - create_table(RepositoryDependency_table) - create_table(RepositoryRepositoryDependencyAssociation_table) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - drop_table(RepositoryRepositoryDependencyAssociation_table) - drop_table(RepositoryDependency_table) diff --git a/lib/galaxy/model/migrate/versions/0110_add_dataset_uuid.py b/lib/galaxy/model/migrate/versions/0110_add_dataset_uuid.py deleted file mode 100644 index 8af310b03d2..00000000000 --- a/lib/galaxy/model/migrate/versions/0110_add_dataset_uuid.py +++ /dev/null @@ -1,45 +0,0 @@ -""" -Add UUID column to dataset table -""" - -import logging - -from sqlalchemy import ( - Column, - MetaData, - Table, -) - -from galaxy.model.custom_types import UUIDType - -log = logging.getLogger(__name__) -dataset_uuid_column = Column("uuid", UUIDType, nullable=True) - - -def upgrade(migrate_engine): - print(__doc__) - metadata = MetaData() - metadata.bind = migrate_engine - metadata.reflect() - - # Add the uuid colum to the dataset table - try: - dataset_table = Table("dataset", metadata, autoload=True) - dataset_uuid_column.create(dataset_table) - assert dataset_uuid_column is dataset_table.c.uuid - except Exception: - log.exception("Adding column 'uuid' to dataset table failed.") - - -def downgrade(migrate_engine): - metadata = MetaData() - metadata.bind = migrate_engine - metadata.reflect() - - # Drop the dataset table's uuid column. - try: - dataset_table = Table("dataset", metadata, autoload=True) - dataset_uuid = dataset_table.c.uuid - dataset_uuid.drop() - except Exception: - log.exception("Dropping 'uuid' column from dataset table failed.") diff --git a/lib/galaxy/model/migrate/versions/0111_add_job_destinations.py b/lib/galaxy/model/migrate/versions/0111_add_job_destinations.py deleted file mode 100644 index 0cc3bda9049..00000000000 --- a/lib/galaxy/model/migrate/versions/0111_add_job_destinations.py +++ /dev/null @@ -1,55 +0,0 @@ -""" -Add support for job destinations to the job table -""" - -import logging - -from sqlalchemy import ( - Column, - MetaData, - String, - Table, -) - -from galaxy.model.custom_types import JSONType - -log = logging.getLogger(__name__) - - -def upgrade(migrate_engine): - print(__doc__) - metadata = MetaData() - metadata.bind = migrate_engine - metadata.reflect() - Job_table = Table("job", metadata, autoload=True) - - c = Column("destination_id", String(255), nullable=True) - try: - c.create(Job_table) - assert c is Job_table.c.destination_id - except Exception: - log.exception("Adding column 'destination_id' to job table failed.") - - c = Column("destination_params", JSONType, nullable=True) - try: - c.create(Job_table) - assert c is Job_table.c.destination_params - except Exception: - log.exception("Adding column 'destination_params' to job table failed.") - - -def downgrade(migrate_engine): - metadata = MetaData() - metadata.bind = migrate_engine - metadata.reflect() - Job_table = Table("job", metadata, autoload=True) - - try: - Job_table.c.destination_params.drop() - except Exception: - log.exception("Dropping column 'destination_params' from job table failed.") - - try: - Job_table.c.destination_id.drop() - except Exception: - log.exception("Dropping column 'destination_id' from job table failed.") diff --git a/lib/galaxy/model/migrate/versions/0112_add_data_manager_history_association_and_data_manager_job_association_tables.py b/lib/galaxy/model/migrate/versions/0112_add_data_manager_history_association_and_data_manager_job_association_tables.py deleted file mode 100644 index 6451924d7d9..00000000000 --- a/lib/galaxy/model/migrate/versions/0112_add_data_manager_history_association_and_data_manager_job_association_tables.py +++ /dev/null @@ -1,66 +0,0 @@ -""" -Migration script to add the data_manager_history_association table and data_manager_job_association. -""" - -import datetime -import logging - -from sqlalchemy import ( - Column, - DateTime, - ForeignKey, - Index, - Integer, - MetaData, - Table, - TEXT, -) - -from galaxy.model.migrate.versions.util import ( - create_table, - drop_table, -) - -log = logging.getLogger(__name__) -now = datetime.datetime.utcnow -metadata = MetaData() - -DataManagerHistoryAssociation_table = Table( - "data_manager_history_association", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, index=True, default=now, onupdate=now), - Column("history_id", Integer, ForeignKey("history.id"), index=True), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True), -) - -DataManagerJobAssociation_table = Table( - "data_manager_job_association", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, index=True, default=now, onupdate=now), - Column("job_id", Integer, ForeignKey("job.id"), index=True), - Column("data_manager_id", TEXT), - Index("ix_data_manager_job_association_data_manager_id", "data_manager_id", mysql_length=200), -) - -TABLES = [DataManagerHistoryAssociation_table, DataManagerJobAssociation_table] - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - for table in TABLES: - create_table(table) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - for table in TABLES: - drop_table(table) diff --git a/lib/galaxy/model/migrate/versions/0113_update_migrate_tools_table.py b/lib/galaxy/model/migrate/versions/0113_update_migrate_tools_table.py deleted file mode 100644 index 64f8ca530f2..00000000000 --- a/lib/galaxy/model/migrate/versions/0113_update_migrate_tools_table.py +++ /dev/null @@ -1,29 +0,0 @@ -""" -Migration script to update the migrate_tools.repository_path column to point to the new location lib/tool_shed/galaxy_install/migrate. -""" - -import logging - -log = logging.getLogger(__name__) - - -def upgrade(migrate_engine): - print(__doc__) - # Create the table. - try: - cmd = "UPDATE migrate_tools set repository_path='lib/galaxy/tool_shed/migrate';" - migrate_engine.execute(cmd) - except Exception: - log.exception( - "Updating migrate_tools.repository_path column to point to the new location lib/tool_shed/galaxy_install/migrate failed." - ) - - -def downgrade(migrate_engine): - try: - cmd = "UPDATE migrate_tools set repository_path='lib/galaxy/tool_shed/migrate';" - migrate_engine.execute(cmd) - except Exception: - log.exception( - "Updating migrate_tools.repository_path column to point to the old location lib/galaxy/tool_shed/migrate failed." - ) diff --git a/lib/galaxy/model/migrate/versions/0114_update_migrate_tools_table_again.py b/lib/galaxy/model/migrate/versions/0114_update_migrate_tools_table_again.py deleted file mode 100644 index a8f25bd807d..00000000000 --- a/lib/galaxy/model/migrate/versions/0114_update_migrate_tools_table_again.py +++ /dev/null @@ -1,29 +0,0 @@ -""" -Migration script to update the migrate_tools.repository_path column to point to the new location lib/tool_shed/galaxy_install/migrate. -""" - -import logging - -log = logging.getLogger(__name__) - - -def upgrade(migrate_engine): - print(__doc__) - # Create the table. - try: - cmd = "UPDATE migrate_tools set repository_path='lib/tool_shed/galaxy_install/migrate';" - migrate_engine.execute(cmd) - except Exception: - log.exception( - "Updating migrate_tools.repository_path column to point to the new location lib/tool_shed/galaxy_install/migrate failed." - ) - - -def downgrade(migrate_engine): - try: - cmd = "UPDATE migrate_tools set repository_path='lib/galaxy/tool_shed/migrate';" - migrate_engine.execute(cmd) - except Exception: - log.exception( - "Updating migrate_tools.repository_path column to point to the old location lib/galaxy/tool_shed/migrate failed." - ) diff --git a/lib/galaxy/model/migrate/versions/0115_longer_user_password_field.py b/lib/galaxy/model/migrate/versions/0115_longer_user_password_field.py deleted file mode 100644 index e5317271b57..00000000000 --- a/lib/galaxy/model/migrate/versions/0115_longer_user_password_field.py +++ /dev/null @@ -1,25 +0,0 @@ -""" -Expand the length of the password fields in the galaxy_user table to allow for other hasing schemes -""" -import logging - -from sqlalchemy import ( - MetaData, - String, - Table, -) - -log = logging.getLogger(__name__) - - -def upgrade(migrate_engine): - meta = MetaData(bind=migrate_engine) - user = Table("galaxy_user", meta, autoload=True) - try: - user.c.password.alter(type=String(255)) - except Exception: - log.exception("Altering password column failed") - - -def downgrade(migrate_engine): - pass diff --git a/lib/galaxy/model/migrate/versions/0116_drop_update_available_col_add_tool_shed_status_col.py b/lib/galaxy/model/migrate/versions/0116_drop_update_available_col_add_tool_shed_status_col.py deleted file mode 100644 index fb6edb48384..00000000000 --- a/lib/galaxy/model/migrate/versions/0116_drop_update_available_col_add_tool_shed_status_col.py +++ /dev/null @@ -1,49 +0,0 @@ -""" -Migration script to drop the update_available Boolean column and replace it with the tool_shed_status JSONType column in the tool_shed_repository table. -""" - -import logging - -from sqlalchemy import ( - Boolean, - Column, - MetaData, - Table, -) - -from galaxy.model.custom_types import JSONType -from galaxy.model.migrate.versions.util import ( - add_column, - drop_column, - engine_false, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - ToolShedRepository_table = Table("tool_shed_repository", metadata, autoload=True) - # SQLAlchemy Migrate has a bug when dropping a boolean column in SQLite - if migrate_engine.name != "sqlite": - drop_column("update_available", ToolShedRepository_table) - c = Column("tool_shed_status", JSONType, nullable=True) - add_column(c, ToolShedRepository_table, metadata) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - ToolShedRepository_table = Table("tool_shed_repository", metadata, autoload=True) - drop_column("tool_shed_status", ToolShedRepository_table) - c = Column("update_available", Boolean, default=False) - add_column(c, ToolShedRepository_table, metadata) - try: - migrate_engine.execute(f"UPDATE tool_shed_repository SET update_available={engine_false(migrate_engine)}") - except Exception: - log.exception("Updating column 'update_available' of table 'tool_shed_repository' failed.") diff --git a/lib/galaxy/model/migrate/versions/0117_add_user_activation.py b/lib/galaxy/model/migrate/versions/0117_add_user_activation.py deleted file mode 100644 index dfb21022a20..00000000000 --- a/lib/galaxy/model/migrate/versions/0117_add_user_activation.py +++ /dev/null @@ -1,53 +0,0 @@ -""" -Adds 'active' and 'activation_token' columns to the galaxy_user table. -""" - -import logging - -from sqlalchemy import ( - Boolean, - Column, - MetaData, - Table, -) - -from galaxy.model.custom_types import TrimmedString - -log = logging.getLogger(__name__) -user_active_column = Column("active", Boolean, default=True, nullable=True) -user_activation_token_column = Column("activation_token", TrimmedString(64), nullable=True) - - -def upgrade(migrate_engine): - print(__doc__) - metadata = MetaData() - metadata.bind = migrate_engine - metadata.reflect() - - # Add the active and activation_token columns to the user table in one try because the depend on each other. - try: - user_table = Table("galaxy_user", metadata, autoload=True) - user_activation_token_column.create(table=user_table) - assert user_activation_token_column is user_table.c.activation_token - user_active_column.create(table=user_table, populate_default=True) - assert user_active_column is user_table.c.active - except Exception: - log.exception("Adding columns 'active' and 'activation_token' to galaxy_user table failed.") - - -def downgrade(migrate_engine): - metadata = MetaData() - metadata.bind = migrate_engine - metadata.reflect() - - # Drop the user table's active and activation_token columns in one try because the depend on each other. - try: - user_table = Table("galaxy_user", metadata, autoload=True) - # SQLAlchemy Migrate has a bug when dropping a boolean column in SQLite - if migrate_engine.name != "sqlite": - user_active = user_table.c.active - user_active.drop() - user_activation_token = user_table.c.activation_token - user_activation_token.drop() - except Exception: - log.exception("Dropping 'active' and 'activation_token' columns from galaxy_user table failed.") diff --git a/lib/galaxy/model/migrate/versions/0118_add_hda_extended_metadata.py b/lib/galaxy/model/migrate/versions/0118_add_hda_extended_metadata.py deleted file mode 100644 index e5b216759a9..00000000000 --- a/lib/galaxy/model/migrate/versions/0118_add_hda_extended_metadata.py +++ /dev/null @@ -1,43 +0,0 @@ -""" -Add link from history_dataset_association to the extended_metadata table -""" - -import logging - -from sqlalchemy import ( - Column, - ForeignKey, - Integer, - MetaData, - Table, -) - -log = logging.getLogger(__name__) -metadata = MetaData() -extended_metadata_hda_col = Column("extended_metadata_id", Integer, ForeignKey("extended_metadata.id"), nullable=True) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - try: - hda_table = Table("history_dataset_association", metadata, autoload=True) - extended_metadata_hda_col.create(hda_table) - assert extended_metadata_hda_col is hda_table.c.extended_metadata_id - except Exception: - log.exception("Adding column 'extended_metadata_id' to history_dataset_association table failed.") - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - # Drop the HDA table's extended metadata ID column. - try: - hda_table = Table("history_dataset_association", metadata, autoload=True) - extended_metadata_id = hda_table.c.extended_metadata_id - extended_metadata_id.drop() - except Exception: - log.exception("Dropping 'extended_metadata_id' column from history_dataset_association table failed.") diff --git a/lib/galaxy/model/migrate/versions/0119_job_metrics.py b/lib/galaxy/model/migrate/versions/0119_job_metrics.py deleted file mode 100644 index e2aee047ba9..00000000000 --- a/lib/galaxy/model/migrate/versions/0119_job_metrics.py +++ /dev/null @@ -1,129 +0,0 @@ -""" -Migration script for job metric plugins. -""" - -import logging - -from sqlalchemy import ( - Column, - ForeignKey, - Integer, - MetaData, - Numeric, - Table, - Unicode, -) - -from galaxy.model.migrate.versions.util import ( - create_table, - drop_table, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - -TEXT_METRIC_MAX_LENGTH = 1023 - -JobMetricText_table = Table( - "job_metric_text", - metadata, - Column("id", Integer, primary_key=True), - Column("job_id", Integer, ForeignKey("job.id"), index=True), - Column( - "plugin", - Unicode(255), - ), - Column( - "metric_name", - Unicode(255), - ), - Column( - "metric_value", - Unicode(TEXT_METRIC_MAX_LENGTH), - ), -) - - -TaskMetricText_table = Table( - "task_metric_text", - metadata, - Column("id", Integer, primary_key=True), - Column("task_id", Integer, ForeignKey("task.id"), index=True), - Column( - "plugin", - Unicode(255), - ), - Column( - "metric_name", - Unicode(255), - ), - Column( - "metric_value", - Unicode(TEXT_METRIC_MAX_LENGTH), - ), -) - - -JobMetricNumeric_table = Table( - "job_metric_numeric", - metadata, - Column("id", Integer, primary_key=True), - Column("job_id", Integer, ForeignKey("job.id"), index=True), - Column( - "plugin", - Unicode(255), - ), - Column( - "metric_name", - Unicode(255), - ), - Column( - "metric_value", - Numeric(22, 7), - ), -) - - -TaskMetricNumeric_table = Table( - "task_metric_numeric", - metadata, - Column("id", Integer, primary_key=True), - Column("task_id", Integer, ForeignKey("task.id"), index=True), - Column( - "plugin", - Unicode(255), - ), - Column( - "metric_name", - Unicode(255), - ), - Column( - "metric_value", - Numeric(22, 7), - ), -) - - -TABLES = [ - JobMetricText_table, - TaskMetricText_table, - JobMetricNumeric_table, - TaskMetricNumeric_table, -] - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - for table in TABLES: - create_table(table) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - for table in TABLES: - drop_table(table) diff --git a/lib/galaxy/model/migrate/versions/0120_dataset_collections.py b/lib/galaxy/model/migrate/versions/0120_dataset_collections.py deleted file mode 100644 index fde673f78a4..00000000000 --- a/lib/galaxy/model/migrate/versions/0120_dataset_collections.py +++ /dev/null @@ -1,228 +0,0 @@ -""" -Migration script for tables related to dataset collections. -""" - -import datetime -import logging - -from sqlalchemy import ( - Boolean, - Column, - DateTime, - ForeignKey, - Integer, - MetaData, - Table, - TEXT, - Unicode, -) - -from galaxy.model.custom_types import TrimmedString -from galaxy.model.migrate.versions.util import ( - add_column, - create_table, - drop_column, - drop_table, -) - -now = datetime.datetime.utcnow -log = logging.getLogger(__name__) -metadata = MetaData() - -DatasetCollection_table = Table( - "dataset_collection", - metadata, - Column("id", Integer, primary_key=True), - Column( - "collection_type", - Unicode(255), - nullable=False, - ), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), -) - -HistoryDatasetCollectionAssociation_table = Table( - "history_dataset_collection_association", - metadata, - Column("id", Integer, primary_key=True), - Column("collection_id", Integer, ForeignKey("dataset_collection.id"), index=True), - Column("history_id", Integer, ForeignKey("history.id"), index=True), - Column("hid", Integer), - Column("name", TrimmedString(255)), - Column("deleted", Boolean, default=False), - Column("visible", Boolean, default=True), - Column( - "copied_from_history_dataset_collection_association_id", - Integer, - ForeignKey("history_dataset_collection_association.id"), - nullable=True, - ), - Column("implicit_output_name", Unicode(255), nullable=True), -) - -LibraryDatasetCollectionAssociation_table = Table( - "library_dataset_collection_association", - metadata, - Column("id", Integer, primary_key=True), - Column("collection_id", Integer, ForeignKey("dataset_collection.id"), index=True), - Column("name", TrimmedString(255)), - Column("deleted", Boolean, default=False), - Column("folder_id", Integer, ForeignKey("library_folder.id"), index=True), -) - -DatasetCollectionElement_table = Table( - "dataset_collection_element", - metadata, - Column("id", Integer, primary_key=True), - Column("dataset_collection_id", Integer, ForeignKey("dataset_collection.id"), index=True, nullable=False), - Column("hda_id", Integer, ForeignKey("history_dataset_association.id"), index=True, nullable=True), - Column("ldda_id", Integer, ForeignKey("library_dataset_dataset_association.id"), index=True, nullable=True), - Column("child_collection_id", Integer, ForeignKey("dataset_collection.id"), index=True, nullable=True), - Column("element_index", Integer, nullable=False), - Column("element_identifier", Unicode(255), nullable=False), -) - -HistoryDatasetCollectionAnnotationAssociation_table = Table( - "history_dataset_collection_annotation_association", - metadata, - Column("id", Integer, primary_key=True), - Column( - "history_dataset_collection_id", Integer, ForeignKey("history_dataset_collection_association.id"), index=True - ), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True), - Column("annotation", TEXT), -) - -LibraryDatasetCollectionAnnotationAssociation_table = Table( - "library_dataset_collection_annotation_association", - metadata, - Column("id", Integer, primary_key=True), - Column( - "library_dataset_collection_id", Integer, ForeignKey("library_dataset_collection_association.id"), index=True - ), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True), - Column("annotation", TEXT), -) - -HistoryDatasetCollectionRatingAssociation_table = Table( - "history_dataset_collection_rating_association", - metadata, - Column("id", Integer, primary_key=True), - Column( - "history_dataset_collection_id", Integer, ForeignKey("history_dataset_collection_association.id"), index=True - ), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True), - Column("rating", Integer, index=True), -) - -LibraryDatasetCollectionRatingAssociation_table = Table( - "library_dataset_collection_rating_association", - metadata, - Column("id", Integer, primary_key=True), - Column( - "library_dataset_collection_id", Integer, ForeignKey("library_dataset_collection_association.id"), index=True - ), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True), - Column("rating", Integer, index=True), -) - -HistoryDatasetCollectionTagAssociation_table = Table( - "history_dataset_collection_tag_association", - metadata, - Column("id", Integer, primary_key=True), - Column( - "history_dataset_collection_id", Integer, ForeignKey("history_dataset_collection_association.id"), index=True - ), - Column("tag_id", Integer, ForeignKey("tag.id"), index=True), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True), - Column("user_tname", Unicode(255), index=True), - Column("value", Unicode(255), index=True), - Column("user_value", Unicode(255), index=True), -) - -LibraryDatasetCollectionTagAssociation_table = Table( - "library_dataset_collection_tag_association", - metadata, - Column("id", Integer, primary_key=True), - Column( - "library_dataset_collection_id", Integer, ForeignKey("library_dataset_collection_association.id"), index=True - ), - Column("tag_id", Integer, ForeignKey("tag.id"), index=True), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True), - Column("user_tname", Unicode(255), index=True), - Column("value", Unicode(255), index=True), - Column("user_value", Unicode(255), index=True), -) - -JobToInputDatasetCollectionAssociation_table = Table( - "job_to_input_dataset_collection", - metadata, - Column("id", Integer, primary_key=True), - Column("job_id", Integer, ForeignKey("job.id"), index=True), - Column("dataset_collection_id", Integer, ForeignKey("history_dataset_collection_association.id"), index=True), - Column("name", Unicode(255)), -) - -JobToOutputDatasetCollectionAssociation_table = Table( - "job_to_output_dataset_collection", - metadata, - Column("id", Integer, primary_key=True), - Column("job_id", Integer, ForeignKey("job.id"), index=True), - Column("dataset_collection_id", Integer, ForeignKey("history_dataset_collection_association.id"), index=True), - Column("name", Unicode(255)), -) - -ImplicitlyCreatedDatasetCollectionInput_table = Table( - "implicitly_created_dataset_collection_inputs", - metadata, - Column("id", Integer, primary_key=True), - Column("dataset_collection_id", Integer, ForeignKey("history_dataset_collection_association.id"), index=True), - Column("input_dataset_collection_id", Integer, ForeignKey("history_dataset_collection_association.id"), index=True), - Column("name", Unicode(255)), -) - - -TABLES = [ - DatasetCollection_table, - HistoryDatasetCollectionAssociation_table, - LibraryDatasetCollectionAssociation_table, - DatasetCollectionElement_table, - JobToInputDatasetCollectionAssociation_table, - JobToOutputDatasetCollectionAssociation_table, - ImplicitlyCreatedDatasetCollectionInput_table, - HistoryDatasetCollectionAnnotationAssociation_table, - HistoryDatasetCollectionRatingAssociation_table, - HistoryDatasetCollectionTagAssociation_table, - LibraryDatasetCollectionAnnotationAssociation_table, - LibraryDatasetCollectionRatingAssociation_table, - LibraryDatasetCollectionTagAssociation_table, -] - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - for table in TABLES: - create_table(table) - - # TODO: Find a better name for this column... - HiddenBeneathCollection_column = Column( - "hidden_beneath_collection_instance_id", - Integer, - ForeignKey("history_dataset_collection_association.id"), - nullable=True, - ) - add_column(HiddenBeneathCollection_column, "history_dataset_association", metadata) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - drop_column("hidden_beneath_collection_instance_id", "history_dataset_association", metadata) - - for table in reversed(TABLES): - drop_table(table) diff --git a/lib/galaxy/model/migrate/versions/0121_workflow_uuids.py b/lib/galaxy/model/migrate/versions/0121_workflow_uuids.py deleted file mode 100644 index c49982af39d..00000000000 --- a/lib/galaxy/model/migrate/versions/0121_workflow_uuids.py +++ /dev/null @@ -1,51 +0,0 @@ -""" -Add UUIDs to workflows -""" - -import logging - -from sqlalchemy import ( - Column, - MetaData, - Table, -) - -from galaxy.model.custom_types import UUIDType - -log = logging.getLogger(__name__) -metadata = MetaData() - - -""" -Because both workflow and job requests can be determined -based the a fixed data structure, their IDs are based on -hashing the data structure -""" -workflow_uuid_column = Column("uuid", UUIDType, nullable=True) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - # Add the uuid colum to the workflow table - try: - workflow_table = Table("workflow", metadata, autoload=True) - workflow_uuid_column.create(workflow_table) - assert workflow_uuid_column is workflow_table.c.uuid - except Exception: - log.exception("Adding column 'uuid' to workflow table failed.") - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - # Drop the workflow table's uuid column. - try: - workflow_table = Table("workflow", metadata, autoload=True) - workflow_uuid = workflow_table.c.uuid - workflow_uuid.drop() - except Exception: - log.exception("Dropping 'uuid' column from workflow table failed.") diff --git a/lib/galaxy/model/migrate/versions/0122_grow_mysql_blobs.py b/lib/galaxy/model/migrate/versions/0122_grow_mysql_blobs.py deleted file mode 100644 index 2092764af8c..00000000000 --- a/lib/galaxy/model/migrate/versions/0122_grow_mysql_blobs.py +++ /dev/null @@ -1,51 +0,0 @@ -""" -Migration script to grow MySQL blobs. -""" - -import logging - -from sqlalchemy import MetaData - -log = logging.getLogger(__name__) -metadata = MetaData() - -BLOB_COLUMNS = [ - ("deferred_job", "params"), - ("extended_metadata", "data"), - ("form_definition", "fields"), - ("form_definition", "layout"), - ("form_values", "content"), - ("history_dataset_association", "metadata"), - ("job", "destination_params"), - ("library_dataset_dataset_association", "metadata"), - ("post_job_action", "action_arguments"), - ("request", "notification"), - ("sample", "workflow"), - ("transfer_job", "params"), - ("workflow_step", "tool_inputs"), - ("workflow_step", "tool_errors"), - ("workflow_step", "position"), - ("workflow_step", "config"), - ("tool_shed_repository", "metadata"), - ("tool_shed_repository", "tool_shed_status"), -] - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - if migrate_engine.name != "mysql": - return - - for (table, column) in BLOB_COLUMNS: - cmd = f"ALTER TABLE {table} MODIFY COLUMN {column} MEDIUMBLOB;" - try: - migrate_engine.execute(cmd) - except Exception: - log.exception("Failed to grow column %s.%s", table, column) - - -def downgrade(migrate_engine): - pass diff --git a/lib/galaxy/model/migrate/versions/0123_add_workflow_request_tables.py b/lib/galaxy/model/migrate/versions/0123_add_workflow_request_tables.py deleted file mode 100644 index 62cc4b810fe..00000000000 --- a/lib/galaxy/model/migrate/versions/0123_add_workflow_request_tables.py +++ /dev/null @@ -1,133 +0,0 @@ -""" -Migration script for workflow request tables. -""" - -import logging - -from sqlalchemy import ( - Column, - ForeignKey, - Integer, - MetaData, - String, - Table, - TEXT, - Unicode, -) - -from galaxy.model.custom_types import ( - JSONType, - TrimmedString, - UUIDType, -) -from galaxy.model.migrate.versions.util import ( - add_column, - create_table, - drop_column, - drop_table, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - - -WorkflowRequestInputParameter_table = Table( - "workflow_request_input_parameters", - metadata, - Column("id", Integer, primary_key=True), - Column( - "workflow_invocation_id", Integer, ForeignKey("workflow_invocation.id", onupdate="CASCADE", ondelete="CASCADE") - ), - Column("name", Unicode(255)), - Column("type", Unicode(255)), - Column("value", TEXT), -) - - -WorkflowRequestStepState_table = Table( - "workflow_request_step_states", - metadata, - Column("id", Integer, primary_key=True), - Column( - "workflow_invocation_id", Integer, ForeignKey("workflow_invocation.id", onupdate="CASCADE", ondelete="CASCADE") - ), - Column("workflow_step_id", Integer, ForeignKey("workflow_step.id")), - Column("value", JSONType), -) - - -WorkflowRequestToInputDatasetAssociation_table = Table( - "workflow_request_to_input_dataset", - metadata, - Column("id", Integer, primary_key=True), - Column("name", String(255)), - Column("workflow_invocation_id", Integer, ForeignKey("workflow_invocation.id"), index=True), - Column("workflow_step_id", Integer, ForeignKey("workflow_step.id")), - Column("dataset_id", Integer, ForeignKey("history_dataset_association.id"), index=True), -) - - -WorkflowRequestToInputDatasetCollectionAssociation_table = Table( - "workflow_request_to_input_collection_dataset", - metadata, - Column("id", Integer, primary_key=True), - Column("name", String(255)), - Column("workflow_invocation_id", Integer, ForeignKey("workflow_invocation.id"), index=True), - Column("workflow_step_id", Integer, ForeignKey("workflow_step.id")), - Column("dataset_collection_id", Integer, ForeignKey("history_dataset_collection_association.id"), index=True), -) - - -TABLES = [ - WorkflowRequestInputParameter_table, - WorkflowRequestStepState_table, - WorkflowRequestToInputDatasetAssociation_table, - WorkflowRequestToInputDatasetCollectionAssociation_table, -] - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - for table in TABLES: - create_table(table) - - History_column = Column("history_id", Integer, ForeignKey("history.id"), nullable=True) - State_column = Column("state", TrimmedString(64)) - - # TODO: Handle indexes correctly - SchedulerId_column = Column("scheduler", TrimmedString(255)) - HandlerId_column = Column("handler", TrimmedString(255)) - WorkflowUUID_column = Column("uuid", UUIDType, nullable=True) - add_column(History_column, "workflow_invocation", metadata) - add_column(State_column, "workflow_invocation", metadata) - add_column(SchedulerId_column, "workflow_invocation", metadata, index_name="id_workflow_invocation_scheduler") - add_column(HandlerId_column, "workflow_invocation", metadata, index_name="id_workflow_invocation_handler") - add_column(WorkflowUUID_column, "workflow_invocation", metadata) - - # All previous invocations have been scheduled... - cmd = "UPDATE workflow_invocation SET state = 'scheduled'" - try: - migrate_engine.execute(cmd) - except Exception: - log.exception("failed to update past workflow invocation states.") - - WorkflowInvocationStepAction_column = Column("action", JSONType, nullable=True) - add_column(WorkflowInvocationStepAction_column, "workflow_invocation_step", metadata) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - for table in TABLES: - drop_table(table) - - drop_column("state", "workflow_invocation", metadata) - drop_column("scheduler", "workflow_invocation", metadata) - drop_column("uuid", "workflow_invocation", metadata) - drop_column("history_id", "workflow_invocation", metadata) - drop_column("handler", "workflow_invocation", metadata) - drop_column("action", "workflow_invocation_step", metadata) diff --git a/lib/galaxy/model/migrate/versions/0124_job_state_history.py b/lib/galaxy/model/migrate/versions/0124_job_state_history.py deleted file mode 100644 index ea9dcfc679c..00000000000 --- a/lib/galaxy/model/migrate/versions/0124_job_state_history.py +++ /dev/null @@ -1,52 +0,0 @@ -""" -Migration script for the job state history table -""" - -import datetime -import logging - -from sqlalchemy import ( - Column, - DateTime, - ForeignKey, - Integer, - MetaData, - String, - Table, -) - -from galaxy.model.custom_types import TrimmedString -from galaxy.model.migrate.versions.util import ( - create_table, - drop_table, -) - -now = datetime.datetime.utcnow -log = logging.getLogger(__name__) -metadata = MetaData() - -JobStateHistory_table = Table( - "job_state_history", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("job_id", Integer, ForeignKey("job.id"), index=True), - Column("state", String(64), index=True), - Column("info", TrimmedString(255)), -) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - create_table(JobStateHistory_table) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - drop_table(JobStateHistory_table) diff --git a/lib/galaxy/model/migrate/versions/0125_workflow_step_tracking.py b/lib/galaxy/model/migrate/versions/0125_workflow_step_tracking.py deleted file mode 100644 index 155e0d60506..00000000000 --- a/lib/galaxy/model/migrate/versions/0125_workflow_step_tracking.py +++ /dev/null @@ -1,41 +0,0 @@ -""" -Migration script to enhance workflow step usability by adding labels and UUIDs. -""" - -import logging - -from sqlalchemy import ( - Column, - MetaData, -) - -from galaxy.model.custom_types import ( - TrimmedString, - UUIDType, -) -from galaxy.model.migrate.versions.util import ( - add_column, - drop_column, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - metadata.bind = migrate_engine - print(__doc__) - metadata.reflect() - - StepLabel_column = Column("label", TrimmedString(255)) - StepUUID_column = Column("uuid", UUIDType, nullable=True) - add_column(StepLabel_column, "workflow_step", metadata) - add_column(StepUUID_column, "workflow_step", metadata) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - drop_column("label", "workflow_step", metadata) - drop_column("uuid", "workflow_step", metadata) diff --git a/lib/galaxy/model/migrate/versions/0126_password_reset.py b/lib/galaxy/model/migrate/versions/0126_password_reset.py deleted file mode 100644 index 27124de1006..00000000000 --- a/lib/galaxy/model/migrate/versions/0126_password_reset.py +++ /dev/null @@ -1,46 +0,0 @@ -""" -Migration script for the password reset table -""" - -import logging - -from sqlalchemy import ( - Column, - DateTime, - ForeignKey, - Integer, - MetaData, - String, - Table, -) - -from galaxy.model.migrate.versions.util import ( - create_table, - drop_table, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - -PasswordResetToken_table = Table( - "password_reset_token", - metadata, - Column("token", String(32), primary_key=True, unique=True, index=True), - Column("expiration_time", DateTime), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True), -) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - create_table(PasswordResetToken_table) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - drop_table(PasswordResetToken_table) diff --git a/lib/galaxy/model/migrate/versions/0127_output_collection_adjustments.py b/lib/galaxy/model/migrate/versions/0127_output_collection_adjustments.py deleted file mode 100644 index 6346bff0d26..00000000000 --- a/lib/galaxy/model/migrate/versions/0127_output_collection_adjustments.py +++ /dev/null @@ -1,64 +0,0 @@ -""" -Migration script updating collections tables for output collections. -""" - -import logging - -from sqlalchemy import ( - Column, - ForeignKey, - Integer, - MetaData, - Table, - TEXT, - Unicode, -) - -from galaxy.model.custom_types import TrimmedString -from galaxy.model.migrate.versions.util import ( - add_column, - create_table, - drop_column, - drop_table, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - -JobToImplicitOutputDatasetCollectionAssociation_table = Table( - "job_to_implicit_output_dataset_collection", - metadata, - Column("id", Integer, primary_key=True), - Column("job_id", Integer, ForeignKey("job.id"), index=True), - Column("dataset_collection_id", Integer, ForeignKey("dataset_collection.id"), index=True), - Column("name", Unicode(255)), -) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - create_table(JobToImplicitOutputDatasetCollectionAssociation_table) - - dataset_collection_table = Table("dataset_collection", metadata, autoload=True) - # need server_default because column in non-null - populated_state_column = Column( - "populated_state", TrimmedString(64), default="ok", server_default="ok", nullable=False - ) - add_column(populated_state_column, dataset_collection_table, metadata) - - populated_message_column = Column("populated_state_message", TEXT, nullable=True) - add_column(populated_message_column, dataset_collection_table, metadata) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - drop_table(JobToImplicitOutputDatasetCollectionAssociation_table) - - dataset_collection_table = Table("dataset_collection", metadata, autoload=True) - drop_column("populated_state", dataset_collection_table) - drop_column("populated_state_message", dataset_collection_table) diff --git a/lib/galaxy/model/migrate/versions/0128_session_timeout.py b/lib/galaxy/model/migrate/versions/0128_session_timeout.py deleted file mode 100644 index 062242585fc..00000000000 --- a/lib/galaxy/model/migrate/versions/0128_session_timeout.py +++ /dev/null @@ -1,35 +0,0 @@ -""" -Migration script to add session update time (used for timeouts) -""" - -import logging - -from sqlalchemy import ( - Column, - DateTime, - MetaData, -) - -from galaxy.model.migrate.versions.util import ( - add_column, - drop_column, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - lastaction_column = Column("last_action", DateTime) - add_column(lastaction_column, "galaxy_session", metadata) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - drop_column("last_action", "galaxy_session", metadata) diff --git a/lib/galaxy/model/migrate/versions/0129_job_external_output_metadata_validity.py b/lib/galaxy/model/migrate/versions/0129_job_external_output_metadata_validity.py deleted file mode 100644 index 2f6ad569bc2..00000000000 --- a/lib/galaxy/model/migrate/versions/0129_job_external_output_metadata_validity.py +++ /dev/null @@ -1,36 +0,0 @@ -""" -Migration script to allow invalidation of job external output metadata temp files -""" - -import logging - -from sqlalchemy import ( - Boolean, - Column, - MetaData, -) - -from galaxy.model.migrate.versions.util import ( - add_column, - drop_column, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - metadata.bind = migrate_engine - print(__doc__) - metadata.reflect() - - isvalid_column = Column("is_valid", Boolean, default=True) - add_column(isvalid_column, "job_external_output_metadata", metadata) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - # SQLAlchemy Migrate has a bug when dropping a boolean column in SQLite - if migrate_engine.name != "sqlite": - drop_column("is_valid", "job_external_output_metadata", metadata) diff --git a/lib/galaxy/model/migrate/versions/0130_change_pref_datatype.py b/lib/galaxy/model/migrate/versions/0130_change_pref_datatype.py deleted file mode 100644 index e5ed362792d..00000000000 --- a/lib/galaxy/model/migrate/versions/0130_change_pref_datatype.py +++ /dev/null @@ -1,28 +0,0 @@ -""" -Migration script to change the 'value' column of 'user_preference' table from varchar to text. -""" - -import logging - -from sqlalchemy import ( - MetaData, - Table, - Text, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - metadata.bind = migrate_engine - print(__doc__) - metadata.reflect() - t = Table("user_preference", metadata, autoload=True) - t.c.value.alter(type=Text) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - # Pass, since we don't want to potentially truncate data. diff --git a/lib/galaxy/model/migrate/versions/0131_subworkflow_and_input_parameter_modules.py b/lib/galaxy/model/migrate/versions/0131_subworkflow_and_input_parameter_modules.py deleted file mode 100644 index a75d820a66a..00000000000 --- a/lib/galaxy/model/migrate/versions/0131_subworkflow_and_input_parameter_modules.py +++ /dev/null @@ -1,115 +0,0 @@ -""" -Migration script to support subworkflows and workflow request input parameters -""" - -import logging - -from sqlalchemy import ( - Column, - ForeignKey, - ForeignKeyConstraint, - Index, - Integer, - MetaData, - Table, -) - -from galaxy.model.custom_types import ( - JSONType, - TrimmedString, - UUIDType, -) -from galaxy.model.migrate.versions.util import ( - add_column, - alter_column, - create_table, - drop_column, - drop_table, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - -WorkflowInvocationToSubworkflowInvocationAssociation_table = Table( - "workflow_invocation_to_subworkflow_invocation_association", - metadata, - Column("id", Integer, primary_key=True), - Column("workflow_invocation_id", Integer), - Column("subworkflow_invocation_id", Integer), - Column("workflow_step_id", Integer), - ForeignKeyConstraint(["workflow_invocation_id"], ["workflow_invocation.id"], name="fk_wfi_swi_wfi"), - ForeignKeyConstraint(["subworkflow_invocation_id"], ["workflow_invocation.id"], name="fk_wfi_swi_swi"), - ForeignKeyConstraint(["workflow_step_id"], ["workflow_step.id"], name="fk_wfi_swi_ws"), -) - -WorkflowRequestInputStepParameter_table = Table( - "workflow_request_input_step_parameter", - metadata, - Column("id", Integer, primary_key=True), - Column("workflow_invocation_id", Integer), - Column("workflow_step_id", Integer), - Column("parameter_value", JSONType), - ForeignKeyConstraint(["workflow_invocation_id"], ["workflow_invocation.id"], name="fk_wfreq_isp_wfi"), - ForeignKeyConstraint(["workflow_step_id"], ["workflow_step.id"], name="fk_wfreq_isp_ws"), -) - -TABLES = [ - WorkflowInvocationToSubworkflowInvocationAssociation_table, - WorkflowRequestInputStepParameter_table, -] - -INDEXES = [ - Index("ix_wfinv_swfinv_wfi", WorkflowInvocationToSubworkflowInvocationAssociation_table.c.workflow_invocation_id), - Index( - "ix_wfinv_swfinv_swfi", WorkflowInvocationToSubworkflowInvocationAssociation_table.c.subworkflow_invocation_id - ), - Index("ix_wfreq_inputstep_wfi", WorkflowRequestInputStepParameter_table.c.workflow_invocation_id), -] - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - if migrate_engine.name in ["postgres", "postgresql"]: - subworkflow_id_column = Column("subworkflow_id", Integer, ForeignKey("workflow.id"), nullable=True) - input_subworkflow_step_id_column = Column( - "input_subworkflow_step_id", Integer, ForeignKey("workflow_step.id"), nullable=True - ) - parent_workflow_id_column = Column("parent_workflow_id", Integer, ForeignKey("workflow.id"), nullable=True) - else: - subworkflow_id_column = Column("subworkflow_id", Integer, nullable=True) - input_subworkflow_step_id_column = Column("input_subworkflow_step_id", Integer, nullable=True) - parent_workflow_id_column = Column("parent_workflow_id", Integer, nullable=True) - add_column(subworkflow_id_column, "workflow_step", metadata) - add_column(input_subworkflow_step_id_column, "workflow_step_connection", metadata) - add_column(parent_workflow_id_column, "workflow", metadata) - workflow_output_label_column = Column("label", TrimmedString(255)) - workflow_output_uuid_column = Column("uuid", UUIDType, nullable=True) - add_column(workflow_output_label_column, "workflow_output", metadata) - add_column(workflow_output_uuid_column, "workflow_output", metadata) - - # Make stored_workflow_id nullable, since now workflows can belong to either - # a stored workflow or a parent workflow. - alter_column("stored_workflow_id", "workflow", metadata, nullable=True) - - for table in TABLES: - # Indexes are automatically created when the tables are. - create_table(table) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - drop_column("subworkflow_id", "workflow_step", metadata) - drop_column("parent_workflow_id", "workflow", metadata) - - drop_column("input_subworkflow_step_id", "workflow_step_connection", metadata) - - drop_column("label", "workflow_output", metadata) - drop_column("uuid", "workflow_output", metadata) - - for table in TABLES: - drop_table(table) diff --git a/lib/galaxy/model/migrate/versions/0132_add_lastpasswordchange_to_user.py b/lib/galaxy/model/migrate/versions/0132_add_lastpasswordchange_to_user.py deleted file mode 100644 index d1fe77793a3..00000000000 --- a/lib/galaxy/model/migrate/versions/0132_add_lastpasswordchange_to_user.py +++ /dev/null @@ -1,23 +0,0 @@ -""" -Migration script to add a last_password_change field to the user table -""" - -from sqlalchemy import ( - Column, - DateTime, - MetaData, - Table, -) - - -def upgrade(migrate_engine): - meta = MetaData(bind=migrate_engine) - account = Table("galaxy_user", meta, autoload=True) - lpc = Column("last_password_change", DateTime()) - lpc.create(account) - - -def downgrade(migrate_engine): - meta = MetaData(bind=migrate_engine) - account = Table("galaxy_user", meta, autoload=True) - account.c.last_password_change.drop() diff --git a/lib/galaxy/model/migrate/versions/0133_add_dependency_column_to_job.py b/lib/galaxy/model/migrate/versions/0133_add_dependency_column_to_job.py deleted file mode 100644 index 9e264bfa2d5..00000000000 --- a/lib/galaxy/model/migrate/versions/0133_add_dependency_column_to_job.py +++ /dev/null @@ -1,45 +0,0 @@ -""" -Add dependencies column to jobs table -""" - -import logging - -from sqlalchemy import ( - Column, - MetaData, - Table, -) - -from galaxy.model.custom_types import JSONType - -log = logging.getLogger(__name__) -jobs_dependencies_column = Column("dependencies", JSONType, nullable=True) - - -def upgrade(migrate_engine): - print(__doc__) - metadata = MetaData() - metadata.bind = migrate_engine - metadata.reflect() - - # Add the dependencies column to the job table - try: - jobs_table = Table("job", metadata, autoload=True) - jobs_dependencies_column.create(jobs_table) - assert jobs_dependencies_column is jobs_table.c.dependencies - except Exception: - log.exception("Adding column 'dependencies' to job table failed.") - - -def downgrade(migrate_engine): - metadata = MetaData() - metadata.bind = migrate_engine - metadata.reflect() - - # Drop the job table's dependencies column. - try: - jobs_table = Table("job", metadata, autoload=True) - jobs_dependencies = jobs_table.c.dependencies - jobs_dependencies.drop() - except Exception: - log.exception("Dropping 'dependencies' column from job table failed.") diff --git a/lib/galaxy/model/migrate/versions/0134_hda_set_deleted_if_purged.py b/lib/galaxy/model/migrate/versions/0134_hda_set_deleted_if_purged.py deleted file mode 100644 index 2457748b492..00000000000 --- a/lib/galaxy/model/migrate/versions/0134_hda_set_deleted_if_purged.py +++ /dev/null @@ -1,11 +0,0 @@ -""" -Postponed to migration 160. -""" - - -def upgrade(migrate_engine): - print(__doc__) - - -def downgrade(migrate_engine): - pass diff --git a/lib/galaxy/model/migrate/versions/0135_add_library_tags.py b/lib/galaxy/model/migrate/versions/0135_add_library_tags.py deleted file mode 100644 index 9c5a68a883a..00000000000 --- a/lib/galaxy/model/migrate/versions/0135_add_library_tags.py +++ /dev/null @@ -1,58 +0,0 @@ -""" -This migration script adds support for storing tags in the context of a dataset in a library -""" - -import logging - -from sqlalchemy import ( - Column, - ForeignKey, - Integer, - MetaData, - Table, -) - -# Need our custom types, but don't import anything else from model -from galaxy.model.custom_types import TrimmedString - -log = logging.getLogger(__name__) -metadata = MetaData() - - -LibraryDatasetDatasetAssociationTagAssociation_table = Table( - "library_dataset_dataset_association_tag_association", - metadata, - Column("id", Integer, primary_key=True), - Column( - "library_dataset_dataset_association_id", - Integer, - ForeignKey("library_dataset_dataset_association.id"), - index=True, - ), - Column("tag_id", Integer, ForeignKey("tag.id"), index=True), - Column("user_tname", TrimmedString(255), index=True), - Column("value", TrimmedString(255), index=True), - Column("user_value", TrimmedString(255), index=True), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True), -) - - -def upgrade(migrate_engine): - metadata.bind = migrate_engine - print(__doc__) - metadata.reflect() - - try: - LibraryDatasetDatasetAssociationTagAssociation_table.create() - except Exception: - log.exception("Creating library_dataset_association_tag_association table failed.") - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - try: - LibraryDatasetDatasetAssociationTagAssociation_table.drop() - except Exception: - log.exception("Dropping library_dataset_association_tag_association table failed.") diff --git a/lib/galaxy/model/migrate/versions/0136_collection_and_workflow_state.py b/lib/galaxy/model/migrate/versions/0136_collection_and_workflow_state.py deleted file mode 100644 index be58fa9aa74..00000000000 --- a/lib/galaxy/model/migrate/versions/0136_collection_and_workflow_state.py +++ /dev/null @@ -1,163 +0,0 @@ -""" -Migration script for collections and workflows connections. -""" - -import datetime -import logging - -from sqlalchemy import ( - Column, - ForeignKey, - Integer, - MetaData, - String, - Table, -) - -from galaxy.model.custom_types import TrimmedString -from galaxy.model.migrate.versions.util import ( - add_column, - create_table, - drop_column, - drop_table, -) - -log = logging.getLogger(__name__) -now = datetime.datetime.utcnow -metadata = MetaData() - -workflow_invocation_output_dataset_association_table = Table( - "workflow_invocation_output_dataset_association", - metadata, - Column("id", Integer, primary_key=True), - Column("workflow_invocation_id", Integer, ForeignKey("workflow_invocation.id"), index=True), - Column("workflow_step_id", Integer, ForeignKey("workflow_step.id")), - Column("dataset_id", Integer, ForeignKey("history_dataset_association.id"), index=True), - Column("workflow_output_id", Integer, ForeignKey("workflow_output.id")), -) - -workflow_invocation_output_dataset_collection_association_table = Table( - "workflow_invocation_output_dataset_collection_association", - metadata, - Column("id", Integer, primary_key=True), - Column("workflow_invocation_id", Integer, ForeignKey("workflow_invocation.id", name="fk_wiodca_wii"), index=True), - Column("workflow_step_id", Integer, ForeignKey("workflow_step.id", name="fk_wiodca_wsi")), - Column( - "dataset_collection_id", - Integer, - ForeignKey("history_dataset_collection_association.id", name="fk_wiodca_dci"), - index=True, - ), - Column("workflow_output_id", Integer, ForeignKey("workflow_output.id", name="fk_wiodca_woi")), -) - -workflow_invocation_step_output_dataset_association_table = Table( - "workflow_invocation_step_output_dataset_association", - metadata, - Column("id", Integer, primary_key=True), - Column("workflow_invocation_step_id", Integer, ForeignKey("workflow_invocation_step.id"), index=True), - Column("dataset_id", Integer, ForeignKey("history_dataset_association.id"), index=True), - Column("output_name", String(255), nullable=True), -) - -workflow_invocation_step_output_dataset_collection_association_table = Table( - "workflow_invocation_step_output_dataset_collection_association", - metadata, - Column("id", Integer, primary_key=True), - Column( - "workflow_invocation_step_id", - Integer, - ForeignKey("workflow_invocation_step.id", name="fk_wisodca_wisi"), - index=True, - ), - Column("workflow_step_id", Integer, ForeignKey("workflow_step.id", name="fk_wisodca_wsi")), - Column( - "dataset_collection_id", - Integer, - ForeignKey("history_dataset_collection_association.id", name="fk_wisodca_dci"), - index=True, - ), - Column("output_name", String(255), nullable=True), -) - -implicit_collection_jobs_table = Table( - "implicit_collection_jobs", - metadata, - Column("id", Integer, primary_key=True), - Column("populated_state", TrimmedString(64), default="new", nullable=False), -) - -implicit_collection_jobs_job_association_table = Table( - "implicit_collection_jobs_job_association", - metadata, - Column("id", Integer, primary_key=True), - Column("implicit_collection_jobs_id", Integer, ForeignKey("implicit_collection_jobs.id"), index=True), - Column("job_id", Integer, ForeignKey("job.id"), index=True), # Consider making this nullable... - Column("order_index", Integer, nullable=False), -) - - -def get_new_tables(): - # Normally we define this globally in the file, but we need to delay the - # reading of existing tables because an existing workflow_invocation_step - # table exists that we want to recreate. - return [ - workflow_invocation_output_dataset_association_table, - workflow_invocation_output_dataset_collection_association_table, - workflow_invocation_step_output_dataset_association_table, - workflow_invocation_step_output_dataset_collection_association_table, - implicit_collection_jobs_table, - implicit_collection_jobs_job_association_table, - ] - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - for table in get_new_tables(): - create_table(table) - - # Set default for creation to scheduled, actual mapping has new as default. - workflow_invocation_step_state_column = Column("state", TrimmedString(64), default="scheduled") - if migrate_engine.name in ["postgres", "postgresql"]: - implicit_collection_jobs_id_column = Column( - "implicit_collection_jobs_id", Integer, ForeignKey("implicit_collection_jobs.id"), nullable=True - ) - job_id_column = Column("job_id", Integer, ForeignKey("job.id"), nullable=True) - else: - implicit_collection_jobs_id_column = Column("implicit_collection_jobs_id", Integer, nullable=True) - job_id_column = Column("job_id", Integer, nullable=True) - dataset_collection_element_count_column = Column("element_count", Integer, nullable=True) - - add_column(implicit_collection_jobs_id_column, "history_dataset_collection_association", metadata) - add_column(job_id_column, "history_dataset_collection_association", metadata) - add_column(dataset_collection_element_count_column, "dataset_collection", metadata) - - implicit_collection_jobs_id_column = Column( - "implicit_collection_jobs_id", Integer, ForeignKey("implicit_collection_jobs.id"), nullable=True - ) - add_column(implicit_collection_jobs_id_column, "workflow_invocation_step", metadata) - add_column(workflow_invocation_step_state_column, "workflow_invocation_step", metadata) - - cmd = ( - "UPDATE dataset_collection SET element_count = " - + "(SELECT (CASE WHEN count(*) > 0 THEN count(*) ELSE 0 END) FROM dataset_collection_element WHERE " - + "dataset_collection_element.dataset_collection_id = dataset_collection.id)" - ) - migrate_engine.execute(cmd) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - drop_column("implicit_collection_jobs_id", "history_dataset_collection_association", metadata) - drop_column("job_id", "history_dataset_collection_association", metadata) - drop_column("implicit_collection_jobs_id", "workflow_invocation_step", metadata) - drop_column("state", "workflow_invocation_step", metadata) - drop_column("element_count", "dataset_collection", metadata) - - for table in reversed(get_new_tables()): - drop_table(table) diff --git a/lib/galaxy/model/migrate/versions/0137_add_copied_from_job_id_column.py b/lib/galaxy/model/migrate/versions/0137_add_copied_from_job_id_column.py deleted file mode 100644 index 5ed23b7dc6f..00000000000 --- a/lib/galaxy/model/migrate/versions/0137_add_copied_from_job_id_column.py +++ /dev/null @@ -1,44 +0,0 @@ -""" -Add copied_from_job_id column to jobs table -""" - -import logging - -from sqlalchemy import ( - Column, - Integer, - MetaData, - Table, -) - -log = logging.getLogger(__name__) -copied_from_job_id_column = Column("copied_from_job_id", Integer, nullable=True) - - -def upgrade(migrate_engine): - print(__doc__) - metadata = MetaData() - metadata.bind = migrate_engine - metadata.reflect() - - # Add the copied_from_job_id column to the job table - try: - jobs_table = Table("job", metadata, autoload=True) - copied_from_job_id_column.create(jobs_table) - assert copied_from_job_id_column is jobs_table.c.copied_from_job_id - except Exception: - log.exception("Adding column 'copied_from_job_id_column' to job table failed.") - - -def downgrade(migrate_engine): - metadata = MetaData() - metadata.bind = migrate_engine - metadata.reflect() - - # Drop the job table's copied_from_job_id column. - try: - jobs_table = Table("job", metadata, autoload=True) - copied_from_job_id = jobs_table.c.copied_from_job_id - copied_from_job_id.drop() - except Exception: - log.exception("Dropping 'copied_from_job_id_column' column from job table failed.") diff --git a/lib/galaxy/model/migrate/versions/0138_add_hda_version.py b/lib/galaxy/model/migrate/versions/0138_add_hda_version.py deleted file mode 100644 index f7aca943885..00000000000 --- a/lib/galaxy/model/migrate/versions/0138_add_hda_version.py +++ /dev/null @@ -1,44 +0,0 @@ -""" -Add version column to history_dataset_association table -""" - -import logging - -from sqlalchemy import ( - Column, - Integer, - MetaData, - Table, -) - -log = logging.getLogger(__name__) -version_column = Column("version", Integer, default=1) - - -def upgrade(migrate_engine): - print(__doc__) - metadata = MetaData() - metadata.bind = migrate_engine - metadata.reflect() - - # Add the version column to the history_dataset_association table - try: - hda_table = Table("history_dataset_association", metadata, autoload=True) - version_column.create(hda_table) - assert version_column is hda_table.c.version - except Exception: - log.exception("Adding column 'copied_from_job_id_column' to job table failed.") - - -def downgrade(migrate_engine): - metadata = MetaData() - metadata.bind = migrate_engine - metadata.reflect() - - # Drop the history_dataset_association table's version column. - try: - hda_table = Table("history_dataset_association", metadata, autoload=True) - version_column = hda_table.c.version - version_column.drop() - except Exception: - log.exception("Dropping 'copied_from_job_id_column' column from job table failed.") diff --git a/lib/galaxy/model/migrate/versions/0139_add_history_dataset_association_history_table.py b/lib/galaxy/model/migrate/versions/0139_add_history_dataset_association_history_table.py deleted file mode 100644 index bd55c013a51..00000000000 --- a/lib/galaxy/model/migrate/versions/0139_add_history_dataset_association_history_table.py +++ /dev/null @@ -1,56 +0,0 @@ -""" -Migration script to add the history_dataset_association_history table. -""" - -import datetime -import logging - -from sqlalchemy import ( - Column, - DateTime, - ForeignKey, - Integer, - MetaData, - Table, -) - -from galaxy.model.custom_types import ( - MetadataType, - TrimmedString, -) -from galaxy.model.migrate.versions.util import ( - create_table, - drop_table, -) - -log = logging.getLogger(__name__) -now = datetime.datetime.utcnow -metadata = MetaData() - -HistoryDatasetAssociationHistory_table = Table( - "history_dataset_association_history", - metadata, - Column("id", Integer, primary_key=True), - Column("history_dataset_association_id", Integer, ForeignKey("history_dataset_association.id"), index=True), - Column("update_time", DateTime, default=now), - Column("version", Integer, index=True), - Column("name", TrimmedString(255)), - Column("extension", TrimmedString(64)), - Column("metadata", MetadataType, key="_metadata"), - Column("extended_metadata_id", Integer, ForeignKey("extended_metadata.id"), index=True), -) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - create_table(HistoryDatasetAssociationHistory_table) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - drop_table(HistoryDatasetAssociationHistory_table) diff --git a/lib/galaxy/model/migrate/versions/0140_add_dataset_version_to_job_to_input_dataset_association_table.py b/lib/galaxy/model/migrate/versions/0140_add_dataset_version_to_job_to_input_dataset_association_table.py deleted file mode 100644 index d02febf1784..00000000000 --- a/lib/galaxy/model/migrate/versions/0140_add_dataset_version_to_job_to_input_dataset_association_table.py +++ /dev/null @@ -1,44 +0,0 @@ -""" -Add dataset_version column to job_to_input_dataset table -""" - -import logging - -from sqlalchemy import ( - Column, - Integer, - MetaData, - Table, -) - -log = logging.getLogger(__name__) -dataset_version_column = Column("dataset_version", Integer) - - -def upgrade(migrate_engine): - print(__doc__) - metadata = MetaData() - metadata.bind = migrate_engine - metadata.reflect() - - # Add the version column to the job_to_input_dataset table - try: - job_to_input_dataset_table = Table("job_to_input_dataset", metadata, autoload=True) - dataset_version_column.create(job_to_input_dataset_table) - assert dataset_version_column is job_to_input_dataset_table.c.dataset_version - except Exception: - log.exception("Adding column 'dataset_history_id' to job_to_input_dataset table failed.") - - -def downgrade(migrate_engine): - metadata = MetaData() - metadata.bind = migrate_engine - metadata.reflect() - - # Drop the job_to_input_dataset table's version column. - try: - job_to_input_dataset_table = Table("job_to_input_dataset", metadata, autoload=True) - dataset_version_column = job_to_input_dataset_table.c.dataset_version - dataset_version_column.drop() - except Exception: - log.exception("Dropping 'dataset_version' column from job_to_input_dataset table failed.") diff --git a/lib/galaxy/model/migrate/versions/0141_add_oidc_tables.py b/lib/galaxy/model/migrate/versions/0141_add_oidc_tables.py deleted file mode 100644 index 0bdc1a0cbe7..00000000000 --- a/lib/galaxy/model/migrate/versions/0141_add_oidc_tables.py +++ /dev/null @@ -1,104 +0,0 @@ -""" -Migration script to add a new tables for an OpenID Connect authentication and authorization. -""" - -import logging - -from sqlalchemy import ( - Column, - ForeignKey, - Integer, - MetaData, - Table, - TEXT, - VARCHAR, -) - -from galaxy.model.custom_types import JSONType - -log = logging.getLogger(__name__) -metadata = MetaData() - -psa_association = Table( - "psa_association", - metadata, - Column("id", Integer, primary_key=True), - Column("server_url", VARCHAR(255)), - Column("handle", VARCHAR(255)), - Column("secret", VARCHAR(255)), - Column("issued", Integer), - Column("lifetime", Integer), - Column("assoc_type", VARCHAR(64)), -) - - -psa_code = Table( - "psa_code", - metadata, - Column("id", Integer, primary_key=True), - Column("email", VARCHAR(200)), - Column("code", VARCHAR(32)), -) - - -psa_nonce = Table( - "psa_nonce", - metadata, - Column("id", Integer, primary_key=True), - Column("server_url", VARCHAR(255)), - Column("timestamp", Integer), - Column("salt", VARCHAR(40)), -) - - -psa_partial = Table( - "psa_partial", - metadata, - Column("id", Integer, primary_key=True), - Column("token", VARCHAR(32)), - Column("data", TEXT), - Column("next_step", Integer), - Column("backend", VARCHAR(32)), -) - - -oidc_user_authnz_tokens = Table( - "oidc_user_authnz_tokens", - metadata, - Column("id", Integer, primary_key=True), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True), - Column("uid", VARCHAR(255)), - Column("provider", VARCHAR(32)), - Column("extra_data", JSONType, nullable=True), - Column("lifetime", Integer), - Column("assoc_type", VARCHAR(64)), -) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - try: - psa_association.create() - psa_code.create() - psa_nonce.create() - psa_partial.create() - oidc_user_authnz_tokens.create() - except Exception: - log.exception("Creating OIDC table failed") - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - try: - psa_association.drop() - psa_code.drop() - psa_nonce.drop() - psa_partial.drop() - oidc_user_authnz_tokens.drop() - except Exception: - log.exception("Dropping OIDC table failed") diff --git a/lib/galaxy/model/migrate/versions/0142_change_numeric_metric_precision.py b/lib/galaxy/model/migrate/versions/0142_change_numeric_metric_precision.py deleted file mode 100644 index 4941abd8404..00000000000 --- a/lib/galaxy/model/migrate/versions/0142_change_numeric_metric_precision.py +++ /dev/null @@ -1,33 +0,0 @@ -""" -Migration script to change the 'value' column of 'user_preference' table from numeric(22, 7) to numeric(26, 7) -""" - -import logging - -from sqlalchemy import ( - MetaData, - Numeric, - Table, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - try: - t = Table("job_metric_numeric", metadata, autoload=True) - t.c.metric_value.alter(type=Numeric(26, 7)) - t = Table("task_metric_numeric", metadata, autoload=True) - t.c.metric_value.alter(type=Numeric(26, 7)) - except Exception: - log.exception("Modifying numeric column failed") - - -def downgrade(migrate_engine): - # truncating columns would require truncating data in those columns, so it's best not to downgrade them - pass diff --git a/lib/galaxy/model/migrate/versions/0143_add_cloudauthz_tables.py b/lib/galaxy/model/migrate/versions/0143_add_cloudauthz_tables.py deleted file mode 100644 index fc5b3ae3750..00000000000 --- a/lib/galaxy/model/migrate/versions/0143_add_cloudauthz_tables.py +++ /dev/null @@ -1,56 +0,0 @@ -""" -Migration script to add a new tables for CloudAuthz (tokens required to access cloud-based resources). -""" - -import logging - -from sqlalchemy import ( - Column, - DateTime, - ForeignKey, - Integer, - MetaData, - String, - Table, - TEXT, -) - -from galaxy.model.custom_types import JSONType - -log = logging.getLogger(__name__) -metadata = MetaData() - -cloudauthz = Table( - "cloudauthz", - metadata, - Column("id", Integer, primary_key=True), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True), - Column("provider", String(255)), - Column("config", JSONType), - Column("authn_id", Integer, ForeignKey("oidc_user_authnz_tokens.id"), index=True), - Column("tokens", JSONType), - Column("last_update", DateTime), - Column("last_activity", DateTime), - Column("description", TEXT), -) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - try: - cloudauthz.create() - except Exception: - log.exception("Failed to create cloudauthz table") - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - try: - cloudauthz.drop() - except Exception: - log.exception("Failed to drop cloudauthz table") diff --git a/lib/galaxy/model/migrate/versions/0144_add_cleanup_event_user_table.py b/lib/galaxy/model/migrate/versions/0144_add_cleanup_event_user_table.py deleted file mode 100644 index 0e3facdbc26..00000000000 --- a/lib/galaxy/model/migrate/versions/0144_add_cleanup_event_user_table.py +++ /dev/null @@ -1,49 +0,0 @@ -""" -Migration script to add the cleanup_event_user_association table. -""" - -import datetime -import logging - -from sqlalchemy import ( - Column, - DateTime, - ForeignKey, - Integer, - MetaData, - Table, -) - -from galaxy.model.migrate.versions.util import ( - create_table, - drop_table, -) - -now = datetime.datetime.utcnow -log = logging.getLogger(__name__) -metadata = MetaData() - -# New table to log cleanup events -CleanupEventUserAssociation_table = Table( - "cleanup_event_user_association", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("cleanup_event_id", Integer, ForeignKey("cleanup_event.id"), index=True, nullable=True), - Column("user_id", Integer, ForeignKey("galaxy_user.id"), index=True), -) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - create_table(CleanupEventUserAssociation_table) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - drop_table(CleanupEventUserAssociation_table) diff --git a/lib/galaxy/model/migrate/versions/0145_add_workflow_step_input.py b/lib/galaxy/model/migrate/versions/0145_add_workflow_step_input.py deleted file mode 100644 index bbcfd21b98d..00000000000 --- a/lib/galaxy/model/migrate/versions/0145_add_workflow_step_input.py +++ /dev/null @@ -1,151 +0,0 @@ -""" -Migration script for workflow step input table. -""" - -import logging - -from migrate import ForeignKeyConstraint as MigrateForeignKeyConstraint -from sqlalchemy import ( - Boolean, - Column, - ForeignKey, - Index, - Integer, - MetaData, - Table, - TEXT, -) - -from galaxy.model.custom_types import JSONType -from galaxy.model.migrate.versions.util import ( - create_table, - drop_index, - drop_table, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - -WorkflowStepInput_table = Table( - "workflow_step_input", - metadata, - Column("id", Integer, primary_key=True), - Column("workflow_step_id", Integer, ForeignKey("workflow_step.id"), index=True), - Column("name", TEXT), - Column("merge_type", TEXT), - Column("scatter_type", TEXT), - Column("value_from", JSONType), - Column("value_from_type", TEXT), - Column("default_value", JSONType), - Column("default_value_set", Boolean, default=False), - Column("runtime_value", Boolean, default=False), - Index( - "ix_workflow_step_input_workflow_step_id_name_unique", - "workflow_step_id", - "name", - unique=True, - mysql_length={"name": 200}, - ), -) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - OldWorkflowStepConnection_table = Table("workflow_step_connection", metadata, autoload=True) - for fkc in OldWorkflowStepConnection_table.foreign_key_constraints: - mfkc = MigrateForeignKeyConstraint( - [_.parent for _ in fkc.elements], [_.column for _ in fkc.elements], name=fkc.name - ) - try: - mfkc.drop() - except Exception: - log.exception( - "Dropping foreign key constraint '%s' from table '%s' failed", - mfkc.name, - OldWorkflowStepConnection_table, - ) - - for index in OldWorkflowStepConnection_table.indexes: - drop_index(index, OldWorkflowStepConnection_table) - OldWorkflowStepConnection_table.rename("workflow_step_connection_preupgrade145") - # Try to deregister that table to work around some caching problems it seems. - OldWorkflowStepConnection_table.deregister() - metadata._remove_table("workflow_step_connection", metadata.schema) - metadata.reflect() - - NewWorkflowStepConnection_table = Table( - "workflow_step_connection", - metadata, - Column("id", Integer, primary_key=True), - Column("output_step_id", Integer, ForeignKey("workflow_step.id"), index=True), - Column("input_step_input_id", Integer, ForeignKey("workflow_step_input.id"), index=True), - Column("output_name", TEXT), - Column("input_subworkflow_step_id", Integer, ForeignKey("workflow_step.id"), index=True), - ) - for table in (WorkflowStepInput_table, NewWorkflowStepConnection_table): - create_table(table) - - insert_step_inputs_cmd = ( - "INSERT INTO workflow_step_input (workflow_step_id, name) " - + "SELECT DISTINCT input_step_id, input_name FROM workflow_step_connection_preupgrade145" - ) - migrate_engine.execute(insert_step_inputs_cmd) - - insert_step_connections_cmd = ( - "INSERT INTO workflow_step_connection (output_step_id, input_step_input_id, output_name, input_subworkflow_step_id) " - + "SELECT wsc.output_step_id, wsi.id, wsc.output_name, wsc.input_subworkflow_step_id " - + "FROM workflow_step_connection_preupgrade145 AS wsc JOIN workflow_step_input AS wsi ON wsc.input_step_id = wsi.workflow_step_id AND wsc.input_name = wsi.name ORDER BY wsc.id" - ) - migrate_engine.execute(insert_step_connections_cmd) - drop_table(OldWorkflowStepConnection_table) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - - NewWorkflowStepConnection_table = Table("workflow_step_connection", metadata, autoload=True) - for fkc in NewWorkflowStepConnection_table.foreign_key_constraints: - mfkc = MigrateForeignKeyConstraint( - [_.parent for _ in fkc.elements], [_.column for _ in fkc.elements], name=fkc.name - ) - try: - mfkc.drop() - except Exception: - log.exception( - "Dropping foreign key constraint '%s' from table '%s' failed", - mfkc.name, - NewWorkflowStepConnection_table, - ) - - for index in NewWorkflowStepConnection_table.indexes: - drop_index(index, NewWorkflowStepConnection_table) - NewWorkflowStepConnection_table.rename("workflow_step_connection_predowngrade145") - # Try to deregister that table to work around some caching problems it seems. - NewWorkflowStepConnection_table.deregister() - metadata._remove_table("workflow_step_connection", metadata.schema) - metadata.reflect() - - OldWorkflowStepConnection_table = Table( - "workflow_step_connection", - metadata, - Column("id", Integer, primary_key=True), - Column("output_step_id", Integer, ForeignKey("workflow_step.id"), index=True), - Column("input_step_id", Integer, ForeignKey("workflow_step.id"), index=True), - Column("output_name", TEXT), - Column("input_name", TEXT), - Column("input_subworkflow_step_id", Integer, ForeignKey("workflow_step.id"), index=True), - ) - create_table(OldWorkflowStepConnection_table) - - insert_step_connections_cmd = ( - "INSERT INTO workflow_step_connection (output_step_id, input_step_id, output_name, input_name, input_subworkflow_step_id) " - + "SELECT wsc.output_step_id, wsi.workflow_step_id, wsc.output_name, wsi.name, wsc.input_subworkflow_step_id " - + "FROM workflow_step_connection_predowngrade145 AS wsc JOIN workflow_step_input AS wsi ON wsc.input_step_input_id = wsi.id ORDER BY wsc.id" - ) - migrate_engine.execute(insert_step_connections_cmd) - - for table in (NewWorkflowStepConnection_table, WorkflowStepInput_table): - drop_table(table) diff --git a/lib/galaxy/model/migrate/versions/0146_workflow_paths.py b/lib/galaxy/model/migrate/versions/0146_workflow_paths.py deleted file mode 100644 index 5c267337377..00000000000 --- a/lib/galaxy/model/migrate/versions/0146_workflow_paths.py +++ /dev/null @@ -1,34 +0,0 @@ -""" -Migration script for workflow paths. -""" - -import logging - -from sqlalchemy import ( - Column, - MetaData, - TEXT, -) - -from galaxy.model.migrate.versions.util import ( - add_column, - drop_column, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - from_path_column = Column("from_path", TEXT) - add_column(from_path_column, "stored_workflow", metadata) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - - drop_column("from_path", "stored_workflow", metadata) diff --git a/lib/galaxy/model/migrate/versions/0147_job_messages.py b/lib/galaxy/model/migrate/versions/0147_job_messages.py deleted file mode 100644 index 74ea07dfe74..00000000000 --- a/lib/galaxy/model/migrate/versions/0147_job_messages.py +++ /dev/null @@ -1,62 +0,0 @@ -""" -Add structured failure reason column to jobs table -""" - -import logging - -from sqlalchemy import ( - Column, - MetaData, - Table, - TEXT, -) - -from galaxy.model.custom_types import JSONType -from galaxy.model.migrate.versions.util import ( - add_column, - alter_column, - drop_column, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - jobs_table = Table("job", metadata, autoload=True) - job_messages_column = Column("job_messages", JSONType, nullable=True) - add_column(job_messages_column, jobs_table, metadata) - job_job_stdout_column = Column("job_stdout", TEXT, nullable=True) - add_column(job_job_stdout_column, jobs_table, metadata) - job_job_stderr_column = Column("job_stderr", TEXT, nullable=True) - add_column(job_job_stderr_column, jobs_table, metadata) - - tasks_table = Table("task", metadata, autoload=True) - task_job_messages_column = Column("job_messages", JSONType, nullable=True) - add_column(task_job_messages_column, tasks_table, metadata) - task_job_stdout_column = Column("job_stdout", TEXT, nullable=True) - add_column(task_job_stdout_column, tasks_table, metadata) - task_job_stderr_column = Column("job_stderr", TEXT, nullable=True) - add_column(task_job_stderr_column, tasks_table, metadata) - - for table in [jobs_table, tasks_table]: - alter_column("stdout", table, name="tool_stdout") - alter_column("stderr", table, name="tool_stderr") - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - jobs_table = Table("job", metadata, autoload=True) - tasks_table = Table("task", metadata, autoload=True) - for colname in ["job_messages", "job_stdout", "job_stderr"]: - drop_column(colname, jobs_table) - drop_column(colname, tasks_table) - for table in [jobs_table, tasks_table]: - alter_column("tool_stdout", table, name="stdout") - alter_column("tool_stderr", table, name="stderr") diff --git a/lib/galaxy/model/migrate/versions/0148_add_checksum_table.py b/lib/galaxy/model/migrate/versions/0148_add_checksum_table.py deleted file mode 100644 index 7d865633f62..00000000000 --- a/lib/galaxy/model/migrate/versions/0148_add_checksum_table.py +++ /dev/null @@ -1,69 +0,0 @@ -""" -Migration script to add dataset source and hash tables. -""" - -import logging - -from sqlalchemy import ( - Column, - ForeignKey, - Integer, - MetaData, - Table, - TEXT, -) - -from galaxy.model.custom_types import JSONType -from galaxy.model.migrate.versions.util import ( - create_table, - drop_table, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - -dataset_source_table = Table( - "dataset_source", - metadata, - Column("id", Integer, primary_key=True), - Column("dataset_id", Integer, ForeignKey("dataset.id"), index=True), - Column("source_uri", TEXT), - Column("extra_files_path", TEXT), - Column("transform", JSONType), -) - -dataset_hash_table = Table( - "dataset_hash", - metadata, - Column("id", Integer, primary_key=True), - Column("dataset_id", Integer, ForeignKey("dataset.id"), index=True), - Column("hash_function", TEXT), - Column("hash_value", TEXT), - Column("extra_files_path", TEXT), -) - -dataset_source_hash_table = Table( - "dataset_source_hash", - metadata, - Column("id", Integer, primary_key=True), - Column("dataset_source_id", Integer, ForeignKey("dataset_source.id"), index=True), - Column("hash_function", TEXT), - Column("hash_value", TEXT), -) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - create_table(dataset_source_table) - create_table(dataset_hash_table) - create_table(dataset_source_hash_table) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - drop_table(dataset_source_hash_table) - drop_table(dataset_hash_table) - drop_table(dataset_source_table) diff --git a/lib/galaxy/model/migrate/versions/0149_dynamic_tools.py b/lib/galaxy/model/migrate/versions/0149_dynamic_tools.py deleted file mode 100644 index e1d4006fe58..00000000000 --- a/lib/galaxy/model/migrate/versions/0149_dynamic_tools.py +++ /dev/null @@ -1,73 +0,0 @@ -""" -Migration script to add the dynamic_tool table. -""" -import datetime -import logging - -from sqlalchemy import ( - Boolean, - Column, - DateTime, - ForeignKey, - Integer, - MetaData, - Table, - Unicode, -) - -from galaxy.model.custom_types import ( - JSONType, - UUIDType, -) -from galaxy.model.migrate.versions.util import ( - add_column, - create_table, - drop_column, - drop_table, -) - -log = logging.getLogger(__name__) -now = datetime.datetime.utcnow -metadata = MetaData() - - -DynamicTool_table = Table( - "dynamic_tool", - metadata, - Column("id", Integer, primary_key=True), - Column("uuid", UUIDType()), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("tool_id", Unicode(255)), - Column("tool_version", Unicode(255)), - Column("tool_format", Unicode(255)), - Column("tool_path", Unicode(255)), - Column("tool_directory", Unicode(255)), - Column("hidden", Boolean), - Column("active", Boolean), - Column("value", JSONType), -) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - create_table(DynamicTool_table) - - workflow_dynamic_tool_id_column = Column("dynamic_tool_id", Integer, ForeignKey("dynamic_tool.id"), nullable=True) - add_column(workflow_dynamic_tool_id_column, "workflow_step", metadata) - job_workflow_dynamic_tool_id_column = Column( - "dynamic_tool_id", Integer, ForeignKey("dynamic_tool.id"), nullable=True - ) - add_column(job_workflow_dynamic_tool_id_column, "job", metadata) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - drop_column("dynamic_tool_id", "workflow_step", metadata) - drop_column("dynamic_tool_id", "job", metadata) - drop_table(DynamicTool_table) diff --git a/lib/galaxy/model/migrate/versions/0150_add_create_time_field_for_cloudauthz.py b/lib/galaxy/model/migrate/versions/0150_add_create_time_field_for_cloudauthz.py deleted file mode 100644 index 60fed37e4a9..00000000000 --- a/lib/galaxy/model/migrate/versions/0150_add_create_time_field_for_cloudauthz.py +++ /dev/null @@ -1,39 +0,0 @@ -""" -Adds `create_time` columns to cloudauthz table. -""" - - -import logging - -from sqlalchemy import ( - Column, - DateTime, - MetaData, - Table, -) - -from galaxy.model.migrate.versions.util import ( - add_column, - drop_column, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - cloudauthz_table = Table("cloudauthz", metadata, autoload=True) - create_time_column = Column("create_time", DateTime) - add_column(create_time_column, cloudauthz_table, metadata) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - cloudauthz_table = Table("cloudauthz", metadata, autoload=True) - drop_column("create_time", cloudauthz_table) diff --git a/lib/galaxy/model/migrate/versions/0151_add_worker_process.py b/lib/galaxy/model/migrate/versions/0151_add_worker_process.py deleted file mode 100644 index 4a024ebbe61..00000000000 --- a/lib/galaxy/model/migrate/versions/0151_add_worker_process.py +++ /dev/null @@ -1,50 +0,0 @@ -""" -Add table for worker processes -""" - -import logging - -from sqlalchemy import ( - Column, - DateTime, - Integer, - MetaData, - String, - Table, - UniqueConstraint, -) - -from galaxy.model.migrate.versions.util import ( - create_table, - drop_table, -) -from galaxy.model.orm.now import now - -log = logging.getLogger(__name__) -metadata = MetaData() - - -WorkerProcess_table = Table( - "worker_process", - metadata, - Column("id", Integer, primary_key=True), - Column("server_name", String(255), index=True), - Column("hostname", String(255)), - Column("update_time", DateTime, default=now, onupdate=now), - UniqueConstraint("server_name", "hostname"), -) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - create_table(WorkerProcess_table) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - drop_table(WorkerProcess_table) diff --git a/lib/galaxy/model/migrate/versions/0152_add_metadata_file_uuid.py b/lib/galaxy/model/migrate/versions/0152_add_metadata_file_uuid.py deleted file mode 100644 index 3e2e3c3ee73..00000000000 --- a/lib/galaxy/model/migrate/versions/0152_add_metadata_file_uuid.py +++ /dev/null @@ -1,37 +0,0 @@ -""" -Adds `uuid` column to MetadataFile table. -""" - - -import logging - -from sqlalchemy import ( - Column, - MetaData, -) - -from galaxy.model.custom_types import UUIDType -from galaxy.model.migrate.versions.util import ( - add_column, - drop_column, -) - -log = logging.getLogger(__name__) - - -def upgrade(migrate_engine): - print(__doc__) - metadata = MetaData() - metadata.bind = migrate_engine - metadata.reflect() - - uuid_column = Column("uuid", UUIDType()) - add_column(uuid_column, "metadata_file", metadata) - - -def downgrade(migrate_engine): - metadata = MetaData() - metadata.bind = migrate_engine - metadata.reflect() - - drop_column("uuid", "metadata_file", metadata) diff --git a/lib/galaxy/model/migrate/versions/0153_add_custos_authnz_token_table.py b/lib/galaxy/model/migrate/versions/0153_add_custos_authnz_token_table.py deleted file mode 100644 index fa6de685a00..00000000000 --- a/lib/galaxy/model/migrate/versions/0153_add_custos_authnz_token_table.py +++ /dev/null @@ -1,56 +0,0 @@ -""" -Migration for adding custos_authnz_token table. -""" - -import logging - -from sqlalchemy import ( - Column, - DateTime, - ForeignKey, - Integer, - MetaData, - String, - Table, - Text, - UniqueConstraint, -) - -from galaxy.model.migrate.versions.util import ( - create_table, - drop_table, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - -CustosAuthnzToken_table = Table( - "custos_authnz_token", - metadata, - Column("id", Integer, primary_key=True), - Column("user_id", Integer, ForeignKey("galaxy_user.id")), - Column("external_user_id", String(64)), - Column("provider", String(255)), - Column("access_token", Text), - Column("id_token", Text), - Column("refresh_token", Text), - Column("expiration_time", DateTime), - Column("refresh_expiration_time", DateTime), - UniqueConstraint("user_id", "external_user_id", "provider"), - UniqueConstraint("external_user_id", "provider"), -) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - create_table(CustosAuthnzToken_table) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - drop_table(CustosAuthnzToken_table) diff --git a/lib/galaxy/model/migrate/versions/0154_created_from_basename.py b/lib/galaxy/model/migrate/versions/0154_created_from_basename.py deleted file mode 100644 index d998d45adf6..00000000000 --- a/lib/galaxy/model/migrate/versions/0154_created_from_basename.py +++ /dev/null @@ -1,36 +0,0 @@ -""" -Adds created_from_basename to dataset. -""" -import datetime -import logging - -from sqlalchemy import ( - Column, - MetaData, - TEXT, -) - -from galaxy.model.migrate.versions.util import ( - add_column, - drop_column, -) - -log = logging.getLogger(__name__) -now = datetime.datetime.utcnow -metadata = MetaData() - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - created_from_basename_column = Column("created_from_basename", TEXT, default=None) - add_column(created_from_basename_column, "dataset", metadata) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - drop_column("created_from_basename", "dataset", metadata) diff --git a/lib/galaxy/model/migrate/versions/0155_job_galaxy_version.py b/lib/galaxy/model/migrate/versions/0155_job_galaxy_version.py deleted file mode 100644 index 78b81fcf0c2..00000000000 --- a/lib/galaxy/model/migrate/versions/0155_job_galaxy_version.py +++ /dev/null @@ -1,37 +0,0 @@ -""" -Add 'galaxy_version' attribute to Job table. -""" - -import datetime -import logging - -from sqlalchemy import ( - Column, - MetaData, - String, -) - -from galaxy.model.migrate.versions.util import ( - add_column, - drop_column, -) - -log = logging.getLogger(__name__) -now = datetime.datetime.utcnow -metadata = MetaData() - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - created_from_basename_column = Column("galaxy_version", String(64), default=None) - add_column(created_from_basename_column, "job", metadata) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - drop_column("galaxy_version", "job", metadata) diff --git a/lib/galaxy/model/migrate/versions/0156_add_interactivetools.py b/lib/galaxy/model/migrate/versions/0156_add_interactivetools.py deleted file mode 100644 index 23af62a7978..00000000000 --- a/lib/galaxy/model/migrate/versions/0156_add_interactivetools.py +++ /dev/null @@ -1,84 +0,0 @@ -""" -Migration script to add new tables for InteractiveTools. -""" - -import logging - -from sqlalchemy import ( - Boolean, - Column, - DateTime, - ForeignKey, - Integer, - MetaData, - Table, - TEXT, -) - -from galaxy.model.custom_types import JSONType -from galaxy.model.orm.now import now - -log = logging.getLogger(__name__) -metadata = MetaData() - -interactivetool_entry_point = Table( - "interactivetool_entry_point", - metadata, - Column("id", Integer, primary_key=True), - Column("job_id", Integer, ForeignKey("job.id"), index=True), - Column("name", TEXT), - Column("token", TEXT), - Column("tool_port", Integer), - Column("host", TEXT), - Column("port", Integer), - Column("protocol", TEXT), - Column("entry_url", TEXT), - Column("info", JSONType, nullable=True), - Column("configured", Boolean, default=False), - Column("deleted", Boolean, default=False), - Column("created_time", DateTime, default=now), - Column("modified_time", DateTime, default=now, onupdate=now), -) - -job_container_association = Table( - "job_container_association", - metadata, - Column("id", Integer, primary_key=True), - Column("job_id", Integer, ForeignKey("job.id"), index=True), - Column("container_type", TEXT), - Column("container_name", TEXT), - Column("container_info", JSONType, nullable=True), - Column("created_time", DateTime, default=now), - Column("modified_time", DateTime, default=now, onupdate=now), -) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - try: - job_container_association.create() - except Exception: - log.exception("Failed to create job_container_association table") - - try: - interactivetool_entry_point.create() - except Exception: - log.exception("Failed to create interactivetool_entry_point table") - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - try: - job_container_association.drop() - except Exception: - log.exception("Failed to drop job_container_association table") - - try: - interactivetool_entry_point.drop() - except Exception: - log.exception("Failed to drop interactivetool_entry_point table") diff --git a/lib/galaxy/model/migrate/versions/0157_rework_dataset_validation.py b/lib/galaxy/model/migrate/versions/0157_rework_dataset_validation.py deleted file mode 100644 index 9675975e7c4..00000000000 --- a/lib/galaxy/model/migrate/versions/0157_rework_dataset_validation.py +++ /dev/null @@ -1,67 +0,0 @@ -""" -Rework dataset validation in database. -""" - -import logging - -from sqlalchemy import ( - Column, - ForeignKey, - Integer, - MetaData, - Table, - TEXT, -) - -from galaxy.model.custom_types import TrimmedString -from galaxy.model.migrate.versions.util import ( - add_column, - create_table, - drop_column, - drop_table, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - -validation_error_table = Table( - "validation_error", - metadata, - Column("id", Integer, primary_key=True), - Column("dataset_id", Integer, ForeignKey("history_dataset_association.id"), index=True), - Column("message", TrimmedString(255)), - Column("err_type", TrimmedString(64)), - Column("attributes", TEXT), -) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - drop_table(validation_error_table) - - history_dataset_association_table = Table("history_dataset_association", metadata, autoload=True) - library_dataset_dataset_association_table = Table("library_dataset_dataset_association", metadata, autoload=True) - for dataset_instance_table in [history_dataset_association_table, library_dataset_dataset_association_table]: - validated_state_column = Column( - "validated_state", TrimmedString(64), default="unknown", server_default="unknown", nullable=False - ) - add_column(validated_state_column, dataset_instance_table, metadata) - - validated_state_message_column = Column("validated_state_message", TEXT) - add_column(validated_state_message_column, dataset_instance_table, metadata) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - create_table(validation_error_table) - - history_dataset_association_table = Table("history_dataset_association", metadata, autoload=True) - library_dataset_dataset_association_table = Table("library_dataset_dataset_association", metadata, autoload=True) - for dataset_instance_table in [history_dataset_association_table, library_dataset_dataset_association_table]: - drop_column("validated_state", dataset_instance_table, metadata) - drop_column("validated_state_message", dataset_instance_table, metadata) diff --git a/lib/galaxy/model/migrate/versions/0158_workflow_reports.py b/lib/galaxy/model/migrate/versions/0158_workflow_reports.py deleted file mode 100644 index 212c5368f66..00000000000 --- a/lib/galaxy/model/migrate/versions/0158_workflow_reports.py +++ /dev/null @@ -1,36 +0,0 @@ -""" -Adds reports_config to workflow. -""" -import datetime -import logging - -from sqlalchemy import ( - Column, - MetaData, -) - -from galaxy.model.custom_types import JSONType -from galaxy.model.migrate.versions.util import ( - add_column, - drop_column, -) - -now = datetime.datetime.utcnow -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - metadata.bind = migrate_engine - print(__doc__) - metadata.reflect() - - reports_config_column = Column("reports_config", JSONType, default=None) - add_column(reports_config_column, "workflow", metadata) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - drop_column("reports_config", "workflow", metadata) diff --git a/lib/galaxy/model/migrate/versions/0159_add_job_external_id_index.py b/lib/galaxy/model/migrate/versions/0159_add_job_external_id_index.py deleted file mode 100644 index d68e18ae5fd..00000000000 --- a/lib/galaxy/model/migrate/versions/0159_add_job_external_id_index.py +++ /dev/null @@ -1,28 +0,0 @@ -""" -Add index for job runner external ID. -""" - -import logging - -from sqlalchemy import MetaData - -from galaxy.model.migrate.versions.util import ( - add_index, - drop_index, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - add_index("ix_job_job_runner_external_id", "job", "job_runner_external_id", metadata) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - drop_index("ix_job_job_runner_external_id", "job", "job_runner_external_id", metadata) diff --git a/lib/galaxy/model/migrate/versions/0160_hda_set_deleted_if_purged_again.py b/lib/galaxy/model/migrate/versions/0160_hda_set_deleted_if_purged_again.py deleted file mode 100644 index 7969c9e4c5a..00000000000 --- a/lib/galaxy/model/migrate/versions/0160_hda_set_deleted_if_purged_again.py +++ /dev/null @@ -1,23 +0,0 @@ -""" -Migration script to set the 'deleted' column of the -'history_dataset_association' table to True if 'purged' is True. -""" - -import logging - -from galaxy.model.migrate.versions.util import engine_true - -log = logging.getLogger(__name__) - - -def upgrade(migrate_engine): - print(__doc__) - cmd = f"UPDATE history_dataset_association SET deleted={engine_true(migrate_engine)} WHERE purged AND NOT deleted;" - try: - migrate_engine.execute(cmd) - except Exception: - log.exception("Exception executing SQL command: %s", cmd) - - -def downgrade(migrate_engine): - pass diff --git a/lib/galaxy/model/migrate/versions/0161_add_workflow_invocation_output_table.py b/lib/galaxy/model/migrate/versions/0161_add_workflow_invocation_output_table.py deleted file mode 100644 index c60c103ec9b..00000000000 --- a/lib/galaxy/model/migrate/versions/0161_add_workflow_invocation_output_table.py +++ /dev/null @@ -1,49 +0,0 @@ -""" -Migration script to add a new workflow_invocation_output_parameter table to track output parameters. -""" - -import logging - -from sqlalchemy import ( - Column, - ForeignKey, - Integer, - MetaData, - Table, -) - -from galaxy.model.custom_types import JSONType - -log = logging.getLogger(__name__) -metadata = MetaData() - -workflow_invocation_output_parameter_table = Table( - "workflow_invocation_output_value", - metadata, - Column("id", Integer, primary_key=True), - Column("workflow_invocation_id", Integer, ForeignKey("workflow_invocation.id"), index=True), - Column("workflow_step_id", Integer, ForeignKey("workflow_step.id")), - Column("workflow_output_id", Integer, ForeignKey("workflow_output.id"), index=True), - Column("value", JSONType), -) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - try: - workflow_invocation_output_parameter_table.create() - except Exception: - log.exception("Creating workflow_invocation_output_parameter table failed") - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - try: - workflow_invocation_output_parameter_table.drop() - except Exception: - log.exception("Dropping workflow_invocation_output_parameter table failed") diff --git a/lib/galaxy/model/migrate/versions/0162_job_only_pjas.py b/lib/galaxy/model/migrate/versions/0162_job_only_pjas.py deleted file mode 100644 index 97dcdc3b161..00000000000 --- a/lib/galaxy/model/migrate/versions/0162_job_only_pjas.py +++ /dev/null @@ -1,31 +0,0 @@ -""" -Migration script to allow null workflow_step for PostJobActions. -This enables using PJAs with individual job executions. -""" - -import logging - -from sqlalchemy import MetaData - -from galaxy.model.migrate.versions.util import alter_column - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - # Make workflow_step_id nullable to allow for PJAs to be created for - # individual jobs. - alter_column("workflow_step_id", "post_job_action", metadata, nullable=True) - - -def downgrade(migrate_engine): - # This is not a reversible migration, because post-migrate we may introduce - # null values to the column which cannot later be easily 'fixed'. They - # should not cause any issue to simply ignore, though -- I don't think - # there was really a great reason this was non-nullable when I first wrote it. - pass diff --git a/lib/galaxy/model/migrate/versions/0163_worker_process_pid.py b/lib/galaxy/model/migrate/versions/0163_worker_process_pid.py deleted file mode 100644 index 7ae8578cba3..00000000000 --- a/lib/galaxy/model/migrate/versions/0163_worker_process_pid.py +++ /dev/null @@ -1,37 +0,0 @@ -""" -Adds `pid` column to worker_process table. -""" - - -import logging - -from sqlalchemy import ( - Column, - Integer, - MetaData, -) - -from galaxy.model.migrate.versions.util import ( - add_column, - drop_column, -) - -log = logging.getLogger(__name__) - - -def upgrade(migrate_engine): - print(__doc__) - metadata = MetaData() - metadata.bind = migrate_engine - metadata.reflect() - - pid_column = Column("pid", Integer) - add_column(pid_column, "worker_process", metadata) - - -def downgrade(migrate_engine): - metadata = MetaData() - metadata.bind = migrate_engine - metadata.reflect() - - drop_column("pid", "worker_process", metadata) diff --git a/lib/galaxy/model/migrate/versions/0164_page_format.py b/lib/galaxy/model/migrate/versions/0164_page_format.py deleted file mode 100644 index 5c791732e7e..00000000000 --- a/lib/galaxy/model/migrate/versions/0164_page_format.py +++ /dev/null @@ -1,38 +0,0 @@ -""" -Adds page content format. -""" -import datetime -import logging - -from sqlalchemy import ( - Column, - MetaData, -) - -from galaxy.model.custom_types import TrimmedString -from galaxy.model.migrate.versions.util import ( - add_column, - drop_column, -) - -now = datetime.datetime.utcnow -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - metadata.bind = migrate_engine - print(__doc__) - metadata.reflect() - - content_format_column = Column( - "content_format", TrimmedString(32), default="html", server_default="html", nullable=False - ) - add_column(content_format_column, "page_revision", metadata) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - drop_column("content_format", "page_revision", metadata) diff --git a/lib/galaxy/model/migrate/versions/0165_add_content_update_time.py b/lib/galaxy/model/migrate/versions/0165_add_content_update_time.py deleted file mode 100644 index cb369939ef3..00000000000 --- a/lib/galaxy/model/migrate/versions/0165_add_content_update_time.py +++ /dev/null @@ -1,57 +0,0 @@ -""" -Adds timestamps to hdca table. Adds triggers to dataset, hda, hdca tables -to update history.update_time when contents are changed. -""" - -import logging - -from sqlalchemy import ( - Column, - DateTime, - MetaData, - Table, -) - -from galaxy.model.migrate.triggers.history_update_time_field import ( - drop_timestamp_triggers, - install_timestamp_triggers, -) -from galaxy.model.migrate.versions.util import ( - add_column, - drop_column, -) -from galaxy.model.orm.now import now - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - create_timestamps(metadata, "history_dataset_collection_association") - install_timestamp_triggers(migrate_engine) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - drop_timestamp_triggers(migrate_engine) - drop_timestamps(metadata, "history_dataset_collection_association") - - -def create_timestamps(metadata, table_name): - target_table = Table(table_name, metadata, autoload=True) - if "create_time" not in target_table.c: - create_time_column = Column("create_time", DateTime, default=now) - add_column(create_time_column, target_table, metadata) - if "update_time" not in target_table.c: - update_time_column = Column("update_time", DateTime, default=now, onupdate=now) - add_column(update_time_column, target_table, metadata) - - -def drop_timestamps(metadata, table_name): - target_table = Table(table_name, metadata, autoload=True) - drop_column("create_time", target_table) - drop_column("update_time", target_table) diff --git a/lib/galaxy/model/migrate/versions/0166_job_state_summary_view.py b/lib/galaxy/model/migrate/versions/0166_job_state_summary_view.py deleted file mode 100644 index 40b986408c6..00000000000 --- a/lib/galaxy/model/migrate/versions/0166_job_state_summary_view.py +++ /dev/null @@ -1,30 +0,0 @@ -""" -Job state trigger syncs update_time in hdca table. -Add job-state-summary view for hdca elements -""" - -import logging - -from galaxy.model.view import HistoryDatasetCollectionJobStateSummary -from galaxy.model.view.utils import ( - CreateView, - DropView, -) - -log = logging.getLogger(__name__) - - -def upgrade(migrate_engine): - print(__doc__) - # drop first because sqlite does not support or_replace - downgrade(migrate_engine) - view = HistoryDatasetCollectionJobStateSummary - create_view = CreateView(view.name, view.__view__) - # print(str(create_view.compile(migrate_engine))) - migrate_engine.execute(create_view) - - -def downgrade(migrate_engine): - drop_view = DropView(HistoryDatasetCollectionJobStateSummary.name) - # print(str(drop_view.compile(migrate_engine))) - migrate_engine.execute(drop_view) diff --git a/lib/galaxy/model/migrate/versions/0167_add_job_to_input_dataset_collection_element_association.py b/lib/galaxy/model/migrate/versions/0167_add_job_to_input_dataset_collection_element_association.py deleted file mode 100644 index 681e1030959..00000000000 --- a/lib/galaxy/model/migrate/versions/0167_add_job_to_input_dataset_collection_element_association.py +++ /dev/null @@ -1,47 +0,0 @@ -""" -Migration script to add a new job_to_input_dataset_collection_element table to track job inputs. -""" - -import logging - -from sqlalchemy import ( - Column, - ForeignKey, - Integer, - MetaData, - Table, - Unicode, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - -job_to_input_dataset_collection_element_table = Table( - "job_to_input_dataset_collection_element", - metadata, - Column("id", Integer, primary_key=True), - Column("job_id", Integer, ForeignKey("job.id"), index=True), - Column("dataset_collection_element_id", Integer, ForeignKey("dataset_collection_element.id"), index=True), - Column("name", Unicode(255)), -) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - try: - job_to_input_dataset_collection_element_table.create() - except Exception: - log.exception("Creating job_to_input_dataset_collection_element table failed") - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - try: - job_to_input_dataset_collection_element_table.drop() - except Exception: - log.exception("Dropping job_to_input_dataset_collection_element table failed") diff --git a/lib/galaxy/model/migrate/versions/0168_stored_workflow_hidden_col.py b/lib/galaxy/model/migrate/versions/0168_stored_workflow_hidden_col.py deleted file mode 100644 index 5405c49bd90..00000000000 --- a/lib/galaxy/model/migrate/versions/0168_stored_workflow_hidden_col.py +++ /dev/null @@ -1,37 +0,0 @@ -""" -Migration script to add a 'hidden' column to the 'StoredWorkflow' table. -""" - -import logging - -from sqlalchemy import ( - Boolean, - Column, - MetaData, -) - -from galaxy.model.migrate.versions.util import ( - add_column, - drop_column, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - -# Column to add. -hidden_col = Column("hidden", Boolean, default=False) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - add_column(hidden_col, "stored_workflow", metadata) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - drop_column("hidden", "stored_workflow", metadata) diff --git a/lib/galaxy/model/migrate/versions/0169_add_missing_indexes.py b/lib/galaxy/model/migrate/versions/0169_add_missing_indexes.py deleted file mode 100644 index b3da46c043e..00000000000 --- a/lib/galaxy/model/migrate/versions/0169_add_missing_indexes.py +++ /dev/null @@ -1,66 +0,0 @@ -""" -Migration script to create missing indexes. Adding new columns to existing tables via SQLAlchemy does not create the index, even if the column definition includes index=True. -""" - -import logging - -from sqlalchemy import MetaData - -from galaxy.model.migrate.versions.util import ( - add_index, - drop_index, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - -indexes = [ - ["ix_galaxy_user_activation_token", "galaxy_user", "activation_token"], - ["ix_workflow_step_dynamic_tool_id", "workflow_step", "dynamic_tool_id"], - ["ix_history_dataset_association_version", "history_dataset_association", "version"], - ["ix_workflow_invocation_scheduler", "workflow_invocation", "scheduler"], - ["ix_page_slug", "page", "slug"], - ["ix_workflow_invocation_state", "workflow_invocation", "state"], - [ - "ix_history_dataset_collection_association_implicit_collection_jobs_id", - "history_dataset_collection_association", - "implicit_collection_jobs_id", - ], - ["ix_workflow_step_subworkflow_id", "workflow_step", "subworkflow_id"], - ["ix_dynamic_tool_update_time", "dynamic_tool", "update_time"], - [ - "ix_library_dataset_dataset_association_extended_metadata_id", - "library_dataset_dataset_association", - "extended_metadata_id", - ], - [ - "ix_workflow_invocation_step_implicit_collection_jobs_id", - "workflow_invocation_step", - "implicit_collection_jobs_id", - ], - ["ix_workflow_invocation_step_state", "workflow_invocation_step", "state"], - ["ix_workflow_invocation_history_id", "workflow_invocation", "history_id"], - ["ix_workflow_parent_workflow_id", "workflow", "parent_workflow_id"], - ["ix_metadata_file_uuid", "metadata_file", "uuid"], - ["ix_history_dataset_collection_association_job_id", "history_dataset_collection_association", "job_id"], - ["ix_galaxy_user_active", "galaxy_user", "active"], - ["ix_job_dynamic_tool_id", "job", "dynamic_tool_id"], - ["ix_history_dataset_association_extended_metadata_id", "history_dataset_association", "extended_metadata_id"], -] - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - for ix, table, col in indexes: - add_index(ix, table, col, metadata) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - for ix, table, col in indexes: - drop_index(ix, table, col, metadata) diff --git a/lib/galaxy/model/migrate/versions/0170_add_more_missing_indexes.py b/lib/galaxy/model/migrate/versions/0170_add_more_missing_indexes.py deleted file mode 100644 index f2e8f331205..00000000000 --- a/lib/galaxy/model/migrate/versions/0170_add_more_missing_indexes.py +++ /dev/null @@ -1,60 +0,0 @@ -""" -Migration script to create missing indexes. Adding new columns to existing tables via SQLAlchemy does not create the index, even if the column definition includes index=True. -""" - -import logging - -from sqlalchemy import MetaData - -from galaxy.model.migrate.versions.util import ( - add_index, - drop_index, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - -indexes = [ - [ - "ix_workflow_invocation_output_dataset_association_workflow_output_id", - "workflow_invocation_output_dataset_association", - "workflow_output_id", - ], - [ - "ix_workflow_invocation_output_dataset_association_workflow_step_id", - "workflow_invocation_output_dataset_association", - "workflow_step_id", - ], - [ - "ix_workflow_invocation_output_dataset_collection_association_workflow_output_id", - "workflow_invocation_output_dataset_collection_association", - "workflow_output_id", - ], - [ - "ix_workflow_invocation_output_dataset_collection_association_workflow_step_id", - "workflow_invocation_output_dataset_collection_association", - "workflow_step_id", - ], - [ - "ix_workflow_invocation_step_output_dataset_collection_association_workflow_step_id", - "workflow_invocation_step_output_dataset_collection_association", - "workflow_step_id", - ], -] - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - for ix, table, col in indexes: - add_index(ix, table, col, metadata) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - for ix, table, col in indexes: - drop_index(ix, table, col, metadata) diff --git a/lib/galaxy/model/migrate/versions/0171_schemaorg_metadata.py b/lib/galaxy/model/migrate/versions/0171_schemaorg_metadata.py deleted file mode 100644 index c89af15f692..00000000000 --- a/lib/galaxy/model/migrate/versions/0171_schemaorg_metadata.py +++ /dev/null @@ -1,46 +0,0 @@ -""" -Adds license and creator metadata to workflow. -""" -import datetime -import logging - -from sqlalchemy import ( - Column, - MetaData, - TEXT, -) - -from galaxy.model.custom_types import JSONType -from galaxy.model.migrate.versions.util import ( - add_column, - drop_column, -) - -now = datetime.datetime.utcnow -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - metadata.bind = migrate_engine - print(__doc__) - metadata.reflect() - - # Add person metadata in future pass at this. - # person_metadata_column = Column('person_metadata', JSONType, default=None) - # add_column(person_metadata_column, 'galaxy_user', metadata) - - creator_metadata_column = Column("creator_metadata", JSONType, default=None) - add_column(creator_metadata_column, "workflow", metadata) - - license_column = Column("license", TEXT, default=None) - add_column(license_column, "workflow", metadata) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - # drop_column('person_metadata', 'galaxy_user', metadata) - drop_column("creator_metadata", "workflow", metadata) - drop_column("license", "workflow", metadata) diff --git a/lib/galaxy/model/migrate/versions/0172_it_entrypoint_requires_domain.py b/lib/galaxy/model/migrate/versions/0172_it_entrypoint_requires_domain.py deleted file mode 100644 index 6cb997b4005..00000000000 --- a/lib/galaxy/model/migrate/versions/0172_it_entrypoint_requires_domain.py +++ /dev/null @@ -1,36 +0,0 @@ -""" -Adds requires_domain column to InteractiveTools Entry Point (interactivetool_entry_point). -""" -import datetime -import logging - -from sqlalchemy import ( - Boolean, - Column, - MetaData, -) - -from galaxy.model.migrate.versions.util import ( - add_column, - drop_column, -) - -log = logging.getLogger(__name__) -now = datetime.datetime.utcnow -metadata = MetaData() - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - requires_domain = Column("requires_domain", Boolean, default=None) - add_column(requires_domain, "interactivetool_entry_point", metadata) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - drop_column("requires_domain", "interactivetool_entry_point", metadata) diff --git a/lib/galaxy/model/migrate/versions/0173_add_job_id_to_dataset.py b/lib/galaxy/model/migrate/versions/0173_add_job_id_to_dataset.py deleted file mode 100644 index 69ebfa8f067..00000000000 --- a/lib/galaxy/model/migrate/versions/0173_add_job_id_to_dataset.py +++ /dev/null @@ -1,36 +0,0 @@ -""" -Migration script for adding job_id column to dataset table. -""" - -import logging - -from sqlalchemy import ( - Column, - ForeignKey, - Integer, - MetaData, -) - -from galaxy.model.migrate.versions.util import ( - add_column, - drop_column, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - job_id_column = Column("job_id", Integer, ForeignKey("job.id"), index=True) - add_column(job_id_column, "dataset", metadata, index_name="ix_dataset_job_id") - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - drop_column("job_id", "dataset", metadata) diff --git a/lib/galaxy/model/migrate/versions/0174_readd_update_time_triggers.py b/lib/galaxy/model/migrate/versions/0174_readd_update_time_triggers.py deleted file mode 100644 index bd1ad2931c8..00000000000 --- a/lib/galaxy/model/migrate/versions/0174_readd_update_time_triggers.py +++ /dev/null @@ -1,28 +0,0 @@ -""" -Re-add triggers to update history.update_time when contents are changed. -""" - -import logging - -from sqlalchemy import MetaData - -from galaxy.model.migrate.triggers.history_update_time_field import ( - drop_timestamp_triggers, - install_timestamp_triggers, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - install_timestamp_triggers(migrate_engine) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - drop_timestamp_triggers(migrate_engine) diff --git a/lib/galaxy/model/migrate/versions/0175_history_audit.py b/lib/galaxy/model/migrate/versions/0175_history_audit.py deleted file mode 100644 index ab47956bea6..00000000000 --- a/lib/galaxy/model/migrate/versions/0175_history_audit.py +++ /dev/null @@ -1,89 +0,0 @@ -""" -Add history audit table and associated triggers -""" - -import datetime -import logging - -from sqlalchemy import ( - Column, - DateTime, - ForeignKey, - Integer, - MetaData, - PrimaryKeyConstraint, - Table, -) - -from galaxy.model.migrate.triggers import history_update_time_field as old_triggers # rollback to old ones -from galaxy.model.migrate.triggers import update_audit_table as new_triggers # install me -from galaxy.model.migrate.versions.util import ( - create_table, - drop_table, -) - -log = logging.getLogger(__name__) -now = datetime.datetime.utcnow -metadata = MetaData() - -AuditTable = Table( - "history_audit", - metadata, - Column("history_id", Integer, ForeignKey("history.id"), primary_key=True, nullable=False), - Column("update_time", DateTime, default=now, primary_key=True, nullable=False), - PrimaryKeyConstraint(sqlite_on_conflict="IGNORE"), -) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - # create table + index - AuditTable.drop(migrate_engine, checkfirst=True) - create_table(AuditTable) - - # populate with update_time from every history - copy_update_times = """ - INSERT INTO history_audit (history_id, update_time) - SELECT id, update_time FROM history - """ - migrate_engine.execute(copy_update_times) - - # drop existing timestamp triggers - old_triggers.drop_timestamp_triggers(migrate_engine) - - # install new timestamp triggers - new_triggers.install(migrate_engine) - - -def downgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - # drop existing timestamp triggers - new_triggers.remove(migrate_engine) - - try: - # update history.update_time with vals from audit table - put_em_back = """ - UPDATE history h - SET update_time = a.max_update_time - FROM ( - SELECT history_id, max(update_time) as max_update_time - FROM history_audit - GROUP BY history_id - ) a - WHERE h.id = a.history_id - """ - migrate_engine.execute(put_em_back) - except Exception: - print("Unable to put update_times back") - - # drop audit table - drop_table(AuditTable) - - # install old timestamp triggers - old_triggers.install_timestamp_triggers(migrate_engine) diff --git a/lib/galaxy/model/migrate/versions/0176_add_indexes_on_update_time.py b/lib/galaxy/model/migrate/versions/0176_add_indexes_on_update_time.py deleted file mode 100644 index 12f3afe225c..00000000000 --- a/lib/galaxy/model/migrate/versions/0176_add_indexes_on_update_time.py +++ /dev/null @@ -1,41 +0,0 @@ -""" -Migration script to add indexes on update_time columns that are frequently used in ORDER BY clauses. -""" - -import logging - -from sqlalchemy import MetaData - -from galaxy.model.migrate.versions.util import ( - add_index, - drop_index, -) - -log = logging.getLogger(__name__) -metadata = MetaData() - -indexes = [ - ["ix_history_dataset_association_update_time", "history_dataset_association", "update_time"], - ["ix_library_dataset_dataset_association_update_time", "library_dataset_dataset_association", "update_time"], - ["ix_job_update_time", "job", "update_time"], - ["ix_history_dataset_collection_association_update_time", "history_dataset_collection_association", "update_time"], - ["ix_workflow_invocation_update_time", "workflow_invocation", "update_time"], - ["ix_stored_workflow_update_time", "stored_workflow", "update_time"], -] - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - for ix, table, col in indexes: - add_index(ix, table, col, metadata) - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - for ix, table, col in indexes: - drop_index(ix, table, col, metadata) diff --git a/lib/galaxy/model/migrate/versions/0177_update_job_state_summary.py b/lib/galaxy/model/migrate/versions/0177_update_job_state_summary.py deleted file mode 100644 index 7823b7512ae..00000000000 --- a/lib/galaxy/model/migrate/versions/0177_update_job_state_summary.py +++ /dev/null @@ -1,27 +0,0 @@ -""" -Update job-state-summary view for hdca elements to include job directly tied with the hdca -""" - -import logging - -from galaxy.model.view import HistoryDatasetCollectionJobStateSummary -from galaxy.model.view.utils import ( - CreateView, - DropView, -) - -log = logging.getLogger(__name__) - - -def upgrade(migrate_engine): - print(__doc__) - # drop first because sqlite does not support or_replace - downgrade(migrate_engine) - view = HistoryDatasetCollectionJobStateSummary - create_view = CreateView(view.name, view.__view__) - migrate_engine.execute(create_view) - - -def downgrade(migrate_engine): - drop_view = DropView(HistoryDatasetCollectionJobStateSummary.name) - migrate_engine.execute(drop_view) diff --git a/lib/galaxy/model/migrate/versions/0178_drop_deferredjob_table.py b/lib/galaxy/model/migrate/versions/0178_drop_deferredjob_table.py deleted file mode 100644 index 1e94a7f7505..00000000000 --- a/lib/galaxy/model/migrate/versions/0178_drop_deferredjob_table.py +++ /dev/null @@ -1,67 +0,0 @@ -""" -Drop unused DeferredJob table and foreign key column on genome_index_tool_data. -""" - -import logging - -from sqlalchemy import ( - Column, - DateTime, - ForeignKey, - Integer, - MetaData, - String, - Table, - TEXT, -) - -from galaxy.model.custom_types import JSONType -from galaxy.model.migrate.versions.util import ( - add_column, - create_table, - drop_column, - drop_table, -) -from galaxy.model.orm.now import now - -log = logging.getLogger(__name__) -metadata = MetaData() - -DeferredJob_table = Table( - "deferred_job", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("state", String(64), index=True), - Column("plugin", String(128), index=True), - Column("params", JSONType), - Column("info", TEXT), -) - -deferred_job_id = Column("deferred_job_id", Integer, ForeignKey("deferred_job.id"), index=True) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - try: - drop_column(deferred_job_id.name, "genome_index_tool_data", metadata) - drop_table(DeferredJob_table) - except Exception: - log.exception("Dropping deferred_job table failed") - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - try: - create_table(DeferredJob_table) - add_column( - deferred_job_id, "genome_index_tool_data", metadata, index_name="ix_genome_index_tool_data_deferred_job_id" - ) - except Exception: - log.exception("Creating deferred_job table failed") diff --git a/lib/galaxy/model/migrate/versions/0179_drop_transferjob_table.py b/lib/galaxy/model/migrate/versions/0179_drop_transferjob_table.py deleted file mode 100644 index acd6c76744b..00000000000 --- a/lib/galaxy/model/migrate/versions/0179_drop_transferjob_table.py +++ /dev/null @@ -1,67 +0,0 @@ -""" -Drop unused TransferJob table and foreign key column on genome_index_tool_data. -""" - -import logging - -from sqlalchemy import ( - Column, - DateTime, - ForeignKey, - Integer, - MetaData, - String, - Table, -) - -from galaxy.model.custom_types import JSONType -from galaxy.model.migrate.versions.util import ( - add_column, - create_table, - drop_column, - drop_table, -) -from galaxy.model.orm.now import now - -log = logging.getLogger(__name__) -metadata = MetaData() - -TransferJob_table = Table( - "transfer_job", - metadata, - Column("id", Integer, primary_key=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), - Column("state", String(64), index=True), - Column("path", String(1024)), - Column("params", JSONType), - Column("pid", Integer), - Column("socket", Integer), -) - -transfer_job_id = Column("transfer_job_id", Integer, ForeignKey("transfer_job.id"), index=True) - - -def upgrade(migrate_engine): - print(__doc__) - metadata.bind = migrate_engine - metadata.reflect() - - try: - drop_column(transfer_job_id.name, "genome_index_tool_data", metadata) - drop_table(TransferJob_table) - except Exception: - log.exception("Dropping transfer_job table failed") - - -def downgrade(migrate_engine): - metadata.bind = migrate_engine - metadata.reflect() - - try: - create_table(TransferJob_table) - add_column( - transfer_job_id, "genome_index_tool_data", metadata, index_name="ix_genome_index_tool_data_transfer_job_id" - ) - except Exception: - log.exception("Creating transfer_job table failed") diff --git a/lib/galaxy/model/migrate/versions/0180_add_vault_table.py b/lib/galaxy/model/migrate/versions/0180_add_vault_table.py deleted file mode 100644 index 7f18605b1b8..00000000000 --- a/lib/galaxy/model/migrate/versions/0180_add_vault_table.py +++ /dev/null @@ -1,41 +0,0 @@ -import datetime - -from sqlalchemy import ( - Column, - DateTime, - ForeignKey, - MetaData, - Table, - Text, -) - -now = datetime.datetime.utcnow -meta = MetaData() - -vault = Table( - "vault", - meta, - Column("key", Text, primary_key=True), - Column("parent_key", Text, ForeignKey("vault.key"), index=True, nullable=True), - Column("value", Text, nullable=True), - Column("create_time", DateTime, default=now), - Column("update_time", DateTime, default=now, onupdate=now), -) - - -def upgrade(migrate_engine): - meta.bind = migrate_engine - vault.create() - - -def downgrade(migrate_engine): - meta.bind = migrate_engine - # This revision is not part of 21.09, but in 22.01 it will be handled by Alembic. - # It is possible to reach a state (via upgrading/downgrading) where this table - # will have been dropped by Alembic, but SQLAlchemy Migrate (our previous - # db migrations tool) will have version 180 (which includes this table). - # Downgrading from such a state would raise an error - which this code prevents. - try: - vault.drop() - except Exception: - pass diff --git a/lib/galaxy/model/migrate/versions/util.py b/lib/galaxy/model/migrate/versions/util.py deleted file mode 100644 index 5bc4ab61a72..00000000000 --- a/lib/galaxy/model/migrate/versions/util.py +++ /dev/null @@ -1,205 +0,0 @@ -import hashlib -import logging - -from sqlalchemy import ( - BLOB, - DDL, - Index, - Table, - Text, -) -from sqlalchemy.dialects.mysql import MEDIUMBLOB - -log = logging.getLogger(__name__) - - -def engine_false(migrate_engine): - if migrate_engine.name in ["postgres", "postgresql"]: - return "FALSE" - elif migrate_engine.name in ["mysql", "sqlite"]: - return 0 - else: - raise Exception(f"Unknown database type: {migrate_engine.name}") - - -def engine_true(migrate_engine): - if migrate_engine.name in ["postgres", "postgresql"]: - return "TRUE" - elif migrate_engine.name in ["mysql", "sqlite"]: - return 1 - else: - raise Exception(f"Unknown database type: {migrate_engine.name}") - - -def nextval(migrate_engine, table, col="id"): - if migrate_engine.name in ["postgres", "postgresql"]: - return f"nextval('{table}_{col}_seq')" - elif migrate_engine.name in ["mysql", "sqlite"]: - return "null" - else: - raise Exception(f"Unable to convert data for unknown database type: {migrate_engine.name}") - - -def localtimestamp(migrate_engine): - if migrate_engine.name in ["mysql", "postgres", "postgresql"]: - return "LOCALTIMESTAMP" - elif migrate_engine.name == "sqlite": - return "current_date || ' ' || current_time" - else: - raise Exception(f"Unable to convert data for unknown database type: {migrate_engine.name}") - - -def truncate_index_name(index_name, engine): - # does what sqlalchemy does, see https://github.com/sqlalchemy/sqlalchemy/blob/8455a11bcc23e97afe666873cd872b0f204848d8/lib/sqlalchemy/sql/compiler.py#L4696 - max_index_name_length = engine.dialect.max_index_name_length or engine.dialect.max_identifier_length - if len(index_name) > max_index_name_length: - suffix = hashlib.md5(index_name.encode("utf-8")).hexdigest()[-4:] - index_name = f"{index_name[0:max_index_name_length - 8]}_{suffix}" - return index_name - - -def create_table(table): - try: - table.create() - except Exception: - log.exception("Creating table '%s' failed.", table) - - -def drop_table(table, metadata=None): - """ - :param table: Table to drop - :type table: :class:`Table` or str - """ - try: - if not isinstance(table, Table): - assert metadata is not None - table = Table(table, metadata, autoload=True) - table.drop() - except Exception: - log.exception("Dropping table '%s' failed.", table) - - -def add_column(column, table, metadata, **kwds): - """ - :param table: Table to add the column to - :type table: :class:`Table` or str - - :type metadata: :class:`Metadata` - """ - try: - index_to_create = None - migrate_engine = metadata.bind - if not isinstance(table, Table): - table = Table(table, metadata, autoload=True) - if migrate_engine.name == "sqlite" and column.index and column.foreign_keys: - # SQLAlchemy Migrate has a bug when adding a column with both a - # ForeignKey and an index in SQLite. Since SQLite creates an index - # anyway, we can drop the explicit index creation. - # TODO: this is hacky, but it solves this^ problem. Needs better solution. - index_to_create = (kwds["index_name"], table, column.name) - del kwds["index_name"] - column.index = False - column.create(table, **kwds) - assert column is table.c[column.name] - if index_to_create: - add_index(*index_to_create) - except Exception: - log.exception("Adding column '%s' to table '%s' failed.", column, table) - - -def alter_column(column_name, table, metadata=None, **kwds): - """ - :param table: Table to alter - :type table: :class:`Table` or str - - :param metadata: Needed only if ``table`` is a table name - :type metadata: :class:`Metadata` - """ - try: - if not isinstance(table, Table): - assert metadata is not None - table = Table(table, metadata, autoload=True) - column = table.c[column_name] - column.alter(**kwds) - except Exception: - log.exception("Modifying column '%s' of table '%s' failed.", column_name, table) - - -def drop_column(column_name, table, metadata=None): - """ - :param table: Table to drop the column from - :type table: :class:`Table` or str - - :param metadata: Needed only if ``table`` is a table name - :type metadata: :class:`Metadata` - """ - try: - if not isinstance(table, Table): - assert metadata is not None - table = Table(table, metadata, autoload=True) - column = table.c[column_name] - column.drop() - except Exception: - log.exception("Dropping column '%s' from table '%s' failed.", column_name, table) - - -def add_index(index_name, table, column_name, metadata=None, **kwds): - """ - :param table: Table to add the index to - :type table: :class:`Table` or str - - :param metadata: Needed only if ``table`` is a table name - :type metadata: :class:`Metadata` - """ - try: - if not isinstance(table, Table): - assert metadata is not None - table = Table(table, metadata, autoload=True) - index_name = truncate_index_name(index_name, table.metadata.bind) - if index_name not in [ix.name for ix in table.indexes]: - column = table.c[column_name] - # MySQL cannot index a TEXT/BLOB column without specifying mysql_length - if isinstance(column.type, (BLOB, MEDIUMBLOB, Text)): - kwds.setdefault("mysql_length", 200) - index = Index(index_name, column, **kwds) - index.create() - else: - log.debug("Index '%s' on column '%s' in table '%s' already exists.", index_name, column_name, table) - except Exception: - log.exception("Adding index '%s' on column '%s' to table '%s' failed.", index_name, column_name, table) - - -def drop_index(index, table, column_name=None, metadata=None): - """ - :param index: Index to drop - :type index: :class:`Index` or str - - :param table: Table to drop the index from - :type table: :class:`Table` or str - - :param metadata: Needed only if ``table`` is a table name - :type metadata: :class:`Metadata` - """ - try: - if not isinstance(index, Index): - if not isinstance(table, Table): - assert metadata is not None - table = Table(table, metadata, autoload=True) - index_name = truncate_index_name(index, table.metadata.bind) - if index_name in [ix.name for ix in table.indexes]: - index = Index(index_name, table.c[column_name]) - else: - log.debug("Index '%s' in table '%s' does not exist.", index, table) - return - index.drop() - except Exception: - log.exception("Dropping index '%s' from table '%s' failed", index, table) - - -def execute_statements(engine, raw_sql): - statements = raw_sql if isinstance(raw_sql, list) else [raw_sql] - for sql in statements: - cmd = DDL(sql) - with engine.connect() as connection: - with connection.begin(): - connection.execute(cmd) diff --git a/lib/galaxy/model/migrations/README.md b/lib/galaxy/model/migrations/README.md new file mode 100644 index 00000000000..9c5bcff49f6 --- /dev/null +++ b/lib/galaxy/model/migrations/README.md @@ -0,0 +1,76 @@ +# Galaxy's Migrations System + +## Overview + +To manage its database migrations, Galaxy uses [Alembic](https://alembic.sqlalchemy.org). + +Galaxy's data model is split into the [galaxy model](https://github.com/galaxyproject/galaxy/blob/dev/lib/galaxy/model/__init__.py) and the [install model](https://github.com/galaxyproject/galaxy/blob/dev/lib/galaxy/model/tool_shed_install/__init__.py). These two models may be persisted in one combined database (which is the default) or two separate databases (which is enabled by setting the [`install_database_connection`](https://github.com/galaxyproject/galaxy/blob/dev/lib/galaxy/webapps/galaxy/config_schema.yml#L157) configuration option). + +To accommodate this setup, the Alembic-based migrations system uses [branches](https://alembic.sqlalchemy.org/en/latest/branches.html#working-with-branches). A branch is a versioning lineage that starts at a common base revision and represents part of Galaxy's data model. These branches are identified by labels (`gxy` for the galaxy model and `tsi` for the install model) and may share the same Alembic version table (if they share the same database; otherwise, each database has its own version table). Each branch has its own version history, represented by revision scripts located in the branch version directory (`migrations/alembic/versions_gxy` for `gxy` and `migrations/alembic/versions_tsi` for `tsi`). + +## Administering Galaxy: upgrading and downgrading the database + +To create a database or initialize an empty database, use the `create_db.sh` script. For usage and options, see the inline documentation in the file. Note that this will create and/or initialize new databases (or one combined database) for the `gxy` and `tsi` models. This script does not handle the Tool Shed database. + +To upgrade or downgrade an existing database ***that has been migrated to Alembic***, use the `run_alembic.sh` script. For usage and options, see the inline documentation in the file. This script is a thin wrapper around the Alembic runner. It takes care of adjusting the path, initializing the Python virtual environtment, retrieving necessary configuration values, and invoking Alembic with appropriate arguments. + +Since Galaxy uses branch labels to distinguish between the galaxy and the install models, in most cases, you'll need to identify the target branch to which your command should be applied. Use branch labels: `gxy` for the galaxy model, and `tsi` for install model. + +### Examples of usage: + +Remember to first backup your database(s). + +#### To upgrade: +``` +./run_alembic.sh upgrade gxy@head # upgrade gxy to head revision +./run_alembic.sh upgrade gxy@+1 # upgrade gxy to 1 revision above current +./run_alembic.sh upgrade [revision identifier] # upgrade branch to a specific revision +./run_alembic.sh upgrade [revision identifier]+1 # upgrade branch to 1 revision above specific revision +./run_alembic.sh upgrade heads # upgrade gxy and tsi to head revisions +``` + +#### To downgrade: +``` +./run_alembic.sh downgrade gxy@base # downgrade gxy to base (empty db with empty alembic table) +./run_alembic.sh downgrade gxy@-1 # downgrade gxy to 1 revision below current +./run_alembic.sh downgrade [revision identifier] # downgrade branch to a specific revision +./run_alembic.sh downgrade [revision identifier]-1 # downgrade branch to 1 revision below specific revision +``` +Check [Alembic documentation](https://alembic.sqlalchemy.org/en/latest/branches.html#working-with-branches) for more examples. + +Note: relative upgrades and downgrades without a revision identifier are not supported - i.e., you cannot `upgrade +1` or `downgrade -1` without providing a revision identifier. However, you can upgrade both branches to their latest versions (head revisions) without providing a branch label: `upgrade heads`. + +### Legacy script + +The `manage_db.sh` script is still available, but is considered legacy. The script supports a subset of command line options offered previously by SQLAlchemy Migrate. For usage, see documentation in the file. + + +### Upgrading from SQLAlchemy Migrate + +Galaxy no longer supports SQLAlchemy Migrate. To upgrade to Alembic, follow these steps: + +1. Backup your database(s). + +2. Verify that your database is at the latest SQLAlchemy Migrate version. If you have a combined database, it should be version 179 (check the `migrate_version` table). If you have separate galaxy model and install model databases, your galaxy version should be 179, and your install model version should be 17. + +3. If your database is not current, before upgrading to the 22.01 release or the current dev branch, run `manage_db.sh upgrade` to upgrade your database. +Once your database has the latest SQLAlchemy Migrate version, you can check out the new code base (22.01 release or the current dev branch) and proceed to migrating to Alembic. + +4. If you want Alembic to upgrade your database automatically, you can set the [`database_auto_migrate`](https://github.com/galaxyproject/galaxy/blob/dev/lib/galaxy/webapps/galaxy/config_schema.yml#L170) configuration option and simply start Galaxy. Otherwise, use the `run_alembic.sh` script. + +## Developing Galaxy: creating new revisions + +When creating new revisions, as with upgrading/downgrading, you need to indicate the target branch. You use the same `run_alembic.sh` script; however, the syntax is slightly different: + +To create a revision for the galaxy model: +```./run_alembic.sh revision --head=gxy@head -m "your description"``` + +To create a revision for the install model: +```./run_alembic.sh revision --head=tsi@head -m "your description"``` + +Alembic will generate a revision script in the appropriate version directory (the location of the version directories is specified in the `alembic.ini` file). You'll need to fill out the `upgrade()` and `downgrade` functions. Use Alembic documentation for examples: +- [https://alembic.sqlalchemy.org/en/latest/tutorial.html#create-a-migration-script](https://alembic.sqlalchemy.org/en/latest/tutorial.html#create-a-migration-script) +- [https://alembic.sqlalchemy.org/en/latest/ops.html](https://alembic.sqlalchemy.org/en/latest/ops.html) + +After that, you may run the upgrade script: `manage_sb.sh upgrade heads`. And you're done! +(*NOTE: This step should be taken after updating the model and adding appropriate tests.*) diff --git a/lib/galaxy/model/migrations/__init__.py b/lib/galaxy/model/migrations/__init__.py new file mode 100644 index 00000000000..6b8318cfc79 --- /dev/null +++ b/lib/galaxy/model/migrations/__init__.py @@ -0,0 +1,518 @@ +import logging +import os +from typing import ( + cast, + Dict, + Iterable, + NamedTuple, + NewType, + NoReturn, + Optional, + Union, +) + +import alembic +from alembic import command +from alembic.config import Config +from alembic.runtime.migration import MigrationContext +from alembic.script import ScriptDirectory +from alembic.script.base import Script +from sqlalchemy import ( + create_engine, + MetaData, + Table, + text, +) +from sqlalchemy.engine import ( + Connection, + CursorResult, + Engine, +) + +from galaxy.model import Base as gxy_base +from galaxy.model.database_utils import ( + create_database, + database_exists, +) +from galaxy.model.mapping import create_additional_database_objects +from galaxy.model.tool_shed_install import Base as tsi_base + +ModelId = NewType("ModelId", str) +# These identifiers are used throughout the migrations system to distinquish +# between the two models; they refer to version directories, branch labels, etc. +# (if you rename these, you need to rename branch labels in alembic version directories) +GXY = ModelId("gxy") # galaxy model identifier +TSI = ModelId("tsi") # tool_shed_install model identifier + +ALEMBIC_TABLE = "alembic_version" +SQLALCHEMYMIGRATE_TABLE = "migrate_version" +SQLALCHEMYMIGRATE_LAST_VERSION_GXY = 180 +SQLALCHEMYMIGRATE_LAST_VERSION_TSI = 17 +log = logging.getLogger(__name__) + + +class DatabaseConfig(NamedTuple): + url: str + template: str + encoding: str + + +class NoVersionTableError(Exception): + # The database has no version table (neither SQLAlchemy Migrate, nor Alembic), so it is + # impossible to automatically determine the state of the database. Manual update required. + def __init__(self, model: str) -> None: + super().__init__(f"Your {model} database has no version table; manual update is required") + + +class IncorrectVersionError(Exception): + # The database has a SQLAlchemy Migrate version table, but its version is either older or more recent + # than {SQLALCHEMYMIGRATE_LAST_VERSION_GXY/TSI}, so it cannot be upgraded with Alembic. + # (A more recent version may indicate that something has changed in the database past the point + # where we can automatically migrate from SQLAlchemy Migrate to Alembic.) + # Manual update required. + def __init__(self, model: str, expected_version: int) -> None: + msg = f"Your {model} database version is incorrect; version {expected_version} is expected. " + msg += "Manual update is required. " + msg += "Please see this issue: https://github.com/galaxyproject/galaxy/issues/13528" + super().__init__(msg) + + +class OutdatedDatabaseError(Exception): + # The database is under Alembic version control, but is out-of-date. Automatic upgrade possible. + def __init__(self, model: str) -> None: + msg = f"Your {model} database is out-of-date; automatic update requires setting `database_auto_migrate`" + super().__init__(msg) + + +class InvalidModelIdError(Exception): + def __init__(self, model: str) -> None: + super().__init__(f"Invalid model: {model}") + + +class RevisionNotFoundError(Exception): + # The database has an Alembic version table; however, that table does not contain a revision identifier + # for the given model. As a result, it is impossible to determine the state of the database for this model + # (gxy or tsi). + def __init__(self, model: str) -> None: + msg = "The database has an alembic version table, but that table does not contain " + msg += f"a revision for the {model} model" + super().__init__(msg) + + +class AlembicManager: + """ + Alembic operations on one database. + """ + + @staticmethod + def is_at_revision(engine: Engine, revision: Union[str, Iterable[str]]) -> bool: + """ + True if revision is a subset of the set of version heads stored in the database. + """ + revision = listify(revision) + with engine.connect() as conn: + context = MigrationContext.configure(conn) + db_version_heads = context.get_current_heads() + return set(revision) <= set(db_version_heads) + + def __init__(self, engine: Engine, config_dict: Optional[dict] = None) -> None: + self.engine = engine + self.alembic_cfg = self._load_config(config_dict) + self.script_directory = ScriptDirectory.from_config(self.alembic_cfg) + self._db_heads: Optional[Iterable[str]] + self._reset_db_heads() + + def _load_config(self, config_dict: Optional[dict]) -> Config: + alembic_root = os.path.dirname(__file__) + _alembic_file = os.path.join(alembic_root, "alembic.ini") + config = Config(_alembic_file) + url = get_url_string(self.engine) + config.set_main_option("sqlalchemy.url", url) + if config_dict: + for key, value in config_dict.items(): + config.set_main_option(key, value) + return config + + def stamp_model_head(self, model: ModelId) -> None: + """Partial proxy to alembic's stamp command.""" + command.stamp(self.alembic_cfg, f"{model}@head") + self._reset_db_heads() + + def stamp_revision(self, revision: Union[str, Iterable[str]]) -> None: + """Partial proxy to alembic's stamp command.""" + command.stamp(self.alembic_cfg, revision) # type: ignore[arg-type] # https://alembic.sqlalchemy.org/en/latest/api/commands.html#alembic.command.stamp.params.revision + self._reset_db_heads() + + def upgrade(self, model: ModelId) -> None: + """Partial proxy to alembic's upgrade command.""" + # This works with or without an existing alembic version table. + command.upgrade(self.alembic_cfg, f"{model}@head") + self._reset_db_heads() + + def is_under_version_control(self, model: ModelId) -> bool: + """ + True if the database version table contains a revision that corresponds to a revision + in the script directory that has branch label `model`. + """ + if self.db_heads: + for db_head in self.db_heads: + try: + revision = self._get_revision(db_head) + if revision and model in revision.branch_labels: + log.info(f"The version of the {model} model in the database is {db_head}.") + return True + except alembic.util.exc.CommandError: # No need to raise exception. + log.info(f"Revision {db_head} does not exist in the script directory.") + return False + + def is_up_to_date(self, model: ModelId) -> bool: + """ + True if the head revision for `model` in the script directory is stored + in the database. + """ + head_id = self.get_model_script_head(model) + return bool(self.db_heads and head_id in self.db_heads) + + def get_model_db_head(self, model: ModelId) -> Optional[str]: + return self._get_head_revision(model, cast(Iterable[str], self.db_heads)) + + def get_model_script_head(self, model: ModelId) -> Optional[str]: + return self._get_head_revision(model, self.script_directory.get_heads()) + + def _get_head_revision(self, model: ModelId, heads: Iterable[str]) -> Optional[str]: + for head in heads: + revision = self._get_revision(head) + if revision and model in revision.branch_labels: + return head + return None + + @property + def db_heads(self) -> Iterable: + if self._db_heads is None: # Explicitly check for None: could be an empty tuple. + with self.engine.connect() as conn: + context: MigrationContext = MigrationContext.configure(conn) + self._db_heads = context.get_current_heads() + # We get a tuple as long as we use branches. Otherwise, we'd get a single value. + # listify() is a safeguard in case we stop using branches. + self._db_heads = listify(self._db_heads) + return self._db_heads + + def _get_revision(self, revision_id: str) -> Optional[Script]: + try: + return self.script_directory.get_revision(revision_id) + except alembic.util.exc.CommandError as e: + log.error(f"Revision {revision_id} not found in the script directory") + raise e + + def _reset_db_heads(self) -> None: + self._db_heads = None + + +class DatabaseStateCache: + """ + Snapshot of database state. + """ + + def __init__(self, engine: Engine) -> None: + self._load_db(engine) + + @property + def tables(self) -> Dict[str, Table]: + return self.db_metadata.tables + + def is_database_empty(self) -> bool: + return not bool(self.db_metadata.tables) + + def has_alembic_version_table(self) -> bool: + return ALEMBIC_TABLE in self.db_metadata.tables + + def has_sqlalchemymigrate_version_table(self) -> bool: + return SQLALCHEMYMIGRATE_TABLE in self.db_metadata.tables + + def is_last_sqlalchemymigrate_version(self, last_version: int) -> bool: + return self.sqlalchemymigrate_version == last_version + + def _load_db(self, engine: Engine) -> None: + with engine.connect() as conn: + self.db_metadata = self._load_db_metadata(conn) + self.sqlalchemymigrate_version = self._load_sqlalchemymigrate_version(conn) + + def _load_db_metadata(self, conn: Connection) -> MetaData: + metadata = MetaData() + metadata.reflect(bind=conn) + return metadata + + def _load_sqlalchemymigrate_version(self, conn: Connection) -> CursorResult: + if self.has_sqlalchemymigrate_version_table(): + sql = text(f"select version from {SQLALCHEMYMIGRATE_TABLE}") + return conn.execute(sql).scalar() + + +def verify_databases_via_script( + gxy_config: DatabaseConfig, + tsi_config: DatabaseConfig, + is_auto_migrate: bool = False, +) -> None: + # This function serves a use case when an engine has not been created yet + # (e.g. when called from a script). + gxy_engine = create_engine(gxy_config.url) + tsi_engine = None + if tsi_config.url and tsi_config.url != gxy_config.url: + tsi_engine = create_engine(tsi_config.url) + + verify_databases( + gxy_engine, + gxy_config.template, + gxy_config.encoding, + tsi_engine, + tsi_config.template, + tsi_config.encoding, + is_auto_migrate, + ) + gxy_engine.dispose() + if tsi_engine: + tsi_engine.dispose() + + +def verify_databases( + gxy_engine: Engine, + gxy_template: Optional[str], + gxy_encoding: Optional[str], + tsi_engine: Optional[Engine], + tsi_template: Optional[str], + tsi_encoding: Optional[str], + is_auto_migrate: bool, +) -> None: + # Verify gxy model. + gxy_verifier = DatabaseStateVerifier(gxy_engine, GXY, gxy_template, gxy_encoding, is_auto_migrate) + gxy_verifier.run() + + # New database = one engine or same engine, and gxy model has just been initialized. + is_new_database = (not tsi_engine or gxy_engine == tsi_engine) and gxy_verifier.is_new_database + + # Determine engine for tsi model. + tsi_engine = tsi_engine or gxy_engine + + # Verify tsi model model. + tsi_verifier = DatabaseStateVerifier(tsi_engine, TSI, tsi_template, tsi_encoding, is_auto_migrate, is_new_database) + tsi_verifier.run() + + +class DatabaseStateVerifier: + def __init__( + self, + engine: Engine, + model: ModelId, + database_template: Optional[str], + database_encoding: Optional[str], + is_auto_migrate: bool, + is_new_database: Optional[bool] = False, + ) -> None: + self.engine = engine + self.model = model + self.database_template = database_template + self.database_encoding = database_encoding + self._is_auto_migrate = is_auto_migrate + self.metadata = get_metadata(model) + # True if database has been initialized for another model. + self.is_new_database = is_new_database + # These values may or may not be required, so do a lazy load. + self._db_state: Optional[DatabaseStateCache] = None + self._alembic_manager: Optional[AlembicManager] = None + + @property + def is_auto_migrate(self) -> bool: + return self._is_auto_migrate + + @property + def db_state(self) -> DatabaseStateCache: + if not self._db_state: + self._db_state = DatabaseStateCache(engine=self.engine) + return self._db_state + + @property + def alembic_manager(self) -> AlembicManager: + if not self._alembic_manager: + self._alembic_manager = get_alembic_manager(self.engine) + return self._alembic_manager + + def run(self) -> None: + if self._handle_no_database(): + return + if self._handle_empty_database(): + return + self._handle_nonempty_database() + + def _handle_no_database(self) -> bool: + url = get_url_string(self.engine) + if not database_exists(url): + self._create_database(url) + self._initialize_database() + return True + return False + + def _handle_empty_database(self) -> bool: + if self.is_new_database or self._is_database_empty(): + self._initialize_database() + return True + return False + + def _handle_nonempty_database(self) -> None: + if self._has_alembic_version_table(): + self._handle_with_alembic() + elif self._has_sqlalchemymigrate_version_table(): + if self._is_last_sqlalchemymigrate_version(): + self._try_to_upgrade() + else: + self._handle_wrong_sqlalchemymigrate_version() + else: + self._handle_no_version_table() + + def _handle_with_alembic(self) -> None: + am = self.alembic_manager + model = self._get_model_name() + + if am.is_up_to_date(self.model): + log.info(f"Your {model} database is up-to-date") + return + if am.is_under_version_control(self.model): + # Model is under version control, but outdated. Try to upgrade. + self._try_to_upgrade() + else: + # Model is not under version control. We fail for the gxy model because we can't guess + # what the state of the database is if there is an alembic table without a gxy revision. + # For the tsi model, we can guess. If there are no tsi tables in the database, we treat it + # as a new install; but if there is at least one table, we assume it is the same version as gxy. + # See more details in this PR description: https://github.com/galaxyproject/galaxy/pull/13108 + if self.model == TSI: + if self._no_model_tables_exist(): + self._initialize_database() + else: + self._try_to_upgrade() + else: + raise RevisionNotFoundError(model) + + def _try_to_upgrade(self): + am = self.alembic_manager + model = self._get_model_name() + code_version = am.get_model_script_head(self.model) + if not self.is_auto_migrate: + db_version = am.get_model_db_head(self.model) + msg = self._get_upgrade_message(model, cast(str, db_version), cast(str, code_version)) + log.warning(msg) + raise OutdatedDatabaseError(model) + else: + log.info("Database is being upgraded to current version: {code_version}") + am.upgrade(self.model) + return + + def _get_upgrade_message(self, model: str, db_version: str, code_version: str) -> str: + msg = f"Your {model} database has version {db_version}, but this code expects " + msg += f"version {code_version}. " + msg += "This database can be upgraded automatically if database_auto_migrate is set. " + msg += "To upgrade manually, run `run_alembic.sh` (see instructions in that file). " + msg += "Please remember to backup your database before migrating." + return msg + + def _get_model_name(self) -> str: + return "galaxy" if self.model == GXY else "tool shed install" + + def _no_model_tables_exist(self) -> bool: + # True if there are no tables from `self.model` in the database. + db_tables = self.db_state.tables + for tablename in set(self.metadata.tables) - {ALEMBIC_TABLE}: + if tablename in db_tables: + return False + return True + + def _create_database(self, url: str) -> None: + create_kwds = {} + message = f"Creating database for URI [{url}]" + if self.database_template: + message += f" from template [{self.database_template}]" + create_kwds["template"] = self.database_template + if self.database_encoding: + message += f" with encoding [{self.database_encoding}]" + create_kwds["encoding"] = self.database_encoding + log.info(message) + create_database(url, **create_kwds) + + def _initialize_database(self) -> None: + load_metadata(self.metadata, self.engine) + if self.model == GXY: + self._create_additional_database_objects() + self.alembic_manager.stamp_model_head(self.model) + self.is_new_database = True + + def _create_additional_database_objects(self) -> None: + create_additional_database_objects(self.engine) + + def _is_database_empty(self) -> bool: + return self.db_state.is_database_empty() + + def _has_alembic_version_table(self) -> bool: + return self.db_state.has_alembic_version_table() + + def _has_sqlalchemymigrate_version_table(self) -> bool: + return self.db_state.has_sqlalchemymigrate_version_table() + + def _is_last_sqlalchemymigrate_version(self) -> bool: + last_version = get_last_sqlalchemymigrate_version(self.model) + return self.db_state.is_last_sqlalchemymigrate_version(last_version) + + def _handle_no_version_table(self) -> NoReturn: + model = self._get_model_name() + raise NoVersionTableError(model) + + def _handle_wrong_sqlalchemymigrate_version(self) -> NoReturn: + if self.model == GXY: + expected_version = SQLALCHEMYMIGRATE_LAST_VERSION_GXY + else: + expected_version = SQLALCHEMYMIGRATE_LAST_VERSION_TSI + model = self._get_model_name() + raise IncorrectVersionError(model, expected_version) + + +def get_last_sqlalchemymigrate_version(model: ModelId) -> int: + if model == GXY: + return SQLALCHEMYMIGRATE_LAST_VERSION_GXY + elif model == TSI: + return SQLALCHEMYMIGRATE_LAST_VERSION_TSI + else: + raise InvalidModelIdError(model) + + +def get_url_string(engine: Engine) -> str: + return engine.url.render_as_string(hide_password=False) + + +def get_alembic_manager(engine: Engine) -> AlembicManager: + return AlembicManager(engine) + + +def get_metadata(model: ModelId) -> MetaData: + if model == GXY: + return get_gxy_metadata() + elif model == TSI: + return get_tsi_metadata() + else: + raise InvalidModelIdError(model) + + +def load_metadata(metadata: MetaData, engine: Engine) -> None: + with engine.connect() as conn: + metadata.create_all(bind=conn) + + +def listify(data: Union[str, Iterable[str]]) -> Iterable[str]: + if not isinstance(data, (list, tuple)): + return [cast(str, data)] + return data + + +def get_gxy_metadata() -> MetaData: + return gxy_base.metadata + + +def get_tsi_metadata() -> MetaData: + return tsi_base.metadata diff --git a/lib/galaxy/model/migrations/alembic.ini b/lib/galaxy/model/migrations/alembic.ini new file mode 100644 index 00000000000..7e163261d5d --- /dev/null +++ b/lib/galaxy/model/migrations/alembic.ini @@ -0,0 +1,42 @@ +[alembic] +script_location = %(here)s/alembic +prepend_sys_path = . +version_locations = %(here)s/alembic/versions_gxy;%(here)s/alembic/versions_tsi +version_path_separator = ; + +[post_write_hooks] + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/lib/galaxy/model/migrations/alembic/env.py b/lib/galaxy/model/migrations/alembic/env.py new file mode 100644 index 00000000000..a1ac4b29fb4 --- /dev/null +++ b/lib/galaxy/model/migrations/alembic/env.py @@ -0,0 +1,129 @@ +import re +from typing import ( + Callable, + cast, + Dict, +) + +from alembic import context +from alembic.script import ScriptDirectory +from alembic.script.base import Script +from sqlalchemy import create_engine + +from galaxy.model.migrations import ( + GXY, + TSI, +) + +config = context.config +target_metadata = None # Not implemented: used for autogenerate, which we don't use here. + + +def run_migrations_offline() -> None: + """Run migrations in offline mode; database url required.""" + if not config.cmd_opts: # invoked programmatically + url = _get_url_from_config() + _configure_and_run_migrations_offline(url) + else: # invoked via script + f = _configure_and_run_migrations_offline + _run_migrations_invoked_via_script(f) + + +def run_migrations_online() -> None: + """Run migrations in online mode: engine and connection required.""" + if not config.cmd_opts: # invoked programmatically + url = _get_url_from_config() + _configure_and_run_migrations_online(url) + else: # invoked via script + f = _configure_and_run_migrations_online + _run_migrations_invoked_via_script(f) + + +def _run_migrations_invoked_via_script(run_migrations: Callable[[str], None]) -> None: + urls = _load_urls() + + # Special case: the `current` command has no config.cmd_opts.revision property, + # so we check for it before checking for `upgrade/downgrade`. + if _process_cmd_current(urls): + return # we're done + + revision_str = config.cmd_opts.revision # type: ignore[union-attr] + + if revision_str.startswith(f"{GXY}@"): + url = urls[GXY] + elif revision_str.startswith(f"{TSI}@"): + url = urls[TSI] + else: + revision = _get_revision(revision_str) + if GXY in revision.branch_labels: + url = urls[GXY] + elif TSI in revision.branch_labels: + url = urls[TSI] + + run_migrations(url) + + +def _process_cmd_current(urls: Dict[str, str]) -> bool: + if config.cmd_opts.cmd[0].__name__ == "current": # type: ignore[union-attr] + for url in urls.values(): + _configure_and_run_migrations_online(url) + return True + return False + + +def _get_revision(revision_str: str) -> Script: + revision_id = _get_revision_id(revision_str) + script_directory = ScriptDirectory.from_config(config) + revision = script_directory.get_revision(revision_id) + if not revision: + raise Exception(f'Revision not found: "{revision}"') + return revision + + +def _get_revision_id(revision_str: str) -> str: + # Match a full or partial revision (GUID) or a relative migration identifier + p = re.compile(r"([0-9A-Fa-f]+)([+-]\d)?") + m = p.match(revision_str) + if not m: + raise Exception(f'Invalid revision or migration identifier: "{revision_str}"') + return m.group(1) + + +def _configure_and_run_migrations_offline(url: str) -> None: + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + with context.begin_transaction(): + context.run_migrations() + + +def _configure_and_run_migrations_online(url) -> None: + engine = create_engine(url) + with engine.connect() as connection: + context.configure(connection=connection, target_metadata=target_metadata) + with context.begin_transaction(): + context.run_migrations() + engine.dispose() + + +def _get_url_from_config() -> str: + url = config.get_main_option("sqlalchemy.url") + return cast(str, url) + + +def _load_urls() -> Dict[str, str]: + gxy_url = context.get_x_argument(as_dictionary=True).get(f"{GXY}_url") + tsi_url = context.get_x_argument(as_dictionary=True).get(f"{TSI}_url") + return { + GXY: gxy_url, + TSI: tsi_url, + } + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/lib/galaxy/model/migrations/alembic/script.py.mako b/lib/galaxy/model/migrations/alembic/script.py.mako new file mode 100644 index 00000000000..2c0156303a8 --- /dev/null +++ b/lib/galaxy/model/migrations/alembic/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/lib/galaxy/model/migrations/alembic/versions_gxy/b182f655505f_add_workflow_source_metadata_column.py b/lib/galaxy/model/migrations/alembic/versions_gxy/b182f655505f_add_workflow_source_metadata_column.py new file mode 100644 index 00000000000..5b037cdedd2 --- /dev/null +++ b/lib/galaxy/model/migrations/alembic/versions_gxy/b182f655505f_add_workflow_source_metadata_column.py @@ -0,0 +1,26 @@ +"""add workflow.source_metadata column + +Revision ID: b182f655505f +Revises: e7b6dcb09efd +Create Date: 2022-03-14 12:56:57.067748 + +""" +from alembic import op +from sqlalchemy import Column + +from galaxy.model.custom_types import JSONType +from galaxy.model.migrations.util import drop_column + +# revision identifiers, used by Alembic. +revision = "b182f655505f" +down_revision = "e7b6dcb09efd" +branch_labels = None +depends_on = None + + +def upgrade(): + op.add_column("workflow", Column("source_metadata", JSONType)) + + +def downgrade(): + drop_column("workflow", "source_metadata") diff --git a/lib/galaxy/model/migrations/alembic/versions_gxy/e7b6dcb09efd_create_gxy_branch.py b/lib/galaxy/model/migrations/alembic/versions_gxy/e7b6dcb09efd_create_gxy_branch.py new file mode 100644 index 00000000000..e76c4d87e30 --- /dev/null +++ b/lib/galaxy/model/migrations/alembic/versions_gxy/e7b6dcb09efd_create_gxy_branch.py @@ -0,0 +1,22 @@ +"""create gxy branch + +Revision ID: e7b6dcb09efd +Revises: +Create Date: 2021-11-05 16:32:43.243049 + +""" + + +# revision identifiers, used by Alembic. +revision = "e7b6dcb09efd" +down_revision = None +branch_labels = ("gxy",) +depends_on = None + + +def upgrade(): + pass + + +def downgrade(): + pass diff --git a/lib/galaxy/model/migrations/alembic/versions_tsi/d4a650f47a3c_create_tsi_branch.py b/lib/galaxy/model/migrations/alembic/versions_tsi/d4a650f47a3c_create_tsi_branch.py new file mode 100644 index 00000000000..87510a7e58a --- /dev/null +++ b/lib/galaxy/model/migrations/alembic/versions_tsi/d4a650f47a3c_create_tsi_branch.py @@ -0,0 +1,22 @@ +"""create tsi branch + +Revision ID: d4a650f47a3c +Revises: +Create Date: 2021-11-05 16:32:25.113750 + +""" + + +# revision identifiers, used by Alembic. +revision = "d4a650f47a3c" +down_revision = None +branch_labels = ("tsi",) +depends_on = None + + +def upgrade(): + pass + + +def downgrade(): + pass diff --git a/lib/galaxy/model/migrations/scripts.py b/lib/galaxy/model/migrations/scripts.py new file mode 100644 index 00000000000..429a9751726 --- /dev/null +++ b/lib/galaxy/model/migrations/scripts.py @@ -0,0 +1,307 @@ +import os +import re +import sys +from typing import ( + List, + Optional, + Tuple, +) + +import alembic.config +from alembic.config import Config +from alembic.runtime.migration import MigrationContext +from alembic.script import ScriptDirectory +from sqlalchemy import create_engine +from sqlalchemy.engine import Engine + +from galaxy.model.database_utils import is_one_database +from galaxy.model.migrations import ( + AlembicManager, + DatabaseConfig, + DatabaseStateCache, + GXY, + IncorrectVersionError, + SQLALCHEMYMIGRATE_LAST_VERSION_GXY, + TSI, +) +from galaxy.util.properties import ( + find_config_file, + get_data_dir, + load_app_properties, +) + +DEFAULT_CONFIG_NAMES = ["galaxy", "universe_wsgi"] +CONFIG_FILE_ARG = "--galaxy-config" +CONFIG_DIR_NAME = "config" +GXY_CONFIG_PREFIX = "GALAXY_CONFIG_" +TSI_CONFIG_PREFIX = "GALAXY_INSTALL_CONFIG_" + + +def get_configuration(argv: List[str], cwd: str) -> Tuple[DatabaseConfig, DatabaseConfig, bool]: + """ + Return a 3-item-tuple with configuration values used for managing databases. + """ + config_file = _pop_config_file(argv) + if config_file is None: + cwds = [cwd, os.path.join(cwd, CONFIG_DIR_NAME)] + config_file = find_config_file(DEFAULT_CONFIG_NAMES, dirs=cwds) + + # load gxy properties and auto-migrate + properties = load_app_properties(config_file=config_file, config_prefix=GXY_CONFIG_PREFIX) + default_url = f"sqlite:///{os.path.join(get_data_dir(properties), 'universe.sqlite')}?isolation_level=IMMEDIATE" + url = properties.get("database_connection", default_url) + template = properties.get("database_template", None) + encoding = properties.get("database_encoding", None) + is_auto_migrate = properties.get("database_auto_migrate", False) + gxy_config = DatabaseConfig(url, template, encoding) + + # load tsi properties + properties = load_app_properties(config_file=config_file, config_prefix=TSI_CONFIG_PREFIX) + default_url = gxy_config.url + url = properties.get("install_database_connection", default_url) + template = properties.get("database_template", None) + encoding = properties.get("database_encoding", None) + tsi_config = DatabaseConfig(url, template, encoding) + + return (gxy_config, tsi_config, is_auto_migrate) + + +def _pop_config_file(argv: List[str]) -> Optional[str]: + if CONFIG_FILE_ARG in argv: + pos = argv.index(CONFIG_FILE_ARG) + argv.pop(pos) # pop argument name + return argv.pop(pos) # pop and return argument value + return None + + +def add_db_urls_to_command_arguments(argv: List[str], gxy_url: str, tsi_url: str) -> None: + _insert_x_argument(argv, "tsi_url", tsi_url) + _insert_x_argument(argv, "gxy_url", gxy_url) + + +def _insert_x_argument(argv, key: str, value: str) -> None: + # `_insert_x_argument('mykey', 'myval')` transforms `foo -a 1` into `foo -x mykey=myval -a 42` + argv.insert(1, f"{key}={value}") + argv.insert(1, "-x") + + +def invoke_alembic() -> None: + """ + Invoke the Alembic command line runner. + + Accept 'heads' as the target revision argument to enable upgrading both gxy and tsi in one command. + This is consistent with Alembic's CLI, which allows `upgrade heads`. However, this would not work for + separate gxy and tsi databases: we can't attach a database url to a revision after Alembic has been + invoked with the 'upgrade' command and the 'heads' argument. So, instead we invoke Alembic for each head. + """ + if "heads" in sys.argv and "upgrade" in sys.argv: + i = sys.argv.index("heads") + sys.argv[i] = f"{GXY}@head" + alembic.config.main() + sys.argv[i] = f"{TSI}@head" + alembic.config.main() + else: + alembic.config.main() + + +class LegacyScriptsException(Exception): + # Misc. errors caused by incorrect arguments passed to a legacy script. + def __init__(self, message: str) -> None: + super().__init__(message) + + +class LegacyScripts: + + LEGACY_CONFIG_FILE_ARG_NAMES = ["-c", "--config", "--config-file"] + ALEMBIC_CONFIG_FILE_ARG = "--alembic-config" # alembic config file, set in the calling script + DEFAULT_DB_ARG = "default" + + def __init__(self, argv: List[str], cwd: Optional[str] = None) -> None: + self.argv = argv + self.cwd = cwd or os.getcwd() + self.database = self.DEFAULT_DB_ARG + + def run(self) -> None: + """ + Convert legacy arguments to current spec required by Alembic, + then add db url arguments required by Alembic + """ + self.convert_args() + add_db_urls_to_command_arguments(self.argv, self.gxy_url, self.tsi_url) + + def convert_args(self) -> None: + """ + Convert legacy arguments to current spec required by Alembic. + + Note: The following method calls must be done in this sequence. + """ + self.pop_database_argument() + self.rename_config_argument() + self.rename_alembic_config_argument() + self.load_db_urls() + self.convert_version_argument() + + def pop_database_argument(self) -> None: + """ + If last argument is a valid database name, pop and assign it; otherwise assign default. + """ + arg = self.argv[-1] + if arg in ["galaxy", "install"]: + self.database = self.argv.pop() + + def rename_config_argument(self) -> None: + """ + Rename the optional config argument: we can't use '-c' because that option is used by Alembic. + """ + for arg in self.LEGACY_CONFIG_FILE_ARG_NAMES: + if arg in self.argv: + self._rename_arg(arg, CONFIG_FILE_ARG) + return + + def rename_alembic_config_argument(self) -> None: + """ + Rename argument name: `--alembic-config` to `-c`. There should be no `-c` argument present. + """ + if "-c" in self.argv: + raise LegacyScriptsException("Cannot rename alembic config argument: `-c` argument present.") + self._rename_arg(self.ALEMBIC_CONFIG_FILE_ARG, "-c") + + def convert_version_argument(self) -> None: + """ + Convert legacy version argument to current spec required by Alembic. + """ + if "--version" in self.argv: + # Just remove it: the following argument should be the version/revision identifier. + pos = self.argv.index("--version") + self.argv.pop(pos) + else: + # If we find --version=foo, extract foo and replace arg with foo (which is the revision identifier) + p = re.compile(r"--version=([0-9A-Fa-f]+)") + for i, arg in enumerate(self.argv): + m = p.match(arg) + if m: + self.argv[i] = m.group(1) + return + # No version argument found: construct argument for an upgrade operation. + # Raise exception otherwise. + if "upgrade" not in self.argv: + raise LegacyScriptsException("If no `--version` argument supplied, `upgrade` argument is requried") + + if self._is_one_database(): # upgrade both regardless of database argument + self.argv.append("heads") + else: # for separate databases, choose one + if self.database in ["galaxy", self.DEFAULT_DB_ARG]: + self.argv.append("gxy@head") + elif self.database == "install": + self.argv.append("tsi@head") + + def _rename_arg(self, old_name, new_name) -> None: + pos = self.argv.index(old_name) + self.argv[pos] = new_name + + def load_db_urls(self) -> None: + gxy_config, tsi_config, _ = get_configuration(self.argv, self.cwd) + self.gxy_url = gxy_config.url + self.tsi_url = tsi_config.url + + def _is_one_database(self): + return is_one_database(self.gxy_url, self.tsi_url) + + +class LegacyManageDb: + def __init__(self): + self._set_db_urls() + + def get_gxy_version(self): + """ + Get the head revision for the gxy branch from the Alembic script directory. + (previously referred to as "max/repository version") + """ + script_directory = self._get_script_directory() + heads = script_directory.get_heads() + for head in heads: + revision = script_directory.get_revision(head) + if revision and GXY in revision.branch_labels: + return head + return None + + def get_gxy_db_version(self, gxy_db_url=None): + """ + Get the head revision for the gxy branch from the galaxy database. If there + is no alembic_version table, get the sqlalchemy migrate version. Raise error + if that version is not the latest. + (previously referred to as "database version") + """ + db_url = gxy_db_url or self.gxy_db_url + try: + engine = create_engine(db_url) + version = self._get_gxy_alembic_db_version(engine) + if not version: + version = self._get_gxy_sam_db_version(engine) + if version != SQLALCHEMYMIGRATE_LAST_VERSION_GXY: + raise IncorrectVersionError(GXY, SQLALCHEMYMIGRATE_LAST_VERSION_GXY) + return version + finally: + engine.dispose() + + def run_upgrade(self, gxy_db_url=None, tsi_db_url=None): + """ + Alembic will upgrade both branches, gxy and tsi, to their head revisions. + """ + gxy_db_url = gxy_db_url or self.gxy_db_url + tsi_db_url = tsi_db_url or self.tsi_db_url + self._upgrade(gxy_db_url, GXY) + self._upgrade(tsi_db_url, TSI) + + def _upgrade(self, db_url, model): + try: + engine = create_engine(db_url) + am = get_alembic_manager(engine) + am.upgrade(model) + finally: + engine.dispose() + + def _set_db_urls(self): + ls = LegacyScripts(sys.argv, os.getcwd()) + ls.rename_config_argument() + ls.load_db_urls() + self.gxy_db_url = ls.gxy_url + self.tsi_db_url = ls.tsi_url + + def _get_gxy_sam_db_version(self, engine): + dbcache = DatabaseStateCache(engine) + return dbcache.sqlalchemymigrate_version + + def _get_script_directory(self): + alembic_cfg = self._get_alembic_cfg() + return ScriptDirectory.from_config(alembic_cfg) + + def _get_alembic_cfg(self): + config_file = os.path.join(os.path.dirname(__file__), "alembic.ini") + config_file = os.path.abspath(config_file) + return Config(config_file) + + def _get_gxy_alembic_db_version(self, engine): + # We may get 2 values, one for each branch (gxy and tsi). So we need to + # determine which one is the gxy head. + with engine.connect() as conn: + context = MigrationContext.configure(conn) + db_heads = context.get_current_heads() + if db_heads: + gxy_revisions = self._get_all_gxy_revisions() + for db_head in db_heads: + if db_head in gxy_revisions: + return db_head + return None + + def _get_all_gxy_revisions(self): + gxy_revisions = set() + script_directory = self._get_script_directory() + for rev in script_directory.walk_revisions(): + if GXY in rev.branch_labels: + gxy_revisions.add(rev.revision) + return gxy_revisions + + +def get_alembic_manager(engine: Engine) -> AlembicManager: + return AlembicManager(engine) diff --git a/lib/galaxy/model/migrations/util.py b/lib/galaxy/model/migrations/util.py new file mode 100644 index 00000000000..4e101d7cbdf --- /dev/null +++ b/lib/galaxy/model/migrations/util.py @@ -0,0 +1,6 @@ +from alembic import op + + +def drop_column(table, column): + with op.batch_alter_table(table) as batch_op: + batch_op.drop_column(column) diff --git a/lib/galaxy/model/orm/engine_factory.py b/lib/galaxy/model/orm/engine_factory.py index d8def0560bd..933f52c607c 100644 --- a/lib/galaxy/model/orm/engine_factory.py +++ b/lib/galaxy/model/orm/engine_factory.py @@ -8,6 +8,7 @@ from multiprocessing.util import register_after_fork from sqlalchemy import ( create_engine, event, + exc, ) from sqlalchemy.engine import Engine @@ -48,7 +49,7 @@ def pretty_stack(): def build_engine( url, - engine_options, + engine_options=None, database_query_profiling_proxy=False, trace_logger=None, slow_query_log_threshold=0, @@ -104,6 +105,24 @@ def build_engine( if "sqlite://" in url: connect_args["check_same_thread"] = False # Create the database engine + engine_options = engine_options or {} engine = create_engine(url, connect_args=connect_args, **engine_options) + + # Prevent sharing connection across fork: https://docs.sqlalchemy.org/en/14/core/pooling.html#using-connection-pools-with-multiprocessing-or-os-fork register_after_fork(engine, lambda e: e.dispose()) + + @event.listens_for(engine, "connect") + def connect(dbapi_connection, connection_record): + connection_record.info["pid"] = os.getpid() + + @event.listens_for(engine, "checkout") + def checkout(dbapi_connection, connection_record, connection_proxy): + pid = os.getpid() + if connection_record.info["pid"] != pid: + connection_record.dbapi_connection = connection_proxy.dbapi_connection = None + raise exc.DisconnectionError( + "Connection record belongs to pid %s, " + "attempting to check out in pid %s" % (connection_record.info["pid"], pid) + ) + return engine diff --git a/lib/galaxy/model/orm/scripts.py b/lib/galaxy/model/orm/scripts.py index f62f4fb0e73..42e3d612b89 100644 --- a/lib/galaxy/model/orm/scripts.py +++ b/lib/galaxy/model/orm/scripts.py @@ -1,13 +1,18 @@ """ -Code to support database helper scripts (create_db.py, manage_db.py, etc...). +Code to support database helper scripts (create_toolshed_db.py, migrate_toolshed_db.py, etc...). """ import argparse import logging import os import sys -from migrate.versioning.shell import main as migrate_main +import alembic.config +from galaxy.model.migrations import ( + GXY, + TSI, +) +from galaxy.model.migrations.scripts import get_configuration from galaxy.util.path import get_ext from galaxy.util.properties import ( find_config_file, @@ -25,12 +30,6 @@ DEFAULT_DATABASE = "galaxy" DATABASE = { "galaxy": { - "repo": "galaxy/model/migrate", - "default_sqlite_file": "universe.sqlite", - "config_override": "GALAXY_CONFIG_", - }, - "tools": { - "repo": "galaxy/model/tool_shed_install/migrate", "default_sqlite_file": "universe.sqlite", "config_override": "GALAXY_CONFIG_", }, @@ -42,7 +41,6 @@ DATABASE = { "config_section": "tool_shed", }, "install": { - "repo": "galaxy/model/tool_shed_install/migrate", "config_prefix": "install_", "default_sqlite_file": "install.sqlite", "config_override": "GALAXY_INSTALL_CONFIG_", @@ -109,8 +107,6 @@ def get_config(argv, use_argparse=True, cwd=None): >>> uri_with_env = os.getenv("GALAXY_TEST_DBURI", "sqlite:////moo/universe.sqlite?isolation_level=IMMEDIATE") >>> config['db_url'] == uri_with_env True - >>> config['repo'].endswith('galaxy/model/migrate') - True >>> rmtree(config_dir) """ config_file, config_section, database = _read_model_arguments(argv, use_argparse=use_argparse) @@ -123,7 +119,10 @@ def get_config(argv, use_argparse=True, cwd=None): cwd = [DEFAULT_CONFIG_DIR] config_file = find_config_file(config_names, dirs=cwd) - repo = os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, os.pardir, database_defaults["repo"]) + repo = database_defaults.get("repo") + if repo: + repo = os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, os.pardir, repo) + config_prefix = database_defaults.get("config_prefix", DEFAULT_CONFIG_PREFIX) config_override = database_defaults.get("config_override", "GALAXY_CONFIG_") default_sqlite_file = database_defaults["default_sqlite_file"] @@ -153,6 +152,21 @@ def get_config(argv, use_argparse=True, cwd=None): def manage_db(): - # Migrate has its own args, so cannot use argparse - config = get_config(sys.argv, use_argparse=False, cwd=os.getcwd()) - migrate_main(repository=config["repo"], url=config["db_url"]) + # This is a duplicate implementation of scripts/migrate_db.py. + # See run_alembic.sh for usage. + def _insert_x_argument(key, value): + sys.argv.insert(1, f"{key}={value}") + sys.argv.insert(1, "-x") + + gxy_config, tsi_config, _ = get_configuration(sys.argv, os.getcwd()) + _insert_x_argument("tsi_url", tsi_config.url) + _insert_x_argument("gxy_url", gxy_config.url) + + if "heads" in sys.argv and "upgrade" in sys.argv: + i = sys.argv.index("heads") + sys.argv[i] = f"{GXY}@head" + alembic.config.main() + sys.argv[i] = f"{TSI}@head" + alembic.config.main() + else: + alembic.config.main() diff --git a/lib/galaxy/model/orm/util.py b/lib/galaxy/model/orm/util.py new file mode 100644 index 00000000000..f8b064a9ba9 --- /dev/null +++ b/lib/galaxy/model/orm/util.py @@ -0,0 +1,32 @@ +from sqlalchemy import inspect + + +def add_object_to_object_session(object, object_with_session): + """ + Explicitly add object to the session. + Addresses SQLAlchemy 2.0 compatibility issue: + https://docs.sqlalchemy.org/en/14/changelog/migration_14.html#cascade-backrefs-behavior-deprecated-for-removal-in-2-0 + + This function preserves SQLAlchemy's pre-2.0 logic and should be used when: + 1. foo and bar are model instances, that are associated (via SQLAlchemy's relationship), AND + 2. bar is assigned to foo's bar relationship (e.g. foo.bar = bar), AND + 3. bar is in a session and foo is not, AND + 4. foo is implicitly added to bar's session upon assignment(2), as indicated + by a RemovedIn20Warning specifying that the '"foo" object is being merged into a Session + along the backref cascade path...' + """ + if object_with_session: + session = get_object_session(object_with_session) + if session: + add_object_to_session(object, session) + + +def add_object_to_session(object, session): + """Explicitly add object to the session.""" + if session and object: + session.add(object) + + +def get_object_session(object): + if object: + return inspect(object).session diff --git a/lib/galaxy/model/store/__init__.py b/lib/galaxy/model/store/__init__.py index 9f63050c9a6..ed59a4ad37a 100644 --- a/lib/galaxy/model/store/__init__.py +++ b/lib/galaxy/model/store/__init__.py @@ -30,6 +30,11 @@ from galaxy.exceptions import ( ObjectNotFound, ) from galaxy.model.metadata import MetadataCollection +from galaxy.model.orm.util import ( + add_object_to_object_session, + add_object_to_session, + get_object_session, +) from galaxy.security.idencoding import IdEncodingHelper from galaxy.util import ( FILENAME_VALID_CHARS, @@ -582,6 +587,7 @@ class ModelImportStore(metaclass=abc.ABCMeta): self._session_add(dc) return dc + history_sa_session = get_object_session(history) for collection_attrs in collections_attrs: if "collection" in collection_attrs: dc = import_collection(collection_attrs["collection"]) @@ -597,6 +603,7 @@ class ModelImportStore(metaclass=abc.ABCMeta): ) self._attach_raw_id_if_editing(hdca, collection_attrs) + add_object_to_session(hdca, history_sa_session) hdca.history = history if new_history and self.trust_hid(collection_attrs): hdca.hid = collection_attrs["hid"] @@ -730,6 +737,7 @@ class ModelImportStore(metaclass=abc.ABCMeta): # jobs_attrs = self.jobs_properties() # Create each job. + history_sa_session = get_object_session(history) for job_attrs in jobs_attrs: if "id" in job_attrs and not self.sessionless: # only thing we allow editing currently is associations for incoming jobs. @@ -743,6 +751,7 @@ class ModelImportStore(metaclass=abc.ABCMeta): imported_job = model.Job() imported_job.id = job_attrs.get("id") imported_job.user = self.user + add_object_to_session(imported_job, history_sa_session) imported_job.history = history imported_job.imported = True imported_job.tool_id = job_attrs["tool_id"] @@ -776,9 +785,12 @@ class ModelImportStore(metaclass=abc.ABCMeta): icj.jobs = [] for order_index, job in enumerate(icj_attrs["jobs"]): icja = model.ImplicitCollectionJobsJobAssociation() + add_object_to_object_session(icja, icj) icja.implicit_collection_jobs = icj if job in object_import_tracker.jobs_by_key: - icja.job = object_import_tracker.jobs_by_key[job] + job_instance = object_import_tracker.jobs_by_key[job] + add_object_to_object_session(icja, job_instance) + icja.job = job_instance icja.order_index = order_index icj.jobs.append(icja) self._session_add(icja) @@ -1115,6 +1127,10 @@ class DirectoryImportModelStoreLatest(BaseDirectoryImportModelStore): else: raise NotImplementedError() new_obj = obj.copy() + if not new_id and self.import_options.allow_edit: + # We may not have exported all job parameters, such as dces, + # but we shouldn't set the object_id to none in that case. + new_id = obj["id"] new_obj["id"] = new_id return (k, new_obj) @@ -1463,14 +1479,14 @@ class DirectoryModelExportStore(ModelExportStore): sa_session.query(model.HistoryDatasetAssociation) .filter(model.HistoryDatasetAssociation.history == history) .join(model.Dataset) - .options(joinedload("dataset").joinedload("actions")) + .options(joinedload(model.HistoryDatasetAssociation.dataset).joinedload(model.Dataset.actions)) .order_by(model.HistoryDatasetAssociation.hid) .filter(model.Dataset.purged == expression.false()) ) datasets = query.all() for dataset in datasets: dataset.annotation = get_item_annotation_str(sa_session, history.user, dataset) - add_dataset = (not dataset.visible or not include_hidden) and (not dataset.deleted or include_deleted) + add_dataset = (dataset.visible or include_hidden) and (not dataset.deleted or include_deleted) if dataset.id in self.collection_datasets: add_dataset = True diff --git a/lib/galaxy/model/store/discover.py b/lib/galaxy/model/store/discover.py index a12d042f460..9dd5a6000bc 100644 --- a/lib/galaxy/model/store/discover.py +++ b/lib/galaxy/model/store/discover.py @@ -8,7 +8,6 @@ corresponding to files in other contexts. import abc import logging import os -from collections import namedtuple from typing import ( Any, Callable, @@ -51,6 +50,9 @@ class MaxDiscoveredFilesExceededError(ValueError): pass +CollectorT = Any # TODO: setup an interface for these file collectors data classes. + + class ModelPersistenceContext(metaclass=abc.ABCMeta): """Class for creating datasets while finding files. @@ -162,8 +164,8 @@ class ModelPersistenceContext(metaclass=abc.ABCMeta): primary_data.created_from_basename = created_from_basename if tag_list: - # TODO: handle tag list without a session here... - self.tag_handler.add_tags_from_list(self.user, primary_data, tag_list, flush=False) + job = getattr(self, "job", None) + self.tag_handler.add_tags_from_list(job and job.user, primary_data, tag_list, flush=False) # If match specified a name use otherwise generate one from # designation. @@ -532,7 +534,6 @@ class SessionlessModelPersistenceContext(ModelPersistenceContext): self._flush_per_n_datasets = None self.discovered_file_count = 0 self.max_discovered_files = float("inf") - self.job_working_directory = working_directory # TODO: rename... @property @@ -734,7 +735,6 @@ def persist_elements_to_folder(model_persistence_context, elements, library_fold visible = fields_match.visible ext = fields_match.ext dbkey = fields_match.dbkey - info = element.get("info", None) link_data = discovered_file.match.link_data # Create new primary dataset @@ -743,10 +743,7 @@ def persist_elements_to_folder(model_persistence_context, elements, library_fold sources = fields_match.sources hashes = fields_match.hashes created_from_basename = fields_match.created_from_basename - state = "ok" - if hasattr(discovered_file, "error_message"): - info = discovered_file.error_message - state = "error" + info, state = discovered_file.discovered_state(element) model_persistence_context.create_dataset( ext=ext, designation=designation, @@ -779,13 +776,14 @@ def persist_hdas(elements, model_persistence_context, final_job_state="ok"): designation = fields_match.designation ext = fields_match.ext dbkey = fields_match.dbkey - info = element.get("info", None) + tag_list = element.get("tags") link_data = discovered_file.match.link_data # Create new primary dataset name = fields_match.name or designation hda_id = discovered_file.match.object_id + primary_dataset = None if hda_id: sa_session = ( @@ -797,10 +795,8 @@ def persist_hdas(elements, model_persistence_context, final_job_state="ok"): hashes = fields_match.hashes created_from_basename = fields_match.created_from_basename extra_files = fields_match.extra_files - state = final_job_state - if hasattr(discovered_file, "error_message"): - state = "error" - info = discovered_file.error_message + + info, state = discovered_file.discovered_state(element, final_job_state) dataset = model_persistence_context.create_dataset( ext=ext, designation=designation, @@ -810,6 +806,7 @@ def persist_hdas(elements, model_persistence_context, final_job_state="ok"): filename=discovered_file.path, extra_files=extra_files, info=info, + tag_list=tag_list, link_data=link_data, primary_data=primary_dataset, sources=sources, @@ -882,7 +879,22 @@ def replace_request_syntax_sugar(obj): obj["hashes"].extend(new_hashes) -DiscoveredFile = namedtuple("DiscoveredFile", ["path", "collector", "match"]) +class DiscoveredFile(NamedTuple): + path: str + collector: Optional[CollectorT] + match: "JsonCollectedDatasetMatch" + + def discovered_state(self, element: Dict[str, Any], final_job_state="ok") -> "DiscoveredResultState": + info = element.get("info", None) + return DiscoveredResultState(info, final_job_state) + + +class DiscoveredResultState(NamedTuple): + info: Optional[str] + state: str + + +DiscoveredResult = Union[DiscoveredFile, "DiscoveredFileError"] def discovered_file_for_element( @@ -890,7 +902,7 @@ def discovered_file_for_element( model_persistence_context: Union["JobContext", "SessionlessJobContext", SessionlessModelPersistenceContext], parent_identifiers=None, collector=None, -): +) -> DiscoveredResult: model_persistence_context.increment_discovered_file_count() parent_identifiers = parent_identifiers or [] target_directory = discover_target_directory( @@ -935,7 +947,7 @@ def discover_target_directory(dir_name, job_working_directory): class JsonCollectedDatasetMatch: - def __init__(self, as_dict, collector, filename, path=None, parent_identifiers=None): + def __init__(self, as_dict, collector: Optional[CollectorT], filename, path=None, parent_identifiers=None): parent_identifiers = parent_identifiers or [] self.as_dict = as_dict self.collector = collector @@ -1025,12 +1037,16 @@ class JsonCollectedDatasetMatch: class RegexCollectedDatasetMatch(JsonCollectedDatasetMatch): - def __init__(self, re_match, collector, filename, path=None): + def __init__(self, re_match, collector: Optional[CollectorT], filename, path=None): super().__init__(re_match.groupdict(), collector, filename, path=path) class DiscoveredFileError(NamedTuple): error_message: str - collector: Any # TODO: setup interface for this + collector: Optional[CollectorT] match: JsonCollectedDatasetMatch path: Optional[str] = None + + def discovered_state(self, element: Dict[str, Any], final_job_state="ok") -> DiscoveredResultState: + info = self.error_message + return DiscoveredResultState(info, "error") diff --git a/lib/galaxy/model/tags.py b/lib/galaxy/model/tags.py index cd02f4d54c2..bcd078e5ba4 100644 --- a/lib/galaxy/model/tags.py +++ b/lib/galaxy/model/tags.py @@ -256,8 +256,10 @@ class TagHandler: def _create_tag_instance(self, tag_name): # For good performance caller should first check if there's already an appropriate tag - Session = sessionmaker(self.sa_session.bind) tag = galaxy.model.Tag(type=0, name=tag_name) + if not self.sa_session: + return tag + Session = sessionmaker(self.sa_session.bind) with Session() as separate_session: separate_session.add(tag) try: @@ -437,7 +439,7 @@ class GalaxySessionlessTagHandler(GalaxyTagHandlerSession): return self.created_tags.get(tag_name) def get_tag_by_name(self, tag_name): - self.created_tags.get(tag_name) + return self.created_tags.get(tag_name) class CommunityTagHandler(TagHandler): diff --git a/lib/galaxy/model/tool_shed_install/mapping.py b/lib/galaxy/model/tool_shed_install/mapping.py index ec5586d9da9..acef2ca8c77 100644 --- a/lib/galaxy/model/tool_shed_install/mapping.py +++ b/lib/galaxy/model/tool_shed_install/mapping.py @@ -7,15 +7,16 @@ metadata = mapper_registry.metadata def init(url, engine_options=None, create_tables=False): - """Connect mappings to the database""" - # Load the appropriate db module - engine_options = engine_options or {} engine = build_engine(url, engine_options) - result = ModelMapping([install_model], engine=engine) - # Create tables if needed if create_tables: - metadata.create_all(bind=engine) - # metadata.engine.commit() - result.create_tables = create_tables - # load local galaxy security policy - return result + create_database_objects(engine) + return configure_model_mapping(engine) + + +def create_database_objects(engine): + mapper_registry.metadata.create_all(bind=engine) + + +def configure_model_mapping(engine): + # TODO: do we need to load local galaxy security policy? + return ModelMapping([install_model], engine=engine) diff --git a/lib/galaxy/model/tool_shed_install/migrate/check.py b/lib/galaxy/model/tool_shed_install/migrate/check.py deleted file mode 100644 index 199b31b8bdf..00000000000 --- a/lib/galaxy/model/tool_shed_install/migrate/check.py +++ /dev/null @@ -1,120 +0,0 @@ -import logging -import os.path -import sys - -from migrate.versioning import ( - repository, - schema, -) -from sqlalchemy import ( - create_engine, - MetaData, - Table, -) -from sqlalchemy.exc import NoSuchTableError - -from galaxy.model.database_utils import ( - create_database, - database_exists, -) -from galaxy.model.tool_shed_install import mapping - -log = logging.getLogger(__name__) - -# path relative to galaxy -migrate_repository_directory = os.path.abspath(os.path.dirname(__file__)).replace(os.getcwd() + os.path.sep, "", 1) -migrate_repository = repository.Repository(migrate_repository_directory) - - -def create_or_verify_database(url, engine_options=None, app=None): - """ """ - # Create engine and metadata - engine_options = engine_options or {} - if not database_exists(url): - message = f"Creating database for URI [{url}]" - log.info(message) - create_database(url) - - engine = create_engine(url, **engine_options) - - def migrate(): - try: - # Declare the database to be under a repository's version control - db_schema = schema.ControlledSchema.create(engine, migrate_repository) - except Exception: - # The database is already under version control - db_schema = schema.ControlledSchema(engine, migrate_repository) - # Apply all scripts to get to current version - migrate_to_current_version(engine, db_schema) - - meta = MetaData(bind=engine) - if app and getattr(app.config, "database_auto_migrate", False): - migrate() - return - - # Try to load tool_shed_repository table - try: - Table("tool_shed_repository", meta, autoload=True) - except NoSuchTableError: - # No table means a completely uninitialized database. If we - # have an app, we'll set its new_installation setting to True - # so the tool migration process will be skipped. - log.info("Creating install database from scratch, skipping migrations") - mapping.init(url=url, create_tables=True) - current_version = migrate_repository.version().version - schema.ControlledSchema.create(engine, migrate_repository, version=current_version) - db_schema = schema.ControlledSchema(engine, migrate_repository) - assert db_schema.version == current_version - migrate() - return - - try: - Table("migrate_version", meta, autoload=True) - except NoSuchTableError: - # The database exists but is not yet under migrate version control, so init with version 1 - log.info("Adding version control to existing database") - try: - Table("metadata_file", meta, autoload=True) - schema.ControlledSchema.create(engine, migrate_repository, version=2) - except NoSuchTableError: - schema.ControlledSchema.create(engine, migrate_repository, version=1) - - # Verify that the code and the DB are in sync - db_schema = schema.ControlledSchema(engine, migrate_repository) - if migrate_repository.versions.latest != db_schema.version: - exception_msg = "Your database has version '%d' but this code expects version '%d'. " % ( - db_schema.version, - migrate_repository.versions.latest, - ) - exception_msg += "Back up your database and then migrate the schema by running the following from your Galaxy installation directory:" - exception_msg += "\n\nsh manage_db.sh upgrade install\n" - - else: - log.info("At database version %d" % db_schema.version) - - -def migrate_to_current_version(engine, schema): - # Changes to get to current version - changeset = schema.changeset(None) - for ver, change in changeset: - nextver = ver + changeset.step - log.info(f"Migrating {ver} -> {nextver}... ") - old_stdout = sys.stdout - - class FakeStdout: - def __init__(self): - self.buffer = [] - - def write(self, s): - self.buffer.append(s) - - def flush(self): - pass - - sys.stdout = FakeStdout() - try: - schema.runchange(ver, change, changeset.step) - finally: - for message in "".join(sys.stdout.buffer).split("\n"): - log.info(message) - sys.stdout = old_stdout diff --git a/lib/galaxy/model/tool_shed_install/migrate/migrate.cfg b/lib/galaxy/model/tool_shed_install/migrate/migrate.cfg deleted file mode 100644 index acfc9879b87..00000000000 --- a/lib/galaxy/model/tool_shed_install/migrate/migrate.cfg +++ /dev/null @@ -1,20 +0,0 @@ -[db_settings] -# Used to identify which repository this database is versioned under. -# You can use the name of your project. -repository_id=ToolShedInstall - -# The name of the database table used to track the schema version. -# This name shouldn't already be used by your project. -# If this is changed once a database is under version control, you'll need to -# change the table name in each database too. -version_table=migrate_version - -# When committing a change script, Migrate will attempt to generate the -# sql for all supported databases; normally, if one of them fails - probably -# because you don't have that database installed - it is ignored and the -# commit continues, perhaps ending successfully. -# Databases in this list MUST compile successfully during a commit, or the -# entire commit will fail. List the databases your application will actually -# be using to ensure your updates to that database work properly. -# This must be a list; example: ['postgres','sqlite'] -required_dbs=[] diff --git a/lib/galaxy/model/tool_shed_install/migrate/versions/0001_add_tool_shed_repository_table.py b/lib/galaxy/model/tool_shed_install/migrate/versions/0001_add_tool_shed_repository_table.py deleted file mode 120000 index e0b183100f2..00000000000 --- a/lib/galaxy/model/tool_shed_install/migrate/versions/0001_add_tool_shed_repository_table.py +++ /dev/null @@ -1 +0,0 @@ -../../../migrate/versions/0082_add_tool_shed_repository_table.py \ No newline at end of file diff --git a/lib/galaxy/model/tool_shed_install/migrate/versions/0002_add_tool_shed_repository_table_columns.py b/lib/galaxy/model/tool_shed_install/migrate/versions/0002_add_tool_shed_repository_table_columns.py deleted file mode 120000 index 7cacf92509e..00000000000 --- a/lib/galaxy/model/tool_shed_install/migrate/versions/0002_add_tool_shed_repository_table_columns.py +++ /dev/null @@ -1 +0,0 @@ -../../../migrate/versions/0086_add_tool_shed_repository_table_columns.py \ No newline at end of file diff --git a/lib/galaxy/model/tool_shed_install/migrate/versions/0003_tool_id_guid_map_table.py b/lib/galaxy/model/tool_shed_install/migrate/versions/0003_tool_id_guid_map_table.py deleted file mode 120000 index 3e9dea9c20a..00000000000 --- a/lib/galaxy/model/tool_shed_install/migrate/versions/0003_tool_id_guid_map_table.py +++ /dev/null @@ -1 +0,0 @@ -../../../migrate/versions/0087_tool_id_guid_map_table.py \ No newline at end of file diff --git a/lib/galaxy/model/tool_shed_install/migrate/versions/0004_add_installed_changeset_revison_column.py b/lib/galaxy/model/tool_shed_install/migrate/versions/0004_add_installed_changeset_revison_column.py deleted file mode 120000 index 4e57eb188b8..00000000000 --- a/lib/galaxy/model/tool_shed_install/migrate/versions/0004_add_installed_changeset_revison_column.py +++ /dev/null @@ -1 +0,0 @@ -../../../migrate/versions/0088_add_installed_changeset_revison_column.py \ No newline at end of file diff --git a/lib/galaxy/model/tool_shed_install/migrate/versions/0005_add_tool_shed_repository_table_columns.py b/lib/galaxy/model/tool_shed_install/migrate/versions/0005_add_tool_shed_repository_table_columns.py deleted file mode 120000 index 6359c3b9bbb..00000000000 --- a/lib/galaxy/model/tool_shed_install/migrate/versions/0005_add_tool_shed_repository_table_columns.py +++ /dev/null @@ -1 +0,0 @@ -../../../migrate/versions/0090_add_tool_shed_repository_table_columns.py \ No newline at end of file diff --git a/lib/galaxy/model/tool_shed_install/migrate/versions/0006_add_tool_version_tables.py b/lib/galaxy/model/tool_shed_install/migrate/versions/0006_add_tool_version_tables.py deleted file mode 120000 index 9a4526d9b96..00000000000 --- a/lib/galaxy/model/tool_shed_install/migrate/versions/0006_add_tool_version_tables.py +++ /dev/null @@ -1 +0,0 @@ -../../../migrate/versions/0091_add_tool_version_tables.py \ No newline at end of file diff --git a/lib/galaxy/model/tool_shed_install/migrate/versions/0007_add_migrate_tools_table.py b/lib/galaxy/model/tool_shed_install/migrate/versions/0007_add_migrate_tools_table.py deleted file mode 120000 index 9fbb816a5cf..00000000000 --- a/lib/galaxy/model/tool_shed_install/migrate/versions/0007_add_migrate_tools_table.py +++ /dev/null @@ -1 +0,0 @@ -../../../migrate/versions/0092_add_migrate_tools_table.py \ No newline at end of file diff --git a/lib/galaxy/model/tool_shed_install/migrate/versions/0008_add_ctx_rev_column.py b/lib/galaxy/model/tool_shed_install/migrate/versions/0008_add_ctx_rev_column.py deleted file mode 120000 index a1268f1ff03..00000000000 --- a/lib/galaxy/model/tool_shed_install/migrate/versions/0008_add_ctx_rev_column.py +++ /dev/null @@ -1 +0,0 @@ -../../../migrate/versions/0097_add_ctx_rev_column.py \ No newline at end of file diff --git a/lib/galaxy/model/tool_shed_install/migrate/versions/0009_add_tool_dependency_table.py b/lib/galaxy/model/tool_shed_install/migrate/versions/0009_add_tool_dependency_table.py deleted file mode 120000 index a119ddd378f..00000000000 --- a/lib/galaxy/model/tool_shed_install/migrate/versions/0009_add_tool_dependency_table.py +++ /dev/null @@ -1 +0,0 @@ -../../../migrate/versions/0099_add_tool_dependency_table.py \ No newline at end of file diff --git a/lib/galaxy/model/tool_shed_install/migrate/versions/0010_alter_tool_dependency_table_version_column.py b/lib/galaxy/model/tool_shed_install/migrate/versions/0010_alter_tool_dependency_table_version_column.py deleted file mode 120000 index 1a39c5872a2..00000000000 --- a/lib/galaxy/model/tool_shed_install/migrate/versions/0010_alter_tool_dependency_table_version_column.py +++ /dev/null @@ -1 +0,0 @@ -../../../migrate/versions/0100_alter_tool_dependency_table_version_column.py \ No newline at end of file diff --git a/lib/galaxy/model/tool_shed_install/migrate/versions/0011_drop_installed_changeset_revision_column.py b/lib/galaxy/model/tool_shed_install/migrate/versions/0011_drop_installed_changeset_revision_column.py deleted file mode 120000 index eed3a389c27..00000000000 --- a/lib/galaxy/model/tool_shed_install/migrate/versions/0011_drop_installed_changeset_revision_column.py +++ /dev/null @@ -1 +0,0 @@ -../../../migrate/versions/0101_drop_installed_changeset_revision_column.py \ No newline at end of file diff --git a/lib/galaxy/model/tool_shed_install/migrate/versions/0012_add_tool_dependency_status_columns.py b/lib/galaxy/model/tool_shed_install/migrate/versions/0012_add_tool_dependency_status_columns.py deleted file mode 120000 index c5759e0e88d..00000000000 --- a/lib/galaxy/model/tool_shed_install/migrate/versions/0012_add_tool_dependency_status_columns.py +++ /dev/null @@ -1 +0,0 @@ -../../../migrate/versions/0102_add_tool_dependency_status_columns.py \ No newline at end of file diff --git a/lib/galaxy/model/tool_shed_install/migrate/versions/0013_add_tool_shed_repository_status_columns.py b/lib/galaxy/model/tool_shed_install/migrate/versions/0013_add_tool_shed_repository_status_columns.py deleted file mode 120000 index c17fdea9421..00000000000 --- a/lib/galaxy/model/tool_shed_install/migrate/versions/0013_add_tool_shed_repository_status_columns.py +++ /dev/null @@ -1 +0,0 @@ -../../../migrate/versions/0103_add_tool_shed_repository_status_columns.py \ No newline at end of file diff --git a/lib/galaxy/model/tool_shed_install/migrate/versions/0014_add_repository_dependency_tables.py b/lib/galaxy/model/tool_shed_install/migrate/versions/0014_add_repository_dependency_tables.py deleted file mode 120000 index 5f07fdf3a63..00000000000 --- a/lib/galaxy/model/tool_shed_install/migrate/versions/0014_add_repository_dependency_tables.py +++ /dev/null @@ -1 +0,0 @@ -../../../migrate/versions/0109_add_repository_dependency_tables.py \ No newline at end of file diff --git a/lib/galaxy/model/tool_shed_install/migrate/versions/0015_update_migrate_tools_table.py b/lib/galaxy/model/tool_shed_install/migrate/versions/0015_update_migrate_tools_table.py deleted file mode 120000 index 55c1ced6cd5..00000000000 --- a/lib/galaxy/model/tool_shed_install/migrate/versions/0015_update_migrate_tools_table.py +++ /dev/null @@ -1 +0,0 @@ -../../../migrate/versions/0113_update_migrate_tools_table.py \ No newline at end of file diff --git a/lib/galaxy/model/tool_shed_install/migrate/versions/0016_update_migrate_tools_table_again.py b/lib/galaxy/model/tool_shed_install/migrate/versions/0016_update_migrate_tools_table_again.py deleted file mode 120000 index 7af8a2a1890..00000000000 --- a/lib/galaxy/model/tool_shed_install/migrate/versions/0016_update_migrate_tools_table_again.py +++ /dev/null @@ -1 +0,0 @@ -../../../migrate/versions/0114_update_migrate_tools_table_again.py \ No newline at end of file diff --git a/lib/galaxy/model/tool_shed_install/migrate/versions/0017_drop_update_available_col_add_tool_shed_status_col.py b/lib/galaxy/model/tool_shed_install/migrate/versions/0017_drop_update_available_col_add_tool_shed_status_col.py deleted file mode 120000 index d874f486173..00000000000 --- a/lib/galaxy/model/tool_shed_install/migrate/versions/0017_drop_update_available_col_add_tool_shed_status_col.py +++ /dev/null @@ -1 +0,0 @@ -../../../migrate/versions/0116_drop_update_available_col_add_tool_shed_status_col.py \ No newline at end of file diff --git a/lib/galaxy/model/tool_shed_install/migrate/versions/__init__.py b/lib/galaxy/model/tool_shed_install/migrate/versions/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/lib/galaxy/model/migrate/__init__.py b/lib/galaxy/model/triggers/__init__.py similarity index 100% rename from lib/galaxy/model/migrate/__init__.py rename to lib/galaxy/model/triggers/__init__.py diff --git a/lib/galaxy/model/migrate/triggers/history_update_time_field.py b/lib/galaxy/model/triggers/history_update_time_field.py similarity index 98% rename from lib/galaxy/model/migrate/triggers/history_update_time_field.py rename to lib/galaxy/model/triggers/history_update_time_field.py index f25208eeae1..91315ae8289 100644 --- a/lib/galaxy/model/migrate/triggers/history_update_time_field.py +++ b/lib/galaxy/model/triggers/history_update_time_field.py @@ -2,7 +2,7 @@ Database trigger installation and removal """ -from galaxy.model.migrate.versions.util import execute_statements +from galaxy.model.triggers.utils import execute_statements def install_timestamp_triggers(engine): diff --git a/lib/galaxy/model/migrate/triggers/update_audit_table.py b/lib/galaxy/model/triggers/update_audit_table.py similarity index 98% rename from lib/galaxy/model/migrate/triggers/update_audit_table.py rename to lib/galaxy/model/triggers/update_audit_table.py index 59f69f6f5ac..1f88fe069ae 100644 --- a/lib/galaxy/model/migrate/triggers/update_audit_table.py +++ b/lib/galaxy/model/triggers/update_audit_table.py @@ -1,4 +1,4 @@ -from galaxy.model.migrate.versions.util import execute_statements +from galaxy.model.triggers.utils import execute_statements # function name prefix fn_prefix = "fn_audit_history_by" diff --git a/lib/galaxy/model/triggers/utils.py b/lib/galaxy/model/triggers/utils.py new file mode 100644 index 00000000000..6644d601697 --- /dev/null +++ b/lib/galaxy/model/triggers/utils.py @@ -0,0 +1,9 @@ +from sqlalchemy import DDL + + +def execute_statements(engine, raw_sql): + statements = raw_sql if isinstance(raw_sql, list) else [raw_sql] + with engine.begin() as connection: + for sql in statements: + cmd = DDL(sql) + connection.execute(cmd) diff --git a/lib/galaxy/model/unittest_utils/data_app.py b/lib/galaxy/model/unittest_utils/data_app.py index 5ff5dc3de92..fb334da88e7 100644 --- a/lib/galaxy/model/unittest_utils/data_app.py +++ b/lib/galaxy/model/unittest_utils/data_app.py @@ -8,6 +8,7 @@ galaxy-data. import os import shutil import tempfile +from typing import Optional from galaxy import ( model, @@ -19,6 +20,7 @@ from galaxy.model.mapping import ( init, ) from galaxy.model.security import GalaxyRBACAgent +from galaxy.model.tags import GalaxyTagHandler from galaxy.security.idencoding import IdEncodingHelper from galaxy.util.bunch import Bunch @@ -61,6 +63,7 @@ class GalaxyDataTestConfig(Bunch): self.jobs_directory = os.path.join(self.data_dir, "jobs_directory") self.new_file_path = os.path.join(self.data_dir, "tmp") self.file_path = os.path.join(self.data_dir, "files") + self.server_name = "main" def __del__(self): if self._remove_root: @@ -74,13 +77,14 @@ class GalaxyDataTestApp: model: GalaxyModelMapping security_agent: GalaxyRBACAgent - def __init__(self, config: GalaxyDataTestConfig = None, **kwd): + def __init__(self, config: Optional[GalaxyDataTestConfig] = None, **kwd): config = config or GalaxyDataTestConfig(**kwd) self.config = config self.security = config.security self.object_store = objectstore.build_object_store_from_config(self.config) self.model = init("/tmp", self.config.database_connection, create_tables=True, object_store=self.object_store) self.security_agent = self.model.security_agent + self.tag_handler = GalaxyTagHandler(self.model.context) self.init_datatypes() def init_datatypes(self): diff --git a/lib/galaxy/objectstore/cloud.py b/lib/galaxy/objectstore/cloud.py index d13cc390676..163df8a73a9 100644 --- a/lib/galaxy/objectstore/cloud.py +++ b/lib/galaxy/objectstore/cloud.py @@ -614,7 +614,7 @@ class Cloud(ConcreteObjectStore, CloudConfigMixin): if self._exists(obj, **kwargs): return bool(self._size(obj, **kwargs) > 0) else: - raise ObjectNotFound("objectstore.empty, object does not exist: %s, kwargs: %s" % (str(obj), str(kwargs))) + raise ObjectNotFound(f"objectstore.empty, object does not exist: {obj}, kwargs: {kwargs}") def _size(self, obj, **kwargs): rel_path = self._construct_path(obj, **kwargs) @@ -711,7 +711,7 @@ class Cloud(ConcreteObjectStore, CloudConfigMixin): # even if it does not exist. # if dir_only: # return cache_path - raise ObjectNotFound("objectstore.get_filename, no cache_path: %s, kwargs: %s" % (str(obj), str(kwargs))) + raise ObjectNotFound(f"objectstore.get_filename, no cache_path: {obj}, kwargs: {kwargs}") # return cache_path # Until the upload tool does not explicitly create the dataset, return expected path def _update_from_file(self, obj, file_name=None, create=False, **kwargs): @@ -736,9 +736,7 @@ class Cloud(ConcreteObjectStore, CloudConfigMixin): # Update the file on cloud self._push_to_os(rel_path, source_file) else: - raise ObjectNotFound( - "objectstore.update_from_file, object does not exist: %s, kwargs: %s" % (str(obj), str(kwargs)) - ) + raise ObjectNotFound(f"objectstore.update_from_file, object does not exist: {obj}, kwargs: {kwargs}") def _get_object_url(self, obj, **kwargs): if self._exists(obj, **kwargs): diff --git a/lib/galaxy/objectstore/irods.py b/lib/galaxy/objectstore/irods.py index f5cb3955fce..702bdc9b137 100644 --- a/lib/galaxy/objectstore/irods.py +++ b/lib/galaxy/objectstore/irods.py @@ -314,8 +314,8 @@ class IRODSObjectStore(DiskObjectStore, CloudConfigMixin): data_object_name = p.stem + p.suffix subcollection_name = p.parent - collection_path = f"{self.home}/{str(subcollection_name)}" - data_object_path = f"{collection_path}/{str(data_object_name)}" + collection_path = f"{self.home}/{subcollection_name}" + data_object_path = f"{collection_path}/{data_object_name}" options = {kw.DEST_RESC_NAME_KW: self.resource} try: @@ -334,8 +334,8 @@ class IRODSObjectStore(DiskObjectStore, CloudConfigMixin): data_object_name = p.stem + p.suffix subcollection_name = p.parent - collection_path = f"{self.home}/{str(subcollection_name)}" - data_object_path = f"{collection_path}/{str(data_object_name)}" + collection_path = f"{self.home}/{subcollection_name}" + data_object_path = f"{collection_path}/{data_object_name}" options = {kw.DEST_RESC_NAME_KW: self.resource} try: @@ -372,8 +372,8 @@ class IRODSObjectStore(DiskObjectStore, CloudConfigMixin): data_object_name = p.stem + p.suffix subcollection_name = p.parent - collection_path = f"{self.home}/{str(subcollection_name)}" - data_object_path = f"{collection_path}/{str(data_object_name)}" + collection_path = f"{self.home}/{subcollection_name}" + data_object_path = f"{collection_path}/{data_object_name}" options = {kw.DEST_RESC_NAME_KW: self.resource} try: @@ -411,8 +411,8 @@ class IRODSObjectStore(DiskObjectStore, CloudConfigMixin): return False # Check if the data object exists in iRODS - collection_path = f"{self.home}/{str(subcollection_name)}" - data_object_path = f"{collection_path}/{str(data_object_name)}" + collection_path = f"{self.home}/{subcollection_name}" + data_object_path = f"{collection_path}/{data_object_name}" exists = False try: @@ -542,7 +542,7 @@ class IRODSObjectStore(DiskObjectStore, CloudConfigMixin): if self._exists(obj, **kwargs): return bool(self._size(obj, **kwargs) > 0) else: - raise ObjectNotFound("objectstore.empty, object does not exist: %s, kwargs: %s" % (str(obj), str(kwargs))) + raise ObjectNotFound(f"objectstore.empty, object does not exist: {obj}, kwargs: {kwargs}") def _size(self, obj, **kwargs): ipt_timer = ExecutionTimer() @@ -584,7 +584,7 @@ class IRODSObjectStore(DiskObjectStore, CloudConfigMixin): if entire_dir and extra_dir: shutil.rmtree(self._get_cache_path(rel_path), ignore_errors=True) - col_path = f"{self.home}/{str(rel_path)}" + col_path = f"{self.home}/{rel_path}" col = None try: col = self.session.collections.get(col_path) @@ -612,8 +612,8 @@ class IRODSObjectStore(DiskObjectStore, CloudConfigMixin): data_object_name = p.stem + p.suffix subcollection_name = p.parent - collection_path = f"{self.home}/{str(subcollection_name)}" - data_object_path = f"{collection_path}/{str(data_object_name)}" + collection_path = f"{self.home}/{subcollection_name}" + data_object_path = f"{collection_path}/{data_object_name}" try: data_obj = self.session.data_objects.get(data_object_path, **options) @@ -682,7 +682,7 @@ class IRODSObjectStore(DiskObjectStore, CloudConfigMixin): # if dir_only: # return cache_path log.debug("irods_pt _get_filename: %s", ipt_timer) - raise ObjectNotFound("objectstore.get_filename, no cache_path: %s, kwargs: %s" % (str(obj), str(kwargs))) + raise ObjectNotFound(f"objectstore.get_filename, no cache_path: {obj}, kwargs: {kwargs}") # return cache_path # Until the upload tool does not explicitly create the dataset, return expected path def _update_from_file(self, obj, file_name=None, create=False, **kwargs): @@ -709,9 +709,7 @@ class IRODSObjectStore(DiskObjectStore, CloudConfigMixin): self._push_to_irods(rel_path, source_file) else: log.debug("irods_pt _update_from_file: %s", ipt_timer) - raise ObjectNotFound( - "objectstore.update_from_file, object does not exist: %s, kwargs: %s" % (str(obj), str(kwargs)) - ) + raise ObjectNotFound(f"objectstore.update_from_file, object does not exist: {obj}, kwargs: {kwargs}") log.debug("irods_pt _update_from_file: %s", ipt_timer) # Unlike S3, url is not really applicable to iRODS @@ -723,8 +721,8 @@ class IRODSObjectStore(DiskObjectStore, CloudConfigMixin): data_object_name = p.stem + p.suffix subcollection_name = p.parent - collection_path = f"{self.home}/{str(subcollection_name)}" - data_object_path = f"{collection_path}/{str(data_object_name)}" + collection_path = f"{self.home}/{subcollection_name}" + data_object_path = f"{collection_path}/{data_object_name}" return data_object_path diff --git a/lib/galaxy/objectstore/s3.py b/lib/galaxy/objectstore/s3.py index 41d18260c3c..f6bf9684e21 100644 --- a/lib/galaxy/objectstore/s3.py +++ b/lib/galaxy/objectstore/s3.py @@ -626,7 +626,7 @@ class S3ObjectStore(ConcreteObjectStore, CloudConfigMixin): if self._exists(obj, **kwargs): return bool(self._size(obj, **kwargs) > 0) else: - raise ObjectNotFound("objectstore.empty, object does not exist: %s, kwargs: %s" % (str(obj), str(kwargs))) + raise ObjectNotFound(f"objectstore.empty, object does not exist: {obj}, kwargs: {kwargs}") def _size(self, obj, **kwargs): rel_path = self._construct_path(obj, **kwargs) @@ -723,7 +723,7 @@ class S3ObjectStore(ConcreteObjectStore, CloudConfigMixin): # even if it does not exist. # if dir_only: # return cache_path - raise ObjectNotFound("objectstore.get_filename, no cache_path: %s, kwargs: %s" % (str(obj), str(kwargs))) + raise ObjectNotFound(f"objectstore.get_filename, no cache_path: {obj}, kwargs: {kwargs}") # return cache_path # Until the upload tool does not explicitly create the dataset, return expected path def _update_from_file(self, obj, file_name=None, create=False, **kwargs): @@ -748,9 +748,7 @@ class S3ObjectStore(ConcreteObjectStore, CloudConfigMixin): # Update the file on S3 self._push_to_os(rel_path, source_file) else: - raise ObjectNotFound( - "objectstore.update_from_file, object does not exist: %s, kwargs: %s" % (str(obj), str(kwargs)) - ) + raise ObjectNotFound(f"objectstore.update_from_file, object does not exist: {obj}, kwargs: {kwargs}") def _get_object_url(self, obj, **kwargs): if self._exists(obj, **kwargs): diff --git a/lib/galaxy/queue_worker.py b/lib/galaxy/queue_worker.py index 2ba2271d442..1dee1f9c3aa 100644 --- a/lib/galaxy/queue_worker.py +++ b/lib/galaxy/queue_worker.py @@ -298,9 +298,7 @@ def admin_job_lock(app, **kwargs): # job_queue is exposed in the root app, but this will be 'fixed' at some # point, so we're using the reference from the handler. app.job_manager.job_lock = job_lock - log.info( - "Administrative Job Lock is now set to %s. Jobs will %s dispatch." % (job_lock, "not" if job_lock else "now") - ) + log.info(f"Administrative Job Lock is now set to {job_lock}. Jobs will {'not' if job_lock else 'now'} dispatch.") control_message_to_task = { diff --git a/lib/galaxy/schema/schema.py b/lib/galaxy/schema/schema.py index 448f7597da0..3a22ddf5579 100644 --- a/lib/galaxy/schema/schema.py +++ b/lib/galaxy/schema/schema.py @@ -581,13 +581,6 @@ class HDAExtended(HDADetailed): ) -class HDABeta(HDADetailed): # TODO: change HDABeta name to a more appropriate one. - """History Dataset Association information used in the new Beta History.""" - - # Equivalent to `betawebclient` serialization view for HDAs - pass - - class DCSummary(Model): """Dataset Collection summary information.""" @@ -663,6 +656,76 @@ class DCDetailed(DCSummary): elements: List[DCESummary] = ElementsField +class HDCJobStateSummary(Model): + """Overview of the job states working inside a dataset collection.""" + + all_jobs: int = Field( + 0, + title="All jobs", + description="Total number of jobs associated with a dataset collection.", + ) + new: int = Field( + 0, + title="New jobs", + description="Number of jobs in the `new` state.", + ) + waiting: int = Field( + 0, + title="Waiting jobs", + description="Number of jobs in the `waiting` state.", + ) + running: int = Field( + 0, + title="Running jobs", + description="Number of jobs in the `running` state.", + ) + error: int = Field( + 0, + title="Jobs with errors", + description="Number of jobs in the `error` state.", + ) + paused: int = Field( + 0, + title="Paused jobs", + description="Number of jobs in the `paused` state.", + ) + deleted_new: int = Field( + 0, + title="Deleted new jobs", + description="Number of jobs in the `deleted_new` state.", + ) + resubmitted: int = Field( + 0, + title="Resubmitted jobs", + description="Number of jobs in the `resubmitted` state.", + ) + queued: int = Field( + 0, + title="Queued jobs", + description="Number of jobs in the `queued` state.", + ) + ok: int = Field( + 0, + title="OK jobs", + description="Number of jobs in the `ok` state.", + ) + failed: int = Field( + 0, + title="Failed jobs", + description="Number of jobs in the `failed` state.", + ) + deleted: int = Field( + 0, + title="Deleted jobs", + description="Number of jobs in the `deleted` state.", + ) + upload: int = Field( + 0, + title="Upload jobs", + description="Number of jobs in the `upload` state.", + ) + + class HDCASummary(HistoryItemCommon): """History Dataset Collection Association summary information.""" @@ -689,7 +752,17 @@ class HDCASummary(HistoryItemCommon): title="Job Source Type", description="The type of job (model class) that produced this dataset collection. Used to track the state of the job.", ) + job_state_summary: Optional[HDCJobStateSummary] = Field( + None, + title="Job State Summary", + description="Overview of the job states working inside the dataset collection.", + ) contents_url: RelativeUrl = ContentsUrlField + collection_id: EncodedDatabaseIdField = Field( + ..., + title="Collection ID", + description="The encoded ID of the dataset collection associated with this HDCA.", + ) class HDCADetailed(HDCASummary): @@ -697,6 +770,9 @@ class HDCADetailed(HDCASummary): populated: bool = PopulatedField elements: List[DCESummary] = ElementsField + elements_datatypes: Set[str] = Field( + ..., description="A set containing all the different element datatypes in the collection." + ) class HistoryBase(BaseModel): @@ -1161,97 +1237,6 @@ class JobIdResponse(BaseModel): ) -class HDCJobStateSummary(Model): - """Overview of the job states working inside a dataset collection.""" - - all_jobs: int = Field( - 0, - title="All jobs", - description="Total number of jobs associated with a dataset collection.", - ) - new: int = Field( - 0, - title="New jobs", - description="Number of jobs in the `new` state.", - ) - waiting: int = Field( - 0, - title="Waiting jobs", - description="Number of jobs in the `waiting` state.", - ) - running: int = Field( - 0, - title="Running jobs", - description="Number of jobs in the `running` state.", - ) - error: int = Field( - 0, - title="Jobs with errors", - description="Number of jobs in the `error` state.", - ) - paused: int = Field( - 0, - title="Paused jobs", - description="Number of jobs in the `paused` state.", - ) - deleted_new: int = Field( - 0, - title="Deleted new jobs", - description="Number of jobs in the `deleted_new` state.", - ) - resubmitted: int = Field( - 0, - title="Resubmitted jobs", - description="Number of jobs in the `resubmitted` state.", - ) - queued: int = Field( - 0, - title="Queued jobs", - description="Number of jobs in the `queued` state.", - ) - ok: int = Field( - 0, - title="OK jobs", - description="Number of jobs in the `ok` state.", - ) - failed: int = Field( - 0, - title="Failed jobs", - description="Number of jobs in the `failed` state.", - ) - deleted: int = Field( - 0, - title="Deleted jobs", - description="Number of jobs in the `deleted` state.", - ) - upload: int = Field( - 0, - title="Upload jobs", - description="Number of jobs in the `upload` state.", - ) - - -class HDCABeta(HDCADetailed): # TODO: change HDCABeta name to a more appropriate one. - """History Dataset Collection Association information used in the new Beta History.""" - - # Equivalent to `betawebclient` serialization view for HDCAs - collection_id: EncodedDatabaseIdField = Field( - # TODO: inconsistency? the equivalent counterpart for HDAs, `dataset_id`, is declared in `HDASummary` scope - # while in HDCAs it is only serialized in the new `betawebclient` view? - ..., - title="Collection ID", - description="The encoded ID of the dataset collection associated with this HDCA.", - ) - job_state_summary: Optional[HDCJobStateSummary] = Field( - None, - title="Job State Summary", - description="Overview of the job states working inside the dataset collection.", - ) - elements_datatypes: Set[str] = Field( - ..., description="A set containing all the different element datatypes in the collection." - ) - - class JobBaseModel(Model): id: EncodedDatabaseIdField = EncodedEntityIdField model_class: str = ModelClassField(JOB_MODEL_CLASS_NAME) @@ -2407,8 +2392,8 @@ class CustomHistoryItem(Model): extra = Extra.allow -AnyHDA = Union[HDABeta, HDADetailed, HDASummary] -AnyHDCA = Union[HDCABeta, HDCADetailed, HDCASummary] +AnyHDA = Union[HDADetailed, HDASummary] +AnyHDCA = Union[HDCADetailed, HDCASummary] AnyHistoryContentItem = Union[ AnyHDA, AnyHDCA, diff --git a/lib/galaxy/schema/tasks.py b/lib/galaxy/schema/tasks.py new file mode 100644 index 00000000000..71b04ec320a --- /dev/null +++ b/lib/galaxy/schema/tasks.py @@ -0,0 +1,10 @@ +from pydantic import BaseModel + + +class SetupHistoryExportJob(BaseModel): + history_id: int + job_id: int + store_directory: str + include_files: bool + include_hidden: bool + include_deleted: bool diff --git a/lib/galaxy/security/vault.py b/lib/galaxy/security/vault.py index 104b90e2763..7c3ae3370a6 100644 --- a/lib/galaxy/security/vault.py +++ b/lib/galaxy/security/vault.py @@ -260,7 +260,7 @@ class VaultKeyPrefixWrapper(Vault): raise NotImplementedError() -class VaultFactory(object): +class VaultFactory: @staticmethod def load_vault_config(vault_conf_yml: str) -> Optional[dict]: if os.path.exists(vault_conf_yml): diff --git a/lib/galaxy/selenium/data.py b/lib/galaxy/selenium/data.py index 80a8ea7f969..31b8b0d3c9e 100644 --- a/lib/galaxy/selenium/data.py +++ b/lib/galaxy/selenium/data.py @@ -1,10 +1,10 @@ import yaml -from pkg_resources import resource_string +from galaxy.util.resources import resource_string from .components import Component def load_root_component() -> Component: - new_data_yaml = resource_string(__name__, "navigation.yml").decode("UTF-8") + new_data_yaml = resource_string(__package__, "navigation.yml") navigation_raw = yaml.safe_load(new_data_yaml) return Component.from_dict("root", navigation_raw) diff --git a/lib/galaxy/selenium/navigates_galaxy.py b/lib/galaxy/selenium/navigates_galaxy.py index 2c44da5810b..e47b3af9af6 100644 --- a/lib/galaxy/selenium/navigates_galaxy.py +++ b/lib/galaxy/selenium/navigates_galaxy.py @@ -134,7 +134,7 @@ def retry_during_transitions( return _retry -def edit_details(f, scope=".beta.history"): +def edit_details(f, scope=".history-index"): """Open the editor, run the edits, hit the save button""" @wraps(f) @@ -1012,6 +1012,11 @@ class NavigatesGalaxy(HasDriver): self.click_masthead_user() self.components.masthead.invocations.wait_for_and_click() + def navigate_to_pages(self): + self.home() + self.click_masthead_user() + self.components.masthead.pages.wait_for_and_click() + def admin_open(self): self.components.masthead.admin.wait_for_and_click() @@ -1238,7 +1243,7 @@ class NavigatesGalaxy(HasDriver): self.send_enter(tag_area) def workflow_run_submit(self): - self.wait_for_and_click_selector("#run-workflow") + self.wait_for_and_click_selector("button.btn-primary") def workflow_create_new(self, annotation=None, clear_placeholder=False): self.workflow_index_open() @@ -1265,6 +1270,27 @@ class NavigatesGalaxy(HasDriver): self.driver.execute_script("arguments[0].scrollIntoView(true);", tool_element) tool_link.wait_for_and_click() + def create_page_and_edit(self, name=None, slug=None, content_format=None, screenshot_name=None): + name = self.create_page(name=name, slug=slug, content_format=content_format, screenshot_name=screenshot_name) + self.click_grid_popup_option(name, "Edit content") + self.components.pages.editor.wym_iframe.wait_for_visible() + return name + + def create_page(self, name=None, slug=None, content_format=None, screenshot_name=None): + self.components.pages.create.wait_for_and_click() + name = name or self._get_random_name(prefix="page") + slug = slug = self._get_random_name(prefix="pageslug") + content_format = content_format or "HTML" + self.tool_set_value("title", name) + self.tool_set_value("slug", slug) + self.tool_set_value("content_format", content_format, expected_type="select") + if screenshot_name: + self.screenshot(screenshot_name) + # Sometimes 'submit' button not yet hooked up? + self.sleep_for(self.wait_types.UX_RENDER) + self.components.pages.submit.wait_for_and_click() + return name + def tool_parameter_div(self, expanded_parameter_id): return self.components.tool_form.parameter_div(parameter=expanded_parameter_id).wait_for_clickable() @@ -1342,7 +1368,7 @@ class NavigatesGalaxy(HasDriver): return not self.components.history_panel.beta.is_absent # avoids problematic ID and classes on markup - def beta_history_element(self, attribute_value, attribute_name="data-description", scope=".beta.history"): + def beta_history_element(self, attribute_value, attribute_name="data-description", scope=".history-index"): return self.components._.by_attribute(name=attribute_name, value=attribute_value, scope=scope) # join list of attrs into css attribute selectors and append to base beta_item selector @@ -1852,7 +1878,7 @@ class NavigatesGalaxy(HasDriver): def snapshot(self, description): """Test case subclass overrides this to provide detailed logging.""" - def open_history_editor(self, scope=".beta.history"): + def open_history_editor(self, scope=".history-index"): if self.is_beta_history(): panel = self.components.history_panel.editor.selector(scope=scope) toggle = panel.toggle @@ -1860,7 +1886,7 @@ class NavigatesGalaxy(HasDriver): editor = panel.form editor.wait_for_present() - def close_history_editor(self, scope=".beta.history"): + def close_history_editor(self, scope=".history-index"): if self.is_beta_history(): toggle = self.components.history_panel.edit_toggle toggle.wait_for_and_click() diff --git a/lib/galaxy/selenium/navigation.yml b/lib/galaxy/selenium/navigation.yml index ebc97f3e2b7..dae911eb35b 100644 --- a/lib/galaxy/selenium/navigation.yml +++ b/lib/galaxy/selenium/navigation.yml @@ -154,7 +154,7 @@ history_panel: content_item: selectors: # specific rows selectable with attributes via suffix - _: '.beta.history .content-item${suffix}' + _: '.history-index .content-item${suffix}' title: '${_} .content-title' details: '${_} .details' summary: '${_} .summary' @@ -177,7 +177,7 @@ history_panel: info_button: '${_} .params-btn' nametags: '${_} .nametags .badge-tags' - dataset_operations_dropdown: '${_} .dataset-operations-dropdown' + dataset_operations_dropdown: '${_} .dataset-actions' # re-usable history editor, scoped for use in different layout scenarios (multi, etc.) editor: @@ -193,10 +193,11 @@ history_panel: # beta history tags tag_editor: selectors: - _: '${scope} .history-details .tags' + _: '${scope} .details .tags' toggle: '${_} .toggle-link' display: '${_} .tag-area .tag-name' input: '${_} input' + tag_area: '${_} .tag-area' tag_close_btn: '${_} .ti-icon-close' multi_operations: @@ -226,7 +227,7 @@ history_panel: list_items_beta: '.dataset-collection-panel .listing .content-item' selectors: - beta: '.beta.history' + beta: '.history-index' _: '#current-history-panel' search: '#current-history-panel input.search-query' refresh_button: '.history-refresh-button' @@ -240,25 +241,25 @@ history_panel: empty_message: '.empty-message' size: '.history-size' tag_icon: '.actions .history-tag-btn' - tag_area: '.history-details .tags-display' - tag_area_input: '.history-details .tags-display .tags-input input' + tag_area: '.details .tags-display' + tag_area_input: '.details .tags-display .tags-input input' tag_close_btn: '.tags-display .ti-icon-close' tags: 'li.ti-tag.ti-valid .tag-name' annotation_icon: '.actions .history-annotate-btn' - annotation_area: '.history-details .history-annotation' + annotation_area: '.details .history-annotation' annotation_editable_text: - selector: 'history annotation' + selector: 'annotation value' type: data-description annotation_edit: selector: 'annotation input' type: data-description - annotation_done: '.history-details .history-annotation .annotation button' + annotation_done: '.details .history-annotation .annotation button' options_button: '#history-options-button' - options_button_icon: '.history-details .menu-expand-button' - options_button_icon_beta: '.history-details .menu-expand-button' + options_button_icon: '.details .menu-expand-button' + options_button_icon_beta: '.details .menu-expand-button' options_button_copy_datasets_beta: '.copy-datasets-menu-item' options_menu: '#history-options-button-menu' options_menu_item: @@ -441,6 +442,7 @@ pages: selectors: create: '.manage-table-actions .action-button' submit: '#submit' + export: '.markdown-pdf-export' editor: selectors: wym_iframe: 'div.wym_iframe iframe' @@ -449,7 +451,7 @@ pages: embed_button: '#embed-galaxy-object' dataset_selector: '.saved-datasets' embed_dialog_add_button: '.pages-embed .buttons #button-0' - + markdown_editor: '.markdown-textarea' labels: embed_dataset: 'Embed Dataset' @@ -526,6 +528,9 @@ workflow_run: subworkflow_step_icon: ".portlet-title-icon.fa-sitemap" run_workflow: "#run-workflow" validation_error: ".validation-error" + input_select_field: + type: xpath + selector: '//div[@data-label="${label}"]//span[@class="select2-chosen"]' workflow_editor: @@ -628,7 +633,7 @@ invocations: step_output_collection_toggle: '.workflow-invocation-state-component .invocation-steps-details [data-step="${order_index}"] .invocation-step-output-collection-details .name' step_output_collection_element_identifier: type: xpath - selector: '//span[@class="name"][text()="${element_identifier}"]' + selector: '//span[@class="content-title name"][text()="${element_identifier}"]' step_output_collection_element_datatype: '.workflow-invocation-state-component .invocation-steps-details [data-step="${order_index}"] .invocation-step-output-collection-details .datatype .value' step_job_details: '.workflow-invocation-state-component .invocation-steps-details [data-step="${order_index}"] .invocation-step-job-details' step_job_table: '.workflow-invocation-state-component .invocation-steps-details [data-step="${order_index}"] .invocation-step-job-details table' diff --git a/lib/galaxy/structured_app.py b/lib/galaxy/structured_app.py index 31d98c0bce1..cac460811cb 100644 --- a/lib/galaxy/structured_app.py +++ b/lib/galaxy/structured_app.py @@ -72,7 +72,6 @@ class MinimalToolApp(BasicApp): class MinimalApp(BasicSharedApp): is_webapp: bool # is_webapp will be set to true when building WSGI app - new_installation: bool tag_handler: GalaxyTagHandler model: GalaxyModelMapping install_model: ModelMapping @@ -115,7 +114,6 @@ class StructuredApp(MinimalManagerApp): """ is_webapp: bool # is_webapp will be set to true when building WSGI app - new_installation: bool tag_handler: GalaxyTagHandler amqp_internal_connection_obj: Optional[Connection] dependency_resolvers_view: DependencyResolversView diff --git a/lib/galaxy/tool_shed/galaxy_install/install_manager.py b/lib/galaxy/tool_shed/galaxy_install/install_manager.py index 036d31c041b..5dc27715983 100644 --- a/lib/galaxy/tool_shed/galaxy_install/install_manager.py +++ b/lib/galaxy/tool_shed/galaxy_install/install_manager.py @@ -181,7 +181,7 @@ class InstallToolDependencyManager: # Parse the tool_dependencies.xml config. tree, error_message = xml_util.parse_xml(tool_dependencies_config) if tree is None: - log.debug(f"The received tool_dependencies.xml file is likely invalid: {str(error_message)}") + log.debug(f"The received tool_dependencies.xml file is likely invalid: {error_message}") return installed_packages root = tree.getroot() elems = [] @@ -228,11 +228,7 @@ class InstallToolDependencyManager: elem, tool_shed_repository, tool_dependencies=tool_dependencies ) except Exception as e: - error_message = "Error installing tool dependency %s version %s: %s" % ( - str(name), - str(version), - str(e), - ) + error_message = f"Error installing tool dependency {name} version {version}: {e}" log.exception(error_message) if tool_dependency: # Since there was an installation error, update the tool dependency status to Error. The @@ -553,12 +549,7 @@ class InstallRepositoryManager: ) except Exception: message = "Error attempting to retrieve installation information from tool shed " - message += "%s for revision %s of repository %s owned by %s" % ( - tool_shed_url, - changeset_revision, - name, - owner, - ) + message += f"{tool_shed_url} for revision {changeset_revision} of repository {name} owned by {owner}" log.exception(message) raise exceptions.InternalServerError(message) if raw_text: diff --git a/lib/galaxy/tool_shed/galaxy_install/installed_repository_manager.py b/lib/galaxy/tool_shed/galaxy_install/installed_repository_manager.py index 88fe03c8c3b..d99e7bd3263 100644 --- a/lib/galaxy/tool_shed/galaxy_install/installed_repository_manager.py +++ b/lib/galaxy/tool_shed/galaxy_install/installed_repository_manager.py @@ -729,10 +729,10 @@ class InstalledRepositoryManager: try: # Remove the repository from disk. shutil.rmtree(repository_install_dir) - log.debug(f"Removed repository installation directory: {str(repository_install_dir)}") + log.debug(f"Removed repository installation directory: {repository_install_dir}") removed = True except Exception as e: - log.debug(f"Error removing repository installation directory {str(repository_install_dir)}: {str(e)}") + log.debug(f"Error removing repository installation directory {repository_install_dir}: {e}") if isinstance(e, OSError) and not os.path.exists(repository_install_dir): removed = True log.debug("Repository directory does not exist on disk, marking as uninstalled.") @@ -874,10 +874,7 @@ class InstalledRepositoryManager: # Purge the repository. sa_session.delete(repository) sa_session.flush() - message = "The repository named %s with status %s has been purged.
    " % ( - str(repository.name), - str(repository.status), - ) + message = f"The repository named {repository.name} with status {repository.status} has been purged.
    " message += "Total associated tool_version records purged: %d
    " % purged_tool_versions message += "Total associated tool_dependency records purged: %d
    " % purged_tool_dependencies message += ( @@ -895,7 +892,7 @@ class InstalledRepositoryManager: else: status = "error" message = "A repository must have the status New in order to be purged. This repository has " - message += f" the status {str(repository.status)}." + message += f" the status {repository.status}." return status, message def remove_entry_from_installed_repository_dependencies_of_installed_repositories(self, repository): @@ -909,17 +906,11 @@ class InstalledRepositoryManager: altered_installed_dependent_repositories_of_installed_repositories = {} for r_tup, v_tups in self.installed_dependent_repositories_of_installed_repositories.items(): if repository_tup in v_tups: - debug_msg = "Removing entry for revision %s of repository %s owned by %s " % ( - installed_changeset_revision, - name, - owner, + debug_msg = ( + f"Removing entry for revision {installed_changeset_revision} of repository {name} owned by {owner} " ) r_tool_shed, r_name, r_owner, r_installed_changeset_revision = r_tup - debug_msg += "from the dependent list for revision %s of repository %s owned by %s " % ( - r_installed_changeset_revision, - r_name, - r_owner, - ) + debug_msg += f"from the dependent list for revision {r_installed_changeset_revision} of repository {r_name} owned by {r_owner} " debug_msg += "in installed_repository_dependencies_of_installed_repositories." log.debug(debug_msg) v_tups.remove(repository_tup) diff --git a/lib/galaxy/tool_shed/galaxy_install/metadata/installed_repository_metadata_manager.py b/lib/galaxy/tool_shed/galaxy_install/metadata/installed_repository_metadata_manager.py index db5717fe946..05cce5680ae 100644 --- a/lib/galaxy/tool_shed/galaxy_install/metadata/installed_repository_metadata_manager.py +++ b/lib/galaxy/tool_shed/galaxy_install/metadata/installed_repository_metadata_manager.py @@ -166,7 +166,7 @@ class InstalledRepositoryMetadataManager(MetadataGenerator): inflector.cond_plural(successful_count, "repository"), ) if unsuccessful_count: - message += "Error setting metadata on %d %s - see the paster log for details. " % ( + message += "Error setting metadata on %d %s - see the galaxy log for details. " % ( unsuccessful_count, inflector.cond_plural(unsuccessful_count, "repository"), ) diff --git a/lib/galaxy/tool_shed/galaxy_install/tool_dependencies/env_manager.py b/lib/galaxy/tool_shed/galaxy_install/tool_dependencies/env_manager.py index 15514739203..a8bee1b8329 100644 --- a/lib/galaxy/tool_shed/galaxy_install/tool_dependencies/env_manager.py +++ b/lib/galaxy/tool_shed/galaxy_install/tool_dependencies/env_manager.py @@ -93,42 +93,23 @@ class EnvManager: env_shell_file_paths.append(env_shell_file_path) else: error_message = "Skipping tool dependency definition because unable to locate env.sh file for tool dependency " - error_message += "type %s, name %s, version %s for repository %s" % ( - str(tool_dependency_type), - str(tool_dependency_name), - str(tool_dependency_version), - str(repository.name), - ) + error_message += f"type {tool_dependency_type}, name {tool_dependency_name}, version {tool_dependency_version} for repository {repository.name}" log.debug(error_message) continue else: error_message = ( "Skipping tool dependency definition because unable to locate tool dependency " ) - error_message += "type %s, name %s, version %s for repository %s" % ( - str(tool_dependency_type), - str(tool_dependency_name), - str(tool_dependency_version), - str(repository.name), - ) + error_message += f"type {tool_dependency_type}, name {tool_dependency_name}, version {tool_dependency_version} for repository {repository.name}" log.debug(error_message) continue else: - error_message = "Skipping invalid tool dependency definition: type %s, name %s, version %s." % ( - str(tool_dependency_type), - str(tool_dependency_name), - str(tool_dependency_version), - ) + error_message = f"Skipping invalid tool dependency definition: type {tool_dependency_type}, name {tool_dependency_name}, version {tool_dependency_version}." log.debug(error_message) continue else: error_message = "Skipping set_environment_for_install definition because unable to locate required installed tool shed repository: " - error_message += "toolshed %s, name %s, owner %s, changeset_revision %s." % ( - str(toolshed), - str(repository_name), - str(repository_owner), - str(changeset_revision), - ) + error_message += f"toolshed {toolshed}, name {repository_name}, owner {repository_owner}, changeset_revision {changeset_revision}." log.debug(error_message) else: error_message = ( diff --git a/lib/galaxy/tool_shed/galaxy_install/tool_dependencies/recipe/install_environment.py b/lib/galaxy/tool_shed/galaxy_install/tool_dependencies/recipe/install_environment.py index 6525e57ac2f..923bf1b3ed3 100644 --- a/lib/galaxy/tool_shed/galaxy_install/tool_dependencies/recipe/install_environment.py +++ b/lib/galaxy/tool_shed/galaxy_install/tool_dependencies/recipe/install_environment.py @@ -65,11 +65,11 @@ class InstallEnvironment: break except OSError as e: # Undoubtedly close() was called during a concurrent operation on the same file object. - log.debug(f"Error closing file descriptor: {str(e)}") + log.debug(f"Error closing file descriptor: {e}") time.sleep(0.5) current_wait_time = time.time() - start_timer if current_wait_time >= 600: - error = f"Error closing file descriptor: {str(e)}" + error = f"Error closing file descriptor: {e}" break return error @@ -100,7 +100,7 @@ class InstallEnvironment: for env_setting in open(env_shell_file_path): cmds.append(env_setting.strip("\n")) else: - log.debug(f"Invalid file {str(env_shell_file_path)} specified, ignoring {str(action_type)} action.") + log.debug(f"Invalid file {env_shell_file_path} specified, ignoring {action_type} action.") return cmds def environment_dict(self, action_type="template_command"): @@ -112,7 +112,7 @@ class InstallEnvironment: env_name, env_path = env_string.split("=") env_vars[env_name] = env_path else: - log.debug(f"Invalid file {str(env_shell_file_path)} specified, ignoring template_command action.") + log.debug(f"Invalid file {env_shell_file_path} specified, ignoring template_command action.") return env_vars def handle_command(self, tool_dependency, cmd, return_output=False, job_name=""): @@ -124,14 +124,14 @@ class InstallEnvironment: stderr = output.stderr if len(stdout) > DATABASE_MAX_STRING_SIZE: log.warning( - f"Length of stdout > {str(DATABASE_MAX_STRING_SIZE_PRETTY)}, so only a portion will be saved in the database." + f"Length of stdout > {DATABASE_MAX_STRING_SIZE_PRETTY}, so only a portion will be saved in the database." ) stdout = shrink_string_by_size( stdout, DATABASE_MAX_STRING_SIZE, join_by="\n..\n", left_larger=True, beginning_on_size_error=True ) if len(stderr) > DATABASE_MAX_STRING_SIZE: log.warning( - f"Length of stderr > {str(DATABASE_MAX_STRING_SIZE_PRETTY)}, so only a portion will be saved in the database." + f"Length of stderr > {DATABASE_MAX_STRING_SIZE_PRETTY}, so only a portion will be saved in the database." ) stderr = shrink_string_by_size( stderr, DATABASE_MAX_STRING_SIZE, join_by="\n..\n", left_larger=True, beginning_on_size_error=True @@ -144,9 +144,8 @@ class InstallEnvironment: error_message = unicodify(stdout) else: # We have a problem if there was no stdout and no stderr. - error_message = "Unknown error occurred executing shell command %s, return_code: %s" % ( - str(cmd), - str(output.return_code), + error_message = ( + f"Unknown error occurred executing shell command {cmd}, return_code: {output.return_code}" ) set_tool_dependency_attributes( self.app, tool_dependency=tool_dependency, status=status, error_message=error_message @@ -171,7 +170,7 @@ class InstallEnvironment: llog_name += f":{job_name}" llog = logging.getLogger(llog_name) # Print the command we're about to execute, ``set -x`` style. - llog.debug(f"+ {str(command)}") + llog.debug(f"+ {command}") # Launch the command as subprocess. A bufsize of 1 means line buffered. process_handle = subprocess.Popen( str(command), diff --git a/lib/galaxy/tool_shed/galaxy_install/tool_dependencies/recipe/step_handler.py b/lib/galaxy/tool_shed/galaxy_install/tool_dependencies/recipe/step_handler.py index 51f5eeebee9..73c4f04dc3d 100644 --- a/lib/galaxy/tool_shed/galaxy_install/tool_dependencies/recipe/step_handler.py +++ b/lib/galaxy/tool_shed/galaxy_install/tool_dependencies/recipe/step_handler.py @@ -54,7 +54,7 @@ class Download: try: download_to_file(download_url, file_path, chunk_size=basic_util.CHUNK_SIZE) except Exception as e: - err_msg = f"Error downloading from URL {str(download_url)} : {str(e)}" + err_msg = f"Error downloading from URL {download_url} : {e}" raise Exception(err_msg) if "sha256sum" in checksums or "#sha256#" in download_url: @@ -174,7 +174,7 @@ class AssertDirectoryExecutable(RecipeStep): full_path = os.path.join(current_dir, action_dict["full_path"]) if not self.assert_directory_executable(full_path=full_path): status = self.app.install_model.ToolDependency.installation_status.ERROR - error_message = f"The path {str(full_path)} is not a directory or is not executable by the owner." + error_message = f"The path {full_path} is not a directory or is not executable by the owner." tool_dependency = tool_dependency_util.set_tool_dependency_attributes( self.app, tool_dependency, status=status, error_message=error_message ) @@ -229,7 +229,7 @@ class AssertDirectoryExists(RecipeStep): full_path = os.path.join(current_dir, action_dict["full_path"]) if not self.assert_directory_exists(full_path=full_path): status = self.app.install_model.ToolDependency.installation_status.ERROR - error_message = f"The path {str(full_path)} is not a directory or does not exist." + error_message = f"The path {full_path} is not a directory or does not exist." tool_dependency = tool_dependency_util.set_tool_dependency_attributes( self.app, tool_dependency, status=status, error_message=error_message ) @@ -287,7 +287,7 @@ class AssertFileExecutable(RecipeStep): full_path = os.path.join(current_dir, action_dict["full_path"]) if not self.assert_file_executable(full_path=full_path): status = self.app.install_model.ToolDependency.installation_status.ERROR - error_message = f"The path {str(full_path)} is not a file or is not executable by the owner." + error_message = f"The path {full_path} is not a file or is not executable by the owner." tool_dependency = tool_dependency_util.set_tool_dependency_attributes( self.app, tool_dependency, status=status, error_message=error_message ) @@ -342,7 +342,7 @@ class AssertFileExists(RecipeStep): full_path = os.path.join(current_dir, action_dict["full_path"]) if not self.assert_file_exists(full_path=full_path): status = self.app.install_model.ToolDependency.installation_status.ERROR - error_message = f"The path {str(full_path)} is not a file or does not exist." + error_message = f"The path {full_path} is not a file or does not exist." tool_dependency = tool_dependency_util.set_tool_dependency_attributes( self.app, tool_dependency, status=status, error_message=error_message ) @@ -1570,36 +1570,28 @@ class SetupRubyEnvironment(Download, RecipeStep): gem_parameters = "" if os.path.isfile(gem): # we assume a local shipped gem file - cmd = """PATH=$PATH:$RUBY_HOME/bin; export PATH; GEM_HOME=$INSTALL_DIR; export GEM_HOME; - gem install --local {} {}""".format( - gem, gem_parameters - ) + cmd = f"""PATH=$PATH:$RUBY_HOME/bin; export PATH; GEM_HOME=$INSTALL_DIR; export GEM_HOME; + gem install --local {gem} {gem_parameters}""" elif gem.find("://") != -1: # We assume a URL to a gem file. url = gem gem_name = url.split("/")[-1] checksums = ruby_package_tup_dict.get("checksums", {}) self.url_download(work_dir, gem_name, url, extract=False, checksums=checksums) - cmd = """PATH=$PATH:$RUBY_HOME/bin; export PATH; GEM_HOME=$INSTALL_DIR; export GEM_HOME; - gem install --local {} {}""".format( - gem_name, gem_parameters - ) + cmd = f"""PATH=$PATH:$RUBY_HOME/bin; export PATH; GEM_HOME=$INSTALL_DIR; export GEM_HOME; + gem install --local {gem_name} {gem_parameters}""" else: # gem file from rubygems.org with or without version number if gem_version: # Specific ruby gem version was requested. # Use raw strings so that python won't automatically unescape the quotes before passing the command # to subprocess.Popen. - cmd = r"""PATH=$PATH:$RUBY_HOME/bin; export PATH; GEM_HOME=$INSTALL_DIR; export GEM_HOME; - gem install {} --version "={}" {}""".format( - gem, gem_version, gem_parameters - ) + cmd = rf"""PATH=$PATH:$RUBY_HOME/bin; export PATH; GEM_HOME=$INSTALL_DIR; export GEM_HOME; + gem install {gem} --version "={gem_version}" {gem_parameters}""" else: # no version number given - cmd = """PATH=$PATH:$RUBY_HOME/bin; export PATH; GEM_HOME=$INSTALL_DIR; export GEM_HOME; - gem install {} {}""".format( - gem, gem_parameters - ) + cmd = f"""PATH=$PATH:$RUBY_HOME/bin; export PATH; GEM_HOME=$INSTALL_DIR; export GEM_HOME; + gem install {gem} {gem_parameters}""" cmd = install_environment.build_command(basic_util.evaluate_template(cmd, install_environment)) return_code = install_environment.handle_command( tool_dependency=tool_dependency, cmd=cmd, return_output=False, job_name=package_name @@ -1869,7 +1861,7 @@ class SetupVirtualEnv(Download, RecipeStep): # and well defined behavior in bash/zsh. activate_command = f"POSIXLY_CORRECT=1; . {os.path.join(venv_directory, 'bin', 'activate')}" if action_dict["use_requirements_file"]: - install_command = "python '%s' install -r '%s' --log '%s'" % ( + install_command = "python '{}' install -r '{}' --log '{}'".format( os.path.join(venv_directory, "bin", "pip"), requirements_path, os.path.join(install_environment.install_dir, "pip_install.log"), @@ -1883,7 +1875,7 @@ class SetupVirtualEnv(Download, RecipeStep): break line = line.strip() if line: - line_install_command = "python '%s' install %s --log '%s'" % ( + line_install_command = "python '{}' install {} --log '{}'".format( os.path.join(venv_directory, "bin", "pip"), line, os.path.join(install_environment.install_dir, "pip_install_%s.log" % (line)), @@ -2087,6 +2079,6 @@ class TemplateCommand(RecipeStep): action_dict["language"] = language action_dict["command"] = action_elem_text else: - log.debug(f"Unsupported template language '{str(language)}'. Not proceeding.") - raise Exception(f"Unsupported template language '{str(language)}' in tool dependency definition.") + log.debug(f"Unsupported template language '{language}'. Not proceeding.") + raise Exception(f"Unsupported template language '{language}' in tool dependency definition.") return action_dict diff --git a/lib/galaxy/tool_shed/galaxy_install/tool_dependencies/recipe/tag_handler.py b/lib/galaxy/tool_shed/galaxy_install/tool_dependencies/recipe/tag_handler.py index 1a4be0d81e9..fb29205880c 100644 --- a/lib/galaxy/tool_shed/galaxy_install/tool_dependencies/recipe/tag_handler.py +++ b/lib/galaxy/tool_shed/galaxy_install/tool_dependencies/recipe/tag_handler.py @@ -210,7 +210,9 @@ class Install(RecipeTag, SyncDatabase): ) if not actions_elem_tuples: proceed_with_install = False - error_message = f"Version {str(package_version)} of the {str(package_name)} package cannot be installed because " + error_message = ( + f"Version {package_version} of the {package_name} package cannot be installed because " + ) error_message += "the recipe for installing the package is missing either an <actions> tag set or an <actions_group> " error_message += "tag set." # Since there was an installation error, update the tool dependency status to Error. @@ -321,7 +323,7 @@ class Repository(RecipeTag, SyncDatabase): return tmp_filename else: message = "Unable to retrieve required tool_dependencies.xml file from the Tool Shed for revision " - message += f"{str(changeset_revision)} of installed repository {str(name)} owned by {str(owner)}." + message += f"{changeset_revision} of installed repository {name} owned by {owner}." raise Exception(message) def create_tool_dependency_with_initialized_env_sh_file( @@ -390,9 +392,8 @@ class Repository(RecipeTag, SyncDatabase): env_file_builder.append_line(action="source", value=required_tool_dependency_env_file_path) return_code = env_file_builder.return_code if return_code: - error_message = "Error defining env.sh file for package %s, return_code: %s" % ( - str(package_name), - str(return_code), + error_message = ( + f"Error defining env.sh file for package {package_name}, return_code: {return_code}" ) tool_dependency = tool_dependency_util.set_tool_dependency_attributes( self.app, @@ -512,7 +513,7 @@ class Repository(RecipeTag, SyncDatabase): if required_repository.is_deactivated_or_installed: if not os.path.exists(required_repository_package_install_dir): log.error( - f"Missing required tool dependency directory {str(required_repository_package_install_dir)}" + f"Missing required tool dependency directory {required_repository_package_install_dir}" ) repo_files_dir = required_repository.repo_files_directory(self.app) if not repo_files_dir: @@ -572,11 +573,7 @@ class Repository(RecipeTag, SyncDatabase): ) self.remove_file(tmp_filename) else: - message = "Unable to locate required tool shed repository named %s owned by %s with revision %s." % ( - str(required_repository_name), - str(required_repository_owner), - str(default_required_repository_changeset_revision), - ) + message = f"Unable to locate required tool shed repository named {required_repository_name} owned by {required_repository_owner} with revision {default_required_repository_changeset_revision}." raise Exception(message) return handled_tool_dependencies @@ -599,7 +596,7 @@ class Repository(RecipeTag, SyncDatabase): if rd_tool_dependency.status == self.app.install_model.ToolDependency.installation_status.ERROR: # We'll log the error here, but continue installing packages since some may not require this dependency. log.error( - f"Error installing tool dependency for required repository: {str(rd_tool_dependency.error_message)}" + f"Error installing tool dependency for required repository: {rd_tool_dependency.error_message}" ) return tool_dependency, proceed_with_install, action_elem_tuples @@ -644,7 +641,7 @@ class SetEnvironment(RecipeTag): try: self.set_environment(package_elem, tool_shed_repository, attr_tups_of_dependencies_for_install) except Exception as e: - error_message = f"Error setting environment for tool dependency: {str(e)}" + error_message = f"Error setting environment for tool dependency: {e}" log.debug(error_message) return tool_dependency, proceed_with_install, action_elem_tuples @@ -731,10 +728,7 @@ class SetEnvironment(RecipeTag): env_file_builder = EnvFileBuilder(install_dir) return_code = env_file_builder.append_line(make_executable=True, **env_var_dict) if return_code: - error_message = "Error creating env.sh file for tool dependency %s, return_code: %s" % ( - str(tool_dependency.name), - str(return_code), - ) + error_message = f"Error creating env.sh file for tool dependency {tool_dependency.name}, return_code: {return_code}" log.debug(error_message) status = self.app.install_model.ToolDependency.installation_status.ERROR tool_dependency = tool_dependency_util.set_tool_dependency_attributes( diff --git a/lib/galaxy/tool_shed/tools/data_table_manager.py b/lib/galaxy/tool_shed/tools/data_table_manager.py index cdf89ece2be..9f181281650 100644 --- a/lib/galaxy/tool_shed/tools/data_table_manager.py +++ b/lib/galaxy/tool_shed/tools/data_table_manager.py @@ -185,3 +185,9 @@ class ShedToolDataTableManager: # For backwards compatibility with exisiting data managers ToolDataTableManager = ShedToolDataTableManager + + +__all__ = ( + "ToolDataTableManager", + "ShedToolDataTableManager", +) diff --git a/lib/galaxy/tool_shed/tools/tool_validator.py b/lib/galaxy/tool_shed/tools/tool_validator.py index 4e4c2e71947..1220acacc21 100644 --- a/lib/galaxy/tool_shed/tools/tool_validator.py +++ b/lib/galaxy/tool_shed/tools/tool_validator.py @@ -6,9 +6,9 @@ from galaxy.tool_shed.util import ( hg_util, ) from galaxy.tool_util.fetcher import ToolLocationFetcher +from galaxy.tool_util.parser import get_tool_source from galaxy.tools import ( create_tool_from_source, - get_tool_source, parameters, ) from galaxy.tools.parameters import dynamic_options diff --git a/lib/galaxy/tool_shed/util/basic_util.py b/lib/galaxy/tool_shed/util/basic_util.py index 3a81420cf70..bdc371346e2 100644 --- a/lib/galaxy/tool_shed/util/basic_util.py +++ b/lib/galaxy/tool_shed/util/basic_util.py @@ -152,3 +152,23 @@ def to_html_string(text): text = text.replace(" ", "    ") text = text.replace(" ", " ") return text + + +__all__ = ( + "CHUNK_SIZE", + "DOCKER_IMAGE_TEMPLATE", + "evaluate_template", + "get_env_var_values", + "get_file_type_str", + "INSTALLATION_LOG", + "MAX_DISPLAY_SIZE", + "MAXDIFFSIZE", + "NO_OUTPUT_TIMEOUT", + "move_file", + "remove_dir", + "SELECTED_REPOSITORIES_TEMPLATE", + "size_string", + "stringify", + "strip_path", + "to_html_string", +) diff --git a/lib/galaxy/tool_shed/util/metadata_util.py b/lib/galaxy/tool_shed/util/metadata_util.py index 36387a28fba..4eee9dbfb05 100644 --- a/lib/galaxy/tool_shed/util/metadata_util.py +++ b/lib/galaxy/tool_shed/util/metadata_util.py @@ -10,12 +10,7 @@ def get_updated_changeset_revisions_from_tool_shed(app, tool_shed_url, name, own tool_shed_url = get_tool_shed_url_from_tool_shed_registry(app, tool_shed_url) if tool_shed_url is None or name is None or owner is None or changeset_revision is None: message = "Unable to get updated changeset revisions from the Tool Shed because one or more of the following " - message += "required parameters is None: tool_shed_url: %s, name: %s, owner: %s, changeset_revision: %s " % ( - str(tool_shed_url), - str(name), - str(owner), - str(changeset_revision), - ) + message += f"required parameters is None: tool_shed_url: {tool_shed_url}, name: {name}, owner: {owner}, changeset_revision: {changeset_revision}" raise Exception(message) params = dict(name=name, owner=owner, changeset_revision=changeset_revision) pathspec = ["repository", "updated_changeset_revisions"] diff --git a/lib/galaxy/tool_shed/util/repository_util.py b/lib/galaxy/tool_shed/util/repository_util.py index 39d599c5e62..ac2d108c73a 100644 --- a/lib/galaxy/tool_shed/util/repository_util.py +++ b/lib/galaxy/tool_shed/util/repository_util.py @@ -369,7 +369,7 @@ def get_repo_info_tuple_contents(repo_info_tuple): def get_repository_admin_role_name(repository_name, repository_owner): - return f"{str(repository_name)}_{str(repository_owner)}_admin" + return f"{repository_name}_{repository_owner}_admin" def get_repository_and_repository_dependencies_from_repo_info_dict(app, repo_info_dict): @@ -482,12 +482,7 @@ def get_repository_for_dependency_relationship(app, tool_shed, name, owner, chan tool_shed = common_util.remove_protocol_from_tool_shed_url(tool_shed) if tool_shed is None or name is None or owner is None or changeset_revision is None: message = "Unable to retrieve the repository record from the database because one or more of the following " - message += "required parameters is None: tool_shed: %s, name: %s, owner: %s, changeset_revision: %s " % ( - str(tool_shed), - str(name), - str(owner), - str(changeset_revision), - ) + message += f"required parameters is None: tool_shed: {tool_shed}, name: {name}, owner: {owner}, changeset_revision: {changeset_revision}" raise Exception(message) repository = get_installed_repository( app=app, tool_shed=tool_shed, name=name, owner=owner, installed_changeset_revision=changeset_revision diff --git a/lib/galaxy/tool_shed/util/tool_dependency_util.py b/lib/galaxy/tool_shed/util/tool_dependency_util.py index 0ee1faefca7..a4c8b64d9b0 100644 --- a/lib/galaxy/tool_shed/util/tool_dependency_util.py +++ b/lib/galaxy/tool_shed/util/tool_dependency_util.py @@ -33,7 +33,7 @@ def build_tool_dependencies_select_field( app.install_model.ToolDependency.installation_status.UNINSTALLED, ]: continue - option_label = f"{str(tool_dependency.name)} version {str(tool_dependency.version)}" + option_label = f"{tool_dependency.name} version {tool_dependency.version}" option_value = app.security.encode_id(tool_dependency.id) tool_dependencies_select_field.add_option(option_label, option_value) return tool_dependencies_select_field @@ -58,13 +58,8 @@ def create_or_update_tool_dependency(app, tool_shed_repository, name, version, t set_tool_dependency_attributes(app, tool_dependency=tool_dependency, status=status) else: # Create a new tool_dependency record for the tool_shed_repository. - debug_msg = "Creating a new record for version %s of tool dependency %s for revision %s of repository %s. " % ( - str(version), - str(name), - str(tool_shed_repository.changeset_revision), - str(tool_shed_repository.name), - ) - debug_msg += f"The status is being set to {str(status)}." + debug_msg = f"Creating a new record for version {version} of tool dependency {name} for revision {tool_shed_repository.changeset_revision} of repository {tool_shed_repository.name}. " + debug_msg += f"The status is being set to {status}." log.debug(debug_msg) tool_dependency = app.install_model.ToolDependency(tool_shed_repository.id, name, version, type, status) context.add(tool_dependency) @@ -371,10 +366,7 @@ def parse_package_elem(package_elem, platform_info_dict=None, include_after_inst debug_msg = ( "Error: tags are only allowed at the end of an tag set after " ) - debug_msg += "all tags. Skipping <%s> element with type %s." % ( - child_element.tag, - child_element.get("type", "unknown"), - ) + debug_msg += f"all tags. Skipping <{child_element.tag}> element with type {child_element.get('type', 'unknown')}." log.debug(debug_msg) continue if platform_info_dict is None and not include_after_install_actions: @@ -420,12 +412,10 @@ def remove_tool_dependency_installation_directory(dependency_install_dir): shutil.rmtree(dependency_install_dir) removed = True error_message = "" - log.debug(f"Removed tool dependency installation directory: {str(dependency_install_dir)}") + log.debug(f"Removed tool dependency installation directory: {dependency_install_dir}") except Exception as e: removed = False - error_message = ( - f"Error removing tool dependency installation directory {str(dependency_install_dir)}: {str(e)}" - ) + error_message = f"Error removing tool dependency installation directory {dependency_install_dir}: {e}" log.warning(error_message) else: removed = True @@ -450,7 +440,7 @@ def set_tool_dependency_attributes(app, tool_dependency, status, error_message=N str(tool_shed_repository.name), ) ) - debug_msg += f"by updating the status from {str(tool_dependency.status)} to {str(status)}." + debug_msg += f"by updating the status from {tool_dependency.status} to {status}." log.debug(debug_msg) tool_dependency.status = status sa_session.add(tool_dependency) diff --git a/lib/galaxy/tool_shed/util/tool_util.py b/lib/galaxy/tool_shed/util/tool_util.py index fe7ec9489a2..f93370d4860 100644 --- a/lib/galaxy/tool_shed/util/tool_util.py +++ b/lib/galaxy/tool_shed/util/tool_util.py @@ -118,11 +118,9 @@ def generate_message_for_invalid_tools( sample_ext = f"{missing_file}.sample" else: sample_ext = missing_file - correction_msg = "This file refers to a missing file %s%s%s. " % (bold_start, str(missing_file), bold_end) - correction_msg += "Upload a file named %s%s%s to the repository to correct this error." % ( - bold_start, - sample_ext, - bold_end, + correction_msg = f"This file refers to a missing file {bold_start}{missing_file}{bold_end}. " + correction_msg += ( + f"Upload a file named {bold_start}{sample_ext}{bold_end} to the repository to correct this error." ) else: if as_html: @@ -248,3 +246,17 @@ def panel_entry_per_tool(tool_section_dict): if k not in ["id", "version", "name"]: return True return False + + +__all__ = ( + "build_shed_tool_conf_select_field", + "build_tool_panel_section_select_field", + "copy_sample_file", + "copy_sample_files", + "generate_message_for_invalid_tools", + "get_tool_path_install_dir", + "handle_missing_index_file", + "is_data_index_sample_file", + "new_state", + "panel_entry_per_tool", +) diff --git a/lib/galaxy/tool_util/cwl/cwltool_deps.py b/lib/galaxy/tool_util/cwl/cwltool_deps.py index f707477c13e..de13a7aa2a0 100644 --- a/lib/galaxy/tool_util/cwl/cwltool_deps.py +++ b/lib/galaxy/tool_util/cwl/cwltool_deps.py @@ -43,14 +43,8 @@ except ImportError: try: from cwltool import load_tool - from cwltool.load_tool import ( - default_loader, - resolve_and_validate_document, - ) except ImportError: - default_loader = None # type: ignore[assignment] load_tool = None # type: ignore[assignment] - resolve_and_validate_document = None # type: ignore[assignment] try: @@ -59,10 +53,23 @@ except ImportError: command_line_tool = None # type: ignore[assignment] try: - from cwltool.load_tool import resolve_and_validate_document + from cwltool.load_tool import ( + default_loader, + resolve_and_validate_document, + ) except ImportError: + default_loader = None # type: ignore[assignment] resolve_and_validate_document = None # type: ignore[assignment] +try: + from cwltool.utils import ( + normalizeFilesDirs, + visit_class, + ) +except ImportError: + visit_class = None # type: ignore[assignment] + normalizeFilesDirs = None # type: ignore[assignment] + try: import shellescape except ImportError: @@ -74,10 +81,12 @@ try: ref_resolver, sourceline, ) + from schema_salad.utils import yaml_no_ts except ImportError: schema_salad = None # type: ignore[assignment] ref_resolver = None # type: ignore[assignment] sourceline = None # type: ignore[assignment] + yaml_no_ts = None # type: ignore[assignment] needs_shell_quoting = re.compile(r"""(^$|[\s|&;()<>\'"$@])""").search @@ -113,6 +122,7 @@ __all__ = ( "LoadingContext", "main", "needs_shell_quoting", + "normalizeFilesDirs", "pathmapper", "process", "ref_resolver", @@ -123,5 +133,7 @@ __all__ = ( "shellescape", "sourceline", "StdFsAccess", + "visit_class", "workflow", + "yaml_no_ts", ) diff --git a/lib/galaxy/tool_util/cwl/parser.py b/lib/galaxy/tool_util/cwl/parser.py index 47d43f39487..737484f94ae 100644 --- a/lib/galaxy/tool_util/cwl/parser.py +++ b/lib/galaxy/tool_util/cwl/parser.py @@ -38,6 +38,7 @@ from .cwltool_deps import ( beta_relaxed_fmt_check, ensure_cwltool_available, getdefault, + normalizeFilesDirs, pathmapper, process, ref_resolver, @@ -45,6 +46,8 @@ from .cwltool_deps import ( RuntimeContext, sourceline, StdFsAccess, + visit_class, + yaml_no_ts, ) from .representation import ( field_to_field_type, @@ -86,25 +89,25 @@ SUPPORTED_WORKFLOW_REQUIREMENTS = SUPPORTED_TOOL_REQUIREMENTS + [] PERSISTED_REPRESENTATION = "cwl_tool_object" ToolStateType = Dict[str, Union[None, str, bool, Dict[str, str]]] -InputInstanceDict = TypedDict( - "InputInstanceDict", - { - "type": str, - "name": str, - "title": str, - "label": str, - "help": str, - "optional": bool, - "area": bool, - "value": str, - "collection_type": str, - }, - total=False, -) -InputInstanceArrayDict = TypedDict( - "InputInstanceArrayDict", - {"type": str, "name": str, "title": str, "blocks": List[InputInstanceDict]}, -) + + +class InputInstanceDict(TypedDict, total=False): + type: str + name: str + title: str + label: str + help: str + optional: bool + area: bool + value: str + collection_type: str + + +class InputInstanceArrayDict(TypedDict): + type: str + name: str + title: str + blocks: List[InputInstanceDict] def tool_proxy(tool_path=None, tool_object=None, strict_cwl_validation=True, tool_directory=None, uuid=None): @@ -187,10 +190,8 @@ def _to_cwl_tool_object( assert cwl_tool_object is None # Allow loading tools from YAML... - from ruamel import yaml as ryaml - as_str = json.dumps(tool_object) - tool_object = ryaml.round_trip_load(as_str) + tool_object = yaml_no_ts().load(as_str) path = tool_directory if path is None: path = os.getcwd() @@ -523,9 +524,9 @@ class JobProxy: make_fs_access = getdefault(runtime_context.make_fs_access, StdFsAccess) fs_access = make_fs_access(runtime_context.basedir) process.fill_in_defaults(self._tool_proxy._tool.tool["inputs"], self._input_dict, fs_access) - process.visit_class(self._input_dict, ("File", "Directory"), pathToLoc) + visit_class(self._input_dict, ("File", "Directory"), pathToLoc) # TODO: Why doesn't fillInDefault fill in locations instead of paths? - process.normalizeFilesDirs(self._input_dict) + normalizeFilesDirs(self._input_dict) # TODO: validate like cwltool process _init_job. # validate.validate_ex(self.names.get_name("input_record_schema", ""), builder.job, # strict=False, logger=_logger_validation_warnings) diff --git a/lib/galaxy/tool_util/deps/conda_util.py b/lib/galaxy/tool_util/deps/conda_util.py index feb74a45fe6..94da3a13d34 100644 --- a/lib/galaxy/tool_util/deps/conda_util.py +++ b/lib/galaxy/tool_util/deps/conda_util.py @@ -8,13 +8,18 @@ import shlex import shutil import sys import tempfile +from typing import ( + List, + Optional, +) import packaging.version from galaxy.util import ( commands, + listify, smart_str, - unicodify, + which, ) from . import installable @@ -88,8 +93,8 @@ class CondaContext(installable.InstallableContext): ): self.condarc_override = condarc_override if not conda_exec and use_path_exec: - conda_exec = commands.which("conda") - if conda_exec: + conda_exec = which("conda") + if conda_exec and isinstance(conda_exec, str): conda_exec = os.path.normpath(conda_exec) self.conda_exec = conda_exec self.debug = debug @@ -179,8 +184,8 @@ class CondaContext(installable.InstallableContext): def conda_info(self): if self.conda_exec is not None: - info_out = commands.execute([self.conda_exec, "info", "--json"]) - info_out = unicodify(info_out) + cmd = listify(self.conda_exec) + ["info", "--json"] + info_out = commands.execute(cmd) info = json.loads(info_out) return info else: @@ -230,8 +235,7 @@ class CondaContext(installable.InstallableContext): Return the process exit code (i.e. 0 in case of success). """ - cmd = [self.conda_exec] - cmd.extend(operation.split()) + cmd = listify(self.conda_exec) + operation.split() if self.debug: cmd.append("--debug") cmd.extend(args) @@ -304,6 +308,24 @@ class CondaContext(installable.InstallableContext): stdout_path = "/dev/null" return self.exec_command("clean", clean_args, stdout_path=stdout_path) + def exec_search(self, args: List[str], json: bool = False, offline: bool = False, platform: Optional[str] = None): + """ + Search conda channels for a package + + Return the standard output of the conda process. + """ + cmd = listify(self.conda_exec)[:] + cmd.append("search") + cmd.extend(self._override_channels_args) + if json: + cmd.append("--json") + if offline: + cmd.append("--offline") + if platform: + cmd.extend(["--platform", platform]) + cmd.extend(args) + return commands.execute(cmd) + def export_list(self, name, path): """ Return the process exit code (i.e. 0 in case of success). @@ -494,33 +516,16 @@ def cleanup_failed_install(conda_target, conda_context=None): cleanup_failed_install_of_environment(conda_target.install_environment, conda_context=conda_context) -def best_search_result(conda_target, conda_context, channels_override=None, offline=False, platform=None): +def best_search_result( + conda_target, conda_context: CondaContext, offline: bool = False, platform: Optional[str] = None +): """Find best "conda search" result for specified target. Return ``None`` if no results match. """ - search_cmd = [] - conda_exec = conda_context.conda_exec - if isinstance(conda_exec, list): - # for CondaInDockerContext - search_cmd.extend(conda_exec) - else: - search_cmd.append(conda_exec) - search_cmd.extend(["search", "--full-name", "--json"]) - if offline: - search_cmd.append("--offline") - if platform: - search_cmd.extend(["--platform", platform]) - if channels_override: - search_cmd.append("--override-channels") - for channel in channels_override: - search_cmd.extend(["--channel", channel]) - else: - search_cmd.extend(conda_context._override_channels_args) - search_cmd.append(conda_target.package) + search_args = [conda_target.package] try: - res = commands.execute(search_cmd) - res = unicodify(res) + res = conda_context.exec_search(search_args, json=True, offline=offline, platform=platform) # Use python's stable list sorting to sort by date, # then build_number, then version. The top of the list # then is the newest version with the newest build and @@ -528,8 +533,8 @@ def best_search_result(conda_target, conda_context, channels_override=None, offl hits = json.loads(res).get(conda_target.package, [])[::-1] hits = sorted(hits, key=lambda hit: hit["build_number"], reverse=True) hits = sorted(hits, key=lambda hit: packaging.version.parse(hit["version"]), reverse=True) - except commands.CommandLineException: - log.error("Could not execute: '%s'", search_cmd) + except commands.CommandLineException as e: + log.error("Could not execute: '%s'\n%s", e.command, e) hits = [] if len(hits) == 0: diff --git a/lib/galaxy/tool_util/deps/mulled/mulled_build.py b/lib/galaxy/tool_util/deps/mulled/mulled_build.py index 054133fffe4..4625175cb8d 100644 --- a/lib/galaxy/tool_util/deps/mulled/mulled_build.py +++ b/lib/galaxy/tool_util/deps/mulled/mulled_build.py @@ -23,7 +23,10 @@ from sys import platform as _platform import yaml from galaxy.tool_util.deps import installable -from galaxy.tool_util.deps.conda_util import best_search_result +from galaxy.tool_util.deps.conda_util import ( + best_search_result, + CondaContext, +) from galaxy.tool_util.deps.docker_util import command_list as docker_command_list from galaxy.util import ( commands, @@ -326,18 +329,27 @@ def context_from_args(args): return InvolucroContext(involucro_bin=args.involucro_path, verbose=verbose) -class CondaInDockerContext: - @property - def conda_exec(self): - conda_image = CONDA_IMAGE or "continuumio/miniconda3:latest" - return docker_command_list("run", [conda_image, "conda"]) - - @property - def _override_channels_args(self): - override_channels_args = ["--override-channels"] - for channel in DEFAULT_CHANNELS: - override_channels_args.extend(["--channel", channel]) - return override_channels_args +class CondaInDockerContext(CondaContext): + def __init__( + self, + conda_prefix=None, + conda_exec=None, + shell_exec=None, + debug=False, + ensure_channels=DEFAULT_CHANNELS, + condarc_override=None, + ): + if not conda_exec: + conda_image = CONDA_IMAGE or "continuumio/miniconda3:latest" + conda_exec = docker_command_list("run", [conda_image, "conda"]) + super().__init__( + conda_prefix=conda_prefix, + conda_exec=conda_exec, + shell_exec=shell_exec, + debug=debug, + ensure_channels=ensure_channels, + condarc_override=condarc_override, + ) class InvolucroContext(installable.InstallableContext): @@ -545,7 +557,10 @@ def main(argv=None): sys.exit(mull_targets(targets, **args_to_mull_targets_kwds(args))) -__all__ = ("main",) +__all__ = ( + "main", + "build_target", +) if __name__ == "__main__": diff --git a/lib/galaxy/tool_util/deps/mulled/mulled_search.py b/lib/galaxy/tool_util/deps/mulled/mulled_search.py index e2c1ccc6880..20700fbf29e 100755 --- a/lib/galaxy/tool_util/deps/mulled/mulled_search.py +++ b/lib/galaxy/tool_util/deps/mulled/mulled_search.py @@ -5,9 +5,15 @@ import json import logging import sys import tempfile +from datetime import ( + datetime, + timezone, +) import requests +from galaxy.tool_util.deps.conda_util import CondaContext +from galaxy.util import which from .mulled_list import get_singularity_containers from .util import ( build_target, @@ -15,11 +21,6 @@ from .util import ( v2_image_name, ) -try: - from conda.cli.python_api import run_command -except ImportError: - run_command = None - try: from whoosh.fields import ( Schema, @@ -32,6 +33,7 @@ except ImportError: Schema = TEXT = STORED = create_in = QueryParser = None QUAY_API_URL = "https://quay.io/api/v1/repository" +conda_path = which("conda") class QuaySearch: @@ -127,11 +129,13 @@ class CondaSearch: Function takes search_string variable and returns results from the bioconda channel in JSON format """ - if run_command is None: - raise Exception(f"Invalid search destination. {deps_error_message('conda')}") - raw_out, err, exit_code = run_command("search", "-c", self.channel, search_string, use_exception_handler=True) - if exit_code != 0: - logging.info(f"Search failed with: {err}") + if not conda_path: + raise Exception("Invalid search destination. Required dependency [conda] is not in your PATH.") + try: + conda_context = CondaContext(conda_exec=conda_path, ensure_channels=self.channel) + raw_out = conda_context.exec_search([search_string]) + except Exception as e: + logging.info(f"Search failed with: {e}") return [] return [ {"package": n.split()[0], "version": n.split()[1], "build": n.split()[2]} for n in raw_out.split("\n")[2:-1] @@ -143,14 +147,29 @@ class GitHubSearch: Tool to search the GitHub bioconda-recipes repo """ + @staticmethod + def _check_response_rate_limit(response): + if response.status_code == 403 and "API rate limit exceeded" in response.json()["message"]: + # It can take tens of minutes before the rate limit window resets + message = "GitHub API rate limit exceeded." + rate_limit_reset_UTC_timestamp = response.headers.get("X-RateLimit-Reset") + if rate_limit_reset_UTC_timestamp: + rate_limit_reset_datetime = datetime.fromtimestamp(int(rate_limit_reset_UTC_timestamp), tz=timezone.utc) + message += f" The rate limit window will reset at {rate_limit_reset_datetime.isoformat()}." + raise Exception(message) + def get_json(self, search_string): """ Takes search_string variable and return results from the bioconda-recipes github repository in JSON format + + DEPRECATED: this method is currently unreliable because the API query + sometimes succeeds but returns no items. """ response = requests.get( f"https://api.github.com/search/code?q={search_string}+in:path+repo:bioconda/bioconda-recipes+path:recipes", timeout=MULLED_SOCKET_TIMEOUT, ) + self._check_response_rate_limit(response) response.raise_for_status() return response.json() @@ -169,6 +188,7 @@ class GitHubSearch: f"https://api.github.com/repos/bioconda/bioconda-recipes/contents/recipes/{search_string}", timeout=MULLED_SOCKET_TIMEOUT, ) + self._check_response_rate_limit(response) return response.status_code == 200 @@ -254,7 +274,7 @@ def readable_output(json, organization="biocontainers", channel="bioconda"): "quay", result["package"], result["version"], - "docker pull quay.io/%s/%s:%s\n" % (organization, result["package"], result["version"]), + f"docker pull quay.io/{organization}/{result['package']}:{result['version']}\n", ] ) # NOT a real solution for results in json.get("conda", {}).values(): @@ -335,7 +355,7 @@ def main(argv=None): return destination_defaults = ["quay", "singularity", "github"] - if run_command is not None: + if conda_path: destination_defaults.append("conda") parser = argparse.ArgumentParser(description="Searches in a given quay organization for a repository") @@ -359,7 +379,7 @@ def main(argv=None): "--channel", dest="channel_string", default="bioconda", - help="Change conda channel to search; default is bioconda.", + help="Change conda channels to search; default is bioconda.", ) parser.add_argument( "--non-strict", @@ -392,10 +412,11 @@ def main(argv=None): github = GitHubSearch() for item in args.search: - github_json = github.get_json(item) - github_results[item] = github.process_json(github_json, item) if github.recipe_present(item): github_recipe_present.append(item) + else: + github_json = github.get_json(item) + github_results[item] = github.process_json(github_json, item) json_results["github"] = github_results json_results["github_recipe_present"] = {"recipes": github_recipe_present} @@ -422,5 +443,8 @@ def main(argv=None): readable_output(json_results, args.organization_string, args.channel_string) +__all__ = ("main",) + + if __name__ == "__main__": main() diff --git a/lib/galaxy/tool_util/deps/mulled/util.py b/lib/galaxy/tool_util/deps/mulled/util.py index be3edad1923..cb832455cc0 100644 --- a/lib/galaxy/tool_util/deps/mulled/util.py +++ b/lib/galaxy/tool_util/deps/mulled/util.py @@ -64,14 +64,23 @@ def _namespace_has_repo_name(namespace, repo_name, resolution_cache): if resolution_cache is not None and cache_key in resolution_cache: repo_names = resolution_cache.get(cache_key) else: - repos_parameters = {"public": "true", "namespace": namespace} + next_page = None + repo_names = [] repos_headers = {"Accept-encoding": "gzip", "Accept": "application/json"} - repos_response = requests.get( - QUAY_REPOSITORY_API_ENDPOINT, headers=repos_headers, params=repos_parameters, timeout=MULLED_SOCKET_TIMEOUT - ) - - repos = repos_response.json()["repositories"] - repo_names = [r["name"] for r in repos] + while True: + repos_parameters = {"public": "true", "namespace": namespace, "next_page": next_page} + repos_response = requests.get( + QUAY_REPOSITORY_API_ENDPOINT, + headers=repos_headers, + params=repos_parameters, + timeout=MULLED_SOCKET_TIMEOUT, + ) + repos_response_json = repos_response.json() + repos = repos_response_json["repositories"] + repo_names += [r["name"] for r in repos] + next_page = repos_response_json.get("next_page") + if not next_page: + break if resolution_cache is not None: resolution_cache[cache_key] = repo_names return repo_name in repo_names diff --git a/lib/galaxy/tool_util/lint.py b/lib/galaxy/tool_util/lint.py index 3d3e89fe32d..df943559c83 100644 --- a/lib/galaxy/tool_util/lint.py +++ b/lib/galaxy/tool_util/lint.py @@ -104,17 +104,13 @@ class XMLLintMessageLine(LintMessage): def __init__(self, level: str, message: str, node: Optional[etree.Element] = None): super().__init__(level, message) self.line = None - self.fname = None if node is not None: self.line = node.sourceline - self.fname = node.base def __str__(self) -> str: rval = super().__str__() if self.line is not None: rval += " (" - if self.fname: - rval += f"{self.fname}:" rval += str(self.line) rval += ")" return rval diff --git a/lib/galaxy/tool_util/linters/outputs.py b/lib/galaxy/tool_util/linters/outputs.py index e988edbc799..898c3c1f52a 100644 --- a/lib/galaxy/tool_util/linters/outputs.py +++ b/lib/galaxy/tool_util/linters/outputs.py @@ -17,6 +17,8 @@ def lint_output(tool_xml, lint_ctx): if len(outputs) > 1: lint_ctx.warn("Tool contains multiple output sections, behavior undefined.", node=outputs[1]) num_outputs = 0 + labels = set() + names = set() for output in list(outputs[0]): if output.tag not in ["data", "collection"]: lint_ctx.warn(f"Unknown element found in outputs [{output.tag}]", node=output) @@ -30,6 +32,17 @@ def lint_output(tool_xml, lint_ctx): f'Tool output name [{output.attrib["name"]}] is not a valid Cheetah placeholder.', node=output ) + name = output.attrib.get("name") + if name is not None: + if name in names: + lint_ctx.error(f"Tool output [{name}] has duplicated name", node=output) + names.add(name) + + label = output.attrib.get("label", "${tool.name} on ${on_string}") + if label in labels: + lint_ctx.error(f"Tool output [{name}] uses duplicated label '{label}'", node=output) + labels.add(label) + format_set = False if __check_format(output, lint_ctx): format_set = True diff --git a/lib/galaxy/tool_util/linters/tests.py b/lib/galaxy/tool_util/linters/tests.py index 70560824883..90f3219ebbc 100644 --- a/lib/galaxy/tool_util/linters/tests.py +++ b/lib/galaxy/tool_util/linters/tests.py @@ -1,4 +1,5 @@ """This module contains a linting functions for tool tests.""" +import typing from inspect import ( Parameter, signature, @@ -118,12 +119,16 @@ def _check_asserts(test_idx, assertions, lint_ctx): if attrib not in assert_function_sig.parameters: lint_ctx.error(f"Test {test_idx}: unknown attribute '{attrib}' for '{a.tag}'", node=a) continue - if assert_function_sig.parameters[attrib].annotation is not Parameter.empty: + annotation = assert_function_sig.parameters[attrib].annotation + annotation = _handle_optionals(annotation) + if annotation is not Parameter.empty: try: - assert_function_sig.parameters[attrib].annotation(a.attrib[attrib]) + annotation(a.attrib[attrib]) + except TypeError: + raise Exception(f"Faild to instantiate {attrib} for {assert_function_name}") except ValueError: lint_ctx.error( - f"Test {test_idx}: attribute '{attrib}' for '{a.tag}' needs to be '{assert_function_sig.parameters[attrib].annotation.__name__}' got '{a.attrib[attrib]}'", + f"Test {test_idx}: attribute '{attrib}' for '{a.tag}' needs to be '{annotation.__name__}' got '{a.attrib[attrib]}'", node=a, ) # check missing required attributes @@ -141,6 +146,13 @@ def _check_asserts(test_idx, assertions, lint_ctx): lint_ctx.error(f"Test {test_idx}: '{a.tag}' needs to specify 'n', 'min', or 'max'", node=a) +def _handle_optionals(annotation): + as_dict = annotation.__dict__ + if "__origin__" in as_dict and as_dict["__origin__"] == typing.Union: + return as_dict["__args__"][0] + return annotation + + def _collect_output_names(tool_xml): output_data_names = [] output_collection_names = [] diff --git a/lib/galaxy/model/migrate/triggers/__init__.py b/lib/galaxy/tool_util/ontologies/__init__.py similarity index 100% rename from lib/galaxy/model/migrate/triggers/__init__.py rename to lib/galaxy/tool_util/ontologies/__init__.py diff --git a/lib/galaxy/tools/biotools_mappings.tsv b/lib/galaxy/tool_util/ontologies/biotools_mappings.tsv similarity index 100% rename from lib/galaxy/tools/biotools_mappings.tsv rename to lib/galaxy/tool_util/ontologies/biotools_mappings.tsv diff --git a/lib/galaxy/tool_util/ontologies/edam_operation_mappings.tsv b/lib/galaxy/tool_util/ontologies/edam_operation_mappings.tsv new file mode 100644 index 00000000000..bd68054193a --- /dev/null +++ b/lib/galaxy/tool_util/ontologies/edam_operation_mappings.tsv @@ -0,0 +1,2 @@ +# tool-id edam-operation +sort1 operation_3802 diff --git a/lib/galaxy/tool_util/ontologies/edam_topic_mappings.tsv b/lib/galaxy/tool_util/ontologies/edam_topic_mappings.tsv new file mode 100644 index 00000000000..df031e46d87 --- /dev/null +++ b/lib/galaxy/tool_util/ontologies/edam_topic_mappings.tsv @@ -0,0 +1 @@ +# tool-id edam-topic diff --git a/lib/galaxy/tool_util/ontologies/ontology_data.py b/lib/galaxy/tool_util/ontologies/ontology_data.py new file mode 100644 index 00000000000..edd3a308f65 --- /dev/null +++ b/lib/galaxy/tool_util/ontologies/ontology_data.py @@ -0,0 +1,108 @@ +from typing import ( + cast, + Dict, + List, + NamedTuple, + Optional, + Tuple, +) + +from galaxy.tool_util.biotools import BiotoolsMetadataSource +from galaxy.tool_util.parser import ToolSource +from galaxy.util.resources import files + + +def _multi_dict_mapping(content: str) -> Dict[str, List[str]]: + mapping: Dict[str, List[str]] = {} + for x in content.splitlines(): + if x.startswith("#"): + continue + key, value = cast(Tuple[str, str], tuple(x.split("\t"))) + mapping.setdefault(key, []).append(value) + return mapping + + +def _read_ontology_data_text(filename: str) -> str: + return files(PACKAGE).joinpath(filename).read_text() + + +PACKAGE = "galaxy.tool_util.ontologies" +BIOTOOLS_MAPPING_FILENAME = "biotools_mappings.tsv" +EDAM_OPERATION_MAPPING_FILENAME = "edam_operation_mappings.tsv" +EDAM_TOPIC_MAPPING_FILENAME = "edam_topic_mappings.tsv" + +BIOTOOLS_MAPPING_CONTENT = _read_ontology_data_text(BIOTOOLS_MAPPING_FILENAME) +BIOTOOLS_MAPPING: Dict[str, str] = dict( + [ + cast(Tuple[str, str], tuple(x.split("\t"))) + for x in BIOTOOLS_MAPPING_CONTENT.splitlines() + if not x.startswith("#") + ] +) +EDAM_OPERATION_MAPPING_CONTENT = _read_ontology_data_text(EDAM_OPERATION_MAPPING_FILENAME) +EDAM_OPERATION_MAPPING: Dict[str, List[str]] = _multi_dict_mapping(EDAM_OPERATION_MAPPING_CONTENT) + +EDAM_TOPIC_MAPPING_CONTENT = _read_ontology_data_text(EDAM_TOPIC_MAPPING_FILENAME) +EDAM_TOPIC_MAPPING: Dict[str, List[str]] = _multi_dict_mapping(EDAM_TOPIC_MAPPING_CONTENT) + + +class OntologyData(NamedTuple): + xrefs: List[Dict[str, str]] + edam_operations: Optional[List[str]] + edam_topics: Optional[List[str]] + + +def biotools_reference(xrefs): + for xref in xrefs: + if xref["reftype"] == "bio.tools": + return xref["value"] + return None + + +def legacy_biotools_external_reference(all_ids: List[str]) -> Optional[str]: + for tool_id in all_ids: + if tool_id in BIOTOOLS_MAPPING: + return BIOTOOLS_MAPPING[tool_id] + return None + + +def expand_ontology_data( + tool_source: ToolSource, all_ids: List[str], biotools_metadata_source: Optional[BiotoolsMetadataSource] +) -> OntologyData: + xrefs = tool_source.parse_xrefs() + has_biotools_reference = any(x["reftype"] == "bio.tools" for x in xrefs) + if not has_biotools_reference: + legacy_biotools_ref = legacy_biotools_external_reference(all_ids) + if legacy_biotools_ref is not None: + xrefs.append({"value": legacy_biotools_ref, "reftype": "bio.tools"}) + + edam_operations = tool_source.parse_edam_operations() + edam_topics = tool_source.parse_edam_topics() + + for tool_id in all_ids: + if tool_id in EDAM_OPERATION_MAPPING: + edam_operations = EDAM_OPERATION_MAPPING[tool_id] + break + + for tool_id in all_ids: + if tool_id in EDAM_TOPIC_MAPPING: + edam_topics = EDAM_TOPIC_MAPPING[tool_id] + break + + has_missing_data = len(edam_operations) == 0 or len(edam_topics) == 0 + if has_missing_data: + biotools_reference_str = biotools_reference(xrefs) + if biotools_reference_str and biotools_metadata_source: + biotools_entry = biotools_metadata_source.get_biotools_metadata(biotools_reference_str) + if biotools_entry: + edam_info = biotools_entry.edam_info + if len(edam_operations) == 0: + edam_operations = edam_info.edam_operations + if len(edam_topics) == 0: + edam_topics = edam_info.edam_topics + + return OntologyData( + xrefs, + edam_operations, + edam_topics, + ) diff --git a/lib/galaxy/tool_util/parser/cwl.py b/lib/galaxy/tool_util/parser/cwl.py index 9f8d0a35d23..5c3abcae2b9 100644 --- a/lib/galaxy/tool_util/parser/cwl.py +++ b/lib/galaxy/tool_util/parser/cwl.py @@ -182,3 +182,9 @@ class CwlPageSource(PageSource): def parse_input_sources(self): return self._input_list + + +__all__ = ( + "CwlToolSource", + "tool_proxy", +) diff --git a/lib/galaxy/tool_util/parser/xml.py b/lib/galaxy/tool_util/parser/xml.py index a07a565a85a..995d068c0d0 100644 --- a/lib/galaxy/tool_util/parser/xml.py +++ b/lib/galaxy/tool_util/parser/xml.py @@ -1044,7 +1044,7 @@ class StdioParser: elif re.search("fatal", err_level, re.IGNORECASE): return_level = StdioErrorLevel.FATAL else: - log.debug("Tool %s: error level %s did not match log/warning/fatal" % (self.id, err_level)) + log.debug(f"Tool {self.id}: error level {err_level} did not match log/warning/fatal") except Exception: log.exception("Exception in parse_error_level") return return_level diff --git a/lib/galaxy/tool_util/toolbox/base.py b/lib/galaxy/tool_util/toolbox/base.py index 775c31210ee..d32754f782d 100644 --- a/lib/galaxy/tool_util/toolbox/base.py +++ b/lib/galaxy/tool_util/toolbox/base.py @@ -668,6 +668,8 @@ class AbstractToolBox(Dictifiable, ManagesIntegratedToolPanelMixin): return self._tools_by_id[tool_id] elif tool_version in self._tool_versions_by_id[tool_id]: return self._tool_versions_by_id[tool_id][tool_version] + # should be if exact=True not elif? Otherwise we can end up doing non-exact searches even + # if exact=True. Anyway, changing it breaks a lot of tests involving built-in tools elif exact: # We're looking for an exact match, so we skip lineage and # versionless mapping, though we may want to check duplicate @@ -1443,3 +1445,16 @@ class BaseGalaxyToolBox(AbstractToolBox): def reload_dependency_manager(self): self._init_dependency_manager() + + def load_builtin_converters(self): + id = "builtin_converters" + section = ToolSection({"name": "Built-in Converters", "id": id}) + self._tool_panel[id] = section + + converters = self.app.datatypes_registry.datatype_converters + for source, targets in converters.items(): + for target, tool in targets.items(): + tool.name = f"{source}-to-{target}" + tool.description = "converter" + tool.hidden = False + section.elems.append_tool(tool) diff --git a/lib/galaxy/tool_util/verify/asserts/archive.py b/lib/galaxy/tool_util/verify/asserts/archive.py index e119ce5a3b4..a65bb814df3 100644 --- a/lib/galaxy/tool_util/verify/asserts/archive.py +++ b/lib/galaxy/tool_util/verify/asserts/archive.py @@ -3,6 +3,7 @@ import re import tarfile import tempfile import zipfile +from typing import Optional from galaxy.util import asbool from ._util import _assert_presence_number @@ -55,10 +56,10 @@ def assert_has_archive_member( verify_assertions_function, children, all="false", - n: int = None, + n: Optional[int] = None, delta: int = 0, - min: int = None, - max: int = None, + min: Optional[int] = None, + max: Optional[int] = None, negate: bool = False, ): """Recursively checks the specified children assertions against the text of diff --git a/lib/galaxy/tool_util/verify/asserts/size.py b/lib/galaxy/tool_util/verify/asserts/size.py index f40afee9558..089ba549227 100644 --- a/lib/galaxy/tool_util/verify/asserts/size.py +++ b/lib/galaxy/tool_util/verify/asserts/size.py @@ -1,8 +1,15 @@ +from typing import Optional + from ._util import _assert_number def assert_has_size( - output_bytes, value: int = None, delta: int = 0, min: int = None, max: int = None, negate: bool = False + output_bytes, + value: Optional[int] = None, + delta: int = 0, + min: Optional[int] = None, + max: Optional[int] = None, + negate: bool = False, ): """ Asserts the specified output has a size of the specified value, diff --git a/lib/galaxy/tool_util/verify/asserts/tabular.py b/lib/galaxy/tool_util/verify/asserts/tabular.py index 9c686a10945..6679fa9c06b 100644 --- a/lib/galaxy/tool_util/verify/asserts/tabular.py +++ b/lib/galaxy/tool_util/verify/asserts/tabular.py @@ -1,4 +1,5 @@ import re +from typing import Optional from ._util import _assert_number @@ -18,7 +19,14 @@ def get_first_line(output, comment): def assert_has_n_columns( - output, n: int = None, delta: int = 0, min: int = None, max: int = None, sep="\t", comment="", negate: bool = False + output, + n: Optional[int] = None, + delta: int = 0, + min: Optional[int] = None, + max: Optional[int] = None, + sep="\t", + comment="", + negate: bool = False, ): """Asserts the tabular output contains n columns. The optional sep argument specifies the column seperator used to determine the diff --git a/lib/galaxy/tool_util/verify/asserts/text.py b/lib/galaxy/tool_util/verify/asserts/text.py index 155df596498..a29a88bb336 100644 --- a/lib/galaxy/tool_util/verify/asserts/text.py +++ b/lib/galaxy/tool_util/verify/asserts/text.py @@ -1,4 +1,5 @@ import re +from typing import Optional from ._util import ( _assert_number, @@ -7,7 +8,13 @@ from ._util import ( def assert_has_text( - output, text, n: int = None, delta: int = 0, min: int = None, max: int = None, negate: bool = False + output, + text, + n: Optional[int] = None, + delta: int = 0, + min: Optional[int] = None, + max: Optional[int] = None, + negate: bool = False, ): """Asserts specified output contains the substring specified by the argument text. The exact number of occurrences can be @@ -37,7 +44,13 @@ def assert_not_has_text(output, text): def assert_has_line( - output, line, n: int = None, delta: int = 0, min: int = None, max: int = None, negate: bool = False + output, + line, + n: Optional[int] = None, + delta: int = 0, + min: Optional[int] = None, + max: Optional[int] = None, + negate: bool = False, ): """Asserts the specified output contains the line specified by the argument line. The exact number of occurrences can be optionally @@ -59,7 +72,14 @@ def assert_has_line( ) -def assert_has_n_lines(output, n: int = None, delta: int = 0, min: int = None, max: int = None, negate: bool = False): +def assert_has_n_lines( + output, + n: Optional[int] = None, + delta: int = 0, + min: Optional[int] = None, + max: Optional[int] = None, + negate: bool = False, +): """Asserts the specified output contains ``n`` lines allowing for a difference in the number of lines (delta) or relative differebce in the number of lines""" @@ -78,7 +98,13 @@ def assert_has_n_lines(output, n: int = None, delta: int = 0, min: int = None, m def assert_has_text_matching( - output, expression, n: int = None, delta: int = 0, min: int = None, max: int = None, negate: bool = False + output, + expression, + n: Optional[int] = None, + delta: int = 0, + min: Optional[int] = None, + max: Optional[int] = None, + negate: bool = False, ): """Asserts the specified output contains text matching the regular expression specified by the argument expression. @@ -102,7 +128,13 @@ def assert_has_text_matching( def assert_has_line_matching( - output, expression, n: int = None, delta: int = 0, min: int = None, max: int = None, negate: bool = False + output, + expression, + n: Optional[int] = None, + delta: int = 0, + min: Optional[int] = None, + max: Optional[int] = None, + negate: bool = False, ): """Asserts the specified output contains a line matching the regular expression specified by the argument expression. If n is given diff --git a/lib/galaxy/tool_util/verify/asserts/xml.py b/lib/galaxy/tool_util/verify/asserts/xml.py index 26acacd6bee..14d858d38bb 100644 --- a/lib/galaxy/tool_util/verify/asserts/xml.py +++ b/lib/galaxy/tool_util/verify/asserts/xml.py @@ -1,4 +1,5 @@ import re +from typing import Optional from lxml.etree import XMLSyntaxError @@ -28,7 +29,13 @@ def assert_has_element_with_path(output, path, negate: bool = False): def assert_has_n_elements_with_path( - output, path, n: int = None, delta: int = 0, min: int = None, max: int = None, negate: bool = False + output, + path, + n: Optional[int] = None, + delta: int = 0, + min: Optional[int] = None, + max: Optional[int] = None, + negate: bool = False, ): """Asserts the specified output has exactly n elements matching the path specified.""" @@ -74,10 +81,10 @@ def assert_xml_element( children=None, attribute=None, all=False, - n: int = None, + n: Optional[int] = None, delta: int = 0, - min: int = None, - max: int = None, + min: Optional[int] = None, + max: Optional[int] = None, negate: bool = False, ): """ diff --git a/lib/galaxy/tool_util/verify/interactor.py b/lib/galaxy/tool_util/verify/interactor.py index 9673081ba3c..b2eda787436 100644 --- a/lib/galaxy/tool_util/verify/interactor.py +++ b/lib/galaxy/tool_util/verify/interactor.py @@ -11,6 +11,7 @@ import urllib.parse import zipfile from json import dumps from logging import getLogger +from typing import Optional import requests from packaging.version import parse as parse_version @@ -810,7 +811,12 @@ class GalaxyInteractorApi: return urllib.parse.urljoin(f"{self.api_url}/", path) def _prepare_request_params( - self, data=None, files=None, as_json: bool = False, params: dict = None, headers: dict = None + self, + data=None, + files=None, + as_json: bool = False, + params: Optional[dict] = None, + headers: Optional[dict] = None, ): """Handle some Galaxy conventions and work around requests issues. diff --git a/lib/galaxy/tool_util/xsd/galaxy.xsd b/lib/galaxy/tool_util/xsd/galaxy.xsd index 5fa1fd5813a..1cb3d9b6e93 100644 --- a/lib/galaxy/tool_util/xsd/galaxy.xsd +++ b/lib/galaxy/tool_util/xsd/galaxy.xsd @@ -6006,7 +6006,7 @@ will contain the tool parameter inputs. ```xml - + ``` The following will instead write the inputs to the tool's working directory with @@ -6015,7 +6015,7 @@ the specified name (i.e. ``inputs.json``). ```xml - + ``` A contrived example of a tool that uses this is the test tool diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index 9f79340a6df..7999f3be04d 100644 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -30,7 +30,6 @@ import packaging.version import webob.exc from lxml import etree from mako.template import Template -from pkg_resources import resource_string from webob.compat import cgi_FieldStorage from galaxy import ( @@ -49,6 +48,10 @@ from galaxy.tool_util.loader import ( raw_tool_xml_tree, template_macro_params, ) +from galaxy.tool_util.ontologies.ontology_data import ( + biotools_reference, + expand_ontology_data, +) from galaxy.tool_util.output_checker import DETECTED_JOB_STATE from galaxy.tool_util.parser import ( get_tool_source, @@ -217,15 +220,6 @@ GALAXY_LIB_TOOLS_VERSIONED = { "winSplitter": packaging.version.parse("1.0.1"), } -BIOTOOLS_MAPPING_CONTENT = resource_string(__name__, "biotools_mappings.tsv").decode("UTF-8") -BIOTOOLS_MAPPING: Dict[str, str] = dict( - [ - cast(Tuple[str, str], tuple(x.split("\t"))) - for x in BIOTOOLS_MAPPING_CONTENT.splitlines() - if not x.startswith("#") - ] -) - REQUIRE_FULL_DIRECTORY = { "includes": [{"path": "**", "path_type": "glob"}], } @@ -1053,31 +1047,14 @@ class Tool(Dictifiable): self.required_files = required_files self.citations = self._parse_citations(tool_source) - xrefs = tool_source.parse_xrefs() - has_biotools_reference = any(x["reftype"] == "bio.tools" for x in xrefs) - if not has_biotools_reference: - legacy_biotools_ref = self.legacy_biotools_external_reference - if legacy_biotools_ref is not None: - xrefs.append({"value": legacy_biotools_ref, "reftype": "bio.tools"}) - self.xrefs = xrefs - - edam_operations = tool_source.parse_edam_operations() - edam_topics = tool_source.parse_edam_topics() - has_missing_data = len(edam_operations) == 0 or len(edam_topics) == 0 - if has_missing_data: - biotools_reference = self.biotools_reference - metadata_source = self.app.biotools_metadata_source - if biotools_reference and metadata_source: - biotools_entry = metadata_source.get_biotools_metadata(biotools_reference) - if biotools_entry: - edam_info = biotools_entry.edam_info - if len(edam_operations) == 0: - edam_operations = edam_info.edam_operations - if len(edam_topics) == 0: - edam_topics = edam_info.edam_topics - - self.edam_operations = edam_operations - self.edam_topics = edam_topics + ontology_data = expand_ontology_data( + tool_source, + self.all_ids, + self.app.biotools_metadata_source, + ) + self.xrefs = ontology_data.xrefs + self.edam_operations = ontology_data.edam_operations + self.edam_topics = ontology_data.edam_topics self.__parse_trackster_conf(tool_source) # Record macro paths so we can reload a tool if any of its macro has changes @@ -1553,24 +1530,13 @@ class Tool(Dictifiable): self.app, self.tool_shed, self.repository_owner, self.repository_name ) - @property - def legacy_biotools_external_reference(self) -> Optional[str]: - """Return a bio.tools ID if any of tool's IDs are BIOTOOLS_MAPPING.""" - for tool_id in self.all_ids: - if tool_id in BIOTOOLS_MAPPING: - return BIOTOOLS_MAPPING[tool_id] - return None - @property def biotools_reference(self) -> Optional[str]: """Return a bio.tools ID if external reference to it is found. If multiple bio.tools references are found, return just the first one. """ - for xref in self.xrefs: - if xref["reftype"] == "bio.tools": - return xref["value"] - return None + return biotools_reference(self.xrefs) @property def help(self): @@ -3433,9 +3399,7 @@ class SortTool(DatabaseOperationTool): presort_elements = [(dce.element_identifier, dce) for dce in elements] elif sorttype == "numeric": presort_elements = [(int(re.sub("[^0-9]", "", dce.element_identifier)), dce) for dce in elements] - if presort_elements: - sorted_elements = [x[1] for x in sorted(presort_elements, key=lambda x: x[0])] - if sorttype == "file": + elif sorttype == "file": hda = incoming["sort_type"]["sort_file"] data_lines = hda.metadata.get("data_lines", 0) if data_lines == len(elements): @@ -3450,8 +3414,13 @@ class SortTool(DatabaseOperationTool): message = f"List of element identifiers does not match element identifiers in collection '{hdca_history_name}'" raise Exception(message) else: - message = "Number of lines must match number of list elements (%i), but file has %i lines" - raise Exception(message % (data_lines, len(elements))) + message = f"Number of lines must match number of list elements ({len(elements)}), but file has {data_lines} lines" + raise Exception(message) + else: + raise Exception(f"Unknown sort_type '{sorttype}'") + + if presort_elements: + sorted_elements = [x[1] for x in sorted(presort_elements, key=lambda x: x[0])] for dce in sorted_elements: dce_object = dce.element_object diff --git a/lib/galaxy/tools/actions/__init__.py b/lib/galaxy/tools/actions/__init__.py index 29d9271ab8c..13085c84546 100644 --- a/lib/galaxy/tools/actions/__init__.py +++ b/lib/galaxy/tools/actions/__init__.py @@ -776,12 +776,13 @@ class DefaultToolAction(ToolAction): return remapped_hdas def __remap_parameters(self, job_to_remap, jtid, jtod, out_data): - input_values = {p.name: json.loads(p.value) for p in job_to_remap.parameters} + input_values = {p.name: json.loads(p.value) for p in job_to_remap.parameters if p.value is not None} old_dataset_id = jtod.dataset_id new_dataset_id = out_data[jtod.name].id input_values = update_dataset_ids(input_values, {old_dataset_id: new_dataset_id}, src="hda") for p in job_to_remap.parameters: - p.value = json.dumps(input_values[p.name]) + if p.name in input_values: + p.value = json.dumps(input_values[p.name]) jtid.dataset = out_data[jtod.name] jtid.dataset.hid = jtod.dataset.hid log.info(f"Job {job_to_remap.id} input HDA {jtod.dataset.id} remapped to new HDA {jtid.dataset.id}") diff --git a/lib/galaxy/tools/data/__init__.py b/lib/galaxy/tools/data/__init__.py index a2eb2cca077..4ca5dc345b8 100644 --- a/lib/galaxy/tools/data/__init__.py +++ b/lib/galaxy/tools/data/__init__.py @@ -1025,9 +1025,7 @@ class RefgenieToolDataTable(TabularToolDataTable): rval = [] for genome in rgc.list_genomes_by_asset(self.rg_asset): genome_attributes = rgc.get_genome_attributes(genome) - description = genome_attributes.get("genome_description", None) - if description: - description = f"{description} (refgenie: {genome})" + genome_description = genome_attributes.get("genome_description", None) asset_list = rgc.list(genome, include_tags=True)[genome] for tagged_asset in asset_list: asset, tag = tagged_asset.rsplit(":", 1) @@ -1035,7 +1033,10 @@ class RefgenieToolDataTable(TabularToolDataTable): continue digest = rgc.id(genome, asset, tag=tag) uuid = f"refgenie:{genome}/{self.rg_asset}:{tag}@{digest}" - display_name = description or f"{genome}/{tagged_asset}" + if genome_description: + display_name = f"{genome_description} (refgenie: {genome}@{digest})" + else: + display_name = f"{genome}/{tagged_asset}@{digest}" def _seek_key(key): return rgc.seek(genome, asset, tag_name=tag, seek_key=key) diff --git a/lib/galaxy/tools/evaluation.py b/lib/galaxy/tools/evaluation.py index eb0daba297b..4f1a34fc6fc 100644 --- a/lib/galaxy/tools/evaluation.py +++ b/lib/galaxy/tools/evaluation.py @@ -94,6 +94,7 @@ class ToolEvaluator: self.param_dict: Dict[str, Any] = {} self.extra_filenames: List[str] = [] self.environment_variables: List[Dict[str, str]] = [] + self.version_command_line: Optional[str] = None self.command_line: Optional[str] = None def set_compute_environment(self, compute_environment: ComputeEnvironment, get_special: Optional[Callable] = None): @@ -451,8 +452,9 @@ class ToolEvaluator: global_tool_logs(self._build_config_files, config_file, "Building Config Files") global_tool_logs(self._build_param_file, config_file, "Building Param File") global_tool_logs(self._build_command_line, config_file, "Building Command Line") + global_tool_logs(self._build_version_command, config_file, "Building Version Command Line") global_tool_logs(self._build_environment_variables, config_file, "Building Environment Variables") - return self.command_line, self.extra_filenames, self.environment_variables + return self.command_line, self.version_command_line, self.extra_filenames, self.environment_variables def _build_command_line(self): """ @@ -461,7 +463,6 @@ class ToolEvaluator: command = self.tool.command or "" param_dict = self.param_dict interpreter = self.tool.interpreter - version_string_cmd_raw = self.tool.version_string_cmd command_line = None if not command: return @@ -487,13 +488,16 @@ class ToolEvaluator: tool_dir = os.path.abspath(self.tool.tool_dir) abs_executable = os.path.join(tool_dir, executable) command_line = command_line.replace(executable, f"{interpreter} {shlex.quote(abs_executable)}", 1) + self.command_line = command_line + + def _build_version_command(self): + version_string_cmd_raw = self.tool.version_string_cmd if version_string_cmd_raw: version_command_template = string.Template(version_string_cmd_raw) - version_string_cmd = version_command_template.safe_substitute( + version_command = version_command_template.safe_substitute( {"__tool_directory__": self.compute_environment.tool_directory()} ) - command_line = f"{version_string_cmd} > {self.compute_environment.version_path()} 2>&1;\n{command_line}" - self.command_line = command_line + self.version_command_line = f"{version_command} > {self.compute_environment.version_path()} 2>&1;\n" def _build_config_files(self): """ @@ -662,7 +666,7 @@ class PartialToolEvaluator(ToolEvaluator): def build(self): config_file = self.tool.config_file global_tool_logs(self._build_environment_variables, config_file, "Building Environment Variables") - return self.command_line, self.extra_filenames, self.environment_variables + return self.command_line, self.version_command_line, self.extra_filenames, self.environment_variables class RemoteToolEvaluator(ToolEvaluator): @@ -677,4 +681,5 @@ class RemoteToolEvaluator(ToolEvaluator): global_tool_logs(self._build_config_files, config_file, "Building Config Files") global_tool_logs(self._build_param_file, config_file, "Building Param File") global_tool_logs(self._build_command_line, config_file, "Building Command Line") - return self.command_line, self.extra_filenames, self.environment_variables + global_tool_logs(self._build_version_command, config_file, "Building Version Command Line") + return self.command_line, self.version_command_line, self.extra_filenames, self.environment_variables diff --git a/lib/galaxy/tools/imp_exp/__init__.py b/lib/galaxy/tools/imp_exp/__init__.py index df125fc5dda..d07af19fee5 100644 --- a/lib/galaxy/tools/imp_exp/__init__.py +++ b/lib/galaxy/tools/imp_exp/__init__.py @@ -5,6 +5,7 @@ import shutil from galaxy import model from galaxy.model import store +from galaxy.schema.tasks import SetupHistoryExportJob from galaxy.util.path import external_chown log = logging.getLogger(__name__) @@ -98,23 +99,19 @@ class JobExportHistoryArchiveWrapper: Perform setup for job to export a history into an archive. """ app = self.app - + # TODO: prevent circular import here... from galaxy.celery.tasks import export_history + request = SetupHistoryExportJob( + history_id=history.id, + job_id=self.job_id, + store_directory=store_directory, + include_files=True, + include_hidden=include_hidden, + include_deleted=include_deleted, + ) if app.config.enable_celery_tasks: # symlink files on export, on worker files will tarred up in a dereferenced manner. - export_history.delay( - store_directory=store_directory, - history_id=history.id, - job_id=self.job_id, - include_hidden=include_hidden, - include_deleted=include_deleted, - ) + export_history.delay(request=request) else: - export_history( - store_directory=store_directory, - history_id=history.id, - job_id=self.job_id, - include_hidden=include_hidden, - include_deleted=include_deleted, - ) + export_history(request=request) diff --git a/lib/galaxy/tools/parameters/basic.py b/lib/galaxy/tools/parameters/basic.py index b2598f0c2a9..0fe105665bd 100644 --- a/lib/galaxy/tools/parameters/basic.py +++ b/lib/galaxy/tools/parameters/basic.py @@ -331,6 +331,14 @@ class SimpleTextToolParameter(ToolParameter): else: self.value = "" + def to_json(self, value, app, use_security): + """Convert a value to a string representation suitable for persisting""" + if value is None: + rval = "" if not self.optional else None + else: + rval = unicodify(value) + return rval + def get_initial_value(self, trans, other_values): return self.value @@ -611,10 +619,13 @@ class FileToolParameter(ToolParameter): if "session_id" in value: # handle api upload session_id = value["session_id"] - upload_store = trans.app.config.new_file_path + upload_store = trans.app.config.tus_upload_store or trans.app.config.new_file_path if re.match(r"^[\w-]+$", session_id) is None: raise ValueError("Invalid session id format.") local_filename = os.path.abspath(os.path.join(upload_store, session_id)) + if upload_store != trans.app.config.new_file_path and not os.path.exists(local_filename): + # Fallback for old chunked API, remove in 22.05 + local_filename = os.path.abspath(os.path.join(trans.app.config.new_file_path, session_id)) else: # handle nginx upload upload_store = trans.app.config.nginx_upload_store diff --git a/lib/galaxy/tools/parameters/dynamic_options.py b/lib/galaxy/tools/parameters/dynamic_options.py index fcd6794cc8e..af9fbd2f1f0 100644 --- a/lib/galaxy/tools/parameters/dynamic_options.py +++ b/lib/galaxy/tools/parameters/dynamic_options.py @@ -160,9 +160,6 @@ class DataMetaFilter(Filter): self.column = d_option.column_spec_to_index(self.column) self.multiple = string_as_bool(elem.get("multiple", "False")) self.separator = elem.get("separator", ",") - log.error( - f"data_meta.init: ref_name {self.ref_name} key {self.key} column {self.column} multiple {self.multiple} separator {self.separator}" - ) def get_dependency_name(self): return self.ref_name diff --git a/lib/galaxy/tools/parameters/validation.py b/lib/galaxy/tools/parameters/validation.py index ae38ca7db93..f907bde7857 100644 --- a/lib/galaxy/tools/parameters/validation.py +++ b/lib/galaxy/tools/parameters/validation.py @@ -742,9 +742,7 @@ class EmptyTextfieldValidator(Validator): return cls(message, negate) def validate(self, value, trans=None): - # Ideally the empty string validator should only check against an actual empty string, - # but the fill_defaults logic will store the result of `get_initial_value``, which is None. - super().validate(value not in ("", None)) + super().validate(value != "") class MetadataInFileColumnValidator(Validator): diff --git a/lib/galaxy/tools/remote_tool_eval.py b/lib/galaxy/tools/remote_tool_eval.py index d7c67391929..a9d6513954b 100644 --- a/lib/galaxy/tools/remote_tool_eval.py +++ b/lib/galaxy/tools/remote_tool_eval.py @@ -105,8 +105,8 @@ def main(TMPDIR, WORKING_DIRECTORY, IMPORT_STORE_DIRECTORY): ) tool_evaluator.set_compute_environment(compute_environment=SharedComputeEnvironment(job_io=job_io, job=job_io.job)) with open(os.path.join(WORKING_DIRECTORY, "tool_script.sh"), "a") as out: - command_line, extra_filenames, environment_variables = tool_evaluator.build() - out.write(command_line) + command_line, version_command_line, extra_filenames, environment_variables = tool_evaluator.build() + out.write(f'{version_command_line or ""}{command_line}') if __name__ == "__main__": diff --git a/lib/galaxy/util/__init__.py b/lib/galaxy/util/__init__.py index ec2b61cd186..429013e71c8 100644 --- a/lib/galaxy/util/__init__.py +++ b/lib/galaxy/util/__init__.py @@ -50,16 +50,17 @@ try: except ImportError: # For Pulsar on Windows (which does not use the function that uses grp) grp = None # type: ignore[assignment] -try: - import uwsgi -except ImportError: - uwsgi = None LXML_AVAILABLE = True try: from lxml import etree + + Element = etree._Element except ImportError: LXML_AVAILABLE = False import xml.etree.ElementTree as etree # type: ignore[assignment,no-redef] + + Element = etree.Element + try: import docutils.core as docutils_core import docutils.writers.html4css1 as docutils_html4css1 diff --git a/lib/galaxy/util/pastescript/__init__.py b/lib/galaxy/util/pastescript/__init__.py deleted file mode 100644 index 45db1d62d60..00000000000 --- a/lib/galaxy/util/pastescript/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -""" -Command for loading and serving wsgi apps taken from PasteScript -""" diff --git a/lib/galaxy/util/pastescript/loadwsgi.py b/lib/galaxy/util/pastescript/loadwsgi.py deleted file mode 100644 index 7bd7f9986e3..00000000000 --- a/lib/galaxy/util/pastescript/loadwsgi.py +++ /dev/null @@ -1,728 +0,0 @@ -# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) -# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php - -# Mostly taken from PasteDeploy and stripped down for Galaxy - -import inspect -import os -import re -import sys -from inspect import getfullargspec -from typing import ( - Callable, - Dict, - List, - Optional, - Union, -) -from urllib.parse import unquote - -import pkg_resources - -from galaxy.util.properties import NicerConfigParser - -__all__ = ("loadapp", "loadserver", "loadfilter", "appconfig") - -# ---- from paste.deploy.compat -------------------------------------- - -"""Python 2<->3 compatibility module""" - - -def print_(template, *args, **kwargs): - template = str(template) - if args: - template = template % args - elif kwargs: - template = template % kwargs - sys.stdout.writelines(template) - - -def reraise(t, e, tb): - exec("raise e from tb", dict(e=e, tb=tb)) - - -# ---- from paste.deploy.util ---------------------------------------- - - -def fix_type_error(exc_info, callable, varargs, kwargs): - """ - Given an exception, this will test if the exception was due to a - signature error, and annotate the error with better information if - so. - - Usage:: - - try: - val = callable(*args, **kw) - except TypeError: - exc_info = fix_type_error(None, callable, args, kw) - raise exc_info[0], exc_info[1], exc_info[2] - """ - if exc_info is None: - exc_info = sys.exc_info() - if ( - exc_info[0] != TypeError - or str(exc_info[1]).find("argument") == -1 - or getattr(exc_info[1], "_type_error_fixed", False) - ): - return exc_info - exc_info[1]._type_error_fixed = True - argspec = inspect.formatargspec(*getfullargspec(callable)) - args = ", ".join(map(_short_repr, varargs)) - if kwargs and args: - args += ", " - if kwargs: - kwargs = sorted(kwargs.keys()) - args += ", ".join(f"{n}=..." for n in kwargs) - gotspec = f"({args})" - msg = f"{exc_info[1]}; got {gotspec}, wanted {argspec}" - exc_info[1].args = (msg,) - return exc_info - - -def _short_repr(v): - v = repr(v) - if len(v) > 12: - v = f"{v[:8]}...{v[-4:]}" - return v - - -def fix_call(callable, *args, **kw): - """ - Call ``callable(*args, **kw)`` fixing any type errors that come out. - """ - try: - val = callable(*args, **kw) - except TypeError: - exc_info = fix_type_error(None, callable, args, kw) - reraise(*exc_info) - return val - - -def lookup_object(spec): - """ - Looks up a module or object from a some.module:func_name specification. - To just look up a module, omit the colon and everything after it. - """ - parts, target = spec.split(":") if ":" in spec else (spec, None) - module = __import__(parts) - - for part in parts.split(".")[1:] + ([target] if target else []): - module = getattr(module, part) - - return module - - -# ---- from paste.deploy.loadwsgi ------------------------------------ - -############################################################ -# Utility functions -############################################################ - - -def import_string(s): - return pkg_resources.EntryPoint.parse(f"x={s}").load(False) - - -def _aslist(obj): - """ - Turn object into a list; lists and tuples are left as-is, None - becomes [], and everything else turns into a one-element list. - """ - if obj is None: - return [] - elif isinstance(obj, (list, tuple)): - return obj - else: - return [obj] - - -def _flatten(lst): - """ - Flatten a nested list. - """ - if not isinstance(lst, (list, tuple)): - return [lst] - result = [] - for item in lst: - result.extend(_flatten(item)) - return result - - -############################################################ -# Object types -############################################################ - - -class _ObjectType: - - name: Optional[str] = None - egg_protocols: Optional[List[Union[str, List[str]]]] = None - config_prefixes: Optional[List[Union[List[str], str]]] = None - - def __init__(self): - # Normalize these variables: - self.egg_protocols = [_aslist(p) for p in _aslist(self.egg_protocols)] - self.config_prefixes = [_aslist(p) for p in _aslist(self.config_prefixes)] - - def __repr__(self): - return "<{} protocols={!r} prefixes={!r}>".format(self.name, self.egg_protocols, self.config_prefixes) - - def invoke(self, context): - assert context.protocol in _flatten(self.egg_protocols) - return fix_call(context.object, context.global_conf, **context.local_conf) - - -class _App(_ObjectType): - - name = "application" - egg_protocols = ["paste.app_factory", "paste.composite_factory", "paste.composit_factory"] - config_prefixes = [["app", "application"], ["composite", "composit"], "pipeline", "filter-app"] - - def invoke(self, context): - if context.protocol in ("paste.composit_factory", "paste.composite_factory"): - return fix_call(context.object, context.loader, context.global_conf, **context.local_conf) - elif context.protocol == "paste.app_factory": - return fix_call(context.object, context.global_conf, **context.local_conf) - else: - assert 0, f"Protocol {context.protocol!r} unknown" - - -APP = _App() - - -class _Filter(_ObjectType): - name = "filter" - egg_protocols = [["paste.filter_factory", "paste.filter_app_factory"]] - config_prefixes = ["filter"] - - def invoke(self, context): - if context.protocol == "paste.filter_factory": - return fix_call(context.object, context.global_conf, **context.local_conf) - elif context.protocol == "paste.filter_app_factory": - - def filter_wrapper(wsgi_app): - # This should be an object, so it has a nicer __repr__ - return fix_call(context.object, wsgi_app, context.global_conf, **context.local_conf) - - return filter_wrapper - else: - assert 0, f"Protocol {context.protocol!r} unknown" - - -FILTER = _Filter() - - -class _Server(_ObjectType): - name = "server" - egg_protocols = [["paste.server_factory", "paste.server_runner"]] - config_prefixes = ["server"] - - def invoke(self, context): - if context.protocol == "paste.server_factory": - return fix_call(context.object, context.global_conf, **context.local_conf) - elif context.protocol == "paste.server_runner": - - def server_wrapper(wsgi_app): - # This should be an object, so it has a nicer __repr__ - return fix_call(context.object, wsgi_app, context.global_conf, **context.local_conf) - - return server_wrapper - else: - assert 0, f"Protocol {context.protocol!r} unknown" - - -SERVER = _Server() - - -# Virtual type: (@@: There's clearly something crufty here; -# this probably could be more elegant) -class _PipeLine(_ObjectType): - name = "pipeline" - - def invoke(self, context): - app = context.app_context.create() - filters = [c.create() for c in context.filter_contexts] - filters.reverse() - for filter_ in filters: - app = filter_(app) - return app - - -PIPELINE = _PipeLine() - - -class _FilterApp(_ObjectType): - name = "filter_app" - - def invoke(self, context): - next_app = context.next_context.create() - filter_ = context.filter_context.create() - return filter_(next_app) - - -FILTER_APP = _FilterApp() - - -class _FilterWith(_App): - name = "filtered_with" - - def invoke(self, context): - filter_ = context.filter_context.create() - filtered = context.next_context.create() - if context.next_context.object_type is APP: - return filter_(filtered) - else: - # filtering a filter - def composed(app): - return filter_(filtered(app)) - - return composed - - -FILTER_WITH = _FilterWith() - -############################################################ -# Loaders -############################################################ - - -def loadapp(uri, name=None, **kw): - return loadobj(APP, uri, name=name, **kw) - - -def loadfilter(uri, name=None, **kw): - return loadobj(FILTER, uri, name=name, **kw) - - -def loadserver(uri, name=None, **kw): - return loadobj(SERVER, uri, name=name, **kw) - - -def appconfig(uri, name=None, relative_to=None, global_conf=None): - context = loadcontext(APP, uri, name=name, relative_to=relative_to, global_conf=global_conf) - return context.config() - - -_loaders: Dict[str, Callable] = {} - - -def loadobj(object_type, uri, name=None, relative_to=None, global_conf=None): - context = loadcontext(object_type, uri, name=name, relative_to=relative_to, global_conf=global_conf) - return context.create() - - -def loadcontext(object_type, uri, name=None, relative_to=None, global_conf=None): - if "#" in uri: - if name is None: - uri, name = uri.split("#", 1) - else: - # @@: Ignore fragment or error? - uri = uri.split("#", 1)[0] - if name is None: - name = "main" - if ":" not in uri: - raise LookupError(f"URI has no scheme: {uri!r}") - scheme, path = uri.split(":", 1) - scheme = scheme.lower() - if scheme not in _loaders: - raise LookupError(f"URI scheme not known: {scheme!r} (from {', '.join(_loaders.keys())})") - return _loaders[scheme](object_type, uri, path, name=name, relative_to=relative_to, global_conf=global_conf) - - -def _loadconfig(object_type, uri, path, name, relative_to, global_conf): - isabs = os.path.isabs(path) - # De-Windowsify the paths: - path = path.replace("\\", "/") - if not isabs: - if not relative_to: - raise ValueError("Cannot resolve relative uri %r; no relative_to keyword " "argument given" % uri) - relative_to = relative_to.replace("\\", "/") - if relative_to.endswith("/"): - path = relative_to + path - else: - path = f"{relative_to}/{path}" - if path.startswith("///"): - path = path[2:] - path = unquote(path) - loader = ConfigLoader(path) - if global_conf: - loader.update_defaults(global_conf, overwrite=False) - return loader.get_context(object_type, name, global_conf) - - -_loaders["config"] = _loadconfig - - -def _loadegg(object_type, uri, spec, name, relative_to, global_conf): - loader = EggLoader(spec) - return loader.get_context(object_type, name, global_conf) - - -_loaders["egg"] = _loadegg - - -def _loadfunc(object_type, uri, spec, name, relative_to, global_conf): - - loader = FuncLoader(spec) - return loader.get_context(object_type, name, global_conf) - - -_loaders["call"] = _loadfunc - -############################################################ -# Loaders -############################################################ - - -class _Loader: - def get_app(self, name=None, global_conf=None): - return self.app_context(name=name, global_conf=global_conf).create() - - def get_filter(self, name=None, global_conf=None): - return self.filter_context(name=name, global_conf=global_conf).create() - - def get_server(self, name=None, global_conf=None): - return self.server_context(name=name, global_conf=global_conf).create() - - def app_context(self, name=None, global_conf=None): - return self.get_context(APP, name=name, global_conf=global_conf) - - def filter_context(self, name=None, global_conf=None): - return self.get_context(FILTER, name=name, global_conf=global_conf) - - def server_context(self, name=None, global_conf=None): - return self.get_context(SERVER, name=name, global_conf=global_conf) - - _absolute_re = re.compile(r"^[a-zA-Z]+:") - - def absolute_name(self, name): - """ - Returns true if the name includes a scheme - """ - if name is None: - return False - return self._absolute_re.search(name) - - -class ConfigLoader(_Loader): - def __init__(self, filename): - self.filename = filename = filename.strip() - defaults = {"here": os.path.dirname(os.path.abspath(filename)), "__file__": os.path.abspath(filename)} - self.parser = NicerConfigParser(filename, defaults=defaults) - self.parser.optionxform = str # Don't lower-case keys - with open(filename) as f: - self.parser.read_file(f) - - def update_defaults(self, new_defaults, overwrite=True): - for key, value in new_defaults.items(): - if not overwrite and key in self.parser._defaults: - continue - self.parser._defaults[key] = value - - def get_context(self, object_type, name=None, global_conf=None): - if self.absolute_name(name): - return loadcontext(object_type, name, relative_to=os.path.dirname(self.filename), global_conf=global_conf) - section = self.find_config_section(object_type, name=name) - if global_conf is None: - global_conf = {} - else: - global_conf = global_conf.copy() - defaults = self.parser.defaults() - global_conf.update(defaults) - local_conf = {} - global_additions = {} - get_from_globals = {} - for option in self.parser.options(section): - if option.startswith("set "): - name = option[4:].strip() - global_additions[name] = global_conf[name] = self.parser.get(section, option) - elif option.startswith("get "): - name = option[4:].strip() - get_from_globals[name] = self.parser.get(section, option) - else: - if option in defaults: - # @@: It's a global option (?), so skip it - continue - local_conf[option] = self.parser.get(section, option) - for local_var, glob_var in get_from_globals.items(): - local_conf[local_var] = global_conf[glob_var] - if object_type in (APP, FILTER) and "filter-with" in local_conf: - filter_with = local_conf.pop("filter-with") - else: - filter_with = None - if "require" in local_conf: - for spec in local_conf["require"].split(): - pkg_resources.require(spec) - del local_conf["require"] - if section.startswith("filter-app:"): - context = self._filter_app_context( - object_type, - section, - name=name, - global_conf=global_conf, - local_conf=local_conf, - global_additions=global_additions, - ) - elif section.startswith("pipeline:"): - context = self._pipeline_app_context( - object_type, - section, - name=name, - global_conf=global_conf, - local_conf=local_conf, - global_additions=global_additions, - ) - elif "use" in local_conf: - context = self._context_from_use(object_type, local_conf, global_conf, global_additions, section) - else: - context = self._context_from_explicit(object_type, local_conf, global_conf, global_additions, section) - if filter_with is not None: - filter_with_context = LoaderContext( - obj=None, - object_type=FILTER_WITH, - protocol=None, - global_conf=global_conf, - local_conf=local_conf, - loader=self, - ) - filter_with_context.filter_context = self.filter_context(name=filter_with, global_conf=global_conf) - filter_with_context.next_context = context - return filter_with_context - return context - - def _context_from_use(self, object_type, local_conf, global_conf, global_additions, section): - use = local_conf.pop("use") - context = self.get_context(object_type, name=use, global_conf=global_conf) - context.global_conf.update(global_additions) - context.local_conf.update(local_conf) - if "__file__" in global_conf: - # use sections shouldn't overwrite the original __file__ - context.global_conf["__file__"] = global_conf["__file__"] - # @@: Should loader be overwritten? - context.loader = self - - if context.protocol is None: - # Determine protocol from section type - section_protocol = section.split(":", 1)[0] - if section_protocol in ("application", "app"): - context.protocol = "paste.app_factory" - elif section_protocol in ("composit", "composite"): - context.protocol = "paste.composit_factory" - else: - # This will work with 'server' and 'filter', otherwise it - # could fail but there is an error message already for - # bad protocols - context.protocol = f"paste.{section_protocol}_factory" - - return context - - def _context_from_explicit(self, object_type, local_conf, global_conf, global_addition, section): - possible = [] - for protocol_options in object_type.egg_protocols: - for protocol in protocol_options: - if protocol in local_conf: - possible.append((protocol, local_conf[protocol])) - break - if len(possible) > 1: - raise LookupError(f"Multiple protocols given in section {section!r}: {possible}") - if not possible: - raise LookupError(f"No loader given in section {section!r}") - found_protocol, found_expr = possible[0] - del local_conf[found_protocol] - value = import_string(found_expr) - context = LoaderContext(value, object_type, found_protocol, global_conf, local_conf, self) - return context - - def _filter_app_context(self, object_type, section, name, global_conf, local_conf, global_additions): - if "next" not in local_conf: - raise LookupError("The [%s] section in %s is missing a 'next' setting" % (section, self.filename)) - next_name = local_conf.pop("next") - context = LoaderContext(None, FILTER_APP, None, global_conf, local_conf, self) - context.next_context = self.get_context(APP, next_name, global_conf) - if "use" in local_conf: - context.filter_context = self._context_from_use(FILTER, local_conf, global_conf, global_additions, section) - else: - context.filter_context = self._context_from_explicit( - FILTER, local_conf, global_conf, global_additions, section - ) - return context - - def _pipeline_app_context(self, object_type, section, name, global_conf, local_conf, global_additions): - if "pipeline" not in local_conf: - raise LookupError("The [%s] section in %s is missing a 'pipeline' setting" % (section, self.filename)) - pipeline = local_conf.pop("pipeline").split() - if local_conf: - raise LookupError( - "The [%s] pipeline section in %s has extra " - "(disallowed) settings: %s" % (", ".join(local_conf.keys())) - ) - context = LoaderContext(None, PIPELINE, None, global_conf, local_conf, self) - context.app_context = self.get_context(APP, pipeline[-1], global_conf) - context.filter_contexts = [self.get_context(FILTER, pname, global_conf) for pname in pipeline[:-1]] - return context - - def find_config_section(self, object_type, name=None): - """ - Return the section name with the given name prefix (following the - same pattern as ``protocol_desc`` in ``config``. It must have the - given name, or for ``'main'`` an empty name is allowed. The - prefix must be followed by a ``:``. - - Case is *not* ignored. - """ - possible = [] - for name_options in object_type.config_prefixes: - for name_prefix in name_options: - found = self._find_sections(self.parser.sections(), name_prefix, name) - if found: - possible.extend(found) - break - if not possible: - raise LookupError( - "No section %r (prefixed by %s) found in config %s" - % (name, " or ".join(map(repr, _flatten(object_type.config_prefixes))), self.filename) - ) - if len(possible) > 1: - raise LookupError( - "Ambiguous section names %r for section %r (prefixed by %s) " - "found in config %s" - % (possible, name, " or ".join(map(repr, _flatten(object_type.config_prefixes))), self.filename) - ) - return possible[0] - - def _find_sections(self, sections, name_prefix, name): - found = [] - if name is None: - if name_prefix in sections: - found.append(name_prefix) - name = "main" - for section in sections: - if section.startswith(f"{name_prefix}:"): - if section[len(name_prefix) + 1 :].strip() == name: - found.append(section) - return found - - -class EggLoader(_Loader): - def __init__(self, spec): - self.spec = spec - - def get_context(self, object_type, name=None, global_conf=None): - if self.absolute_name(name): - return loadcontext(object_type, name, global_conf=global_conf) - entry_point, protocol, ep_name = self.find_egg_entry_point(object_type, name=name) - return LoaderContext( - entry_point, - object_type, - protocol, - global_conf or {}, - {}, - self, - distribution=pkg_resources.get_distribution(self.spec), - entry_point_name=ep_name, - ) - - def find_egg_entry_point(self, object_type, name=None): - """ - Returns the (entry_point, protocol) for the with the given - ``name``. - """ - if name is None: - name = "main" - possible = [] - for protocol_options in object_type.egg_protocols: - for protocol in protocol_options: - pkg_resources.require(self.spec) - entry = pkg_resources.get_entry_info(self.spec, protocol, name) - if entry is not None: - possible.append((entry.load(), protocol, entry.name)) - break - if not possible: - # Better exception - dist = pkg_resources.get_distribution(self.spec) - raise LookupError( - "Entry point %r not found in egg %r (dir: %s; protocols: %s; " - "entry_points: %s)" - % ( - name, - self.spec, - dist.location, - ", ".join(_flatten(object_type.egg_protocols)), - ", ".join( - _flatten( - [ - list((pkg_resources.get_entry_info(self.spec, prot, name) or {}).keys()) - for prot in protocol_options - ] - or "(no entry points)" - ) - ), - ) - ) - if len(possible) > 1: - raise LookupError( - "Ambiguous entry points for %r in egg %r (protocols: %s)" - % (name, self.spec, ", ".join(_flatten(protocol_options))) - ) - return possible[0] - - -class FuncLoader(_Loader): - """Loader that supports specifying functions inside modules, without - using eggs at all. Configuration should be in the format: - use = call:my.module.path:function_name - - Dot notation is supported in both the module and function name, e.g.: - use = call:my.module.path:object.method - """ - - def __init__(self, spec): - self.spec = spec - if ":" not in spec: - raise LookupError("Configuration not in format module:function") - - def get_context(self, object_type, name=None, global_conf=None): - obj = lookup_object(self.spec) - return LoaderContext( - obj, - object_type, - None, # determine protocol from section type - global_conf or {}, - {}, - self, - ) - - -class LoaderContext: - def __init__( - self, obj, object_type, protocol, global_conf, local_conf, loader, distribution=None, entry_point_name=None - ): - self.object = obj - self.object_type = object_type - self.protocol = protocol - # assert protocol in _flatten(object_type.egg_protocols), ( - # "Bad protocol %r; should be one of %s" - # % (protocol, ', '.join(map(repr, _flatten(object_type.egg_protocols))))) - self.global_conf = global_conf - self.local_conf = local_conf - self.loader = loader - self.distribution = distribution - self.entry_point_name = entry_point_name - - def create(self): - return self.object_type.invoke(self) - - def config(self): - conf = AttrDict(self.global_conf) - conf.update(self.local_conf) - conf.local_conf = self.local_conf - conf.global_conf = self.global_conf - conf.context = self - return conf - - -class AttrDict(dict): - """ - A dictionary that can be assigned to. - """ diff --git a/lib/galaxy/util/pastescript/serve.py b/lib/galaxy/util/pastescript/serve.py deleted file mode 100644 index 42cdf4c26fb..00000000000 --- a/lib/galaxy/util/pastescript/serve.py +++ /dev/null @@ -1,1016 +0,0 @@ -# Most of this code is: - -# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) -# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php - -# The server command includes the additional header: - -# For discussion of daemonizing: -# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/278731 -# Code taken also from QP: -# http://www.mems-exchange.org/software/qp/ -# From lib/site.py - -# Galaxy originally used PasteScript and PasteDeploy for application -# loading, to maintain compatibility we've internalized some of that -# code here, stripping out uneeded functionality. - -# All top level imports from each package moved here and organized - -import atexit -import configparser -import errno -import grp -import logging -import optparse -import os -import pwd -import re -import resource -import signal -import socket -import subprocess -import sys -import textwrap -import threading -import time -from gettext import gettext as _ -from logging.config import fileConfig -from typing import Optional - -from .loadwsgi import ( - loadapp, - loadserver, -) - -difflib = None - -# ---- from paste.script.bool_optparse -------------------------------- - -""" -A subclass of ``optparse.OptionParser`` that allows boolean long -options (like ``--verbose``) to also take arguments (like -``--verbose=true``). Arguments *must* use ``=``. -""" - - -class BoolOptionParser(optparse.OptionParser): - def _process_long_opt(self, rargs, values): - arg = rargs.pop(0) - - # Value explicitly attached to arg? Pretend it's the next - # argument. - if "=" in arg: - (opt, next_arg) = arg.split("=", 1) - rargs.insert(0, next_arg) - had_explicit_value = True - else: - opt = arg - had_explicit_value = False - - opt = self._match_long_opt(opt) - option = self._long_opt[opt] - if option.takes_value(): - nargs = option.nargs - if len(rargs) < nargs: - if nargs == 1: - self.error(_("%s option requires an argument") % opt) - else: - self.error(_("%s option requires %d arguments") % (opt, nargs)) - elif nargs == 1: - value = rargs.pop(0) - else: - value = tuple(rargs[0:nargs]) - del rargs[0:nargs] - - elif had_explicit_value: - value = rargs[0].lower().strip() - del rargs[0:1] - if value in ("true", "yes", "on", "1", "y", "t"): - value = None - elif value in ("false", "no", "off", "0", "n", "f"): - # Don't process - return - else: - self.error(_("%s option takes a boolean value only (true/false)") % opt) - - else: - value = None - - option.process(opt, value, values, self) - - -# ---- from paste.script.command -------------------------------------- - -# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) -# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php - - -class BadCommand(Exception): - def __init__(self, message, exit_code=2): - self.message = message - self.exit_code = exit_code - Exception.__init__(self, message) - - def _get_message(self): - """Getter for 'message'; needed only to override deprecation - in BaseException.""" - return self.__message - - def _set_message(self, value): - """Setter for 'message'; needed only to override deprecation - in BaseException.""" - self.__message = value - - # BaseException.message has been deprecated since Python 2.6. - # To prevent DeprecationWarning from popping up over this - # pre-existing attribute, use a new property that takes lookup - # precedence. - message = property(_get_message, _set_message) - - -class NoDefault: - pass - - -# run and invoke methods moved below ServeCommand -class Command: - def __init__(self, name): - self.command_name = name - - max_args = None - max_args_error = "You must provide no more than %(max_args)s arguments" - min_args: Optional[int] = None - min_args_error = "You must provide at least %(min_args)s arguments" - required_args = None - # If this command takes a configuration file, set this to 1 or -1 - # Then if invoked through #! the config file will be put into the positional - # arguments -- at the beginning with 1, at the end with -1 - takes_config_file: Optional[int] = None - - # Grouped in help messages by this: - group_name = "" - - required_args = () - description: Optional[str] = None - usage = "" - hidden = False - # This is the default verbosity level; --quiet subtracts, - # --verbose adds: - default_verbosity = 0 - # This is the default interactive state: - default_interactive = 0 - return_code = 0 - - BadCommand = BadCommand - - # Must define: - # parser - # summary - # command() - - def run(self, args): - self.parse_args(args) - - # Setup defaults: - for name, default in [("verbose", 0), ("quiet", 0), ("interactive", False), ("overwrite", False)]: - if not hasattr(self.options, name): - setattr(self.options, name, default) - if getattr(self.options, "simulate", False): - self.options.verbose = max(self.options.verbose, 1) - self.interactive = self.default_interactive - if getattr(self.options, "interactive", False): - self.interactive += self.options.interactive - if getattr(self.options, "no_interactive", False): - self.interactive = False - self.verbose = self.default_verbosity - self.verbose += self.options.verbose - self.verbose -= self.options.quiet - self.simulate = getattr(self.options, "simulate", False) - - # For #! situations: - if os.environ.get("PASTE_CONFIG_FILE") and self.takes_config_file is not None: - take = self.takes_config_file - filename = os.environ.get("PASTE_CONFIG_FILE") - if take == 1: - self.args.insert(0, filename) - elif take == -1: - self.args.append(filename) - else: - assert 0, f"Value takes_config_file must be None, 1, or -1 (not {take!r})" - - if os.environ.get("PASTE_DEFAULT_QUIET"): - self.verbose = 0 - - # Validate: - if self.min_args is not None and len(self.args) < self.min_args: - raise BadCommand(self.min_args_error % {"min_args": self.min_args, "actual_args": len(self.args)}) - if self.max_args is not None and len(self.args) > self.max_args: - raise BadCommand(self.max_args_error % {"max_args": self.max_args, "actual_args": len(self.args)}) - for var_name, option_name in self.required_args: - if not getattr(self.options, var_name, None): - raise BadCommand(f"You must provide the option {option_name}") - result = self.command() - if result is None: - return self.return_code - else: - return result - - def parse_args(self, args): - if self.usage: - usage = f" {self.usage}" - else: - usage = "" - self.parser.usage = f"%prog [options]{usage}\n{self.summary}" - self.parser.prog = self._prog_name() - if self.description: - desc = self.description - desc = textwrap.dedent(desc) - self.parser.description = desc - self.options, self.args = self.parser.parse_args(args) - - def _prog_name(self): - return f"{os.path.basename(sys.argv[0])} {self.command_name}" - - ######################################## - # Utility methods - ######################################## - - def pad(self, s, length, dir="left"): - if len(s) >= length: - return s - if dir == "left": - return s + " " * (length - len(s)) - else: - return " " * (length - len(s)) + s - - def _standard_parser( - cls, verbose=True, interactive=False, no_interactive=False, simulate=False, quiet=False, overwrite=False - ): - """ - Create a standard ``OptionParser`` instance. - - Typically used like:: - - class MyCommand(Command): - parser = Command.standard_parser() - - Subclasses may redefine ``standard_parser``, so use the - nearest superclass's class method. - """ - parser = BoolOptionParser() - if verbose: - parser.add_option("-v", "--verbose", action="count", dest="verbose", default=0) - if quiet: - parser.add_option("-q", "--quiet", action="count", dest="quiet", default=0) - if no_interactive: - parser.add_option("--no-interactive", action="count", dest="no_interactive", default=0) - if interactive: - parser.add_option("-i", "--interactive", action="count", dest="interactive", default=0) - if simulate: - parser.add_option("-n", "--simulate", action="store_true", dest="simulate", default=False) - if overwrite: - parser.add_option( - "-f", - "--overwrite", - dest="overwrite", - action="store_true", - help="Overwrite files (warnings will be emitted for non-matching files otherwise)", - ) - return parser - - standard_parser = classmethod(_standard_parser) - - def quote_first_command_arg(self, arg): - """ - There's a bug in Windows when running an executable that's - located inside a path with a space in it. This method handles - that case, or on non-Windows systems or an executable with no - spaces, it just leaves well enough alone. - """ - if sys.platform != "win32" or " " not in arg: - # Problem does not apply: - return arg - try: - import win32api - except ImportError: - raise ValueError( - "The executable %r contains a space, and in order to " - "handle this issue you must have the win32api module " - "installed" % arg - ) - arg = win32api.GetShortPathName(arg) - return arg - - def parse_vars(self, args): - """ - Given variables like ``['a=b', 'c=d']`` turns it into ``{'a': - 'b', 'c': 'd'}`` - """ - result = {} - for arg in args: - if "=" not in arg: - raise BadCommand(f'Variable assignment {arg!r} invalid (no "=")') - name, value = arg.split("=", 1) - result[name] = value - return result - - def logging_file_config(self, config_file): - """ - Setup logging via the logging module's fileConfig function with the - specified ``config_file``, if applicable. - - ConfigParser defaults are specified for the special ``__file__`` - and ``here`` variables, similar to PasteDeploy config loading. - """ - parser = configparser.ConfigParser() - parser.read([config_file]) - if parser.has_section("loggers"): - config_file = os.path.abspath(config_file) - fileConfig(config_file, dict(__file__=config_file, here=os.path.dirname(config_file))) - - -class NotFoundCommand(Command): - def run(self, args): - print("Command %r not known (you may need to run setup.py egg_info)" % self.command_name) - commands = list() - commands.sort() - if not commands: - print("No commands registered.") - print("Have you installed Paste Script?") - print("(try running python setup.py develop)") - return 2 - print("Known commands:") - longest = max(len(n) for n, c in commands) - for name, command in commands: - print(f" {self.pad(name, length=longest)} {command.load().summary}") - return 2 - - -# ---- From paste.script.serve ---------------------------------------- - -MAXFD = 1024 - -jython = sys.platform.startswith("java") - - -class DaemonizeException(Exception): - pass - - -class ServeCommand(Command): - - min_args = 0 - usage = "CONFIG_FILE [start|stop|restart|status] [var=value]" - takes_config_file = 1 - summary = "Serve the described application" - description: Optional[ - str - ] = """\ - This command serves a web application that uses a paste.deploy - configuration file for the server and application. - - If start/stop/restart is given, then --daemon is implied, and it will - start (normal operation), stop (--stop-daemon), or do both. - - You can also include variable assignments like 'http_port=8080' - and then use %(http_port)s in your config files. - """ - - # used by subclasses that configure apps and servers differently - requires_config_file = True - - parser = Command.standard_parser(quiet=True) - parser.add_option( - "-n", "--app-name", dest="app_name", metavar="NAME", help="Load the named application (default main)" - ) - parser.add_option("-s", "--server", dest="server", metavar="SERVER_TYPE", help="Use the named server.") - parser.add_option( - "--server-name", - dest="server_name", - metavar="SECTION_NAME", - help="Use the named server as defined in the configuration file (default: main)", - ) - if hasattr(os, "fork"): - parser.add_option("--daemon", dest="daemon", action="store_true", help="Run in daemon (background) mode") - parser.add_option( - "--pid-file", - dest="pid_file", - metavar="FILENAME", - help="Save PID to file (default to paster.pid if running in daemon mode)", - ) - parser.add_option( - "--log-file", dest="log_file", metavar="LOG_FILE", help="Save output to the given log file (redirects stdout)" - ) - parser.add_option("--reload", dest="reload", action="store_true", help="Use auto-restart file monitor") - parser.add_option( - "--reload-interval", - dest="reload_interval", - default=1, - help="Seconds between checking files (low number can cause significant CPU usage)", - ) - parser.add_option( - "--monitor-restart", dest="monitor_restart", action="store_true", help="Auto-restart server if it dies" - ) - parser.add_option( - "--status", - action="store_true", - dest="show_status", - help="Show the status of the (presumably daemonized) server", - ) - - if hasattr(os, "setuid"): - # I don't think these are available on Windows - parser.add_option( - "--user", dest="set_user", metavar="USERNAME", help="Set the user (usually only possible when run as root)" - ) - parser.add_option( - "--group", dest="set_group", metavar="GROUP", help="Set the group (usually only possible when run as root)" - ) - - parser.add_option( - "--stop-daemon", - dest="stop_daemon", - action="store_true", - help="Stop a daemonized server (given a PID file, or default paster.pid file)", - ) - - if jython: - parser.add_option( - "--disable-jython-reloader", - action="store_true", - dest="disable_jython_reloader", - help="Disable the Jython reloader", - ) - - _scheme_re = re.compile(r"^[a-z][a-z]+:", re.I) - - default_verbosity = 1 - - _reloader_environ_key = "PYTHON_RELOADER_SHOULD_RUN" - _monitor_environ_key = "PASTE_MONITOR_SHOULD_RUN" - - possible_subcommands = ("start", "stop", "restart", "status") - - def command(self): - if self.options.stop_daemon: - return self.stop_daemon() - - if not hasattr(self.options, "set_user"): - # Windows case: - self.options.set_user = self.options.set_group = None - # @@: Is this the right stage to set the user at? - self.change_user_group(self.options.set_user, self.options.set_group) - - if self.requires_config_file: - if not self.args: - raise BadCommand("You must give a config file") - app_spec = self.args[0] - if len(self.args) > 1 and self.args[1] in self.possible_subcommands: - cmd = self.args[1] - restvars = self.args[2:] - else: - cmd = None - restvars = self.args[1:] - else: - app_spec = "" - if self.args and self.args[0] in self.possible_subcommands: - cmd = self.args[0] - restvars = self.args[1:] - else: - cmd = None - restvars = self.args[:] - - if getattr(self.options, "daemon", False) and getattr(self.options, "reload", False): - raise BadCommand("The --daemon and --reload options may not be used together") - - jython_monitor = False - if self.options.reload: - if jython and not self.options.disable_jython_reloader: - # JythonMonitor raises the special SystemRestart - # exception that'll cause the Jython interpreter to - # reload in the existing Java process (avoiding - # subprocess startup time) - try: - from paste.reloader import JythonMonitor - except ImportError: - pass - else: - jython_monitor = JythonMonitor(poll_interval=int(self.options.reload_interval)) - if self.requires_config_file: - jython_monitor.watch_file(self.args[0]) - - if not jython_monitor: - if os.environ.get(self._reloader_environ_key): - from paste import reloader - - if self.verbose > 1: - print("Running reloading file monitor") - reloader.install(int(self.options.reload_interval)) - if self.requires_config_file: - reloader.watch_file(self.args[0]) - else: - return self.restart_with_reloader() - - if cmd not in (None, "start", "stop", "restart", "status"): - raise BadCommand(f"Error: must give start|stop|restart (not {cmd})") - - if cmd == "status" or self.options.show_status: - return self.show_status() - - if cmd == "restart" or cmd == "stop": - result = self.stop_daemon() - if result: - print("Could not stop daemon") - # It's ok to continue trying to restart if stop_daemon returns - # a 1, otherwise shortcut and return. - if cmd == "restart" and result != 1: - return result - if cmd == "stop": - return result - self.options.daemon = True - - if cmd == "start": - self.options.daemon = True - - app_name = self.options.app_name - vars = self.parse_vars(restvars) - if not self._scheme_re.search(app_spec): - app_spec = f"config:{app_spec}" - server_name = self.options.server_name - if self.options.server: - server_spec = "egg:PasteScript" - assert server_name is None - server_name = self.options.server - else: - server_spec = app_spec - base = os.getcwd() - - if getattr(self.options, "daemon", False): - if not self.options.pid_file: - self.options.pid_file = "paster.pid" - if not self.options.log_file: - self.options.log_file = "paster.log" - - # Ensure the log file is writeable - if self.options.log_file: - try: - writeable_log_file = open(self.options.log_file, "a") - except OSError as ioe: - msg = f"Error: Unable to write to log file: {ioe}" - raise BadCommand(msg) - writeable_log_file.close() - - # Ensure the pid file is writeable - if self.options.pid_file: - try: - writeable_pid_file = open(self.options.pid_file, "a") - except OSError as ioe: - msg = f"Error: Unable to write to pid file: {ioe}" - raise BadCommand(msg) - writeable_pid_file.close() - - if getattr(self.options, "daemon", False): - try: - self.daemonize() - except DaemonizeException as ex: - if self.verbose > 0: - print(str(ex)) - return - - if self.options.monitor_restart and not os.environ.get(self._monitor_environ_key): - return self.restart_with_monitor() - - if self.options.pid_file: - self.record_pid(self.options.pid_file) - - if self.options.log_file: - stdout_log = LazyWriter(self.options.log_file, "a") - sys.stdout = stdout_log - sys.stderr = stdout_log - logging.basicConfig(stream=stdout_log) - - log_fn = app_spec - if log_fn.startswith("config:"): - log_fn = app_spec[len("config:") :] - elif log_fn.startswith("egg:"): - log_fn = None - if log_fn: - log_fn = os.path.join(base, log_fn) - self.logging_file_config(log_fn) - - server = loadserver(server_spec, name=server_name, relative_to=base, global_conf=vars) - - app = loadapp(app_spec, name=app_name, relative_to=base, global_conf=vars) - - if self.verbose > 0: - if hasattr(os, "getpid"): - msg = "Starting server in PID %i." % os.getpid() - else: - msg = "Starting server." - print(msg) - - def serve(): - try: - server(app) - except (SystemExit, KeyboardInterrupt) as e: - if self.verbose > 1: - raise - if str(e): - msg = f" {str(e)}" - else: - msg = "" - print(f"Exiting{msg} (-v to see traceback)") - except AttributeError as e: - # Capturing bad error response from paste - if str(e) == "'WSGIThreadPoolServer' object has no attribute 'thread_pool'": - raise OSError(98, "Address already in use") - else: - raise AttributeError(e) - - if jython_monitor: - # JythonMonitor has to be ran from the main thread - threading.Thread(target=serve).start() - print("Starting Jython file monitor") - jython_monitor.periodic_reload() - else: - serve() - - def daemonize(self): - pid = live_pidfile(self.options.pid_file) - if pid: - raise DaemonizeException( - "Daemon is already running (PID: %s from PID file %s)" % (pid, self.options.pid_file) - ) - - if self.verbose > 0: - print("Entering daemon mode") - pid = os.fork() - if pid: - # The forked process also has a handle on resources, so we - # *don't* want proper termination of the process, we just - # want to exit quick (which os._exit() does) - os._exit(0) - # Make this the session leader - os.setsid() - # Fork again for good measure! - pid = os.fork() - if pid: - os._exit(0) - - # @@: Should we set the umask and cwd now? - - maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1] - if maxfd == resource.RLIM_INFINITY: - maxfd = MAXFD - # Iterate through and close all file descriptors. - for fd in range(0, maxfd): - try: - os.close(fd) - except OSError: # ERROR, fd wasn't open to begin with (ignored) - pass - - if hasattr(os, "devnull"): - REDIRECT_TO = os.devnull - else: - REDIRECT_TO = "/dev/null" - os.open(REDIRECT_TO, os.O_RDWR) # standard input (0) - # Duplicate standard input to standard output and standard error. - os.dup2(0, 1) # standard output (1) - os.dup2(0, 2) # standard error (2) - - def record_pid(self, pid_file): - pid = os.getpid() - if self.verbose > 1: - print(f"Writing PID {pid} to {pid_file}") - f = open(pid_file, "w") - f.write(str(pid)) - f.close() - atexit.register(_remove_pid_file, pid, pid_file, self.verbose) - - def stop_daemon(self): - pid_file = self.options.pid_file or "paster.pid" - if not os.path.exists(pid_file): - print(f"No PID file exists in {pid_file}") - return 1 - pid = read_pidfile(pid_file) - if not pid: - print(f"Not a valid PID file in {pid_file}") - return 1 - pid = live_pidfile(pid_file) - if not pid: - print(f"PID in {pid_file} is not valid (deleting)") - try: - os.unlink(pid_file) - except OSError as e: - print(f"Could not delete: {e}") - return 2 - return 1 - for _i in range(10): - if not live_pidfile(pid_file): - break - os.kill(pid, signal.SIGTERM) - time.sleep(1) - else: - print(f"failed to kill web process {pid}") - return 3 - if os.path.exists(pid_file): - os.unlink(pid_file) - return 0 - - def show_status(self): - pid_file = self.options.pid_file or "paster.pid" - if not os.path.exists(pid_file): - print(f"No PID file {pid_file}") - return 1 - pid = read_pidfile(pid_file) - if not pid: - print(f"No PID in file {pid_file}") - return 1 - pid = live_pidfile(pid_file) - if not pid: - print(f"PID {pid} in {pid_file} is not running") - return 1 - print(f"Server running in PID {pid}") - return 0 - - def restart_with_reloader(self): - self.restart_with_monitor(reloader=True) - - def restart_with_monitor(self, reloader=False): - if self.verbose > 0: - if reloader: - print("Starting subprocess with file monitor") - else: - print("Starting subprocess with monitor parent") - while 1: - args = [self.quote_first_command_arg(sys.executable)] + sys.argv - new_environ = os.environ.copy() - if reloader: - new_environ[self._reloader_environ_key] = "true" - else: - new_environ[self._monitor_environ_key] = "true" - proc = None - try: - try: - _turn_sigterm_into_systemexit() - proc = subprocess.Popen(args, env=new_environ) - exit_code = proc.wait() - proc = None - except KeyboardInterrupt: - print("^C caught in monitor process") - if self.verbose > 1: - raise - return 1 - finally: - if proc is not None and hasattr(os, "kill"): - try: - os.kill(proc.pid, signal.SIGTERM) - except OSError: - pass - - if reloader: - # Reloader always exits with code 3; but if we are - # a monitor, any exit code will restart - if exit_code != 3: - return exit_code - if self.verbose > 0: - print("-" * 20, "Restarting", "-" * 20) - - def change_user_group(self, user, group): - if not user and not group: - return - uid = gid = None - if group: - try: - gid = int(group) - group = grp.getgrgid(gid).gr_name - except ValueError: - try: - entry = grp.getgrnam(group) - except KeyError: - raise BadCommand(f"Bad group: {group!r}; no such group exists") - gid = entry.gr_gid - try: - uid = int(user) - user = pwd.getpwuid(uid).pw_name - except ValueError: - try: - entry = pwd.getpwnam(user) - except KeyError: - raise BadCommand(f"Bad username: {user!r}; no such user exists") - if not gid: - gid = entry.pw_gid - uid = entry.pw_uid - if self.verbose > 0: - print("Changing user to {}:{} ({}:{})".format(user, group or "(unknown)", uid, gid)) - if hasattr(os, "initgroups"): - os.initgroups(user, gid) - else: - os.setgroups([e.gr_gid for e in grp.getgrall() if user in e.gr_mem] + [gid]) - if gid: - os.setgid(gid) - if uid: - os.setuid(uid) - - -class LazyWriter: - - """ - File-like object that opens a file lazily when it is first written - to. - """ - - def __init__(self, filename, mode="w"): - self.filename = filename - self.fileobj = None - self.lock = threading.Lock() - self.mode = mode - - def open(self): - if self.fileobj is None: - self.lock.acquire() - try: - if self.fileobj is None: - self.fileobj = open(self.filename, self.mode) - finally: - self.lock.release() - return self.fileobj - - def write(self, text): - fileobj = self.open() - fileobj.write(text) - fileobj.flush() - - def writelines(self, text): - fileobj = self.open() - fileobj.writelines(text) - fileobj.flush() - - def flush(self): - self.open().flush() - - -def live_pidfile(pidfile): - """(pidfile:str) -> int | None - Returns an int found in the named file, if there is one, - and if there is a running process with that process id. - Return None if no such process exists. - """ - pid = read_pidfile(pidfile) - if pid: - try: - os.kill(int(pid), 0) - return pid - except OSError as e: - if e.errno == errno.EPERM: - return pid - return None - - -def read_pidfile(filename): - if os.path.exists(filename): - try: - f = open(filename) - content = f.read() - f.close() - return int(content.strip()) - except (ValueError, OSError): - return None - else: - return None - - -def _remove_pid_file(written_pid, filename, verbosity): - current_pid = os.getpid() - if written_pid != current_pid: - # A forked process must be exiting, not the process that - # wrote the PID file - return - if not os.path.exists(filename): - return - f = open(filename) - content = f.read().strip() - f.close() - try: - pid_in_file = int(content) - except ValueError: - pass - else: - if pid_in_file != current_pid: - print("PID file {} contains {}, not expected PID {}".format(filename, pid_in_file, current_pid)) - return - if verbosity > 0: - print(f"Removing PID file {filename}") - try: - os.unlink(filename) - return - except OSError as e: - # Record, but don't give traceback - print(f"Cannot remove PID file: {e}") - # well, at least lets not leave the invalid PID around... - try: - f = open(filename, "w") - f.write("") - f.close() - except OSError as e: - print(f"Stale PID left in file: {filename} ({e:e})") - else: - print("Stale PID removed") - - -def ensure_port_cleanup(bound_addresses, maxtries=30, sleeptime=2): - """ - This makes sure any open ports are closed. - - Does this by connecting to them until they give connection - refused. Servers should call like:: - - import paste.script - ensure_port_cleanup([80, 443]) - """ - atexit.register(_cleanup_ports, bound_addresses, maxtries=maxtries, sleeptime=sleeptime) - - -def _cleanup_ports(bound_addresses, maxtries=30, sleeptime=2): - # Wait for the server to bind to the port. - for bound_address in bound_addresses: - for _i in range(maxtries): - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - try: - sock.connect(bound_address) - except OSError as e: - if e.errno != errno.ECONNREFUSED: - raise - break - else: - time.sleep(sleeptime) - else: - raise SystemExit("Timeout waiting for port.") - sock.close() - - -def _turn_sigterm_into_systemexit(): - """ - Attempts to turn a SIGTERM exception into a SystemExit exception. - """ - - def handle_term(signo, frame): - raise SystemExit - - signal.signal(signal.SIGTERM, handle_term) - - -# ---- from paste.script.command -------------------------------------- -python_version = sys.version.splitlines()[0].strip() - -parser = optparse.OptionParser( - add_help_option=False, - # version='%s from %s (python %s)' - # % (dist, dist.location, python_version), - usage="%prog [paster_options] COMMAND [command_options]", -) - -parser.add_option("-h", "--help", action="store_true", dest="do_help", help="Show this help message") -parser.disable_interspersed_args() - -# @@: Add an option to run this in another Python interpreter - -commands = {"serve": ServeCommand} - - -def run(args=None): - if ( - not args - and len(sys.argv) >= 2 - and os.environ.get("_") - and sys.argv[0] != os.environ["_"] - and os.environ["_"] == sys.argv[1] - ): - # probably it's an exe execution - args = ["exe", os.environ["_"]] + sys.argv[2:] - if args is None: - args = sys.argv[1:] - options, args = parser.parse_args(args) - options.base_parser = parser - if options.do_help: - args = ["help"] + args - if not args: - print(f"Usage: {sys.argv[0]} COMMAND") - args = ["help"] - command_name = args[0] - if command_name not in commands: - command = NotFoundCommand - else: - command = commands[command_name] - invoke(command, command_name, options, args[1:]) - - -def invoke(command, command_name, options, args): - try: - runner = command(command_name) - exit_code = runner.run(args) - except BadCommand as e: - print(e) - exit_code = e.exit_code - sys.exit(exit_code) diff --git a/lib/galaxy/util/path/__init__.py b/lib/galaxy/util/path/__init__.py index c53f028a472..a220871c92b 100644 --- a/lib/galaxy/util/path/__init__.py +++ b/lib/galaxy/util/path/__init__.py @@ -478,6 +478,7 @@ __all__ = ( "extensions", "get_ext", "has_ext", + "join", "joinext", "safe_contains", "safe_makedirs", diff --git a/lib/galaxy/util/resources.py b/lib/galaxy/util/resources.py new file mode 100644 index 00000000000..3a99b409f0a --- /dev/null +++ b/lib/galaxy/util/resources.py @@ -0,0 +1,22 @@ +"""Provide a consistent interface into and utilities for importlib file resources. +""" +try: + from importlib.resources import files # type: ignore[attr-defined] +except ImportError: + # Python < 3.9 + from importlib_resources import files # type: ignore[no-redef] + + +def resource_string(package_or_requirement, resource_name): + """ + Return specified resource as a string. + + Replacement function for pkg_resources.resource_string, but returns unicode string instead of bytestring. + """ + return files(package_or_requirement).joinpath(resource_name).read_text() + + +__all__ = ( + "files", + "resource_string", +) diff --git a/lib/galaxy/util/rules_dsl.py b/lib/galaxy/util/rules_dsl.py index 122a6841444..f58762a1637 100644 --- a/lib/galaxy/util/rules_dsl.py +++ b/lib/galaxy/util/rules_dsl.py @@ -7,11 +7,12 @@ from typing import ( ) import yaml -from pkg_resources import resource_stream + +from galaxy.util.resources import resource_string def get_rules_specification(): - return yaml.safe_load(resource_stream(__name__, "rules_dsl_spec.yml")) + return yaml.safe_load(resource_string(__package__, "rules_dsl_spec.yml")) def _ensure_rule_contains_keys(rule, keys): diff --git a/lib/galaxy/util/sockets.py b/lib/galaxy/util/sockets.py index 7b864ab9ecd..9ad57efc978 100644 --- a/lib/galaxy/util/sockets.py +++ b/lib/galaxy/util/sockets.py @@ -1,10 +1,15 @@ import random import socket +import sys from galaxy.util import commands def get_ip(): + if sys.platform == "darwin": + # If we're on OSX it is likely that the docker host is localhost. + return socket.gethostbyname(socket.gethostname()) + # This method assumes that the ip with default route is the ip we want to return s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) try: # doesn't even have to be reachable diff --git a/lib/galaxy/util/tool_shed/common_util.py b/lib/galaxy/util/tool_shed/common_util.py index 224b590c3b6..ca4da9fe248 100644 --- a/lib/galaxy/util/tool_shed/common_util.py +++ b/lib/galaxy/util/tool_shed/common_util.py @@ -428,3 +428,28 @@ def remove_protocol_and_user_from_clone_url(repository_clone_url): def remove_protocol_from_tool_shed_url(tool_shed_url): """Return a partial Tool Shed URL, eliminating the protocol if it exists.""" return util.remove_protocol_from_url(tool_shed_url) + + +__all__ = ( + "accumulate_tool_dependencies", + "check_for_missing_tools", + "check_tool_tag_set", + "generate_clone_url_for_installed_repository", + "generate_clone_url_for_repository_in_tool_shed", + "generate_clone_url_from_repo_info_tup", + "get_non_shed_tool_panel_configs", + "get_repository_dependencies", + "get_protocol_from_tool_shed_url", + "get_tool_dependencies", + "get_tool_shed_repository_ids", + "get_tool_shed_url_from_tool_shed_registry", + "get_tool_shed_repository_url", + "get_user_by_username", + "handle_galaxy_url", + "handle_tool_shed_url_protocol", + "parse_repository_dependency_tuple", + "remove_port_from_tool_shed_url", + "remove_protocol_and_port_from_tool_shed_url", + "remove_protocol_and_user_from_clone_url", + "remove_protocol_from_tool_shed_url", +) diff --git a/lib/galaxy/util/tool_shed/xml_util.py b/lib/galaxy/util/tool_shed/xml_util.py index b82d41df951..6781b4828a6 100644 --- a/lib/galaxy/util/tool_shed/xml_util.py +++ b/lib/galaxy/util/tool_shed/xml_util.py @@ -79,3 +79,10 @@ def parse_xml(file_name, check_exists=True): log.exception(error_message) return None, error_message return tree, error_message + + +__all__ = ( + "create_and_write_tmp_file", + "create_element", + "parse_xml", +) diff --git a/lib/galaxy/util/validation.py b/lib/galaxy/util/validation.py index c8ba7fef107..b14c4e2b207 100644 --- a/lib/galaxy/util/validation.py +++ b/lib/galaxy/util/validation.py @@ -9,7 +9,7 @@ from galaxy.util.sanitize_html import sanitize_html def validate_and_sanitize_basestring(key, val): if not isinstance(val, str): - raise exceptions.RequestParameterInvalidException("%s must be a string or unicode: %s" % (key, str(type(val)))) + raise exceptions.RequestParameterInvalidException(f"{key} must be a string or unicode: {type(val)}") return sanitize_html(val) @@ -18,12 +18,12 @@ def validate_and_sanitize_basestring_list(key, val): assert isinstance(val, list) return [sanitize_html(t) for t in val] except (AssertionError, TypeError): - raise exceptions.RequestParameterInvalidException("%s must be a list of strings: %s" % (key, str(type(val)))) + raise exceptions.RequestParameterInvalidException(f"{key} must be a list of strings: {type(val)}") def validate_boolean(key, val): if not isinstance(val, bool): - raise exceptions.RequestParameterInvalidException("%s must be a boolean: %s" % (key, str(type(val)))) + raise exceptions.RequestParameterInvalidException(f"{key} must be a boolean: {type(val)}") return val diff --git a/lib/galaxy/util/web_compat.py b/lib/galaxy/util/web_compat.py deleted file mode 100644 index 8d99a8792d8..00000000000 --- a/lib/galaxy/util/web_compat.py +++ /dev/null @@ -1,11 +0,0 @@ -"""Work around for gross circular dependency between galaxy.util and galaxy.web_stack. - -Provide a function that will delay to forking in a uwsgi environment but run immediately -otherwise. -""" -try: - from galaxy.web_stack import register_postfork_function -except ImportError: - - def register_postfork_function(f, *args, **kwargs): - f(*args, **kwargs) diff --git a/lib/galaxy/util/xml_macros.py b/lib/galaxy/util/xml_macros.py index bf608a2e0e2..392b30a3958 100644 --- a/lib/galaxy/util/xml_macros.py +++ b/lib/galaxy/util/xml_macros.py @@ -1,10 +1,14 @@ import os -from copy import ( - copy, - deepcopy, +from copy import deepcopy +from typing import ( + Dict, + List, ) -from galaxy.util import parse_xml +from galaxy.util import ( + Element, + parse_xml, +) REQUIRED_PARAMETER = object() @@ -18,36 +22,29 @@ def load_with_references(path): tree = raw_xml_tree(path) root = tree.getroot() - macro_paths = _import_macros(root, path) - - # temporarily remove the children of the macros node - # and create a copy. this is done because this allows - # to iterate over all expand nodes of the tree - # that are not included in the macros node macros_el = _macros_el(root) - if macros_el is not None: - macros_copy = copy(macros_el) - macros_el.clear() - else: - macros_copy = None + if macros_el is None: + return tree, [] + + macros: Dict[str, List[Element]] = {} + macro_paths = _import_macros(macros_el, path, macros) + macros_el.clear() # Collect tokens - tokens = _macros_of_type(macros_copy, "token", lambda el: el.text or "") + tokens = {} + for m in macros.get("token", []): + tokens[m.get("name")] = m.text or "" tokens = expand_nested_tokens(tokens) # Expand xml macros - macro_dict = _macros_of_type(macros_copy, "xml", lambda el: XmlMacroDef(el)) + macro_dict = {} + for m in macros.get("xml", []): + macro_dict[m.get("name")] = XmlMacroDef(m) _expand_macros([root], macro_dict, tokens) - # readd the stashed children of the macros node - # TODO is this this really necesary? Since macro nodes are removed anyway just below. - if macros_copy is not None: - _xml_set_children(macros_el, list(macros_copy)) - - for el in root.xpath("//macro"): - if el.get("type") != "template": - # Only keep template macros - el.getparent().remove(el) + # reinsert template macro which are used during tool execution + for m in macros.get("template", []): + macros_el.append(m) _expand_tokens_for_el(root, tokens) return tree, macro_paths @@ -83,16 +80,15 @@ def imported_macro_paths(root): return _imported_macro_paths_from_el(macros_el) -def _import_macros(root, path): +def _import_macros(macros_el, path, macros): """ root the parsed XML tree path the path to the main xml document """ xml_base_dir = os.path.dirname(path) - macros_el = _macros_el(root) if macros_el is not None: - macro_els, macro_paths = _load_macros(macros_el, xml_base_dir) - _xml_set_children(macros_el, macro_els) + macro_paths = _load_macros(macros_el, xml_base_dir, macros) + # _xml_set_children(macros_el, macro_els) return macro_paths @@ -160,22 +156,22 @@ def _expand_macros(elements, macros, tokens, visited=None): if not macros and not tokens: return + if visited is None: + v = [] + else: + v = visited + for element in elements: while True: expand_el = element.find(".//expand") if expand_el is None: break - if visited is None: - v = [] - else: - v = visited _expand_macro(expand_el, macros, tokens, v) def _expand_macro(expand_el, macros, tokens, visited): macro_name = expand_el.get("macro") assert macro_name is not None, "Attempted to expand macro with no 'macro' attribute defined." - # check for cycles in the nested macro expansion assert ( macro_name not in visited @@ -222,54 +218,47 @@ def _expand_yield_statements(macro_def, expand_el): _xml_replace(yield_el, expand_el_children) -def _load_macros(macros_el, xml_base_dir): - macros = [] +def _load_macros(macros_el, xml_base_dir, macros): # Import macros from external files. - imported_macros, macro_paths = _load_imported_macros(macros_el, xml_base_dir) - macros.extend(imported_macros) + macro_paths = _load_imported_macros(macros_el, xml_base_dir, macros) # Load all directly defined macros. - macros.extend(_load_embedded_macros(macros_el, xml_base_dir)) - return macros, macro_paths + _load_embedded_macros(macros_el, xml_base_dir, macros) + return macro_paths -def _load_embedded_macros(macros_el, xml_base_dir): - macros = [] - - macro_els = [] +def _load_embedded_macros(macros_el, xml_base_dir, macros): + if macros_el is None: + return # attribute typed macro - if macros_el is not None: - macro_els = macros_el.findall("macro") - for macro in macro_els: + for macro in macros_el.iterfind("macro"): if "type" not in macro.attrib: macro.attrib["type"] = "xml" - macros.append(macro) + try: + macros[macro.attrib["type"]].append(macro) + except KeyError: + macros[macro.attrib["type"]] = [macro] # type shortcuts ( is a shortcut for . - typed_tag = ["template", "xml", "token"] - for tag in typed_tag: - macro_els = [] - if macros_el is not None: - macro_els = macros_el.findall(tag) - for macro_el in macro_els: + for tag in ["template", "xml", "token"]: + for macro_el in macros_el.iterfind(tag): macro_el.attrib["type"] = tag macro_el.tag = "macro" - macros.append(macro_el) - - return macros + try: + macros[tag].append(macro_el) + except KeyError: + macros[tag] = [macro_el] -def _load_imported_macros(macros_el, xml_base_dir): - macros = [] +def _load_imported_macros(macros_el, xml_base_dir, macros): macro_paths = [] for tool_relative_import_path in _imported_macro_paths_from_el(macros_el): import_path = os.path.join(xml_base_dir, tool_relative_import_path) macro_paths.append(import_path) - file_macros, current_macro_paths = _load_macro_file(import_path, xml_base_dir) - macros.extend(file_macros) + current_macro_paths = _load_macro_file(import_path, xml_base_dir, macros) macro_paths.extend(current_macro_paths) - return macros, macro_paths + return macro_paths def _imported_macro_paths_from_el(macros_el): @@ -283,19 +272,10 @@ def _imported_macro_paths_from_el(macros_el): return imported_macro_paths -def _load_macro_file(path, xml_base_dir): +def _load_macro_file(path, xml_base_dir, macros): tree = parse_xml(path, strip_whitespace=False) - for node in tree.iter(): - # little hack: node.base contains that path to the file which is used in - # the linter. it is a property that apparently is determined from the - # xmltree, so if the macro is inserted into the main xml node.base will - # give the path of the main xml file. - # luckily lxml allows to set the property by adding xml:base to the node - # which will be returned if the property is read - # https://github.com/lxml/lxml/blob/5a5c7fb01d15af58def4bab2ba7b15c937042835/src/lxml/etree.pyx#L1106 - node.base = node.base root = tree.getroot() - return _load_macros(root, xml_base_dir) + return _load_macros(root, xml_base_dir, macros) def _xml_set_children(element, new_children): diff --git a/lib/galaxy/visualization/plugins/interactive_environments.py b/lib/galaxy/visualization/plugins/interactive_environments.py index db11ead565b..673702a663d 100644 --- a/lib/galaxy/visualization/plugins/interactive_environments.py +++ b/lib/galaxy/visualization/plugins/interactive_environments.py @@ -217,8 +217,6 @@ class InteractiveEnvironmentRequest: conf_file["galaxy_url"] = f"{self.attr.galaxy_config.galaxy_infrastructure_url.rstrip('/')}/" else: conf_file["galaxy_url"] = f"{request.application_url.rstrip('/')}/" - # Galaxy paster port is deprecated - conf_file["galaxy_paster_port"] = conf_file["galaxy_web_port"] return conf_file diff --git a/lib/galaxy/web/buildapp.py b/lib/galaxy/web/buildapp.py deleted file mode 100644 index ef953ccd1ba..00000000000 --- a/lib/galaxy/web/buildapp.py +++ /dev/null @@ -1,10 +0,0 @@ -"""For backward compatibility only, pulls app_factor from galaxy.webapps.main""" - -try: - from galaxy.webapps.galaxy.buildapp import app_factory -except ImportError: - # when installed as a library, galaxy-web-apps may not be available - this - # is fine - we shouldn't be using this entry point anyway. - app_factory = None # type: ignore[misc, assignment] - -__all__ = ("app_factory",) diff --git a/lib/galaxy/web/formatting.py b/lib/galaxy/web/formatting.py deleted file mode 100644 index 1233fb8b9e6..00000000000 --- a/lib/galaxy/web/formatting.py +++ /dev/null @@ -1,32 +0,0 @@ -import locale -from string import Template - -DEFAULT_LOCALE_FORMAT = "%a %b %e %H:%M:%S %Y" -ISO_DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S" - - -def expand_pretty_datetime_format(value): - """ - - >>> expand_pretty_datetime_format("%H:%M:%S %Z") - '%H:%M:%S %Z' - >>> locale_format = expand_pretty_datetime_format("$locale (UTC)") - >>> import locale - >>> expected_format = '%s (UTC)' % locale.nl_langinfo(locale.D_T_FMT) - >>> locale_format == expected_format - True - >>> expand_pretty_datetime_format("$iso8601") - '%Y-%m-%d %H:%M:%S' - """ - locale_format = None - try: - locale_format = locale.nl_langinfo(locale.D_T_FMT) - except AttributeError: # nl_langinfo not available - pass - if not locale_format: - locale_format = DEFAULT_LOCALE_FORMAT - stock_formats = dict( - locale=locale_format, - iso8601=ISO_DATETIME_FORMAT, - ) - return Template(value).safe_substitute(**stock_formats) diff --git a/lib/galaxy/web/framework/base.py b/lib/galaxy/web/framework/base.py index b8020ebf9d8..fb2a5d69b13 100644 --- a/lib/galaxy/web/framework/base.py +++ b/lib/galaxy/web/framework/base.py @@ -19,7 +19,7 @@ import routes import webob.compat import webob.cookies import webob.exc -import webob.exc as httpexceptions # noqa: F401 +import webob.exc as httpexceptions # We will use some very basic HTTP/wsgi utilities from the paste library from paste.response import HeaderDict @@ -560,3 +560,13 @@ def walk_controller_modules(package_name): module_name = f"{package_name}.{name}" module = import_module(module_name) yield name, module + + +__all__ = ( + "DefaultWebTransaction", + "httpexceptions", + "lazy_property", + "routes", + "walk_controller_modules", + "WebApplication", +) diff --git a/lib/galaxy/web/framework/middleware/error.py b/lib/galaxy/web/framework/middleware/error.py index cb6ccfb8d99..ba4b47ad83e 100644 --- a/lib/galaxy/web/framework/middleware/error.py +++ b/lib/galaxy/web/framework/middleware/error.py @@ -409,7 +409,7 @@ def handle_exception( else: reported = True else: - error_stream.write("Error - {}: {}\n".format(exc_data.exception_type, exc_data.exception_value)) + error_stream.write(f"Error - {exc_data.exception_type}: {exc_data.exception_value}\n") if html: if debug_mode and simple_html_error: return_error = formatter.format_html( diff --git a/lib/galaxy/web_stack/__init__.py b/lib/galaxy/web_stack/__init__.py index f26d01d805b..9df08d21ae7 100644 --- a/lib/galaxy/web_stack/__init__.py +++ b/lib/galaxy/web_stack/__init__.py @@ -1,65 +1,32 @@ """Web application stack operations.""" -import inspect -import json import logging import multiprocessing import os +import sys +import threading from typing import ( Callable, - Dict, FrozenSet, List, Optional, - Tuple, Type, ) -from urllib.request import install_opener -# The uwsgi module is automatically injected by the parent uwsgi process and only exists that way. If anything works, -# this is a uwsgi-managed process. -try: - import uwsgi -except ImportError: - uwsgi = None - -import yaml - -from galaxy.util import unicodify from galaxy.util.facts import get_facts -from galaxy.util.path import has_ext -from galaxy.util.properties import nice_config_parser from .handlers import HANDLER_ASSIGNMENT_METHODS -from .message import ( - ApplicationStackMessage, - ApplicationStackMessageDispatcher, -) -from .transport import ( - ApplicationStackTransport, - UWSGIFarmMessageTransport, -) log = logging.getLogger(__name__) class ApplicationStackLogFilter(logging.Filter): def filter(self, record): - record.worker_id = None - record.mule_id = None - return True - - -class UWSGILogFilter(logging.Filter): - def filter(self, record): - record.worker_id = uwsgi.worker_id() - record.mule_id = uwsgi.mule_id() return True class ApplicationStack: name: Optional[str] = None prohibited_middleware: FrozenSet[str] = frozenset() - transport_class = ApplicationStackTransport log_filter_class: Type[logging.Filter] = ApplicationStackLogFilter log_format = "%(name)s %(levelname)s %(asctime)s [pN:%(processName)s,p:%(process)d,tN:%(threadName)s] %(message)s" # TODO: this belongs in the pool configuration @@ -213,376 +180,57 @@ class ApplicationStack: def set_postfork_server_name(self, app): new_server_name = self.server_name_template.format(**self.facts) + if "GUNICORN_WORKER_ID" in os.environ: + new_server_name = f"{new_server_name}.{os.environ['GUNICORN_WORKER_ID']}" multiprocessing.current_process().name = app.config.server_name = new_server_name log.debug("server_name set to: %s", new_server_name) - def register_message_handler(self, func, name=None): - pass - - def deregister_message_handler(self, func=None, name=None): - pass - - def send_message(self, dest, msg=None, target=None, params=None, **kwargs): - pass - def shutdown(self): pass -class MessageApplicationStack(ApplicationStack): - def __init__(self, app=None, config=None): - super().__init__(app=app, config=config) - self.use_messaging = False - self.dispatcher = ApplicationStackMessageDispatcher() - self.transport = self.transport_class(app, stack=self, dispatcher=self.dispatcher) - - def init_late_prefork(self): - self.transport.init_late_prefork() - - def start(self): - super().start() - if self.use_messaging and not self.running: - self.transport.start() - self.running = True - - def register_message_handler(self, func, name=None): - self.dispatcher.register_func(func, name) - self.transport.start_if_needed() - - def deregister_message_handler(self, func=None, name=None): - self.dispatcher.deregister_func(func, name) - self.transport.stop_if_unneeded() - - def send_message(self, dest, msg=None, target=None, params=None, **kwargs): - assert msg is not None or target is not None, "Either 'msg' or 'target' parameters must be set" - if not msg: - msg = ApplicationStackMessage(target=target, params=params, **kwargs) - self.transport.send_message(msg.encode(), dest) - - def shutdown(self): - if self.use_messaging and self.running: - log.info("Application stack interface shutting down") - self.transport.shutdown() - self.running = False +class WebApplicationStack(ApplicationStack): + name = "Web" -class UWSGIApplicationStack(MessageApplicationStack): - """Interface to the uWSGI application stack. Supports running additional webless Galaxy workers as mules. Mules - must be farmed to be communicable via uWSGI mule messaging, unfarmed mules are not supported. - - Note that mules will use this as their stack class even though they start with the "webless" loading point. - """ - - name = "uWSGI" - prohibited_middleware = frozenset( - { - "wrap_in_static", - "EvalException", - } - ) - transport_class = UWSGIFarmMessageTransport - log_filter_class = UWSGILogFilter - log_format = "%(name)s %(levelname)s %(asctime)s [pN:%(processName)s,p:%(process)d,w:%(worker_id)s,m:%(mule_id)s,tN:%(threadName)s] %(message)s" - server_name_template = "{server_name}.{pool_name}.{instance_id}" - - postfork_functions: List[Tuple[Callable, List, Dict]] = [] - - localhost_addrs = ("127.0.0.1", "[::1]") - bind_all_addrs = ("", "0.0.0.0", "[::]") - - @staticmethod - def _get_config_file(confs, loader, section): - """uWSGI allows config merging, in which case the corresponding config file option will be a list.""" - conf = None - if isinstance(confs, list): - gconfs = [_ for _ in confs if os.path.exists(_) and section in loader(open(_))] - if len(gconfs) == 1: - conf = gconfs[0] - elif len(gconfs) == 0: - log.warning("Could not locate a config file containing a Galaxy config from: %s", ", ".join(confs)) - else: - log.warning( - "Multiple config files contain Galaxy configs, merging is not supported: %s", ", ".join(gconfs) - ) - else: - conf = confs - return conf - - @staticmethod - def _socket_opt_to_str(opt, val): - try: - opt = unicodify(opt) - val = unicodify(val) - if val.startswith("="): - val = unicodify(uwsgi.opt.get("shared-socket", [])[int(val.split("=")[1])]) - proto = opt if opt != "socket" else "uwsgi" - if proto == "uwsgi" and ":" not in val: - return f"uwsgi://{val}" - else: - proto = f"{proto}://" - host, port = val.rsplit(":", 1) - port = f":{port.split(',', 1)[0]}" - if host in UWSGIApplicationStack.bind_all_addrs: - host = UWSGIApplicationStack.localhost_addrs[0] - return proto + host + port - except (IndexError, AttributeError): - return f"{opt} {val}" - - @staticmethod - def _socket_opts(): - for opt in ("https", "http", "socket"): - if opt in uwsgi.opt: - val = uwsgi.opt[opt] - if isinstance(val, list): - for v in val: - yield (opt, v) - else: - yield (opt, val) - - @staticmethod - def _serving_on(): - for opt, val in UWSGIApplicationStack._socket_opts(): - yield UWSGIApplicationStack._socket_opt_to_str(opt, val) - - @classmethod - def get_app_kwds(cls, config_section, app_name=None): - kwds = { - "config_file": None, - "config_section": config_section, - } - uwsgi_opt = uwsgi.opt - config_file = None - # check for --set galaxy_config_file=, this overrides whatever config file uWSGI was loaded with (which - # may not actually include a Galaxy config) - if uwsgi_opt.get("galaxy_config_file"): - config_file = uwsgi_opt.get("galaxy_config_file") - # check for --yaml or --json uWSGI config options next - if config_file is None: - config_file = UWSGIApplicationStack._get_config_file( - uwsgi_opt.get("yaml"), yaml.safe_load, config_section - ) or UWSGIApplicationStack._get_config_file(uwsgi_opt.get("json"), json.load, config_section) - # --ini and --ini-paste don't behave the same way, but this method will only be called by mules if the main - # application was loaded with --ini-paste, so we can make some assumptions, most notably, uWSGI does not have - # any way to set the app name when loading with paste.deploy:loadapp(), so hardcoding the alternate section - # name to `app:main` is fine. - has_ini_config = config_file is None and uwsgi_opt.get("ini") or uwsgi_opt.get("ini-paste") - has_ini_config = has_ini_config or (config_file and has_ext(config_file, "ini", aliases=True, ignore="sample")) - if has_ini_config: - config_file = config_file or uwsgi_opt.get("ini") or uwsgi_opt.get("ini-paste") - parser = nice_config_parser(config_file) - if not parser.has_section(config_section) and parser.has_section("app:main"): - kwds["config_section"] = "app:main" - kwds["config_file"] = unicodify(config_file) - return kwds +class GunicornApplicationStack(ApplicationStack): + name = "Gunicorn" + do_post_fork = "--preload" in os.environ.get("GUNICORN_CMD_ARGS", "") or "--preload" in sys.argv + postfork_functions: List[Callable] = [] + # Will be set to True by external hook + late_postfork_event = threading.Event() @classmethod def register_postfork_function(cls, f, *args, **kwargs): - if uwsgi.mule_id() == 0: - cls.postfork_functions.append((f, args, kwargs)) + # do_post_fork determines if we need to run postfork functions + if cls.do_post_fork: + # if so, we call ApplicationStack.late_postfork once after forking ... + if not cls.postfork_functions: + os.register_at_fork(after_in_child=cls.late_postfork) + # ... and store everything we need to run in ApplicationStack.postfork_functions + cls.postfork_functions.append(lambda: f(*args, **kwargs)) else: - # mules are forked from the master and run the master's postfork functions immediately before the forked - # process is replaced. that is prevented in the _do_uwsgi_postfork function, and because programmed mules - # are standalone non-forking processes, they should run postfork functions immediately f(*args, **kwargs) - def __init__(self, app=None, config=None): - self._farms_dict = None - self._mules_list = None - self._farm_server_names_dict = None + @classmethod + def run_postfork(cls): + cls.late_postfork_event.wait(1) + for f in cls.postfork_functions: + f() - # If there's more than one farm *and* more than one farm will be using farm messaging, additional locks need to - # be configured by the admin. This allows us to keep track of how many such farms are configured. - self._lock_farms = set() - - super().__init__(app=app, config=config) - - def _set_default_job_handler_assignment_methods(self, job_config, base_pool): - # Disable DB_SELF if a valid farm (pool) is configured. Use mule messaging unless the job_config doesn't allow - # it (e.g. workflow scheduling manager), in which case, use DB-SKIP-LOCKED or DB-TRANSACTION-ISOLATION. - # - # TODO MULTIPOOL: if there is no default in any base_pool (and no job_config.default_handler_id) then don't - # remove DB_SELF - conf_class_name = job_config.__class__.__name__ - remove_methods = [HANDLER_ASSIGNMENT_METHODS.DB_SELF] - if HANDLER_ASSIGNMENT_METHODS.UWSGI_MULE_MESSAGE not in job_config.UNSUPPORTED_HANDLER_ASSIGNMENT_METHODS: - add_method = HANDLER_ASSIGNMENT_METHODS.UWSGI_MULE_MESSAGE - else: - add_method = self.get_preferred_handler_assignment_method() - remove_methods.append(HANDLER_ASSIGNMENT_METHODS.UWSGI_MULE_MESSAGE) - log.debug( - "%s: No job handler assignment methods were configured but a uWSGI farm named '%s' exists," - " automatically enabling the '%s' assignment method", - conf_class_name, - base_pool, - add_method, - ) - for m in remove_methods: - try: - job_config.handler_assignment_methods.remove(m) - log.debug("%s: Removed '%s' from handler assignment methods due to use of mules", conf_class_name, m) - except ValueError: - pass - if add_method not in job_config.handler_assignment_methods: - job_config.handler_assignment_methods.insert(0, add_method) - log.debug( - "%s: handler assignment methods updated to: %s", - conf_class_name, - ", ".join(job_config.handler_assignment_methods), - ) - - def _init_job_handler_assignment_methods(self, job_config, base_pool): - super()._init_job_handler_assignment_methods(job_config, base_pool) - # Determine if stack messaging should be enabled - if HANDLER_ASSIGNMENT_METHODS.UWSGI_MULE_MESSAGE in job_config.handler_assignment_methods: - self.use_messaging = True - - def _init_job_handler_subpools(self, job_config, base_pool): - super()._init_job_handler_subpools(job_config, base_pool) - # Count the required number of uWSGI locks - if job_config.use_messaging: - for pool_name in self.configured_pools: - if pool_name == base_pool or pool_name.startswith(f"{base_pool}."): - self._lock_farms.add(pool_name) - - @property - def _configured_mules(self): - if self._mules_list is None: - self._mules_list = _uwsgi_configured_mules() - return self._mules_list - - @property - def _is_mule(self): - return uwsgi.mule_id() > 0 - - @property - def configured_pools(self): - if self._farms_dict is None: - self._farms_dict = {} - farms = uwsgi.opt.get("farm", []) - farms = farms if isinstance(farms, list) else [farms] - for farm in farms: - farm = unicodify(farm) - name, mules = farm.split(":", 1) - self._farms_dict[name] = [int(m) for m in mules.split(",")] - return self._farms_dict - - @property - def _farms(self): - farms = [] - for farm, mules in self.configured_pools.items(): - if uwsgi.mule_id() in mules: - farms.append(farm) - return farms - - def _mule_index_in_farm(self, farm_name, mule_id=None): - mule_id = mule_id or uwsgi.mule_id() - try: - mules = self.configured_pools[farm_name] - return mules.index(mule_id) - except (KeyError, ValueError): - return -1 - - @property - def _farm_name(self): - # TODO: to allow mules to be in multiple farms you'll need to start here - try: - return self._farms[0] - except IndexError: - return None - - @property - def _farm_server_names(self): - if self._farm_server_names_dict is None: - self._farm_server_names_dict = {} - for farm_name, mules in self.configured_pools.items(): - server_names = [] - facts = self.facts - for mule in mules: - facts.update( - { - "pool_name": farm_name, - "server_id": mule, - "instance_id": self._mule_index_in_farm(farm_name, mule) + 1, - } - ) - server_names.append(self.server_name_template.format(**facts)) - self._farm_server_names_dict[farm_name] = server_names - return self._farm_server_names_dict - - @property - def instance_id(self): - if not self._is_mule: - instance_id = uwsgi.worker_id() - elif self._farm_name: - return self._mule_index_in_farm(self._farm_name) + 1 - else: - instance_id = uwsgi.mule_id() - return instance_id + @classmethod + def late_postfork(cls): + # We can't run postfork functions immediately, because this is before the gunicorn `post_fork` hook runs, + # and we depend on the `post_fork` hook to set a worker id. + t = threading.Thread(target=cls.run_postfork) + t.start() def log_startup(self): msg = [f"Galaxy server instance '{self.config.server_name}' is running"] - # Log the next messages when the first worker finishes starting. This - # may not be the first to finish (so Galaxy could be serving already), - # but it's a good approximation and gives the correct root_pid below - # when there is no master process. - if not self._is_mule and self.instance_id == 1: - # We use the same text printed by Paste to not break scripts - # grepping for this line. Here root_pid is the same that gets - # written to file when using the --pidfile option of uwsgi - root_pid = uwsgi.masterpid() or os.getpid() - msg.append("Starting server in PID %d." % root_pid) - for s in UWSGIApplicationStack._serving_on(): - msg.append(f"serving on {s}") - if len(msg) == 1: - msg.append("serving on unknown URL") + if "GUNICORN_LISTENERS" in os.environ: + msg.append(f'serving on {os.environ["GUNICORN_LISTENERS"]}') log.info("\n".join(msg)) - def start(self): - # Does a generalized `is_worker` attribute make sense? Hard to say w/o other stack paradigms. - if self._is_mule and self._farm_name: - # used by main.py to send a shutdown message on termination - os.environ["_GALAXY_UWSGI_FARM_NAMES"] = ",".join(self._farms) - super().start() - - def in_pool(self, pool_name): - if not self._is_mule: - return False - else: - return pool_name in self._farms - - def pool_members(self, pool_name): - return self._farm_server_names.get(pool_name, None) - - def workers(self): - return uwsgi.workers() - - @property - def facts(self): - facts = super().facts - if not self._is_mule: - facts.update( - { - "pool_name": "web", - "server_id": uwsgi.worker_id(), - } - ) - else: - facts.update( - { - "pool_name": self._farm_name, - "server_id": uwsgi.mule_id(), - } - ) - facts["instance_id"] = self.instance_id - return facts - - def shutdown(self): - super().shutdown() - - -class PasteApplicationStack(ApplicationStack): - name = "Python Paste" - class WeblessApplicationStack(ApplicationStack): name = "Webless" @@ -641,13 +289,10 @@ def application_stack_class() -> Type[ApplicationStack]: """Returns the correct ApplicationStack class for the stack under which this Galaxy process is running. """ - if uwsgi is not None and hasattr(uwsgi, "numproc"): - return UWSGIApplicationStack - else: - # cleverer ideas welcome - for frame in inspect.stack(): - if frame[1].endswith(os.path.join("pastescript", "loadwsgi.py")): - return PasteApplicationStack + if "gunicorn" in os.environ.get("SERVER_SOFTWARE", ""): + return GunicornApplicationStack + elif os.environ.get("IS_WEBAPP") == "1": + return WebApplicationStack return WeblessApplicationStack @@ -670,31 +315,3 @@ def register_postfork_function(f, *args, **kwargs): def get_app_kwds(config_section, app_name=None): return application_stack_class().get_app_kwds(config_section, app_name=app_name) - - -def get_stack_facts(config=None): - return application_stack_instance(config=config).facts - - -def _uwsgi_configured_mules(): - mules = uwsgi.opt.get("mule", []) - return [mules] if isinstance(mules, str) or mules is True else mules - - -def _do_uwsgi_postfork(): - for i, mule in enumerate(_uwsgi_configured_mules()): - if mule is not True and i + 1 == uwsgi.mule_id(): - # mules will inherit the postfork function list and call them immediately upon fork, but programmed mules - # should not do that (they will call the postfork functions in-place as they start up after exec()) - UWSGIApplicationStack.postfork_functions = [(_mule_fixup, (), {})] - for f, args, kwargs in [t for t in UWSGIApplicationStack.postfork_functions]: - log.debug("Calling postfork function: %s", f) - f(*args, **kwargs) - - -def _mule_fixup(): - install_opener(None) - - -if uwsgi: - uwsgi.post_fork_hook = _do_uwsgi_postfork diff --git a/lib/galaxy/web_stack/gunicorn_config.py b/lib/galaxy/web_stack/gunicorn_config.py new file mode 100644 index 00000000000..6f4249a8acd --- /dev/null +++ b/lib/galaxy/web_stack/gunicorn_config.py @@ -0,0 +1,67 @@ +""" +Gunicorn config file based on https://gist.github.com/hynek/ba655c8756924a5febc5285c712a7946 +""" +import logging +import os +import sys + +log = logging.getLogger(__name__) + + +def on_starting(server): + """ + Attach a set of IDs that can be temporarily re-used. + + Used on reloads when each worker exists twice. + """ + server._worker_id_overload = set() + + +def nworkers_changed(server, new_value, old_value): + """ + Gets called on startup too. + + Set the current number of workers. Required if we raise the worker count + temporarily using TTIN because server.cfg.workers won't be updated and if + one of those workers dies, we wouldn't know the ids go that far. + """ + server._worker_id_current_workers = new_value + + +def _next_worker_id(server): + """ + If there are IDs open for re-use, take one. Else look for a free one. + """ + if server._worker_id_overload: + return server._worker_id_overload.pop() + + in_use = set(w._worker_id for w in tuple(server.WORKERS.values()) if w.alive) + free = set(range(1, server._worker_id_current_workers + 1)) - in_use + + return free.pop() + + +def on_reload(server): + """ + Add a full set of ids into overload so it can be re-used once. + """ + server._worker_id_overload = set(range(1, server.cfg.workers + 1)) + + +def pre_fork(server, worker): + """ + Attach the next free worker_id before forking off. + """ + worker._worker_id = _next_worker_id(server) + + +def post_fork(server, worker): + """ + Put worker_id and listeners into an env variable for further use within the app. + """ + os.environ["GUNICORN_WORKER_ID"] = str(worker._worker_id) + os.environ["GUNICORN_LISTENERS"] = ",".join(str(bind) for bind in server.LISTENERS) + if "--preload" in os.environ.get("GUNICORN_CMD_ARGS", "") or "--preload" in sys.argv: + from galaxy.web_stack import GunicornApplicationStack + + GunicornApplicationStack.late_postfork_event.set() diff --git a/lib/galaxy/web_stack/handlers.py b/lib/galaxy/web_stack/handlers.py index 1487800f85f..14c6095cd28 100644 --- a/lib/galaxy/web_stack/handlers.py +++ b/lib/galaxy/web_stack/handlers.py @@ -8,10 +8,7 @@ import logging import os import random from enum import Enum -from typing import ( - Set, - Tuple, -) +from typing import Tuple from sqlalchemy.orm import object_session @@ -30,7 +27,6 @@ class HANDLER_ASSIGNMENT_METHODS(str, Enum): DB_PREASSIGN = "db-preassign" DB_TRANSACTION_ISOLATION = "db-transaction-isolation" DB_SKIP_LOCKED = "db-skip-locked" - UWSGI_MULE_MESSAGE = "uwsgi-mule-message" @classmethod def has_value(cls, value): @@ -44,7 +40,6 @@ class HandlerAssignmentSkip(Exception): class ConfiguresHandlers: DEFAULT_HANDLER_TAG = "_default_" DEFAULT_BASE_HANDLER_POOLS: Tuple[str, ...] = () - UNSUPPORTED_HANDLER_ASSIGNMENT_METHODS: Set[str] = set() def add_handler(self, handler_id, tags): if handler_id not in self.handlers: @@ -121,7 +116,6 @@ class ConfiguresHandlers: HANDLER_ASSIGNMENT_METHODS.DB_PREASSIGN: self._assign_db_preassign_handler, HANDLER_ASSIGNMENT_METHODS.DB_TRANSACTION_ISOLATION: self._assign_db_tag, HANDLER_ASSIGNMENT_METHODS.DB_SKIP_LOCKED: self._assign_db_tag, - HANDLER_ASSIGNMENT_METHODS.UWSGI_MULE_MESSAGE: self._assign_uwsgi_mule_message_handler, } if handling_config_dict: for method in handling_config_dict.get("assign", []): @@ -144,10 +138,7 @@ class ConfiguresHandlers: def _set_default_handler_assignment_methods(self): if not self.handler_assignment_methods_configured: - if ( - not self.app.config.track_jobs_in_database - and HANDLER_ASSIGNMENT_METHODS.MEM_SELF not in self.UNSUPPORTED_HANDLER_ASSIGNMENT_METHODS - ): + if not self.app.config.track_jobs_in_database: # DEPRECATED: You should just set mem_self as the only method if you want this log.warning( "The `track_jobs_in_database` option is deprecated, please set `%s` as the job" @@ -238,22 +229,10 @@ class ConfiguresHandlers: rval.append(elem) return rval - @property - def use_messaging(self): - # The job manager uses this to determine whether the messaging job queue should be used - return HANDLER_ASSIGNMENT_METHODS.UWSGI_MULE_MESSAGE in self.handler_assignment_methods - @property def deterministic_handler_assignment(self): return self.handler_assignment_methods and any( - filter( - lambda x: x - in ( - HANDLER_ASSIGNMENT_METHODS.UWSGI_MULE_MESSAGE, - HANDLER_ASSIGNMENT_METHODS.DB_PREASSIGN, - ), - self.handler_assignment_methods, - ) + filter(lambda x: x == HANDLER_ASSIGNMENT_METHODS.DB_PREASSIGN), self.handler_assignment_methods ) def _get_is_handler(self): @@ -434,36 +413,6 @@ class ConfiguresHandlers: _timed_flush_obj(obj) return handler - def _assign_uwsgi_mule_message_handler(self, obj, method, configured, message_callback=None, flush=True, **kwargs): - """Assign object to a handler by sending a setup message to the appropriate handler pool (farm), where a handler - (mule) will receive the message and assign itself. - - :param obj: Same as :method:`ConfiguresHandlers.assign_handler()`. - :param method: Same as :method:`ConfiguresHandlers._assign_db_preassign_handler()`. - :param configured: Same as :method:`ConfiguresHandlers.assign_handler()`. - :param queue_callback: Callback returning a setup message to be sent via the stack messaging interface's - ``send_message()`` method. No arguments are passed. - :type queue_callback: callable - - :raises HandlerAssignmentSkip: if the configured or default handler is not a known handler pool (farm) - :returns: str -- The assigned handler pool. - """ - assert message_callback is not None, ( - "Cannot perform '%s' handler assignment: `message_callback` is None" - % HANDLER_ASSIGNMENT_METHODS.UWSGI_MULE_MESSAGE - ) - tag = configured or self.DEFAULT_HANDLER_TAG - pool = self.pool_for_tag.get(tag) - if pool is None: - log.debug("(%s) No handler pool (uWSGI farm) for '%s' found", obj.log_str(), tag) - raise HandlerAssignmentSkip() - else: - if flush or not obj.id: - _timed_flush_obj(obj) - message = message_callback() - self.app.application_stack.send_message(pool, message) - return pool - def assign_handler(self, obj, configured=None, **kwargs): """Set a job handler, flush obj diff --git a/lib/galaxy/web_stack/transport.py b/lib/galaxy/web_stack/transport.py index 2420cc94b84..b5836fcefc2 100644 --- a/lib/galaxy/web_stack/transport.py +++ b/lib/galaxy/web_stack/transport.py @@ -3,15 +3,6 @@ import logging import threading -from typing import List - -from galaxy.util import unicodify - -try: - import uwsgi -except ImportError: - uwsgi = None - log = logging.getLogger(__name__) @@ -77,106 +68,3 @@ class ApplicationStackTransport: log.info("Joining application stack transport dispatcher thread") self.dispatcher_thread.join() self.dispatcher_thread = None - - -class UWSGIFarmMessageTransport(ApplicationStackTransport): - """Communication via uWSGI Mule Farm messages. Communication is unidirectional (workers -> mules).""" - - # Define any static lock names here, additional locks will be appended for each configured farm's message handler - _locks: List[str] = [] - - def init_late_prefork(self): - num = int(uwsgi.opt.get("locks", 0)) + 1 - need = len(self.stack._lock_farms) - if num < need: - raise RuntimeError( - "Need %i uWSGI locks but only %i exist(s): Set `locks = %i` in uWSGI configuration" - % (need, num, need - 1) - ) - self._locks.extend([f"RECV_MSG_FARM_{x}" for x in sorted(self.stack._lock_farms)]) - # this would be nice, but in my 2.0.15 uWSGI, the uwsgi module has no set_option function, and I don't know if it'd work even if the function existed as documented - # if len(self.lock_map) > 1: - # uwsgi.set_option('locks', len(self.lock_map)) - # log.debug('Created %s uWSGI locks' % len(self.lock_map)) - - def __init__(self, app, stack, dispatcher=None): - super().__init__(app, stack, dispatcher=dispatcher) - - def __lock(self, name_or_id): - try: - uwsgi.lock(name_or_id) - except TypeError: - uwsgi.lock(self._locks.index(name_or_id)) - - def __unlock(self, name_or_id): - try: - uwsgi.unlock(name_or_id) - except TypeError: - uwsgi.unlock(self._locks.index(name_or_id)) - - def _farm_recv_msg_lock_num(self): - return self._locks.index(f"RECV_MSG_FARM_{self.stack._farm_name}") - - def _dispatch_messages(self): - # this could be moved to the base class if locking was abstracted and a get_message method was added - log.info("Application stack message dispatcher thread starting up") - # we are going to do this a lot, so cache the lock number - lock = self._farm_recv_msg_lock_num() - while self.running: - msg = None - self.__lock(lock) - try: - log.debug("Acquired message lock, waiting for new message") - msg = unicodify(uwsgi.farm_get_msg()) - log.debug("Received message: %s", msg) - if msg == self.SHUTDOWN_MSG: - self.running = False - else: - self.dispatcher.dispatch(msg) - except Exception: - log.exception("Exception in mule message handling") - finally: - self.__unlock(lock) - log.debug("Released lock") - log.info("Application stack message dispatcher thread exiting") - - # TODO: start_if_needed would be called on a web worker by the stack's register_message_handler function if a - # function were registered in a web handler, that should probably be prevented. - - def start(self): - """Post-fork initialization. - - This is mainly done here for the future possibility that we'll be able to run mules post-fork without exec()ing. In a programmed mule it could be done at __init__ time. - """ - if self.stack._is_mule: - if not uwsgi.in_farm(): - raise RuntimeError( - "Mule %s is not in a farm! Set `farm = :%s` in uWSGI configuration" - % ( - uwsgi.mule_id(), - ",".join( - map( - str, - range( - 1, - len([x for x in self.stack._configured_mules if x.endswith("galaxy/main.py")]) + 1, - ), - ) - ), - ) - ) - elif len(self.stack._farms) > 1: - raise RuntimeError( - f"Mule {uwsgi.mule_id()} is in multiple farms! This configuration is not supported due to locking issues" - ) - # only mules receive messages so don't bother starting the dispatcher if we're not a mule (although - # currently it doesn't have any registered handlers and so wouldn't start anyway) - super().start() - - def shutdown(self): - if self.stack._is_mule: - super().shutdown() - - def send_message(self, msg, dest): - log.debug("Sending message to farm %s: %s", dest, msg) - uwsgi.farm_msg(dest, msg) diff --git a/lib/galaxy/webapps/__init__.py b/lib/galaxy/webapps/__init__.py index 988de1719ee..265603ef3de 100644 --- a/lib/galaxy/webapps/__init__.py +++ b/lib/galaxy/webapps/__init__.py @@ -1,3 +1,5 @@ """Galaxy webapps root package -- this is a namespace package.""" -__import__("pkg_resources").declare_namespace(__name__) +from pkgutil import extend_path + +__path__ = extend_path(__path__, __name__) # type: ignore[has-type] diff --git a/lib/galaxy/webapps/base/controller.py b/lib/galaxy/webapps/base/controller.py index 383e2cefe2d..0fe5a77fc0c 100644 --- a/lib/galaxy/webapps/base/controller.py +++ b/lib/galaxy/webapps/base/controller.py @@ -1577,3 +1577,15 @@ def sort_by_attr(seq, attr): intermed = [(getattr(v, attr), i, v) for i, v in enumerate(seq)] intermed.sort() return [_[-1] for _ in intermed] + + +__all__ = ( + "HTTPBadRequest", + "SharableMixin", + "sort_by_attr", + "url_for", + "UsesExtendedMetadataMixin", + "UsesFormDefinitionsMixin", + "UsesTagsMixin", + "web", +) diff --git a/lib/galaxy/webapps/base/webapp.py b/lib/galaxy/webapps/base/webapp.py index efa7dc4f0a3..8505d9784ef 100644 --- a/lib/galaxy/webapps/base/webapp.py +++ b/lib/galaxy/webapps/base/webapp.py @@ -5,6 +5,7 @@ import inspect import logging import os import random +import re import socket import string import time @@ -17,6 +18,7 @@ from urllib.parse import urlparse import mako.lookup import mako.runtime +from apispec import APISpec from babel import Locale from babel.support import Translations from sqlalchemy import ( @@ -45,7 +47,6 @@ from galaxy.web.framework import ( helpers, url_for, ) -from galaxy.web_stack import get_app_kwds try: from importlib.resources import files # type: ignore[attr-defined] @@ -101,6 +102,55 @@ class WebApplication(base.WebApplication): # Security helper self.security = galaxy_app.security + def build_apispec(self): + """ + Traverse all route paths starting with "api" and create an APISpec instance. + """ + # API specification builder + apispec = APISpec( + title=self.name, version="0.0.0-unsupported", openapi_version="3.0.2" # galaxy_app.config.version_major, + ) + RE_URL = re.compile( + r""" + (?::\(|{) + (\w*) + (?::.*)? + (?:\)|})""", + re.X, + ) + for rule in self.mapper.matchlist: + if rule.routepath.endswith(".:(format)") or not rule.routepath.startswith("api/"): + continue + # Try to replace routes various ways to encode variables with simple swagger {form} + swagger_path = "/%s" % RE_URL.sub(r"{\1}", rule.routepath) + controller = rule.defaults.get("controller", "") + action = rule.defaults.get("action", "") + # Get the list of methods for the route + methods = [] + if rule.conditions: + m = rule.conditions.get("method", []) + methods = type(m) is str and [m] or m + # Find the controller class + if controller not in self.api_controllers: + log.warning("No controller class found for '%s' while building API spec", controller) + continue + controller_class = self.api_controllers[controller] + if not hasattr(controller_class, action): + log.warning("No action found for '%s' in class '%s' while building API spec", action, controller_class) + continue + action_method = getattr(controller_class, action) + operations = {} + # Add methods that have routes but are not documents + for method in methods: + if method.lower() not in operations: + operations[method.lower()] = { + "description": f"This route has not yet been ported to FastAPI. The documentation may not be complete.\n{action_method.__doc__}", + "tags": ["undocumented"], + } + # Store the swagger path + apispec.path(path=swagger_path, operations=operations) + return apispec + def create_mako_template_lookup(self, galaxy_app, name): paths = [] base_package = ( @@ -203,7 +253,8 @@ def config_allows_origin(origin_raw, config): def url_builder(*args, **kwargs) -> str: - """Wrapper around the uWSGI version of the function for reversing URLs.""" + """Wrapper around the WSGI version of the function for reversing URLs.""" + kwargs.update(kwargs.pop("query_params", {})) return url_for(*args, **kwargs) @@ -1033,17 +1084,7 @@ def default_url_path(path): return os.path.abspath(os.path.join(os.path.dirname(__file__), path)) -def build_native_uwsgi_app(paste_factory, config_section): - """uwsgi can load paste factories with --ini-paste, but this builds non-paste uwsgi apps. - - In particular these are useful with --yaml or --json for config.""" - # TODO: just move this to a classmethod on stack? - app_kwds = get_app_kwds(config_section) - uwsgi_app = paste_factory({}, load_app_kwds=app_kwds) - return uwsgi_app - - -def build_url_map(app, global_conf, local_conf): +def build_url_map(app, global_conf, **local_conf): from paste.urlmap import URLMap from galaxy.web.framework.middleware.static import CacheableStaticURLParser as Static @@ -1075,4 +1116,4 @@ def build_url_map(app, global_conf, local_conf): if "static_local_dir" in conf: urlmap["/static_local"] = Static(conf["static_local_dir"], cache_time) - return urlmap, cache_time + return urlmap diff --git a/lib/galaxy/webapps/galaxy/api/__init__.py b/lib/galaxy/webapps/galaxy/api/__init__.py index c27c236a61c..c11591fbf15 100644 --- a/lib/galaxy/webapps/galaxy/api/__init__.py +++ b/lib/galaxy/webapps/galaxy/api/__init__.py @@ -10,6 +10,7 @@ from typing import ( Type, TypeVar, ) +from urllib.parse import urlencode from fastapi import ( Cookie, @@ -153,12 +154,20 @@ class UrlBuilder: def __call__(self, name: str, **path_params): qualified = path_params.pop("qualified", False) + # starlette does not support query parameters in url_path_for: https://github.com/encode/starlette/issues/560 + query_params = path_params.pop("query_params", None) try: if qualified: - return self.request.url_for(name, **path_params) - return self.request.app.url_path_for(name, **path_params) + url = self.request.url_for(name, **path_params) + else: + url = self.request.app.url_path_for(name, **path_params) + if query_params: + url = f"{url}?{urlencode(query_params)}" + return url except NoMatchFound: # Fallback to legacy url_for + if query_params: + path_params.update(query_params) return web.url_for(name, **path_params) diff --git a/lib/galaxy/webapps/galaxy/api/cloud.py b/lib/galaxy/webapps/galaxy/api/cloud.py index 2d1c2c1a580..e3fe3c881f4 100644 --- a/lib/galaxy/webapps/galaxy/api/cloud.py +++ b/lib/galaxy/webapps/galaxy/api/cloud.py @@ -130,7 +130,7 @@ class CloudController(BaseGalaxyAPIController): if not isinstance(objects, list): raise ActionInputError( - "The `objects` should be a list, but received an object of type {} instead.".format(type(objects)) + f"The `objects` should be a list, but received an object of type {type(objects)} instead." ) datasets = self.cloud_manager.get( diff --git a/lib/galaxy/webapps/galaxy/api/datasets.py b/lib/galaxy/webapps/galaxy/api/datasets.py index d47143399f1..4015792f39d 100644 --- a/lib/galaxy/webapps/galaxy/api/datasets.py +++ b/lib/galaxy/webapps/galaxy/api/datasets.py @@ -47,7 +47,7 @@ from galaxy.webapps.galaxy.api.common import ( ) from galaxy.webapps.galaxy.services.datasets import ( ConvertedDatasetsMap, - DatasetInheritanceChainEntry, + DatasetInheritanceChain, DatasetsService, DatasetStorageDetails, DatasetTextContentDetails, @@ -116,7 +116,7 @@ class FastAPIDatasets: trans=DependsOnTrans, dataset_id: EncodedDatabaseIdField = DatasetIDPathParam, hda_ldda: DatasetSourceType = DatasetSourceQueryParam, - ) -> List[DatasetInheritanceChainEntry]: + ) -> DatasetInheritanceChain: return self.service.show_inheritance_chain(trans, dataset_id, hda_ldda) @router.get( @@ -292,7 +292,7 @@ class FastAPIDatasets: and return different kinds of responses, the documentation here will be limited. To get more information please check the source code. """ - exclude_params = set(["hda_ldda", "data_type"]) + exclude_params = {"hda_ldda", "data_type"} exclude_params.update(SerializationParams.__fields__.keys()) extra_params = get_query_parameters_from_request_excluding(request, exclude_params) diff --git a/lib/galaxy/webapps/galaxy/api/genomes.py b/lib/galaxy/webapps/galaxy/api/genomes.py index dc693936690..9e6fbe38a5b 100644 --- a/lib/galaxy/webapps/galaxy/api/genomes.py +++ b/lib/galaxy/webapps/galaxy/api/genomes.py @@ -143,6 +143,17 @@ class GenomesController(BaseGalaxyAPIController): def index(self, trans, **kwd): """ GET /api/genomes: returns a list of installed genomes + --- + get: + description: returns a list of installed genomes + responses: + 200: + content: + application/json: + schema: + type: array + items: + type: string """ chrom_info = kwd.get("chrom_info") return self.manager.get_dbkeys(trans.user, chrom_info) diff --git a/lib/galaxy/webapps/galaxy/api/histories.py b/lib/galaxy/webapps/galaxy/api/histories.py index 6a460b1f5db..d1a206d5474 100644 --- a/lib/galaxy/webapps/galaxy/api/histories.py +++ b/lib/galaxy/webapps/galaxy/api/histories.py @@ -55,7 +55,7 @@ from galaxy.web import ( expose_api, expose_api_anonymous, expose_api_anonymous_and_sessionless, - expose_api_raw, + expose_api_raw_anonymous_and_sessionless, ) from galaxy.webapps.galaxy.api.common import ( parse_serialization_params, @@ -751,7 +751,7 @@ class HistoriesController(BaseGalaxyAPIController): trans.response.status = 202 return export_result - @expose_api_raw + @expose_api_raw_anonymous_and_sessionless def archive_download(self, trans, id, jeha_id, **kwds): """ GET /api/histories/{id}/exports/{jeha_id} diff --git a/lib/galaxy/webapps/galaxy/api/history_contents.py b/lib/galaxy/webapps/galaxy/api/history_contents.py index 21427c08089..601f3fc25d7 100644 --- a/lib/galaxy/webapps/galaxy/api/history_contents.py +++ b/lib/galaxy/webapps/galaxy/api/history_contents.py @@ -710,13 +710,12 @@ class FastAPIHistoryContents: Example: ?update_time-gt=2015-01-29 """ - serialization_params.default_view = serialization_params.default_view or "betawebclient" # Needed to parse arbitrary query parameter names for the filters. # Since we are directly accessing the request's query_params we also need to exclude the # known params that are already parsed by FastAPI or they may be treated as filter params too. # This looks a bit hacky... - exclude_params = set(["since"]) + exclude_params = {"since"} exclude_params.update(SerializationParams.__fields__.keys()) filter_params = parse_content_filter_params(request.query_params._dict, exclude=exclude_params) @@ -999,7 +998,9 @@ class HistoryContentsController(BaseGalaxyAPIController, UsesLibraryMixinItems, # return self.hda_manager.serialize_dataset_association_roles(trans, hda) @expose_api - def update_permissions(self, trans, history_id, history_content_id, payload: Dict[str, Any] = None, **kwd): + def update_permissions( + self, trans, history_id, history_content_id, payload: Optional[Dict[str, Any]] = None, **kwd + ): """ Set permissions of the given library dataset to the given role ids. @@ -1186,7 +1187,7 @@ class HistoryContentsController(BaseGalaxyAPIController, UsesLibraryMixinItems, GET /api/histories/{history_id}/contents/{direction:near|before|after}/{hid}/{limit} """ - serialization_params = parse_serialization_params(default_view="betawebclient", **kwd) + serialization_params = parse_serialization_params(**kwd) since_str = kwd.pop("since", None) if since_str: diff --git a/lib/galaxy/webapps/galaxy/api/libraries.py b/lib/galaxy/webapps/galaxy/api/libraries.py index 2734d97a00b..b675b81e1e2 100644 --- a/lib/galaxy/webapps/galaxy/api/libraries.py +++ b/lib/galaxy/webapps/galaxy/api/libraries.py @@ -303,7 +303,9 @@ class LibrariesController(BaseGalaxyAPIController): return self.service.update(trans, id, update_payload) @expose_api - def delete(self, trans: ProvidesUserContext, id: EncodedDatabaseIdField, payload: Dict[str, Any] = None, **kwd): + def delete( + self, trans: ProvidesUserContext, id: EncodedDatabaseIdField, payload: Optional[Dict[str, Any]] = None, **kwd + ): """ * DELETE /api/libraries/{id} marks the library with the given ``id`` as `deleted` (or removes the `deleted` mark if the `undelete` param is true) diff --git a/lib/galaxy/webapps/galaxy/api/tool_shed_repositories.py b/lib/galaxy/webapps/galaxy/api/tool_shed_repositories.py index 7ea6515e1c7..dd87dc3002e 100644 --- a/lib/galaxy/webapps/galaxy/api/tool_shed_repositories.py +++ b/lib/galaxy/webapps/galaxy/api/tool_shed_repositories.py @@ -419,17 +419,10 @@ class ToolShedRepositoriesController(BaseGalaxyAPIController): ) results["unsuccessful_count"] += 1 else: - message = "Successfully reset metadata on repository %s owned by %s" % ( - str(repository.name), - str(repository.owner), - ) + message = f"Successfully reset metadata on repository {repository.name} owned by {repository.owner}" results["successful_count"] += 1 except Exception as e: - message = "Error resetting metadata on repository %s owned by %s: %s" % ( - str(repository.name), - str(repository.owner), - util.unicodify(e), - ) + message = f"Error resetting metadata on repository {repository.name} owned by {repository.owner}: {e}" results["unsuccessful_count"] += 1 results["repository_status"].append(message) stop_time = strftime("%Y-%m-%d %H:%M:%S") @@ -472,7 +465,7 @@ class ToolShedRepositoriesController(BaseGalaxyAPIController): # Example URL: http://localhost:8763/api/tool_shed_repositories/df7a1f0c02a5b08e tool_shed_repository = get_tool_shed_repository_by_id(self.app, id) if tool_shed_repository is None: - log.debug(f"Unable to locate tool_shed_repository record for id {str(id)}.") + log.debug(f"Unable to locate tool_shed_repository record for id {id}.") return {} tool_shed_repository_dict = tool_shed_repository.as_dict( value_mapper=self.__get_value_mapper(trans, tool_shed_repository) diff --git a/lib/galaxy/webapps/galaxy/api/users.py b/lib/galaxy/webapps/galaxy/api/users.py index e5bfde0ed4c..8ffbf7d9f91 100644 --- a/lib/galaxy/webapps/galaxy/api/users.py +++ b/lib/galaxy/webapps/galaxy/api/users.py @@ -6,6 +6,10 @@ import json import logging import re +from fastapi import ( + Response, + status, +) from markupsafe import escape from sqlalchemy import ( false, @@ -50,13 +54,35 @@ from galaxy.webapps.base.controller import ( UsesTagsMixin, ) from galaxy.webapps.base.webapp import GalaxyWebTransaction +from galaxy.webapps.galaxy.services.users import UsersService from . import ( BaseGalaxyAPIController, depends, + DependsOnTrans, + Router, ) log = logging.getLogger(__name__) +router = Router(tags=["users"]) + + +@router.cbv +class FastAPIHistories: + service: UsersService = depends(UsersService) + + @router.put( + "/api/users/recalculate_disk_usage", + summary="Triggers a recalculation of the current user disk usage.", + status_code=status.HTTP_204_NO_CONTENT, + ) + def recalculate_disk_usage( + self, + trans: ProvidesUserContext = DependsOnTrans, + ): + self.service.recalculate_disk_usage(trans) + return Response(status_code=status.HTTP_204_NO_CONTENT) + class UserAPIController(BaseGalaxyAPIController, UsesTagsMixin, BaseUIController, UsesFormDefinitionsMixin): user_manager: users.UserManager = depends(users.UserManager) diff --git a/lib/galaxy/webapps/galaxy/api/workflows.py b/lib/galaxy/webapps/galaxy/api/workflows.py index 7382f476577..cbd5550348d 100644 --- a/lib/galaxy/webapps/galaxy/api/workflows.py +++ b/lib/galaxy/webapps/galaxy/api/workflows.py @@ -215,10 +215,12 @@ class WorkflowsAPIController(BaseGalaxyAPIController, UsesStoredWorkflowMixin, U workflows = [] workflows_by_toolshed = dict() for value in rval: - tool_ids = self.workflow_contents_manager.get_all_tool_ids( + tools = self.workflow_contents_manager.get_all_tools( self.__get_stored_workflow(trans, value["id"]).latest_workflow ) - missing_tool_ids = [tool_id for tool_id in tool_ids if self.app.toolbox.is_missing_shed_tool(tool_id)] + missing_tool_ids = [ + tool["tool_id"] for tool in tools if self.app.toolbox.is_missing_shed_tool(tool["tool_id"]) + ] if len(missing_tool_ids) > 0: value["missing_tools"] = missing_tool_ids workflows_missing_tools.append(value) @@ -581,6 +583,10 @@ class WorkflowsAPIController(BaseGalaxyAPIController, UsesStoredWorkflowMixin, U stored_workflow.published = workflow_dict["published"] trans.sa_session.flush() + if "importable" in workflow_dict and stored_workflow.importable != workflow_dict["importable"]: + stored_workflow.importable = workflow_dict["importable"] + trans.sa_session.flush() + if "annotation" in workflow_dict and not steps_updated: newAnnotation = sanitize_html(workflow_dict["annotation"]) self.add_item_annotation(trans.sa_session, trans.user, stored_workflow, newAnnotation) @@ -808,12 +814,24 @@ class WorkflowsAPIController(BaseGalaxyAPIController, UsesStoredWorkflowMixin, U if not is_batch and len(run_configs) != 1: raise exceptions.RequestParameterInvalidException("Must specify 'batch' to use batch parameters.") - tool_ids = self.workflow_contents_manager.get_all_tool_ids(workflow) - missing_tool_ids = [tool_id for tool_id in tool_ids if not self.app.toolbox.has_tool(tool_id)] - if missing_tool_ids: - raise exceptions.MessageException( - f"Workflow was not invoked; the following required tools are not installed: {', '.join(missing_tool_ids)}" + require_exact_tool_versions = util.string_as_bool(payload.get("require_exact_tool_versions", "true")) + tools = self.workflow_contents_manager.get_all_tools(workflow) + missing_tools = [ + tool + for tool in tools + if not self.app.toolbox.has_tool( + tool["tool_id"], tool_version=tool["tool_version"], exact=require_exact_tool_versions ) + ] + if missing_tools: + missing_tools_message = "Workflow was not invoked; the following required tools are not installed: " + if require_exact_tool_versions: + missing_tools_message += ", ".join( + [f"{tool['tool_id']} (version {tool['tool_version']})" for tool in missing_tools] + ) + else: + missing_tools_message += ", ".join([tool["tool_id"] for tool in missing_tools]) + raise exceptions.MessageException(missing_tools_message) invocations = [] for run_config in run_configs: diff --git a/lib/galaxy/webapps/galaxy/buildapp.py b/lib/galaxy/webapps/galaxy/buildapp.py index 2dbb96d3a56..7aeb549e2c9 100644 --- a/lib/galaxy/webapps/galaxy/buildapp.py +++ b/lib/galaxy/webapps/galaxy/buildapp.py @@ -25,6 +25,7 @@ from galaxy.web.framework.middleware.batch import BatchMiddleware from galaxy.web.framework.middleware.error import ErrorMiddleware from galaxy.web.framework.middleware.request_id import RequestIDMiddleware from galaxy.web.framework.middleware.xforwardedhost import XForwardedHostMiddleware +from galaxy.webapps.base.webapp import build_url_map from galaxy.webapps.util import wrap_if_allowed log = logging.getLogger(__name__) @@ -259,18 +260,13 @@ def app_pair(global_conf, load_app_kwds=None, wsgi_preflight=True, **kwargs): # ==== Done # Indicate that all configuration settings have been provided webapp.finalize_config() + app.api_spec = webapp.build_apispec() # Wrap the webapp in some useful middleware if kwargs.get("middleware", True): webapp = wrap_in_middleware(webapp, global_conf, app.application_stack, **kwargs) if asbool(kwargs.get("static_enabled", True)): - webapp = wrap_if_allowed( - webapp, - app.application_stack, - wrap_in_static, - args=(global_conf,), - kwargs=dict(plugin_frameworks=[app.visualizations_registry], **kwargs), - ) + webapp = wrap_if_allowed(webapp, app.application_stack, build_url_map, args=(global_conf,), kwargs=kwargs) app.application_stack.register_postfork_function(postfork_setup) for th in threading.enumerate(): @@ -280,14 +276,6 @@ def app_pair(global_conf, load_app_kwds=None, wsgi_preflight=True, **kwargs): return webapp, app -def uwsgi_app(): - return galaxy.webapps.base.webapp.build_native_uwsgi_app(app_factory, "galaxy") - - -# For backwards compatibility -uwsgi_app_factory = uwsgi_app - - def postfork_setup(): from galaxy.app import app @@ -378,7 +366,7 @@ def populate_api_routes(webapp, app): conditions=dict(method=["GET"]), ) webapp.mapper.connect( - "history_contents_metadata_file", + "get_metadata_file", "/api/histories/{history_id}/contents/{history_content_id}/metadata_file", controller="datasets", action="get_metadata_file", @@ -1837,6 +1825,8 @@ def populate_api_routes(webapp, app): "create", "/api/metrics", controller="metrics", action="create", conditions=dict(method=["POST"]) ) + webapp.mapper.connect("docs", "/api/docs", controller="docs", action="index", conditions={"method": "GET"}) + def _add_item_tags_controller(webapp, name_prefix, path_prefix, **kwd): # Not just using map.resources because actions should be based on name not id @@ -1999,7 +1989,7 @@ def wrap_in_middleware(app, global_conf, application_stack, **local_conf): TusMiddleware, kwargs={ "upload_path": urljoin(f"{application_stack.config.galaxy_url_prefix}/", "api/upload/resumable_upload"), - "tmp_dir": application_stack.config.new_file_path, + "tmp_dir": application_stack.config.tus_upload_store or application_stack.config.new_file_path, "max_size": application_stack.config.maximum_upload_file_size, }, ) @@ -2010,8 +2000,3 @@ def wrap_in_middleware(app, global_conf, application_stack, **local_conf): app = wrap_if_allowed(app, stack, SQLDebugMiddleware, args=(webapp, {})) return app - - -def wrap_in_static(app, global_conf, plugin_frameworks=None, **local_conf): - urlmap, cache_time = galaxy.webapps.base.webapp.build_url_map(app, global_conf, local_conf) - return urlmap diff --git a/lib/galaxy/webapps/galaxy/controllers/admin_toolshed.py b/lib/galaxy/webapps/galaxy/controllers/admin_toolshed.py index 213a1304290..fa02c41a9ed 100644 --- a/lib/galaxy/webapps/galaxy/controllers/admin_toolshed.py +++ b/lib/galaxy/webapps/galaxy/controllers/admin_toolshed.py @@ -412,7 +412,7 @@ class AdminToolshed(AdminGalaxy): return trans.response.send_redirect(url) else: return trans.show_error_message( - f"Cannot locate installed tool shed repository with encoded id {str(repository_id)}." + f"Cannot locate installed tool shed repository with encoded id {repository_id}." ) else: return trans.show_error_message( @@ -462,10 +462,7 @@ class AdminToolshed(AdminGalaxy): tool_dependencies = tool_dependency_util.create_tool_dependency_objects( trans.app, repository, relative_install_dir, set_status=False ) - message = "The installed repository named '%s' has been updated to change set revision '%s'. " % ( - escape(str(repository.name)), - updating_to_changeset_revision, - ) + message = f"The installed repository named '{escape(str(repository.name))}' has been updated to change set revision '{updating_to_changeset_revision}'. " self.initiate_tool_dependency_installation(trans, tool_dependencies, message=message, status=status) # Handle tool dependencies check box. if not trans.app.toolbox.dependency_manager.uses_tool_shed_dependencies: @@ -1483,10 +1480,7 @@ class AdminToolshed(AdminGalaxy): status=status, ) return self.install_tool_dependencies_with_update(trans, **new_kwd) - message = "The installed repository named '%s' has been updated to change set revision '%s'. " % ( - name, - latest_changeset_revision, - ) + message = f"The installed repository named '{name}' has been updated to change set revision '{latest_changeset_revision}'. " else: message = ( f"The latest changeset revision could not be retrieved for the installed repository named '{name}'. " diff --git a/lib/galaxy/webapps/galaxy/controllers/dataset.py b/lib/galaxy/webapps/galaxy/controllers/dataset.py index ee68002072f..39c0b0b9506 100644 --- a/lib/galaxy/webapps/galaxy/controllers/dataset.py +++ b/lib/galaxy/webapps/galaxy/controllers/dataset.py @@ -257,6 +257,7 @@ class DatasetInterface(BaseUIController, UsesAnnotations, UsesItemRatings, UsesE "type": "text", "area": True, "label": "Annotation", + "optional": True, "value": self.get_item_annotation_str(trans.sa_session, trans.user, data), "help": "Add an annotation or notes to a dataset; annotations are available when a history is viewed.", }, diff --git a/lib/galaxy/webapps/galaxy/controllers/history.py b/lib/galaxy/webapps/galaxy/controllers/history.py index b6d1e77f768..679e0844f72 100644 --- a/lib/galaxy/webapps/galaxy/controllers/history.py +++ b/lib/galaxy/webapps/galaxy/controllers/history.py @@ -7,7 +7,6 @@ from sqlalchemy import ( true, ) from sqlalchemy.orm import ( - eagerload, joinedload, undefer, ) @@ -264,14 +263,14 @@ class HistoryAllPublishedGrid(grids.Grid): def build_initial_query(self, trans, **kwargs): # TODO: Tags are still loaded one at a time, consider doing this all at once: - # - eagerload would keep everything in one query but would explode the number of rows and potentially + # - joinedload would keep everything in one query but would explode the number of rows and potentially # result in unneeded info transferred over the wire. # - subqueryload("tags").subqueryload("tag") would probably be better under postgres but I'd # like some performance data against a big database first - might cause problems? # - Pull down only username from associated User table since that is all that is used - # (can be used during search). Need join in addition to the eagerload since it is used in - # the .count() query which doesn't respect the eagerload options (could eliminate this with #5523). + # (can be used during search). Need join in addition to the joinedload since it is used in + # the .count() query which doesn't respect the joinedload options (could eliminate this with #5523). # - Undefer average_rating column to prevent loading individual ratings per-history. # - Eager load annotations - this causes a left join which might be inefficient if there were # potentially many items per history (like if joining HDAs for instance) but there should only @@ -279,7 +278,7 @@ class HistoryAllPublishedGrid(grids.Grid): return ( trans.sa_session.query(self.model_class) .join("user") - .options(eagerload("user").load_only("username"), eagerload("annotations"), undefer("average_rating")) + .options(joinedload("user").load_only("username"), joinedload("annotations"), undefer("average_rating")) ) def apply_query_filter(self, trans, query, **kwargs): @@ -685,8 +684,8 @@ class HistoryController(BaseUIController, SharableMixin, UsesAnnotations, UsesIt user = session.query(model.User).filter_by(username=username).first() history = ( trans.sa_session.query(model.History) - .options(eagerload("tags")) - .options(eagerload("annotations")) + .options(joinedload("tags")) + .options(joinedload("annotations")) .filter_by(user=user, slug=slug, deleted=False) .first() ) diff --git a/lib/galaxy/webapps/galaxy/controllers/page.py b/lib/galaxy/webapps/galaxy/controllers/page.py index 4ba9fc15f71..1c6e9c74960 100644 --- a/lib/galaxy/webapps/galaxy/controllers/page.py +++ b/lib/galaxy/webapps/galaxy/controllers/page.py @@ -5,7 +5,7 @@ from sqlalchemy import ( true, ) from sqlalchemy.orm import ( - eagerload, + joinedload, undefer, ) @@ -166,7 +166,7 @@ class PageAllPublishedGrid(grids.Grid): trans.sa_session.query(self.model_class) .join("user") .filter(model.User.deleted == false()) - .options(eagerload("user").load_only("username"), eagerload("annotations"), undefer("average_rating")) + .options(joinedload("user").load_only("username"), joinedload("annotations"), undefer("average_rating")) ) def apply_query_filter(self, trans, query, **kwargs): diff --git a/lib/galaxy/webapps/galaxy/controllers/visualization.py b/lib/galaxy/webapps/galaxy/controllers/visualization.py index 97865af03d6..b9df3334d7a 100644 --- a/lib/galaxy/webapps/galaxy/controllers/visualization.py +++ b/lib/galaxy/webapps/galaxy/controllers/visualization.py @@ -16,7 +16,7 @@ from sqlalchemy import ( true, ) from sqlalchemy.orm import ( - eagerload, + joinedload, undefer, ) @@ -290,7 +290,7 @@ class VisualizationAllPublishedGrid(grids.Grid): return ( trans.sa_session.query(self.model_class) .join("user") - .options(eagerload("user").load_only("username"), eagerload("annotations"), undefer("average_rating")) + .options(joinedload("user").load_only("username"), joinedload("annotations"), undefer("average_rating")) ) def apply_query_filter(self, trans, query, **kwargs): diff --git a/lib/galaxy/webapps/galaxy/controllers/workflow.py b/lib/galaxy/webapps/galaxy/controllers/workflow.py index 95daff9565a..a1664254d4b 100644 --- a/lib/galaxy/webapps/galaxy/controllers/workflow.py +++ b/lib/galaxy/webapps/galaxy/controllers/workflow.py @@ -10,7 +10,6 @@ from sqlalchemy import ( desc, ) from sqlalchemy.orm import ( - eagerload, joinedload, lazyload, undefer, @@ -174,8 +173,8 @@ class StoredWorkflowAllPublishedGrid(grids.Grid): .join("user") .options( lazyload("latest_workflow"), - eagerload("user").load_only("username"), - eagerload("annotations"), + joinedload("user").load_only("username"), + joinedload("annotations"), undefer("average_rating"), ) ) @@ -484,7 +483,7 @@ class WorkflowController(BaseUIController, SharableMixin, UsesStoredWorkflowMixi stored = self.get_stored_workflow(trans, id) if new_annotation: # Sanitize annotation before adding it. - new_annotation = sanitize_html(new_annotation or "") + new_annotation = sanitize_html(new_annotation) self.add_item_annotation(trans.sa_session, trans.get_user(), stored, new_annotation) trans.sa_session.flush() return new_annotation @@ -639,7 +638,7 @@ class WorkflowController(BaseUIController, SharableMixin, UsesStoredWorkflowMixi workflow.stored_workflow = stored_workflow stored_workflow.latest_workflow = workflow # Add annotation. - workflow_annotation = sanitize_html(workflow_annotation or "") + workflow_annotation = sanitize_html(workflow_annotation) self.add_item_annotation(trans.sa_session, trans.get_user(), stored_workflow, workflow_annotation) # Persist session = trans.sa_session @@ -668,7 +667,7 @@ class WorkflowController(BaseUIController, SharableMixin, UsesStoredWorkflowMixi workflow.stored_workflow = stored_workflow stored_workflow.latest_workflow = workflow # Add annotation. - workflow_annotation = sanitize_html(workflow_annotation or "") + workflow_annotation = sanitize_html(workflow_annotation) self.add_item_annotation(trans.sa_session, trans.get_user(), stored_workflow, workflow_annotation) # Persist diff --git a/lib/galaxy/webapps/galaxy/fast_app.py b/lib/galaxy/webapps/galaxy/fast_app.py index df9a26baeba..028b8c26119 100644 --- a/lib/galaxy/webapps/galaxy/fast_app.py +++ b/lib/galaxy/webapps/galaxy/fast_app.py @@ -1,17 +1,19 @@ from pathlib import Path from typing import cast +from a2wsgi import WSGIMiddleware from fastapi import ( FastAPI, Request, ) -from fastapi.middleware.wsgi import WSGIMiddleware +from fastapi.openapi.utils import get_openapi from starlette.middleware.cors import CORSMiddleware from starlette.responses import ( FileResponse, Response, ) +from galaxy.version import VERSION from galaxy.webapps.base.api import ( add_exception_handler, add_request_id_middleware, @@ -25,10 +27,16 @@ api_tags_metadata = [ "name": "configuration", "description": "Configuration-related endpoints.", }, + {"name": "datasets", "description": "Operations on datasets."}, + {"name": "dataset collections"}, { "name": "datatypes", "description": "Operations with supported data types.", }, + { + "name": "datatypes", + "description": "Operations on dataset collections.", + }, { "name": "genomes", "description": "Operations with genome data.", @@ -37,6 +45,18 @@ api_tags_metadata = [ "name": "group_roles", "description": "Operations with group roles.", }, + {"name": "histories"}, + {"name": "libraries"}, + {"name": "folders"}, + {"name": "job_lock"}, + {"name": "metrics"}, + {"name": "default"}, + {"name": "users"}, + {"name": "jobs"}, + {"name": "roles"}, + {"name": "quotas"}, + {"name": "visualizations"}, + {"name": "pages"}, { "name": "licenses", "description": "Operations with [SPDX licenses](https://spdx.org/licenses/).", @@ -57,6 +77,7 @@ api_tags_metadata = [ "name": "remote files", "description": "Operations with remote dataset sources.", }, + {"name": "undocumented", "description": "API routes that have not yet been ported to FastAPI."}, ] @@ -121,6 +142,22 @@ def add_galaxy_middleware(app: FastAPI, gx_app): return response +def include_legacy_openapi(app, gx_app): + if app.openapi_schema: + return app.openapi_schema + openapi_schema = get_openapi( + title="Galaxy API", + version=VERSION, + routes=app.routes, + tags=api_tags_metadata, + ) + legacy_openapi = gx_app.api_spec.to_dict() + legacy_openapi["paths"].update(openapi_schema["paths"]) + openapi_schema["paths"] = legacy_openapi["paths"] + app.openapi_schema = openapi_schema + return app.openapi_schema + + def initialize_fast_app(gx_wsgi_webapp, gx_app): app = FastAPI( title="Galaxy API", @@ -131,6 +168,7 @@ def initialize_fast_app(gx_wsgi_webapp, gx_app): add_galaxy_middleware(app, gx_app) add_request_id_middleware(app) include_all_package_routers(app, "galaxy.webapps.galaxy.api") + include_legacy_openapi(app, gx_app) wsgi_handler = WSGIMiddleware(gx_wsgi_webapp) app.mount("/", wsgi_handler) if gx_app.config.galaxy_url_prefix != "/": @@ -138,3 +176,9 @@ def initialize_fast_app(gx_wsgi_webapp, gx_app): parent_app.mount(gx_app.config.galaxy_url_prefix, app=app) return parent_app return app + + +__all__ = ( + "add_galaxy_middleware", + "initialize_fast_app", +) diff --git a/lib/galaxy/webapps/galaxy/fast_factory.py b/lib/galaxy/webapps/galaxy/fast_factory.py index d2210e6f3fd..fa386c0380a 100644 --- a/lib/galaxy/webapps/galaxy/fast_factory.py +++ b/lib/galaxy/webapps/galaxy/fast_factory.py @@ -37,7 +37,7 @@ with the following command-line. :: - gunicorn 'galaxy.webapps.galaxy.fast_factory:factory()' --env GALAXY_CONFIG_FILE=config/galaxy.ini --pythonpath lib -w 4 -k uvicorn.workers.UvicornWorker + gunicorn 'galaxy.webapps.galaxy.fast_factory:factory()' --env GALAXY_CONFIG_FILE=config/galaxy.ini --pythonpath lib -w 4 -k uvicorn.workers.UvicornWorker --config lib/galaxy/web_stack/gunicorn_config.py """ diff --git a/lib/galaxy/webapps/galaxy/services/datasets.py b/lib/galaxy/webapps/galaxy/services/datasets.py index 89c4a02eed7..9545e34f4f7 100644 --- a/lib/galaxy/webapps/galaxy/services/datasets.py +++ b/lib/galaxy/webapps/galaxy/services/datasets.py @@ -105,6 +105,13 @@ class DatasetInheritanceChainEntry(Model): ) +class DatasetInheritanceChain(Model): + __root__: List[DatasetInheritanceChainEntry] = Field( + default=[], + title="Dataset inheritance chain", + ) + + class ExtraFilesEntryClass(str, Enum): Directory = "Directory" File = "File" @@ -311,7 +318,7 @@ class DatasetsService(ServiceBase, UsesVisualizationMixin): trans: ProvidesHistoryContext, dataset_id: EncodedDatabaseIdField, hda_ldda: DatasetSourceType = DatasetSourceType.hda, - ) -> List[DatasetInheritanceChainEntry]: + ) -> DatasetInheritanceChain: """ Display inheritance chain for the given dataset. """ @@ -322,7 +329,7 @@ class DatasetsService(ServiceBase, UsesVisualizationMixin): for dep in inherit_chain: result.append(DatasetInheritanceChainEntry(name=f"{dep[0].name}", dep=dep[1])) - return result + return DatasetInheritanceChain(__root__=result) def update_permissions( self, diff --git a/lib/galaxy/webapps/galaxy/services/users.py b/lib/galaxy/webapps/galaxy/services/users.py new file mode 100644 index 00000000000..d46820b644e --- /dev/null +++ b/lib/galaxy/webapps/galaxy/services/users.py @@ -0,0 +1,39 @@ +from galaxy import exceptions as glx_exceptions +from galaxy.managers.context import ProvidesUserContext +from galaxy.managers.users import UserManager +from galaxy.queue_worker import send_local_control_task +from galaxy.security.idencoding import IdEncodingHelper +from galaxy.webapps.galaxy.services.base import ServiceBase + + +class UsersService(ServiceBase): + """Common interface/service logic for interactions with users in the context of the API. + + Provides the logic of the actions invoked by API controllers and uses type definitions + and pydantic models to declare its parameters and return types. + """ + + def __init__( + self, + security: IdEncodingHelper, + user_manager: UserManager, + ): + super().__init__(security) + self.user_manager = user_manager + + def recalculate_disk_usage( + self, + trans: ProvidesUserContext, + ): + if trans.anonymous: + raise glx_exceptions.AuthenticationRequired("Only registered users can recalculate disk usage.") + if trans.app.config.enable_celery_tasks: + from galaxy.celery.tasks import recalculate_user_disk_usage + + recalculate_user_disk_usage.delay(user_id=trans.user.id) + else: + send_local_control_task( + trans.app, + "recalculate_user_disk_usage", + kwargs={"user_id": self.encode_id(trans.user.id)}, + ) diff --git a/lib/galaxy/webapps/reports/buildapp.py b/lib/galaxy/webapps/reports/buildapp.py index fdd7a7c7df4..ea406d67790 100644 --- a/lib/galaxy/webapps/reports/buildapp.py +++ b/lib/galaxy/webapps/reports/buildapp.py @@ -13,6 +13,7 @@ import galaxy.model.mapping import galaxy.webapps.base.webapp from galaxy.util import asbool from galaxy.util.properties import load_app_properties +from galaxy.webapps.base.webapp import build_url_map from galaxy.webapps.util import wrap_if_allowed log = logging.getLogger(__name__) @@ -67,6 +68,8 @@ def app_factory(global_conf, load_app_kwds=None, **kwargs): # Wrap the webapp in some useful middleware if kwargs.get("middleware", True): webapp = wrap_in_middleware(webapp, global_conf, app.application_stack, **kwargs) + if asbool(kwargs.get("static_enabled", True)): + webapp = wrap_if_allowed(webapp, app.application_stack, build_url_map, args=(global_conf,), kwargs=kwargs) return webapp @@ -121,7 +124,3 @@ def wrap_in_middleware(app, global_conf, application_stack, **local_conf): app = wrap_if_allowed(app, stack, XForwardedHostMiddleware) return app - - -def uwsgi_app(): - return galaxy.webapps.base.webapp.build_native_uwsgi_app(app_factory, "reports") diff --git a/lib/galaxy/webapps/reports/controllers/jobs.py b/lib/galaxy/webapps/reports/controllers/jobs.py index b0cdf720ea6..333d3c6da8f 100644 --- a/lib/galaxy/webapps/reports/controllers/jobs.py +++ b/lib/galaxy/webapps/reports/controllers/jobs.py @@ -348,7 +348,7 @@ class Jobs(BaseUIController, ReportQueryBuilder): year = int(row.date.strftime("%Y")) if str(by_destination).lower() == "true": - month_year_str = "%s %s" % (month_name, year) + month_year_str = f"{month_name} {year}" curr_month_year_str, unique_month_year_strs = get_curr_item(month_year_str, unique_month_year_strs) if curr_month_year_str == "": curr_month = "" @@ -989,7 +989,7 @@ class Jobs(BaseUIController, ReportQueryBuilder): unique_month_year_strs = set() for row in trans.sa_session.execute(q): if by_destination == "true": - month_year_str = "%s %s" % (row.date.strftime("%B"), row.date.strftime("%Y")) + month_year_str = f"{row.date.strftime('%B')} {row.date.strftime('%Y')}" curr_month_year_str, unique_month_year_strs = get_curr_item(month_year_str, unique_month_year_strs) if curr_month_year_str == "": curr_month = "" diff --git a/lib/galaxy/webapps/reports/controllers/system.py b/lib/galaxy/webapps/reports/controllers/system.py index 6fad63d995d..0c8bfbe63ee 100644 --- a/lib/galaxy/webapps/reports/controllers/system.py +++ b/lib/galaxy/webapps/reports/controllers/system.py @@ -13,7 +13,7 @@ from sqlalchemy import ( null, true, ) -from sqlalchemy.orm import eagerload +from sqlalchemy.orm import joinedload from galaxy import ( model, @@ -115,7 +115,7 @@ class System(BaseUIController): model.History.update_time < cutoff_time, ) ) - .options(eagerload("datasets")) + .options(joinedload("datasets")) ) for history in histories: diff --git a/lib/galaxy/webapps/reports/fast_app.py b/lib/galaxy/webapps/reports/fast_app.py index 46a72913a09..016d14529db 100644 --- a/lib/galaxy/webapps/reports/fast_app.py +++ b/lib/galaxy/webapps/reports/fast_app.py @@ -1,5 +1,5 @@ +from a2wsgi import WSGIMiddleware from fastapi import FastAPI -from fastapi.middleware.wsgi import WSGIMiddleware from galaxy.webapps.base.api import ( add_exception_handler, diff --git a/lib/galaxy/webapps/reports/uwsgi_schema.yml b/lib/galaxy/webapps/reports/uwsgi_schema.yml deleted file mode 120000 index 321ce78cbe3..00000000000 --- a/lib/galaxy/webapps/reports/uwsgi_schema.yml +++ /dev/null @@ -1 +0,0 @@ -../../config/schemas/uwsgi_schema.yml \ No newline at end of file diff --git a/lib/galaxy/workflow/modules.py b/lib/galaxy/workflow/modules.py index 69b00ff14d3..1ba26293a17 100644 --- a/lib/galaxy/workflow/modules.py +++ b/lib/galaxy/workflow/modules.py @@ -9,6 +9,7 @@ from typing import ( Any, cast, Dict, + Iterable, List, Optional, Union, @@ -22,18 +23,21 @@ from galaxy import ( model, web, ) -from galaxy.exceptions import ToolMissingException +from galaxy.exceptions import ( + ToolInputsNotReadyException, + ToolMissingException, +) from galaxy.job_execution.actions.post import ActionBox from galaxy.model import ( PostJobAction, Workflow, + WorkflowStepConnection, ) from galaxy.model.dataset_collections import matching from galaxy.tool_util.parser.output_objects import ToolExpressionOutput from galaxy.tools import ( DatabaseOperationTool, DefaultToolState, - ToolInputsNotReadyException, WORKFLOW_SAFE_TOOL_VERSION_UPDATES, ) from galaxy.tools.actions import filter_output @@ -817,7 +821,12 @@ class InputDataCollectionModule(InputModule): ] input_collection_type = TextToolParameter(None, collection_type_source) tag_source = dict( - name="tag", label="Tag filter", type="text", value=tag, help="Tags to automatically filter inputs" + name="tag", + label="Tag filter", + type="text", + optional="true", + value=tag, + help="Tags to automatically filter inputs", ) input_tag = TextToolParameter(None, tag_source) inputs = {} @@ -881,7 +890,7 @@ class InputParameterModule(WorkflowModule): name = "Input parameter" default_parameter_type = "text" default_optional = False - default_default_value = "" + default_default_value = None parameter_type = default_parameter_type optional = default_optional default_value = default_default_value @@ -918,9 +927,9 @@ class InputParameterModule(WorkflowModule): ) if param_type == "text": if parameter_type == "text": - text_default = parameter_def.get("default") + text_default = parameter_def.get("default") or "" else: - text_default = None + text_default = "" default_source["value"] = text_default input_default_value: Union[ TextToolParameter, @@ -1097,10 +1106,53 @@ class InputParameterModule(WorkflowModule): parameter_type_cond.cases = cases return {"parameter_definition": parameter_type_cond} - def get_runtime_inputs(self, connections=None, **kwds): + def restrict_options(self, connections: Iterable[WorkflowStepConnection], default_value): + try: + static_options = [] + # Retrieve possible runtime options for 'select' type inputs + for connection in connections: + # Well this isn't a great assumption... + module = connection.input_step.module # type: ignore[union-attr] + tool_inputs = module.tool.inputs # may not be set, but we're catching the Exception below. + + def callback(input, prefixed_name, context, **kwargs): + if prefixed_name == connection.input_name and hasattr(input, "get_options"): + static_options.append(input.get_options(self.trans, {})) + + visit_input_values(tool_inputs, module.state.inputs, callback) + + options = None + if static_options and len(static_options) == 1: + # If we are connected to a single option, just use it as is so order is preserved cleanly and such. + options = [ + {"label": o[0], "value": o[1], "selected": bool(default_value and o[1] == default_value)} + for o in static_options[0] + ] + elif static_options: + # Intersection based on values of multiple option connections. + intxn_vals = set.intersection(*({option[1] for option in options} for options in static_options)) + intxn_opts = {option for options in static_options for option in options if option[1] in intxn_vals} + d = defaultdict(set) # Collapse labels with same values + for label, value, _ in intxn_opts: + d[value].add(label) + options = [ + { + "label": ", ".join(label), + "value": value, + "selected": bool(default_value and value == default_value), + } + for value, label in d.items() + ] + + return options + except Exception: + log.debug("Failed to generate options for text parameter, falling back to free text.", exc_info=True) + + def get_runtime_inputs(self, connections: Optional[Iterable[WorkflowStepConnection]] = None, **kwds): parameter_def = self._parse_state_into_dict() parameter_type = parameter_def["parameter_type"] optional = parameter_def["optional"] + default_value = parameter_def.get("default", self.default_default_value) if parameter_type not in ["text", "boolean", "integer", "float", "color"]: raise ValueError("Invalid parameter type for workflow parameters encountered.") @@ -1113,41 +1165,12 @@ class InputParameterModule(WorkflowModule): # Really is just an attempt - tool module may not be available (small problem), get_options may really depend on other # values we are not setting, so this isn't great. Be sure to just fallback to text in this case. attemptRestrictOnConnections = is_text and parameter_def.get("restrictOnConnections") and connections - try: - if attemptRestrictOnConnections: - static_options = [] - # Retrieve possible runtime options for 'select' type inputs - for connection in connections: - # Well this isn't a great assumption... - module = connection.input_step.module - tool_inputs = module.tool.inputs # may not be set, but we're catching the Exception below. - - def callback(input, prefixed_name, context, **kwargs): - if prefixed_name == connection.input_name and hasattr(input, "get_options"): - static_options.append(input.get_options(self.trans, {})) - - visit_input_values(tool_inputs, module.state.inputs, callback) - - options = None - if static_options and len(static_options) == 1: - # If we are connected to a single option, just use it as is so order is preserved cleanly and such. - options = [{"label": o[0], "value": o[1]} for o in static_options[0]] - elif static_options: - # Intersection based on values of multiple option connections. - intxn_vals = set.intersection(*({option[1] for option in options} for options in static_options)) - intxn_opts = {option for options in static_options for option in options if option[1] in intxn_vals} - d = defaultdict(set) # Collapse labels with same values - for label, value, _ in intxn_opts: - d[value].add(label) - options = [ - {"label": ", ".join(label), "value": value, "selected": False} for value, label in d.items() - ] - - if options is not None: - parameter_kwds["options"] = options - restricted_inputs = True - except Exception: - log.debug("Failed to generate options for text parameter, falling back to free text.", exc_info=True) + if attemptRestrictOnConnections: + connections = cast(Iterable[WorkflowStepConnection], connections) + restricted_options = self.restrict_options(connections=connections, default_value=default_value) + if restricted_options is not None: + restricted_inputs = True + parameter_kwds["options"] = restricted_options def _parameter_def_list_to_options(parameter_value): options = [] @@ -1178,8 +1201,10 @@ class InputParameterModule(WorkflowModule): parameter_class = parameter_types[client_parameter_type] if optional: - default_value = parameter_def.get("default", self.default_default_value) - parameter_kwds["value"] = default_value + if client_parameter_type == "select": + parameter_kwds["selected"] = default_value + else: + parameter_kwds["value"] = default_value if parameter_type == "boolean": parameter_kwds["checked"] = default_value @@ -1531,7 +1556,7 @@ class ToolModule(WorkflowModule): return self.tool.name if self.tool else self.tool_id def get_content_id(self): - return self.tool_id + return self.tool.id if self.tool else self.tool_id def get_version(self): return self.tool.version if self.tool else self.tool_version diff --git a/lib/galaxy/workflow/run_request.py b/lib/galaxy/workflow/run_request.py index 1c9e2fa97d5..6cb858ac948 100644 --- a/lib/galaxy/workflow/run_request.py +++ b/lib/galaxy/workflow/run_request.py @@ -94,17 +94,10 @@ def _normalize_inputs(steps, inputs, inputs_by): # start to ensure tool state is being preserved and loaded in a type safe way. assert isinstance(optional, bool) if not inputs_key and default_value is None and not optional: - message = f"Workflow cannot be run because an expected input step '{step.order_index}' ({step.label}) is not optional and no input." + message = f"Workflow cannot be run because an expected input step '{step.id}' ({step.label}) is not optional and no input." raise exceptions.MessageException(message) if inputs_key: - input_value = inputs[inputs_key] - if input_value is None and default_value is None and not optional: - # We've got a null value in a required parameter. - # I can't think of a situation where this should be valid, - # so let's fail early here - message = f"Workflow cannot be run because an expected input step '{step.order_index}' ({step.label}) is not optional and input is null." - raise exceptions.MessageException(message) - normalized_inputs[step.id] = input_value + normalized_inputs[step.id] = inputs[inputs_key] return normalized_inputs diff --git a/lib/galaxy/workflow/scheduling_manager.py b/lib/galaxy/workflow/scheduling_manager.py index 31e5f8138dc..4b981a144b4 100644 --- a/lib/galaxy/workflow/scheduling_manager.py +++ b/lib/galaxy/workflow/scheduling_manager.py @@ -9,10 +9,7 @@ from galaxy.util import plugin_config from galaxy.util.custom_logging import get_logger from galaxy.util.monitors import Monitors from galaxy.util.xml_macros import load -from galaxy.web_stack.handlers import ( - ConfiguresHandlers, - HANDLER_ASSIGNMENT_METHODS, -) +from galaxy.web_stack.handlers import ConfiguresHandlers from galaxy.web_stack.message import WorkflowSchedulingMessage log = get_logger(__name__) @@ -38,9 +35,6 @@ class WorkflowSchedulingManager(ConfiguresHandlers): """ DEFAULT_BASE_HANDLER_POOLS = ("workflow-schedulers", "job-handlers") - UNSUPPORTED_HANDLER_ASSIGNMENT_METHODS = { - HANDLER_ASSIGNMENT_METHODS.UWSGI_MULE_MESSAGE, - } def __init__(self, app): self.app = app @@ -74,13 +68,7 @@ class WorkflowSchedulingManager(ConfiguresHandlers): else: self.__handlers_config = app.job_config - if self._is_workflow_handler(): - if self.__handlers_config.use_messaging: - WorkflowSchedulingMessage().bind_default_handler(self, "_handle_message") - self.app.application_stack.register_message_handler( - self._handle_message, name=WorkflowSchedulingMessage.target - ) - else: + if not self._is_workflow_handler(): # Process should not schedule workflows but should check for any unassigned to handlers self.__startup_recovery() diff --git a/lib/galaxy_test/api/test_datasets.py b/lib/galaxy_test/api/test_datasets.py index a9b0e79698a..87afb2857ce 100644 --- a/lib/galaxy_test/api/test_datasets.py +++ b/lib/galaxy_test/api/test_datasets.py @@ -189,7 +189,6 @@ class DatasetsApiTestCase(ApiTestCase): original_hda = self._get(f"histories/{self.history_id}/contents/{hda_id}").json() assert original_hda["extension"] == "txt" assert original_hda["data_type"] == "galaxy.datatypes.data.Text" - assert "scatterplot" not in [viz["name"] for viz in original_hda["visualizations"]] inputs = { "input1": {"src": "hda", "id": hda_id}, @@ -214,7 +213,6 @@ class DatasetsApiTestCase(ApiTestCase): ).json() assert successful_updated_hda_response["extension"] == "tabular" assert successful_updated_hda_response["data_type"] == "galaxy.datatypes.tabular.Tabular" - assert "scatterplot" in [viz["name"] for viz in successful_updated_hda_response["visualizations"]] invalidly_updated_hda_response = self._put( # try updating with invalid datatype f"histories/{self.history_id}/contents/{hda_id}", data={"datatype": "invalid"}, json=True diff --git a/lib/galaxy_test/api/test_group_roles.py b/lib/galaxy_test/api/test_group_roles.py index e631a5b80d5..f87e9dfb438 100644 --- a/lib/galaxy_test/api/test_group_roles.py +++ b/lib/galaxy_test/api/test_group_roles.py @@ -1,4 +1,7 @@ -from typing import List +from typing import ( + List, + Optional, +) from galaxy_test.base.populators import DatasetPopulator from ._framework import ApiTestCase @@ -9,7 +12,7 @@ class GroupRolesApiTestCase(ApiTestCase): super().setUp() self.dataset_populator = DatasetPopulator(self.galaxy_interactor) - def test_index(self, group_name: str = None): + def test_index(self, group_name: Optional[str] = None): group_name = group_name or "test-group_roles" group = self._create_group(group_name) encoded_group_id = group["id"] @@ -109,7 +112,7 @@ class GroupRolesApiTestCase(ApiTestCase): delete_response = self._delete(f"groups/{encoded_group_id}/roles/{encoded_role_id}", admin=True) self._assert_status_code_is(delete_response, 400) - def _create_group(self, group_name: str, encoded_role_ids: List[str] = None): + def _create_group(self, group_name: str, encoded_role_ids: Optional[List[str]] = None): if encoded_role_ids is None: encoded_role_ids = [self.dataset_populator.user_private_role_id()] role_ids = encoded_role_ids diff --git a/lib/galaxy_test/api/test_group_users.py b/lib/galaxy_test/api/test_group_users.py index 77df40867fa..0f54ade60bb 100644 --- a/lib/galaxy_test/api/test_group_users.py +++ b/lib/galaxy_test/api/test_group_users.py @@ -1,4 +1,7 @@ -from typing import List +from typing import ( + List, + Optional, +) from galaxy_test.base.populators import DatasetPopulator from ._framework import ApiTestCase @@ -9,7 +12,7 @@ class GroupUsersApiTestCase(ApiTestCase): super().setUp() self.dataset_populator = DatasetPopulator(self.galaxy_interactor) - def test_index(self, group_name: str = None): + def test_index(self, group_name: Optional[str] = None): group_name = group_name or "test-group_users" group = self._create_group(group_name) encoded_group_id = group["id"] @@ -109,7 +112,7 @@ class GroupUsersApiTestCase(ApiTestCase): delete_response = self._delete(f"groups/{encoded_group_id}/users/{encoded_user_id}", admin=True) self._assert_status_code_is(delete_response, 400) - def _create_group(self, group_name: str, encoded_user_ids: List[str] = None): + def _create_group(self, group_name: str, encoded_user_ids: Optional[List[str]] = None): if encoded_user_ids is None: encoded_user_ids = [self.dataset_populator.user_id()] user_ids = encoded_user_ids diff --git a/lib/galaxy_test/api/test_groups.py b/lib/galaxy_test/api/test_groups.py index 3771bf6a81d..bf853979573 100644 --- a/lib/galaxy_test/api/test_groups.py +++ b/lib/galaxy_test/api/test_groups.py @@ -1,3 +1,5 @@ +from typing import Optional + from galaxy_test.base.populators import DatasetPopulator from ._framework import ApiTestCase @@ -7,7 +9,7 @@ class GroupsApiTestCase(ApiTestCase): super().setUp() self.dataset_populator = DatasetPopulator(self.galaxy_interactor) - def test_create_valid(self, group_name: str = None): + def test_create_valid(self, group_name: Optional[str] = None): payload = self._build_valid_group_payload(group_name) response = self._post("groups", payload, admin=True, json=True) self._assert_status_code_is(response, 200) @@ -102,7 +104,7 @@ class GroupsApiTestCase(ApiTestCase): if assert_id is not None: assert group["id"] == assert_id - def _build_valid_group_payload(self, name: str = None): + def _build_valid_group_payload(self, name: Optional[str] = None): name = name or self.dataset_populator.get_random_name() user_id = self.dataset_populator.user_id() role_id = self.dataset_populator.user_private_role_id() diff --git a/lib/galaxy_test/api/test_histories.py b/lib/galaxy_test/api/test_histories.py index 059bedfd0e3..135074fc0ec 100644 --- a/lib/galaxy_test/api/test_histories.py +++ b/lib/galaxy_test/api/test_histories.py @@ -1,4 +1,5 @@ import time +from uuid import uuid4 from requests import put @@ -251,7 +252,7 @@ class ImportExportTests(BaseHistories): self.dataset_collection_populator = DatasetCollectionPopulator(self.galaxy_interactor) def test_import_export(self): - history_name = "for_export_default" + history_name = f"for_export_default_{uuid4()}" history_id = self.dataset_populator.setup_history_for_export_testing(history_name) imported_history_id = self._reimport_history(history_id, history_name, wait_on_history_length=2) @@ -284,7 +285,7 @@ class ImportExportTests(BaseHistories): self._import_history_and_wait(import_data, "API Test History", wait_on_history_length=2) def test_import_export_include_deleted(self): - history_name = "for_export_include_deleted" + history_name = f"for_export_include_deleted_{uuid4()}" history_id = self.dataset_populator.new_history(name=history_name) self.dataset_populator.new_dataset(history_id, content="1 2 3") deleted_hda = self.dataset_populator.new_dataset(history_id, content="1 2 3", wait=True) @@ -316,7 +317,7 @@ class ImportExportTests(BaseHistories): @skip_without_tool("job_properties") def test_import_export_failed_job(self): - history_name = "for_export_include_failed_job" + history_name = f"for_export_include_failed_job_{uuid4()}" history_id = self.dataset_populator.new_history(name=history_name) self.dataset_populator.run_tool_raw("job_properties", inputs={"failbool": True}, history_id=history_id) self.dataset_populator.wait_for_history(history_id, assert_ok=False) @@ -337,7 +338,7 @@ class ImportExportTests(BaseHistories): ) def test_import_metadata_regeneration(self): - history_name = "for_import_metadata_regeneration" + history_name = f"for_import_metadata_regeneration_{uuid4()}" history_id = self.dataset_populator.new_history(name=history_name) self.dataset_populator.new_dataset( history_id, content=open(self.test_data_resolver.get_filename("1.bam"), "rb"), file_type="bam", wait=True @@ -359,13 +360,14 @@ class ImportExportTests(BaseHistories): self.dataset_populator.wait_for_history_jobs(imported_history_id, assert_ok=True) bai_metadata = import_bam_metadata["meta_files"][0] assert bai_metadata["file_type"] == "bam_index" + assert "api/" in bai_metadata["download_url"], bai_metadata["download_url"] api_url = bai_metadata["download_url"].split("api/", 1)[1] bai_response = self._get(api_url) self._assert_status_code_is(bai_response, 200) assert len(bai_response.content) > 4 def test_import_export_collection(self): - history_name = "for_export_with_collections" + history_name = f"for_export_with_collections_{uuid4()}" history_id = self.dataset_populator.new_history(name=history_name) self.dataset_collection_populator.create_list_in_history( history_id, contents=["Hello", "World"], direct_upload=True @@ -391,7 +393,7 @@ class ImportExportTests(BaseHistories): ) def test_import_export_nested_collection(self): - history_name = "for_export_with_nested_collections" + history_name = f"for_export_with_nested_collections_{uuid4()}" history_id = self.dataset_populator.new_history(name=history_name) self.dataset_collection_populator.create_list_of_pairs_in_history(history_id) @@ -424,7 +426,6 @@ class ImportExportTests(BaseHistories): history_name, wait_on_history_length=wait_on_history_length, export_kwds=export_kwds, - api_key=self.galaxy_interactor.api_key, ) def _import_history_and_wait(self, import_data, history_name, wait_on_history_length=None): diff --git a/lib/galaxy_test/api/test_history_contents.py b/lib/galaxy_test/api/test_history_contents.py index 1679fefa3f1..b8ca806ae58 100644 --- a/lib/galaxy_test/api/test_history_contents.py +++ b/lib/galaxy_test/api/test_history_contents.py @@ -164,7 +164,7 @@ class HistoryContentsApiTestCase(ApiTestCase): self._assert_status_code_is(show_response, 200) self.__assert_matches_hda(hda1, show_response.json()) - def test_hda_copy(self): + def _create_copy(self): hda1 = self.dataset_populator.new_dataset(self.history_id) create_data = dict( source="hda", @@ -174,7 +174,18 @@ class HistoryContentsApiTestCase(ApiTestCase): assert self.__count_contents(second_history_id) == 0 create_response = self._post(f"histories/{second_history_id}/contents", create_data, json=True) self._assert_status_code_is(create_response, 200) - assert self.__count_contents(second_history_id) == 1 + return create_response.json() + + def test_hda_copy(self): + response = self._create_copy() + assert self.__count_contents(response["history_id"]) == 1 + + def test_inheritance_chain(self): + response = self._create_copy() + inheritance_chain_response = self._get(f"datasets/{response['id']}/inheritance_chain") + self._assert_status_code_is_ok(inheritance_chain_response) + inheritance_chain = inheritance_chain_response.json() + assert len(inheritance_chain) == 1 def test_library_copy(self): ld = self.library_populator.new_library_dataset("lda_test_library") @@ -819,7 +830,7 @@ class HistoryContentsApiTestCase(ApiTestCase): def _assert_collection_has_expected_elements_datatypes(self, history_id, collection_name, expected_datatypes): contents_response = self._get( - f"histories/{history_id}/contents?v=dev&view=betawebclient&q=name-eq&qv={collection_name}" + f"histories/{history_id}/contents?v=dev&view=detailed&q=name-eq&qv={collection_name}" ) self._assert_status_code_is(contents_response, 200) collection = contents_response.json()[0] diff --git a/lib/galaxy_test/api/test_pages.py b/lib/galaxy_test/api/test_pages.py index fd7231d243d..af017abb9e2 100644 --- a/lib/galaxy_test/api/test_pages.py +++ b/lib/galaxy_test/api/test_pages.py @@ -14,11 +14,13 @@ from ._framework import ApiTestCase class BasePageApiTestCase(ApiTestCase): + def setUp(self): + super().setUp() + self.dataset_populator = DatasetPopulator(self.galaxy_interactor) + self.workflow_populator = WorkflowPopulator(self.galaxy_interactor) + def _create_valid_page_with_slug(self, slug): - page_request = self._test_page_payload(slug=slug) - page_response = self._post("pages", page_request, json=True) - self._assert_status_code_is(page_response, 200) - return page_response.json() + return self.dataset_populator.new_page(slug=slug) def _create_valid_page_as(self, other_email, slug): run_as_user = self._setup_user(other_email) @@ -28,30 +30,13 @@ class BasePageApiTestCase(ApiTestCase): return page_response.json() def _test_page_payload(self, **kwds): - content_format = kwds.get("content_format", "html") - if content_format == "html": - content = "

    Page!

    " - else: - content = "*Page*\n\n" - request = dict( - slug="mypage", - title="MY PAGE", - content=content, - content_format=content_format, - ) - request.update(**kwds) - return request + return self.dataset_populator.new_page_payload(**kwds) class PageApiTestCase(BasePageApiTestCase, SharingApiTests): api_name = "pages" - def setUp(self): - super().setUp() - self.dataset_populator = DatasetPopulator(self.galaxy_interactor) - self.workflow_populator = WorkflowPopulator(self.galaxy_interactor) - def create(self, name: str) -> str: response_json = self._create_valid_page_with_slug(name) return response_json["id"] diff --git a/lib/galaxy_test/api/test_workflow_extraction.py b/lib/galaxy_test/api/test_workflow_extraction.py index ec7ca769502..4f78005d1b2 100644 --- a/lib/galaxy_test/api/test_workflow_extraction.py +++ b/lib/galaxy_test/api/test_workflow_extraction.py @@ -507,7 +507,6 @@ test_data: history_name, wait_on_history_length=history_length, export_kwds={}, - api_key=self.galaxy_interactor.api_key, ) # wait a little more for those jobs, todo fix to wait for history imported false or # for a specific number of jobs... diff --git a/lib/galaxy_test/api/test_workflows.py b/lib/galaxy_test/api/test_workflows.py index cc8102cce6f..3d3d5c14c5f 100644 --- a/lib/galaxy_test/api/test_workflows.py +++ b/lib/galaxy_test/api/test_workflows.py @@ -645,6 +645,41 @@ steps: other_import_response = self.__import_workflow(workflow_id) self._assert_status_code_is(other_import_response, 403) + def test_url_import(self): + url = "https://raw.githubusercontent.com/galaxyproject/galaxy/release_19.09/test/base/data/test_workflow_1.ga" + workflow_id = self._post("workflows", data={"archive_source": url}).json()["id"] + workflow = self._download_workflow(workflow_id) + assert "TestWorkflow1" in workflow["name"] + assert ( + workflow.get("source_metadata").get("url") == url + ) # disappearance of source_metadata on modification is tested in test_trs_import + + def test_trs_import(self): + trs_payload = { + "archive_source": "trs_tool", + "trs_server": "dockstore", + "trs_tool_id": "#workflow/github.com/jmchilton/galaxy-workflow-dockstore-example-1/mycoolworkflow", + "trs_version_id": "master", + } + workflow_id = self._post("workflows", data=trs_payload).json()["id"] + original_workflow = self._download_workflow(workflow_id) + assert "Test Workflow" in original_workflow["name"] + assert original_workflow.get("source_metadata").get("trs_tool_id") == trs_payload["trs_tool_id"] + assert original_workflow.get("source_metadata").get("trs_version_id") == trs_payload["trs_version_id"] + + # refactor workflow and check that the trs id is removed + actions = [ + {"action_type": "update_step_label", "step": {"order_index": 0}, "label": "new_label"}, + ] + self.workflow_populator.refactor_workflow(workflow_id, actions) + refactored_workflow = self._download_workflow(workflow_id) + assert refactored_workflow.get("source_metadata") is None + + # reupload original_workflow and check that the trs id is removed + reuploaded_workflow_id = self.workflow_populator.create_workflow(original_workflow) + reuploaded_workflow = self._download_workflow(reuploaded_workflow_id) + assert reuploaded_workflow.get("source_metadata") is None + def test_anonymous_published(self): def anonymous_published_workflows(): workflows_url = self._api_url("workflows?show_published=True") @@ -919,24 +954,6 @@ steps: invocation = self._invocation_details(workflow_id, invocation_id) assert invocation["state"] == "scheduled", invocation - def test_run_workflow_with_missing_tool(self): - with self.dataset_populator.test_history() as history_id: - workflow_id = self._upload_yaml_workflow( - """ -class: GalaxyWorkflow -steps: - step1: - tool_id: nonexistent_tool - tool_version: "0.1" -""" - ) - invocation_response = self.__invoke_workflow(workflow_id, history_id=history_id, assert_ok=False) - self._assert_status_code_is(invocation_response, 400) - self.assertEqual( - invocation_response.json().get("err_msg"), - "Workflow was not invoked; the following required tools are not installed: nonexistent_tool", - ) - @skip_without_tool("collection_creates_pair") def test_workflow_run_output_collections(self) -> None: with self.dataset_populator.test_history() as history_id: @@ -964,6 +981,7 @@ steps: cond_param_inner: true input1: $link: 0/out_file1 + thedata: null cat: tool_id: cat1 in: @@ -2899,43 +2917,6 @@ text_input: jobs = self._history_jobs(history_id) assert len(jobs) == 1 - def test_run_with_required_text_parameter_not_provided_fails(self): - with self.dataset_populator.test_history() as history_id: - try: - self._run_workflow( - """ -class: GalaxyWorkflow -inputs: - text_input: text -steps: [] -""", - assert_ok=True, - history_id=history_id, - ) - except AssertionError as e: - assert "(text_input) is not optional" in str(e) - - def test_run_with_required_text_parameter_null_fails(self): - with self.dataset_populator.test_history() as history_id: - try: - self._run_workflow( - """ -class: GalaxyWorkflow -inputs: - text_input: text -steps: [] -""", - test_data=""" -text_input: - value: null - type: raw -""", - assert_ok=True, - history_id=history_id, - ) - except AssertionError as e: - assert "(text_input) is not optional" in str(e) - def test_run_with_int_parameter(self): with self.dataset_populator.test_history() as history_id: failed = False @@ -5053,10 +5034,15 @@ input_c: workflow_id = self.workflow_populator.simple_workflow("dummy") response = self._show_workflow(workflow_id) assert not response["published"] + assert not response["importable"] published_worklow = self._put(f"workflows/{workflow_id}", data={"published": True}, json=True).json() assert published_worklow["published"] + importable_worklow = self._put(f"workflows/{workflow_id}", data={"importable": True}, json=True).json() + assert importable_worklow["importable"] unpublished_worklow = self._put(f"workflows/{workflow_id}", data={"published": False}, json=True).json() assert not unpublished_worklow["published"] + unimportable_worklow = self._put(f"workflows/{workflow_id}", data={"importable": False}, json=True).json() + assert not unimportable_worklow["importable"] def test_workflow_from_path_requires_admin(self): # There are two ways to import workflows from paths, just verify both require an admin. diff --git a/lib/galaxy_test/base/populators.py b/lib/galaxy_test/base/populators.py index 9407309fb00..6f53c8f9a14 100644 --- a/lib/galaxy_test/base/populators.py +++ b/lib/galaxy_test/base/populators.py @@ -72,7 +72,6 @@ from gxformat2 import ( ImporterGalaxyInterface, ) from gxformat2._yaml import ordered_load -from pkg_resources import resource_string from requests.models import Response from galaxy.tool_util.client.staging import InteractorStaging @@ -86,18 +85,18 @@ from galaxy.tool_util.verify.wait import wait_on as tool_util_wait_on from galaxy.util import ( DEFAULT_SOCKET_TIMEOUT, galaxy_root_path, - unicodify, ) +from galaxy.util.resources import resource_string from . import api_asserts from .api import ApiTestInteractor CWL_TOOL_DIRECTORY = os.path.join(galaxy_root_path, "test", "functional", "tools", "cwl_tools") # Simple workflow that takes an input and call cat wrapper on it. -workflow_str = unicodify(resource_string(__name__, "data/test_workflow_1.ga")) +workflow_str = resource_string(__package__, "data/test_workflow_1.ga") # Simple workflow that takes an input and filters with random lines twice in a # row - first grabbing 8 lines at random and then 6. -workflow_random_x2_str = unicodify(resource_string(__name__, "data/test_workflow_2.ga")) +workflow_random_x2_str = resource_string(__package__, "data/test_workflow_2.ga") DEFAULT_TIMEOUT = 60 # Secs to wait for state to turn ok @@ -567,7 +566,7 @@ class BaseDatasetPopulator(BasePopulator): def wait_for_job(self, job_id: str, assert_ok: bool = False, timeout: timeout_type = DEFAULT_TIMEOUT): return wait_on_state( - lambda: self.get_job_details(job_id), desc="job state", assert_ok=assert_ok, timeout=timeout + lambda: self.get_job_details(job_id, full=True), desc="job state", assert_ok=assert_ok, timeout=timeout ) def get_job_details(self, job_id: str, full: bool = False) -> Response: @@ -923,6 +922,11 @@ class BaseDatasetPopulator(BasePopulator): assert update_response.status_code == 200, update_response.content return update_response.json() + def make_public(self, history_id: str) -> dict: + sharing_response = self._put(f"histories/{history_id}/publish") + assert sharing_response.status_code == 200 + return sharing_response.json() + def validate_dataset(self, history_id: str, dataset_id: str) -> Dict[str, Any]: url = f"histories/{history_id}/contents/{dataset_id}/validate" update_response = self._put(url) @@ -975,20 +979,19 @@ class BaseDatasetPopulator(BasePopulator): assert "job_id" in job_desc return self.wait_for_job(job_desc["job_id"]) - def export_url(self, history_id: str, data, api_key: str, check_download: bool = True) -> str: + def export_url(self, history_id: str, data, check_download: bool = True) -> str: put_response = self.prepare_export(history_id, data) response = put_response.json() api_asserts.assert_has_keys(response, "download_url") download_url = urllib.parse.urljoin(self.galaxy_interactor.api_url, response["download_url"].strip("/")) if check_download: - self.get_export_url(download_url, api_key) + self.get_export_url(download_url) return download_url - def get_export_url(self, export_url, api_key) -> Response: - full_download_url = f"{export_url}?key={api_key}" - download_response = self._get(full_download_url) + def get_export_url(self, export_url) -> Response: + download_response = self._get(export_url) api_asserts.assert_status_code_is(download_response, 200) return download_response @@ -999,6 +1002,7 @@ class BaseDatasetPopulator(BasePopulator): files["archive_file"] = archive_file import_response = self._post("histories", data=import_data, files=files) api_asserts.assert_status_code_is(import_response, 200) + return import_response.json()["id"] def import_history_and_wait_for_name(self, import_data, history_name): def history_names(): @@ -1007,7 +1011,8 @@ class BaseDatasetPopulator(BasePopulator): import_name = f"imported from archive: {history_name}" assert import_name not in history_names() - self.import_history(import_data) + job_id = self.import_history(import_data) + self.wait_for_job(job_id, assert_ok=True) def has_history_with_name(): histories = history_names() @@ -1041,14 +1046,13 @@ class BaseDatasetPopulator(BasePopulator): contents = contents_response.json() return len(contents) - def reimport_history(self, history_id, history_name, wait_on_history_length, export_kwds, api_key): + def reimport_history(self, history_id, history_name, wait_on_history_length, export_kwds): + # Make history public so we can import by url + self.make_public(history_id) # Export the history. - download_path = self.export_url(history_id, export_kwds, api_key, check_download=True) + download_url = self.export_url(history_id, export_kwds, check_download=True) - # Create download for history - full_download_url = urllib.parse.urljoin(download_path, f"?key={api_key}") - - import_data = dict(archive_source=full_download_url, archive_type="url") + import_data = dict(archive_source=download_url, archive_type="url") imported_history_id = self.import_history_and_wait_for_name(import_data, history_name) @@ -1073,6 +1077,36 @@ class BaseDatasetPopulator(BasePopulator): timeout=timeout, ) + def new_page( + self, slug: str = "mypage", title: str = "MY PAGE", content_format: str = "html", content: Optional[str] = None + ) -> Dict[str, Any]: + page_response = self.new_page_raw(slug=slug, title=title, content_format=content_format, content=content) + page_response.raise_for_status() + return page_response.json() + + def new_page_raw( + self, slug: str = "mypage", title: str = "MY PAGE", content_format: str = "html", content: Optional[str] = None + ) -> Response: + page_request = self.new_page_payload(slug=slug, title=title, content_format=content_format, content=content) + page_response = self._post("pages", page_request, json=True) + return page_response + + def new_page_payload( + self, slug: str = "mypage", title: str = "MY PAGE", content_format: str = "html", content: Optional[str] = None + ) -> Dict[str, str]: + if content is None: + if content_format == "html": + content = "

    Page!

    " + else: + content = "*Page*\n\n" + request = dict( + slug=slug, + title=title, + content=content, + content_format=content_format, + ) + return request + class GalaxyInteractorHttpMixin: galaxy_interactor: ApiTestInteractor @@ -1130,7 +1164,7 @@ class BaseWorkflowPopulator(BasePopulator): def load_workflow_from_resource(self, name: str, filename: Optional[str] = None) -> dict: if filename is None: filename = f"data/{name}.ga" - content = unicodify(resource_string(__name__, filename)) + content = resource_string(__package__, filename) return self.load_workflow(name, content=content) def simple_workflow(self, name: str, **create_kwds) -> str: @@ -2236,12 +2270,13 @@ def wait_on_state( def get_state(): response = state_func() assert response.status_code == 200, f"Failed to fetch state update while waiting. [{response.content}]" - state = response.json()["state"] + state_response = response.json() + state = state_response["state"] if state in skip_states: return None else: if assert_ok: - assert state in ok_states, f"Final state - {state} - not okay." + assert state in ok_states, f"Final state - {state} - not okay. Full response: {state_response}" return state if skip_states is None: diff --git a/lib/galaxy_test/driver/driver_util.py b/lib/galaxy_test/driver/driver_util.py index a20a769c6ba..2c6cf48ef49 100644 --- a/lib/galaxy_test/driver/driver_util.py +++ b/lib/galaxy_test/driver/driver_util.py @@ -1,13 +1,11 @@ """Scripts for drivers of Galaxy functional tests.""" import http.client +import json import logging import os import random -import re -import shlex import shutil -import signal import socket import string import subprocess @@ -15,6 +13,7 @@ import sys import tempfile import threading import time +from pathlib import Path from typing import Optional from urllib.parse import urlparse @@ -22,11 +21,8 @@ import nose.config import nose.core import nose.loader import nose.plugins.manager -import yaml -from paste import httpserver from galaxy.app import UniverseApplication as GalaxyUniverseApplication -from galaxy.app_unittest_utils.celery_helper import rebind_container_to_task from galaxy.config import LOGGING_CONFIG_DEFAULT from galaxy.model import mapping from galaxy.model.database_utils import ( @@ -45,6 +41,7 @@ from galaxy.util import ( ) from galaxy.util.properties import load_app_properties from galaxy.webapps.galaxy import buildapp +from galaxy.webapps.galaxy.fast_app import initialize_fast_app as init_galaxy_fast_app from galaxy_test.base.api_util import ( get_admin_api_key, get_user_api_key, @@ -56,6 +53,7 @@ from galaxy_test.base.env import ( from galaxy_test.base.instrument import StructuredTestDataPlugin from galaxy_test.base.nose_util import run from tool_shed.webapp.app import UniverseApplication as ToolshedUniverseApplication +from tool_shed.webapp.fast_app import initialize_fast_app as init_tool_shed_fast_app from .test_logging import logging_config_file galaxy_root = galaxy_directory() @@ -70,28 +68,7 @@ FRAMEWORK_SAMPLE_TOOLS_CONF = os.path.join(FRAMEWORK_TOOLS_DIR, "samples_tool_co FRAMEWORK_DATATYPES_CONF = os.path.join(FRAMEWORK_TOOLS_DIR, "sample_datatypes_conf.xml") MIGRATED_TOOL_PANEL_CONFIG = "config/migrated_tools_conf.xml" INSTALLED_TOOL_PANEL_CONFIGS = [os.environ.get("GALAXY_TEST_SHED_TOOL_CONF", "config/shed_tool_conf.xml")] -REALTIME_PROXY_TEMPLATE = string.Template( - r""" -uwsgi: - http-raw-body: true - interactivetools_map: $tempdir/interactivetools_map.sqlite - python-raw: scripts/interactivetools/key_type_token_mapping.py - # if interactive tool path, jump to interactive tool, else skip to - # endendend (default uwsgi params). - route-host: ^([A-Za-z0-9]+(?:-[A-Za-z0-9]+)*)-([A-Za-z0-9]+(?:-[A-Za-z0-9]+)*)\.([A-Za-z0-9]+(?:-[A-Za-z0-9]+)*)\.(interactivetool\.$test_host:$test_port)$ goto:interactivetool - route-run: goto:endendend - - route-label: interactivetool - route-host: ^([A-Za-z0-9]+(?:-[A-Za-z0-9]+)*)-([A-Za-z0-9]+(?:-[A-Za-z0-9]+)*)\.([A-Za-z0-9]+(?:-[A-Za-z0-9]+)*)\.(interactivetool\.$test_host:$test_port)$ rpcvar:TARGET_HOST rtt_key_type_token_mapper_cached $1 $3 $2 $4 $0 5 - route-if-not: empty:${TARGET_HOST} httpdumb:${TARGET_HOST} - route: .* break:404 Not Found - - route-label: endendend -""" -) - DEFAULT_LOCALES = "en" -USE_UVICORN = asbool(os.environ.get("GALAXY_TEST_USE_UVICORN", True)) log = logging.getLogger("test_driver") @@ -553,7 +530,7 @@ def attempt_port(port): return None -def attempt_ports(port): +def attempt_ports(port=None, set_galaxy_web_port=True): if port is not None: return port @@ -564,26 +541,13 @@ def attempt_ports(port): port = attempt_port(random.randint(8000, 10000)) if port: port = str(port) - os.environ["GALAXY_WEB_PORT"] = port + if set_galaxy_web_port: + os.environ["GALAXY_WEB_PORT"] = port return port raise Exception(f"Unable to open a port between {8000} and {10000} to start Galaxy server") -def serve_webapp(webapp, port=None, host=None): - """Serve the webapp on a recommend port or a free one. - - Return the port the webapp is running on. - """ - server = None - port = attempt_ports(port) - server = httpserver.serve(webapp, host=host, port=port, start_loop=False) - t = threading.Thread(target=server.serve_forever) - t.start() - - return server, port - - def uvicorn_serve(app, port, host=None): """Serve the webapp on a recommend port or a free one. @@ -663,9 +627,6 @@ def build_galaxy_app(simple_kwargs) -> GalaxyUniverseApplication: simple_kwargs = load_app_properties(kwds=simple_kwargs) # Build the Universe Application app = GalaxyUniverseApplication(**simple_kwargs) - if not simple_kwargs.get("enable_celery_tasks"): - rebind_container_to_task(app) - log.info("Embedded Galaxy application started") global galaxy_context @@ -675,7 +636,7 @@ def build_galaxy_app(simple_kwargs) -> GalaxyUniverseApplication: # Toolbox indexing happens via the work queue out of band recently, and, # beyond potentially running async after tests execute doesn't execute - # without building a uwsgi app (app.is_webapp = False for this test kit). + # without building a webapp (app.is_webapp = False for this test kit). # We need to ensure to build an index for the test galaxy app -- this is # pretty fast with the limited toolset app.reindex_tool_search() @@ -787,152 +748,113 @@ class EmbeddedServerWrapper(ServerWrapper): log.info(f"{threading.active_count()} active after stopping embedded server") -class UwsgiServerWrapper(ServerWrapper): - def __init__(self, p, name, host, port): +class GravityServerWrapper(ServerWrapper): + def __init__(self, name, host, port, state_dir, stop_command): super().__init__(name, host, port) - self._p = p - self._r = None - self._t = threading.Thread(target=self.wait) - self._t.start() + self.url_prefix = os.environ.get("GALAXY_CONFIG_GALAXY_URL_PREFIX", "/") + self.state_dir = Path(state_dir) + self.stop_command = stop_command - def __del__(self): - self._t.join() + def wait_for_server(self): + try: + set_and_wait_for_http_target( + "GALAXY", + self.host, + self.port, + url_prefix=self.url_prefix, + sleep_amount=0.4, + ) + except Exception: + log.error("Gravity logs:\n%s", self.get_logs()) - def wait(self): - self._r = self._p.wait() + def get_logs(self): + gunicorn_logs = (self.state_dir / "log" / "gunicorn.log").read_text() + gxit_logs = (self.state_dir / "log" / "gx-it-proxy.log").read_text() + return f"Gunicorn logs:{os.linesep}{gunicorn_logs}{os.linesep}gx-it-proxy logs:{os.linesep}{gxit_logs}" def stop(self): - try: - os.killpg(os.getpgid(self._p.pid), signal.SIGTERM) - except Exception: - pass - time.sleep(0.1) - try: - os.killpg(os.getpgid(self._p.pid), signal.SIGKILL) - except Exception: - pass - self._t.join() + log.info("Gravity logs:\n%s", self.get_logs()) + self.stop_command() -def launch_uwsgi(kwargs, tempdir, prefix=DEFAULT_CONFIG_PREFIX, config_object=None): +def launch_gravity(port, gxit_port=None, galaxy_config=None): + galaxy_config = galaxy_config or {} + if not gxit_port: + gxit_port = attempt_ports(set_galaxy_web_port=False) + if "interactivetools_proxy_host" not in galaxy_config: + galaxy_config["interactivetools_proxy_host"] = f"localhost:{gxit_port}" + config = { + "gravity": { + "gunicorn": {"bind": f"localhost:{port}"}, + "gx_it_proxy": { + "enable": galaxy_config.get("interactivetools_enable", False), + "port": gxit_port, + "verbose": True, + "sessions": galaxy_config.get("interactivetools_map"), + }, + }, + "galaxy": galaxy_config, + } + state_dir = tempfile.mkdtemp(suffix="state") + with tempfile.NamedTemporaryFile("w", dir=state_dir, delete=False, suffix=".galaxy.yml") as config_fh: + json.dump(config, config_fh) + with tempfile.NamedTemporaryFile(delete=True) as socket: + supervisord_socket = socket.name + gravity_env = os.environ.copy() + gravity_env["SUPERVISORD_SOCKET"] = supervisord_socket + subprocess.check_output(["galaxyctl", "--state-dir", state_dir, "register", config_fh.name], env=gravity_env) + subprocess.check_output(["galaxyctl", "--state-dir", state_dir, "update"], env=gravity_env) + subprocess.check_output(["galaxyctl", "--state-dir", state_dir, "start"], env=gravity_env) + return state_dir, lambda: subprocess.check_output( + ["galaxyctl", "--state-dir", state_dir, "shutdown"], env=gravity_env + ) + + +def launch_server(app_factory, webapp_factory, prefix=DEFAULT_CONFIG_PREFIX, galaxy_config=None, config_object=None): name = prefix.lower() - host, port = explicitly_configured_host_and_port(prefix, config_object) - - config = {} - config["galaxy"] = kwargs.copy() + port = attempt_ports(port) enable_realtime_mapping = getattr(config_object, "enable_realtime_mapping", False) if enable_realtime_mapping: interactive_tool_defaults = { - "interactivetools_prefix": "interactivetool", - "interactivetools_map": os.path.join(tempdir, "interactivetools_map.sqlite"), + "interactivetools_map": tempfile.NamedTemporaryFile( + delete=False, suffix="interactivetools_map.sqlite" + ).name, "interactivetools_enable": True, + "interactivetools_upstream_proxy": False, + "galaxy_infrastructure_url": f"http://localhost:{port}", } - for key, value in interactive_tool_defaults.items(): - if key not in config["galaxy"]: - config["galaxy"][key] = value + interactive_tool_defaults.update(galaxy_config or {}) + galaxy_config = interactive_tool_defaults - yaml_config_path = os.path.join(tempdir, "galaxy.yml") - with open(yaml_config_path, "w") as f: - yaml.dump(config, f) + if enable_realtime_mapping or os.environ.get("GALAXY_TEST_GRAVITY"): + galaxy_config["root"] = galaxy_root + state_dir, stop_command = launch_gravity(port=port, galaxy_config=galaxy_config) + gravity_wrapper = GravityServerWrapper(name, host, port, state_dir, stop_command) + gravity_wrapper.wait_for_server() + return gravity_wrapper - if enable_realtime_mapping: - # Avoid YAML.dump configuration since uwsgi doesn't like real YAML :( - - # though maybe it would work? - with open(yaml_config_path) as f: - old_contents = f.read() - with open(yaml_config_path, "w") as f: - test_port = str(port) if port else r"[0-9]+" - test_host = re.escape(host) if host else "localhost" - uwsgi_section = REALTIME_PROXY_TEMPLATE.safe_substitute( - test_host=test_host, test_port=test_port, tempdir=tempdir - ) - f.write(uwsgi_section) - f.write(old_contents) - - def attempt_port_bind(port): - uwsgi_command = [ - "uwsgi", - "--http", - f"{host}:{port}", - "--yaml", - yaml_config_path, - "--module", - "galaxy.webapps.galaxy.buildapp:uwsgi_app_factory()", - "--enable-threads", - "--die-on-term", - ] - for path in sys.path: - uwsgi_command.append("--pythonpath") - uwsgi_command.append(path) - - handle_uwsgi_cli_command = getattr(config_object, "handle_uwsgi_cli_command", None) - if handle_uwsgi_cli_command is not None: - handle_uwsgi_cli_command(uwsgi_command) - - # we don't want to quote every argument but we don't want to print unquoted ones either, so do this - log.info("Starting uwsgi with command line: %s", " ".join(shlex.quote(x) for x in uwsgi_command)) - p = subprocess.Popen( - uwsgi_command, - cwd=galaxy_root, - preexec_fn=os.setsid, - ) - return UwsgiServerWrapper(p, name, host, port) - - port = attempt_ports(port) - server_wrapper = attempt_port_bind(port) - try: - set_and_wait_for_http_target(prefix, host, port, url_prefix="/", sleep_tries=50) - log.info(f"Test-managed uwsgi web server for {name} started at {host}:{port}") - return server_wrapper - except Exception: - server_wrapper.stop() - - -def launch_uvicorn(webapp_factory, prefix=DEFAULT_CONFIG_PREFIX, galaxy_config=None, config_object=None): - name = prefix.lower() - - host, port = explicitly_configured_host_and_port(prefix, config_object) - port = attempt_ports(port) - gx_app = build_galaxy_app(galaxy_config) - - gx_wsgi_webapp = webapp_factory( + app = app_factory() + url_prefix = getattr(app.config, f"{name}_url_prefix", "/") + wsgi_webapp = webapp_factory( galaxy_config["global_conf"], - app=gx_app, + app=app, use_translogger=False, static_enabled=True, register_shutdown_at_exit=False, ) - from galaxy.webapps.galaxy.fast_app import initialize_fast_app + if name == "galaxy": + asgi_app = init_galaxy_fast_app(wsgi_webapp, app) + elif name == "tool_shed": + asgi_app = init_tool_shed_fast_app(wsgi_webapp) + else: + raise NotImplementedError(f"Launching {name} not implemented") - app = initialize_fast_app(gx_wsgi_webapp, gx_app) - server, port, thread = uvicorn_serve(app, host=host, port=port) - set_and_wait_for_http_target(prefix, host, port, url_prefix=gx_app.config.galaxy_url_prefix) - log.info(f"Embedded uvicorn web server for {name} started at {host}:{port}{gx_app.config.galaxy_url_prefix}") - return EmbeddedServerWrapper( - gx_app, server, name, host, port, thread=thread, prefix=gx_app.config.galaxy_url_prefix - ) - - -def launch_server(app, webapp_factory, kwargs, prefix=DEFAULT_CONFIG_PREFIX, config_object=None): - """Launch a web server for a given app using supplied factory. - - Consistently read either GALAXY_TEST_HOST and GALAXY_TEST_PORT or - TOOL_SHED_TEST_HOST and TOOL_SHED_TEST_PORT and ensure these are - all set after this method has been called. - """ - name = prefix.lower() - - host, port = explicitly_configured_host_and_port(prefix, config_object) - - webapp = webapp_factory( - kwargs["global_conf"], app=app, use_translogger=False, static_enabled=True, register_shutdown_at_exit=False - ) - server, port = serve_webapp(webapp, host=host, port=port) - set_and_wait_for_http_target(prefix, host, port, url_prefix="/") - log.info(f"Embedded paste web server for {name} started at {host}:{port}") - return EmbeddedServerWrapper(app, server, name, host, port) + server, port, thread = uvicorn_serve(asgi_app, host=host, port=port) + set_and_wait_for_http_target(prefix, host, port, url_prefix=url_prefix) + log.info(f"Embedded uvicorn web server for {name} started at {host}:{port}{url_prefix}") + return EmbeddedServerWrapper(app, server, name, host, port, thread=thread, prefix=url_prefix) class TestDriver: @@ -1003,29 +925,8 @@ class GalaxyTestDriver(TestDriver): config_object = self._ensure_config_object(config_object) self.external_galaxy = os.environ.get("GALAXY_TEST_EXTERNAL", None) - # Allow a particular test to force uwsgi or any test to use uwsgi with - # the GALAXY_TEST_UWSGI environment variable. - use_uwsgi = bool(os.environ.get("GALAXY_TEST_UWSGI", None)) - if not use_uwsgi: - if getattr(config_object, "require_uwsgi", None): - use_uwsgi = True - self.use_uwsgi = use_uwsgi - - if getattr(config_object, "use_uvicorn", USE_UVICORN): - self.else_use_uvicorn = True - else: - self.else_use_uvicorn = False - # Allow controlling the log format - log_format = os.environ.get("GALAXY_TEST_LOG_FORMAT", None) - if not log_format and use_uwsgi: - log_format = ( - "%(name)s %(levelname)-5.5s %(asctime)s " - "[p:%(process)s,w:%(worker_id)s,m:%(mule_id)s] " - "[%(threadName)s] %(message)s" - ) - - self.log_format = log_format + self.log_format = os.environ.get("GALAXY_TEST_LOG_FORMAT") self.galaxy_test_tmp_dir = get_galaxy_test_tmp_dir() self.temp_directories.append(self.galaxy_test_tmp_dir) @@ -1101,37 +1002,22 @@ class GalaxyTestDriver(TestDriver): if handle_galaxy_config_kwds is not None: handle_galaxy_config_kwds(galaxy_config) - if self.use_uwsgi: - server_wrapper = launch_uwsgi( - galaxy_config, - tempdir=tempdir, - config_object=config_object, - ) - elif self.else_use_uvicorn: - server_wrapper = launch_uvicorn( - lambda *args, **kwd: buildapp.app_factory(*args, wsgi_preflight=False, **kwd), - galaxy_config=galaxy_config, - config_object=config_object, - ) - self.app = server_wrapper.app - else: - # ---- Build Application -------------------------------------------------- - self.app = build_galaxy_app(galaxy_config) - server_wrapper = launch_server( - self.app, - buildapp.app_factory, - galaxy_config, - config_object=config_object, - ) - log.info( - f"Functional tests will be run against external Galaxy server {server_wrapper.host}:{server_wrapper.port}" - ) + server_wrapper = launch_server( + app_factory=lambda: self.build_galaxy_app(galaxy_config), + webapp_factory=lambda *args, **kwd: buildapp.app_factory(*args, wsgi_preflight=False, **kwd), + galaxy_config=galaxy_config, + config_object=config_object, + ) self.server_wrappers.append(server_wrapper) else: - log.info(f"Functional tests will be run against test managed Galaxy server {self.external_galaxy}") + log.info(f"Functional tests will be run against test external Galaxy server {self.external_galaxy}") # Ensure test file directory setup even though galaxy config isn't built. ensure_test_file_dir_set() + def build_galaxy_app(self, galaxy_config): + self.app = build_galaxy_app(galaxy_config) + return self.app + def _ensure_config_object(self, config_object): if config_object is None: config_object = self diff --git a/lib/galaxy_test/driver/integration_util.py b/lib/galaxy_test/driver/integration_util.py index ec1adf7aaed..ed2c8df1cc4 100644 --- a/lib/galaxy_test/driver/integration_util.py +++ b/lib/galaxy_test/driver/integration_util.py @@ -89,8 +89,6 @@ class IntegrationInstance(UsesApiTestCaseMixin): _app_available: ClassVar[bool] prefer_template_database = True - # Subclasses can override this to force uwsgi for tests. - require_uwsgi = False # Don't pull in default configs for un-configured things from Galaxy's # config directory and such. @@ -165,13 +163,6 @@ class IntegrationInstance(UsesApiTestCaseMixin): the test as needed. """ - @classmethod - def handle_uwsgi_cli_command(cls, command): - """Extension point sub subclasses to modify arguments used to launch uWSGI server. - - Command will a list that can be modified. - """ - def _run_tool_test(self, *args, **kwargs): return self._test_driver.run_tool_test(*args, **kwargs) @@ -181,8 +172,21 @@ class IntegrationInstance(UsesApiTestCaseMixin): return os.path.realpath(os.path.join(cls._test_driver.galaxy_test_tmp_dir, name)) +def setup_celery_includes(): + from galaxy.celery import TASKS_MODULES + + def celery_includes(): + return TASKS_MODULES + + return pytest.fixture(scope="session")(celery_includes) + + class UsesCeleryTasks: - enable_celery_tasks = True + @classmethod + def setup_celery_config(cls, config): + config["enable_celery_tasks"] = True + config["celery_broker"] = "memory://" + config["celery_backend"] = "cache+memory://" @pytest.fixture(autouse=True) def _request_celery_app(self, celery_app): diff --git a/lib/galaxy_test/selenium/framework.py b/lib/galaxy_test/selenium/framework.py index e5f787833a7..4df2499796f 100644 --- a/lib/galaxy_test/selenium/framework.py +++ b/lib/galaxy_test/selenium/framework.py @@ -412,6 +412,12 @@ class TestWithSeleniumMixin(GalaxyTestSeleniumContext, UsesApiTestCaseMixin): save_button.wait_for_and_click() self.sleep_for(self.wait_types.UX_RENDER) + @retry_assertion_during_transitions + def assert_modal_has_text(self, expected_text): + modal_element = self.components.workflow_editor.state_modal_body.wait_for_visible() + text = modal_element.text + assert expected_text in text, f"Failed to find expected text [{expected_text}] in modal text [{text}]" + def workflow_upload_yaml_with_random_name(self, content, **kwds): workflow_populator = self.workflow_populator name = self._get_random_name() @@ -712,3 +718,6 @@ class SeleniumSessionWorkflowPopulator( def upload_yaml_workflow(self, has_yaml, **kwds) -> str: workflow = convert_and_import_workflow(has_yaml, galaxy_interface=self, **kwds) return workflow["id"] + + +__all__ = ("retry_during_transitions",) diff --git a/lib/galaxy_test/selenium/test_collection_builders.py b/lib/galaxy_test/selenium/test_collection_builders.py index f72ee720b28..84cceb3bae0 100644 --- a/lib/galaxy_test/selenium/test_collection_builders.py +++ b/lib/galaxy_test/selenium/test_collection_builders.py @@ -185,19 +185,11 @@ class CollectionBuildersTestCase(SeleniumTestCase): depending on how fast the upload goes compared to the history polling updates, it might just skip to the end for a really fast upload """ - - if self.is_beta_history(): - for hid in hids: - timeout = self.wait_length(self.wait_types.JOB_COMPLETION) - row_selector = self.content_item_by_attributes(hid=hid, state="ok") - row = self.wait_for_present(row_selector, timeout=timeout) - row.send_keys(" ") - else: - for hid in hids: - self.history_panel_wait_for_hid_ok(hid) - self.history_panel_multi_operations_show() - for hid in hids: - self.history_panel_muli_operation_select_hid(hid) + for hid in hids: + self.history_panel_wait_for_hid_ok(hid) + self.history_panel_multi_operations_show() + for hid in hids: + self.history_panel_muli_operation_select_hid(hid) def _show_hidden_content(self): """Switches the beta hidden filter toggle on""" diff --git a/lib/galaxy_test/selenium/test_collection_edit.py b/lib/galaxy_test/selenium/test_collection_edit.py index 08106a04848..fcc2e934b7c 100644 --- a/lib/galaxy_test/selenium/test_collection_edit.py +++ b/lib/galaxy_test/selenium/test_collection_edit.py @@ -58,12 +58,11 @@ class CollectionEditTestCase(SeleniumTestCase): depending on how fast the upload goes compared to the history polling updates, it might just skip to the end for a really fast upload """ - for hid in hids: - timeout = self.wait_length(self.wait_types.JOB_COMPLETION) - row_selector = self.content_item_by_attributes(hid=hid, state="ok") - row = self.wait_for_present(row_selector, timeout=timeout) - row.send_keys(" ") + self.history_panel_wait_for_hid_ok(hid) + self.history_panel_multi_operations_show() + for hid in hids: + self.history_panel_muli_operation_select_hid(hid) def _collection_dropdown(self, option_description): return self.use_bootstrap_dropdown(option=option_description, menu="selected content menu") diff --git a/lib/galaxy_test/selenium/test_histories_list.py b/lib/galaxy_test/selenium/test_histories_list.py index 1b241d26993..3bf6dd6e222 100644 --- a/lib/galaxy_test/selenium/test_histories_list.py +++ b/lib/galaxy_test/selenium/test_histories_list.py @@ -259,7 +259,7 @@ class SavedHistoriesTestCase(SharedStateSeleniumTestCase): self.sleep_for(self.wait_types.UX_RENDER) def unset_filter(self, filter_key, filter_value): - close_button_selector = 'a[filter_key="%s"][filter_val="%s"]' % (filter_key, filter_value) + close_button_selector = f'a[filter_key="{filter_key}"][filter_val="{filter_value}"]' self.wait_for_and_click_selector(close_button_selector) self.sleep_for(self.wait_types.UX_RENDER) diff --git a/lib/galaxy_test/selenium/test_history_panel.py b/lib/galaxy_test/selenium/test_history_panel.py index 976cc70144c..9bb1903feb9 100644 --- a/lib/galaxy_test/selenium/test_history_panel.py +++ b/lib/galaxy_test/selenium/test_history_panel.py @@ -20,7 +20,7 @@ class HistoryPanelTestCase(SeleniumTestCase): name_element = self.history_panel_name_element() if self.is_beta_history(): # look for the editor icon - editor = self.components.history_panel.editor.selector(scope=".beta.history") + editor = self.components.history_panel.editor.selector(scope=".history-index") toggle = editor.toggle toggle.wait_for_visible() else: @@ -60,7 +60,7 @@ class HistoryPanelTestCase(SeleniumTestCase): @edit_details def test_history_tags_and_annotations_buttons(self): if self.is_beta_history(): - history_editor = self.components.history_panel.editor.selector(scope=".beta.history") + history_editor = self.components.history_panel.editor.selector(scope=".history-index") history_editor.annotation_input.wait_for_clickable() history_editor.tags_input.wait_for_clickable() else: @@ -158,7 +158,7 @@ class HistoryPanelTestCase(SeleniumTestCase): # looks like this is intended to check if the tag editor is open def assert_no_tags(): if self.is_beta_history(): - tags_component = self.components.history_panel.tag_editor.selector(scope=".beta.history") + tags_component = self.components.history_panel.tag_editor.selector(scope=".history-index") tags_component.display.assert_absent_or_hidden() else: self.components.history_panel.tag_area.assert_absent_or_hidden() @@ -172,18 +172,21 @@ class HistoryPanelTestCase(SeleniumTestCase): # add more tags to non-empty tags area tags += add_tags(tags_size) + self.sleep_for(self.wait_types.UX_RENDER) + if self.is_beta_history(): + tags.sort() assert_current_tags(tags) # delete all tags expected_tags_len = len(tags) self.clear_tags(expected_tags_len) - + self.sleep_for(self.wait_types.UX_RENDER) assert_no_tags() # after about 5 tags, a toggle link shows up and you have to click it to see the full list def open_tags(self): - tags_component = self.components.history_panel.tag_editor.selector(scope=".beta.history") - if not tags_component.toggle.is_absent: + tags_component = self.components.history_panel.tag_editor.selector(scope=".history-index") + if tags_component.tag_area.is_absent: tags_component.toggle.wait_for_and_click() tags_component.display.wait_for_visible() return tags_component.display @@ -192,7 +195,7 @@ class HistoryPanelTestCase(SeleniumTestCase): def clear_tags(self, expected_tags_size): if self.is_beta_history(): self.open_tags() - tags = self.components.history_panel.tag_editor.selector(scope=".beta.history") + tags = self.components.history_panel.tag_editor.selector(scope=".history-index") close_tag_buttons = tags.tag_close_btn.all() else: close_tag_buttons = self.components.history_panel.tag_close_btn.all() diff --git a/lib/galaxy_test/selenium/test_history_panel_collections.py b/lib/galaxy_test/selenium/test_history_panel_collections.py index e7efa96b4ee..7db94cf6fff 100644 --- a/lib/galaxy_test/selenium/test_history_panel_collections.py +++ b/lib/galaxy_test/selenium/test_history_panel_collections.py @@ -25,6 +25,8 @@ class HistoryPanelCollectionsTestCase(SeleniumTestCase): input_collection, failed_collection = self._generate_partially_failed_collection_with_input() input_hid = input_collection["hid"] failed_hid = failed_collection["hid"] + self.home() + ok_inputs = { "input1": {"batch": True, "values": [{"src": "hdca", "id": input_collection["id"]}]}, "sleep_time": 0, @@ -36,12 +38,6 @@ class HistoryPanelCollectionsTestCase(SeleniumTestCase): ) ok_hid = ok_response["implicit_collections"][0]["hid"] - if not self.is_beta_history(): - # sleep really shouldn't be needed :( - time.sleep(1) - - self.home() - self.history_panel_wait_for_hid_state(input_hid, "ok") self.history_panel_wait_for_hid_state(failed_hid, "error") self.history_panel_wait_for_hid_state(ok_hid, "ok") @@ -174,6 +170,7 @@ class HistoryPanelCollectionsTestCase(SeleniumTestCase): if self.is_beta_history(): self._rename_collection(new_name) self.screenshot("history_panel_collection_view_rename_beta") + self.sleep_for(WAIT_TYPES.UX_RENDER) title_element = self.beta_history_element("collection name display").wait_for_present() else: title_element = collection_view.title.wait_for_visible() @@ -219,15 +216,16 @@ class HistoryPanelCollectionsTestCase(SeleniumTestCase): input_collection = self._populated_paired_and_wait_for_it() collection_hid = input_collection["hid"] collection_view = self._click_and_wait_for_collection_view(collection_hid) - + self.sleep_for(WAIT_TYPES.UX_TRANSITION) if self.is_beta_history(): - self.sleep_for(WAIT_TYPES.HISTORY_POLL) - dataset_elements = self.content_item_by_attributes().all() + dataset_elements = collection_view.list_items_beta.all() else: dataset_elements = collection_view.list_items.all() - assert len(dataset_elements) == 2, dataset_elements - titles = [de.find_element_by_css_selector(".title .name").text for de in dataset_elements] + selector = ".title .name" + if self.is_beta_history(): + selector = ".content-title" + titles = [de.find_element_by_css_selector(selector).text for de in dataset_elements] assert titles == ["forward", "reverse"] self.screenshot("history_panel_collection_view_paired") @@ -249,7 +247,10 @@ class HistoryPanelCollectionsTestCase(SeleniumTestCase): else: dataset_elements = collection_view.list_items.all() assert len(dataset_elements) == 4, dataset_elements - title_elements = [de.find_element_by_css_selector(".title .name").text for de in dataset_elements] + selector = ".title .name" + if self.is_beta_history(): + selector = ".content-title" + title_elements = [de.find_element_by_css_selector(selector).text for de in dataset_elements] assert title_elements == ["data1", "data2", "data3", "data4"] check_four_datasets_shown() diff --git a/lib/galaxy_test/selenium/test_navigates_galaxy.py b/lib/galaxy_test/selenium/test_navigates_galaxy.py index 8e1f7c2fcc1..cff144710ce 100644 --- a/lib/galaxy_test/selenium/test_navigates_galaxy.py +++ b/lib/galaxy_test/selenium/test_navigates_galaxy.py @@ -1,7 +1,5 @@ -from galaxy.selenium.navigates_galaxy import ( - exception_indicates_not_clickable, - exception_seems_to_indicate_transition, -) +from galaxy.selenium.has_driver import exception_indicates_not_clickable +from galaxy.selenium.navigates_galaxy import exception_seems_to_indicate_transition from .framework import ( selenium_test, SeleniumTestCase, diff --git a/lib/galaxy_test/selenium/test_pages.py b/lib/galaxy_test/selenium/test_pages.py index 261171a19de..1a6f5cd3d50 100644 --- a/lib/galaxy_test/selenium/test_pages.py +++ b/lib/galaxy_test/selenium/test_pages.py @@ -18,22 +18,7 @@ class PagesTestCase(SeleniumTestCase): self.history_panel_wait_for_hid_ok(1) self.navigate_to_pages() self.screenshot("pages_grid") - - self.components.pages.create.wait_for_and_click() - name = self._get_random_name(prefix="page") - slug = self._get_random_name(prefix="pageslug") - self.tool_set_value("title", name) - self.tool_set_value("slug", slug) - self.tool_set_value("content_format", "HTML", expected_type="select") - self.screenshot("pages_create_form") - - # Sometimes 'submit' button not yet hooked up? - self.sleep_for(self.wait_types.UX_RENDER) - - self.components.pages.submit.wait_for_and_click() - - self.click_grid_popup_option(name, "Edit content") - self.components.pages.editor.wym_iframe.wait_for_visible() + name = self.create_page_and_edit(screenshot_name="pages_create_form") self.screenshot("pages_editor_new") self.driver.switch_to.frame(0) try: @@ -58,7 +43,3 @@ class PagesTestCase(SeleniumTestCase): self.navigate_to_pages() self.click_grid_popup_option(name, "View") self.screenshot("pages_view_simple") - - def navigate_to_pages(self): - self.click_masthead_user() # Open masthead menu - self.components.masthead.pages.wait_for_and_click() diff --git a/lib/galaxy_test/selenium/test_published_histories_grid.py b/lib/galaxy_test/selenium/test_published_histories_grid.py index 4496ad0024f..356443ab726 100644 --- a/lib/galaxy_test/selenium/test_published_histories_grid.py +++ b/lib/galaxy_test/selenium/test_published_histories_grid.py @@ -154,7 +154,7 @@ class HistoryGridTestCase(SharedStateSeleniumTestCase): self.send_enter(filter_input) def unset_filter(self, filter_key, filter_value): - close_link_selector = 'a[filter_key="%s"][filter_val="%s"]' % (filter_key, filter_value) + close_link_selector = f'a[filter_key="{filter_key}"][filter_val="{filter_value}"]' self.wait_for_and_click_selector(close_link_selector) self.sleep_for(self.wait_types.UX_RENDER) diff --git a/lib/galaxy_test/selenium/test_tool_form.py b/lib/galaxy_test/selenium/test_tool_form.py index 13176b6f1d6..8d4a9e4f1be 100644 --- a/lib/galaxy_test/selenium/test_tool_form.py +++ b/lib/galaxy_test/selenium/test_tool_form.py @@ -60,13 +60,12 @@ class ToolFormTestCase(SeleniumTestCase, UsesHistoryItemAssertions): job_outputs = self._table_to_key_value_elements("table#job-outputs") assert job_outputs[0][0].text == "environment_variables" - assert "View data\nEdit attributes\nDelete" in job_outputs[0][1].text - job_outputs[0][1].click() - assert job_outputs[0][1].find_element_by_css_selector("pre").text == "42\nmoo\nNOTTHREE" - dataset_operations = self.wait_for_selector_visible("table#job-outputs div.dropdown") - dataset_operations.click() - menu = self.wait_for_selector_visible("table#job-outputs div.dropdown ul") - self.click_menu_item(menu, "Run job again") + generic_item = job_outputs[0][1] + assert "1 : environment_variables" in generic_item.text + generic_item.click() + self.sleep_for(self.wait_types.UX_RENDER) + assert generic_item.find_element_by_css_selector("pre").text == "42\nmoo\nNOTTHREE" + generic_item.find_element_by_css_selector("[title='Run Job Again']").click() self.components.tool_form.execute.wait_for_visible() @staticmethod diff --git a/lib/galaxy_test/selenium/test_workflow_run.py b/lib/galaxy_test/selenium/test_workflow_run.py index 977cbbe5a24..28cb9aa82af 100644 --- a/lib/galaxy_test/selenium/test_workflow_run.py +++ b/lib/galaxy_test/selenium/test_workflow_run.py @@ -190,6 +190,34 @@ steps: content = self.dataset_populator.get_history_dataset_content(history_id, hid=7) self.assertEqual("10.0\n30.0\n20.0\n40.0\n", content) + @selenium_test + @managed_history + def test_execution_with_text_default_value_connected_to_restricted_select(self): + self.open_in_workflow_run( + """ +class: GalaxyWorkflow +inputs: + text_param: + optional: true + default: ex2 + restrictOnConnections: true + type: text +steps: + multi_select: + tool_id: multi_select + in: + select_ex: + source: text_param +""" + ) + element = self.components.workflow_run.input_select_field(label="text_param").wait_for_present() + assert element.text == "Ex2" + self.workflow_run_submit() + history_id = self.current_history_id() + self.workflow_populator.wait_for_history_workflows(history_id, expected_invocation_count=1) + content = self.dataset_populator.get_history_dataset_content(history_id, hid=1) + assert content == "ex2" + @selenium_test @managed_history def test_execution_with_rules(self): @@ -230,21 +258,6 @@ steps: history_id = self.current_history_id() self.workflow_populator.wait_for_history_workflows(history_id, expected_invocation_count=1) - @selenium_test - def test_simple_workflow_run_form_validation_required_text_param(self): - workflow_id = self.workflow_populator.upload_yaml_workflow( - """ -class: GalaxyWorkflow -inputs: - text_input: text -steps: [] -""" - ) - self.simplified_workflow_run_by_id(workflow_id) - run_button = self.components.workflow_run.run_workflow.wait_for_visible() - assert run_button.get_attribute("disabled") == "true" - self.components.workflow_run.validation_error.wait_for_visible() - def open_in_workflow_run(self, yaml_content): name = self.workflow_upload_yaml_with_random_name(yaml_content) self.workflow_run_with_name(name) @@ -271,9 +284,6 @@ steps: [] self.workflow_index_search_for(name) self.workflow_click_option(".workflow-run") - def simplified_workflow_run_by_id(self, workflow_id): - self.get(f"workflows/run?id={workflow_id}&simplified_workflow_run_ui=prefer") - def _assert_has_3_lines_after_run(self, hid): if self.is_beta_history(): self.content_item_by_attributes(hid=hid, state="ok").wait_for_present() diff --git a/lib/tool_shed/dependencies/attribute_handlers.py b/lib/tool_shed/dependencies/attribute_handlers.py index c3df2ecb2cc..726ab53c5a9 100644 --- a/lib/tool_shed/dependencies/attribute_handlers.py +++ b/lib/tool_shed/dependencies/attribute_handlers.py @@ -121,9 +121,9 @@ class RepositoryDependencyAttributeHandler: error_message = "Invalid latest installable changeset_revision %s " % str( lastest_installable_changeset_revision ) - error_message += f"retrieved for repository {str(name)} owned by {str(owner)}. " + error_message += f"retrieved for repository {name} owned by {owner}. " else: - error_message = f"Unable to locate repository with name {str(name)} and owner {str(owner)}. " + error_message = f"Unable to locate repository with name {name} and owner {owner}. " return altered, elem, error_message def handle_sub_elem(self, parent_elem, elem_index, elem): @@ -141,10 +141,7 @@ class RepositoryDependencyAttributeHandler: if sub_elem.tag == "repository": altered, new_sub_elem, message = self.handle_elem(sub_elem) if message: - error_message += "The %s file contains an invalid tag. %s" % ( - TOOL_DEPENDENCY_DEFINITION_FILENAME, - message, - ) + error_message += f"The {TOOL_DEPENDENCY_DEFINITION_FILENAME} file contains an invalid tag. {message}" if altered: if not sub_elem_altered: sub_elem_altered = True diff --git a/lib/tool_shed/dependencies/repository/relation_builder.py b/lib/tool_shed/dependencies/repository/relation_builder.py index 49ef4411468..39b34bd147e 100644 --- a/lib/tool_shed/dependencies/repository/relation_builder.py +++ b/lib/tool_shed/dependencies/repository/relation_builder.py @@ -272,7 +272,7 @@ class RelationBuilder: "The revision %s defined for repository %s owned by %s is invalid, so repository " % (str(rd_changeset_revision), str(rd_name), str(rd_owner)) ) - message += f"dependencies defined for repository {str(repository_name)} will be ignored." + message += f"dependencies defined for repository {repository_name} will be ignored." log.debug(message) else: repository_components_tuple = container_util.get_components_from_key(key) @@ -280,12 +280,8 @@ class RelationBuilder: repository_components_tuple ) toolshed, repository_name, repository_owner, repository_changeset_revision = components_list[0:4] - message = "The revision %s defined for repository %s owned by %s is invalid, so repository " % ( - str(rd_changeset_revision), - str(rd_name), - str(rd_owner), - ) - message += f"dependencies defined for repository {str(repository_name)} will be ignored." + message = f"The revision {rd_changeset_revision} defined for repository {rd_name} owned by {rd_owner} is invalid, " + message += f"so repository dependencies defined for repository {repository_name} will be ignored." log.debug(message) return updated_key_rd_dicts diff --git a/lib/tool_shed/dependencies/tool/tag_attribute_handler.py b/lib/tool_shed/dependencies/tool/tag_attribute_handler.py index baae6d23c3b..00221307ca4 100644 --- a/lib/tool_shed/dependencies/tool/tag_attribute_handler.py +++ b/lib/tool_shed/dependencies/tool/tag_attribute_handler.py @@ -141,12 +141,9 @@ class TagAttributeHandler: else: package_name = elem.get("name", "") package_version = elem.get("version", "") - error_message += "Version %s of the %s package cannot be installed because " % ( - str(package_version), - str(package_name), - ) - error_message += "the recipe for installing the package is missing either an " - error_message += "<actions> tag set or an <actions_group> tag set." + error_message += f"Version {package_version} of the {package_name} package cannot be " + error_message += "installed because the recipe for installing the package is missing " + error_message += "either an <actions> tag set or an <actions_group> tag set." if error_message and error_message not in message: message += error_message if altered: diff --git a/lib/tool_shed/galaxy_install/dependency_display.py b/lib/tool_shed/galaxy_install/dependency_display.py index ffd93158a7d..d49b057fe20 100644 --- a/lib/tool_shed/galaxy_install/dependency_display.py +++ b/lib/tool_shed/galaxy_install/dependency_display.py @@ -76,7 +76,7 @@ class DependencyDisplayer: repository_dependency_tup, contains_error=True ) if error: - message += f"{str(error)} " + message += f"{error} " else: # The complete dependency hierarchy could not be determined for a repository being installed into # Galaxy. This is likely due to invalid repository dependency definitions, so we'll get them from @@ -110,13 +110,7 @@ class DependencyDisplayer: pir_str = "True" else: pir_str = "" - message += "
    %s%s%s%s%s
    {tool_shed}{name}{owner}{changeset_revision}{pir_str}
    " return message @@ -134,7 +128,7 @@ class DependencyDisplayer: for requirement_dict in invalid_tool_dependencies.values(): error = requirement_dict.get("error", None) if error: - message = f"{str(error)} " + message = f"{error} " return message def generate_message_for_orphan_tool_dependencies(self, repository, metadata_dict): @@ -177,10 +171,7 @@ class DependencyDisplayer: if self.tool_dependency_is_orphan(type, name, None, tools): if not has_orphan_set_environment_dependencies: has_orphan_set_environment_dependencies = True - set_environment_orphans_str += "* name: %s, type: %s
    " % ( - str(name), - str(type), - ) + set_environment_orphans_str += f"* name: {name}, type: {type}
    " else: # "R/2.15.1": {"name": "R", "readme": "some string", "type": "package", "version": "2.15.1"} name = requirements_dict["name"] @@ -189,10 +180,8 @@ class DependencyDisplayer: if self.tool_dependency_is_orphan(type, name, version, tools): if not has_orphan_package_dependencies: has_orphan_package_dependencies = True - package_orphans_str += "* name: %s, type: %s, version: %s
    " % ( - str(name), - str(type), - str(version), + package_orphans_str += ( + f"* name: {name}, type: {type}, version: {version}
    " ) if has_orphan_package_dependencies: message += "The settings for name, version and type from a " @@ -358,7 +347,7 @@ class DependencyDisplayer: tool_dependencies_root_folder.folders.append(tool_dependencies_folder) new_containers_dict["tool_dependencies"] = tool_dependencies_root_folder except Exception as e: - log.debug(f"Exception in merge_containers_dicts_for_new_install: {str(e)}") + log.debug(f"Exception in merge_containers_dicts_for_new_install: {e}") finally: lock.release() return new_containers_dict diff --git a/lib/tool_shed/grids/repository_grids.py b/lib/tool_shed/grids/repository_grids.py index 017cbb56440..7df800a4409 100644 --- a/lib/tool_shed/grids/repository_grids.py +++ b/lib/tool_shed/grids/repository_grids.py @@ -149,10 +149,7 @@ class RepositoryGrid(grids.Grid): rval = "
      " if repository.categories: for rca in repository.categories: - rval += '
    • %s
    • ' % ( - trans.security.encode_id(rca.category.id), - rca.category.name, - ) + rval += f'
    • {rca.category.name}
    • ' else: rval += "
    • not set
    • " rval += "
    " diff --git a/lib/tool_shed/test/base/twilltestcase.py b/lib/tool_shed/test/base/twilltestcase.py index b69ba442a3e..6cd0d31df80 100644 --- a/lib/tool_shed/test/base/twilltestcase.py +++ b/lib/tool_shed/test/base/twilltestcase.py @@ -235,7 +235,7 @@ class ShedTwillTestCase(DrivenFunctionalTestCase): url += f"?{urlencode(params, doseq=doseq)}" new_url = tc.go(url) return_code = tc.browser.code - assert return_code in allowed_codes, "Invalid HTTP return code %s, allowed codes: %s" % ( + assert return_code in allowed_codes, "Invalid HTTP return code {}, allowed codes: {}".format( return_code, ", ".join(str(code) for code in allowed_codes), ) @@ -333,10 +333,9 @@ class ShedTwillTestCase(DrivenFunctionalTestCase): def check_galaxy_repository_db_status(self, repository_name, owner, expected_status): installed_repository = test_db_util.get_installed_repository_by_name_owner(repository_name, owner) - assert installed_repository.status == expected_status, "Status in database is %s, expected %s" % ( - installed_repository.status, - expected_status, - ) + assert ( + installed_repository.status == expected_status + ), f"Status in database is {installed_repository.status}, expected {expected_status}" def check_galaxy_repository_tool_panel_section(self, repository, expected_tool_panel_section): metadata = repository.metadata_ @@ -353,11 +352,7 @@ class ShedTwillTestCase(DrivenFunctionalTestCase): tool_panel_section = self.get_tool_panel_section_from_api(metadata) assert ( tool_panel_section == expected_tool_panel_section - ), "Expected to find tool panel section *%s*, but instead found *%s*\nMetadata: %s\n" % ( - expected_tool_panel_section, - tool_panel_section, - metadata, - ) + ), f"Expected to find tool panel section *{expected_tool_panel_section}*, but instead found *{tool_panel_section}*\nMetadata: {metadata}\n" def check_installed_repository_tool_dependencies( self, installed_repository, strings_displayed=None, strings_not_displayed=None, dependencies_installed=False @@ -398,7 +393,7 @@ class ShedTwillTestCase(DrivenFunctionalTestCase): else: assert ( len(self.get_repository_metadata_revisions(repository)) > 0 - ), "Repository tip is not a metadata revision: Repository tip - %s, metadata revisions - %s." % ( + ), "Repository tip is not a metadata revision: Repository tip - {}, metadata revisions - {}.".format( self.get_repository_tip(repository), ", ".join(self.get_repository_metadata_revisions(repository)), ) @@ -1224,7 +1219,7 @@ class ShedTwillTestCase(DrivenFunctionalTestCase): self.submit_form(form=form, button=submit_button, **kwds) self.check_for_strings(post_submit_strings_displayed, strings_not_displayed) repository_ids = self.initiate_installation_process(new_tool_panel_section_label=new_tool_panel_section_label) - log.debug(f"Waiting for the installation of repository IDs: {str(repository_ids)}") + log.debug(f"Waiting for the installation of repository IDs: {repository_ids}") self.wait_for_repository_installation(repository_ids) def load_citable_url( @@ -1463,7 +1458,7 @@ class ShedTwillTestCase(DrivenFunctionalTestCase): form_id = form.attrib.get("id") controls = [control for control in form.inputs if str(control.name) == field_name] if len(controls) > 0: - log.debug(f"Setting field {field_name} of form {form_id} to {str(field_value)}.") + log.debug(f"Setting field {field_name} of form {form_id} to {field_value}.") tc.formvalue(form_id, field_name, str(field_value)) kwd[field_name] = str(field_value) else: diff --git a/lib/tool_shed/test/functional/test_1230_uninstall_reinstall_repository_with_dependency_revisions.py b/lib/tool_shed/test/functional/test_1230_uninstall_reinstall_repository_with_dependency_revisions.py index 9f987463788..35f35a84bae 100644 --- a/lib/tool_shed/test/functional/test_1230_uninstall_reinstall_repository_with_dependency_revisions.py +++ b/lib/tool_shed/test/functional/test_1230_uninstall_reinstall_repository_with_dependency_revisions.py @@ -208,10 +208,7 @@ class UninstallingAndReinstallingRepositories(ShedTwillTestCase): else: assert ( current_datatypes == base_datatypes_count - ), "Installing emboss added new datatypes, current datatypes: %s, original datatypes: %s" % ( - str(current_datatypes), - str(base_datatypes_count), - ) + ), f"Installing emboss added new datatypes, current datatypes: {current_datatypes}, original datatypes: {base_datatypes_count}" def test_0015_uninstall_emboss_repository(self): """Uninstall the emboss repository.""" diff --git a/lib/tool_shed/test/functional/test_1300_reset_all_metadata.py b/lib/tool_shed/test/functional/test_1300_reset_all_metadata.py index eecf3f81602..895f54bf607 100644 --- a/lib/tool_shed/test/functional/test_1300_reset_all_metadata.py +++ b/lib/tool_shed/test/functional/test_1300_reset_all_metadata.py @@ -652,9 +652,4 @@ class TestResetInstalledRepositoryMetadata(ShedTwillTestCase): del old_metadata["tool_panel_section"] assert ( repository.metadata_ == old_metadata - ), "Metadata for %s repository %s changed after reset. \nOld: %s\nNew: %s" % ( - repository.status, - repository.name, - old_metadata, - repository.metadata_, - ) + ), f"Metadata for {repository.status} repository {repository.name} changed after reset. \nOld: {old_metadata}\nNew: {repository.metadata_}" diff --git a/lib/tool_shed/test/functional_tests.py b/lib/tool_shed/test/functional_tests.py index 4856de28b63..18a989c5a9d 100644 --- a/lib/tool_shed/test/functional_tests.py +++ b/lib/tool_shed/test/functional_tests.py @@ -14,7 +14,7 @@ galaxy_root = os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pa sys.path[0:1] = [os.path.join(galaxy_root, "lib")] # This is for the tool shed application. -from galaxy.web import buildapp as galaxybuildapp +from galaxy.webapps.galaxy.buildapp import app_factory as galaxy_app_factory from galaxy_test.driver import driver_util from tool_shed.webapp import buildapp as toolshedbuildapp @@ -111,16 +111,12 @@ class ToolShedTestDriver(driver_util.TestDriver): with open(shed_tool_data_table_conf_file, "w") as fh: fh.write(tool_data_table_conf_xml_template) os.environ["TOOL_SHED_TEST_TOOL_DATA_TABLE_CONF"] = shed_tool_data_table_conf_file - # ---- Build Tool Shed Application -------------------------------------------------- - toolshedapp = driver_util.build_shed_app(kwargs) - # ---- Run tool shed webserver ------------------------------------------------------ # TODO: Needed for hg middleware ('lib/galaxy/webapps/tool_shed/framework/middleware/hg.py') - kwargs["global_conf"]["database_connection"] = kwargs["database_connection"] tool_shed_server_wrapper = driver_util.launch_server( - toolshedapp, - toolshedbuildapp.app_factory, - kwargs, + app_factory=lambda: driver_util.build_shed_app(kwargs), + webapp_factory=toolshedbuildapp.app_factory, + galaxy_config=kwargs, prefix="TOOL_SHED", ) self.server_wrappers.append(tool_shed_server_wrapper) @@ -175,11 +171,10 @@ class ToolShedTestDriver(driver_util.TestDriver): print("Galaxy database connection:", kwargs["database_connection"]) # ---- Run galaxy webserver ------------------------------------------------------ - galaxyapp = driver_util.build_galaxy_app(kwargs) galaxy_server_wrapper = driver_util.launch_server( - galaxyapp, - galaxybuildapp.app_factory, - kwargs, + app_factory=lambda: driver_util.build_galaxy_app(kwargs), + webapp_factory=galaxy_app_factory, + galaxy_config=kwargs, ) log.info(f"Galaxy tests will be run against {galaxy_server_wrapper.host}:{galaxy_server_wrapper.port}") self.server_wrappers.append(galaxy_server_wrapper) diff --git a/lib/tool_shed/tools/data_table_manager.py b/lib/tool_shed/tools/data_table_manager.py index 0dafc864b1e..2797198a5b2 100644 --- a/lib/tool_shed/tools/data_table_manager.py +++ b/lib/tool_shed/tools/data_table_manager.py @@ -1 +1,9 @@ -from galaxy.tool_shed.tools.data_table_manager import * # noqa: F401,F403 +from galaxy.tool_shed.tools.data_table_manager import ( + ShedToolDataTableManager, + ToolDataTableManager, +) + +__all__ = ( + "ToolDataTableManager", + "ShedToolDataTableManager", +) diff --git a/lib/tool_shed/tools/tool_validator.py b/lib/tool_shed/tools/tool_validator.py index 1ed90229a70..862fbfdc969 100644 --- a/lib/tool_shed/tools/tool_validator.py +++ b/lib/tool_shed/tools/tool_validator.py @@ -6,12 +6,12 @@ import tempfile from galaxy.tool_shed.tools.tool_validator import ToolValidator as GalaxyToolValidator from galaxy.tools import Tool from galaxy.util import unicodify +from galaxy.util.tool_shed.xml_util import parse_xml from tool_shed.util import ( basic_util, hg_util, repository_util, tool_util, - xml_util, ) log = logging.getLogger(__name__) @@ -234,7 +234,7 @@ class ToolValidator(GalaxyToolValidator): message = "" tmp_tool_config = hg_util.get_named_tmpfile_from_ctx(ctx, ctx_file, work_dir) if tmp_tool_config: - tool_element, error_message = xml_util.parse_xml(tmp_tool_config) + tool_element, error_message = parse_xml(tmp_tool_config) if tool_element is None: return tool, message # Look for external files required by the tool config. diff --git a/lib/tool_shed/util/basic_util.py b/lib/tool_shed/util/basic_util.py index b0ceeff4562..001b5749e04 100644 --- a/lib/tool_shed/util/basic_util.py +++ b/lib/tool_shed/util/basic_util.py @@ -1 +1,37 @@ -from galaxy.tool_shed.util.basic_util import * # noqa: F401,F403 +from galaxy.tool_shed.util.basic_util import ( + CHUNK_SIZE, + DOCKER_IMAGE_TEMPLATE, + evaluate_template, + get_env_var_values, + get_file_type_str, + INSTALLATION_LOG, + MAX_DISPLAY_SIZE, + MAXDIFFSIZE, + move_file, + NO_OUTPUT_TIMEOUT, + remove_dir, + SELECTED_REPOSITORIES_TEMPLATE, + size_string, + stringify, + strip_path, + to_html_string, +) + +__all__ = ( + "CHUNK_SIZE", + "DOCKER_IMAGE_TEMPLATE", + "evaluate_template", + "get_env_var_values", + "get_file_type_str", + "INSTALLATION_LOG", + "MAX_DISPLAY_SIZE", + "MAXDIFFSIZE", + "NO_OUTPUT_TIMEOUT", + "move_file", + "remove_dir", + "SELECTED_REPOSITORIES_TEMPLATE", + "size_string", + "stringify", + "strip_path", + "to_html_string", +) diff --git a/lib/tool_shed/util/commit_util.py b/lib/tool_shed/util/commit_util.py index 25a1d6dc127..ad3fffef93b 100644 --- a/lib/tool_shed/util/commit_util.py +++ b/lib/tool_shed/util/commit_util.py @@ -184,7 +184,7 @@ def handle_directory_changes( repo_path = repository.repo_path(app) content_alert_str = "" files_to_remove = [] - filenames_in_archive = [os.path.join(full_path, name) for name in filenames_in_archive] + filenames_in_archive = [os.path.normpath(os.path.join(full_path, name)) for name in filenames_in_archive] if remove_repo_files_not_in_tar and not repository.is_new(): # We have a repository that is not new (it contains files), so discover those files that are in the # repository, but not in the uploaded archive. diff --git a/lib/tool_shed/util/common_util.py b/lib/tool_shed/util/common_util.py index 6e6f78e5720..a73f6283a05 100644 --- a/lib/tool_shed/util/common_util.py +++ b/lib/tool_shed/util/common_util.py @@ -1 +1,47 @@ -from galaxy.util.tool_shed.common_util import * # noqa: F401,F403 +from galaxy.util.tool_shed.common_util import ( + accumulate_tool_dependencies, + check_for_missing_tools, + check_tool_tag_set, + generate_clone_url_for_installed_repository, + generate_clone_url_for_repository_in_tool_shed, + generate_clone_url_from_repo_info_tup, + get_non_shed_tool_panel_configs, + get_protocol_from_tool_shed_url, + get_repository_dependencies, + get_tool_dependencies, + get_tool_shed_repository_ids, + get_tool_shed_repository_url, + get_tool_shed_url_from_tool_shed_registry, + get_user_by_username, + handle_galaxy_url, + handle_tool_shed_url_protocol, + parse_repository_dependency_tuple, + remove_port_from_tool_shed_url, + remove_protocol_and_port_from_tool_shed_url, + remove_protocol_and_user_from_clone_url, + remove_protocol_from_tool_shed_url, +) + +__all__ = ( + "accumulate_tool_dependencies", + "check_for_missing_tools", + "check_tool_tag_set", + "generate_clone_url_for_installed_repository", + "generate_clone_url_for_repository_in_tool_shed", + "generate_clone_url_from_repo_info_tup", + "get_non_shed_tool_panel_configs", + "get_repository_dependencies", + "get_protocol_from_tool_shed_url", + "get_tool_dependencies", + "get_tool_shed_repository_ids", + "get_tool_shed_url_from_tool_shed_registry", + "get_tool_shed_repository_url", + "get_user_by_username", + "handle_galaxy_url", + "handle_tool_shed_url_protocol", + "parse_repository_dependency_tuple", + "remove_port_from_tool_shed_url", + "remove_protocol_and_port_from_tool_shed_url", + "remove_protocol_and_user_from_clone_url", + "remove_protocol_from_tool_shed_url", +) diff --git a/lib/tool_shed/util/hg_util.py b/lib/tool_shed/util/hg_util.py index 34aa49587ac..fdfeb068b3e 100644 --- a/lib/tool_shed/util/hg_util.py +++ b/lib/tool_shed/util/hg_util.py @@ -170,14 +170,14 @@ def get_rev_label_changeset_revision_from_repository_metadata( if include_date: changeset_revision_date = get_readable_ctx_date(ctx) if include_hash: - label = f"{str(ctx.rev())}:{changeset_revision} ({changeset_revision_date})" + label = f"{ctx.rev()}:{changeset_revision} ({changeset_revision_date})" else: - label = f"{str(ctx.rev())} ({changeset_revision_date})" + label = f"{ctx.rev()} ({changeset_revision_date})" else: if include_hash: - label = f"{str(ctx.rev())}:{changeset_revision}" + label = f"{ctx.rev()}:{changeset_revision}" else: - label = f"{str(ctx.rev())}" + label = f"{ctx.rev()}" else: rev = "-1" if include_hash: @@ -190,16 +190,12 @@ def get_rev_label_changeset_revision_from_repository_metadata( def get_revision_label_from_ctx(ctx, include_date=True, include_hash=True): if include_date: if include_hash: - return '%s:%s (%s)' % ( - str(ctx.rev()), - str(ctx), - str(get_readable_ctx_date(ctx)), - ) + return f'{ctx.rev()}:{ctx} ({get_readable_ctx_date(ctx)})' else: - return '%s (%s)' % (str(ctx.rev()), str(get_readable_ctx_date(ctx))) + return f'{ctx.rev()} ({get_readable_ctx_date(ctx)})' else: if include_hash: - return f"{str(ctx.rev())}:{str(ctx)}" + return f"{ctx.rev()}:{ctx}" else: return str(ctx.rev()) diff --git a/lib/tool_shed/util/tool_util.py b/lib/tool_shed/util/tool_util.py index 6e634ea91d2..63b85802f6a 100644 --- a/lib/tool_shed/util/tool_util.py +++ b/lib/tool_shed/util/tool_util.py @@ -1 +1,25 @@ -from galaxy.tool_shed.util.tool_util import * # noqa: F401,F403 +from galaxy.tool_shed.util.tool_util import ( + build_shed_tool_conf_select_field, + build_tool_panel_section_select_field, + copy_sample_file, + copy_sample_files, + generate_message_for_invalid_tools, + get_tool_path_install_dir, + handle_missing_index_file, + is_data_index_sample_file, + new_state, + panel_entry_per_tool, +) + +__all__ = ( + "build_shed_tool_conf_select_field", + "build_tool_panel_section_select_field", + "copy_sample_file", + "copy_sample_files", + "generate_message_for_invalid_tools", + "get_tool_path_install_dir", + "handle_missing_index_file", + "is_data_index_sample_file", + "new_state", + "panel_entry_per_tool", +) diff --git a/lib/tool_shed/util/xml_util.py b/lib/tool_shed/util/xml_util.py index 61405712e31..1944d9eae02 100644 --- a/lib/tool_shed/util/xml_util.py +++ b/lib/tool_shed/util/xml_util.py @@ -1 +1,11 @@ -from galaxy.util.tool_shed.xml_util import * # noqa: F401,F403 +from galaxy.util.tool_shed.xml_util import ( + create_and_write_tmp_file, + create_element, + parse_xml, +) + +__all__ = ( + "create_and_write_tmp_file", + "create_element", + "parse_xml", +) diff --git a/lib/tool_shed/utility_containers/utility_container_manager.py b/lib/tool_shed/utility_containers/utility_container_manager.py index 5db3f1294b0..43e9704c8e2 100644 --- a/lib/tool_shed/utility_containers/utility_container_manager.py +++ b/lib/tool_shed/utility_containers/utility_container_manager.py @@ -766,12 +766,7 @@ class UtilityContainerManager: prior_installation_required_str = " (prior install required)" else: prior_installation_required_str = "" - label = "Repository %s revision %s owned by %s%s" % ( - repository_name, - changeset_revision, - repository_owner, - prior_installation_required_str, - ) + label = f"Repository {repository_name} revision {changeset_revision} owned by {repository_owner}{prior_installation_required_str}" return label def get_components_from_repository_dependency_for_installed_repository(self, repository_dependency): diff --git a/lib/tool_shed/webapp/api/repositories.py b/lib/tool_shed/webapp/api/repositories.py index 8bd824fba4e..17e7ad9989d 100644 --- a/lib/tool_shed/webapp/api/repositories.py +++ b/lib/tool_shed/webapp/api/repositories.py @@ -84,7 +84,7 @@ class RepositoriesController(BaseAPIController): raise HTTPBadRequest(detail="Missing required parameter 'owner'.") repository = repository_util.get_repository_by_name_and_owner(self.app, name, owner) if repository is None: - error_message = f"Cannot locate repository with name {str(name)} and owner {str(owner)}," + error_message = f"Cannot locate repository with name {name} and owner {owner}," log.debug(error_message) response_dict["status"] = "error" response_dict["message"] = error_message @@ -94,10 +94,7 @@ class RepositoriesController(BaseAPIController): response_dict["status"] = "ok" response_dict[ "message" - ] = "Entries for repository %s owned by %s have been added to the Tool Shed repository registry." % ( - name, - owner, - ) + ] = f"Entries for repository {name} owned by {owner} have been added to the Tool Shed repository registry." return response_dict @web.legacy_expose_api_anonymous @@ -219,7 +216,7 @@ class RepositoriesController(BaseAPIController): self.app, name, owner, eagerload_columns=["downloadable_revisions"] ) if repository is None: - log.debug(f"Cannot locate repository {str(name)} owned by {str(owner)}") + log.debug(f"Cannot locate repository {name} owned by {owner}") return {}, {}, {} encoded_repository_id = trans.security.encode_id(repository.id) repository_dict = repository.to_dict(view="element", value_mapper=self.__get_value_mapper(trans)) @@ -266,11 +263,7 @@ class RepositoriesController(BaseAPIController): return repository_dict, {}, {} else: debug_msg = "Error in the Tool Shed repositories API in get_repository_revision_install_info: " - debug_msg += "Invalid name %s or owner %s or changeset_revision %s received." % ( - str(name), - str(owner), - str(changeset_revision), - ) + debug_msg += f"Invalid name {name} or owner {owner} or changeset_revision {changeset_revision} received." log.debug(debug_msg) return {}, {}, {} @@ -521,7 +514,7 @@ class RepositoriesController(BaseAPIController): raise HTTPBadRequest(detail="Missing required parameter 'owner'.") repository = repository_util.get_repository_by_name_and_owner(self.app, name, owner) if repository is None: - error_message = f"Cannot locate repository with name {str(name)} and owner {str(owner)}," + error_message = f"Cannot locate repository with name {name} and owner {owner}," log.debug(error_message) response_dict["status"] = "error" response_dict["message"] = error_message @@ -531,10 +524,7 @@ class RepositoriesController(BaseAPIController): response_dict["status"] = "ok" response_dict[ "message" - ] = "Entries for repository %s owned by %s have been removed from the Tool Shed repository registry." % ( - name, - owner, - ) + ] = f"Entries for repository {name} owned by {owner} have been removed from the Tool Shed repository registry." return response_dict @web.legacy_expose_api @@ -594,7 +584,7 @@ class RepositoriesController(BaseAPIController): """ def handle_repository(trans, rmm, repository, results): - log.debug(f"Resetting metadata on repository {str(repository.name)}") + log.debug(f"Resetting metadata on repository {repository.name}") try: rmm.set_repository(repository) rmm.reset_all_metadata_on_repository_in_tool_shed() @@ -605,19 +595,14 @@ class RepositoriesController(BaseAPIController): ) results["unsuccessful_count"] += 1 else: - message = "Successfully reset metadata on repository %s owned by %s" % ( - str(repository.name), - str(repository.user.username), - ) + message = f"Successfully reset metadata on repository {repository.name} owned by {repository.user.username}" results["successful_count"] += 1 except Exception as e: - message = "Error resetting metadata on repository %s owned by %s: %s" % ( - str(repository.name), - str(repository.user.username), - util.unicodify(e), + message = ( + f"Error resetting metadata on repository {repository.name} owned by {repository.user.username}: {e}" ) results["unsuccessful_count"] += 1 - status = f"{str(repository.name)} : {message}" + status = f"{repository.name} : {message}" results["repository_status"].append(status) return results @@ -703,19 +688,14 @@ class RepositoriesController(BaseAPIController): ) results["status"] = "warning" else: - message = "Successfully reset metadata on repository %s owned by %s" % ( - str(repository.name), - str(repository.user.username), - ) + message = f"Successfully reset metadata on repository {repository.name} owned by {repository.user.username}" results["status"] = "ok" except Exception as e: - message = "Error resetting metadata on repository %s owned by %s: %s" % ( - str(repository.name), - str(repository.user.username), - util.unicodify(e), + message = ( + f"Error resetting metadata on repository {repository.name} owned by {repository.user.username}: {e}" ) results["status"] = "error" - status = f"{str(repository.name)} : {message}" + status = f"{repository.name} : {message}" results["repository_status"].append(status) return results @@ -723,7 +703,7 @@ class RepositoriesController(BaseAPIController): if repository_id is not None: repository = repository_util.get_repository_in_tool_shed(self.app, repository_id) start_time = strftime("%Y-%m-%d %H:%M:%S") - log.debug(f"{start_time}...resetting metadata on repository {str(repository.name)}") + log.debug(f"{start_time}...resetting metadata on repository {repository.name}") results = handle_repository(trans, start_time, repository) stop_time = strftime("%Y-%m-%d %H:%M:%S") results["stop_time"] = stop_time diff --git a/lib/tool_shed/webapp/api/repository_revisions.py b/lib/tool_shed/webapp/api/repository_revisions.py index 591b07d8d6c..da37cfd71e8 100644 --- a/lib/tool_shed/webapp/api/repository_revisions.py +++ b/lib/tool_shed/webapp/api/repository_revisions.py @@ -88,11 +88,11 @@ class RepositoryRevisionsController(BaseAPIController): repository_dependencies_dicts = [] repository_metadata = metadata_util.get_repository_metadata_by_id(trans.app, id) if repository_metadata is None: - log.debug(f"Invalid repository_metadata id received: {str(id)}") + log.debug(f"Invalid repository_metadata id received: {id}") return repository_dependencies_dicts metadata = repository_metadata.metadata if metadata is None: - log.debug(f"The repository_metadata record with id {str(id)} has no metadata.") + log.debug(f"The repository_metadata record with id {id} has no metadata.") return repository_dependencies_dicts if "repository_dependencies" in metadata: rd_tups = metadata["repository_dependencies"]["repository_dependencies"] @@ -125,10 +125,7 @@ class RepositoryRevisionsController(BaseAPIController): "Cannot locate repository_metadata with id %d for repository dependency %s owned by %s " % (decoded_repository_dependency_id, str(name), str(owner)) ) - debug_msg += "using either of these changeset_revisions: %s, %s." % ( - str(changeset_revision), - str(new_changeset_revision), - ) + debug_msg += f"using either of these changeset_revisions: {changeset_revision}, {new_changeset_revision}." log.debug(debug_msg) continue repository_dependency_metadata_dict = repository_dependency_repository_metadata.to_dict( @@ -163,7 +160,7 @@ class RepositoryRevisionsController(BaseAPIController): # Example URL: http://localhost:9009/api/repository_revisions/bb125606ff9ea620 repository_metadata = metadata_util.get_repository_metadata_by_id(trans.app, id) if repository_metadata is None: - log.debug(f"Cannot locate repository_metadata with id {str(id)}") + log.debug(f"Cannot locate repository_metadata with id {id}") return {} encoded_repository_id = trans.security.encode_id(repository_metadata.repository_id) repository_metadata_dict = repository_metadata.to_dict( @@ -188,7 +185,7 @@ class RepositoryRevisionsController(BaseAPIController): repository_metadata = metadata_util.get_repository_metadata_by_id(trans.app, repository_metadata_id) if repository_metadata is None: decoded_repository_metadata_id = trans.security.decode_id(repository_metadata_id) - log.debug(f"Cannot locate repository_metadata with id {str(decoded_repository_metadata_id)}") + log.debug(f"Cannot locate repository_metadata with id {decoded_repository_metadata_id}") return {} else: decoded_repository_metadata_id = repository_metadata.id diff --git a/lib/tool_shed/webapp/app.py b/lib/tool_shed/webapp/app.py index 9f1c1671632..04d030c9b25 100644 --- a/lib/tool_shed/webapp/app.py +++ b/lib/tool_shed/webapp/app.py @@ -13,6 +13,7 @@ import tool_shed.webapp.model from galaxy import auth from galaxy.app import SentryClientMixin from galaxy.config import configure_logging +from galaxy.managers.api_keys import ApiKeyManager from galaxy.managers.citations import CitationsManager from galaxy.managers.users import UserManager from galaxy.model.base import SharedModelMapping @@ -75,7 +76,8 @@ class UniverseApplication(BasicSharedApp, SentryClientMixin): self._register_singleton(SharedModelMapping, model) self._register_singleton(mapping.ToolShedModelMapping, model) self._register_singleton(scoped_session, self.model.context) - self._register_singleton(UserManager, UserManager) + self.user_manager = self._register_singleton(UserManager, UserManager) + self.api_keys_manager = self._register_singleton(ApiKeyManager) # initialize the Tool Shed tag handler. self.tag_handler = CommunityTagHandler(self) # Initialize the Tool Shed tool data tables. Never pass a configuration file here diff --git a/lib/tool_shed/webapp/buildapp.py b/lib/tool_shed/webapp/buildapp.py index 1c165d32ef5..5e741b2a734 100644 --- a/lib/tool_shed/webapp/buildapp.py +++ b/lib/tool_shed/webapp/buildapp.py @@ -15,6 +15,7 @@ import galaxy.webapps.base.webapp from galaxy import util from galaxy.util import asbool from galaxy.util.properties import load_app_properties +from galaxy.webapps.base.webapp import build_url_map from galaxy.webapps.util import wrap_if_allowed log = logging.getLogger(__name__) @@ -204,6 +205,8 @@ def app_factory(global_conf, load_app_kwds=None, **kwargs): # Wrap the webapp in some useful middleware if kwargs.get("middleware", True): webapp = wrap_in_middleware(webapp, global_conf, app.application_stack, **kwargs) + if asbool(kwargs.get("static_enabled", True)): + webapp = wrap_if_allowed(webapp, app.application_stack, build_url_map, args=(global_conf,), kwargs=kwargs) return webapp @@ -306,7 +309,3 @@ def _map_redirects(mapper): conditions=dict(function=forward_qs), ) return mapper - - -def uwsgi_app(): - return galaxy.webapps.base.webapp.build_native_uwsgi_app(app_factory, "tool_shed") diff --git a/lib/tool_shed/webapp/config.py b/lib/tool_shed/webapp/config.py index d9b7fac51bb..ec22c7ff514 100644 --- a/lib/tool_shed/webapp/config.py +++ b/lib/tool_shed/webapp/config.py @@ -10,7 +10,9 @@ from datetime import timedelta from galaxy.config import ( BaseAppConfiguration, CommonConfigurationMixin, + expand_pretty_datetime_format, get_database_engine_options, + TOOL_SHED_CONFIG_SCHEMA_PATH, ) from galaxy.config.schema import AppSchema from galaxy.exceptions import ConfigurationError @@ -20,18 +22,10 @@ from galaxy.version import ( VERSION_MAJOR, VERSION_MINOR, ) -from galaxy.web.formatting import expand_pretty_datetime_format - -try: - from importlib.resources import files # type: ignore[attr-defined] -except ImportError: - # Python < 3.9 - from importlib_resources import files # type: ignore[no-redef] log = logging.getLogger(__name__) TOOLSHED_APP_NAME = "tool_shed" -TOOLSHED_CONFIG_SCHEMA_PATH = files("tool_shed.webapp") / "config_schema.yml" class ToolShedAppConfiguration(BaseAppConfiguration, CommonConfigurationMixin): @@ -40,7 +34,7 @@ class ToolShedAppConfiguration(BaseAppConfiguration, CommonConfigurationMixin): add_sample_file_to_defaults = {"datatypes_config_file"} def _load_schema(self): - return AppSchema(TOOLSHED_CONFIG_SCHEMA_PATH, TOOLSHED_APP_NAME) + return AppSchema(TOOL_SHED_CONFIG_SCHEMA_PATH, TOOLSHED_APP_NAME) def __init__(self, **kwargs): super().__init__(**kwargs) diff --git a/lib/tool_shed/webapp/controllers/hg.py b/lib/tool_shed/webapp/controllers/hg.py index c07e2c665bb..c991fb5eb14 100644 --- a/lib/tool_shed/webapp/controllers/hg.py +++ b/lib/tool_shed/webapp/controllers/hg.py @@ -1,7 +1,6 @@ import logging from mercurial.hgweb.hgwebdir_mod import hgwebdir -from mercurial.hgweb.request import wsgiapplication from galaxy import web from galaxy.exceptions import ObjectNotFound @@ -11,6 +10,15 @@ from tool_shed.util.repository_util import get_repository_by_name_and_owner log = logging.getLogger(__name__) +class PortAsStringMiddleware: + def __init__(self, application): + self.application = application + + def __call__(self, environ, start_response): + environ["SERVER_PORT"] = str(environ["SERVER_PORT"]) + return self.application(environ, start_response) + + class HgController(BaseUIController): @web.expose def handle_request(self, trans, **kwd): @@ -22,7 +30,7 @@ class HgController(BaseUIController): hgwebapp = hgwebdir(hgweb_config.encode("utf-8")) return hgwebapp - wsgi_app = wsgiapplication(make_web_app) + wsgi_app = make_web_app() repository = None path_info = kwd.get("path_info", None) if path_info and len(path_info.split("/")) == 2: @@ -38,4 +46,4 @@ class HgController(BaseUIController): repository.times_downloaded = times_downloaded trans.sa_session.add(repository) trans.sa_session.flush() - return wsgi_app + return PortAsStringMiddleware(wsgi_app) diff --git a/lib/tool_shed/webapp/controllers/repository_review.py b/lib/tool_shed/webapp/controllers/repository_review.py index adf8ebba3c2..02a820ff555 100644 --- a/lib/tool_shed/webapp/controllers/repository_review.py +++ b/lib/tool_shed/webapp/controllers/repository_review.py @@ -200,10 +200,7 @@ class RepositoryReviewController(BaseUIController, ratings_util.ItemRatings): review_to_copy = review_util.get_review(trans.app, previous_review_id) self.copy_review(trans, review_to_copy, review) review_id = trans.security.encode_id(review.id) - message = "Begin your review of revision %s of repository %s." % ( - changeset_revision, - repository.name, - ) + message = f"Begin your review of revision {changeset_revision} of repository {repository.name}." status = "done" trans.response.send_redirect( web.url_for( diff --git a/lib/tool_shed/webapp/controllers/upload.py b/lib/tool_shed/webapp/controllers/upload.py index 5b635be5e33..c65e42ac738 100644 --- a/lib/tool_shed/webapp/controllers/upload.py +++ b/lib/tool_shed/webapp/controllers/upload.py @@ -267,11 +267,7 @@ class UploadController(BaseUIController): else: source_type = "file" source = uploaded_file_filename - message = "The %s %s has been successfully%suploaded to the repository. " % ( - source_type, - escape(source), - uncompress_str, - ) + message = f"The {source_type} {escape(source)} has been successfully{uncompress_str}uploaded to the repository. " if istar and (undesirable_dirs_removed or undesirable_files_removed): items_removed = undesirable_dirs_removed + undesirable_files_removed message += ( diff --git a/lib/tool_shed/webapp/fast_app.py b/lib/tool_shed/webapp/fast_app.py index 488b7c6de7a..eda2deeb7e1 100644 --- a/lib/tool_shed/webapp/fast_app.py +++ b/lib/tool_shed/webapp/fast_app.py @@ -1,5 +1,5 @@ +from a2wsgi import WSGIMiddleware from fastapi import FastAPI -from fastapi.middleware.wsgi import WSGIMiddleware from galaxy.webapps.base.api import ( add_exception_handler, @@ -20,3 +20,9 @@ def initialize_fast_app(gx_webapp): wsgi_handler = WSGIMiddleware(gx_webapp) app.mount("/", wsgi_handler) return app + + +__all__ = ( + "add_request_id_middleware", + "initialize_fast_app", +) diff --git a/lib/tool_shed/webapp/fast_factory.py b/lib/tool_shed/webapp/fast_factory.py index 6b7285dc0e1..73c9c40d625 100644 --- a/lib/tool_shed/webapp/fast_factory.py +++ b/lib/tool_shed/webapp/fast_factory.py @@ -30,7 +30,7 @@ with the following command-line. :: - gunicorn 'tool_shed.webapp.fast_factory:factory()' --env TOOL_SHED_CONFIG_FILE=config/tool_shed.yml --pythonpath lib -w 4 -k uvicorn.workers.UvicornWorker + gunicorn 'tool_shed.webapp.fast_factory:factory()' --env TOOL_SHED_CONFIG_FILE=config/tool_shed.yml --pythonpath lib -w 4 -k uvicorn.workers.UvicornWorker --config lib/galaxy/web_stack/gunicorn_config.py """ diff --git a/lib/tool_shed/webapp/model/__init__.py b/lib/tool_shed/webapp/model/__init__.py index 07dd38feac5..52def04404b 100644 --- a/lib/tool_shed/webapp/model/__init__.py +++ b/lib/tool_shed/webapp/model/__init__.py @@ -39,6 +39,7 @@ from galaxy.model.custom_types import ( TrimmedString, ) from galaxy.model.orm.now import now +from galaxy.model.orm.util import add_object_to_object_session from galaxy.security.validate_user_input import validate_password_str from galaxy.util import unique_id from galaxy.util.bunch import Bunch @@ -182,6 +183,7 @@ class PasswordResetToken(Base, _HasTable): self.token = token else: self.token = unique_id() + add_object_to_object_session(self, user) self.user = user self.expiration_time = now() + timedelta(hours=24) @@ -252,6 +254,7 @@ class UserGroupAssociation(Base, _HasTable): group = relationship("Group", back_populates="users") def __init__(self, user, group): + add_object_to_object_session(self, user) self.user = user self.group = group @@ -268,7 +271,9 @@ class UserRoleAssociation(Base, _HasTable): role = relationship("Role", back_populates="users") def __init__(self, user, role): + add_object_to_object_session(self, user) self.user = user + add_object_to_object_session(self, role) self.role = role @@ -300,6 +305,7 @@ class RepositoryRoleAssociation(Base, _HasTable): role = relationship("Role", back_populates="repositories") def __init__(self, repository, role): + add_object_to_object_session(self, repository) self.repository = repository self.role = role diff --git a/lib/tool_shed/webapp/uwsgi_schema.yml b/lib/tool_shed/webapp/uwsgi_schema.yml deleted file mode 120000 index 3710028ca76..00000000000 --- a/lib/tool_shed/webapp/uwsgi_schema.yml +++ /dev/null @@ -1 +0,0 @@ -../../galaxy/config/schemas/uwsgi_schema.yml \ No newline at end of file diff --git a/manage_db.sh b/manage_db.sh index b5a185ab573..080732a20ac 100755 --- a/manage_db.sh +++ b/manage_db.sh @@ -1,15 +1,49 @@ #!/bin/sh ####### -# NOTE: To downgrade to a specific version, use something like: -# sh manage_db.sh downgrade --version=3 +# The purpose of this script is to preserve the legacy interface provided by +# manage_db.sh, which was a thin wrapper around SQLAlchemy Migrate. Unless you +# need to use the legacy interface, to manage migrations of the "galaxy" and +# "install" databases, you are encouraged to use run_alembic.sh directly and +# take advantage of Alembic's command line options. +# +# Use this script to upgrade or downgrade your database. +# Database options: galaxy (default), install, tool_shed +# To pass a galaxy config file, you may use `-c|--config|--config-file your-config-file` + +# To upgrade or downgrade to some version X: +# sh manage_db.sh [upgrade|downgrade] --version=X [tool_shed|install|galaxy] +# +# You may also skip the version argument when upgrading, in which case the database +# will be upgraded to the latest version. +# +# Example 1: upgrade "galaxy" database to version "abc123" using default config: +# sh manage_db.sh upgrade --version=xyz567 +# +# Example 2: downgrade "install" database to version "xyz789" passing config file "mygalaxy.yml": +# sh manage_db.sh downgrade --version=abc123 -c mygalaxy.yml install +# +# Example 3: upgrade "galaxy" database to latest version using defualt config: +# sh manage_db.sh upgrade +# +# (Note: Tool Shed migrations use the legacy migrations system, so we check the +# last argument (the database) to invoke the appropriate script. Therefore, if +# you don't specify the database (galaxy is used by default) and pass a config +# file, your config file should not be named `tool_shed`.) ####### +ALEMBIC_CONFIG='lib/galaxy/model/migrations/alembic.ini' + cd `dirname $0` . ./scripts/common_startup_functions.sh setup_python -find lib/galaxy/model/migrate/versions -name '*.pyc' -delete -python ./scripts/manage_db.py $@ +for i; do :; done +if [ "$i" = "tool_shed" ]; then + python ./scripts/migrate_toolshed_db.py "$@" tool_shed +else + find lib/galaxy/model/migrations/alembic -name '*.pyc' -delete + python ./scripts/manage_db_adapter.py --alembic-config "$ALEMBIC_CONFIG" "$@" +fi diff --git a/packages/app/MANIFEST.in b/packages/app/MANIFEST.in index a4acb5c99e4..fc212e2a6ae 100644 --- a/packages/app/MANIFEST.in +++ b/packages/app/MANIFEST.in @@ -1,5 +1,2 @@ include *.rst *.txt LICENSE -include galaxy/config/schemas/*.yml -include galaxy/config/sample/*.sample* include galaxy/jobs/runners/util/job_script/*.sh -include galaxy/tools/*tsv diff --git a/packages/app/galaxy/project_galaxy_app.py b/packages/app/galaxy/project_galaxy_app.py index 97e3da53372..4a8f846726d 100644 --- a/packages/app/galaxy/project_galaxy_app.py +++ b/packages/app/galaxy/project_galaxy_app.py @@ -6,4 +6,4 @@ PROJECT_URL = "https://github.com/galaxyproject/galaxy" PROJECT_AUTHOR = "Galaxy Project and Community" PROJECT_DESCRIPTION = "Galaxy Application (backend)" PROJECT_EMAIL = "galaxy-committers@lists.galaxyproject.org" -RAW_CONTENT_URL = "https://raw.github.com/{}/{}/master/".format(PROJECT_USERAME, PROJECT_NAME) +RAW_CONTENT_URL = f"https://raw.github.com/{PROJECT_USERAME}/{PROJECT_NAME}/master/" diff --git a/packages/app/galaxy/schema b/packages/app/galaxy/schema deleted file mode 120000 index 14df7cabc56..00000000000 --- a/packages/app/galaxy/schema +++ /dev/null @@ -1 +0,0 @@ -../../../lib/galaxy/schema \ No newline at end of file diff --git a/packages/app/gxupload_0 b/packages/app/gxupload_0 new file mode 100644 index 00000000000..ca127767826 --- /dev/null +++ b/packages/app/gxupload_0 @@ -0,0 +1,2 @@ +sample data +hello world \ No newline at end of file diff --git a/packages/app/requirements.txt b/packages/app/requirements.txt index 4d77125b4d6..f6a334d786a 100644 --- a/packages/app/requirements.txt +++ b/packages/app/requirements.txt @@ -1,7 +1,9 @@ galaxy-auth +galaxy-config galaxy-data galaxy-job-execution galaxy-job-metrics +galaxy-objectstore galaxy-tool-util[cwl,edam] galaxy-web-framework galaxy-web-stack @@ -18,10 +20,9 @@ Markdown paramiko!=2.9.0,!=2.9.1 pulsar-galaxy-lib>=0.14.13 pydantic -pykwalify +PyJWT refgenconf>=0.12.0 sqlitedict svgwrite -tuswsgi typing-extensions Whoosh diff --git a/packages/app/setup.py b/packages/app/setup.py index 7b9b6928e24..6c6e9756017 100644 --- a/packages/app/setup.py +++ b/packages/app/setup.py @@ -14,20 +14,22 @@ SOURCE_DIR = "galaxy" _version_re = re.compile(r"__version__\s+=\s+(.*)") project_short_name = os.path.basename(os.path.dirname(os.path.realpath(__file__))) -with open(f"{SOURCE_DIR}/project_galaxy_{project_short_name}.py", "rb") as f: - init_contents = f.read().decode("utf-8") +with open(f"{SOURCE_DIR}/project_galaxy_{project_short_name}.py") as f: + init_contents = f.read() - def get_var(var_name): - pattern = re.compile(r"%s\s+=\s+(.*)" % var_name) - match = pattern.search(init_contents).group(1) - return str(ast.literal_eval(match)) - version = get_var("__version__") - PROJECT_NAME = get_var("PROJECT_NAME") - PROJECT_URL = get_var("PROJECT_URL") - PROJECT_AUTHOR = get_var("PROJECT_AUTHOR") - PROJECT_EMAIL = get_var("PROJECT_EMAIL") - PROJECT_DESCRIPTION = get_var("PROJECT_DESCRIPTION") +def get_var(var_name): + pattern = re.compile(rf"{var_name}\s+=\s+(.*)") + match = pattern.search(init_contents).group(1) + return str(ast.literal_eval(match)) + + +version = get_var("__version__") +PROJECT_NAME = get_var("PROJECT_NAME") +PROJECT_URL = get_var("PROJECT_URL") +PROJECT_AUTHOR = get_var("PROJECT_AUTHOR") +PROJECT_EMAIL = get_var("PROJECT_EMAIL") +PROJECT_DESCRIPTION = get_var("PROJECT_DESCRIPTION") TEST_DIR = "tests" PACKAGES = [ @@ -35,7 +37,6 @@ PACKAGES = [ "galaxy.actions", "galaxy.app_unittest_utils", "galaxy.authnz", - "galaxy.config", "galaxy.dependencies", "galaxy.forms", "galaxy.jobs", @@ -50,7 +51,6 @@ PACKAGES = [ "galaxy.jobs.runners.util.job_script", "galaxy.jobs.splitters", "galaxy.managers", - "galaxy.schema", "galaxy.tools", "galaxy.tools.actions", "galaxy.tools.data", @@ -96,14 +96,9 @@ PACKAGES = [ ENTRY_POINTS = """ [console_scripts] galaxy-main=galaxy.main:main - galaxy-config=galaxy.config.script:main """ PACKAGE_DATA = { # Be sure to update MANIFEST.in for source dist. - "galaxy": [ - "config/schemas/*.yml", - "config/sample/*", - ], "tool_shed": [ "scripts/bootstrap_tool_shed/user_info.xml", ], diff --git a/packages/app/test-requirements.txt b/packages/app/test-requirements.txt index 1fd0bf58c90..f93c7d6a95a 100644 --- a/packages/app/test-requirements.txt +++ b/packages/app/test-requirements.txt @@ -1,3 +1,4 @@ mock-ssh-server +pykwalify pytest testfixtures diff --git a/packages/auth/galaxy/project_galaxy_auth.py b/packages/auth/galaxy/project_galaxy_auth.py index c9d4a49aa4b..372ad34f571 100644 --- a/packages/auth/galaxy/project_galaxy_auth.py +++ b/packages/auth/galaxy/project_galaxy_auth.py @@ -6,4 +6,4 @@ PROJECT_URL = "https://github.com/galaxyproject/galaxy" PROJECT_AUTHOR = "Galaxy Project and Community" PROJECT_DESCRIPTION = "Galaxy Auth Framework and Implementations" PROJECT_EMAIL = "galaxy-committers@lists.galaxyproject.org" -RAW_CONTENT_URL = "https://raw.github.com/{}/{}/master/".format(PROJECT_USERAME, PROJECT_NAME) +RAW_CONTENT_URL = f"https://raw.github.com/{PROJECT_USERAME}/{PROJECT_NAME}/master/" diff --git a/packages/auth/setup.py b/packages/auth/setup.py index 6cc48dc18a8..9d60dfdc4bf 100644 --- a/packages/auth/setup.py +++ b/packages/auth/setup.py @@ -14,20 +14,22 @@ SOURCE_DIR = "galaxy" _version_re = re.compile(r"__version__\s+=\s+(.*)") project_short_name = os.path.basename(os.path.dirname(os.path.realpath(__file__))) -with open(f"{SOURCE_DIR}/project_galaxy_{project_short_name}.py", "rb") as f: - init_contents = f.read().decode("utf-8") +with open(f"{SOURCE_DIR}/project_galaxy_{project_short_name}.py") as f: + init_contents = f.read() - def get_var(var_name): - pattern = re.compile(r"%s\s+=\s+(.*)" % var_name) - match = pattern.search(init_contents).group(1) - return str(ast.literal_eval(match)) - version = get_var("__version__") - PROJECT_NAME = get_var("PROJECT_NAME") - PROJECT_URL = get_var("PROJECT_URL") - PROJECT_AUTHOR = get_var("PROJECT_AUTHOR") - PROJECT_EMAIL = get_var("PROJECT_EMAIL") - PROJECT_DESCRIPTION = get_var("PROJECT_DESCRIPTION") +def get_var(var_name): + pattern = re.compile(rf"{var_name}\s+=\s+(.*)") + match = pattern.search(init_contents).group(1) + return str(ast.literal_eval(match)) + + +version = get_var("__version__") +PROJECT_NAME = get_var("PROJECT_NAME") +PROJECT_URL = get_var("PROJECT_URL") +PROJECT_AUTHOR = get_var("PROJECT_AUTHOR") +PROJECT_EMAIL = get_var("PROJECT_EMAIL") +PROJECT_DESCRIPTION = get_var("PROJECT_DESCRIPTION") TEST_DIR = "tests" PACKAGES = [ diff --git a/packages/build_scripts/commit_version.py b/packages/build_scripts/commit_version.py index 1df182e609f..b3f48dc713d 100644 --- a/packages/build_scripts/commit_version.py +++ b/packages/build_scripts/commit_version.py @@ -9,7 +9,7 @@ import sys DEV_RELEASE = os.environ.get("DEV_RELEASE", None) == "1" PROJECT_DIRECTORY = os.getcwd() PROJECT_DIRECTORY_NAME = os.path.basename(os.path.abspath(PROJECT_DIRECTORY)) -PROJECT_MODULE_FILENAME = "project_galaxy_%s.py" % PROJECT_DIRECTORY_NAME +PROJECT_MODULE_FILENAME = f"project_galaxy_{PROJECT_DIRECTORY_NAME}.py" PROJECT_NAME = PROJECT_DIRECTORY_NAME.replace("_", "-") @@ -22,10 +22,10 @@ def main(argv): history = open(history_path).read() today = datetime.datetime.today() today_str = today.strftime("%Y-%m-%d") - history = history.replace(".dev0", " (%s)" % today_str) + history = history.replace(".dev0", f" ({today_str})") open(history_path, "w").write(history) mod = open(mod_path).read() - mod = re.sub(r"__version__ = '[\d\.]*\.dev\d+'", "__version__ = '%s'" % version, mod) + mod = re.sub(r"__version__ = '[\d\.]*\.dev\d+'", f"__version__ = '{version}'", mod) mod = open(mod_path, "w").write(mod) tag = f"galaxy-{PROJECT_NAME}-{version}" shell(["git", "commit", "-m", f"Version {version} of {PROJECT_NAME} (tag {tag}).", "HISTORY.rst", mod_path]) diff --git a/packages/build_scripts/new_version.py b/packages/build_scripts/new_version.py index 3f527f2bd2a..4575540a196 100644 --- a/packages/build_scripts/new_version.py +++ b/packages/build_scripts/new_version.py @@ -9,7 +9,7 @@ from distutils.version import StrictVersion DEV_RELEASE = os.environ.get("DEV_RELEASE", None) == "1" PROJECT_DIRECTORY = os.getcwd() PROJECT_DIRECTORY_NAME = os.path.basename(os.path.abspath(PROJECT_DIRECTORY)) -PROJECT_MODULE_FILENAME = "project_galaxy_%s.py" % PROJECT_DIRECTORY_NAME +PROJECT_MODULE_FILENAME = f"project_galaxy_{PROJECT_DIRECTORY_NAME}.py" PROJECT_NAME = PROJECT_DIRECTORY_NAME.replace("_", "-") @@ -26,7 +26,7 @@ def main(argv): else: dev_version = re.compile(r"dev([\d]+)").search(version).group(1) new_dev_version = int(dev_version) + 1 - new_version = version.replace("dev%s" % dev_version, "dev%s" % new_dev_version) + new_version = version.replace(f"dev{dev_version}", f"dev{new_dev_version}") history_path = os.path.join(PROJECT_DIRECTORY, "HISTORY.rst") if not DEV_RELEASE: @@ -38,22 +38,21 @@ def main(argv): history = extend( ".. to_doc", - """ + f""" --------------------- -%s.dev0 +{new_version}.dev0 --------------------- -""" - % new_version, +""", ) open(history_path, "w").write(history) mod_path = os.path.join(PROJECT_DIRECTORY, source_dir, PROJECT_MODULE_FILENAME) mod = open(mod_path).read() if not DEV_RELEASE: - mod = re.sub(r"__version__ = '[\d\.]+'", "__version__ = '%s.dev0'" % new_version, mod, 1) + mod = re.sub(r"__version__ = '[\d\.]+'", f"__version__ = '{new_version}.dev0'", mod, 1) else: - mod = re.sub("dev%s" % dev_version, "dev%s" % new_dev_version, mod, 1) + mod = re.sub(f"dev{dev_version}", f"dev{new_dev_version}", mod, 1) mod = open(mod_path, "w").write(mod) shell(["git", "commit", "-m", f"Starting work on {PROJECT_NAME} {new_version}", "HISTORY.rst", mod_path]) diff --git a/packages/build_scripts/print_version_for_release.py b/packages/build_scripts/print_version_for_release.py index 6679af9ee24..36c8f97a693 100644 --- a/packages/build_scripts/print_version_for_release.py +++ b/packages/build_scripts/print_version_for_release.py @@ -7,7 +7,7 @@ from distutils.version import LooseVersion DEV_RELEASE = os.environ.get("DEV_RELEASE", None) == "1" PROJECT_DIRECTORY = os.getcwd() PROJECT_DIRECTORY_NAME = os.path.basename(os.path.abspath(PROJECT_DIRECTORY)) -PROJECT_MODULE_FILENAME = "project_galaxy_%s.py" % PROJECT_DIRECTORY_NAME +PROJECT_MODULE_FILENAME = f"project_galaxy_{PROJECT_DIRECTORY_NAME}.py" source_dir = sys.argv[1] PROJECT_MODULE_PATH = os.path.join(PROJECT_DIRECTORY, source_dir, PROJECT_MODULE_FILENAME) diff --git a/packages/config/HISTORY.rst b/packages/config/HISTORY.rst new file mode 100644 index 00000000000..3988a66c6d3 --- /dev/null +++ b/packages/config/HISTORY.rst @@ -0,0 +1,12 @@ +.. :changelog: + +History +------- + +.. to_doc + +--------------------- +22.1.0.dev0 +--------------------- + +* Initial release diff --git a/packages/config/LICENSE b/packages/config/LICENSE new file mode 120000 index 00000000000..1ef648f64b3 --- /dev/null +++ b/packages/config/LICENSE @@ -0,0 +1 @@ +../../LICENSE.txt \ No newline at end of file diff --git a/packages/config/MANIFEST.in b/packages/config/MANIFEST.in new file mode 100644 index 00000000000..ef1f736e7c8 --- /dev/null +++ b/packages/config/MANIFEST.in @@ -0,0 +1,3 @@ +include *.rst *.txt LICENSE +include galaxy/config/schemas/*.yml +include galaxy/config/sample/*.sample* diff --git a/packages/config/Makefile b/packages/config/Makefile new file mode 120000 index 00000000000..37af8bae5ba --- /dev/null +++ b/packages/config/Makefile @@ -0,0 +1 @@ +../package.Makefile \ No newline at end of file diff --git a/packages/config/README.rst b/packages/config/README.rst new file mode 100644 index 00000000000..fde16673c89 --- /dev/null +++ b/packages/config/README.rst @@ -0,0 +1,14 @@ + +.. image:: https://badge.fury.io/py/galaxy-config.svg + :target: https://pypi.org/project/galaxy-config/ + + +Overview +-------- + +The Galaxy_ config module. + +* Free software: Academic Free License version 3.0 +* Code: https://github.com/galaxyproject/galaxy + +.. _Galaxy: http://galaxyproject.org/ diff --git a/packages/config/dev-requirements.txt b/packages/config/dev-requirements.txt new file mode 120000 index 00000000000..467b90d7a23 --- /dev/null +++ b/packages/config/dev-requirements.txt @@ -0,0 +1 @@ +../package-dev-requirements.txt \ No newline at end of file diff --git a/packages/config/galaxy/__init__.py b/packages/config/galaxy/__init__.py new file mode 100644 index 00000000000..2e50d9cce89 --- /dev/null +++ b/packages/config/galaxy/__init__.py @@ -0,0 +1 @@ +__path__ = __import__("pkgutil").extend_path(__path__, __name__) # type: ignore[has-type] diff --git a/packages/app/galaxy/config b/packages/config/galaxy/config similarity index 100% rename from packages/app/galaxy/config rename to packages/config/galaxy/config diff --git a/packages/config/galaxy/project_galaxy_config.py b/packages/config/galaxy/project_galaxy_config.py new file mode 100644 index 00000000000..c06f37bdcb9 --- /dev/null +++ b/packages/config/galaxy/project_galaxy_config.py @@ -0,0 +1,9 @@ +__version__ = "22.1.0" + +PROJECT_NAME = "galaxy-config" +PROJECT_OWNER = PROJECT_USERAME = "galaxyproject" +PROJECT_URL = "https://github.com/galaxyproject/galaxy" +PROJECT_AUTHOR = "Galaxy Project and Community" +PROJECT_DESCRIPTION = "Galaxy Configuration" +PROJECT_EMAIL = "galaxy-committers@lists.galaxyproject.org" +RAW_CONTENT_URL = f"https://raw.github.com/{PROJECT_USERAME}/{PROJECT_NAME}/master/" diff --git a/packages/config/requirements.txt b/packages/config/requirements.txt new file mode 100644 index 00000000000..a3caa0acfea --- /dev/null +++ b/packages/config/requirements.txt @@ -0,0 +1,5 @@ +galaxy-containers +boltons +pykwalify +PyYAML +requests diff --git a/packages/config/scripts b/packages/config/scripts new file mode 120000 index 00000000000..9aec9dc5a06 --- /dev/null +++ b/packages/config/scripts @@ -0,0 +1 @@ +../build_scripts \ No newline at end of file diff --git a/packages/config/setup.cfg b/packages/config/setup.cfg new file mode 120000 index 00000000000..eb7cf09393f --- /dev/null +++ b/packages/config/setup.cfg @@ -0,0 +1 @@ +../../setup.cfg \ No newline at end of file diff --git a/packages/config/setup.py b/packages/config/setup.py new file mode 100644 index 00000000000..a60b82d7120 --- /dev/null +++ b/packages/config/setup.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python + +import ast +import os +import re + +try: + from setuptools import setup +except ImportError: + from distutils.core import setup + +SOURCE_DIR = "galaxy" + +_version_re = re.compile(r"__version__\s+=\s+(.*)") + +project_short_name = os.path.basename(os.path.dirname(os.path.realpath(__file__))) +with open(f"{SOURCE_DIR}/project_galaxy_{project_short_name}.py") as f: + init_contents = f.read() + + +def get_var(var_name): + pattern = re.compile(rf"{var_name}\s+=\s+(.*)") + match = pattern.search(init_contents).group(1) + return str(ast.literal_eval(match)) + + +version = get_var("__version__") +PROJECT_NAME = get_var("PROJECT_NAME") +PROJECT_URL = get_var("PROJECT_URL") +PROJECT_AUTHOR = get_var("PROJECT_AUTHOR") +PROJECT_EMAIL = get_var("PROJECT_EMAIL") +PROJECT_DESCRIPTION = get_var("PROJECT_DESCRIPTION") + +TEST_DIR = "tests" +PACKAGES = [ + "galaxy", + "galaxy.config", +] +ENTRY_POINTS = """ + [console_scripts] + galaxy-config=galaxy.config.script:main +""" +PACKAGE_DATA = { + # Be sure to update MANIFEST.in for source dist. + "galaxy": [ + "config/schemas/*.yml", + "config/sample/*", + ], +} +PACKAGE_DIR = { + SOURCE_DIR: SOURCE_DIR, +} + +readme = open("README.rst").read() +history = open("HISTORY.rst").read().replace(".. :changelog:", "") + +if os.path.exists("requirements.txt"): + requirements = open("requirements.txt").read().split("\n") +else: + # In tox, it will cover them anyway. + requirements = [] + + +test_requirements = open("test-requirements.txt").read().split("\n") + + +setup( + name=PROJECT_NAME, + version=version, + description=PROJECT_DESCRIPTION, + long_description=readme + "\n\n" + history, + long_description_content_type="text/x-rst", + author=PROJECT_AUTHOR, + author_email=PROJECT_EMAIL, + url=PROJECT_URL, + packages=PACKAGES, + entry_points=ENTRY_POINTS, + package_data=PACKAGE_DATA, + package_dir=PACKAGE_DIR, + include_package_data=True, + install_requires=requirements, + license="AFL", + zip_safe=False, + keywords="galaxy", + classifiers=[ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Environment :: Console", + "License :: OSI Approved :: Academic Free License (AFL)", + "Operating System :: POSIX", + "Topic :: Software Development", + "Topic :: Software Development :: Code Generators", + "Topic :: Software Development :: Testing", + "Natural Language :: English", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + ], + test_suite=TEST_DIR, + tests_require=test_requirements, +) diff --git a/packages/config/test-data b/packages/config/test-data new file mode 120000 index 00000000000..e50575ea576 --- /dev/null +++ b/packages/config/test-data @@ -0,0 +1 @@ +../../test-data \ No newline at end of file diff --git a/packages/config/test-requirements.txt b/packages/config/test-requirements.txt new file mode 100644 index 00000000000..f65e968f48e --- /dev/null +++ b/packages/config/test-requirements.txt @@ -0,0 +1,2 @@ +gravity +pytest<6.1 diff --git a/lib/galaxy/model/migrate/versions/__init__.py b/packages/config/tests/__init__.py similarity index 100% rename from lib/galaxy/model/migrate/versions/__init__.py rename to packages/config/tests/__init__.py diff --git a/packages/config/tests/config b/packages/config/tests/config new file mode 120000 index 00000000000..329fc647500 --- /dev/null +++ b/packages/config/tests/config @@ -0,0 +1 @@ +../../../test/unit/config \ No newline at end of file diff --git a/packages/containers/galaxy/project_galaxy_containers.py b/packages/containers/galaxy/project_galaxy_containers.py index 03c735ef605..e05279db943 100644 --- a/packages/containers/galaxy/project_galaxy_containers.py +++ b/packages/containers/galaxy/project_galaxy_containers.py @@ -6,4 +6,4 @@ PROJECT_URL = "https://github.com/galaxyproject/galaxy" PROJECT_AUTHOR = "Galaxy Project and Community" PROJECT_DESCRIPTION = "Galaxy Container Modeling and Interaction Abstractions" PROJECT_EMAIL = "galaxy-committers@lists.galaxyproject.org" -RAW_CONTENT_URL = "https://raw.github.com/{}/{}/master/".format(PROJECT_USERAME, PROJECT_NAME) +RAW_CONTENT_URL = f"https://raw.github.com/{PROJECT_USERAME}/{PROJECT_NAME}/master/" diff --git a/packages/containers/setup.py b/packages/containers/setup.py index b614ada28bd..36ab00beac2 100644 --- a/packages/containers/setup.py +++ b/packages/containers/setup.py @@ -13,20 +13,23 @@ SOURCE_DIR = "galaxy" _version_re = re.compile(r"__version__\s+=\s+(.*)") -with open("%s/project_galaxy_containers.py" % SOURCE_DIR, "rb") as f: - init_contents = f.read().decode("utf-8") +project_short_name = os.path.basename(os.path.dirname(os.path.realpath(__file__))) +with open(f"{SOURCE_DIR}/project_galaxy_{project_short_name}.py") as f: + init_contents = f.read() - def get_var(var_name): - pattern = re.compile(r"%s\s+=\s+(.*)" % var_name) - match = pattern.search(init_contents).group(1) - return str(ast.literal_eval(match)) - version = get_var("__version__") - PROJECT_NAME = get_var("PROJECT_NAME") - PROJECT_URL = get_var("PROJECT_URL") - PROJECT_AUTHOR = get_var("PROJECT_AUTHOR") - PROJECT_EMAIL = get_var("PROJECT_EMAIL") - PROJECT_DESCRIPTION = get_var("PROJECT_DESCRIPTION") +def get_var(var_name): + pattern = re.compile(rf"{var_name}\s+=\s+(.*)") + match = pattern.search(init_contents).group(1) + return str(ast.literal_eval(match)) + + +version = get_var("__version__") +PROJECT_NAME = get_var("PROJECT_NAME") +PROJECT_URL = get_var("PROJECT_URL") +PROJECT_AUTHOR = get_var("PROJECT_AUTHOR") +PROJECT_EMAIL = get_var("PROJECT_EMAIL") +PROJECT_DESCRIPTION = get_var("PROJECT_DESCRIPTION") TEST_DIR = "tests" PACKAGES = [ diff --git a/packages/data/MANIFEST.in b/packages/data/MANIFEST.in index e2e6d43c155..a5e6acd0809 100644 --- a/packages/data/MANIFEST.in +++ b/packages/data/MANIFEST.in @@ -2,5 +2,5 @@ include *.rst *.txt LICENSE include galaxy/datatypes/set_metadata_tool.xml include galaxy/datatypes/converters/*.xml include galaxy/datatypes/test/* -include galaxy/model/migrate/migrate.cfg -include galaxy/model/tool_shed_install/migrate/migrate.cfg +include galaxy/model/migrations/alembic.ini +include galaxy/model/migrations/alembic/script.py.mako diff --git a/packages/data/galaxy/project_galaxy_data.py b/packages/data/galaxy/project_galaxy_data.py index dc1ab8194fd..abe7e4177b3 100644 --- a/packages/data/galaxy/project_galaxy_data.py +++ b/packages/data/galaxy/project_galaxy_data.py @@ -6,4 +6,4 @@ PROJECT_URL = "https://github.com/galaxyproject/galaxy" PROJECT_AUTHOR = "Galaxy Project and Community" PROJECT_DESCRIPTION = "Galaxy Datatype Framework and Datatypes" PROJECT_EMAIL = "galaxy-committers@lists.galaxyproject.org" -RAW_CONTENT_URL = "https://raw.github.com/{}/{}/master/".format(PROJECT_USERAME, PROJECT_NAME) +RAW_CONTENT_URL = f"https://raw.github.com/{PROJECT_USERAME}/{PROJECT_NAME}/master/" diff --git a/packages/data/requirements.txt b/packages/data/requirements.txt index 1ec7811f1ff..2b559ebf2a0 100644 --- a/packages/data/requirements.txt +++ b/packages/data/requirements.txt @@ -2,6 +2,7 @@ galaxy-files galaxy-objectstore galaxy-sequence-utils galaxy-util[template] +alembic==1.7.4; python_version >= "3.6" bdbag bx-python contextvars; python_version >= "3.6" and python_version < "3.7" @@ -15,7 +16,6 @@ pydantic pysam social-auth-core[openidconnect]==4.0.3 SQLAlchemy>=1.4.25,<2 -sqlalchemy-migrate tifffile<=2020.9.3 # Last version compatible with python 3.6 typing-extensions WebOb diff --git a/packages/data/setup.py b/packages/data/setup.py index 8e37013fb5e..81dbc84a7ad 100644 --- a/packages/data/setup.py +++ b/packages/data/setup.py @@ -13,45 +13,48 @@ SOURCE_DIR = "galaxy" _version_re = re.compile(r"__version__\s+=\s+(.*)") -with open("%s/project_galaxy_data.py" % SOURCE_DIR, "rb") as f: - init_contents = f.read().decode("utf-8") +project_short_name = os.path.basename(os.path.dirname(os.path.realpath(__file__))) +with open(f"{SOURCE_DIR}/project_galaxy_{project_short_name}.py") as f: + init_contents = f.read() - def get_var(var_name): - pattern = re.compile(r"%s\s+=\s+(.*)" % var_name) - match = pattern.search(init_contents).group(1) - return str(ast.literal_eval(match)) - version = get_var("__version__") - PROJECT_NAME = get_var("PROJECT_NAME") - PROJECT_URL = get_var("PROJECT_URL") - PROJECT_AUTHOR = get_var("PROJECT_AUTHOR") - PROJECT_EMAIL = get_var("PROJECT_EMAIL") - PROJECT_DESCRIPTION = get_var("PROJECT_DESCRIPTION") +def get_var(var_name): + pattern = re.compile(rf"{var_name}\s+=\s+(.*)") + match = pattern.search(init_contents).group(1) + return str(ast.literal_eval(match)) + + +version = get_var("__version__") +PROJECT_NAME = get_var("PROJECT_NAME") +PROJECT_URL = get_var("PROJECT_URL") +PROJECT_AUTHOR = get_var("PROJECT_AUTHOR") +PROJECT_EMAIL = get_var("PROJECT_EMAIL") +PROJECT_DESCRIPTION = get_var("PROJECT_DESCRIPTION") TEST_DIR = "tests" PACKAGES = [ - "galaxy", - "galaxy.datatypes", - "galaxy.datatypes.converters", - "galaxy.datatypes.dataproviders", - "galaxy.datatypes.display_applications", - "galaxy.datatypes.util", - "galaxy.datatypes.test", - "galaxy.model", - "galaxy.model.dataset_collections", - "galaxy.model.dataset_collections.types", - "galaxy.model.migrate", - "galaxy.model.migrate.triggers", - "galaxy.model.migrate.versions", - "galaxy.model.orm", - "galaxy.model.store", - "galaxy.model.tool_shed_install", - "galaxy.model.tool_shed_install.migrate", - "galaxy.model.tool_shed_install.migrate.versions", - "galaxy.model.unittest_utils", - "galaxy.model.view", - "galaxy.quota", - "galaxy.security", + 'galaxy', + 'galaxy.datatypes', + 'galaxy.datatypes.converters', + 'galaxy.datatypes.dataproviders', + 'galaxy.datatypes.display_applications', + 'galaxy.datatypes.util', + 'galaxy.datatypes.test', + 'galaxy.model', + 'galaxy.model.dataset_collections', + 'galaxy.model.dataset_collections.types', + 'galaxy.model.migrations', + 'galaxy.model.migrations.alembic', + 'galaxy.model.migrations.alembic.versions_gxy', + 'galaxy.model.migrations.alembic.versions_tsi', + 'galaxy.model.orm', + 'galaxy.model.store', + 'galaxy.model.tool_shed_install', + 'galaxy.model.triggers', + 'galaxy.model.unittest_utils', + 'galaxy.model.view', + 'galaxy.quota', + 'galaxy.security', ] ENTRY_POINTS = """ [console_scripts] diff --git a/packages/files/setup.py b/packages/files/setup.py index 8de58d81d92..b4ac6ee9ba6 100644 --- a/packages/files/setup.py +++ b/packages/files/setup.py @@ -14,20 +14,22 @@ SOURCE_DIR = "galaxy" _version_re = re.compile(r"__version__\s+=\s+(.*)") project_short_name = os.path.basename(os.path.dirname(os.path.realpath(__file__))) -with open(f"{SOURCE_DIR}/project_galaxy_{project_short_name}.py", "rb") as f: - init_contents = f.read().decode("utf-8") +with open(f"{SOURCE_DIR}/project_galaxy_{project_short_name}.py") as f: + init_contents = f.read() - def get_var(var_name): - pattern = re.compile(r"%s\s+=\s+(.*)" % var_name) - match = pattern.search(init_contents).group(1) - return str(ast.literal_eval(match)) - version = get_var("__version__") - PROJECT_NAME = get_var("PROJECT_NAME") - PROJECT_URL = get_var("PROJECT_URL") - PROJECT_AUTHOR = get_var("PROJECT_AUTHOR") - PROJECT_EMAIL = get_var("PROJECT_EMAIL") - PROJECT_DESCRIPTION = get_var("PROJECT_DESCRIPTION") +def get_var(var_name): + pattern = re.compile(rf"{var_name}\s+=\s+(.*)") + match = pattern.search(init_contents).group(1) + return str(ast.literal_eval(match)) + + +version = get_var("__version__") +PROJECT_NAME = get_var("PROJECT_NAME") +PROJECT_URL = get_var("PROJECT_URL") +PROJECT_AUTHOR = get_var("PROJECT_AUTHOR") +PROJECT_EMAIL = get_var("PROJECT_EMAIL") +PROJECT_DESCRIPTION = get_var("PROJECT_DESCRIPTION") TEST_DIR = "tests" PACKAGES = [ diff --git a/packages/job_execution/galaxy/project_galaxy_job_execution.py b/packages/job_execution/galaxy/project_galaxy_job_execution.py index 15c4c324512..1b3df1f9213 100644 --- a/packages/job_execution/galaxy/project_galaxy_job_execution.py +++ b/packages/job_execution/galaxy/project_galaxy_job_execution.py @@ -6,4 +6,4 @@ PROJECT_URL = "https://github.com/galaxyproject/galaxy" PROJECT_AUTHOR = "Galaxy Project and Community" PROJECT_DESCRIPTION = "Galaxy Job Execution Runtime Utilities" PROJECT_EMAIL = "galaxy-committers@lists.galaxyproject.org" -RAW_CONTENT_URL = "https://raw.github.com/{}/{}/master/".format(PROJECT_USERAME, PROJECT_NAME) +RAW_CONTENT_URL = f"https://raw.github.com/{PROJECT_USERAME}/{PROJECT_NAME}/master/" diff --git a/packages/job_execution/setup.py b/packages/job_execution/setup.py index 3f72d47bbab..1a6fe2e1bb2 100644 --- a/packages/job_execution/setup.py +++ b/packages/job_execution/setup.py @@ -13,20 +13,23 @@ SOURCE_DIR = "galaxy" _version_re = re.compile(r"__version__\s+=\s+(.*)") -with open("%s/project_galaxy_job_execution.py" % SOURCE_DIR, "rb") as f: - init_contents = f.read().decode("utf-8") +project_short_name = os.path.basename(os.path.dirname(os.path.realpath(__file__))) +with open(f"{SOURCE_DIR}/project_galaxy_{project_short_name}.py") as f: + init_contents = f.read() - def get_var(var_name): - pattern = re.compile(r"%s\s+=\s+(.*)" % var_name) - match = pattern.search(init_contents).group(1) - return str(ast.literal_eval(match)) - version = get_var("__version__") - PROJECT_NAME = get_var("PROJECT_NAME") - PROJECT_URL = get_var("PROJECT_URL") - PROJECT_AUTHOR = get_var("PROJECT_AUTHOR") - PROJECT_EMAIL = get_var("PROJECT_EMAIL") - PROJECT_DESCRIPTION = get_var("PROJECT_DESCRIPTION") +def get_var(var_name): + pattern = re.compile(rf"{var_name}\s+=\s+(.*)") + match = pattern.search(init_contents).group(1) + return str(ast.literal_eval(match)) + + +version = get_var("__version__") +PROJECT_NAME = get_var("PROJECT_NAME") +PROJECT_URL = get_var("PROJECT_URL") +PROJECT_AUTHOR = get_var("PROJECT_AUTHOR") +PROJECT_EMAIL = get_var("PROJECT_EMAIL") +PROJECT_DESCRIPTION = get_var("PROJECT_DESCRIPTION") TEST_DIR = "tests" PACKAGES = [ @@ -76,7 +79,6 @@ setup( package_dir=PACKAGE_DIR, include_package_data=True, install_requires=requirements, - extras_require={}, license="AFL", zip_safe=False, keywords="galaxy", diff --git a/packages/job_metrics/galaxy/project_galaxy_job_metrics.py b/packages/job_metrics/galaxy/project_galaxy_job_metrics.py index a90aa4d8477..1cb374b327d 100644 --- a/packages/job_metrics/galaxy/project_galaxy_job_metrics.py +++ b/packages/job_metrics/galaxy/project_galaxy_job_metrics.py @@ -6,4 +6,4 @@ PROJECT_URL = "https://github.com/galaxyproject/galaxy" PROJECT_AUTHOR = "Galaxy Project and Community" PROJECT_DESCRIPTION = "Galaxy Job Metrics" PROJECT_EMAIL = "galaxy-committers@lists.galaxyproject.org" -RAW_CONTENT_URL = "https://raw.github.com/{}/{}/master/".format(PROJECT_USERAME, PROJECT_NAME) +RAW_CONTENT_URL = f"https://raw.github.com/{PROJECT_USERAME}/{PROJECT_NAME}/master/" diff --git a/packages/job_metrics/setup.py b/packages/job_metrics/setup.py index 16c654333f3..25a3f672873 100644 --- a/packages/job_metrics/setup.py +++ b/packages/job_metrics/setup.py @@ -13,20 +13,23 @@ SOURCE_DIR = "galaxy" _version_re = re.compile(r"__version__\s+=\s+(.*)") -with open("%s/project_galaxy_job_metrics.py" % SOURCE_DIR, "rb") as f: - init_contents = f.read().decode("utf-8") +project_short_name = os.path.basename(os.path.dirname(os.path.realpath(__file__))) +with open(f"{SOURCE_DIR}/project_galaxy_{project_short_name}.py") as f: + init_contents = f.read() - def get_var(var_name): - pattern = re.compile(r"%s\s+=\s+(.*)" % var_name) - match = pattern.search(init_contents).group(1) - return str(ast.literal_eval(match)) - version = get_var("__version__") - PROJECT_NAME = get_var("PROJECT_NAME") - PROJECT_URL = get_var("PROJECT_URL") - PROJECT_AUTHOR = get_var("PROJECT_AUTHOR") - PROJECT_EMAIL = get_var("PROJECT_EMAIL") - PROJECT_DESCRIPTION = get_var("PROJECT_DESCRIPTION") +def get_var(var_name): + pattern = re.compile(rf"{var_name}\s+=\s+(.*)") + match = pattern.search(init_contents).group(1) + return str(ast.literal_eval(match)) + + +version = get_var("__version__") +PROJECT_NAME = get_var("PROJECT_NAME") +PROJECT_URL = get_var("PROJECT_URL") +PROJECT_AUTHOR = get_var("PROJECT_AUTHOR") +PROJECT_EMAIL = get_var("PROJECT_EMAIL") +PROJECT_DESCRIPTION = get_var("PROJECT_DESCRIPTION") TEST_DIR = "tests" PACKAGES = [ diff --git a/packages/meta/galaxy/project_galaxy_meta.py b/packages/meta/galaxy/project_galaxy_meta.py index 22e04986a1b..30f9bef82d4 100644 --- a/packages/meta/galaxy/project_galaxy_meta.py +++ b/packages/meta/galaxy/project_galaxy_meta.py @@ -6,4 +6,4 @@ PROJECT_URL = "https://github.com/galaxyproject/galaxy" PROJECT_AUTHOR = "Galaxy Project and Community" PROJECT_DESCRIPTION = "Galaxy Server Metapackage" PROJECT_EMAIL = "galaxy-committers@lists.galaxyproject.org" -RAW_CONTENT_URL = "https://raw.github.com/{}/{}/master/".format(PROJECT_USERAME, PROJECT_NAME) +RAW_CONTENT_URL = f"https://raw.github.com/{PROJECT_USERAME}/{PROJECT_NAME}/master/" diff --git a/packages/meta/setup.py b/packages/meta/setup.py index 28666c7046e..941b710d8d0 100644 --- a/packages/meta/setup.py +++ b/packages/meta/setup.py @@ -13,20 +13,23 @@ SOURCE_DIR = "galaxy" _version_re = re.compile(r"__version__\s+=\s+(.*)") -with open("%s/project_galaxy_meta.py" % SOURCE_DIR, "rb") as f: - init_contents = f.read().decode("utf-8") +project_short_name = os.path.basename(os.path.dirname(os.path.realpath(__file__))) +with open(f"{SOURCE_DIR}/project_galaxy_{project_short_name}.py") as f: + init_contents = f.read() - def get_var(var_name): - pattern = re.compile(r"%s\s+=\s+(.*)" % var_name) - match = pattern.search(init_contents).group(1) - return str(ast.literal_eval(match)) - version = get_var("__version__") - PROJECT_NAME = get_var("PROJECT_NAME") - PROJECT_URL = get_var("PROJECT_URL") - PROJECT_AUTHOR = get_var("PROJECT_AUTHOR") - PROJECT_EMAIL = get_var("PROJECT_EMAIL") - PROJECT_DESCRIPTION = get_var("PROJECT_DESCRIPTION") +def get_var(var_name): + pattern = re.compile(rf"{var_name}\s+=\s+(.*)") + match = pattern.search(init_contents).group(1) + return str(ast.literal_eval(match)) + + +version = get_var("__version__") +PROJECT_NAME = get_var("PROJECT_NAME") +PROJECT_URL = get_var("PROJECT_URL") +PROJECT_AUTHOR = get_var("PROJECT_AUTHOR") +PROJECT_EMAIL = get_var("PROJECT_EMAIL") +PROJECT_DESCRIPTION = get_var("PROJECT_DESCRIPTION") TEST_DIR = "tests" PACKAGES = [] diff --git a/packages/objectstore/galaxy/project_galaxy_objectstore.py b/packages/objectstore/galaxy/project_galaxy_objectstore.py index 9f9be584e9a..dbb12adeed4 100644 --- a/packages/objectstore/galaxy/project_galaxy_objectstore.py +++ b/packages/objectstore/galaxy/project_galaxy_objectstore.py @@ -6,4 +6,4 @@ PROJECT_URL = "https://github.com/galaxyproject/galaxy" PROJECT_AUTHOR = "Galaxy Project and Community" PROJECT_DESCRIPTION = "Galaxy Objectstore Framework and Plugins" PROJECT_EMAIL = "galaxy-committers@lists.galaxyproject.org" -RAW_CONTENT_URL = "https://raw.github.com/{}/{}/master/".format(PROJECT_USERAME, PROJECT_NAME) +RAW_CONTENT_URL = f"https://raw.github.com/{PROJECT_USERAME}/{PROJECT_NAME}/master/" diff --git a/packages/objectstore/setup.py b/packages/objectstore/setup.py index e3119a12729..848db4bafc6 100644 --- a/packages/objectstore/setup.py +++ b/packages/objectstore/setup.py @@ -13,20 +13,23 @@ SOURCE_DIR = "galaxy" _version_re = re.compile(r"__version__\s+=\s+(.*)") -with open("%s/project_galaxy_objectstore.py" % SOURCE_DIR, "rb") as f: - init_contents = f.read().decode("utf-8") +project_short_name = os.path.basename(os.path.dirname(os.path.realpath(__file__))) +with open(f"{SOURCE_DIR}/project_galaxy_{project_short_name}.py") as f: + init_contents = f.read() - def get_var(var_name): - pattern = re.compile(r"%s\s+=\s+(.*)" % var_name) - match = pattern.search(init_contents).group(1) - return str(ast.literal_eval(match)) - version = get_var("__version__") - PROJECT_NAME = get_var("PROJECT_NAME") - PROJECT_URL = get_var("PROJECT_URL") - PROJECT_AUTHOR = get_var("PROJECT_AUTHOR") - PROJECT_EMAIL = get_var("PROJECT_EMAIL") - PROJECT_DESCRIPTION = get_var("PROJECT_DESCRIPTION") +def get_var(var_name): + pattern = re.compile(rf"{var_name}\s+=\s+(.*)") + match = pattern.search(init_contents).group(1) + return str(ast.literal_eval(match)) + + +version = get_var("__version__") +PROJECT_NAME = get_var("PROJECT_NAME") +PROJECT_URL = get_var("PROJECT_URL") +PROJECT_AUTHOR = get_var("PROJECT_AUTHOR") +PROJECT_EMAIL = get_var("PROJECT_EMAIL") +PROJECT_DESCRIPTION = get_var("PROJECT_DESCRIPTION") TEST_DIR = "tests" PACKAGES = [ diff --git a/packages/selenium/galaxy/project_galaxy_selenium.py b/packages/selenium/galaxy/project_galaxy_selenium.py index 8e08c6a47ba..3f059ca33ad 100644 --- a/packages/selenium/galaxy/project_galaxy_selenium.py +++ b/packages/selenium/galaxy/project_galaxy_selenium.py @@ -6,4 +6,4 @@ PROJECT_URL = "https://github.com/galaxyproject/galaxy" PROJECT_AUTHOR = "Galaxy Project and Community" PROJECT_DESCRIPTION = "Galaxy Selenium Interaction Framework" PROJECT_EMAIL = "galaxy-committers@lists.galaxyproject.org" -RAW_CONTENT_URL = "https://raw.github.com/{}/{}/master/".format(PROJECT_USERAME, PROJECT_NAME) +RAW_CONTENT_URL = f"https://raw.github.com/{PROJECT_USERAME}/{PROJECT_NAME}/master/" diff --git a/packages/selenium/setup.py b/packages/selenium/setup.py index e730f5d2232..c806143fb72 100644 --- a/packages/selenium/setup.py +++ b/packages/selenium/setup.py @@ -14,20 +14,22 @@ SOURCE_DIR = "galaxy" _version_re = re.compile(r"__version__\s+=\s+(.*)") project_short_name = os.path.basename(os.path.dirname(os.path.realpath(__file__))) -with open(f"{SOURCE_DIR}/project_galaxy_{project_short_name}.py", "rb") as f: - init_contents = f.read().decode("utf-8") +with open(f"{SOURCE_DIR}/project_galaxy_{project_short_name}.py") as f: + init_contents = f.read() - def get_var(var_name): - pattern = re.compile(r"%s\s+=\s+(.*)" % var_name) - match = pattern.search(init_contents).group(1) - return str(ast.literal_eval(match)) - version = get_var("__version__") - PROJECT_NAME = get_var("PROJECT_NAME") - PROJECT_URL = get_var("PROJECT_URL") - PROJECT_AUTHOR = get_var("PROJECT_AUTHOR") - PROJECT_EMAIL = get_var("PROJECT_EMAIL") - PROJECT_DESCRIPTION = get_var("PROJECT_DESCRIPTION") +def get_var(var_name): + pattern = re.compile(rf"{var_name}\s+=\s+(.*)") + match = pattern.search(init_contents).group(1) + return str(ast.literal_eval(match)) + + +version = get_var("__version__") +PROJECT_NAME = get_var("PROJECT_NAME") +PROJECT_URL = get_var("PROJECT_URL") +PROJECT_AUTHOR = get_var("PROJECT_AUTHOR") +PROJECT_EMAIL = get_var("PROJECT_EMAIL") +PROJECT_DESCRIPTION = get_var("PROJECT_DESCRIPTION") TEST_DIR = "tests" PACKAGES = [ diff --git a/packages/test.sh b/packages/test.sh index f323203da1b..a9a74cfa57a 100755 --- a/packages/test.sh +++ b/packages/test.sh @@ -23,6 +23,7 @@ PACKAGE_DIRS=( objectstore job_metrics containers + config files tool_util data @@ -33,8 +34,6 @@ PACKAGE_DIRS=( app webapps ) -RUN_TESTS=(1 1 1 1 1 1 1 1 1 1 1 1 0) -RUN_MYPY=(1 1 1 1 1 1 1 1 1 1 1 1 1) for ((i=0; i<${#PACKAGE_DIRS[@]}; i++)); do printf "\n========= TESTING PACKAGE ${PACKAGE_DIRS[$i]} =========\n\n" package_dir=${PACKAGE_DIRS[$i]} @@ -52,11 +51,9 @@ for ((i=0; i<${#PACKAGE_DIRS[@]}; i++)); do pip install -r test-requirements.txt - if [[ "${RUN_TESTS[$i]}" == "1" ]]; then - pytest --doctest-modules galaxy tests - fi - if [[ "${RUN_MYPY[$i]}" == "1" ]]; then - make mypy - fi + # Prevent execution of alembic/env.py at test collection stage (alembic.context not set) + unit_extra='--doctest-modules --ignore galaxy/model/migrations/alembic' + pytest $unit_extra galaxy tests + make mypy cd .. done diff --git a/packages/test_api/galaxy/project_galaxy_test_api.py b/packages/test_api/galaxy/project_galaxy_test_api.py index 99427178fec..ae67153d724 100644 --- a/packages/test_api/galaxy/project_galaxy_test_api.py +++ b/packages/test_api/galaxy/project_galaxy_test_api.py @@ -6,4 +6,4 @@ PROJECT_URL = "https://github.com/galaxyproject/galaxy" PROJECT_AUTHOR = "Galaxy Project and Community" PROJECT_DESCRIPTION = "Galaxy API Tests" PROJECT_EMAIL = "galaxy-committers@lists.galaxyproject.org" -RAW_CONTENT_URL = "https://raw.github.com/{}/{}/master/".format(PROJECT_USERAME, PROJECT_NAME) +RAW_CONTENT_URL = f"https://raw.github.com/{PROJECT_USERAME}/{PROJECT_NAME}/master/" diff --git a/packages/test_api/setup.py b/packages/test_api/setup.py index 486522d2d2b..5df0575feb8 100644 --- a/packages/test_api/setup.py +++ b/packages/test_api/setup.py @@ -14,20 +14,22 @@ SOURCE_DIR = "galaxy" _version_re = re.compile(r"__version__\s+=\s+(.*)") project_short_name = os.path.basename(os.path.dirname(os.path.realpath(__file__))) -with open(f"{SOURCE_DIR}/project_galaxy_{project_short_name}.py", "rb") as f: - init_contents = f.read().decode("utf-8") +with open(f"{SOURCE_DIR}/project_galaxy_{project_short_name}.py") as f: + init_contents = f.read() - def get_var(var_name): - pattern = re.compile(r"%s\s+=\s+(.*)" % var_name) - match = pattern.search(init_contents).group(1) - return str(ast.literal_eval(match)) - version = get_var("__version__") - PROJECT_NAME = get_var("PROJECT_NAME") - PROJECT_URL = get_var("PROJECT_URL") - PROJECT_AUTHOR = get_var("PROJECT_AUTHOR") - PROJECT_EMAIL = get_var("PROJECT_EMAIL") - PROJECT_DESCRIPTION = get_var("PROJECT_DESCRIPTION") +def get_var(var_name): + pattern = re.compile(rf"{var_name}\s+=\s+(.*)") + match = pattern.search(init_contents).group(1) + return str(ast.literal_eval(match)) + + +version = get_var("__version__") +PROJECT_NAME = get_var("PROJECT_NAME") +PROJECT_URL = get_var("PROJECT_URL") +PROJECT_AUTHOR = get_var("PROJECT_AUTHOR") +PROJECT_EMAIL = get_var("PROJECT_EMAIL") +PROJECT_DESCRIPTION = get_var("PROJECT_DESCRIPTION") TEST_DIR = "tests" PACKAGES = [ diff --git a/packages/test_base/galaxy/project_galaxy_test_base.py b/packages/test_base/galaxy/project_galaxy_test_base.py index 0b0d5d94ee1..eaba3cec0e3 100644 --- a/packages/test_base/galaxy/project_galaxy_test_base.py +++ b/packages/test_base/galaxy/project_galaxy_test_base.py @@ -6,4 +6,4 @@ PROJECT_URL = "https://github.com/galaxyproject/galaxy" PROJECT_AUTHOR = "Galaxy Project and Community" PROJECT_DESCRIPTION = "Galaxy Testing Utilities" PROJECT_EMAIL = "galaxy-committers@lists.galaxyproject.org" -RAW_CONTENT_URL = "https://raw.github.com/{}/{}/master/".format(PROJECT_USERAME, PROJECT_NAME) +RAW_CONTENT_URL = f"https://raw.github.com/{PROJECT_USERAME}/{PROJECT_NAME}/master/" diff --git a/packages/test_base/setup.py b/packages/test_base/setup.py index fc29f862c58..1a1948f991d 100644 --- a/packages/test_base/setup.py +++ b/packages/test_base/setup.py @@ -14,20 +14,22 @@ SOURCE_DIR = "galaxy" _version_re = re.compile(r"__version__\s+=\s+(.*)") project_short_name = os.path.basename(os.path.dirname(os.path.realpath(__file__))) -with open(f"{SOURCE_DIR}/project_galaxy_{project_short_name}.py", "rb") as f: - init_contents = f.read().decode("utf-8") +with open(f"{SOURCE_DIR}/project_galaxy_{project_short_name}.py") as f: + init_contents = f.read() - def get_var(var_name): - pattern = re.compile(r"%s\s+=\s+(.*)" % var_name) - match = pattern.search(init_contents).group(1) - return str(ast.literal_eval(match)) - version = get_var("__version__") - PROJECT_NAME = get_var("PROJECT_NAME") - PROJECT_URL = get_var("PROJECT_URL") - PROJECT_AUTHOR = get_var("PROJECT_AUTHOR") - PROJECT_EMAIL = get_var("PROJECT_EMAIL") - PROJECT_DESCRIPTION = get_var("PROJECT_DESCRIPTION") +def get_var(var_name): + pattern = re.compile(rf"{var_name}\s+=\s+(.*)") + match = pattern.search(init_contents).group(1) + return str(ast.literal_eval(match)) + + +version = get_var("__version__") +PROJECT_NAME = get_var("PROJECT_NAME") +PROJECT_URL = get_var("PROJECT_URL") +PROJECT_AUTHOR = get_var("PROJECT_AUTHOR") +PROJECT_EMAIL = get_var("PROJECT_EMAIL") +PROJECT_DESCRIPTION = get_var("PROJECT_DESCRIPTION") TEST_DIR = "tests" PACKAGES = [ diff --git a/packages/test_driver/galaxy/project_galaxy_test_driver.py b/packages/test_driver/galaxy/project_galaxy_test_driver.py index edf016a60d4..6e0ded00d2e 100644 --- a/packages/test_driver/galaxy/project_galaxy_test_driver.py +++ b/packages/test_driver/galaxy/project_galaxy_test_driver.py @@ -6,4 +6,4 @@ PROJECT_URL = "https://github.com/galaxyproject/galaxy" PROJECT_AUTHOR = "Galaxy Project and Community" PROJECT_DESCRIPTION = "Galaxy Test Driver" PROJECT_EMAIL = "galaxy-committers@lists.galaxyproject.org" -RAW_CONTENT_URL = "https://raw.github.com/{}/{}/master/".format(PROJECT_USERAME, PROJECT_NAME) +RAW_CONTENT_URL = f"https://raw.github.com/{PROJECT_USERAME}/{PROJECT_NAME}/master/" diff --git a/packages/test_driver/setup.py b/packages/test_driver/setup.py index e0243f67a06..79b0ebd7677 100644 --- a/packages/test_driver/setup.py +++ b/packages/test_driver/setup.py @@ -14,20 +14,22 @@ SOURCE_DIR = "galaxy" _version_re = re.compile(r"__version__\s+=\s+(.*)") project_short_name = os.path.basename(os.path.dirname(os.path.realpath(__file__))) -with open(f"{SOURCE_DIR}/project_galaxy_{project_short_name}.py", "rb") as f: - init_contents = f.read().decode("utf-8") +with open(f"{SOURCE_DIR}/project_galaxy_{project_short_name}.py") as f: + init_contents = f.read() - def get_var(var_name): - pattern = re.compile(r"%s\s+=\s+(.*)" % var_name) - match = pattern.search(init_contents).group(1) - return str(ast.literal_eval(match)) - version = get_var("__version__") - PROJECT_NAME = get_var("PROJECT_NAME") - PROJECT_URL = get_var("PROJECT_URL") - PROJECT_AUTHOR = get_var("PROJECT_AUTHOR") - PROJECT_EMAIL = get_var("PROJECT_EMAIL") - PROJECT_DESCRIPTION = get_var("PROJECT_DESCRIPTION") +def get_var(var_name): + pattern = re.compile(rf"{var_name}\s+=\s+(.*)") + match = pattern.search(init_contents).group(1) + return str(ast.literal_eval(match)) + + +version = get_var("__version__") +PROJECT_NAME = get_var("PROJECT_NAME") +PROJECT_URL = get_var("PROJECT_URL") +PROJECT_AUTHOR = get_var("PROJECT_AUTHOR") +PROJECT_EMAIL = get_var("PROJECT_EMAIL") +PROJECT_DESCRIPTION = get_var("PROJECT_DESCRIPTION") TEST_DIR = "tests" PACKAGES = [ diff --git a/packages/test_selenium/galaxy/project_galaxy_test_selenium.py b/packages/test_selenium/galaxy/project_galaxy_test_selenium.py index 2a04b8b0ce1..ab6b56a913b 100644 --- a/packages/test_selenium/galaxy/project_galaxy_test_selenium.py +++ b/packages/test_selenium/galaxy/project_galaxy_test_selenium.py @@ -6,4 +6,4 @@ PROJECT_URL = "https://github.com/galaxyproject/galaxy" PROJECT_AUTHOR = "Galaxy Project and Community" PROJECT_DESCRIPTION = "Galaxy Selenium Tests" PROJECT_EMAIL = "galaxy-committers@lists.galaxyproject.org" -RAW_CONTENT_URL = "https://raw.github.com/{}/{}/master/".format(PROJECT_USERAME, PROJECT_NAME) +RAW_CONTENT_URL = f"https://raw.github.com/{PROJECT_USERAME}/{PROJECT_NAME}/master/" diff --git a/packages/test_selenium/setup.py b/packages/test_selenium/setup.py index 2036c1de47f..1bfdcdb04e5 100644 --- a/packages/test_selenium/setup.py +++ b/packages/test_selenium/setup.py @@ -14,20 +14,22 @@ SOURCE_DIR = "galaxy" _version_re = re.compile(r"__version__\s+=\s+(.*)") project_short_name = os.path.basename(os.path.dirname(os.path.realpath(__file__))) -with open(f"{SOURCE_DIR}/project_galaxy_{project_short_name}.py", "rb") as f: - init_contents = f.read().decode("utf-8") +with open(f"{SOURCE_DIR}/project_galaxy_{project_short_name}.py") as f: + init_contents = f.read() - def get_var(var_name): - pattern = re.compile(r"%s\s+=\s+(.*)" % var_name) - match = pattern.search(init_contents).group(1) - return str(ast.literal_eval(match)) - version = get_var("__version__") - PROJECT_NAME = get_var("PROJECT_NAME") - PROJECT_URL = get_var("PROJECT_URL") - PROJECT_AUTHOR = get_var("PROJECT_AUTHOR") - PROJECT_EMAIL = get_var("PROJECT_EMAIL") - PROJECT_DESCRIPTION = get_var("PROJECT_DESCRIPTION") +def get_var(var_name): + pattern = re.compile(rf"{var_name}\s+=\s+(.*)") + match = pattern.search(init_contents).group(1) + return str(ast.literal_eval(match)) + + +version = get_var("__version__") +PROJECT_NAME = get_var("PROJECT_NAME") +PROJECT_URL = get_var("PROJECT_URL") +PROJECT_AUTHOR = get_var("PROJECT_AUTHOR") +PROJECT_EMAIL = get_var("PROJECT_EMAIL") +PROJECT_DESCRIPTION = get_var("PROJECT_DESCRIPTION") TEST_DIR = "tests" PACKAGES = [ diff --git a/packages/tool_util/MANIFEST.in b/packages/tool_util/MANIFEST.in index a01c749e7ee..262b6a0b7b8 100644 --- a/packages/tool_util/MANIFEST.in +++ b/packages/tool_util/MANIFEST.in @@ -3,3 +3,4 @@ include galaxy/tool_util/deps/mulled/invfile.lua include galaxy/tool_util/deps/resolvers/default_conda_mapping.yml include galaxy/tool_util/verify/test_config.sample.yml include galaxy/tool_util/xsd/* +include galaxy/tool_util/ontologies/*tsv diff --git a/packages/tool_util/galaxy/project_galaxy_tool_util.py b/packages/tool_util/galaxy/project_galaxy_tool_util.py index a3d3a33444c..6ee12858487 100644 --- a/packages/tool_util/galaxy/project_galaxy_tool_util.py +++ b/packages/tool_util/galaxy/project_galaxy_tool_util.py @@ -6,4 +6,4 @@ PROJECT_URL = "https://github.com/galaxyproject/galaxy" PROJECT_AUTHOR = "Galaxy Project and Community" PROJECT_DESCRIPTION = "Galaxy Tool and Tool Dependency Utilities" PROJECT_EMAIL = "galaxy-committers@lists.galaxyproject.org" -RAW_CONTENT_URL = "https://raw.github.com/{}/{}/master/".format(PROJECT_USERAME, PROJECT_NAME) +RAW_CONTENT_URL = f"https://raw.github.com/{PROJECT_USERAME}/{PROJECT_NAME}/master/" diff --git a/packages/tool_util/setup.py b/packages/tool_util/setup.py index c79d6175352..d8023e328ca 100644 --- a/packages/tool_util/setup.py +++ b/packages/tool_util/setup.py @@ -13,20 +13,23 @@ SOURCE_DIR = "galaxy" _version_re = re.compile(r"__version__\s+=\s+(.*)") -with open("%s/project_galaxy_tool_util.py" % SOURCE_DIR, "rb") as f: - init_contents = f.read().decode("utf-8") +project_short_name = os.path.basename(os.path.dirname(os.path.realpath(__file__))) +with open(f"{SOURCE_DIR}/project_galaxy_{project_short_name}.py") as f: + init_contents = f.read() - def get_var(var_name): - pattern = re.compile(r"%s\s+=\s+(.*)" % var_name) - match = pattern.search(init_contents).group(1) - return str(ast.literal_eval(match)) - version = get_var("__version__") - PROJECT_NAME = get_var("PROJECT_NAME") - PROJECT_URL = get_var("PROJECT_URL") - PROJECT_AUTHOR = get_var("PROJECT_AUTHOR") - PROJECT_EMAIL = get_var("PROJECT_EMAIL") - PROJECT_DESCRIPTION = get_var("PROJECT_DESCRIPTION") +def get_var(var_name): + pattern = re.compile(rf"{var_name}\s+=\s+(.*)") + match = pattern.search(init_contents).group(1) + return str(ast.literal_eval(match)) + + +version = get_var("__version__") +PROJECT_NAME = get_var("PROJECT_NAME") +PROJECT_URL = get_var("PROJECT_URL") +PROJECT_AUTHOR = get_var("PROJECT_AUTHOR") +PROJECT_EMAIL = get_var("PROJECT_EMAIL") +PROJECT_DESCRIPTION = get_var("PROJECT_DESCRIPTION") TEST_DIR = "tests" PACKAGES = [ @@ -40,6 +43,7 @@ PACKAGES = [ "galaxy.tool_util.deps.resolvers", "galaxy.tool_util.linters", "galaxy.tool_util.locations", + "galaxy.tool_util.ontologies", "galaxy.tool_util.parser", "galaxy.tool_util.toolbox", "galaxy.tool_util.toolbox.filters", diff --git a/packages/util/galaxy/project_galaxy_util.py b/packages/util/galaxy/project_galaxy_util.py index 9cbe67d463c..8f670799cf2 100644 --- a/packages/util/galaxy/project_galaxy_util.py +++ b/packages/util/galaxy/project_galaxy_util.py @@ -6,4 +6,4 @@ PROJECT_URL = "https://github.com/galaxyproject/galaxy" PROJECT_AUTHOR = "Galaxy Project and Community" PROJECT_DESCRIPTION = "Galaxy Generic Utilities" PROJECT_EMAIL = "galaxy-committers@lists.galaxyproject.org" -RAW_CONTENT_URL = "https://raw.github.com/{}/{}/master/".format(PROJECT_USERAME, PROJECT_NAME) +RAW_CONTENT_URL = f"https://raw.github.com/{PROJECT_USERAME}/{PROJECT_NAME}/master/" diff --git a/packages/app/galaxy/version.py b/packages/util/galaxy/version.py similarity index 100% rename from packages/app/galaxy/version.py rename to packages/util/galaxy/version.py diff --git a/packages/util/requirements.txt b/packages/util/requirements.txt index ffffd7b7ee3..0c51c78cf5c 100644 --- a/packages/util/requirements.txt +++ b/packages/util/requirements.txt @@ -1,11 +1,12 @@ bleach boltons docutils -markupsafe +importlib_resources +MarkupSafe packaging pycryptodome -pyyaml +PyYAML requests -routes +Routes typing-extensions zipstream-new diff --git a/packages/util/setup.py b/packages/util/setup.py index 9cf8ce73269..e84ca646425 100644 --- a/packages/util/setup.py +++ b/packages/util/setup.py @@ -13,20 +13,23 @@ SOURCE_DIR = "galaxy" _version_re = re.compile(r"__version__\s+=\s+(.*)") -with open("%s/project_galaxy_util.py" % SOURCE_DIR, "rb") as f: - init_contents = f.read().decode("utf-8") +project_short_name = os.path.basename(os.path.dirname(os.path.realpath(__file__))) +with open(f"{SOURCE_DIR}/project_galaxy_{project_short_name}.py") as f: + init_contents = f.read() - def get_var(var_name): - pattern = re.compile(r"%s\s+=\s+(.*)" % var_name) - match = pattern.search(init_contents).group(1) - return str(ast.literal_eval(match)) - version = get_var("__version__") - PROJECT_NAME = get_var("PROJECT_NAME") - PROJECT_URL = get_var("PROJECT_URL") - PROJECT_AUTHOR = get_var("PROJECT_AUTHOR") - PROJECT_EMAIL = get_var("PROJECT_EMAIL") - PROJECT_DESCRIPTION = get_var("PROJECT_DESCRIPTION") +def get_var(var_name): + pattern = re.compile(rf"{var_name}\s+=\s+(.*)") + match = pattern.search(init_contents).group(1) + return str(ast.literal_eval(match)) + + +version = get_var("__version__") +PROJECT_NAME = get_var("PROJECT_NAME") +PROJECT_URL = get_var("PROJECT_URL") +PROJECT_AUTHOR = get_var("PROJECT_AUTHOR") +PROJECT_EMAIL = get_var("PROJECT_EMAIL") +PROJECT_DESCRIPTION = get_var("PROJECT_DESCRIPTION") TEST_DIR = "tests" PACKAGES = [ diff --git a/packages/web_framework/galaxy/project_galaxy_web_framework.py b/packages/web_framework/galaxy/project_galaxy_web_framework.py index 7b5e0e533a8..118afa2a3b3 100644 --- a/packages/web_framework/galaxy/project_galaxy_web_framework.py +++ b/packages/web_framework/galaxy/project_galaxy_web_framework.py @@ -6,4 +6,4 @@ PROJECT_URL = "https://github.com/galaxyproject/galaxy" PROJECT_AUTHOR = "Galaxy Project and Community" PROJECT_DESCRIPTION = "Galaxy Web Framework" PROJECT_EMAIL = "galaxy-committers@lists.galaxyproject.org" -RAW_CONTENT_URL = "https://raw.github.com/{}/{}/master/".format(PROJECT_USERAME, PROJECT_NAME) +RAW_CONTENT_URL = f"https://raw.github.com/{PROJECT_USERAME}/{PROJECT_NAME}/master/" diff --git a/packages/web_framework/setup.py b/packages/web_framework/setup.py index 172615fa3ac..029e47791d2 100644 --- a/packages/web_framework/setup.py +++ b/packages/web_framework/setup.py @@ -14,20 +14,22 @@ SOURCE_DIR = "galaxy" _version_re = re.compile(r"__version__\s+=\s+(.*)") project_short_name = os.path.basename(os.path.dirname(os.path.realpath(__file__))) -with open(f"{SOURCE_DIR}/project_galaxy_{project_short_name}.py", "rb") as f: - init_contents = f.read().decode("utf-8") +with open(f"{SOURCE_DIR}/project_galaxy_{project_short_name}.py") as f: + init_contents = f.read() - def get_var(var_name): - pattern = re.compile(r"%s\s+=\s+(.*)" % var_name) - match = pattern.search(init_contents).group(1) - return str(ast.literal_eval(match)) - version = get_var("__version__") - PROJECT_NAME = get_var("PROJECT_NAME") - PROJECT_URL = get_var("PROJECT_URL") - PROJECT_AUTHOR = get_var("PROJECT_AUTHOR") - PROJECT_EMAIL = get_var("PROJECT_EMAIL") - PROJECT_DESCRIPTION = get_var("PROJECT_DESCRIPTION") +def get_var(var_name): + pattern = re.compile(rf"{var_name}\s+=\s+(.*)") + match = pattern.search(init_contents).group(1) + return str(ast.literal_eval(match)) + + +version = get_var("__version__") +PROJECT_NAME = get_var("PROJECT_NAME") +PROJECT_URL = get_var("PROJECT_URL") +PROJECT_AUTHOR = get_var("PROJECT_AUTHOR") +PROJECT_EMAIL = get_var("PROJECT_EMAIL") +PROJECT_DESCRIPTION = get_var("PROJECT_DESCRIPTION") TEST_DIR = "tests" PACKAGES = [ diff --git a/packages/web_stack/galaxy/project_galaxy_web_stack.py b/packages/web_stack/galaxy/project_galaxy_web_stack.py index a3a5aafe2be..1059d16a6f8 100644 --- a/packages/web_stack/galaxy/project_galaxy_web_stack.py +++ b/packages/web_stack/galaxy/project_galaxy_web_stack.py @@ -6,4 +6,4 @@ PROJECT_URL = "https://github.com/galaxyproject/galaxy" PROJECT_AUTHOR = "Galaxy Project and Community" PROJECT_DESCRIPTION = "Galaxy Web Strack Abstraction" PROJECT_EMAIL = "galaxy-committers@lists.galaxyproject.org" -RAW_CONTENT_URL = "https://raw.github.com/{}/{}/master/".format(PROJECT_USERAME, PROJECT_NAME) +RAW_CONTENT_URL = f"https://raw.github.com/{PROJECT_USERAME}/{PROJECT_NAME}/master/" diff --git a/packages/web_stack/setup.py b/packages/web_stack/setup.py index b8550991fa7..07451506b18 100644 --- a/packages/web_stack/setup.py +++ b/packages/web_stack/setup.py @@ -14,20 +14,22 @@ SOURCE_DIR = "galaxy" _version_re = re.compile(r"__version__\s+=\s+(.*)") project_short_name = os.path.basename(os.path.dirname(os.path.realpath(__file__))) -with open(f"{SOURCE_DIR}/project_galaxy_{project_short_name}.py", "rb") as f: - init_contents = f.read().decode("utf-8") +with open(f"{SOURCE_DIR}/project_galaxy_{project_short_name}.py") as f: + init_contents = f.read() - def get_var(var_name): - pattern = re.compile(r"%s\s+=\s+(.*)" % var_name) - match = pattern.search(init_contents).group(1) - return str(ast.literal_eval(match)) - version = get_var("__version__") - PROJECT_NAME = get_var("PROJECT_NAME") - PROJECT_URL = get_var("PROJECT_URL") - PROJECT_AUTHOR = get_var("PROJECT_AUTHOR") - PROJECT_EMAIL = get_var("PROJECT_EMAIL") - PROJECT_DESCRIPTION = get_var("PROJECT_DESCRIPTION") +def get_var(var_name): + pattern = re.compile(rf"{var_name}\s+=\s+(.*)") + match = pattern.search(init_contents).group(1) + return str(ast.literal_eval(match)) + + +version = get_var("__version__") +PROJECT_NAME = get_var("PROJECT_NAME") +PROJECT_URL = get_var("PROJECT_URL") +PROJECT_AUTHOR = get_var("PROJECT_AUTHOR") +PROJECT_EMAIL = get_var("PROJECT_EMAIL") +PROJECT_DESCRIPTION = get_var("PROJECT_DESCRIPTION") TEST_DIR = "tests" PACKAGES = [ diff --git a/packages/web_stack/test-requirements.txt b/packages/web_stack/test-requirements.txt index e079f8a6038..527b6063f0f 100644 --- a/packages/web_stack/test-requirements.txt +++ b/packages/web_stack/test-requirements.txt @@ -1 +1,2 @@ +gunicorn pytest diff --git a/packages/webapps/galaxy/project_galaxy_webapps.py b/packages/webapps/galaxy/project_galaxy_webapps.py index ddaf425c5a8..4487c4a2f5e 100644 --- a/packages/webapps/galaxy/project_galaxy_webapps.py +++ b/packages/webapps/galaxy/project_galaxy_webapps.py @@ -6,4 +6,4 @@ PROJECT_URL = "https://github.com/galaxyproject/galaxy" PROJECT_AUTHOR = "Galaxy Project and Community" PROJECT_DESCRIPTION = "Galaxy Web Apps" PROJECT_EMAIL = "jmchilton@gmail.com" -RAW_CONTENT_URL = "https://raw.github.com/{}/{}/master/".format(PROJECT_USERAME, PROJECT_NAME) +RAW_CONTENT_URL = f"https://raw.github.com/{PROJECT_USERAME}/{PROJECT_NAME}/master/" diff --git a/packages/webapps/requirements.txt b/packages/webapps/requirements.txt index 7a195cd8db1..ee968a569dd 100644 --- a/packages/webapps/requirements.txt +++ b/packages/webapps/requirements.txt @@ -1,13 +1,34 @@ galaxy-app +galaxy-data +galaxy-job-execution +galaxy-tool-util +galaxy-util +galaxy-web-framework +a2wsgi +apispec +Babel Cheetah3 fastapi>=0.68.2,!=0.69.0,!=0.70.0,!=0.70.1 fastapi-utils +gunicorn +gxformat2 importlib_resources Mako +MarkupSafe +Paste pydantic +PyJWT +python-dateutil python-multipart # required to support form parsing in FastAPI/Starlette +PyYAML +requests +Routes +SQLAlchemy>=1.4.25,<2 starlette starlette-context +tuswsgi typing-extensions uvicorn +uvloop +WebOb Whoosh diff --git a/packages/webapps/setup.py b/packages/webapps/setup.py index 23aa2956684..c8a8aaff1ee 100644 --- a/packages/webapps/setup.py +++ b/packages/webapps/setup.py @@ -14,20 +14,22 @@ SOURCE_DIR = "galaxy" _version_re = re.compile(r"__version__\s+=\s+(.*)") project_short_name = os.path.basename(os.path.dirname(os.path.realpath(__file__))) -with open(f"{SOURCE_DIR}/project_galaxy_{project_short_name}.py", "rb") as f: - init_contents = f.read().decode("utf-8") +with open(f"{SOURCE_DIR}/project_galaxy_{project_short_name}.py") as f: + init_contents = f.read() - def get_var(var_name): - pattern = re.compile(r"%s\s+=\s+(.*)" % var_name) - match = pattern.search(init_contents).group(1) - return str(ast.literal_eval(match)) - version = get_var("__version__") - PROJECT_NAME = get_var("PROJECT_NAME") - PROJECT_URL = get_var("PROJECT_URL") - PROJECT_AUTHOR = get_var("PROJECT_AUTHOR") - PROJECT_EMAIL = get_var("PROJECT_EMAIL") - PROJECT_DESCRIPTION = get_var("PROJECT_DESCRIPTION") +def get_var(var_name): + pattern = re.compile(rf"{var_name}\s+=\s+(.*)") + match = pattern.search(init_contents).group(1) + return str(ast.literal_eval(match)) + + +version = get_var("__version__") +PROJECT_NAME = get_var("PROJECT_NAME") +PROJECT_URL = get_var("PROJECT_URL") +PROJECT_AUTHOR = get_var("PROJECT_AUTHOR") +PROJECT_EMAIL = get_var("PROJECT_EMAIL") +PROJECT_DESCRIPTION = get_var("PROJECT_DESCRIPTION") TEST_DIR = "tests" PACKAGES = [ diff --git a/packages/webapps/test-requirements.txt b/packages/webapps/test-requirements.txt index e079f8a6038..fa5cdcbb144 100644 --- a/packages/webapps/test-requirements.txt +++ b/packages/webapps/test-requirements.txt @@ -1 +1,2 @@ +httpx pytest diff --git a/packages/webapps/tests/webapps b/packages/webapps/tests/webapps new file mode 120000 index 00000000000..51761702403 --- /dev/null +++ b/packages/webapps/tests/webapps @@ -0,0 +1 @@ +../../../test/unit/webapps/ \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 5c897f85dbd..bcaa54aea77 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,9 @@ extend-exclude = ''' ''' force-exclude = 'lib/galaxy/util/jstree.py' +[tool.darker] +isort = true + [tool.poetry] name = "galaxy" version = "22.01.dev0" @@ -25,7 +28,10 @@ name = "galaxyproject" url = "https://wheels.galaxyproject.org/simple" [tool.poetry.dependencies] +a2wsgi = "*" aiofiles = "*" +alembic = "*" +apispec = "*" Babel = "*" bdbag = "*" Beaker = "1.11.0" @@ -49,7 +55,7 @@ fastapi-utils = "*" fs = "*" future = "*" galaxy_sequence_utils = "*" -gravity = {git = "https://github.com/galaxyproject/gravity.git", branch = "main"} +gravity = ">=0.10.0" gunicorn = "*" gxformat2 = "*" h5py = "*" @@ -72,10 +78,12 @@ psutil = "*" pulsar-galaxy-lib = ">=0.14.13" pycryptodome = "*" pydantic = "*" +PyJWT = "*" pykwalify = "*" pyparsing = "*" pysam = "*" python = ">=3.7,<4" +python-dateutil = "*" python-multipart = "*" # required to support form parsing in FastAPI/Starlette pyuwsgi = "*" PyYAML = "*" @@ -85,7 +93,7 @@ Routes = "*" social-auth-core = {version = "==4.0.3", extras = ["openidconnect"]} sortedcontainers = "*" SQLAlchemy = ">=1.4.25,<2" -sqlalchemy-migrate = "*" +sqlalchemy-migrate = "*" # required to support tool shed sqlitedict = "*" sqlparse = "*" starlette = "*" diff --git a/run.sh b/run.sh index 5b3b7143617..4752ca1702a 100755 --- a/run.sh +++ b/run.sh @@ -50,43 +50,8 @@ if [ "$INITIALIZE_TOOL_DEPENDENCIES" -eq 1 ]; then python ./scripts/manage_tool_dependencies.py init_if_needed fi -[ -n "$GALAXY_UWSGI" ] && APP_WEBSERVER='uwsgi' find_server "${GALAXY_CONFIG_FILE:-none}" galaxy -if [ "$run_server" = "python" -a -n "$GALAXY_RUN_ALL" ]; then - servers=$(sed -n 's/^\[server:\(.*\)\]/\1/ p' "$GALAXY_CONFIG_FILE" | xargs echo) - if [ -z "$stop_daemon_arg_set" -a -z "$daemon_or_restart_arg_set" ]; 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 - for server in $servers; do - echo "Executing: python $server_args --server-name=\"$server\" --pid-file=\"$server.pid\" --log-file=\"$server.log\"" - eval python $server_args --server-name="$server" --pid-file="$server.pid" --log-file="$server.log" - if [ -n "$wait_arg_set" -a -n "$daemon_or_restart_arg_set" ]; then - 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 - fi - done -else - echo "Executing: $run_server $server_args" - # args are properly quoted so use eval - eval GALAXY_ROOT_DIR="." $run_server $server_args -fi +echo "Executing: $run_server $server_args" +# args are properly quoted so use eval +eval GALAXY_ROOT_DIR="." $run_server $server_args diff --git a/run_alembic.sh b/run_alembic.sh new file mode 100755 index 00000000000..3349bbe63c3 --- /dev/null +++ b/run_alembic.sh @@ -0,0 +1,53 @@ +#!/bin/sh + +####### +# Use this script to manage Galaxy and Tool Shed Install migrations. +# (Use the legacy manage_db.sh script to manage Tool Shed migrations.) +# +# NOTE: If your database is empty OR is not under Alembic version control, +# use create_db.sh instead. +# +# We use branch labels to distinguish between the galaxy and the tool_shed_install models, +# so in most cases you'll need to identify the branch to which your command should be applied. +# Use these identifiers: `gxy` for galaxy, and `tsi` for tool_shed_install. +# +# To create a revision for galaxy: +# ./run_alembic.sh revision --head=gxy@head -m "your description" +# +# To create a revision for tool_shed_install: +# ./run_alembic.sh revision --head=tsi@head -m "your description" +# +# To upgrade: +# ./run_alembic.sh upgrade gxy@head # upgrade gxy to head revision +# ./run_alembic.sh upgrade gxy@+1 # upgrade gxy to 1 revision above current +# ./run_alembic.sh upgrade [revision identifier] # upgrade gxy to a specific revision +# ./run_alembic.sh upgrade [revision identifier]+1 # upgrade gxy to 1 revision above specific revision +# ./run_alembic.sh upgrade heads # upgrade gxy and tsi to head revisions +# +# To downgrade: +# ./run_alembic.sh downgrade gxy@base # downgrade gxy to base (empty db with empty alembic table) +# ./run_alembic.sh downgrade gxy@-1 # downgrade gxy to 1 revision below current +# ./run_alembic.sh downgrade [revision identifier] # downgrade gxy to a specific revision +# ./run_alembic.sh downgrade [revision identifier]-1 # downgrade gxy to 1 revision below specific revision +# +# To pass a galaxy config file, use `--galaxy-config` +# +# You may also override the galaxy database url and/or the +# tool shed install database url, as well as the database_template +# and database_encoding configuration options with env vars: +# GALAXY_CONFIG_OVERRIDE_DATABASE_CONNECTION=my-db-url ./run_alembic.sh ... +# GALAXY_INSTALL_CONFIG_OVERRIDE_DATABASE_CONNECTION=my-other-db-url ./run_alembic.sh ... +# +# For more options, see Alembic's documentation at https://alembic.sqlalchemy.org +####### + +ALEMBIC_CONFIG='lib/galaxy/model/migrations/alembic.ini' + +cd `dirname $0` + +. ./scripts/common_startup_functions.sh + +setup_python + +find lib/galaxy/model/migrations/alembic -name '*.pyc' -delete +python ./scripts/migrate_db.py --config "$ALEMBIC_CONFIG" "$@" diff --git a/run_tests.sh b/run_tests.sh index 2aea9c68718..5c407b52cbd 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -61,7 +61,7 @@ as ``test_path``. A few examples are shown below. Run all API tests: ./run_tests.sh -api -The same test as above can be run using nosetests directly as follows: +The same test as above can be run using pytest directly as follows: pytest lib/galaxy_test/api However when using pytest directly output options defined in this @@ -503,7 +503,7 @@ do -u|-unit|--unit) report_file="run_unit_tests.html" test_script="pytest" - unit_extra='--doctest-modules --ignore lib/galaxy/web/proxy/js/node_modules/ --ignore lib/tool_shed/webapp/controllers --ignore lib/galaxy/jobs/runners/chronos.py --ignore lib/tool_shed/webapp/model/migrate --ignore lib/galaxy/tools/bundled --ignore lib/galaxy_test --ignore lib/tool_shed/test' + unit_extra='--doctest-modules --ignore lib/galaxy/web/proxy/js/node_modules/ --ignore lib/tool_shed/webapp/controllers --ignore lib/galaxy/jobs/runners/chronos.py --ignore lib/tool_shed/webapp/model/migrate --ignore lib/galaxy/tools/bundled --ignore lib/galaxy_test --ignore lib/tool_shed/test --ignore lib/galaxy/model/migrations/alembic' if [ $# -gt 1 ]; then unit_extra="$unit_extra $2" shift 2 @@ -574,7 +574,7 @@ do shift fi # Maybe we shouldn't break here but for now to pass more than one argument to the - # underlying test driver (scripts/nosetests.py) use -- instead. + # underlying test driver use -- instead. break ;; esac @@ -644,7 +644,7 @@ fi export GALAXY_TEST_TOOL_CONF if [ "$test_script" = 'pytest' ]; then if [ "$coverage_arg" = '--with-coverage' ]; then - coverage_arg="--cov-report term --cov=lib" + coverage_arg="--cov-report xml --cov-report term --cov=lib" fi if [ -n "$marker" ]; then marker_args=(-m "$marker") diff --git a/run_tool_shed.sh b/run_tool_shed.sh index 91af7bb7114..562261484f9 100755 --- a/run_tool_shed.sh +++ b/run_tool_shed.sh @@ -26,7 +26,7 @@ if [ $? -eq 0 ] ; then fi if [ -z "$TOOL_SHED_CONFIG_FILE" ]; then - TOOL_SHED_CONFIG_FILE=$(PYTHONPATH=lib python -c "from __future__ import print_function; from galaxy.util.properties import find_config_file; print(find_config_file(['tool_shed', 'tool_shed_wsgi']) or '')") + TOOL_SHED_CONFIG_FILE=$(PYTHONPATH=lib python -c "from __future__ import print_function; from galaxy.util.properties import find_config_file; print(find_config_file(['tool_shed', 'tool_shed_wsgi'], include_samples=True) or '')") export TOOL_SHED_CONFIG_FILE fi diff --git a/scripts/api/library_upload_dir.py b/scripts/api/library_upload_dir.py index 9a5f89ca78b..22d8be59a31 100644 --- a/scripts/api/library_upload_dir.py +++ b/scripts/api/library_upload_dir.py @@ -35,7 +35,7 @@ class Uploader: uploading_to = [x for x in existing if x["id"] == self.folder_id] if len(uploading_to) == 0: - raise Exception("Unknown folder [%s] in library [%s]" % (self.folder_id, self.library_id)) + raise Exception(f"Unknown folder [{self.folder_id}] in library [{self.library_id}]") else: uploading_to = uploading_to[0] diff --git a/scripts/check_model.py b/scripts/check_model.py index 58819421495..3f411963270 100644 --- a/scripts/check_model.py +++ b/scripts/check_model.py @@ -17,6 +17,7 @@ from sqlalchemy import ( from galaxy.model import mapping from galaxy.model.orm.scripts import get_config +from galaxy.model.tool_shed_install import mapping as tsi_mapping IndexTuple = namedtuple("IndexTuple", "table column_names") @@ -37,17 +38,22 @@ def find_missing_indexes(): indexes[index_tuple] = index.name return indexes - # load metadata from mapping.py + # load metadata from mapping.py and tool_shed_install/mapping.py metadata = mapping.metadata mapping_indexes = load_indexes(metadata) + tsi_metadata = tsi_mapping.metadata + tsi_mapping_indexes = load_indexes(tsi_metadata) + # create EMPTY metadata, then load from database db_url = get_config(sys.argv)["db_url"] metadata = MetaData(bind=create_engine(db_url)) metadata.reflect() indexes_in_db = load_indexes(metadata) - missing_indexes = set(mapping_indexes.keys()) - set(indexes_in_db.keys()) + all_indexes = set(mapping_indexes.keys()) | set(tsi_mapping_indexes.keys()) + + missing_indexes = all_indexes - set(indexes_in_db.keys()) if missing_indexes: return [(mapping_indexes[index], index.table, index.column_names) for index in missing_indexes] diff --git a/scripts/cleanup_datasets/cleanup_datasets.py b/scripts/cleanup_datasets/cleanup_datasets.py index 2b5abe9c808..8d5834b3b4f 100755 --- a/scripts/cleanup_datasets/cleanup_datasets.py +++ b/scripts/cleanup_datasets/cleanup_datasets.py @@ -19,13 +19,14 @@ from sqlalchemy import ( null, true, ) -from sqlalchemy.orm import eagerload +from sqlalchemy.orm import joinedload sys.path.insert(1, os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, "lib"))) import galaxy.config from galaxy.datatypes.registry import Registry from galaxy.exceptions import ObjectNotFound +from galaxy.model.mapping import init_models_from_config from galaxy.objectstore import build_object_store_from_config from galaxy.util import unicodify from galaxy.util.script import ( @@ -257,7 +258,7 @@ def purge_histories(app, cutoff_time, remove_from_disk, info_only=False, force_r histories = ( app.sa_session.query(app.model.History) .filter(and_(app.model.History.table.c.deleted == true(), app.model.History.update_time < cutoff_time)) - .options(eagerload("datasets")) + .options(joinedload("datasets")) ) else: histories = ( @@ -269,7 +270,7 @@ def purge_histories(app, cutoff_time, remove_from_disk, info_only=False, force_r app.model.History.update_time < cutoff_time, ) ) - .options(eagerload("datasets")) + .options(joinedload("datasets")) ) for history in histories: log.info("### Processing history id %d (%s)", history.id, unicodify(history.name)) @@ -679,7 +680,7 @@ class CleanupDatasetsApplication: def __init__(self, config): self.object_store = build_object_store_from_config(config) # Setup the database engine and ORM - self.model = galaxy.config.init_models_from_config(config, object_store=self.object_store) + self.model = init_models_from_config(config, object_store=self.object_store) registry = Registry() registry.load_datatypes() galaxy.model.set_datatypes_registry(registry) diff --git a/scripts/cleanup_datasets/pgcleanup.py b/scripts/cleanup_datasets/pgcleanup.py index b7183d3aa29..5b28ecfea5a 100755 --- a/scripts/cleanup_datasets/pgcleanup.py +++ b/scripts/cleanup_datasets/pgcleanup.py @@ -13,6 +13,7 @@ import os import string import sys import time +import uuid from collections import namedtuple from functools import partial @@ -257,8 +258,11 @@ class RemovesObjects: def collect_removed_object_info(self, row): object_id = getattr(row, self.id_column, None) + object_uuid = getattr(row, self.uuid_column, None) + if object_uuid: + object_uuid = str(uuid.UUID(object_uuid)) if object_id: - self.objects_to_remove.add(self.object_class(object_id, row.object_store_id)) + self.objects_to_remove.add(self.object_class(object_id, row.object_store_id, object_uuid)) def remove_objects(self): for object_to_remove in sorted(self.objects_to_remove): @@ -305,6 +309,7 @@ class PurgesHDAs: WHERE purged_hda_ids.id = metadata_file.hda_id RETURNING metadata_file.hda_id AS hda_id, metadata_file.id AS id, + metadata_file.uuid AS uuid, metadata_file.object_store_id AS object_store_id), deleted_icda_ids AS ( UPDATE implicitly_converted_dataset_association @@ -405,32 +410,44 @@ class RequiresDiskUsageRecalculation: class RemovesMetadataFiles(RemovesObjects): """Causes MetadataFiles to be removed from the object store. - To use, ensure your query returns ``deleted_metadata_file_id`` and ``object_store_id`` columns. + To use, ensure your query returns ``deleted_metadata_file_id``, ``object_store_id``, and + ``deleted_metadata_file_uuid`` columns. """ - object_class = namedtuple("MetadataFile", ["id", "object_store_id"]) + object_class = namedtuple("MetadataFile", ["id", "object_store_id", "uuid"]) id_column = "deleted_metadata_file_id" + uuid_column = "deleted_metadata_file_uuid" def remove_object(self, metadata_file): + store_by = self.object_store.get_store_by(metadata_file) + if store_by == "uuid": + alt_name = f"metadata_{metadata_file.uuid}.dat" + else: + alt_name = f"metadata_{metadata_file.id}.dat" self.remove_from_object_store( - metadata_file, - dict(extra_dir="_metadata_files", extra_dir_at_root=True, alt_name="metadata_%d.dat" % metadata_file.id), + metadata_file, dict(extra_dir="_metadata_files", extra_dir_at_root=True, alt_name=alt_name) ) class RemovesDatasets(RemovesObjects): """Causes Datasets to be removed from the object store. - To use, ensure your query returns ``purged_dataset_id`` and ``object_store_id`` columns. + To use, ensure your query returns ``purged_dataset_id``, ``object_store_id``, and ``purged_dataset_uuid`` columns. """ - object_class = namedtuple("Dataset", ["id", "object_store_id"]) + object_class = namedtuple("Dataset", ["id", "object_store_id", "uuid"]) id_column = "purged_dataset_id" + uuid_column = "purged_dataset_uuid" def remove_object(self, dataset): + store_by = self.object_store.get_store_by(dataset) + if store_by == "uuid": + extra_dir = f"dataset_{dataset.uuid}_files" + else: + extra_dir = f"dataset_{dataset.id}_files" self.remove_from_object_store(dataset, dict()) self.remove_from_object_store( - dataset, dict(dir_only=True, extra_dir="dataset_%d_files" % dataset.id), entire_dir=True, check_exists=True + dataset, dict(dir_only=True, extra_dir=extra_dir), entire_dir=True, check_exists=True ) @@ -606,6 +623,7 @@ class PurgeDeletedUsers(PurgesHDAs, RemovesMetadataFiles, Action): purged_history_ids.id AS purged_history_id, purged_hda_ids.id AS purged_hda_id, deleted_metadata_file_ids.id AS deleted_metadata_file_id, + deleted_metadata_file_ids.uuid AS deleted_metadata_file_uuid, deleted_metadata_file_ids.object_store_id AS object_store_id, deleted_icda_ids.id AS deleted_icda_id, deleted_icda_ids.hda_id AS deleted_icda_hda_id, @@ -741,6 +759,7 @@ class PurgeDeletedUsersGDPR(PurgesHDAs, RemovesMetadataFiles, Action): purged_history_ids.id AS purged_history_id, purged_hda_ids.id AS purged_hda_id, deleted_metadata_file_ids.id AS deleted_metadata_file_id, + deleted_metadata_file_ids.uuid AS deleted_metadata_file_uuid, deleted_metadata_file_ids.object_store_id AS object_store_id, deleted_icda_ids.id AS deleted_icda_id, deleted_icda_ids.hda_id AS deleted_icda_hda_id, @@ -807,6 +826,7 @@ class PurgeDeletedHDAs(PurgesHDAs, RemovesMetadataFiles, RequiresDiskUsageRecalc SELECT purged_hda_ids.id AS purged_hda_id, history.user_id AS recalculate_disk_usage_user_id, deleted_metadata_file_ids.id AS deleted_metadata_file_id, + deleted_metadata_file_ids.uuid AS deleted_metadata_file_uuid, deleted_metadata_file_ids.object_store_id AS object_store_id, deleted_icda_ids.id AS deleted_icda_id, deleted_icda_ids.hda_id AS deleted_icda_hda_id @@ -845,6 +865,7 @@ class PurgeHistorylessHDAs(PurgesHDAs, RemovesMetadataFiles, RequiresDiskUsageRe {purge_hda_dependencies_sql} SELECT purged_hda_ids.id AS purged_hda_id, deleted_metadata_file_ids.id AS deleted_metadata_file_id, + deleted_metadata_file_ids.uuid AS deleted_metadata_file_uuid, deleted_metadata_file_ids.object_store_id AS object_store_id, deleted_icda_ids.id AS deleted_icda_id, deleted_icda_ids.hda_id AS deleted_icda_hda_id @@ -887,6 +908,7 @@ class PurgeErrorHDAs(PurgesHDAs, RemovesMetadataFiles, RequiresDiskUsageRecalcul SELECT purged_hda_ids.id AS purged_hda_id, history.user_id AS recalculate_disk_usage_user_id, deleted_metadata_file_ids.id AS deleted_metadata_file_id, + deleted_metadata_file_ids.uuid AS deleted_metadata_file_uuid, deleted_metadata_file_ids.object_store_id AS object_store_id, deleted_icda_ids.id AS deleted_icda_id, deleted_icda_ids.hda_id AS deleted_icda_hda_id @@ -931,6 +953,7 @@ class PurgeHDAsOfPurgedHistories(PurgesHDAs, RequiresDiskUsageRecalculation, Act SELECT purged_hda_ids.id AS purged_hda_id, history.user_id AS recalculate_disk_usage_user_id, deleted_metadata_file_ids.id AS deleted_metadata_file_id, + deleted_metadata_file_ids.uuid AS deleted_metadata_file_uuid, deleted_metadata_file_ids.object_store_id AS object_store_id, deleted_icda_ids.id AS deleted_icda_id, deleted_icda_ids.hda_id AS deleted_icda_hda_id @@ -983,6 +1006,7 @@ class PurgeDeletedHistories(PurgesHDAs, RequiresDiskUsageRecalculation, Action): purged_history_ids.user_id AS recalculate_disk_usage_user_id, purged_hda_ids.id AS purged_hda_id, deleted_metadata_file_ids.id AS deleted_metadata_file_id, + deleted_metadata_file_ids.uuid AS deleted_metadata_file_uuid, deleted_metadata_file_ids.object_store_id AS object_store_id, deleted_icda_ids.id AS deleted_icda_id, deleted_icda_ids.hda_id AS deleted_icda_hda_id @@ -1077,6 +1101,7 @@ class PurgeDatasets(RemovesDatasets, Action): WHERE deleted{force_retry_sql} AND update_time < (NOW() AT TIME ZONE 'utc' - interval '%(days)s days') RETURNING id, + uuid, object_store_id), dataset_events AS (INSERT INTO cleanup_event_dataset_association @@ -1084,6 +1109,7 @@ class PurgeDatasets(RemovesDatasets, Action): SELECT NOW() AT TIME ZONE 'utc', %(event_id)s, id FROM purged_dataset_ids) SELECT id AS purged_dataset_id, + uuid AS purged_dataset_uuid, object_store_id AS object_store_id FROM purged_dataset_ids ORDER BY id @@ -1127,7 +1153,7 @@ class Cleanup: @property def conn(self): if self.__conn is None: - url = make_url(galaxy.config.get_database_url(self.config)) + url = make_url(self.config.database_connection) log.info(f"Connecting to database with URL: {url}") args = url.translate_connect_args(username="user") args.update(url.query) diff --git a/scripts/cleanup_datasets/populate_uuid.py b/scripts/cleanup_datasets/populate_uuid.py index 597cd0e849d..b41f2f6db38 100755 --- a/scripts/cleanup_datasets/populate_uuid.py +++ b/scripts/cleanup_datasets/populate_uuid.py @@ -15,6 +15,7 @@ import uuid sys.path.insert(1, os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, "lib"))) import galaxy.config +from galaxy.model.mapping import init_models_from_config from galaxy.util.script import ( app_properties_from_args, populate_config_args, @@ -35,7 +36,7 @@ def main(): app_properties = app_properties_from_args(args) config = galaxy.config.Configuration(**app_properties) - model = galaxy.config.init_models_from_config(config) + model = init_models_from_config(config) for row in model.context.query(model.Dataset): if row.uuid is None: diff --git a/scripts/common_startup.sh b/scripts/common_startup.sh index 146956b38e7..561504675c8 100755 --- a/scripts/common_startup.sh +++ b/scripts/common_startup.sh @@ -122,7 +122,6 @@ if [ $SET_VENV -eq 1 ] && [ $CREATE_VENV -eq 1 ]; then conda_activate fi virtualenv "$GALAXY_VIRTUAL_ENV" - setup_gravity_state_dir else # If $GALAXY_VIRTUAL_ENV does not exist, and there is no conda available, attempt to create it. if [ -z "$GALAXY_PYTHON" ]; then @@ -140,21 +139,16 @@ if [ $SET_VENV -eq 1 ] && [ $CREATE_VENV -eq 1 ]; then echo "existing environment before starting Galaxy." if command -v virtualenv >/dev/null; then virtualenv -p "$GALAXY_PYTHON" "$GALAXY_VIRTUAL_ENV" - setup_gravity_state_dir else - vvers=16.7.9 - vurl="https://files.pythonhosted.org/packages/source/v/virtualenv/virtualenv-${vvers}.tar.gz" - vsha=0d62c70883c0342d59c11d0ddac0d954d0431321a41ab20851facf2b222598f3 + min_python_version=3.7 + vurl="https://bootstrap.pypa.io/virtualenv/${min_python_version}/virtualenv.pyz" vtmp=$(mktemp -d -t galaxy-virtualenv-XXXXXX) vsrc="$vtmp/$(basename $vurl)" - # SSL certificates are not checked to prevent problems with messed - # up client cert environments. We verify the download using a known - # good sha256 sum instead. echo "Fetching $vurl" if command -v curl >/dev/null; then - curl --insecure -L -o "$vsrc" "$vurl" + curl -L -o "$vsrc" "$vurl" elif command -v wget >/dev/null; then - wget --no-check-certificate -O "$vsrc" "$vurl" + wget -O "$vsrc" "$vurl" else "$GALAXY_PYTHON" -c "try: from urllib import urlretrieve @@ -162,15 +156,12 @@ except: from urllib.request import urlretrieve urlretrieve('$vurl', '$vsrc')" fi - echo "Verifying $vsrc checksum is $vsha" - "$GALAXY_PYTHON" -c "import hashlib; assert hashlib.sha256(open('$vsrc', 'rb').read()).hexdigest() == '$vsha', '$vsrc: invalid checksum'" - tar zxf "$vsrc" -C "$vtmp" - "$GALAXY_PYTHON" "$vtmp/virtualenv-$vvers/virtualenv.py" "$GALAXY_VIRTUAL_ENV" + "$GALAXY_PYTHON" "$vsrc" "$GALAXY_VIRTUAL_ENV" rm -rf "$vtmp" - setup_gravity_state_dir fi fi fi + setup_gravity_state_dir fi # activate virtualenv or conda env, sets $GALAXY_VIRTUAL_ENV and $GALAXY_CONDA_ENV @@ -235,6 +226,10 @@ fi if [ -n "$VIRTUAL_ENV" ]; then if ! in_venv "$(command -v node)" || [ "$(node --version)" != "v${NODE_VERSION}" ]; then echo "Installing node into $VIRTUAL_ENV with nodeenv." + if [ -d "${VIRTUAL_ENV}/lib/node_modules" ]; then + echo "Removing old ${VIRTUAL_ENV}/lib/node_modules directory." + rm -rf "${VIRTUAL_ENV}/lib/node_modules" + fi nodeenv -n "$NODE_VERSION" -p fi elif [ -n "$CONDA_DEFAULT_ENV" ] && [ -n "$CONDA_EXE" ]; then diff --git a/scripts/common_startup_functions.sh b/scripts/common_startup_functions.sh index d7b57308cad..24a491e8be0 100644 --- a/scripts/common_startup_functions.sh +++ b/scripts/common_startup_functions.sh @@ -28,7 +28,6 @@ parse_common_args() { gravity_args="stop" paster_args="$paster_args --stop-daemon" add_pid_arg=1 - uwsgi_args="$uwsgi_args --stop \"$PID_FILE\"" stop_daemon_arg_set=1 shift ;; @@ -37,7 +36,6 @@ parse_common_args() { paster_args="$paster_args restart" add_pid_arg=1 add_log_arg=1 - uwsgi_args="$uwsgi_args --reload \"$PID_FILE\"" restart_arg_set=1 daemon_or_restart_arg_set=1 shift @@ -45,12 +43,11 @@ parse_common_args() { --daemon|start) gravity_args="start" paster_args="$paster_args --daemon" - gunicorn_args="$gunicorn_args --daemon" + gunicorn_args="$gunicorn_args --daemon --capture-output" add_pid_arg=1 add_log_arg=1 # --daemonize2 waits until after the application has loaded # to daemonize, thus it stops if any errors are found - uwsgi_args="--master --daemonize2 \"$LOG_FILE\" --pidfile2 \"$PID_FILE\" $uwsgi_args" daemon_or_restart_arg_set=1 shift ;; @@ -69,7 +66,6 @@ parse_common_args() { ;; *) paster_args="$paster_args $1" - uwsgi_args="$uwsgi_args $1" shift ;; esac @@ -162,49 +158,28 @@ find_server() { gunicorn_worker="galaxy.webapps.galaxy.workers.Worker" ;; reports) - # TODO: don't override this var, and is this really the only way to configure the port? - GUNICORN_CMD_ARGS="--bind=localhost:9001" + # TODO: is this really the only way to configure the port? + GUNICORN_CMD_ARGS=${GUNICORN_CMD_ARGS:-"--bind=localhost:9001 --config lib/galaxy/web_stack/gunicorn_config.py"} ;; + tool_shed) + GUNICORN_CMD_ARGS=${GUNICORN_CMD_ARGS:-"--bind=localhost:9009 --config lib/galaxy/web_stack/gunicorn_config.py"} esac APP_WEBSERVER=${APP_WEBSERVER:-$default_webserver} - if [ "$APP_WEBSERVER" = "uwsgi" ]; then - # Look for uwsgi - if [ -z "$skip_venv" ] && [ -x $GALAXY_VIRTUAL_ENV/bin/uwsgi ]; then - UWSGI=$GALAXY_VIRTUAL_ENV/bin/uwsgi - elif command -v uwsgi >/dev/null 2>&1; then - UWSGI=uwsgi - else - echo 'ERROR: Could not find uwsgi executable' - exit 1 - fi - [ "$server_config" != "none" ] && arg_getter_args="-c \"$server_config\"" - [ -n "$server_app" ] && arg_getter_args="$arg_getter_args --app $server_app" - run_server="$UWSGI" - server_args= - if [ -z "$stop_daemon_arg_set" ] && [ -z "$restart_arg_set" ]; then - server_args="$(eval python ./scripts/get_uwsgi_args.py $arg_getter_args)" - fi - server_args="$server_args $uwsgi_args" - elif [ "$APP_WEBSERVER" = "gunicorn" ]; then - export GUNICORN_CMD_ARGS="${GUNICORN_CMD_ARGS:-\"--bind=localhost:8080\"}" + if [ "$APP_WEBSERVER" = "gunicorn" ]; then run_server="gunicorn" - server_args="'galaxy.webapps.${server_app}.fast_factory:factory()' --pythonpath lib -k ${gunicorn_worker:-$default_gunicorn_worker} $gunicorn_args" + export GUNICORN_CMD_ARGS + if [ "$server_app" = "tool_shed" ]; then + server_args="'${server_app}.webapp.fast_factory:factory()' --pythonpath lib -k ${gunicorn_worker:-$default_gunicorn_worker} $gunicorn_args" + else + server_args="'galaxy.webapps.${server_app}.fast_factory:factory()' --pythonpath lib -k ${gunicorn_worker:-$default_gunicorn_worker} $gunicorn_args" + fi if [ "$add_pid_arg" -eq 1 ]; then server_args="$server_args --pid \"$PID_FILE\"" fi if [ "$add_log_arg" -eq 1 ]; then server_args="$server_args --log-file \"$LOG_FILE\"" fi - elif [ "$APP_WEBSERVER" = "paste" ]; then - run_server="python" - server_args="./scripts/paster.py serve \"$server_config\" $paster_args" - if [ "$add_pid_arg" -eq 1 ]; then - server_args="$server_args --pid-file \"$PID_FILE\"" - fi - if [ "$add_log_arg" -eq 1 ]; then - server_args="$server_args --log-file \"$LOG_FILE\"" - fi else if [ "$add_log_arg" -eq 1 ]; then GALAXY_DAEMON_LOG="${GALAXY_LOG:-galaxy.log}" diff --git a/scripts/create_db.py b/scripts/create_db.py index c3e986c8cfb..c9d5b750084 100755 --- a/scripts/create_db.py +++ b/scripts/create_db.py @@ -1,19 +1,12 @@ """ -Creates the initial galaxy database schema using the settings defined in -config/galaxy.ini. - -This script is also wrapped by create_db.sh. - -.. note: pass '-c /location/to/your_config.ini' for non-standard ini file -locations. - -.. note: if no database_connection is set in galaxy.ini, the default, sqlite -database will be constructed. - Using the database_file setting in galaxy.ini will create the file at the - settings location (??) - -.. seealso: galaxy.ini, specifically the settings: database_connection and -database file +This script retrieves relevant configuration values and verifies the state of +the Galaxy and Tool Shed Install database(s). +There may be one combined database (galaxy and tool shed install) or two +separate databases. +If the database does not exist or is empty, it will be created and initialized. +(See inline comments in lib/galaxy/model/migrations/__init__.py for details on +how other database states are handled). +It is wrapped by create_db.sh (see that file for usage). """ import logging import os.path @@ -21,23 +14,16 @@ import sys sys.path.insert(1, os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, "lib"))) -from galaxy.model.migrate.check import create_or_verify_database as create_db -from galaxy.model.orm.scripts import get_config -from galaxy.model.tool_shed_install.migrate.check import create_or_verify_database as create_install_db -from tool_shed.webapp.model.migrate.check import create_or_verify_database as create_tool_shed_db +from galaxy.model.migrations import verify_databases_via_script +from galaxy.model.migrations.scripts import get_configuration logging.basicConfig(level=logging.DEBUG) log = logging.getLogger(__name__) def invoke_create(): - config = get_config(sys.argv) - if config["database"] == "galaxy": - create_db(config["db_url"], config["config_file"], map_install_models=not config["install_database_connection"]) - elif config["database"] == "tool_shed": - create_tool_shed_db(config["db_url"]) - elif config["database"] == "install": - create_install_db(config["db_url"]) + gxy_config, tsi_config, is_auto_migrate = get_configuration(sys.argv, os.getcwd()) + verify_databases_via_script(gxy_config, tsi_config, is_auto_migrate) if __name__ == "__main__": diff --git a/scripts/create_toolshed_db.py b/scripts/create_toolshed_db.py new file mode 100755 index 00000000000..aee1fe839f0 --- /dev/null +++ b/scripts/create_toolshed_db.py @@ -0,0 +1,29 @@ +""" +Creates the initial tool shed database schema using the settings defined in config/tool_shed.yml. + +Note: pass '-c /location/to/your_config.yml' for non-standard ini file locations. + +Note: if no database_connection is set in tool_shed.yml, the default, sqlite database will be constructed. + +This script is also wrapped by create_ts_db.sh. +""" +import logging +import os.path +import sys + +sys.path.insert(1, os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, "lib"))) + +from galaxy.model.orm.scripts import get_config +from tool_shed.webapp.model.migrate.check import create_or_verify_database as create_tool_shed_db + +logging.basicConfig(level=logging.DEBUG) +log = logging.getLogger(__name__) + + +def invoke_create(): + config = get_config(sys.argv) + create_tool_shed_db(config["db_url"]) + + +if __name__ == "__main__": + invoke_create() diff --git a/scripts/galaxy b/scripts/galaxy deleted file mode 100755 index 323875c78fd..00000000000 --- a/scripts/galaxy +++ /dev/null @@ -1,126 +0,0 @@ -#!/bin/bash - -show_help() { -cat <&2 - exit 1 - fi - ;; - -c|--config) - if [ $# -gt 1 ]; then - GALAXY_CONFIG_DIR=$2 - shift 2 - else - echo "--config requires explicit argument" 1>&2 - exit 1 - fi - ;; - --) - shift - break - ;; - *) - break; - ;; - esac -done - -SCRIPTS_DIRECTORY="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -PROJECT_DIRECTORY=$SCRIPTS_DIRECTORY/.. - -GALAXY_CONFIG_DIR=${GALAXY_CONFIG_DIR:-"./config"} -export GALAXY_CONFIG_DIR - -GALAXY_LOCAL_ENV=${GALAXY_LOCAL_ENV:-$GALAXY_CONFIG_DIR/local_env.sh} -export GALAXY_LOCAL_ENV - -GALAXY_VIRTUALENV=${GALAXY_VIRTUALENV:-$PROJECT_DIRECTORY/.venv} -export GALAXY_VIRTUALENV - -if [ -e $GALAXY_LOCAL_ENV ]; -then - echo "Sourcing file $GALAXY_LOCAL_ENV" - . $GALAXY_LOCAL_ENV -fi - -if [ -d $GALAXY_VIRTUALENV ]; -then - echo "Activating virtual environment $GALAXY_VIRTUALENV/bin/activate" - . $GALAXY_VIRTUALENV/bin/activate -fi - -GALAXY_CONFIG_SAMPLE_FILE="${PROJECT_DIRECTORY}/galaxy.ini.sample" -if [ -z "$GALAXY_CONFIG_FILE" ]; then - if [ -f "${GALAXY_CONFIG_DIR}/galaxy.ini" ]; then - GALAXY_CONFIG_FILE="${GALAXY_CONFIG_DIR}/galaxy.ini" - else - GALAXY_CONFIG_FILE="$GALAXY_CONFIG_SAMPLE_FILE" - fi - export GALAXY_CONFIG_FILE -fi - -if [ -z "$MODE" ]; -then - if hash uwsgi 2>/dev/null; then - MODE="uwsgi" - else - MODE="paster" - fi -fi - -if [ "$MODE" == "uwsgi" ]; then - echo "Starting galaxy with command [uwsgi --ini-paste \"$GALAXY_CONFIG_FILE\" \"$@\"]" - uwsgi --ini-paste "$GALAXY_CONFIG_FILE" "$@" -elif [ "$MODE" == "paster" ]; then - echo "Starting galaxy with command [galaxy-paster serve \"$GALAXY_CONFIG_FILE\" \"$@\"]" - galaxy-paster serve "$GALAXY_CONFIG_FILE" "$@" -elif [ "$MODE" == "webless" ]; then - if hash galaxy-main 2>/dev/null; then - # Running from pip install since - # galaxy-main is available on PATH. - echo "Starting galaxy with command [galaxy-main]" - galaxy-main - else - # Running locally. - PROJECT_DIRECTORY=$GALAXY_SCRIPTS_DIR/.. - cd $PROJECT_DIRECTORY - export PYTHONPATH=.:$PYTHONPATH - echo "Starting galaxy with command [python lib/galaxy/main.py]" - python lib/pulsar/main.py - fi -else - echo "Unknown mode passed to --mode argument." 1>&2 - exit 1 -fi diff --git a/scripts/galaxy_main.py b/scripts/galaxy_main.py index 6afaa3a0115..02ef7392860 100755 --- a/scripts/galaxy_main.py +++ b/scripts/galaxy_main.py @@ -16,11 +16,6 @@ defaults logging to a single file with the following: galaxy-main -d --server-name handler0 --daemon-log-file=handler0-daemon.log --pid-file handler0.pid --log-file handler0.log -This can also be used to start Galaxy as a uWSGI mule, e.g. for job handling: - -uwsgi ... --py-call-osafterfork --mule=lib/galaxy/main.py --mule=lib/galaxy/main.py --farm=job-handlers:1,2 - -The --py-call-osafterfork allows for proper shutdown on SIGTERM/SIGINT. """ import functools import logging @@ -37,11 +32,6 @@ try: except ImportError: Daemonize = None -try: - import uwsgi -except ImportError: - uwsgi = None - log = logging.getLogger(__name__) real_file = os.path.realpath(__file__) @@ -73,9 +63,6 @@ DEFAULT_PID = "galaxy.pid" DEFAULT_VERBOSE = True DESCRIPTION = "Daemonized entry point for Galaxy." -SHUTDOWN_MSG = "__SHUTDOWN__" -UWSGI_FARMS_VAR = "_GALAXY_UWSGI_FARM_NAMES" - exit = threading.Event() @@ -109,13 +96,6 @@ def load_galaxy_app(config_builder, config_env=False, log=None, attach_to_pools= def handle_signal(signum, frame): log.info("Received signal %d, exiting", signum) - if uwsgi and "mule_id" in dir(uwsgi) and uwsgi.mule_id() > 0: - farms = os.environ.get(UWSGI_FARMS_VAR, None) - if farms: - for farm in farms.split(","): - uwsgi.farm_msg(farm, SHUTDOWN_MSG) - else: - uwsgi.mule_msg(SHUTDOWN_MSG, uwsgi.mule_id()) exit.set() diff --git a/scripts/get_platforms.py b/scripts/get_platforms.py deleted file mode 100755 index 13f0d88e140..00000000000 --- a/scripts/get_platforms.py +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env python - -import os -import sys - -assert sys.version_info[:2] >= (2, 6) - -lib = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, "lib")) -sys.path.insert(1, lib) - -import pkg_resources - -print(pkg_resources.get_platform()) diff --git a/scripts/get_uwsgi_args.py b/scripts/get_uwsgi_args.py deleted file mode 100644 index 486bee14861..00000000000 --- a/scripts/get_uwsgi_args.py +++ /dev/null @@ -1,142 +0,0 @@ -import os -import shlex -import sys - -sys.path.insert(1, os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, "lib"))) - -from galaxy.util.path import get_ext -from galaxy.util.properties import ( - load_app_properties, - nice_config_parser, -) -from galaxy.util.script import main_factory - -DESCRIPTION = "Script to determine uWSGI command line arguments" -# socket is not an alias for http, but it is assumed that if you configure a socket in your uwsgi config you do not -# want to run the default http server (or you can configure it yourself) -ALIASES = { - "virtualenv": ("home", "venv", "pyhome"), - "pythonpath": ("python-path", "pp"), - "http": ("httprouter", "socket", "uwsgi-socket", "suwsgi-socket", "ssl-socket"), - "module": ("mount",), # mount is not actually an alias for module, but we don't want to set module if mount is set -} -DEFAULT_ARGS = { - "_all_": ( - "pythonpath", - "threads", - "buffer-size", - "http", - "static-map", - "die-on-term", - "hook-master-start", - "enable-threads", - "umask", - ), - "galaxy": ("py-call-osafterfork",), - "reports": (), - "tool_shed": ("cron",), -} -DEFAULT_PORTS = { - "galaxy": 8080, - "reports": 9001, - "tool_shed": 9009, -} - - -def __arg_set(arg, kwargs): - if arg in kwargs: - return True - for alias in ALIASES.get(arg, ()): - if alias in kwargs: - return True - return False - - -def __add_arg(args, arg, value): - optarg = "--%s" % arg - if isinstance(value, bool): - if value is True: - args.append(optarg) - elif isinstance(value, str): - # the = in --optarg=value is usually, but not always, optional - if value.startswith("="): - args.append(shlex.quote(optarg + value)) - else: - args.append(optarg) - args.append(shlex.quote(value)) - else: - [__add_arg(args, arg, v) for v in value] - - -def __add_config_file_arg(args, config_file, app): - ext = None - if config_file: - ext = get_ext(config_file) - if ext in ("yaml", "json"): - __add_arg(args, ext, config_file) - elif ext == "ini": - config = nice_config_parser(config_file) - has_logging = config.has_section("loggers") - if config.has_section("app:main"): - # uWSGI does not have any way to set the app name when loading with paste.deploy:loadapp(), so hardcoding - # the name to `main` is fine - __add_arg(args, "ini-paste" if not has_logging else "ini-paste-logged", config_file) - return # do not add --module - else: - __add_arg(args, ext, config_file) - if has_logging: - __add_arg(args, "paste-logger", True) - - -def _get_uwsgi_args(cliargs, kwargs): - # it'd be nice if we didn't have to reparse here but we need things out of more than one section - config_file = cliargs.config_file or kwargs.get("__file__") - uwsgi_kwargs = load_app_properties(config_file=config_file, config_section="uwsgi") - args = [] - ts_cron_config_option = "" if config_file is None else "-c %s" % config_file - defaults = { - "pythonpath": "lib", - "threads": "4", - "buffer-size": "16384", # https://github.com/galaxyproject/galaxy/issues/1530 - "http": f"localhost:{DEFAULT_PORTS[cliargs.app]}", - "static-map": (f"/static={os.getcwd()}/static", f"/favicon.ico={os.getcwd()}/static/favicon.ico"), - "die-on-term": True, - "enable-threads": True, - "hook-master-start": ("unix_signal:2 gracefully_kill_them_all", "unix_signal:15 gracefully_kill_them_all"), - "py-call-osafterfork": True, - "cron": "0 -1 -1 -1 -1 python scripts/tool_shed/build_ts_whoosh_index.py %s --config-section tool_shed -d" - % ts_cron_config_option, - "umask": "027", - } - __add_config_file_arg(args, config_file, cliargs.app) - if not __arg_set("module", uwsgi_kwargs): - if cliargs.app in ["tool_shed"]: - __add_arg(args, "module", "tool_shed.webapp.buildapp:uwsgi_app()") - else: - __add_arg(args, "module", f"galaxy.webapps.{cliargs.app}.buildapp:uwsgi_app()") - # only include virtualenv if it's set/exists, otherwise this breaks conda-env'd Galaxy - if not __arg_set("virtualenv", uwsgi_kwargs) and ("VIRTUAL_ENV" in os.environ or os.path.exists(".venv")): - __add_arg(args, "virtualenv", os.environ.get("VIRTUAL_ENV", ".venv")) - - # We always want to append client/src/assets as static-safe. - __add_arg(args, "static-safe", f"{os.getcwd()}/client/src/assets") - - # Do not let uwsgi remap stdin to /dev/null if galaxy is in debug mode - galaxy_kwargs = load_app_properties(config_file=config_file, config_section="galaxy") - if __arg_set("debug", galaxy_kwargs) and not __arg_set("honour-stdin", uwsgi_kwargs): - __add_arg(args, "honour-stdin", True) - - for arg in DEFAULT_ARGS["_all_"] + DEFAULT_ARGS[cliargs.app]: - if not __arg_set(arg, uwsgi_kwargs): - __add_arg(args, arg, defaults[arg]) - print(" ".join(args)) - - -ACTIONS = { - "get_uwsgi_args": _get_uwsgi_args, -} - - -if __name__ == "__main__": - main = main_factory(description=DESCRIPTION, actions=ACTIONS, default_action="get_uwsgi_args") - main() diff --git a/scripts/grt/export.py b/scripts/grt/export.py index f2194a8376e..7d073e714bc 100644 --- a/scripts/grt/export.py +++ b/scripts/grt/export.py @@ -18,6 +18,7 @@ sys.path.insert(1, os.path.abspath(os.path.join(os.path.dirname(__file__), os.pa import galaxy import galaxy.app import galaxy.config +from galaxy.model.mapping import init_models_from_config from galaxy.objectstore import build_object_store_from_config from galaxy.util import ( hash_util, @@ -41,7 +42,7 @@ def _init(args): "The database connection is empty. If you are using the default value, please uncomment that in your galaxy.yml" ) - model = galaxy.config.init_models_from_config(config, object_store=object_store) + model = init_models_from_config(config, object_store=object_store) return ( model, object_store, diff --git a/scripts/helper.py b/scripts/helper.py index 2f15a681941..64934a66c12 100644 --- a/scripts/helper.py +++ b/scripts/helper.py @@ -12,6 +12,7 @@ import sys sys.path.insert(1, os.path.join(os.path.dirname(__file__), os.pardir, "lib")) import galaxy.config +from galaxy.model.mapping import init_models_from_config from galaxy.security import idencoding from galaxy.util.script import ( app_properties_from_args, @@ -29,7 +30,7 @@ args = parser.parse_args() app_properties = app_properties_from_args(args) config = galaxy.config.Configuration(**app_properties) helper = idencoding.IdEncodingHelper(id_secret=app_properties.get("id_secret")) -model = galaxy.config.init_models_from_config(config) +model = init_models_from_config(config) if args.encode_id: print(f'Encoded "{args.encode_id}": {helper.encode_id(args.encode_id)}') diff --git a/scripts/interactivetools/key_type_token_mapping.py b/scripts/interactivetools/key_type_token_mapping.py deleted file mode 100644 index edfdbabd9d9..00000000000 --- a/scripts/interactivetools/key_type_token_mapping.py +++ /dev/null @@ -1,101 +0,0 @@ -import itertools -import sqlite3 -from threading import RLock -from time import time - -import uwsgi - -realtime_db_file = uwsgi.opt["interactivetools_map"].decode("utf-8") -db_conn = sqlite3.connect(realtime_db_file) - -DATABASE_TABLE_NAME = "gxitproxy" - - -class CacheEntry: - def __init__(self, key, value, ttl=20): - self.key = key - self.value = value - self.expires_at = time() + ttl - self._expired = False - - def expired(self): - if self._expired is False: - return self.expires_at < time() - else: - return self._expired - - -class CacheList: - def __init__(self): - self.entries = [] - self.lock = RLock() - - def add_entry(self, key, value, ttl=20): - with self.lock: - self.entries.append(CacheEntry(key, value, ttl)) - - def read_entries(self): - with self.lock: - self.entries = list(itertools.dropwhile(lambda x: x.expired(), self.entries)) - return self.entries - - def get_entry_value(self, key, default=None): - entries = self.read_entries() - for entry in entries: - if entry.key == key: - return entry.value - return default - - -key_type_token_mapped_cache = CacheList() - - -def args_as_unicode(func): - def wrap_args(*args): - args = (arg.decode("utf-8") if isinstance(arg, bytes) else arg for arg in args) - return func(*args) - - return wrap_args - - -@args_as_unicode -def key_type_token_mapper_cached(key, key_type, token, route_extra, url, ttl): - global key_type_token_mapped_cache - cache_key = (key, key_type, token) - entry = key_type_token_mapped_cache.get_entry_value(cache_key, None) - if entry is None: - entry = key_type_token_mapper(key, key_type, token, route_extra, url) - if entry is not None: - # Should we cache empty/not authorized entries, perhaps for shorter time? - key_type_token_mapped_cache.add_entry(cache_key, entry, ttl=float(ttl)) - return entry - - -@args_as_unicode -def key_type_token_mapper(key, key_type, token, route_extra, url): - global db_conn - # print 'key %s key_type %s token %s route_extra %s url %s\n' % (key, key_type, token, route_extra, url) - if key and key_type and token: - # sqlite3.ProgrammingError: SQLite objects created in a thread can only be used in that same thread. The object was created in thread id x and this is thread id y. - # So try up to 2 times - for _ in range(2): - # Order by rowid gives us the last row added - try: - row = db_conn.execute( - "SELECT host, port FROM %s WHERE key=? AND key_type=? AND token=? ORDER BY rowid DESC LIMIT 1" - % (DATABASE_TABLE_NAME), - (key, key_type, token), - ).fetchone() - if row: - rval = "%s:%s" % (tuple(row)) - return rval.encode() - break - except sqlite3.ProgrammingError: - db_conn = sqlite3.connect(realtime_db_file) - continue - break - return None - - -uwsgi.register_rpc("rtt_key_type_token_mapper", key_type_token_mapper) -uwsgi.register_rpc("rtt_key_type_token_mapper_cached", key_type_token_mapper_cached) diff --git a/scripts/manage_db.py b/scripts/manage_db.py index 4bbf2eb4abf..f68580ee2c6 100644 --- a/scripts/manage_db.py +++ b/scripts/manage_db.py @@ -1,22 +1,58 @@ -""" This script parses Galaxy or Tool Shed config file for database connection -and then delegates to sqlalchemy_migrate shell main function in -migrate.versioning.shell. """ +""" +This script is legacy and should not be used directly. It is intended to be +used by the ansible galaxy and toolshed roles. For managing the database, please +consult manage_db.sh. +""" import logging import os.path import sys -from migrate.versioning.shell import main - sys.path.insert(1, os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, "lib"))) - -from galaxy.model.orm.scripts import get_config +from galaxy.model.migrations.scripts import LegacyManageDb logging.basicConfig(level=logging.DEBUG) log = logging.getLogger(__name__) -def invoke_migrate_main(): - # Migrate has its own args, so cannot use argparse +def run(): + """ + If the target database is 'tool_shed', delegate to sqlalchemy migrate. + Otherwise, handle with Alembic. + """ + if sys.argv[-1] == "tool_shed": + _run_sqlalchemy_migrate_on_toolshed() + else: + arg = _get_command_argument() + lmdb = LegacyManageDb() + if arg == "version": + result = lmdb.get_gxy_version() + elif arg == "db_version": + result = lmdb.get_gxy_db_version() + else: + result = lmdb.run_upgrade() + if result: + print(result) + + +def _get_command_argument(): + """ + If last argument is a valid command, pop and return it; otherwise raise exception. + """ + arg = sys.argv[-1] + if arg in ["version", "db_version", "upgrade"]: + return arg + else: + raise Exception("Invalid command argument; should be: 'version', 'db_version', or 'upgrade'") + + +def _run_sqlalchemy_migrate_on_toolshed(): + # This is the only case when we use SQLAlchemy Migrate. + # This intentionally duplicates the code in `migrate_toolshed_db.py`. + # The dependency on `migrate` should be removed prior to the move to SQLAlchemy 2.0. + from migrate.versioning.shell import main + + from galaxy.model.orm.scripts import get_config + config = get_config(sys.argv, use_argparse=False, cwd=os.getcwd()) db_url = config["db_url"] repo = config["repo"] @@ -25,4 +61,4 @@ def invoke_migrate_main(): if __name__ == "__main__": - invoke_migrate_main() + run() diff --git a/scripts/manage_db_adapter.py b/scripts/manage_db_adapter.py new file mode 100644 index 00000000000..8026c54bc9a --- /dev/null +++ b/scripts/manage_db_adapter.py @@ -0,0 +1,38 @@ +""" +This script is intended to be invoked by the legacy manage_db.sh script. +It translates the arguments supplied to manage_db.sh into the format used +by migrate_db.py. + +INPUT: | OUTPUT: +---------------------------------------------------------- +upgrade --version=foo | upgrade foo +upgrade --version foo | upgrade foo +upgrade | upgrade heads (if using a combined db for galaxy and install) +upgrade | upgrade gxy@head (if using separate dbs for galaxy and install) +upgrade install | upgrade tsi@head +upgrade --version=bar install | upgrade bar +upgrade -c path-to-galaxy.yml | upgrade --galaxy-config path-to-galaxy.yml gxy@head + +The converted sys.argv will include `-c path-to-alembic.ini`. +The optional `-c` argument name is renamed to `--galaxy-config`. +""" + +import os +import sys + +sys.path.insert(1, os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, "lib"))) + +from galaxy.model.migrations.scripts import ( + invoke_alembic, + LegacyScripts, +) + + +def run(): + ls = LegacyScripts(sys.argv, os.getcwd()) + ls.run() + invoke_alembic() + + +if __name__ == "__main__": + run() diff --git a/scripts/migrate_db.py b/scripts/migrate_db.py new file mode 100755 index 00000000000..78f976ff158 --- /dev/null +++ b/scripts/migrate_db.py @@ -0,0 +1,29 @@ +""" +This script retrieves relevant configuration values and invokes +the Alembic console runner. +It is wrapped by run_alembic.sh (see that file for usage). +""" +import logging +import os.path +import sys + +sys.path.insert(1, os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, "lib"))) + +from galaxy.model.migrations.scripts import ( + add_db_urls_to_command_arguments, + get_configuration, + invoke_alembic, +) + +logging.basicConfig(level=logging.DEBUG) +log = logging.getLogger(__name__) + + +def run(): + gxy_config, tsi_config, _ = get_configuration(sys.argv, os.getcwd()) + add_db_urls_to_command_arguments(sys.argv, gxy_config.url, tsi_config.url) + invoke_alembic() + + +if __name__ == "__main__": + run() diff --git a/scripts/migrate_toolshed_db.py b/scripts/migrate_toolshed_db.py new file mode 100755 index 00000000000..26052126ed6 --- /dev/null +++ b/scripts/migrate_toolshed_db.py @@ -0,0 +1,31 @@ +""" +This script parses the Tool Shed config file for database connection +and then delegates to sqlalchemy_migrate shell main function in +migrate.versioning.shell. +It is wrapped by manage_db.sh (see that file for usage). +""" +import logging +import os.path +import sys + +from migrate.versioning.shell import main + +sys.path.insert(1, os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, "lib"))) + +from galaxy.model.orm.scripts import get_config + +logging.basicConfig(level=logging.DEBUG) +log = logging.getLogger(__name__) + + +def invoke_migrate_main(): + # Migrate has its own args, so cannot use argparse + config = get_config(sys.argv, use_argparse=False, cwd=os.getcwd()) + db_url = config["db_url"] + repo = config["repo"] + + main(repository=repo, url=db_url) + + +if __name__ == "__main__": + invoke_migrate_main() diff --git a/scripts/nosetests.py b/scripts/nosetests.py deleted file mode 100755 index 4f65208a02e..00000000000 --- a/scripts/nosetests.py +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env python -# EASY-INSTALL-ENTRY-SCRIPT: 'nose','console_scripts','nosetests' -# __requires__ = 'nose' -import sys - -from pkg_resources import load_entry_point - -assert sys.version_info[:2] >= (2, 7) - -nose_core_TestProgram = load_entry_point("nose", "console_scripts", "nosetests") -nose_core_TestProgram() diff --git a/scripts/pages_identifier_conversion.py b/scripts/pages_identifier_conversion.py index 256df5d3cd5..cfaa53a7b80 100644 --- a/scripts/pages_identifier_conversion.py +++ b/scripts/pages_identifier_conversion.py @@ -13,6 +13,7 @@ from galaxy.managers.pages import ( PageContentProcessor, placeholderRenderForSave, ) +from galaxy.model.mapping import init_models_from_config from galaxy.objectstore import build_object_store_from_config from galaxy.security.idencoding import IdEncodingHelper from galaxy.util import unicodify @@ -39,7 +40,7 @@ def main(argv): "The database connection is empty. If you are using the default value, please uncomment that in your galaxy.yml" ) - model = galaxy.config.init_models_from_config(config, object_store=object_store) + model = init_models_from_config(config, object_store=object_store) session = model.context.current pagerevs = session.query(model.PageRevision).all() mock_trans = Bunch(app=Bunch(security=security_helper), model=model, user_is_admin=lambda: True, sa_session=session) diff --git a/scripts/paster.py b/scripts/paster.py deleted file mode 100755 index 1734fcab583..00000000000 --- a/scripts/paster.py +++ /dev/null @@ -1,23 +0,0 @@ -""" -Bootstrap the Galaxy framework. - -This should not be called directly! Use the run.sh script in Galaxy's -top level directly. -""" - -import os -import sys - -sys.path.insert(1, os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, "lib"))) - -from check_python import check_python # noqa: I100, I201 - -from galaxy.util.pastescript import serve - -# ensure supported version -try: - check_python() -except Exception: - sys.exit(1) - -serve.run() diff --git a/scripts/release-diff.py b/scripts/release-diff.py index 2d545562a6b..98739758100 100644 --- a/scripts/release-diff.py +++ b/scripts/release-diff.py @@ -87,7 +87,7 @@ def _report_dict(title, subheading, data, mapper): def _indent(s, by=4): whitespace = " " * by s = s if isinstance(s, list) else s.splitlines() - return "\n".join((f"{whitespace}{line}" for line in s)) + return "\n".join(f"{whitespace}{line}" for line in s) def report_diff(added, changed, removed, new_files): @@ -135,8 +135,6 @@ def main(old_revision, new_revision=None): globs = ( "config/*.yml.sample", "lib/galaxy/config/schemas/*schema.yml", - "lib/galaxy/webapps/reports/config_schema.yml", - "lib/tool_shed/webapp/config_schema.yml", ) files_to_diff = [f for g in globs for f in glob.glob(g)] added = {} @@ -148,8 +146,6 @@ def main(old_revision, new_revision=None): filename = file if "config_schema.yml" in file: filename = "config/galaxy.yml.sample:galaxy" - elif "uwsgi_schema.yml" in file: - filename = "config/galaxy.yml.sample:uwsgi" real_path = Path(file).resolve().relative_to(Path.cwd()) try: diff --git a/scripts/runtime_stats.py b/scripts/runtime_stats.py index a746f267adf..378da1a0e3c 100755 --- a/scripts/runtime_stats.py +++ b/scripts/runtime_stats.py @@ -100,11 +100,11 @@ def parse_arguments(): args.source = args.source.lower() if args.source not in ("metrics", "history"): - print("ERROR: Data source `%s` unknown, valid source are: %s" % (args.source, ", ".join(DATA_SOURCES))) + print("ERROR: Data source `{}` unknown, valid source are: {}".format(args.source, ", ".join(DATA_SOURCES))) app_properties = app_properties_from_args(args) config = galaxy.config.Configuration(**app_properties) - uri = args.config.get_database_url(config) + uri = config.database_connection names = {"database": "dbname", "username": "user"} args.connect_args = url.make_url(uri).translate_connect_args(**names) diff --git a/scripts/set_dataset_sizes.py b/scripts/set_dataset_sizes.py index 8d1f070d5eb..0c9b4460abf 100644 --- a/scripts/set_dataset_sizes.py +++ b/scripts/set_dataset_sizes.py @@ -7,6 +7,7 @@ import sys sys.path.insert(1, os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, "lib"))) import galaxy.config +from galaxy.model.mapping import init_models_from_config from galaxy.objectstore import build_object_store_from_config from galaxy.util.script import ( app_properties_from_args, @@ -23,7 +24,7 @@ def init(): config = galaxy.config.Configuration(**app_properties) object_store = build_object_store_from_config(config) - model = galaxy.config.init_models_from_config(config, object_store=object_store) + model = init_models_from_config(config, object_store=object_store) return model, object_store diff --git a/scripts/set_user_disk_usage.py b/scripts/set_user_disk_usage.py index ca0d401a780..1aad6d48a16 100755 --- a/scripts/set_user_disk_usage.py +++ b/scripts/set_user_disk_usage.py @@ -7,6 +7,7 @@ import sys sys.path.insert(1, os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, "lib"))) import galaxy.config +from galaxy.model.mapping import init_models_from_config from galaxy.objectstore import build_object_store_from_config from galaxy.util import nice_size from galaxy.util.script import ( @@ -40,8 +41,8 @@ def init(): app_properties = app_properties_from_args(args) config = galaxy.config.Configuration(**app_properties) object_store = build_object_store_from_config(config) - engine = galaxy.config.get_database_url(config).split(":")[0] - return galaxy.config.init_models_from_config(config, object_store=object_store), object_store, engine + engine = config.database_connection.split(":")[0] + return init_models_from_config(config, object_store=object_store), object_store, engine def quotacheck(sa_session, users, engine): diff --git a/scripts/tool_shed/api/common.py b/scripts/tool_shed/api/common.py index df594cafccb..16ad847ec52 100644 --- a/scripts/tool_shed/api/common.py +++ b/scripts/tool_shed/api/common.py @@ -1,61 +1,4 @@ -import json -import os -import sys -from urllib.error import HTTPError -from urllib.request import ( - build_opener, - HTTPRedirectHandler, - install_opener, - Request, - urlopen, -) - -sys.path.insert(1, os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, os.pardir, "lib")) - -from galaxy import util -from tool_shed.util import hg_util - - -class HTTPRedirectWithDataHandler(HTTPRedirectHandler): - def __init__(self, method): - """ - Upon first inspection, it would seem that this shouldn't be necessary, but for some reason - not having a constructor explicitly set the request method breaks PUT requests. - """ - self.valid_methods = ["GET", "HEAD", "POST", "PUT", "DELETE"] - self.redirect_codes = ["301", "302", "303", "307"] - self.method = method - - def redirect_request(self, request, fp, code, msg, headers, new_url): - request_method = request.get_method() - if str(code) in self.redirect_codes and request_method in self.valid_methods: - new_url = new_url.replace(" ", "%20") - request = Request( - new_url, - data=request.data, - headers=request.headers, - origin_req_host=request.get_origin_req_host(), - unverifiable=True, - ) - if self.method in self.valid_methods: - if request.get_method() != self.method: - request.get_method = lambda: self.method - return request - else: - HTTPRedirectHandler.redirect_request(request, fp, code, msg, headers, new_url) - - -def build_request_with_data(url, data, api_key, method): - """Build a request with the received method.""" - http_redirect_with_data_handler = HTTPRedirectWithDataHandler(method=method) - opener = build_opener(http_redirect_with_data_handler) - install_opener(opener) - url = make_url(url, api_key=api_key, args=None) - request = Request(url, headers={"Content-Type": "application/json"}, data=json.dumps(data)) - request_method = request.get_method() - if request_method != method: - request.get_method = lambda: method - return opener, request +import requests def delete(api_key, url, data, return_formatted=True): @@ -63,17 +6,12 @@ def delete(api_key, url, data, return_formatted=True): Sends an API DELETE request and acts as a generic formatter for the JSON response. The 'data' will become the JSON payload read by the Tool Shed. """ - try: - opener, request = build_request_with_data(url, data, api_key, "DELETE") - delete_request = opener.open(request) - response = json.loads(delete_request.read()) - except HTTPError as e: - if return_formatted: - print(e) - print(e.read(1024)) - sys.exit(1) - else: - return dict(status="error", message=str(e.read(1024))) + headers = {} + if api_key: + headers["x-api-key"] = api_key + response = requests.delete(url, headers=headers) + response.raise_for_status() + response = response.json() if return_formatted: print("Response") print("--------") @@ -84,16 +22,7 @@ def delete(api_key, url, data, return_formatted=True): def display(url, api_key=None, return_formatted=True): """Sends an API GET request and acts as a generic formatter for the JSON response.""" - try: - r = get(url, api_key=api_key) - except HTTPError as e: - print(e) - # Only return the first 1K of errors. - print(e.read(1024)) - sys.exit(1) - if isinstance(r, str): - print("error: %s" % r) - return None + r = get(url, api_key=api_key) if not return_formatted: return r elif isinstance(r, list): @@ -123,119 +52,32 @@ def display(url, api_key=None, return_formatted=True): def get(url, api_key=None): """Do the GET.""" - url = make_url(url, api_key=api_key, args=None) - try: - return json.loads(urlopen(url).read()) - except ValueError: - sys.exit("URL did not return JSON data") - - -def get_api_url(base, parts, params=None): - """Compose and return a URL for the Tool Shed API.""" - if "api" in parts and parts.index("api") != 0: - parts.pop(parts.index("api")) - parts.insert(0, "api") - elif "api" not in parts: - parts.insert(0, "api") - url = util.build_url(base, pathspec=parts, params=params) - return url - - -def get_latest_downloadable_changeset_revision_via_api(url, name, owner): - """ - Return the latest downloadable changeset revision for the repository defined by the received - name and owner. - """ - error_message = "" - parts = ["api", "repositories", "get_ordered_installable_revisions"] - params = dict(name=name, owner=owner) - api_url = get_api_url(base=url, parts=parts, params=params) - changeset_revisions, error_message = json_from_url(api_url) - if changeset_revisions is None or error_message: - return None, error_message - if len(changeset_revisions) >= 1: - return changeset_revisions[-1], error_message - return hg_util.INITIAL_CHANGELOG_HASH, error_message - - -def get_repository_dict(url, repository_dict): - """ - Send a request to the Tool Shed to get additional information about the repository defined - by the received repository_dict. Add the information to the repository_dict and return it. - """ - error_message = "" - if not isinstance(repository_dict, dict): - error_message = "Invalid repository_dict received: %s" % str(repository_dict) - return None, error_message - repository_id = repository_dict.get("repository_id", None) - if repository_id is None: - error_message = "Invalid repository_dict does not contain a repository_id entry: %s" % str(repository_dict) - return None, error_message - parts = ["api", "repositories", repository_id] - api_url = get_api_url(base=url, parts=parts) - extended_dict, error_message = json_from_url(api_url) - if extended_dict is None or error_message: - return None, error_message - name = extended_dict.get("name", None) - owner = extended_dict.get("owner", None) - if name is not None and owner is not None: - name = str(name) - owner = str(owner) - latest_changeset_revision, error_message = get_latest_downloadable_changeset_revision_via_api(url, name, owner) - if latest_changeset_revision is None or error_message: - return None, error_message - extended_dict["latest_revision"] = str(latest_changeset_revision) - return extended_dict, error_message - else: - error_message = "Invalid extended_dict does not contain name or owner entries: %s" % str(extended_dict) - return None, error_message - - -def json_from_url(url): - """Send a request to the Tool Shed via the Tool Shed API and handle the response.""" - error_message = "" - url_handle = urlopen(url) - url_contents = url_handle.read() - try: - parsed_json = json.loads(url_contents) - except Exception as e: - error_message = str(url_contents) - print("Error parsing JSON data in json_from_url():", e) - return None, error_message - return parsed_json, error_message - - -def make_url(url, api_key=None, args=None): - """Adds the API Key to the URL if it's not already there.""" - if args is None: - args = [] - argsep = "&" - if "?" not in url: - argsep = "?" + headers = {} if api_key: - if "?key=" not in url and "&key=" not in url: - args.insert(0, ("key", api_key)) - return url + argsep + "&".join("=".join(t) for t in args) + headers["x-api-key"] = api_key + response = requests.get(url, headers=headers) + response.raise_for_status() + return response.json() def post(url, data, api_key=None): """Do the POST.""" - try: - opener, request = build_request_with_data(url, data, api_key, "POST") - post_request = opener.open(request) - return json.loads(post_request.read()) - except HTTPError as e: - return dict(status="error", message=str(e.read(1024))) + headers = {} + if api_key: + headers["x-api-key"] = api_key + response = requests.post(url, data, headers=headers) + response.raise_for_status() + return response.json() def put(url, data, api_key=None): """Do the PUT.""" - try: - opener, request = build_request_with_data(url, data, api_key, "PUT") - put_request = opener.open(request) - return json.loads(put_request.read()) - except HTTPError as e: - return dict(status="error", message=str(e.read(1024))) + headers = {} + if api_key: + headers["x-api-key"] = api_key + response = requests.put(url, data, headers=headers) + response.raise_for_status() + return response.json() def submit(url, data, api_key=None, return_formatted=True): @@ -243,15 +85,7 @@ def submit(url, data, api_key=None, return_formatted=True): Sends an API POST request and acts as a generic formatter for the JSON response. The 'data' will become the JSON payload read by the Tool Shed. """ - try: - response = post(url, data, api_key=api_key) - except HTTPError as e: - if return_formatted: - print(e) - print(e.read(1024)) - sys.exit(1) - else: - return dict(status="error", message=str(e.read(1024))) + response = post(url, data, api_key) if not return_formatted: return response print("Response") @@ -280,15 +114,7 @@ def update(api_key, url, data, return_formatted=True): Sends an API PUT request and acts as a generic formatter for the JSON response. The 'data' will become the JSON payload read by the Tool Shed. """ - try: - response = put(url, data, api_key=api_key) - except HTTPError as e: - if return_formatted: - print(e) - print(e.read(1024)) - sys.exit(1) - else: - return dict(status="error", message=str(e.read(1024))) + response = put(url, data, api_key=api_key) if return_formatted: print("Response") print("--------") diff --git a/scripts/tool_shed/bootstrap_tool_shed/bootstrap_tool_shed.sh b/scripts/tool_shed/bootstrap_tool_shed/bootstrap_tool_shed.sh index ce7005f9fc1..26e3c99a5f1 100755 --- a/scripts/tool_shed/bootstrap_tool_shed/bootstrap_tool_shed.sh +++ b/scripts/tool_shed/bootstrap_tool_shed/bootstrap_tool_shed.sh @@ -1,17 +1,25 @@ -#!/bin/bash +#!/usr/bin/env bash + +set -x # Activate the virtualenv, if it exists. [ -f ./.venv/bin/activate ] && . ./.venv/bin/activate -: ${TOOL_SHED_CONFIG_FILE:=config/tool_shed.yml.sample} +: "${TOOL_SHED_CONFIG_FILE:=lib/galaxy/config/sample/tool_shed.yml.sample}" +LOCAL_SHED_URL="${LOCAL_SHED_URL:-http://localhost:9009}" +TOOL_SHED_PID=tool_shed_bootstrap.pid +TOOL_SHED_LOG=tool_shed_bootstrap.log +export TOOL_SHED_PID +export TOOL_SHED_LOG +export TOOL_SHED_CONFIG_FILE stop_err() { - echo $1 - python ./scripts/paster.py serve ${TOOL_SHED_CONFIG_FILE} --pid-file=tool_shed_bootstrap.pid --log-file=tool_shed_bootstrap.log --stop-daemon + echo "$1" + kill "$(cat $TOOL_SHED_PID)" exit 1 } -tool_shed=`./scripts/tool_shed/bootstrap_tool_shed/parse_run_sh_args.sh $@` +tool_shed=$(./scripts/tool_shed/bootstrap_tool_shed/parse_run_sh_args.sh "$@") if [ $? -ne 0 ] ; then exit 0 @@ -19,7 +27,7 @@ fi log_file="scripts/tool_shed/bootstrap_tool_shed/bootstrap.log" -database_result=`python ./scripts/tool_shed/bootstrap_tool_shed/bootstrap_util.py --execute check_db --config_file ${TOOL_SHED_CONFIG_FILE}` +database_result="$(python ./scripts/tool_shed/bootstrap_tool_shed/bootstrap_util.py --execute check_db --config_file ${TOOL_SHED_CONFIG_FILE})" if [ $? -ne 0 ] ; then stop_err "Unable to bootstrap tool shed. $database_result" @@ -27,7 +35,7 @@ fi echo "Bootstrapping from tool shed at $tool_shed." echo -n "Creating database... " -python scripts/create_db.py tool_shed +python scripts/create_toolshed_db.py tool_shed if [ $? -eq 0 ] ; then echo "done." @@ -36,13 +44,12 @@ else fi if [ $? -eq 0 ] ; then - user_auth=`python ./scripts/tool_shed/bootstrap_tool_shed/bootstrap_util.py --execute admin_user_info --config_file ${TOOL_SHED_CONFIG_FILE}` - local_shed_url=`python ./scripts/tool_shed/bootstrap_tool_shed/bootstrap_util.py --execute get_url --config_file ${TOOL_SHED_CONFIG_FILE}` + user_auth=$(python ./scripts/tool_shed/bootstrap_tool_shed/bootstrap_util.py --execute admin_user_info --config_file ${TOOL_SHED_CONFIG_FILE}) fi -admin_user_name=`echo $user_auth | awk 'BEGIN { FS="__SEP__" } ; { print \$1 }'` -admin_user_email=`echo $user_auth | awk 'BEGIN { FS="__SEP__" } ; { print \$2 }'` -admin_user_password=`echo $user_auth | awk 'BEGIN { FS="__SEP__" } ; { print \$3 }'` +admin_user_name=$(echo "$user_auth" | awk 'BEGIN { FS="__SEP__" } ; { print $1 }') +admin_user_email=$(echo "$user_auth" | awk 'BEGIN { FS="__SEP__" } ; { print $2 }') +admin_user_password=$(echo "$user_auth" | awk 'BEGIN { FS="__SEP__" } ; { print $3 }') echo -n "Creating user '$admin_user_name' with email address '$admin_user_email'..." @@ -50,16 +57,15 @@ python ./scripts/tool_shed/bootstrap_tool_shed/create_user_with_api_key.py -c ${ echo " done." -sed -i"bak" "s/#admin_users = user1@example.org,user2@example.org/admin_users = $admin_user_email/" "${TOOL_SHED_CONFIG_FILE}" +export TOOL_SHED_CONFIG_ADMIN_USERS=$admin_user_email echo -n "Starting tool shed in order to populate users and categories... " -if [ -f tool_shed_bootstrap.pid ] ; then + +if [ -f "$TOOL_SHED_PID" ] ; then stop_err "A bootstrap process is already running." fi -python ./scripts/paster.py serve ${TOOL_SHED_CONFIG_FILE} --pid-file=tool_shed_bootstrap.pid --log-file=tool_shed_bootstrap.log --daemon > /dev/null - -shed_pid=`cat tool_shed_bootstrap.pid` +./run_tool_shed.sh -bootstrap_from_tool_shed --daemon > /dev/null while : ; do tail -n 1 tool_shed_bootstrap.log | grep -q "Removing PID file tool_shed_webapp.pid" @@ -71,20 +77,20 @@ while : ; do echo "========================================" stop_err " " fi - tail -n 2 tool_shed_bootstrap.log | grep -q "Starting server in PID $shed_pid" + tail -n 2 tool_shed_bootstrap.log | grep -q "Application startup complete." if [ $? -eq 0 ] ; then echo "done." break fi done -echo -n "Retrieving admin user's API key from $local_shed_url..." +echo -n "Retrieving admin user's API key from $LOCAL_SHED_URL..." -curl_response=`curl -s --user $admin_user_email:$admin_user_password $local_shed_url/api/authenticate/baseauth/` +curl_response=$(curl -s --user "$admin_user_email":"$admin_user_password" "$LOCAL_SHED_URL"/api/authenticate/baseauth/) # Gets an empty response only on first attempt for some reason? sleep 1 -curl_response=`curl -s --user $admin_user_email:$admin_user_password $local_shed_url/api/authenticate/baseauth/` -api_key=`echo $curl_response | grep api_key | awk -F\" '{print $4}'` +curl_response=$(curl -s --user "$admin_user_email":"$admin_user_password" "$LOCAL_SHED_URL"/api/authenticate/baseauth/) +api_key=$(echo "$curl_response" | grep api_key | awk -F\" '{print $4}') if [[ -z $api_key && ${api_key+x} ]] ; then stop_err "Error getting API key for user $admin_user_email. Response: $curl_response" @@ -94,16 +100,16 @@ echo " done." if [ $? -eq 0 ] ; then echo -n "Creating users... " - python scripts/tool_shed/api/create_users.py -a $api_key -f $tool_shed -t $local_shed_url >> $log_file + python scripts/tool_shed/api/create_users.py -a "$api_key" -f "$tool_shed" -t "$LOCAL_SHED_URL" >> "$log_file" echo "done." echo -n "Creating categories... " - python scripts/tool_shed/api/create_categories.py -a $api_key -f $tool_shed -t $local_shed_url >> $log_file + python scripts/tool_shed/api/create_categories.py -a "$api_key" -f "$tool_shed" -t "$LOCAL_SHED_URL" >> "$log_file" echo "done." else stop_err "Error getting API key from local tool shed." fi echo "Bootstrap complete, shutting down temporary tool shed process. A log has been saved to tool_shed_bootstrap.log" -python ./scripts/paster.py serve ${TOOL_SHED_CONFIG_FILE} --pid-file=tool_shed_bootstrap.pid --log-file=tool_shed_bootstrap.log --stop-daemon +kill "$(cat $TOOL_SHED_PID)" exit 0 diff --git a/scripts/tool_shed/bootstrap_tool_shed/bootstrap_util.py b/scripts/tool_shed/bootstrap_tool_shed/bootstrap_util.py index 8c5b382fd27..085989a0297 100755 --- a/scripts/tool_shed/bootstrap_tool_shed/bootstrap_util.py +++ b/scripts/tool_shed/bootstrap_tool_shed/bootstrap_util.py @@ -5,6 +5,7 @@ import os import sys from configparser import ConfigParser +from sqlalchemy import text from sqlalchemy.exc import ( OperationalError, ProgrammingError, @@ -14,20 +15,11 @@ sys.path.insert(1, os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, import tool_shed.webapp.model.mapping as tool_shed_model from tool_shed.util import xml_util +from tool_shed.webapp.config import ToolShedAppConfiguration -def check_db(config_parser): - dburi = None - - if config_parser.has_option("app:main", "database_connection"): - dburi = config_parser.get("app:main", "database_connection") - elif config_parser.has_option("app:main", "database_file"): - db_file = config_parser.get("app:main", "database_file") - dburi = "sqlite:///%s?isolation_level=IMMEDIATE" % db_file - else: - sys.exit( - "The database configuration setting is missing from the tool_shed.ini file. Add this setting before attempting to bootstrap." - ) +def check_db(config: ToolShedAppConfiguration): + dburi = config.database_connection sa_session = None @@ -37,9 +29,7 @@ def check_db(config_parser): database_exists_message += "Create a new database that has not been migrated before attempting to bootstrap." try: - model = tool_shed_model.init( - config_parser.get("app:main", "file_path"), dburi, engine_options={}, create_tables=False - ) + model = tool_shed_model.init(config.file_path, dburi, engine_options={}, create_tables=False) sa_session = model.context.current sys.exit(database_exists_message) except ProgrammingError: @@ -49,7 +39,7 @@ def check_db(config_parser): try: if sa_session is not None: - result = sa_session.execute("SELECT version FROM migrate_version").first() + result = sa_session.execute(text("SELECT version FROM migrate_version")).first() if result[0] >= 2: sys.exit(database_exists_message) else: @@ -57,9 +47,9 @@ def check_db(config_parser): except ProgrammingError: pass - if config_parser.has_option("app:main", "hgweb_config_dir"): + if config.hgweb_config_dir: hgweb_config_parser = ConfigParser() - hgweb_dir = config_parser.get("app:main", "hgweb_config_dir") + hgweb_dir = config.hgweb_config_dir hgweb_config_file = os.path.join(hgweb_dir, "hgweb.config") if not os.path.exists(hgweb_config_file): sys.exit(0) @@ -72,8 +62,6 @@ def check_db(config_parser): sys.exit(message) else: sys.exit(0) - else: - sys.exit(0) sys.exit(0) @@ -87,7 +75,6 @@ def admin_user_info(): email = None password = None if tree is None: - print("The XML file ", user_info_config, " seems to be invalid, using defaults.") email = "admin@test.org" password = "testuser" username = "admin" @@ -103,40 +90,22 @@ def admin_user_info(): return (username, email, password) -def get_local_tool_shed_url(config_parser): - port = "9009" - if config_parser.has_section("server:main"): - if config_parser.has_option("server:main", "port"): - port = config_parser.get("server:main", "port") - host = "127.0.0.1" - print(f"http://{host}:{port}") - return 0 - - def main(args): - config_parser = ConfigParser() - - if os.path.exists(args.config): - config_parser.read(args.config) - else: - return 1 + config = ToolShedAppConfiguration(config_file=args.config) if args.method == "check_db": - return check_db(config_parser) + return check_db(config) elif args.method == "admin_user_info": (username, email, password) = admin_user_info() print(f"{username}__SEP__{email}__SEP__{password}") return 0 - elif args.method == "get_url": - return get_local_tool_shed_url(config_parser) else: return 1 -parser = optparse.OptionParser() -parser.add_option("-c", "--config_file", dest="config", action="store", default="config/tool_shed.yml.sample") -parser.add_option("-e", "--execute", dest="method", action="store", default="check_db") -(args, options) = parser.parse_args() - if __name__ == "__main__": + parser = optparse.OptionParser() + parser.add_option("-c", "--config_file", dest="config", action="store", default="config/tool_shed.yml.sample") + parser.add_option("-e", "--execute", dest="method", action="store", default="check_db") + (args, options) = parser.parse_args() sys.exit(main(args)) diff --git a/scripts/tool_shed/bootstrap_tool_shed/create_user_with_api_key.py b/scripts/tool_shed/bootstrap_tool_shed/create_user_with_api_key.py index 6a6850d43bc..80f05643d73 100644 --- a/scripts/tool_shed/bootstrap_tool_shed/create_user_with_api_key.py +++ b/scripts/tool_shed/bootstrap_tool_shed/create_user_with_api_key.py @@ -4,7 +4,6 @@ import logging import optparse import os import sys -from configparser import ConfigParser sys.path.insert(1, os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, os.pardir, "lib")) sys.path.insert(1, os.path.join(os.path.dirname(__file__))) @@ -39,9 +38,6 @@ class BootstrapApplication: self.config.file_path, self.config.database_connection, engine_options={}, create_tables=False ) self.security = IdEncodingHelper(id_secret=self.config.id_secret) - self.hgweb_config_manager = self.model.hgweb_config_manager - self.hgweb_config_manager.hgweb_config_dir = self.config.hgweb_config_dir - print("Using hgweb.config file: ", self.hgweb_config_manager.hgweb_config) @property def sa_session(self): @@ -90,16 +86,9 @@ def validate(email, password, username): if __name__ == "__main__": parser = optparse.OptionParser(description="Create a user with API key.") - parser.add_option("-c", dest="config", action="store", help=".ini file to retrieve toolshed configuration from") + parser.add_option("-c", dest="config", action="store", help="File to retrieve toolshed configuration from") (args, options) = parser.parse_args() - ini_file = args.config - config_parser = ConfigParser({"here": os.getcwd()}) - print("Reading ini file: ", ini_file) - config_parser.read(ini_file) - config_dict = {} - for key, value in config_parser.items("app:main"): - config_dict[key] = value - config = tool_shed_config.Configuration(**config_dict) + config = tool_shed_config.Configuration(config_file=args.config) app = BootstrapApplication(config) user = create_user(app) if user is not None: diff --git a/setup.cfg b/setup.cfg index e1ec6c21393..d7558e1de10 100644 --- a/setup.cfg +++ b/setup.cfg @@ -18,6 +18,8 @@ ignore_missing_imports = True check_untyped_defs = True exclude = lib/galaxy/tools/bundled|test/functional pretty = True +no_implicit_reexport = True +no_implicit_optional = True [mypy-galaxy.util.oset] # lots of tricky code in here... @@ -145,6 +147,7 @@ check_untyped_defs = False check_untyped_defs = False [mypy-galaxy.util] check_untyped_defs = False +no_implicit_reexport = False [mypy-galaxy.tools.bundled.maf.vcf_to_maf_customtrack] check_untyped_defs = False [mypy-tool_shed.webapp.security] @@ -219,10 +222,8 @@ disallow_untyped_defs = True disallow_incomplete_defs = True check_untyped_defs = True disallow_untyped_decorators = True -no_implicit_optional = True warn_unused_ignores = True warn_return_any = True -no_implicit_reexport = True strict_equality = True [mypy-galaxy.tools.bundled.filters.gff.gff_filter_by_feature_count] check_untyped_defs = False @@ -410,7 +411,6 @@ check_untyped_defs = True disallow_untyped_decorators = True no_implicit_optional = True warn_unused_ignores = True -no_implicit_reexport = True strict_equality = True [mypy-galaxy.tools.error_reports.plugins.base_git] check_untyped_defs = False @@ -646,6 +646,8 @@ check_untyped_defs = False check_untyped_defs = False [mypy-galaxy.webapps.base.webapp] check_untyped_defs = False +[mypy-starlette_context.*] +no_implicit_reexport = False [mypy-galaxy.tool_shed.galaxy_install.migrate.common] check_untyped_defs = False [mypy-tool_shed.webapp.buildapp] diff --git a/test/functional/tools/identifier_multiple_in_conditional.xml b/test/functional/tools/identifier_multiple_in_conditional.xml index c5fb19b6742..92dcfa267ba 100644 --- a/test/functional/tools/identifier_multiple_in_conditional.xml +++ b/test/functional/tools/identifier_multiple_in_conditional.xml @@ -18,6 +18,7 @@ + diff --git a/test/functional/tools/version_command_plain.xml b/test/functional/tools/version_command_plain.xml index b2ffc3e07ef..4d3b925624f 100644 --- a/test/functional/tools/version_command_plain.xml +++ b/test/functional/tools/version_command_plain.xml @@ -15,6 +15,9 @@ cp '$input' '$output' + + + diff --git a/test/integration/embedded_pulsar_docker_job_conf.yml b/test/integration/embedded_pulsar_docker_job_conf.yml index ca753780522..4c57e9f92a6 100644 --- a/test/integration/embedded_pulsar_docker_job_conf.yml +++ b/test/integration/embedded_pulsar_docker_job_conf.yml @@ -3,7 +3,7 @@ runners: load: galaxy.jobs.runners.local:LocalJobRunner pulsar_embed: load: galaxy.jobs.runners.pulsar:PulsarEmbeddedJobRunner - pulsar_app_config: # this doesn't start in uwsgi without disabling - filelock problem + pulsar_app_config: # this doesn't start without disabling - filelock problem tool_dependency_dir: none conda_auto_init: false conda_auto_install: false diff --git a/test/integration/test_celery_tasks.py b/test/integration/test_celery_tasks.py index ca8c7cc82e3..d7bfdb0784a 100644 --- a/test/integration/test_celery_tasks.py +++ b/test/integration/test_celery_tasks.py @@ -1,9 +1,7 @@ from celery import shared_task -from galaxy.celery.tasks import ( - galaxy_task, - purge_hda, -) +from galaxy.celery import galaxy_task +from galaxy.celery.tasks import purge_hda from galaxy.model import HistoryDatasetAssociation from galaxy.schema.schema import CreatePagePayload from galaxy_test.base.populators import ( diff --git a/test/integration/test_interactivetools_api.py b/test/integration/test_interactivetools_api.py index 06c63c1b224..ff239d2877d 100644 --- a/test/integration/test_interactivetools_api.py +++ b/test/integration/test_interactivetools_api.py @@ -29,7 +29,6 @@ EMBEDDED_PULSAR_JOB_CONFIG_FILE_DOCKER = os.path.join(SCRIPT_DIRECTORY, "embedde class BaseInteractiveToolsIntegrationTestCase(ContainerizedIntegrationTestCase): framework_tool_and_types = True container_type = "docker" - require_uwsgi = True enable_realtime_mapping = True def setUp(self): @@ -48,6 +47,7 @@ class BaseInteractiveToolsIntegrationTestCase(ContainerizedIntegrationTestCase): faked_host = rest.split("/", 1)[0] url = f"{scheme}://{host_and_port}" response = requests.get(url, timeout=1, headers={"Host": faked_host}) + response.raise_for_status() return response.text except Exception as e: print(e) @@ -143,7 +143,7 @@ class InteractiveToolsPulsarIntegrationTestCase(BaseInteractiveToolsIntegrationT @classmethod def handle_galaxy_config_kwds(cls, config): config["job_config_file"] = EMBEDDED_PULSAR_JOB_CONFIG_FILE_DOCKER - config["galaxy_infrastructure_url"] = "http://localhost:$UWSGI_PORT" + config["galaxy_infrastructure_url"] = "http://localhost:$GALAXY_WEB_PORT" disable_dependency_resolution(config) @@ -190,8 +190,6 @@ class KubeInteractiveToolsRemoteProxyIntegrationTestCase( $ GALAXY_TEST_K8S_EXTERNAL_PROXY_HOST="localhost:9002" GALAXY_TEST_K8S_EXTERNAL_PROXY_MAP="$HOME/gxitk8proxy.sqlite" pytest -s test/integration/test_interactivetools_api.py::KubeInteractiveToolsRemoteProxyIntegrationTestCase """ - require_uwsgi = True - @classmethod def setUpClass(cls): # realpath for docker deployed in a VM on Mac, also done in driver_util. diff --git a/test/integration/test_mules_as_job_handlers.py b/test/integration/test_mules_as_job_handlers.py deleted file mode 100644 index d639b15588e..00000000000 --- a/test/integration/test_mules_as_job_handlers.py +++ /dev/null @@ -1,67 +0,0 @@ -"""Integration tests for job handlers as mules.""" - -import os - -from galaxy_test.base.populators import DatasetPopulator -from galaxy_test.driver import integration_util - -SCRIPT_DIRECTORY = os.path.abspath(os.path.dirname(__file__)) - - -class _BaseMulesIntegrationTestCase(integration_util.IntegrationTestCase): - """Start uwsgi with mules and run a tool test.""" - - framework_tool_and_types = True - require_uwsgi = True - expected_server_name = "main.web" - - def test_runs_on_mule(self): - tool_id = "config_vars" - expect_server_name = self.expected_server_name - dataset_populator = DatasetPopulator(self.galaxy_interactor) - history_id = dataset_populator.new_history() - payload = dataset_populator.run_tool( - tool_id=tool_id, - inputs={"var": "server_name"}, - history_id=history_id, - ) - dataset_id = payload["outputs"][0]["id"] - dataset_populator.wait_for_dataset(history_id, dataset_id, assert_ok=True) - output = dataset_populator.get_history_dataset_content(history_id, dataset_id=dataset_id).strip() - assert output.startswith( - expect_server_name - ), "Job handler's server name '{output}' does not start with expected string '{expected}'".format( - output=output, - expected=expect_server_name, - ) - - -class SingleMuleAsJobHandlersIntegrationTestCase(_BaseMulesIntegrationTestCase): - - expected_server_name = "main.job-handlers" - - @classmethod - def handle_uwsgi_cli_command(cls, command): - command.extend( - [ - "--py-call-osafterfork", - "--mule=lib/galaxy/main.py", - "--farm=job-handlers:1", - ] - ) - - -class MultipleMulesAsJobHandlersIntegrationTestCase(_BaseMulesIntegrationTestCase): - - expected_server_name = "main.job-handlers" - - @classmethod - def handle_uwsgi_cli_command(cls, command): - command.extend( - [ - "--py-call-osafterfork", - "--mule=lib/galaxy/main.py", - "--mule=lib/galaxy/main.py", - "--farm=job-handlers:1,2", - ] - ) diff --git a/test/integration/test_scripts.py b/test/integration/test_scripts.py index 4837e2ec0ac..7eb67daec30 100644 --- a/test/integration/test_scripts.py +++ b/test/integration/test_scripts.py @@ -151,8 +151,8 @@ class ScriptsIntegrationTestCase(integration_util.IntegrationTestCase): assert output.strip() == "1" def test_database_scripts(self): - self._scripts_check_argparse_help("create_db.py") - self._scripts_check_argparse_help("manage_db.py") + self._scripts_check_argparse_help("create_toolshed_db.py") + self._scripts_check_argparse_help("migrate_toolshed_db.py") # TODO: test creating a smaller database - e.g. tool install database based on fresh # config file. diff --git a/test/integration/test_web_framework_config.py b/test/integration/test_web_framework_config.py index b9d00b1f441..2b8c4ef3fc1 100644 --- a/test/integration/test_web_framework_config.py +++ b/test/integration/test_web_framework_config.py @@ -12,8 +12,6 @@ class BaseWebFrameworkTestCase(integration_util.IntegrationTestCase): class CorsDefaultIntegrationTestCase(BaseWebFrameworkTestCase): - use_uvicorn = True - def test_options(self): headers = { "Access-Control-Request-Method": "GET", @@ -35,8 +33,6 @@ class CorsDefaultIntegrationTestCase(BaseWebFrameworkTestCase): class AllowOriginIntegrationTestCase(BaseWebFrameworkTestCase): - use_uvicorn = True - @classmethod def handle_galaxy_config_kwds(cls, config): config["allowed_origin_hostnames"] = "192.168.0.101,/.*.galaxyproject.org/" @@ -73,11 +69,3 @@ class AllowOriginIntegrationTestCase(BaseWebFrameworkTestCase): } options_response = self._options(headers) assert options_response.status_code == 400 - - -class AllowOriginPasteIntegrationTestCase(AllowOriginIntegrationTestCase): - use_uvicorn = False - - -class CorsDefaultPasteIntegrationTestCase(CorsDefaultIntegrationTestCase): - use_uvicorn = False diff --git a/test/integration/test_workflow_invocation.py b/test/integration/test_workflow_invocation.py new file mode 100644 index 00000000000..f8d877207e6 --- /dev/null +++ b/test/integration/test_workflow_invocation.py @@ -0,0 +1,55 @@ +"""Integration tests for workflow syncing.""" + +from galaxy_test.base.populators import ( + DatasetPopulator, + WorkflowPopulator, +) +from galaxy_test.base.uses_shed import UsesShed +from galaxy_test.driver import integration_util + + +class WorkflowInvocationTestCase(integration_util.IntegrationTestCase, UsesShed): + + framework_tool_and_types = True + require_admin_user = False + + def setUp(self): + super().setUp() + self.dataset_populator = DatasetPopulator(self.galaxy_interactor) + self.workflow_populator = WorkflowPopulator(self.galaxy_interactor) + + def test_run_workflow_with_missing_tool(self): + self.install_repository("iuc", "compose_text_param", "feb3acba1e0a") # 0.1.0 + with self.dataset_populator.test_history() as history_id: + workflow_id = self.workflow_populator.upload_yaml_workflow( + """ +class: GalaxyWorkflow +steps: + nonexistent: + tool_id: nonexistent_tool + tool_version: "0.1" + label: nonexistent + compose_text_param: + tool_id: compose_text_param + tool_version: "0.0.1" + label: compose_text_param +""" + ) + # should fail and return both tool ids since version 0.0.1 of compose_text_param does not exist + invocation_response = self.workflow_populator.invoke_workflow( + workflow_id, history_id=history_id, assert_ok=False, request={"require_exact_tool_versions": True} + ) + self._assert_status_code_is(invocation_response, 400) + self.assertEqual( + invocation_response.json().get("err_msg"), + "Workflow was not invoked; the following required tools are not installed: nonexistent_tool (version 0.1), compose_text_param (version 0.0.1)", + ) + # should fail but return only the tool_id of non_existent tool as another version of compose_text_param is installed + invocation_response = self.workflow_populator.invoke_workflow( + workflow_id, history_id=history_id, assert_ok=False, request={"require_exact_tool_versions": False} + ) + self._assert_status_code_is(invocation_response, 400) + self.assertEqual( + invocation_response.json().get("err_msg"), + "Workflow was not invoked; the following required tools are not installed: nonexistent_tool", + ) diff --git a/test/integration_selenium/test_pages_pdf_export.py b/test/integration_selenium/test_pages_pdf_export.py new file mode 100644 index 00000000000..25ebc874630 --- /dev/null +++ b/test/integration_selenium/test_pages_pdf_export.py @@ -0,0 +1,33 @@ +from .framework import ( + selenium_test, + SeleniumIntegrationTestCase, +) + + +class PagesPdfExportSeleniumIntegrationTestCase(SeleniumIntegrationTestCase): + ensure_registered = True + + @classmethod + def handle_galaxy_config_kwds(cls, config): + config["enable_beta_markdown_export"] = True + + @selenium_test + def test_page_pdf_export(self): + self.navigate_to_pages() + self.screenshot("pages_grid") + name = self.create_page( + content_format="Markdown", + ) + self.click_grid_popup_option(name, "Edit content") + self.components.pages.editor.markdown_editor.wait_for_and_send_keys("moo\n\n\ncow\n\n") + self.screenshot("pages_markdown_editor") + self.sleep_for(self.wait_types.UX_RENDER) + self.screenshot("pages_markdown_editor_content") + self.components.pages.editor.save.wait_for_and_click() + self.screenshot("pages_markdown_editor_saved") + self.sleep_for(self.wait_types.UX_RENDER) + self.navigate_to_pages() + self.click_grid_popup_option(name, "View") + self.screenshot("pages_view_simple") + self.components.pages.export.wait_for_and_click() + self.sleep_for(self.wait_types.UX_RENDER) diff --git a/test/integration_selenium/test_user_library_permissions.py b/test/integration_selenium/test_user_library_permissions.py index 7ed9d0dea23..1038d280473 100644 --- a/test/integration_selenium/test_user_library_permissions.py +++ b/test/integration_selenium/test_user_library_permissions.py @@ -1,5 +1,6 @@ import os +from galaxy_test.selenium.framework import retry_assertion_during_transitions from .framework import ( selenium_test, SeleniumIntegrationTestCase, @@ -90,7 +91,8 @@ class TestUserLibraryImport(SeleniumIntegrationTestCase): # search for created user and add him to permission field self.components.libraries.add_items_permission.wait_for_and_click() self.components.libraries.add_items_permission_input_field.wait_for_and_send_keys(email) - self.components.libraries.add_items_permission_option.wait_for_and_click() + + self.select_add_items_permission_option(email) # assert that the right email has been saved allowed_user_email = self.components.libraries.add_items_permission_field_text.wait_for_text() @@ -102,3 +104,9 @@ class TestUserLibraryImport(SeleniumIntegrationTestCase): self.logout() # login back to the 'regular' user account self.submit_login(email=email) + + @retry_assertion_during_transitions + def select_add_items_permission_option(self, option_text): + el = self.components.libraries.add_items_permission_option.wait_for_visible() + assert option_text == el.text + el.click() diff --git a/test/integration_selenium/test_workflow_repository_tool_update.py b/test/integration_selenium/test_workflow_repository_tool_update.py index a0233215318..5ca5339db03 100644 --- a/test/integration_selenium/test_workflow_repository_tool_update.py +++ b/test/integration_selenium/test_workflow_repository_tool_update.py @@ -43,3 +43,33 @@ steps: workflow["steps"]["0"]["tool_id"] == "toolshed.g2.bx.psu.edu/repos/iuc/compose_text_param/compose_text_param/0.1.1" ) + + @selenium_test + def test_tool_shed_unmatched_version_upgrade(self): + self.install_repository("iuc", "compose_text_param", "e188c9826e0f") # 0.1.1 + self.login() + workflow_id = self.workflow_populator.upload_yaml_workflow( + """class: GalaxyWorkflow +inputs: [] +steps: + - tool_id: toolshed.g2.bx.psu.edu/repos/iuc/compose_text_param/compose_text_param/0.0.0 + tool_version: 0.0.0 + label: compose_text_param + """ + ) + self.workflow_index_open() + self.workflow_index_click_option("Edit") + self.assert_modal_has_text("Using version '0.1.1' instead of version '0.0.0'") + self.screenshot("workflow_editor_tool_repository_upgrade") + self.components.workflow_editor.modal_button_continue.wait_for_and_click() + self.assert_workflow_has_changes_and_save() + workflow = self.workflow_populator.download_workflow(workflow_id) + assert ( + workflow["steps"]["0"]["content_id"] + == "toolshed.g2.bx.psu.edu/repos/iuc/compose_text_param/compose_text_param/0.1.1" + ) + assert ( + workflow["steps"]["0"]["tool_id"] + == "toolshed.g2.bx.psu.edu/repos/iuc/compose_text_param/compose_text_param/0.1.1" + ) + assert workflow["steps"]["0"]["tool_version"] == "0.1.1" diff --git a/test/unit/app/authnz/test_custos_authnz.py b/test/unit/app/authnz/test_custos_authnz.py index f0f35ab43b0..d8cba4ae9e6 100644 --- a/test/unit/app/authnz/test_custos_authnz.py +++ b/test/unit/app/authnz/test_custos_authnz.py @@ -344,7 +344,7 @@ class CustosAuthnzTestCase(unittest.TestCase): self.test_nonce_hash = self.test_nonce_hash + "Z" # self.custos_authnz._fetch_token = fetch_token - with self.assertRaisesRegex(Exception, "^Nonce mismatch!$"): + with self.assertRaisesRegex(Exception, "^Nonce mismatch"): self.custos_authnz.callback( state_token="xxx", authz_code=self.test_code, diff --git a/test/unit/app/jobs/job_conf.sample_advanced.yml b/test/unit/app/jobs/job_conf.sample_advanced.yml index 89ae458a310..7bf50ff21be 100644 --- a/test/unit/app/jobs/job_conf.sample_advanced.yml +++ b/test/unit/app/jobs/job_conf.sample_advanced.yml @@ -351,7 +351,6 @@ handling: # - `mem-self` - In-memory Self Assignment # - `db-self`- Database Self Assignment # - `db-preassign` - Database Preassignment - # - `uwsgi-mule-message` - uWSGI Mule Messaging # - `db-transaction-isolation` - Database Transaction Isolation # - `db-skip-locked` - Database SKIP LOCKED # diff --git a/test/unit/app/jobs/test_job_configuration.py b/test/unit/app/jobs/test_job_configuration.py index 3235b201cc9..2693e349f86 100644 --- a/test/unit/app/jobs/test_job_configuration.py +++ b/test/unit/app/jobs/test_job_configuration.py @@ -14,10 +14,7 @@ from galaxy.util import ( galaxy_directory, galaxy_samples_directory, ) -from galaxy.web_stack import ( - ApplicationStack, - UWSGIApplicationStack, -) +from galaxy.web_stack import ApplicationStack from galaxy.web_stack.handlers import HANDLER_ASSIGNMENT_METHODS # File would be slightly more readable if contents were embedded directly, but @@ -34,11 +31,6 @@ class TestApplicationStack(ApplicationStack): return HANDLER_ASSIGNMENT_METHODS.DB_SKIP_LOCKED -class TestUWSGIApplicationStack(UWSGIApplicationStack): - def get_preferred_handler_assignment_method(self): - return HANDLER_ASSIGNMENT_METHODS.DB_SKIP_LOCKED - - class BaseJobConfXmlParserTestCase(unittest.TestCase): extension = "xml" @@ -58,7 +50,6 @@ class BaseJobConfXmlParserTestCase(unittest.TestCase): self._application_stack = None self._job_configuration = None self._job_configuration_base_pools = None - self._uwsgi_opt = None def tearDown(self): shutil.rmtree(self.temp_directory) @@ -82,19 +73,9 @@ class BaseJobConfXmlParserTestCase(unittest.TestCase): @property def job_config(self): if not self._job_configuration: - base_handler_pools = self._job_configuration_base_pools or JobConfiguration.DEFAULT_BASE_HANDLER_POOLS - mock_uwsgi = mock.Mock() - mock_uwsgi.mule_id = lambda: 1 - with mock.patch("galaxy.web_stack.uwsgi", mock_uwsgi), mock.patch( - "galaxy.web_stack.uwsgi.opt", self._uwsgi_opt - ), mock.patch("galaxy.jobs.JobConfiguration.DEFAULT_BASE_HANDLER_POOLS", base_handler_pools): - self._job_configuration = JobConfiguration(self.app) + self._job_configuration = JobConfiguration(self.app) return self._job_configuration - def _with_uwsgi_application_stack(self, **uwsgi_opt): - self._uwsgi_opt = uwsgi_opt - self._application_stack = TestUWSGIApplicationStack() - def _with_handlers_config(self, assign_with=None, default=None, handlers=None, base_pools=None): handlers = handlers or [] template = { @@ -185,22 +166,6 @@ class SimpleJobConfXmlParserTestCase(BaseJobConfXmlParserTestCase): assert self.job_config.default_handler_id is None assert sorted(self.job_config.handlers["_default_"]) == ["handler0", "handler1"] - def test_implict_uwsgi_mule_message_handler_assign(self): - self._with_uwsgi_application_stack(mule="lib/galaxy/main.py", farm="job-handlers:1") - assert self.job_config.handler_assignment_methods == ["uwsgi-mule-message", "db-skip-locked"] - assert self.job_config.default_handler_id is None - assert self.job_config.handlers["_default_"] == ["main.job-handlers.1"] - - def test_implict_uwsgi_mule_message_handler_assign_with_explicit_handlers(self): - self._with_handlers_config(handlers=[{"id": "handler0"}, {"id": "handler1"}]) - self._with_uwsgi_application_stack(mule="lib/galaxy/main.py", farm="job-handlers:1") - assert self.job_config.handler_assignment_methods == [ - "uwsgi-mule-message", - "db-skip-locked", - ], self.job_config.handler_assignment_methods - assert self.job_config.default_handler_id is None - assert sorted(self.job_config.handlers["_default_"]) == ["handler0", "handler1", "main.job-handlers.1"] - def test_explicit_mem_self_handler_assign(self): self._with_handlers_config(assign_with="mem-self") assert self.job_config.handler_assignment_methods == ["mem-self"] @@ -214,56 +179,18 @@ class SimpleJobConfXmlParserTestCase(BaseJobConfXmlParserTestCase): assert self.job_config.default_handler_id is None assert self.job_config.handlers["_default_"] == ["handler0"] - def test_explicit_db_preassign_handler_assign_with_uwsgi(self): - self._with_handlers_config(assign_with="db-preassign", handlers=[{"id": "handler0"}]) - self._with_uwsgi_application_stack(mule="lib/galaxy/main.py", farm="job-handlers:1") - assert self.job_config.handler_assignment_methods == ["db-preassign"] - assert self.job_config.default_handler_id is None - assert sorted(self.job_config.handlers["_default_"]) == ["handler0", "main.job-handlers.1"] - def test_explicit_db_transaction_isolation_handler_assign(self): self._with_handlers_config(assign_with="db-transaction-isolation") assert self.job_config.handler_assignment_methods == ["db-transaction-isolation"] assert self.job_config.default_handler_id is None assert self.job_config.handlers == {} - def test_explicit_db_transaction_isolation_handler_assign_with_uwsgi(self): - self._with_handlers_config(assign_with="db-transaction-isolation", handlers=[{"id": "handler0"}]) - self._with_uwsgi_application_stack(mule="lib/galaxy/main.py", farm="job-handlers:1") - assert self.job_config.handler_assignment_methods == ["db-transaction-isolation"] - assert self.job_config.default_handler_id is None - assert sorted(self.job_config.handlers["_default_"]) == ["handler0", "main.job-handlers.1"] - def test_explicit_db_skip_locked_handler_assign(self): self._with_handlers_config(assign_with="db-skip-locked") assert self.job_config.handler_assignment_methods == ["db-skip-locked"] assert self.job_config.default_handler_id is None assert self.job_config.handlers == {} - def test_explicit_db_skip_locked_handler_assign_with_uwsgi(self): - self._with_handlers_config(assign_with="db-skip-locked", handlers=[{"id": "handler0"}]) - self._with_uwsgi_application_stack(mule="lib/galaxy/main.py", farm="job-handlers:1") - assert self.job_config.handler_assignment_methods == ["db-skip-locked"] - assert self.job_config.default_handler_id is None - assert sorted(self.job_config.handlers["_default_"]) == ["handler0", "main.job-handlers.1"] - - def test_uwsgi_farms_as_handler_tags(self): - self._with_uwsgi_application_stack( - mule=["lib/galaxy/main.py"] * 2, farm=["job-handlers:1", "job-handlers.foo:2"] - ) - assert self.job_config.default_handler_id is None - assert self.job_config.handlers["_default_"] == ["main.job-handlers.1"] - assert self.job_config.handlers["foo"] == ["main.job-handlers.foo.1"] - - def test_uwsgi_overlapping_pools(self): - self._with_handlers_config(base_pools=("workflow-schedulers", "job-handlers")) - self._with_uwsgi_application_stack( - mule=["lib/galaxy/main.py"] * 3, farm=["job-handlers:1", "workflow-schedulers:2", "job-handlers.foo:3"] - ) - assert self.job_config.default_handler_id is None - assert self.job_config.handlers["_default_"] == ["main.workflow-schedulers.1"] - assert self.job_config.handlers["foo"] == ["main.job-handlers.foo.1"] - def test_load_simple_destination(self): local_dest = self.job_config.destinations["local"][0] assert local_dest.id == "local" diff --git a/test/unit/app/jobs/test_job_wrapper.py b/test/unit/app/jobs/test_job_wrapper.py index a03bb35b3d2..0ab3b6374a5 100644 --- a/test/unit/app/jobs/test_job_wrapper.py +++ b/test/unit/app/jobs/test_job_wrapper.py @@ -104,7 +104,7 @@ class MockEvaluator: pass def build(self): - return TEST_COMMAND, [], [] + return TEST_COMMAND, "", [], [] class MockJobQueue: diff --git a/test/unit/app/managers/test_DatasetManager.py b/test/unit/app/managers/test_DatasetManager.py index 4ff56d8a1a0..38d4abd59b1 100644 --- a/test/unit/app/managers/test_DatasetManager.py +++ b/test/unit/app/managers/test_DatasetManager.py @@ -1,6 +1,7 @@ """ """ import unittest +from unittest import mock import sqlalchemy @@ -208,9 +209,7 @@ def testable_url_for(*a, **k): return f"(fake url): {a}, {k}" -DatasetSerializer.url_for = staticmethod(testable_url_for) - - +@mock.patch("galaxy.managers.datasets.DatasetSerializer.url_for", testable_url_for) class DatasetSerializerTestCase(BaseTestCase): def set_up_managers(self): super().set_up_managers() diff --git a/test/unit/app/managers/test_HDAManager.py b/test/unit/app/managers/test_HDAManager.py index b3377c7f026..370054de460 100644 --- a/test/unit/app/managers/test_HDAManager.py +++ b/test/unit/app/managers/test_HDAManager.py @@ -1,4 +1,5 @@ import unittest +from unittest import mock import sqlalchemy @@ -362,9 +363,7 @@ def testable_url_for(*a, **k): return f"(fake url): {a}, {k}" -hdas.HDASerializer.url_for = staticmethod(testable_url_for) - - +@mock.patch("galaxy.managers.hdas.HDASerializer.url_for", testable_url_for) class HDASerializerTestCase(HDATestCase): def set_up_managers(self): super().set_up_managers() @@ -417,8 +416,8 @@ class HDASerializerTestCase(HDATestCase): self.assertKeys(serialized, self.hda_serializer.views["summary"] + ["uuid"]) self.log("should be able to use keys on their own") - serialized = self.hda_serializer.serialize_to_view(hda, keys=["file_path", "visualizations"]) - self.assertKeys(serialized, ["file_path", "visualizations"]) + serialized = self.hda_serializer.serialize_to_view(hda, keys=["file_path"]) + self.assertKeys(serialized, ["file_path"]) def test_serializers(self): hda = self._create_vanilla_hda() @@ -459,7 +458,6 @@ class HDASerializerTestCase(HDATestCase): self.assertIsInstance(serialized["resubmitted"], bool) self.assertIsInstance(serialized["display_apps"], list) self.assertIsInstance(serialized["display_types"], list) - self.assertIsInstance(serialized["visualizations"], list) # remapped self.assertNullableBasestring(serialized["misc_info"]) @@ -523,9 +521,7 @@ class HDASerializerTestCase(HDATestCase): self.log("file_name should be included if app configured to do so") self.dataset_manager.permissions.set_private_to_one_user(dataset1, owner) # request random crap - serialized = self.hda_serializer.serialize_to_view( - item1, view="detailed", keys=["file_path", "visualizations"], user=non_owner - ) + serialized = self.hda_serializer.serialize_to_view(item1, view="detailed", keys=["file_path"], user=non_owner) self.assertEqual(sorted(keys_in_inaccessible_view), sorted(serialized.keys())) # TODO: test extra_files_path as well diff --git a/test/unit/app/managers/test_HDCAManager.py b/test/unit/app/managers/test_HDCAManager.py index 6566e978e4c..ea9e46636cd 100644 --- a/test/unit/app/managers/test_HDCAManager.py +++ b/test/unit/app/managers/test_HDCAManager.py @@ -1,4 +1,5 @@ import unittest +from unittest import mock from galaxy.managers import ( collections, @@ -57,9 +58,7 @@ def testable_url_for(*a, **k): return f"(fake url): {a}, {k}" -hdcas.HDCASerializer.url_for = staticmethod(testable_url_for) - - +@mock.patch("galaxy.managers.hdcas.HDCASerializer.url_for", testable_url_for) class HDCASerializerTestCase(HDCATestCase): def set_up_managers(self): super().set_up_managers() diff --git a/test/unit/app/managers/test_HistoryManager.py b/test/unit/app/managers/test_HistoryManager.py index 73fe8eb6226..22897ce87e4 100644 --- a/test/unit/app/managers/test_HistoryManager.py +++ b/test/unit/app/managers/test_HistoryManager.py @@ -395,10 +395,8 @@ def testable_url_for(*a, **k): return f"(fake url): {a}, {k}" -HistorySerializer.url_for = staticmethod(testable_url_for) -hdas.HDASerializer.url_for = staticmethod(testable_url_for) - - +@mock.patch("galaxy.managers.histories.HistorySerializer.url_for", testable_url_for) +@mock.patch("galaxy.managers.hdas.HDASerializer.url_for", testable_url_for) class HistorySerializerTestCase(BaseTestCase): def set_up_managers(self): super().set_up_managers() diff --git a/test/unit/app/test_remote_shell.py b/test/unit/app/test_remote_shell.py index bae05ddd421..ea49c1f1639 100644 --- a/test/unit/app/test_remote_shell.py +++ b/test/unit/app/test_remote_shell.py @@ -10,7 +10,7 @@ try: except ImportError: raise unittest.SkipTest("Skipping tests that require mockssh") -from galaxy.jobs.runners.cli import CliInterface +from galaxy.jobs.runners.util.cli import CliInterface from galaxy.security.ssh_util import ( generate_ssh_keys, SSHKeys, diff --git a/test/unit/app/tools/test_data_fetch.py b/test/unit/app/tools/test_data_fetch.py index 332b0bd7037..c77d3c5b55d 100644 --- a/test/unit/app/tools/test_data_fetch.py +++ b/test/unit/app/tools/test_data_fetch.py @@ -31,8 +31,8 @@ def test_simple_path_get(): ] } execute_context.execute_request(request) - galaxy_json = execute_context.galaxy_json - assert "__unnamed_outputs" in galaxy_json + output = _unnamed_output(execute_context) + assert output def test_simple_list_path_get(): @@ -53,9 +53,7 @@ def test_simple_list_path_get(): ] } execute_context.execute_request(request) - galaxy_json = execute_context.galaxy_json - assert "__unnamed_outputs" in galaxy_json - output = galaxy_json.get("__unnamed_outputs")[0] + output = _unnamed_output(execute_context) destination = output["destination"] assert "object_id" in destination assert destination["object_id"] == 76 @@ -85,9 +83,7 @@ def test_hdas_single_url_error(): ] } execute_context.execute_request(request) - galaxy_json = execute_context.galaxy_json - assert "__unnamed_outputs" in galaxy_json - output = galaxy_json.get("__unnamed_outputs")[0] + output = _unnamed_output(execute_context) assert "elements" in output elements = output["elements"] assert len(elements) == 2 @@ -123,9 +119,7 @@ def test_hdca_collection_element_failed(): ] } execute_context.execute_request(request) - galaxy_json = execute_context.galaxy_json - assert "__unnamed_outputs" in galaxy_json - output = galaxy_json.get("__unnamed_outputs")[0] + output = _unnamed_output(execute_context) assert "error_message" in output error = output["error_message"] assert ( @@ -158,9 +152,7 @@ def test_hdca_allow_failed_collections(): ], } execute_context.execute_request(request) - galaxy_json = execute_context.galaxy_json - assert "__unnamed_outputs" in galaxy_json - output = galaxy_json.get("__unnamed_outputs")[0] + output = _unnamed_output(execute_context) assert "error_message" not in output assert "elements" in output elements = output["elements"] @@ -193,9 +185,7 @@ def test_hdca_failed_expansion(): ] } execute_context.execute_request(request) - galaxy_json = execute_context.galaxy_json - assert "__unnamed_outputs" in galaxy_json - output = galaxy_json.get("__unnamed_outputs")[0] + output = _unnamed_output(execute_context) assert "elements" in output elements = output["elements"] assert len(elements) == 0 @@ -212,6 +202,16 @@ def _execute_context(): rmtree(job_directory) +def _unnamed_output(execute_context: "ExecuteContext"): + galaxy_json = execute_context.galaxy_json + assert "__unnamed_outputs" in galaxy_json + unnamed_outputs = galaxy_json.get("__unnamed_outputs") + assert isinstance(unnamed_outputs, list) + assert len(unnamed_outputs) > 0 + output = unnamed_outputs[0] + return output + + class ExecuteContext: def __init__(self, directory): self.job_directory = directory diff --git a/test/unit/app/tools/test_evaluation.py b/test/unit/app/tools/test_evaluation.py index d2937fa1db1..3e7db191c87 100644 --- a/test/unit/app/tools/test_evaluation.py +++ b/test/unit/app/tools/test_evaluation.py @@ -53,7 +53,7 @@ class ToolEvaluatorTestCase(TestCase, UsesApp): def test_simple_evaluation(self): self._setup_test_bwa_job() self._set_compute_environment() - command_line, extra_filenames, _ = self.evaluator.build() + command_line = self.evaluator.build()[0] self.assertEqual( command_line, "bwa --thresh=4 --in=/galaxy/files/dataset_1.dat --out=/galaxy/files/dataset_2.dat" ) @@ -68,19 +68,19 @@ class ToolEvaluatorTestCase(TestCase, UsesApp): ] self.tool._command_line = "prog1 #for $r_i in $r # $r_i.thresh#end for#" self._set_compute_environment() - command_line, extra_filenames, _ = self.evaluator.build() + command_line = self.evaluator.build()[0] self.assertEqual(command_line, "prog1 4 5") def test_eval_galaxy_url(self): self.tool._command_line = "prog1 $__galaxy_url__" self._set_compute_environment() - command_line, extra_filenames, _ = self.evaluator.build() + command_line = self.evaluator.build()[0] self.assertEqual(command_line, "prog1 %s" % TEST_GALAXY_URL) def test_eval_history_id(self): self.tool._command_line = "prog1 '$__history_id__'" self._set_compute_environment() - command_line, extra_filenames, _ = self.evaluator.build() + command_line = self.evaluator.build()[0] self.assertEqual(command_line, "prog1 '%s'" % self.app.security.encode_id(42)) def test_conditional_evaluation(self): @@ -100,7 +100,7 @@ class ToolEvaluatorTestCase(TestCase, UsesApp): ] self.tool._command_line = "prog1 --thresh=${c.thresh} --test_param=${c.always_true}" self._set_compute_environment() - command_line, extra_filenames, _ = self.evaluator.build() + command_line = self.evaluator.build()[0] self.assertEqual(command_line, "prog1 --thresh=4 --test_param=true") def test_evaluation_of_optional_datasets(self): @@ -112,7 +112,7 @@ class ToolEvaluatorTestCase(TestCase, UsesApp): self.tool.set_params({"input1": parameter}) self.tool._command_line = "prog1 --opt_input='${input1}'" self._set_compute_environment() - command_line, extra_filenames, _ = self.evaluator.build() + command_line = self.evaluator.build()[0] self.assertEqual(command_line, "prog1 --opt_input='None'") def test_evaluation_with_path_rewrites_wrapped(self): @@ -134,14 +134,14 @@ class ToolEvaluatorTestCase(TestCase, UsesApp): input_paths=[DatasetPath(1, "/galaxy/files/dataset_1.dat", false_path=job_path_1)], output_paths=[DatasetPath(2, "/galaxy/files/dataset_2.dat", false_path=job_path_2)], ) - command_line, extra_filenames, _ = self.evaluator.build() + command_line = self.evaluator.build()[0] self.assertEqual(command_line, f"bwa --thresh=4 --in={job_path_1} --out={job_path_2}") def test_configfiles_evaluation(self): self.tool.config_files.append(("conf1", None, "$thresh")) self.tool._command_line = "prog1 $conf1" self._set_compute_environment() - command_line, extra_filenames, _ = self.evaluator.build() + command_line, _, extra_filenames, _ = self.evaluator.build() self.assertEqual(len(extra_filenames), 1) config_filename = extra_filenames[0] config_basename = os.path.basename(config_filename) @@ -181,15 +181,15 @@ class ToolEvaluatorTestCase(TestCase, UsesApp): self.tool.set_params({"index_path": parameter}) self.tool._command_line = "prog1 $index_path.fields.path" self._set_compute_environment(unstructured_path_rewrites={"/old": "/new"}) - command_line, extra_filenames, _ = self.evaluator.build() + command_line = self.evaluator.build()[0] self.assertEqual(command_line, "prog1 /new/path/human") def test_version_command(self): self.tool.version_string_cmd = "echo v.1.1" self._setup_test_bwa_job() self._set_compute_environment() - command_line, extra_filenames, _ = self.evaluator.build() - assert self.tool.version_string_cmd in command_line + version_command_line = self.evaluator.build()[1] + assert self.tool.version_string_cmd in version_command_line def test_template_property_app(self): self._assert_template_property_is("$__app__.config.new_file_path", self.app.config.new_file_path) @@ -207,7 +207,7 @@ class ToolEvaluatorTestCase(TestCase, UsesApp): self.tool._command_line = "test.exe" self.tool.config_files.append(("conf1", None, """%s""" % expression)) self._set_compute_environment() - _, extra_filenames, _ = self.evaluator.build() + extra_filenames = self.evaluator.build()[2] config_filename = extra_filenames[0] self.assertEqual(open(config_filename).read(), value) diff --git a/test/unit/app/tools/test_execution.py b/test/unit/app/tools/test_execution.py index 0f22d7471ee..bc12e9c4d37 100644 --- a/test/unit/app/tools/test_execution.py +++ b/test/unit/app/tools/test_execution.py @@ -7,6 +7,7 @@ import webob.exc import galaxy.model from galaxy.app_unittest_utils import tools_support +from galaxy.model.orm.util import add_object_to_object_session from galaxy.util.bunch import Bunch BASE_REPEAT_TOOL_CONTENTS = """ @@ -126,6 +127,7 @@ class ToolExecutionTestCase(TestCase, tools_support.UsesTools): hda.dataset.state = "ok" self.trans.sa_session.add(hda) + add_object_to_object_session(self.history, hda) self.history.datasets.append(hda) self.trans.sa_session.flush() return hda diff --git a/test/unit/app/tools/test_history_imp_exp.py b/test/unit/app/tools/test_history_imp_exp.py index 6a087fc46e3..7e4fe15815d 100644 --- a/test/unit/app/tools/test_history_imp_exp.py +++ b/test/unit/app/tools/test_history_imp_exp.py @@ -8,6 +8,7 @@ from unittest.mock import Mock from galaxy import model from galaxy.app_unittest_utils.galaxy_mock import MockApp from galaxy.exceptions import MalformedContents +from galaxy.model.orm.util import add_object_to_object_session from galaxy.objectstore.unittest_utils import Config as TestConfig from galaxy.tools.imp_exp import ( JobExportHistoryArchiveWrapper, @@ -387,11 +388,13 @@ def test_export_collection_with_mapping_history(): ija1 = model.ImplicitCollectionJobsJobAssociation() ija1.order_index = 0 ija1.implicit_collection_jobs = implicit_collection_jobs + add_object_to_object_session(ija1, j1) ija1.job = j1 j2.add_output_dataset_collection("out_file1", hc2) # really? ija2 = model.ImplicitCollectionJobsJobAssociation() ija2.order_index = 1 + add_object_to_object_session(ija2, implicit_collection_jobs) ija2.implicit_collection_jobs = implicit_collection_jobs ija2.job = j2 diff --git a/test/unit/app/visualizations/plugins/test_VisualizationsRegistry.py b/test/unit/app/visualizations/plugins/test_VisualizationsRegistry.py index 1a3d0b61c38..816c61f3fb1 100644 --- a/test/unit/app/visualizations/plugins/test_VisualizationsRegistry.py +++ b/test/unit/app/visualizations/plugins/test_VisualizationsRegistry.py @@ -35,6 +35,13 @@ config1 = """\ dataset_id + + + png + svg + pdf + + """ @@ -111,6 +118,16 @@ class VisualizationsRegistry_TestCase(VisualizationsBase_TestCase): self.assertEqual(vis1.template_path, os.path.join(vis1.path, "templates")) self.assertEqual(vis1.template_lookup.__class__.__name__, "TemplateLookup") + vis1_as_dict = vis1.to_dict() + assert vis1_as_dict["specs"] + specs = vis1_as_dict["specs"] + assert "exports" in specs + exports = specs["exports"] + assert len(exports) == 3 + assert "png" in exports + assert "svg" in exports + assert "pdf" in exports + vis2 = plugin_mgr.plugins["vis2"] self.assertEqual(vis2.name, "vis2") self.assertEqual(vis2.path, os.path.join(expected_plugins_path, "vis2")) diff --git a/test/unit/config/config_manage/embedded/config/galaxy.yml b/test/unit/config/config_manage/embedded/config/galaxy.yml index 0a26e2f9f51..d1851578e97 100644 --- a/test/unit/config/config_manage/embedded/config/galaxy.yml +++ b/test/unit/config/config_manage/embedded/config/galaxy.yml @@ -1,23 +1,3 @@ -uwsgi: - http: 127.0.0.1:8080 - buffer-size: 16384 - processes: 1 - threads: 4 - offload-threads: 2 - static-map: /static=static - static-map: /favicon.ico=static/favicon.ico - master: false - virtualenv: .venv - pythonpath: lib - module: galaxy.webapps.galaxy.buildapp:uwsgi_app() - manage-script-name: false - thunder-lock: false - die-on-term: true - hook-master-start: unix_signal:2 gracefully_kill_them_all - hook-master-start: unix_signal:15 gracefully_kill_them_all - py-call-osafterfork: true - enable-threads: true - galaxy: master_api_key: changethis config_dir: 'false' diff --git a/test/unit/config/config_manage/simple/config/galaxy.yml b/test/unit/config/config_manage/simple/config/galaxy.yml index 0a3f7418e07..56e5f811b0c 100644 --- a/test/unit/config/config_manage/simple/config/galaxy.yml +++ b/test/unit/config/config_manage/simple/config/galaxy.yml @@ -1,23 +1,3 @@ -uwsgi: - http: 127.0.0.1:8080 - buffer-size: 16384 - processes: 1 - threads: 4 - offload-threads: 2 - static-map: /static=static - static-map: /favicon.ico=static/favicon.ico - master: false - virtualenv: .venv - pythonpath: lib - module: galaxy.webapps.galaxy.buildapp:uwsgi_app() - manage-script-name: false - thunder-lock: false - die-on-term: true - hook-master-start: unix_signal:2 gracefully_kill_them_all - hook-master-start: unix_signal:15 gracefully_kill_them_all - py-call-osafterfork: true - enable-threads: true - galaxy: master_api_key: changethis config_dir: 'false' diff --git a/test/unit/config/config_manage/test_config_manage.py b/test/unit/config/config_manage/test_config_manage.py index 4024e562a08..649611b672f 100644 --- a/test/unit/config/config_manage/test_config_manage.py +++ b/test/unit/config/config_manage/test_config_manage.py @@ -20,10 +20,7 @@ def test_reports_conversion_1607_sample(): assert "reports" in config reports_config = config["reports"] or {} assert "use_beaker_session" not in reports_config - - assert "uwsgi" in config - uwsgi_config = config["uwsgi"] - assert uwsgi_config["module"] == "galaxy.webapps.reports.buildapp:uwsgi_app()" + assert "uwsgi" not in config def test_reports_build_sample(): @@ -56,10 +53,7 @@ def test_shed_conversion_1607_prefix(): config_dir.assert_moved("config/tool_shed.ini", "config/tool_shed.ini.backup") with config_dir.open("config/tool_shed.yml") as f: config = yaml.safe_load(f) - assert "uwsgi" in config - uwsgi_config = config["uwsgi"] - assert "module" not in uwsgi_config - assert uwsgi_config["mount"].startswith("/shed=tool_shed.webapp") + assert "uwsgi" not in config def test_allow_library_path_paste_conversion(): @@ -75,11 +69,6 @@ def test_allow_library_path_paste_conversion(): assert galaxy_config["allow_path_paste"] is True -def test_build_uwsgi_yaml(): - with _config_directory("1607_root_samples") as config_dir: - config_dir.manage_cli(["build_uwsgi_yaml"]) - - def test_validate_simple_config(): with _config_directory("simple") as config_dir: config_dir.manage_cli(["validate", "galaxy"]) diff --git a/test/unit/config/test_config_dataset_storage.py b/test/unit/config/test_config_dataset_storage.py index 292c1665a74..1eb4ef1f6ae 100644 --- a/test/unit/config/test_config_dataset_storage.py +++ b/test/unit/config/test_config_dataset_storage.py @@ -1,56 +1,46 @@ import os.path -import pytest - from galaxy import config from galaxy.config import BaseAppConfiguration -@pytest.fixture -def mock_config_file(monkeypatch): - # Patch this; otherwise tempfile.tempdir will be set, which is a global variable that - # defines the value of the default `dir` argument to the functions in Python's - # tempfile module - which breaks multiple tests. - monkeypatch.setattr(config.GalaxyAppConfiguration, "_override_tempdir", lambda a, b: None) - - -def test_object_store_store_by_set(mock_config_file, monkeypatch): +def test_object_store_store_by_set(): # object_store_store_by set by admin - appconfig = config.GalaxyAppConfiguration(object_store_store_by="id") + appconfig = config.GalaxyAppConfiguration(object_store_store_by="id", override_tempdir=False) assert appconfig.object_store_store_by == "id" -def test_uuid_1(mock_config_file, monkeypatch): +def test_uuid_1(): # object_store_store_by not set # file_path set by admin to `objects` (no need for the dir to exist) - appconfig = config.GalaxyAppConfiguration(file_path="objects") + appconfig = config.GalaxyAppConfiguration(file_path="objects", override_tempdir=False) assert appconfig.object_store_store_by == "uuid" -def test_uuid_2(mock_config_file, monkeypatch): +def test_uuid_2(monkeypatch): # object_store_store_by not set # file_path not set, `files` dir doesn't exist monkeypatch.setattr(BaseAppConfiguration, "_path_exists", lambda self, path: False) - appconfig = config.GalaxyAppConfiguration() + appconfig = config.GalaxyAppConfiguration(override_tempdir=False) assert appconfig.object_store_store_by == "uuid" -def test_id_1(mock_config_file, monkeypatch): +def test_id_1(): # object_store_store_by not set # file_path set by admin to `not_objects` (no need for the dir to exist) - appconfig = config.GalaxyAppConfiguration(file_path="not_objects") + appconfig = config.GalaxyAppConfiguration(file_path="not_objects", override_tempdir=False) assert appconfig.object_store_store_by == "id" -def test_id_2(mock_config_file, monkeypatch): +def test_id_2(monkeypatch): # object_store_store_by not set # file_path not set, `files` dir exists monkeypatch.setattr( BaseAppConfiguration, "_path_exists", lambda self, path: True if os.path.basename(path) == "files" else False ) - appconfig = config.GalaxyAppConfiguration() + appconfig = config.GalaxyAppConfiguration(override_tempdir=False) assert appconfig.object_store_store_by == "id" diff --git a/test/unit/config/test_config_values.py b/test/unit/config/test_config_values.py index c5b7be1abed..473a99bdeba 100644 --- a/test/unit/config/test_config_values.py +++ b/test/unit/config/test_config_values.py @@ -5,23 +5,20 @@ from datetime import timedelta import pytest from galaxy import config +from galaxy.config import expand_pretty_datetime_format from galaxy.util import listify -from galaxy.web.formatting import expand_pretty_datetime_format +from galaxy.util.properties import running_from_source TestData = namedtuple("TestData", ("key", "expected", "loaded")) @pytest.fixture(scope="module") def appconfig(): - return config.GalaxyAppConfiguration() + return config.GalaxyAppConfiguration(override_tempdir=False) @pytest.fixture def mock_config_file(monkeypatch): - # Patch this; otherwise tempfile.tempdir will be set, which is a global variable that - # defines the value of the default `dir` argument to the functions in Python's - # tempfile module - which breaks multiple tests. - monkeypatch.setattr(config.GalaxyAppConfiguration, "_override_tempdir", lambda a, b: None) # Set this to return None to force the creation of base config directories # in _set_config_directories(). Used to test the values of these directories only. monkeypatch.setattr(config, "find_config_file", lambda x: None) @@ -33,13 +30,17 @@ def test_root(appconfig): def test_common_base_config(appconfig): assert appconfig.shed_tools_dir == os.path.join(appconfig.data_dir, "shed_tools") - assert appconfig.sample_config_dir == os.path.join(appconfig.root, "lib", "galaxy", "config", "sample") + if running_from_source: + expected_path = os.path.join(appconfig.root, "lib", "galaxy", "config", "sample") + else: + expected_path = os.path.join(appconfig.root, "galaxy", "config", "sample") + assert appconfig.sample_config_dir == expected_path def test_base_config_if_running_from_source(monkeypatch, mock_config_file): # Simulated condition: running from source, config_file is None. monkeypatch.setattr(config, "running_from_source", True) - appconfig = config.GalaxyAppConfiguration() + appconfig = config.GalaxyAppConfiguration(override_tempdir=False) assert not appconfig.config_file assert appconfig.config_dir == os.path.join(appconfig.root, "config") assert appconfig.data_dir == os.path.join(appconfig.root, "database") @@ -49,7 +50,7 @@ def test_base_config_if_running_from_source(monkeypatch, mock_config_file): def test_base_config_if_running_not_from_source(monkeypatch, mock_config_file): # Simulated condition: running not from source, config_file is None. monkeypatch.setattr(config, "running_from_source", False) - appconfig = config.GalaxyAppConfiguration() + appconfig = config.GalaxyAppConfiguration(override_tempdir=False) assert not appconfig.config_file assert appconfig.config_dir == os.getcwd() assert appconfig.data_dir == os.path.join(appconfig.config_dir, "data") @@ -102,7 +103,7 @@ class ExpectedValues: "biotools_service_cache_data_dir": self._in_cache_dir("biotools/data"), "biotools_service_cache_lock_dir": self._in_cache_dir("biotools/locks"), "build_sites_config_file": self._in_sample_dir("build_sites.yml.sample"), - "builds_file_path": self._in_root_dir("tool-data/shared/ucsc/builds.txt"), + "builds_file_path": self._in_root_or_data_dir("tool-data/shared/ucsc/builds.txt"), "cache_dir": self._in_data_dir("cache"), "citation_cache_data_dir": self._in_cache_dir("citations/data"), "citation_cache_lock_dir": self._in_cache_dir("citations/locks"), @@ -117,14 +118,14 @@ class ExpectedValues: "edam_toolbox_ontology_path": self._in_data_dir("EDAM.tsv"), "error_report_file": self._in_config_dir("error_report.yml"), "file_sources_config_file": self._in_config_dir("file_sources_conf.yml"), - "galaxy_data_manager_data_path": self._in_root_dir("tool-data"), + "galaxy_data_manager_data_path": self._in_root_or_data_dir("tool-data"), "integrated_tool_panel_config": self._in_managed_config_dir("integrated_tool_panel.xml"), "interactivetools_map": self._in_data_dir("interactivetools_map.sqlite"), "involucro_path": self._in_root_dir("involucro"), "job_config_file": self._in_config_dir("job_conf.xml"), "job_metrics_config_file": self._in_sample_dir("job_metrics_conf.xml.sample"), "job_resource_params_file": self._in_config_dir("job_resource_params_conf.xml"), - "len_file_path": self._in_root_dir("tool-data/shared/ucsc/chrom"), + "len_file_path": self._in_root_or_data_dir("tool-data/shared/ucsc/chrom"), "local_conda_mapping_file": self._in_config_dir("local_conda_mapping.yml"), "managed_config_dir": self._in_managed_config_dir(), "markdown_export_css": self._in_config_dir("markdown_export.css"), @@ -141,12 +142,12 @@ class ExpectedValues: "sanitize_allowlist_file": self._in_managed_config_dir("sanitize_allowlist.txt"), "shed_data_manager_config_file": self._in_managed_config_dir("shed_data_manager_conf.xml"), "shed_tool_config_file": self._in_managed_config_dir("shed_tool_conf.xml"), - "shed_tool_data_path": self._in_root_dir("tool-data"), + "shed_tool_data_path": self._in_root_or_data_dir("tool-data"), "shed_tool_data_table_config": self._in_managed_config_dir("shed_tool_data_table_conf.xml"), "template_cache_path": self._in_cache_dir("compiled_templates"), "tool_cache_data_dir": self._in_cache_dir("tool_cache"), "tool_config_file": self._in_sample_dir("tool_conf.xml.sample"), - "tool_data_path": self._in_root_dir("tool-data"), + "tool_data_path": self._in_root_or_data_dir("tool-data"), "tool_data_table_config_path": self._in_sample_dir("tool_data_table_conf.xml.sample"), "tool_destinations_config_file": self._in_config_dir("tool_destinations.yml"), "tool_path": self._in_root_dir("tools"), @@ -170,6 +171,12 @@ class ExpectedValues: def _in_root_dir(self, path=None): return self._in_dir(self._config.root, path) + def _in_root_or_data_dir(self, path): + if running_from_source: + return self._in_root_dir(path) + else: + return self._in_data_dir(path) + def _in_config_dir(self, path=None): return self._in_dir(self._config.config_dir, path) @@ -225,8 +232,7 @@ class ExpectedValues: def get_config_data(): - config.GalaxyAppConfiguration._override_tempdir = lambda a, b: None # method must be mocked - configuration = config.GalaxyAppConfiguration() + configuration = config.GalaxyAppConfiguration(override_tempdir=False) ev = ExpectedValues(configuration) items = ((k, v) for k, v in configuration.schema.app_schema.items()) for key, data in items: diff --git a/test/unit/data/model/mapping/test_install_model_mapping.py b/test/unit/data/model/mapping/test_install_model_mapping.py index e873b19949a..bffcb5f30ce 100644 --- a/test/unit/data/model/mapping/test_install_model_mapping.py +++ b/test/unit/data/model/mapping/test_install_model_mapping.py @@ -150,10 +150,9 @@ class TestRepositoryDependency(BaseTest): def test_columns(self, session, cls_, repository): create_time = datetime.now() update_time = create_time + timedelta(hours=1) - obj = cls_() + obj = cls_(repository.id) obj.create_time = create_time obj.update_time = update_time - obj.repository = repository with dbcleanup(session, obj) as obj_id: stored_obj = get_stored_obj(session, cls_, obj_id) @@ -163,8 +162,7 @@ class TestRepositoryDependency(BaseTest): assert stored_obj.tool_shed_repository_id == repository.id def test_relationships(self, session, cls_, repository): - obj = cls_() - obj.repository = repository + obj = cls_(repository.id) with dbcleanup(session, obj) as obj_id: stored_obj = get_stored_obj(session, cls_, obj_id) @@ -313,8 +311,7 @@ def repository_repository_dependency_association(session): @pytest.fixture def repository_dependency(session, repository): - instance = model.RepositoryDependency() - instance.repository = repository + instance = model.RepositoryDependency(repository.id) yield from dbcleanup_wrapper(session, instance) diff --git a/test/unit/data/model/mapping/test_model_mapping.py b/test/unit/data/model/mapping/test_model_mapping.py index 8db4c8eae9e..59a933ac58e 100644 --- a/test/unit/data/model/mapping/test_model_mapping.py +++ b/test/unit/data/model/mapping/test_model_mapping.py @@ -60,10 +60,7 @@ class TestPlanet(BaseTest): # BaseTest is a base class; we need it to get the t See other model tests in this module for examples of more complex setups. """ -from datetime import ( - datetime, - timedelta, -) +from datetime import timedelta from uuid import ( UUID, uuid4, @@ -76,6 +73,8 @@ from sqlalchemy import ( ) from galaxy import model +from galaxy.model.orm.now import now +from galaxy.model.orm.util import add_object_to_object_session from .common import ( AbstractBaseTest, collection_consists_of_objects, @@ -100,7 +99,7 @@ class TestAPIKeys(BaseTest): assert cls_.__tablename__ == "api_keys" def test_columns(self, session, cls_, user): - create_time, user_id, key = datetime.now(), user.id, get_unique_value() + create_time, user_id, key = now(), user.id, get_unique_value() obj = cls_(user_id=user_id, key=key, create_time=create_time) with dbcleanup(session, obj) as obj_id: @@ -123,7 +122,7 @@ class TestCleanupEvent(BaseTest): assert cls_.__tablename__ == "cleanup_event" def test_columns(self, session, cls_): - create_time, message = datetime.now(), "a" + create_time, message = now(), "a" obj = cls_(create_time=create_time, message=message) with dbcleanup(session, obj) as obj_id: @@ -138,7 +137,7 @@ class TestCleanupEventDatasetAssociation(BaseTest): assert cls_.__tablename__ == "cleanup_event_dataset_association" def test_columns(self, session, cls_, cleanup_event, dataset): - create_time = datetime.now() + create_time = now() obj = cls_( create_time=create_time, cleanup_event_id=cleanup_event.id, @@ -158,7 +157,7 @@ class TestCleanupEventHistoryAssociation(BaseTest): assert cls_.__tablename__ == "cleanup_event_history_association" def test_columns(self, session, cls_, cleanup_event, history): - create_time = datetime.now() + create_time = now() obj = cls_( create_time=create_time, cleanup_event_id=cleanup_event.id, @@ -178,7 +177,7 @@ class TestCleanupEventHistoryDatasetAssociationAssociation(BaseTest): assert cls_.__tablename__ == "cleanup_event_hda_association" def test_columns(self, session, cls_, cleanup_event, history_dataset_association): - create_time = datetime.now() + create_time = now() obj = cls_( create_time=create_time, cleanup_event_id=cleanup_event.id, @@ -198,7 +197,7 @@ class TestCleanupEventImplicitlyConvertedDatasetAssociationAssociation(BaseTest) assert cls_.__tablename__ == "cleanup_event_icda_association" def test_columns(self, session, cls_, cleanup_event, implicitly_converted_dataset_association): - create_time = datetime.now() + create_time = now() obj = cls_( create_time=create_time, cleanup_event_id=cleanup_event.id, @@ -218,7 +217,7 @@ class TestCleanupEventLibraryAssociation(BaseTest): assert cls_.__tablename__ == "cleanup_event_library_association" def test_columns(self, session, cls_, cleanup_event, library): - create_time = datetime.now() + create_time = now() obj = cls_( create_time=create_time, cleanup_event_id=cleanup_event.id, @@ -238,7 +237,7 @@ class TestCleanupEventLibraryDatasetAssociation(BaseTest): assert cls_.__tablename__ == "cleanup_event_library_dataset_association" def test_columns(self, session, cls_, cleanup_event, library_dataset): - create_time = datetime.now() + create_time = now() obj = cls_( create_time=create_time, cleanup_event_id=cleanup_event.id, @@ -258,7 +257,7 @@ class TestCleanupEventLibraryDatasetDatasetAssociationAssociation(BaseTest): assert cls_.__tablename__ == "cleanup_event_ldda_association" def test_columns(self, session, cls_, cleanup_event, library_dataset_dataset_association): - create_time = datetime.now() + create_time = now() obj = cls_( create_time=create_time, cleanup_event_id=cleanup_event.id, @@ -278,7 +277,7 @@ class TestCleanupEventLibraryFolderAssociation(BaseTest): assert cls_.__tablename__ == "cleanup_event_library_folder_association" def test_columns(self, session, cls_, cleanup_event, library_folder): - create_time = datetime.now() + create_time = now() obj = cls_( create_time=create_time, cleanup_event_id=cleanup_event.id, @@ -298,7 +297,7 @@ class TestCleanupEventMetadataFileAssociation(BaseTest): assert cls_.__tablename__ == "cleanup_event_metadata_file_association" def test_columns(self, session, cls_, cleanup_event, metadata_file): - create_time = datetime.now() + create_time = now() obj = cls_( create_time=create_time, cleanup_event_id=cleanup_event.id, @@ -324,7 +323,7 @@ class TestCustosAuthnzToken(BaseTest): access_token = "c" id_token = "d" refresh_token = "e" - expiration_time = datetime.now() + expiration_time = now() refresh_expiration_time = expiration_time + timedelta(hours=1) obj = cls_() @@ -393,7 +392,7 @@ class TestDataManagerHistoryAssociation(BaseTest): assert cls_.__tablename__ == "data_manager_history_association" def test_columns(self, session, cls_, history, user): - create_time = datetime.now() + create_time = now() update_time = create_time + timedelta(hours=1) obj = cls_() obj.create_time = create_time @@ -426,13 +425,12 @@ class TestDataManagerJobAssociation(BaseTest): assert has_index(cls_.__table__, ("data_manager_id",)) def test_columns(self, session, cls_, job): - create_time = datetime.now() + create_time = now() update_time = create_time + timedelta(hours=1) data_manager_id = "a" obj = cls_() obj.create_time = create_time obj.update_time = update_time - session.add(job) # must be bound to a session for lazy load of attributes obj.job = job obj.data_manager_id = data_manager_id @@ -445,7 +443,6 @@ class TestDataManagerJobAssociation(BaseTest): assert stored_obj.data_manager_id == data_manager_id def test_relationships(self, session, cls_, job): - session.add(job) # must be bound to a session for lazy load of attributes obj = cls_(job=job) with dbcleanup(session, obj) as obj_id: @@ -459,7 +456,7 @@ class TestDataset(BaseTest): assert cls_.table.name == "dataset" def test_columns(self, session, cls_, job): - create_time = datetime.now() + create_time = now() update_time = create_time + timedelta(hours=1) state = "a" deleted = True @@ -555,7 +552,7 @@ class TestDatasetCollection(BaseTest): populated_state = "b" populated_state_message = "c" element_count = 1 - create_time = datetime.now() + create_time = now() update_time = create_time + timedelta(hours=1) obj = cls_() @@ -690,7 +687,7 @@ class TestDatasetPermissions(BaseTest): def test_columns(self, session, cls_, dataset, role): action = "a" - create_time = datetime.now() + create_time = now() update_time = create_time + timedelta(hours=1) obj = cls_(action, dataset, role) obj.create_time = create_time @@ -801,7 +798,7 @@ class TestDefaultQuotaAssociation(BaseTest): def test_columns(self, session, cls_, quota): type_ = cls_.types.REGISTERED - create_time = datetime.now() + create_time = now() update_time = create_time + timedelta(hours=1) obj = cls_(type_, quota) obj.create_time = create_time @@ -861,7 +858,7 @@ class TestDynamicTool(BaseTest): active = True hidden = True value = "f" - create_time = datetime.now() + create_time = now() update_time = create_time + timedelta(hours=1) obj = cls_() @@ -914,7 +911,7 @@ class TestEvent(BaseTest): def test_columns(self, session, cls_, history, galaxy_session, user): message, tool_id = "a", "b" - create_time = datetime.now() + create_time = now() update_time = create_time + timedelta(hours=1) obj = cls_() obj.message = message @@ -1000,7 +997,7 @@ class TestFormDefinition(BaseTest): def test_columns(self, session, cls_, form_definition_current): name, desc, fields, type, layout = "a", "b", "c", "d", "e" - create_time = datetime.now() + create_time = now() update_time = create_time + timedelta(hours=1) obj = cls_() obj.name = name @@ -1037,7 +1034,7 @@ class TestFormDefinitionCurrent(BaseTest): assert cls_.__tablename__ == "form_definition_current" def test_columns(self, session, cls_, form_definition): - create_time = datetime.now() + create_time = now() update_time = create_time + timedelta(hours=1) deleted = True obj = cls_() @@ -1070,7 +1067,7 @@ class TestFormValues(BaseTest): def test_columns(self, session, cls_, form_definition): content = "a" - create_time = datetime.now() + create_time = now() update_time = create_time + timedelta(hours=1) obj = cls_() obj.form_definition = form_definition @@ -1100,7 +1097,7 @@ class TestGalaxySession(BaseTest): def test_columns(self, session, cls_, user, history, galaxy_session): - create_time = datetime.now() + create_time = now() update_time = create_time + timedelta(hours=1) remote_host = "a" remote_addr = "b" @@ -1155,7 +1152,7 @@ class TestGalaxySessionToHistoryAssociation(BaseTest): assert cls_.__tablename__ == "galaxy_session_to_history" def test_columns(self, session, cls_, galaxy_session, history): - create_time = datetime.now() + create_time = now() obj = cls_(galaxy_session, history) obj.create_time = create_time @@ -1181,7 +1178,7 @@ class TestGenomeIndexToolData(BaseTest): def test_columns(self, session, cls_, job, dataset, user): fasta_path = "a" - created_time = datetime.now() + created_time = now() modified_time = created_time + timedelta(hours=1) indexer = "b" obj = cls_() @@ -1222,7 +1219,7 @@ class TestGroup(BaseTest): assert cls_.__tablename__ == "galaxy_group" def test_columns(self, session, cls_): - create_time = datetime.now() + create_time = now() update_time = create_time + timedelta(hours=1) name = get_unique_value() deleted = True @@ -1267,7 +1264,7 @@ class TestGroupQuotaAssociation(BaseTest): assert cls_.__tablename__ == "group_quota_association" def test_columns(self, session, cls_, group, quota): - create_time = datetime.now() + create_time = now() update_time = create_time + timedelta(hours=1) obj = cls_(group, quota) obj.create_time = create_time @@ -1296,7 +1293,7 @@ class TestGroupRoleAssociation(BaseTest): def test_columns(self, session, cls_, group, role): obj = cls_(group, role) - create_time = datetime.now() + create_time = now() update_time = create_time + timedelta(hours=1) obj.create_time = create_time obj.update_time = update_time @@ -1324,7 +1321,7 @@ class TestHistory(BaseTest): assert has_index(cls_.__table__, ("slug",)) def test_columns(self, session, cls_, user): - create_time = datetime.now() + create_time = now() name = "a" hid_counter = 2 deleted = True @@ -1379,8 +1376,7 @@ class TestHistory(BaseTest): workflow_invocation, job, ): - obj = cls_() - obj.user = user + obj = cls_(user=user) obj.datasets.append(history_dataset_association) obj.dataset_collections.append(history_dataset_collection_association) obj.exports.append(job_export_history_archive) @@ -1473,7 +1469,7 @@ class TestHistoryDatasetAssociation(BaseTest): copied_from_hda = history_dataset_association_factory() parent = history_dataset_association_factory() - create_time = datetime.now() + create_time = now() update_time = create_time + timedelta(hours=1) state = "a" name = "b" @@ -1482,7 +1478,7 @@ class TestHistoryDatasetAssociation(BaseTest): peek = "e" tool_version = "f" extension = "g" - _metadata = "h" + _metadata = {"key": "value"} designation = "i" deleted = False visible = False @@ -1584,9 +1580,8 @@ class TestHistoryDatasetAssociation(BaseTest): icpda, ] - obj = cls_() + obj = cls_(dataset=dataset) obj.history = history - obj.dataset = dataset obj.copied_from_history_dataset_association = copied_from_hda obj.copied_from_library_dataset_dataset_association = copied_from_ldda obj.extended_metadata = extended_metadata @@ -1667,7 +1662,7 @@ class TestHistoryDatasetAssociationDisplayAtAuthorization(BaseTest): assert cls_.__tablename__ == "history_dataset_association_display_at_authorization" def test_columns(self, session, cls_, history_dataset_association, user): - create_time = datetime.now() + create_time = now() update_time = create_time + timedelta(hours=1) site = "a" obj = cls_(history_dataset_association, user, site) @@ -1697,10 +1692,10 @@ class TestHistoryDatasetAssociationHistory(BaseTest): def test_columns(self, session, cls_, history_dataset_association, extended_metadata): name, update_time, version, extension, metadata = ( "a", - datetime.now(), + now(), 2, "b", - "c", + {"key": "value"}, ) obj = cls_( history_dataset_association.id, @@ -1849,7 +1844,7 @@ class TestHistoryDatasetCollectionAssociation(BaseTest): visible = True deleted = True implicit_output_name = "b" - create_time = datetime.now() + create_time = now() update_time = create_time + timedelta(hours=1) obj = cls_() @@ -1906,6 +1901,7 @@ class TestHistoryDatasetCollectionAssociation(BaseTest): obj = cls_() obj.collection = dataset_collection obj.history = history + add_object_to_object_session(obj, history_dataset_collection_association) obj.copied_from_history_dataset_collection_association = history_dataset_collection_association obj.copied_to_history_dataset_collection_association.append(copied_to_hdca) obj.job = job @@ -2150,7 +2146,6 @@ class TestImplicitCollectionJobsJobAssociation(BaseTest): order_index = 1 obj = cls_() obj.implicit_collection_jobs = implicit_collection_jobs - session.add(job) # must be bound to a session for lazy load of attributes obj.job = job obj.order_index = order_index @@ -2164,7 +2159,6 @@ class TestImplicitCollectionJobsJobAssociation(BaseTest): def test_relationships(self, session, cls_, implicit_collection_jobs, job): obj = cls_() obj.implicit_collection_jobs = implicit_collection_jobs - session.add(job) # must be bound to a session for lazy load of attributes obj.job = job obj.order_index = 1 @@ -2185,7 +2179,7 @@ class TestImplicitlyConvertedDatasetAssociation(BaseTest): history_dataset_association, library_dataset_dataset_association, ): - create_time = datetime.now() + create_time = now() update_time = create_time + timedelta(hours=1) deleted = True metadata_safe = False @@ -2293,7 +2287,7 @@ class TestInteractiveToolEntryPoint(BaseTest): info = "f" configured = True deleted = True - created_time = datetime.now() + created_time = now() modified_time = created_time + timedelta(hours=1) obj = cls_() @@ -2353,7 +2347,7 @@ class TestJob(BaseTest): galaxy_session, user, ): - create_time = datetime.now() + create_time = now() update_time = create_time + timedelta(hours=1) tool_id = "a" tool_version = "b" @@ -2506,6 +2500,7 @@ class TestJob(BaseTest): obj.implicit_collection_jobs_association = implicit_collection_jobs_job_association obj.container = job_container_association obj.data_manager_association = data_manager_job_association + add_object_to_object_session(obj, history_dataset_collection_association) obj.history_dataset_collection_associations.append(history_dataset_collection_association) obj.workflow_invocation_step = workflow_invocation_step @@ -2567,12 +2562,10 @@ class TestJobContainerAssociation(BaseTest): container_type = "a" container_name = "b" container_info = "c" - created_time = datetime.now() + created_time = now() modified_time = created_time + timedelta(hours=1) - session.add(job) # must be bound to a session for lazy load of attributes - obj = cls_() - obj.job = job + obj = cls_(job=job) obj.container_type = container_type obj.container_name = container_name obj.container_info = container_info @@ -2590,7 +2583,6 @@ class TestJobContainerAssociation(BaseTest): assert stored_obj.modified_time == modified_time def test_relationships(self, session, cls_, job): - session.add(job) # must be bound to a session for lazy load of attributes obj = cls_(job=job) with dbcleanup(session, obj) as obj_id: @@ -2793,7 +2785,7 @@ class TestJobStateHistory(BaseTest): def test_columns(self, session, cls_, job): state, info = job.state, job.info - create_time = datetime.now() + create_time = now() update_time = create_time + timedelta(hours=1) obj = cls_(job) obj.create_time = create_time @@ -3025,7 +3017,7 @@ class TestLibrary(BaseTest): def test_columns(self, session, cls_, library_folder): name, deleted, purged, description, synopsis = "a", True, True, "b", "c" - create_time = datetime.now() + create_time = now() update_time = create_time + timedelta(hours=1) obj = cls_(name, description, synopsis, library_folder) obj.create_time = create_time @@ -3067,7 +3059,7 @@ class TestLibraryDataset(BaseTest): def test_columns(self, session, cls_, library_folder): order_id = 9 - create_time = datetime.now() + create_time = now() update_time = create_time + timedelta(hours=1) name = "a" info = "b" @@ -3275,7 +3267,7 @@ class TestLibraryDatasetDatasetAssociation(BaseTest): extended_metadata, user, ): - create_time = datetime.now() + create_time = now() state = "a" name = "b" info = "c" @@ -3289,7 +3281,7 @@ class TestLibraryDatasetDatasetAssociation(BaseTest): validated_state_message = "k" visible = True message = "m" - _metadata = "n" + _metadata = {"key": "value"} copied_from_ldda = library_dataset_dataset_association_factory() parent = library_dataset_dataset_association_factory() persist(session, copied_from_ldda) @@ -3297,6 +3289,7 @@ class TestLibraryDatasetDatasetAssociation(BaseTest): obj = cls_() obj.library_dataset = library_dataset + add_object_to_object_session(obj, dataset) obj.dataset = dataset obj.create_time = create_time obj.copied_from_history_dataset_association = history_dataset_association @@ -3385,6 +3378,7 @@ class TestLibraryDatasetDatasetAssociation(BaseTest): obj = cls_() obj.library_dataset = library_dataset + add_object_to_object_session(obj, dataset) obj.dataset = dataset obj.copied_from_history_dataset_association = history_dataset_association obj.copied_from_library_dataset_dataset_association = copied_from_ldda @@ -3431,7 +3425,7 @@ class TestLibraryDatasetDatasetAssociationPermissions(BaseTest): def test_columns(self, session, cls_, library_dataset_dataset_association, role): action = "a" - create_time = datetime.now() + create_time = now() update_time = create_time + timedelta(hours=1) obj = cls_(action, library_dataset_dataset_association, role) obj.create_time = create_time @@ -3533,7 +3527,7 @@ class TestLibraryDatasetPermissions(BaseTest): def test_columns(self, session, cls_, library_dataset, role): action = "a" - create_time = datetime.now() + create_time = now() update_time = create_time + timedelta(hours=1) obj = cls_(action, library_dataset, role) obj.create_time = create_time @@ -3564,7 +3558,7 @@ class TestLibraryFolder(BaseTest): def test_columns(self, session, cls_, library_folder): parent = library_folder - create_time = datetime.now() + create_time = now() update_time = create_time + timedelta(hours=1) name = "a" description = "b" @@ -3611,6 +3605,7 @@ class TestLibraryFolder(BaseTest): library_folder_factory, ): obj = cls_() + add_object_to_object_session(obj, library_folder) obj.parent = library_folder folder1 = library_folder_factory() obj.folders.append(folder1) @@ -3670,7 +3665,7 @@ class TestLibraryFolderPermissions(BaseTest): def test_columns(self, session, cls_, library_folder, role): action = "a" obj = cls_(action, library_folder, role) - create_time = datetime.now() + create_time = now() update_time = create_time + timedelta(hours=1) obj.create_time = create_time obj.update_time = update_time @@ -3727,7 +3722,7 @@ class TestLibraryPermissions(BaseTest): def test_columns(self, session, cls_, library, role): action = "a" obj = cls_(action, library, role) - create_time = datetime.now() + create_time = now() update_time = create_time + timedelta(hours=1) obj = cls_(action, library, role) obj.create_time = create_time @@ -3763,7 +3758,7 @@ class TestMetadataFile(BaseTest): library_dataset_dataset_association, ): name = "a" - create_time = datetime.now() + create_time = now() update_time = create_time + timedelta(hours=1) object_store_id = "b" uuid = uuid4() @@ -3899,7 +3894,7 @@ class TestPage(BaseTest): def test_columns(self, session, cls_, user, page_revision): title, deleted, importable, slug, published = "a", True, True, "b", True - create_time = datetime.now() + create_time = now() update_time = create_time + timedelta(hours=1) obj = cls_() obj.user = user @@ -4023,7 +4018,7 @@ class TestPageRevision(BaseTest): def test_columns(self, session, cls_, page): title, content = "a", "b" - create_time = datetime.now() + create_time = now() update_time = create_time + timedelta(hours=1) obj = cls_() obj.page = page @@ -4114,7 +4109,7 @@ class TestPasswordResetToken(BaseTest): def test_columns_and_relationships(self, session, cls_, user): token = get_unique_value() - expiration_time = datetime.now() + expiration_time = now() obj = cls_(user, token) obj.expiration_time = expiration_time @@ -4184,7 +4179,7 @@ class TestQuota(BaseTest): def test_columns(self, session, cls_): name, description, amount, operation = get_unique_value(), "b", 42, "+" - create_time = datetime.now() + create_time = now() update_time = create_time + timedelta(hours=1) obj = cls_(name, description, amount, operation) obj.create_time = create_time @@ -4237,7 +4232,7 @@ class TestRole(BaseTest): cls_.types.SYSTEM, True, ) - create_time = datetime.now() + create_time = now() update_time = create_time + timedelta(hours=1) obj = cls_(name, description, type_, deleted) obj.create_time = create_time @@ -4280,7 +4275,7 @@ class TestStoredWorkflow(BaseTest): assert has_index(cls_.__table__, ("slug",)) def test_columns(self, session, cls_, workflow, user): - create_time = datetime.now() + create_time = now() update_time = create_time + timedelta(hours=1) name = "a" deleted = True @@ -4405,6 +4400,7 @@ class TestStoredWorkflowMenuEntry(BaseTest): order_index = 1 obj = cls_() obj.stored_workflow = stored_workflow + add_object_to_object_session(obj, user) obj.user = user obj.order_index = order_index @@ -4418,6 +4414,7 @@ class TestStoredWorkflowMenuEntry(BaseTest): def test_relationships(self, session, cls_, stored_workflow, user): obj = cls_() obj.stored_workflow = stored_workflow + add_object_to_object_session(obj, user) obj.user = user with dbcleanup(session, obj) as obj_id: @@ -4555,7 +4552,7 @@ class TestTask(BaseTest): assert cls_.__tablename__ == "task" def test_columns(self, session, cls_, job): - create_time = datetime.now() + create_time = now() execution_time = create_time + timedelta(hours=1) update_time = execution_time + timedelta(hours=1) state = "p" @@ -4695,7 +4692,7 @@ class TestUser(BaseTest): assert cls_.__tablename__ == "galaxy_user" def test_columns(self, session, cls_, form_values): - create_time = datetime.now() + create_time = now() update_time = create_time + timedelta(hours=1) email = get_unique_value() username = get_unique_value() @@ -4845,7 +4842,7 @@ class TestUserAction(BaseTest): def test_columns(self, session, cls_, user, galaxy_session): action, params, context = "a", "b", "c" - create_time = datetime.now() + create_time = now() obj = cls_() obj.user = user obj.session_id = galaxy_session.id @@ -4891,7 +4888,7 @@ class TestUserAddress(BaseTest): deleted, purged, ) = ("a", "b", "c", "d", "e", "f", "g", "h", "i", True, False) - create_time = datetime.now() + create_time = now() update_time = create_time + timedelta(hours=1) obj = cls_() obj.user = user @@ -4969,7 +4966,7 @@ class TestUserGroupAssociation(BaseTest): assert cls_.__tablename__ == "user_group_association" def test_columns(self, session, cls_, user, group): - create_time = datetime.now() + create_time = now() update_time = create_time + timedelta(hours=1) obj = cls_(user, group) obj.create_time = create_time @@ -5016,7 +5013,7 @@ class TestUserQuotaAssociation(BaseTest): assert cls_.__tablename__ == "user_quota_association" def test_columns(self, session, cls_, user, quota): - create_time = datetime.now() + create_time = now() update_time = create_time + timedelta(hours=1) obj = cls_(user, quota) obj.create_time = create_time @@ -5044,7 +5041,7 @@ class TestUserRoleAssociation(BaseTest): assert cls_.__tablename__ == "user_role_association" def test_columns(self, session, cls_, user, role): - create_time = datetime.now() + create_time = now() update_time = create_time + timedelta(hours=1) obj = cls_(user, role) obj.create_time = create_time @@ -5072,7 +5069,7 @@ class TestVault(BaseTest): assert cls_.__tablename__ == "vault" def test_columns(self, session, cls_): - create_time = update_time = datetime.now() + create_time = update_time = now() key = "/some/path" parent_key = "/some" value = "helloworld" @@ -5094,7 +5091,7 @@ class TestVisualization(BaseTest): assert has_index(cls_.__table__, ("slug",)) def test_columns(self, session, cls_, user, visualization_revision): - create_time = datetime.now() + create_time = now() update_time = create_time + timedelta(hours=1) title = "a" type = "b" @@ -5235,7 +5232,7 @@ class TestVisualizationRevision(BaseTest): def test_columns(self, session, cls_, visualization): visualization, title, dbkey, config = visualization, "a", "b", "c" - create_time = datetime.now() + create_time = now() update_time = create_time + timedelta(hours=1) obj = cls_() obj.visualization = visualization @@ -5328,7 +5325,7 @@ class TestWorkerProcess(BaseTest): def test_columns(self, session, cls_): server_name, hostname = get_unique_value(), "a" - update_time = datetime.now() + update_time = now() obj = cls_() obj.server_name = server_name obj.hostname = hostname @@ -5348,7 +5345,7 @@ class TestWorkflow(BaseTest): assert cls_.__tablename__ == "workflow" def test_columns(self, session, cls_, stored_workflow, workflow): - create_time = datetime.now() + create_time = now() update_time = create_time + timedelta(hours=1) name = "a" has_cycles = True @@ -5356,6 +5353,7 @@ class TestWorkflow(BaseTest): reports_config = "b" creator_metadata = "c" license = "d" + source_metadata = "e" uuid = uuid4() obj = cls_() @@ -5369,6 +5367,7 @@ class TestWorkflow(BaseTest): obj.reports_config = reports_config obj.creator_metadata = creator_metadata obj.license = license + obj.source_metadata = source_metadata obj.uuid = uuid with dbcleanup(session, obj) as obj_id: @@ -5384,6 +5383,7 @@ class TestWorkflow(BaseTest): assert stored_obj.reports_config == reports_config assert stored_obj.creator_metadata == creator_metadata assert stored_obj.license == license + assert stored_obj.source_metadata == source_metadata assert stored_obj.uuid == uuid def test_relationships(self, session, cls_, stored_workflow, workflow, workflow_step_factory): @@ -5411,7 +5411,7 @@ class TestWorkflowInvocation(BaseTest): assert cls_.__tablename__ == "workflow_invocation" def test_columns(self, session, cls_, workflow, history): - create_time = datetime.now() + create_time = now() update_time = create_time + timedelta(hours=1) state = "a" scheduler = "b" @@ -5647,6 +5647,7 @@ class TestWorkflowInvocationOutputValue(BaseTest): persist(session, workflow_invocation_step) obj = cls_() + add_object_to_object_session(obj, workflow_invocation) obj.workflow_invocation = workflow_invocation obj.workflow_step = workflow_step obj.workflow_output = workflow_output @@ -5674,13 +5675,10 @@ class TestWorkflowInvocationStep(BaseTest): job, implicit_collection_jobs, ): - create_time = datetime.now() + create_time = now() update_time = create_time + timedelta(hours=1) state, action = "a", "b" - session.add(job) # must be bound to a session for lazy load of attributes - session.add(implicit_collection_jobs) # must be bound to a session for lazy load of attributes - obj = cls_() obj.create_time = create_time obj.update_time = update_time @@ -5715,9 +5713,6 @@ class TestWorkflowInvocationStep(BaseTest): workflow_invocation_step_output_dataset_association, workflow_invocation_output_value, ): - session.add(job) # must be bound to a session for lazy load of attributes - session.add(implicit_collection_jobs) # must be bound to a session for lazy load of attributes - # setup workflow_invocation_output_value to test the output_value attribute output_value = workflow_invocation_output_value output_value.workflow_invocation = workflow_invocation @@ -5725,6 +5720,7 @@ class TestWorkflowInvocationStep(BaseTest): persist(session, output_value) obj = cls_() + add_object_to_object_session(obj, workflow_invocation) obj.workflow_invocation = workflow_invocation obj.workflow_step = workflow_step obj.job = job @@ -6123,7 +6119,7 @@ class TestWorkflowStep(BaseTest): assert cls_.__tablename__ == "workflow_step" def test_columns(self, session, cls_, workflow, dynamic_tool, workflow_factory): - create_time = datetime.now() + create_time = now() update_time = create_time + timedelta(hours=1) type = "a" tool_id = "b" @@ -6953,6 +6949,7 @@ def role(session): @pytest.fixture def stored_workflow(session, user): instance = model.StoredWorkflow() + add_object_to_object_session(instance, user) instance.user = user yield from dbcleanup_wrapper(session, instance) @@ -7080,7 +7077,7 @@ def visualization_rating_association(session, user, visualization): @pytest.fixture def visualization_revision(session, visualization): - instance = model.VisualizationRevision(visualization=visualization) + instance = model.VisualizationRevision(visualization_id=visualization.id) yield from dbcleanup_wrapper(session, instance) @@ -7186,6 +7183,7 @@ def workflow_request_to_input_dataset_collection_association(session): @pytest.fixture def workflow_step(session, workflow): instance = model.WorkflowStep() + add_object_to_object_session(instance, workflow) instance.workflow = workflow yield from dbcleanup_wrapper(session, instance) @@ -7348,8 +7346,8 @@ def visualization_rating_association_factory(): @pytest.fixture def visualization_revision_factory(visualization): def make_instance(*args, **kwds): - if "visualization" not in kwds: - kwds["visualization"] = visualization + if "visualization_id" not in kwds: + kwds["visualization_id"] = visualization.id return model.VisualizationRevision(*args, **kwds) return make_instance @@ -7393,7 +7391,9 @@ def workflow_step_connection_factory(): def workflow_step_factory(workflow): def make_instance(*args, **kwds): instance = model.WorkflowStep() - instance.workflow = kwds.get("workflow", workflow) + workflow2 = kwds.get("workflow", workflow) # rename workflow not to confuse pytest + add_object_to_object_session(instance, workflow2) + instance.workflow = workflow2 instance.subworkflow = kwds.get("subworkflow") return instance diff --git a/lib/galaxy/model/tool_shed_install/migrate/__init__.py b/test/unit/data/model/migrations/__init__.py similarity index 100% rename from lib/galaxy/model/tool_shed_install/migrate/__init__.py rename to test/unit/data/model/migrations/__init__.py diff --git a/test/unit/data/model/migrations/common.py b/test/unit/data/model/migrations/common.py new file mode 100644 index 00000000000..1e5d418a92f --- /dev/null +++ b/test/unit/data/model/migrations/common.py @@ -0,0 +1,115 @@ +import os +import uuid +from contextlib import contextmanager +from typing import ( + Callable, + Iterator, + NewType, + Optional, +) + +import pytest +from sqlalchemy import create_engine +from sqlalchemy.engine import ( + Engine, + make_url, +) +from sqlalchemy.sql.compiler import IdentifierPreparer + +from galaxy.model.database_utils import create_database + +DbUrl = NewType("DbUrl", str) + +# Fixture and helper functions used to generate urls for postgresql and sqlite databases + + +@pytest.fixture +def url_factory(tmp_directory: str) -> Callable[[], DbUrl]: + """ + Return a factory function that produces a database url with a unique database name. + If _get_connection_url() returns a value, the database is postgresql; otherwise, it's + sqlite (referring to a location witin the /tmp directory). + """ + + def url() -> DbUrl: + database = _generate_unique_database_name() + connection_url = _get_connection_url() + if connection_url: + return _make_postgres_db_url(DbUrl(connection_url), database) + else: + return _make_sqlite_db_url(tmp_directory, database) + + return url + + +@contextmanager +def disposing_engine(url: DbUrl) -> Iterator[Engine]: + """Context manager for engine that disposes of its connection pool on exit.""" + engine = create_engine(url) + try: + yield engine + finally: + engine.dispose() + + +@contextmanager +def create_and_drop_database(url: DbUrl) -> Iterator[None]: + """ + Context manager that creates a database. If the database is postgresql, it is dropped on exit; + a sqlite database should be removed automatically by tempfile. + """ + try: + create_database(url) + yield + finally: + if _is_postgres(url): + _drop_postgres_database(url) + + +@contextmanager +def drop_database(url: DbUrl) -> Iterator[None]: + """ + Context manager that ensures a postgres database identified by url is dropped on exit; + a sqlite database should be removed automatically by tempfile. + """ + try: + yield + finally: + if _is_postgres(url): + _drop_postgres_database(url) + + +def _generate_unique_database_name() -> str: + return f"galaxytest_{uuid.uuid4().hex}" + + +def _get_connection_url() -> Optional[str]: + return os.environ.get("GALAXY_TEST_DBURI") + + +def _is_postgres(url: DbUrl) -> bool: + return url.startswith("postgres") + + +def _make_sqlite_db_url(tmpdir: str, database: str) -> DbUrl: + path = os.path.join(tmpdir, database) + return DbUrl(f"sqlite:///{path}") + + +def _make_postgres_db_url(connection_url: DbUrl, database: str) -> DbUrl: + url = make_url(connection_url) + url = url.set(database=database) + return DbUrl(str(url)) + + +def _drop_postgres_database(url: DbUrl) -> None: + db_url = make_url(url) + database = db_url.database + connection_url = db_url.set(database="postgres") + engine = create_engine(connection_url, isolation_level="AUTOCOMMIT") + preparer = IdentifierPreparer(engine.dialect) + database = preparer.quote(database) + stmt = f"DROP DATABASE IF EXISTS {database}" + with engine.connect() as conn: + conn.execute(stmt) + engine.dispose() diff --git a/test/unit/data/model/migrations/conftest.py b/test/unit/data/model/migrations/conftest.py new file mode 100644 index 00000000000..6e2a67e9556 --- /dev/null +++ b/test/unit/data/model/migrations/conftest.py @@ -0,0 +1,307 @@ +import tempfile + +import pytest +import sqlalchemy as sa + +# Helper fixtures + + +@pytest.fixture(scope="module") +def tmp_directory(): + with tempfile.TemporaryDirectory() as tmp_dir: + yield tmp_dir + + +# Fixtures: metadata containing one or more tables and representing database state. +# Used to load a database with a given state. +# Each state has 3 versions, distinguished by suffix: +# - gxy (galaxy database that holds gxy* and migration version tables) +# - tsi (tool_shed_install database that holds tsi* and migration version tables) +# - combined (combined database that holds gxy*, tsi*, and migration version tables) +# +# The following states are represented: +# +# State 1: Non-empty database, no version table. +# (Most ancient state) +# State 2: SQLAlchemy Migrate version table added. +# (Oldest state versioned with SQLAlchemy Migrate) +# State 3: New table added. +# (Last (most recent) state versioned with SQLAlchemy Migrate) +# State 4: Alembic version table added. +# (Oldest state versioned with Alembic) +# State 5: SQLAlchemy Migrate version table removed. +# (Oldest state versioned with Alembic that does not include the SQLAchemy Migrate version table) +# State 6: New table added. +# (Most recent state versioned with Alembic. This is the current state) +# +# Additional edge case: gxy at state3, tsi has no SQLAlchemy Migrate table. +# +# (State 0 assumes an empty database, so it needs no state fixtures.) + + +# state 1 +@pytest.fixture +def metadata_state1_gxy(gxy_table1): + metadata = sa.MetaData() + gxy_table1(metadata) + return metadata + + +@pytest.fixture +def metadata_state1_tsi(tsi_table1): + metadata = sa.MetaData() + tsi_table1(metadata) + return metadata + + +@pytest.fixture +def metadata_state1_combined(gxy_table1, tsi_table1): + metadata = sa.MetaData() + gxy_table1(metadata) + tsi_table1(metadata) + return metadata + + +# state 2 +@pytest.fixture +def metadata_state2_gxy(gxy_table1, sqlalchemymigrate_table): + metadata = sa.MetaData() + gxy_table1(metadata) + sqlalchemymigrate_table(metadata) + return metadata + + +@pytest.fixture +def metadata_state2_tsi(tsi_table1, sqlalchemymigrate_table): + metadata = sa.MetaData() + tsi_table1(metadata) + sqlalchemymigrate_table(metadata) + return metadata + + +@pytest.fixture +def metadata_state2_combined(gxy_table1, tsi_table1, sqlalchemymigrate_table): + metadata = sa.MetaData() + gxy_table1(metadata) + tsi_table1(metadata) + sqlalchemymigrate_table(metadata) + return metadata + + +# state 3 +@pytest.fixture +def metadata_state3_gxy(gxy_table1, gxy_table2, sqlalchemymigrate_table): + metadata = sa.MetaData() + gxy_table1(metadata) + gxy_table2(metadata) + sqlalchemymigrate_table(metadata) + return metadata + + +@pytest.fixture +def metadata_state3_tsi(tsi_table1, tsi_table2, sqlalchemymigrate_table): + metadata = sa.MetaData() + tsi_table1(metadata) + tsi_table2(metadata) + sqlalchemymigrate_table(metadata) + return metadata + + +@pytest.fixture +def metadata_state3_combined(gxy_table1, gxy_table2, tsi_table1, tsi_table2, sqlalchemymigrate_table): + metadata = sa.MetaData() + gxy_table1(metadata) + gxy_table2(metadata) + tsi_table1(metadata) + tsi_table2(metadata) + sqlalchemymigrate_table(metadata) + return metadata + + +# state 4 +@pytest.fixture +def metadata_state4_gxy(gxy_table1, gxy_table2, sqlalchemymigrate_table, alembic_table): + metadata = sa.MetaData() + gxy_table1(metadata) + gxy_table2(metadata) + sqlalchemymigrate_table(metadata) + alembic_table(metadata) + return metadata + + +@pytest.fixture +def metadata_state4_tsi(tsi_table1, tsi_table2, sqlalchemymigrate_table, alembic_table): + metadata = sa.MetaData() + tsi_table1(metadata) + tsi_table2(metadata) + sqlalchemymigrate_table(metadata) + alembic_table(metadata) + return metadata + + +@pytest.fixture +def metadata_state4_combined(gxy_table1, gxy_table2, tsi_table1, tsi_table2, sqlalchemymigrate_table, alembic_table): + metadata = sa.MetaData() + gxy_table1(metadata) + gxy_table2(metadata) + tsi_table1(metadata) + tsi_table2(metadata) + sqlalchemymigrate_table(metadata) + alembic_table(metadata) + return metadata + + +# state 5 +@pytest.fixture +def metadata_state5_gxy(gxy_table1, gxy_table2, alembic_table): + metadata = sa.MetaData() + gxy_table1(metadata) + gxy_table2(metadata) + alembic_table(metadata) + return metadata + + +@pytest.fixture +def metadata_state5_tsi(tsi_table1, tsi_table2, alembic_table): + metadata = sa.MetaData() + tsi_table1(metadata) + tsi_table2(metadata) + alembic_table(metadata) + return metadata + + +@pytest.fixture +def metadata_state5_combined(gxy_table1, gxy_table2, tsi_table1, tsi_table2, alembic_table): + metadata = sa.MetaData() + gxy_table1(metadata) + gxy_table2(metadata) + tsi_table1(metadata) + tsi_table2(metadata) + alembic_table(metadata) + return metadata + + +# state 6 +@pytest.fixture +def metadata_state6_gxy(gxy_table1, gxy_table2, gxy_table3, alembic_table): + metadata = sa.MetaData() + gxy_table1(metadata) + gxy_table2(metadata) + gxy_table3(metadata) + alembic_table(metadata) + return metadata + + +@pytest.fixture +def metadata_state6_tsi(tsi_table1, tsi_table2, tsi_table3, alembic_table): + metadata = sa.MetaData() + tsi_table1(metadata) + tsi_table2(metadata) + tsi_table3(metadata) + alembic_table(metadata) + return metadata + + +@pytest.fixture +def metadata_state6_combined(gxy_table1, gxy_table2, gxy_table3, tsi_table1, tsi_table2, tsi_table3, alembic_table): + metadata = sa.MetaData() + gxy_table1(metadata) + gxy_table2(metadata) + gxy_table3(metadata) + tsi_table1(metadata) + tsi_table2(metadata) + tsi_table3(metadata) + alembic_table(metadata) + return metadata + + +@pytest.fixture +def metadata_state6_gxy_state3_tsi_no_sam( + gxy_table1, gxy_table2, gxy_table3, tsi_table1, tsi_table2, tsi_table3, alembic_table +): + # This does NOT include sqlalchemymigrate_table (sam) + metadata = sa.MetaData() + gxy_table1(metadata) + gxy_table2(metadata) + gxy_table3(metadata) + tsi_table1(metadata) + tsi_table2(metadata) + alembic_table(metadata) + return metadata + + +# Fixture factories: metadata containing one table each. +# Used to compose metadata representing database state. +# (The `_factory` suffix is ommitted to keep the code less verbose) + + +@pytest.fixture +def gxy_table1(): + def make_table(metadata): + return sa.Table("gxy_table1", metadata, sa.Column("id", sa.Integer, primary_key=True)) + + return make_table + + +@pytest.fixture +def gxy_table2(): + def make_table(metadata): + return sa.Table("gxy_table2", metadata, sa.Column("id", sa.Integer, primary_key=True)) + + return make_table + + +@pytest.fixture +def gxy_table3(): + def make_table(metadata): + return sa.Table("gxy_table3", metadata, sa.Column("id", sa.Integer, primary_key=True)) + + return make_table + + +@pytest.fixture +def tsi_table1(): + def make_table(metadata): + return sa.Table("tsi_table1", metadata, sa.Column("id", sa.Integer, primary_key=True)) + + return make_table + + +@pytest.fixture +def tsi_table2(): + def make_table(metadata): + return sa.Table("tsi_table2", metadata, sa.Column("id", sa.Integer, primary_key=True)) + + return make_table + + +@pytest.fixture +def tsi_table3(): + def make_table(metadata): + return sa.Table("tsi_table3", metadata, sa.Column("id", sa.Integer, primary_key=True)) + + return make_table + + +@pytest.fixture +def alembic_table(): + def make_table(metadata): + table = sa.Table("alembic_version", metadata, sa.Column("version_num", sa.String(250), primary_key=True)) + return table + + return make_table + + +@pytest.fixture +def sqlalchemymigrate_table(): + def make_table(metadata): + table = sa.Table( + "migrate_version", + metadata, + sa.Column("repository_id", sa.String(250), primary_key=True), + sa.Column("repository_path", sa.Text), + sa.Column("version", sa.Integer), + ) + return table + + return make_table diff --git a/test/unit/data/model/migrations/test_migrations.py b/test/unit/data/model/migrations/test_migrations.py new file mode 100644 index 00000000000..2d3ff40ba30 --- /dev/null +++ b/test/unit/data/model/migrations/test_migrations.py @@ -0,0 +1,1192 @@ +import os +from typing import Union + +import alembic +import pytest +from alembic.config import Config +from sqlalchemy import ( + MetaData, + text, +) + +from galaxy.model import migrations +from galaxy.model.database_utils import database_exists +from galaxy.model.migrations import ( + AlembicManager, + DatabaseStateCache, + DatabaseStateVerifier, + get_last_sqlalchemymigrate_version, + GXY, + IncorrectVersionError, + listify, + load_metadata, + NoVersionTableError, + OutdatedDatabaseError, + scripts, + SQLALCHEMYMIGRATE_LAST_VERSION_GXY, + SQLALCHEMYMIGRATE_LAST_VERSION_TSI, + SQLALCHEMYMIGRATE_TABLE, + TSI, + verify_databases, +) +from galaxy.model.migrations.scripts import LegacyManageDb +from .common import ( # noqa: F401 (url_factory is a fixture we have to import explicitly) + create_and_drop_database, + disposing_engine, + drop_database, + url_factory, +) + +# Revision numbers from test versions directories +GXY_REVISION_0 = "62695fac6cc0" # oldest/base +GXY_REVISION_1 = "2e8a580bc79a" +GXY_REVISION_2 = "e02cef55763c" # current/head +TSI_REVISION_0 = "1bceec30363a" # oldest/base +TSI_REVISION_1 = "8364ef1cab05" +TSI_REVISION_2 = "0e28bf2fb7b5" # current/head + + +class TestAlembicManager: + def test_is_at_revision__one_head_one_revision(self, url_factory): # noqa: F811 + # Use case: Check if separate tsi database is at a given revision. + db_url = url_factory() + with create_and_drop_database(db_url): + with disposing_engine(db_url) as engine: + revision = GXY_REVISION_0 + assert not AlembicManagerForTests.is_at_revision(engine, revision) + am = AlembicManagerForTests(engine) + am.stamp_revision(revision) + assert AlembicManagerForTests.is_at_revision(engine, revision) + + def test_is_at_revision__two_heads_one_revision(self, url_factory): # noqa: F811 + # Use case: Check if combined gxy and tsi database is at a given gxy revision. + db_url = url_factory() + with create_and_drop_database(db_url): + with disposing_engine(db_url) as engine: + revision = GXY_REVISION_0 + revisions = [GXY_REVISION_0, TSI_REVISION_0] + assert not AlembicManagerForTests.is_at_revision(engine, revision) + am = AlembicManagerForTests(engine) + am.stamp_revision(revisions) + assert AlembicManagerForTests.is_at_revision(engine, revision) + + def test_is_at_revision__two_heads_two_revisions(self, url_factory): # noqa: F811 + # Use case: Check if combined gxy and tsi database is at given gxy and tsi revisions. + db_url = url_factory() + with create_and_drop_database(db_url): + with disposing_engine(db_url) as engine: + revisions = [GXY_REVISION_0, TSI_REVISION_0] + assert not AlembicManagerForTests.is_at_revision(engine, revisions) + am = AlembicManagerForTests(engine) + am.stamp_revision(revisions) + assert AlembicManagerForTests.is_at_revision(engine, revisions) + + def test_is_up_to_date_single_revision(self, url_factory): # noqa: F811 + db_url = url_factory() + with create_and_drop_database(db_url): + with disposing_engine(db_url) as engine: + model = GXY + am = AlembicManagerForTests(engine) + assert not am.is_up_to_date(model) + am.stamp_revision(GXY_REVISION_1) + assert not am.is_up_to_date(model) + am.stamp_revision(GXY_REVISION_2) + assert am.is_up_to_date(model) + + def test_not_is_up_to_date_wrong_model(self, url_factory): # noqa: F811 + db_url = url_factory() + with create_and_drop_database(db_url): + with disposing_engine(db_url) as engine: + am = AlembicManagerForTests(engine) + assert not am.is_up_to_date(GXY) + assert not am.is_up_to_date(TSI) + am.stamp_revision(GXY_REVISION_2) + assert am.is_up_to_date(GXY) + assert not am.is_up_to_date(TSI) + + def test_is_up_to_date_multiple_revisions(self, url_factory): # noqa: F811 + db_url = url_factory() + with create_and_drop_database(db_url): + with disposing_engine(db_url) as engine: + am = AlembicManagerForTests(engine) + assert not am.is_up_to_date(GXY) # False: no head revisions in database + am.stamp_revision([GXY_REVISION_2, TSI_REVISION_2]) + assert am.is_up_to_date(GXY) # True: both are up-to-date + assert am.is_up_to_date(TSI) # True: both are up-to-date + + def test_is_not_up_to_date_multiple_revisions_both(self, url_factory): # noqa: F811 + db_url = url_factory() + with create_and_drop_database(db_url): + with disposing_engine(db_url) as engine: + am = AlembicManagerForTests(engine) + am.stamp_revision([GXY_REVISION_1, TSI_REVISION_1]) + assert not am.is_up_to_date(GXY) # False: both are not up-to-date + assert not am.is_up_to_date(TSI) # False: both are not up-to-date + + def test_is_not_up_to_date_multiple_revisions_one(self, url_factory): # noqa: F811 + db_url = url_factory() + with create_and_drop_database(db_url): + with disposing_engine(db_url) as engine: + am = AlembicManagerForTests(engine) + am.stamp_revision([GXY_REVISION_2, TSI_REVISION_1]) + assert am.is_up_to_date(GXY) # True + assert not am.is_up_to_date(TSI) # False: only one is up-to-date + + def test_is_under_version_control(self, url_factory): # noqa: F811 + db_url = url_factory() + with create_and_drop_database(db_url): + with disposing_engine(db_url) as engine: + am = AlembicManagerForTests(engine) + assert not am.is_under_version_control(GXY) + assert not am.is_under_version_control(TSI) + am.stamp_revision(GXY_REVISION_0) + assert am.is_under_version_control(GXY) + assert not am.is_under_version_control(TSI) + am.stamp_revision(TSI_REVISION_0) + assert am.is_under_version_control(GXY) + assert am.is_under_version_control(TSI) + + def test_get_revision_raises_error_if_revision_not_found(self, url_factory): # noqa: F811 + db_url = url_factory() + with create_and_drop_database(db_url): + with disposing_engine(db_url) as engine: + am = AlembicManagerForTests(engine) + with pytest.raises(alembic.util.exc.CommandError): + am._get_revision("invalid") + + def test_get_model_db_head(self, url_factory): # noqa: F811 + db_url = url_factory() + with create_and_drop_database(db_url): + with disposing_engine(db_url) as engine: + am = AlembicManagerForTests(engine) + revisions = [GXY_REVISION_1, TSI_REVISION_2] + am.stamp_revision(revisions) + db_head = am.get_model_db_head(GXY) + assert db_head == GXY_REVISION_1 # We stamped the db with this GXY revision + + def test_get_model_script_head(self, url_factory): # noqa: F811 + db_url = url_factory() + with create_and_drop_database(db_url): + with disposing_engine(db_url) as engine: + am = AlembicManagerForTests(engine) + script_head = am.get_model_script_head(GXY) + assert script_head == GXY_REVISION_2 # That's the latest GXY revision in our script directory + + +class TestDatabaseStateCache: + def test_is_empty(self, url_factory, metadata_state1_gxy): # noqa: F811 + db_url, metadata = url_factory(), metadata_state1_gxy + with create_and_drop_database(db_url): + with disposing_engine(db_url) as engine: + assert DatabaseStateCache(engine).is_database_empty() + with engine.connect() as conn: + metadata.create_all(bind=conn) + assert not DatabaseStateCache(engine).is_database_empty() + + def test_has_alembic_version_table(self, url_factory, metadata_state4_gxy): # noqa: F811 + db_url, metadata = url_factory(), metadata_state4_gxy + with create_and_drop_database(db_url): + with disposing_engine(db_url) as engine: + assert not DatabaseStateCache(engine).has_alembic_version_table() + with engine.connect() as conn: + metadata.create_all(bind=conn) + assert DatabaseStateCache(engine).has_alembic_version_table() + + def test_has_sqlalchemymigrate_version_table(self, url_factory, metadata_state2_gxy): # noqa: F811 + db_url, metadata = url_factory(), metadata_state2_gxy + with create_and_drop_database(db_url): + with disposing_engine(db_url) as engine: + assert not DatabaseStateCache(engine).has_sqlalchemymigrate_version_table() + with engine.connect() as conn: + metadata.create_all(bind=conn) + assert DatabaseStateCache(engine).has_sqlalchemymigrate_version_table() + + def test_is_last_sqlalchemymigrate_version(self, url_factory, metadata_state2_gxy): # noqa: F811 + db_url = url_factory() + with create_and_drop_database(db_url): + with disposing_engine(db_url) as engine: + load_metadata(metadata_state2_gxy, engine) + load_sqlalchemymigrate_version(db_url, SQLALCHEMYMIGRATE_LAST_VERSION_GXY - 1) + assert not DatabaseStateCache(engine).is_last_sqlalchemymigrate_version( + SQLALCHEMYMIGRATE_LAST_VERSION_GXY + ) + load_sqlalchemymigrate_version(db_url, SQLALCHEMYMIGRATE_LAST_VERSION_GXY) + assert DatabaseStateCache(engine).is_last_sqlalchemymigrate_version(SQLALCHEMYMIGRATE_LAST_VERSION_GXY) + + +# Database fixture tests + + +class TestDatabaseFixtures: + # Verify that database fixtures have the expected state. + # + # The fixtures of the form `db_state#_[gxy|tsi]` are urls that point + # to databases that HAVE BEEN CREATED. Thus, we are not wrapping them here + # in the `create_and_drop_database` context manager: they are wrapped already. + class TestState1: + def test_database_gxy(self, db_state1_gxy, metadata_state1_gxy): + self.verify_state(db_state1_gxy, metadata_state1_gxy) + + def test_database_tsi(self, db_state1_tsi, metadata_state1_tsi): + self.verify_state(db_state1_tsi, metadata_state1_tsi) + + def test_database_combined(self, db_state1_combined, metadata_state1_combined): + self.verify_state(db_state1_combined, metadata_state1_combined) + + def verify_state(self, db_url, metadata): + assert is_metadata_loaded(db_url, metadata) + with disposing_engine(db_url) as engine: + db = DatabaseStateCache(engine) + assert not db.has_sqlalchemymigrate_version_table() + assert not db.has_alembic_version_table() + + class TestState2: + def test_database_gxy(self, db_state2_gxy, metadata_state2_gxy): + self.verify_state(db_state2_gxy, metadata_state2_gxy) + + def test_database_tsi(self, db_state2_tsi, metadata_state2_tsi): + self.verify_state(db_state2_tsi, metadata_state2_tsi) + + def test_database_combined(self, db_state2_combined, metadata_state2_combined): + self.verify_state(db_state2_combined, metadata_state2_combined) + + def verify_state(self, db_url, metadata): + assert is_metadata_loaded(db_url, metadata) + with disposing_engine(db_url) as engine: + db = DatabaseStateCache(engine) + assert db.has_sqlalchemymigrate_version_table() + assert not db.is_last_sqlalchemymigrate_version(SQLALCHEMYMIGRATE_LAST_VERSION_GXY) + assert not db.is_last_sqlalchemymigrate_version(SQLALCHEMYMIGRATE_LAST_VERSION_TSI) + assert not db.has_alembic_version_table() + + class TestState3: + def test_database_gxy(self, db_state3_gxy, metadata_state3_gxy): + self.verify_state(db_state3_gxy, metadata_state3_gxy, SQLALCHEMYMIGRATE_LAST_VERSION_GXY) + + def test_database_tsi(self, db_state3_tsi, metadata_state3_tsi): + self.verify_state(db_state3_tsi, metadata_state3_tsi, SQLALCHEMYMIGRATE_LAST_VERSION_TSI) + + def test_database_combined(self, db_state3_combined, metadata_state3_combined): + self.verify_state(db_state3_combined, metadata_state3_combined, SQLALCHEMYMIGRATE_LAST_VERSION_GXY) + + def verify_state(self, db_url, metadata, last_version): + assert is_metadata_loaded(db_url, metadata) + with disposing_engine(db_url) as engine: + db = DatabaseStateCache(engine) + assert db.has_sqlalchemymigrate_version_table() + assert db.is_last_sqlalchemymigrate_version(last_version) + assert not db.has_alembic_version_table() + + class TestState4: + def test_database_gxy(self, db_state4_gxy, metadata_state4_gxy): + self.verify_state(db_state4_gxy, metadata_state4_gxy, GXY_REVISION_0, SQLALCHEMYMIGRATE_LAST_VERSION_GXY) + + def test_database_tsi(self, db_state4_tsi, metadata_state4_tsi): + self.verify_state(db_state4_tsi, metadata_state4_tsi, TSI_REVISION_0, SQLALCHEMYMIGRATE_LAST_VERSION_TSI) + + def test_database_combined(self, db_state4_combined, metadata_state4_combined): + self.verify_state( + db_state4_combined, + metadata_state4_combined, + [GXY_REVISION_0, TSI_REVISION_0], + SQLALCHEMYMIGRATE_LAST_VERSION_GXY, + ) + + def verify_state(self, db_url, metadata, revision, last_version): + assert is_metadata_loaded(db_url, metadata) + with disposing_engine(db_url) as engine: + db = DatabaseStateCache(engine) + assert db.has_sqlalchemymigrate_version_table() + assert db.is_last_sqlalchemymigrate_version(last_version) + assert db.has_alembic_version_table() + assert AlembicManagerForTests.is_at_revision(engine, revision) + + class TestState5: + def test_database_gxy(self, db_state5_gxy, metadata_state5_gxy): + self.verify_state(db_state5_gxy, metadata_state5_gxy, GXY_REVISION_1) + + def test_database_tsi(self, db_state5_tsi, metadata_state5_tsi): + self.verify_state(db_state5_tsi, metadata_state5_tsi, TSI_REVISION_1) + + def test_database_combined(self, db_state5_combined, metadata_state5_combined): + self.verify_state(db_state5_combined, metadata_state5_combined, [GXY_REVISION_1, TSI_REVISION_1]) + + def verify_state(self, db_url, metadata, revision): + assert is_metadata_loaded(db_url, metadata) + with disposing_engine(db_url) as engine: + db = DatabaseStateCache(engine) + assert not db.has_sqlalchemymigrate_version_table() + assert db.has_alembic_version_table() + assert AlembicManagerForTests.is_at_revision(engine, revision) + + class TestState6: + def test_database_gxy(self, db_state6_gxy, metadata_state6_gxy): + self.verify_state(db_state6_gxy, metadata_state6_gxy, GXY_REVISION_2) + + def test_database_tsi(self, db_state6_tsi, metadata_state6_tsi): + self.verify_state(db_state6_tsi, metadata_state6_tsi, TSI_REVISION_2) + + def test_database_combined(self, db_state6_combined, metadata_state6_combined): + self.verify_state(db_state6_combined, metadata_state6_combined, [GXY_REVISION_2, TSI_REVISION_2]) + + def verify_state(self, db_url, metadata, revision): + assert is_metadata_loaded(db_url, metadata) + with disposing_engine(db_url) as engine: + db = DatabaseStateCache(engine) + assert not db.has_sqlalchemymigrate_version_table() + assert db.has_alembic_version_table() + + class TestState6GxyState3TsiNoSam: + def test_database_combined(self, db_state6_gxy_state3_tsi_no_sam, metadata_state6_gxy_state3_tsi_no_sam): + db_url = db_state6_gxy_state3_tsi_no_sam + metadata = metadata_state6_gxy_state3_tsi_no_sam + assert is_metadata_loaded(db_url, metadata) + with disposing_engine(db_url) as engine: + db = DatabaseStateCache(engine) + assert not db.has_sqlalchemymigrate_version_table() + assert db.has_alembic_version_table() + assert AlembicManagerForTests.is_at_revision(engine, GXY_REVISION_2) + + +def _verify_databases(gxy_engine, tsi_engine=None): + verify_databases(gxy_engine, None, None, tsi_engine, None, None, False) + + +class TestDatabaseStates: + # Tests of primary function under different scenarios and database state. + + class TestNoState: + # Initial state: database does not exist. + # Expect: database created, initialized, versioned w/alembic. + # (we use `metadata_state6_{gxy|tsi|combined}` for final database schema) + def test_combined_database(self, url_factory, metadata_state6_combined): # noqa: F811 + db_url = url_factory() + with drop_database(db_url): + assert not database_exists(db_url) + with disposing_engine(db_url) as engine: + _verify_databases(engine) + assert database_is_up_to_date(db_url, metadata_state6_combined, GXY) + assert database_is_up_to_date(db_url, metadata_state6_combined, TSI) + + def test_separate_databases(self, url_factory, metadata_state6_gxy, metadata_state6_tsi): # noqa: F811 + db1_url, db2_url = url_factory(), url_factory() + assert not database_exists(db1_url) + assert not database_exists(db2_url) + with drop_database(db1_url), drop_database(db2_url): + with disposing_engine(db1_url) as engine1, disposing_engine(db2_url) as engine2: + _verify_databases(engine1, engine2) + assert database_is_up_to_date(db1_url, metadata_state6_gxy, GXY) + assert database_is_up_to_date(db2_url, metadata_state6_tsi, TSI) + + class TestState0: + # Initial state: database is empty. + # Expect: database created, initialized, versioned w/alembic. + def test_combined_database(self, url_factory, metadata_state6_combined): # noqa: F811 + db_url = url_factory() + with create_and_drop_database(db_url): + with disposing_engine(db_url) as engine: + assert database_exists(db_url) + assert database_is_empty(db_url) + _verify_databases(engine) + assert database_is_up_to_date(db_url, metadata_state6_combined, GXY) + assert database_is_up_to_date(db_url, metadata_state6_combined, TSI) + + def test_separate_databases(self, url_factory, metadata_state6_gxy, metadata_state6_tsi): # noqa: F811 + db1_url, db2_url = url_factory(), url_factory() + with create_and_drop_database(db1_url), create_and_drop_database(db2_url): + with disposing_engine(db1_url) as engine1, disposing_engine(db2_url) as engine2: + assert database_exists(db1_url) + assert database_exists(db2_url) + assert database_is_empty(db1_url) + assert database_is_empty(db2_url) + _verify_databases(engine1, engine2) + assert database_is_up_to_date(db1_url, metadata_state6_gxy, GXY) + assert database_is_up_to_date(db2_url, metadata_state6_tsi, TSI) + + class TestState1: + # Initial state: non-empty database, no version table. + # Expect: fail with appropriate message. + def test_combined_database(self, db_state1_combined): + with pytest.raises(NoVersionTableError): + with disposing_engine(db_state1_combined) as engine: + _verify_databases(engine) + + def test_separate_databases_gxy_raises_error(self, db_state1_gxy, db_state6_tsi): + with pytest.raises(NoVersionTableError): + with disposing_engine(db_state1_gxy) as engine1, disposing_engine(db_state6_tsi) as engine2: + _verify_databases(engine1, engine2) + + def test_separate_databases_tsi_raises_error(self, db_state6_gxy, db_state1_tsi): + with pytest.raises(NoVersionTableError): + with disposing_engine(db_state6_gxy) as engine1, disposing_engine(db_state1_tsi) as engine2: + _verify_databases(engine1, engine2) + + class TestState2: + # Initial state: non-empty database, SQLAlchemy Migrate version table present; however, + # the stored version is not the latest after which we could transition to Alembic. + # Expect: fail with appropriate message. + def test_combined_database(self, db_state2_combined): + with pytest.raises(IncorrectVersionError): + with disposing_engine(db_state2_combined) as engine: + _verify_databases(engine) + + def test_separate_databases_gxy_raises_error(self, db_state2_gxy, db_state6_tsi): + with pytest.raises(IncorrectVersionError): + with disposing_engine(db_state2_gxy) as engine1, disposing_engine(db_state6_tsi) as engine2: + _verify_databases(engine1, engine2) + + def test_separate_databases_tsi_raises_error(self, db_state6_gxy, db_state2_tsi): + with pytest.raises(IncorrectVersionError): + with disposing_engine(db_state6_gxy) as engine1, disposing_engine(db_state2_tsi) as engine2: + _verify_databases(engine1, engine2) + + class TestState3: + # Initial state: non-empty database, SQLAlchemy Migrate version table contains latest version + # under SQLAlchemy Migrate. + # Expect: + # a) auto-migrate enabled: alembic version table added, database upgraded to current version. + # b) auto-migrate disabled: fail with appropriate message. + def test_combined_database_automigrate( + self, + db_state3_combined, + metadata_state6_combined, + set_automigrate, + ): + db_url = db_state3_combined + with disposing_engine(db_state3_combined) as engine: + _verify_databases(engine) + assert database_is_up_to_date(db_url, metadata_state6_combined, GXY) + assert database_is_up_to_date(db_url, metadata_state6_combined, TSI) + + def test_separate_databases_automigrate( + self, + db_state3_gxy, + db_state3_tsi, + metadata_state6_gxy, + metadata_state6_tsi, + set_automigrate, + ): + db1_url, db2_url = db_state3_gxy, db_state3_tsi + with disposing_engine(db1_url) as engine1, disposing_engine(db2_url) as engine2: + _verify_databases(engine1, engine2) + assert database_is_up_to_date(db1_url, metadata_state6_gxy, GXY) + assert database_is_up_to_date(db2_url, metadata_state6_tsi, TSI) + + def test_combined_database_no_automigrate(self, db_state3_combined): + with pytest.raises(OutdatedDatabaseError): + with disposing_engine(db_state3_combined) as engine: + _verify_databases(engine) + + def test_separate_databases_no_automigrate_gxy_raises_error(self, db_state3_gxy, db_state6_tsi): + with pytest.raises(OutdatedDatabaseError): + with disposing_engine(db_state3_gxy) as engine1, disposing_engine(db_state6_tsi) as engine2: + _verify_databases(engine1, engine2) + + def test_separate_databases_no_automigrate_tsi_raises_error(self, db_state6_gxy, db_state3_tsi): + with pytest.raises(OutdatedDatabaseError): + with disposing_engine(db_state6_gxy) as engine1, disposing_engine(db_state3_tsi) as engine2: + _verify_databases(engine1, engine2) + + class TestState4: + # Initial state: non-empty database, SQLAlchemy Migrate version table present, Alembic version table present. + # Oldest Alembic revision. + # Expect: + # a) auto-migrate enabled: database upgraded to current version. + # b) auto-migrate disabled: fail with appropriate message. + def test_combined_database_automigrate( + self, + db_state4_combined, + metadata_state6_combined, + set_automigrate, + ): + db_url = db_state4_combined + with disposing_engine(db_url) as engine: + _verify_databases(engine) + assert database_is_up_to_date(db_url, metadata_state6_combined, GXY) + assert database_is_up_to_date(db_url, metadata_state6_combined, TSI) + + def test_separate_databases_automigrate( + self, + db_state4_gxy, + db_state4_tsi, + metadata_state6_gxy, + metadata_state6_tsi, + set_automigrate, + ): + db1_url, db2_url = db_state4_gxy, db_state4_tsi + with disposing_engine(db1_url) as engine1, disposing_engine(db2_url) as engine2: + _verify_databases(engine1, engine2) + assert database_is_up_to_date(db1_url, metadata_state6_gxy, GXY) + assert database_is_up_to_date(db2_url, metadata_state6_tsi, TSI) + + def test_combined_database_no_automigrate(self, db_state4_combined): + with pytest.raises(OutdatedDatabaseError): + with disposing_engine(db_state4_combined) as engine: + _verify_databases(engine) + + def test_separate_databases_no_automigrate_gxy_raises_error(self, db_state4_gxy, db_state6_tsi): + with pytest.raises(OutdatedDatabaseError): + with disposing_engine(db_state4_gxy) as engine1, disposing_engine(db_state6_tsi) as engine2: + _verify_databases(engine1, engine2) + + def test_separate_databases_no_automigrate_tsi_raises_error(self, db_state6_gxy, db_state4_tsi): + with pytest.raises(OutdatedDatabaseError): + with disposing_engine(db_state6_gxy) as engine1, disposing_engine(db_state4_tsi) as engine2: + _verify_databases(engine1, engine2) + + class TestState5: + # Initial state: non-empty database, Alembic version table present. + # Oldest Alembic revision that does not include SQLAlchemy Migrate version table. + # Expect: + # a) auto-migrate enabled: database upgraded to current version. + # b) auto-migrate disabled: fail with appropriate message. + def test_combined_database_automigrate(self, db_state5_combined, metadata_state6_combined, set_automigrate): + db_url = db_state5_combined + with disposing_engine(db_url) as engine: + _verify_databases(engine) + assert database_is_up_to_date(db_url, metadata_state6_combined, GXY) + assert database_is_up_to_date(db_url, metadata_state6_combined, TSI) + + def test_separate_databases_automigrate( + self, db_state5_gxy, db_state5_tsi, metadata_state6_gxy, metadata_state6_tsi, set_automigrate + ): + db1_url, db2_url = db_state5_gxy, db_state5_tsi + with disposing_engine(db1_url) as engine1, disposing_engine(db2_url) as engine2: + _verify_databases(engine1, engine2) + assert database_is_up_to_date(db1_url, metadata_state6_gxy, GXY) + assert database_is_up_to_date(db2_url, metadata_state6_tsi, TSI) + + def test_combined_database_no_automigrate(self, db_state5_combined): + with pytest.raises(OutdatedDatabaseError): + with disposing_engine(db_state5_combined) as engine: + _verify_databases(engine) + + def test_separate_databases_no_automigrate_gxy_raises_error(self, db_state5_gxy, db_state6_tsi): + with pytest.raises(OutdatedDatabaseError): + with disposing_engine(db_state5_gxy) as engine1, disposing_engine(db_state6_tsi) as engine2: + _verify_databases(engine1, engine2) + + def test_separate_databases_no_automigrate_tsi_raises_error(self, db_state6_gxy, db_state5_tsi): + with pytest.raises(OutdatedDatabaseError): + with disposing_engine(db_state6_gxy) as engine1, disposing_engine(db_state5_tsi) as engine2: + _verify_databases(engine1, engine2) + + class TestState6: + # Initial state: non-empty database, Alembic version table present, database up-to-date. + # Expect: do nothing. + def test_combined_database(self, db_state6_combined, metadata_state6_combined): + db_url = db_state6_combined + with disposing_engine(db_url) as engine: + _verify_databases(engine) + assert database_is_up_to_date(db_url, metadata_state6_combined, GXY) + assert database_is_up_to_date(db_url, metadata_state6_combined, TSI) + + def test_separate_databases(self, db_state6_gxy, db_state6_tsi, metadata_state6_gxy, metadata_state6_tsi): + db1_url, db2_url = db_state6_gxy, db_state6_tsi + with disposing_engine(db1_url) as engine1, disposing_engine(db2_url) as engine2: + _verify_databases(engine1, engine2) + assert database_is_up_to_date(db1_url, metadata_state6_gxy, GXY) + assert database_is_up_to_date(db2_url, metadata_state6_tsi, TSI) + + class TestPartiallyUpgradedCombinedDatabase: + # This only applies to an EXISTING, NONEMPTY database, that is COMBINED (both GXY and TSI models + # are in the same database). + # This covers several edge cases when the GXY model is up-to-date, but the TSI model is not. + # This may happen for a variety of reasons: + # - a database is being upgraded to Alembic: the GXY model is processed first, + # so GXY will become up-to-date before TSI; + # - a database is changed to "combined" (via setting install_database_connection): so TSI has not + # been initialized in this database. + # - a glitch in the Matrix, an act of god, etc. + # These tests verify the system's handling of such cases for 2 TSI states: + # - Database has no TSI tables: assume new TSI install. + # - Database has some TSI tables: assume TSI is at last pre-alembic state and can be migrated. + # (We expect a severly outdated TSI model to have been upgraded manually together with the GXY model; + # for there is no reasonable way to determine its state automatically without a version table.) + # + # Initial state for all tests: + # - combined database + # - GXY model: up-to-date + # - TSI model: not in Alembic version table + def test_case1_automigrate(self, db_state6_gxy, metadata_state6_combined, set_automigrate): + # Initial state: + # - no TSI tables exist + # - auto-migrate enabled + # Expect: database is up-to-date + db_url = db_state6_gxy + with disposing_engine(db_url) as engine: + _verify_databases(engine) + assert database_is_up_to_date(db_url, metadata_state6_combined, GXY) + assert database_is_up_to_date(db_url, metadata_state6_combined, TSI) + + def test_case1_no_automigrate(self, db_state6_gxy, metadata_state6_combined): + # Initial state: + # - no TSI tables exist + # - auto-migrate not enabled + # Expect: database is up-to-date (same as auto-migrate enabled) + db_url = db_state6_gxy + with disposing_engine(db_url) as engine: + _verify_databases(engine) + assert database_is_up_to_date(db_url, metadata_state6_combined, GXY) + assert database_is_up_to_date(db_url, metadata_state6_combined, TSI) + + def test_case2_automigrate(self, db_state6_gxy_state3_tsi_no_sam, metadata_state6_combined, set_automigrate): + # Initial state: + # - all pre-alembic TSI tables exist + # - auto-migrate enabled + # Expect: database is up-to-date + db_url = db_state6_gxy_state3_tsi_no_sam + with disposing_engine(db_url) as engine: + _verify_databases(engine) + assert database_is_up_to_date(db_url, metadata_state6_combined, GXY) + assert database_is_up_to_date(db_url, metadata_state6_combined, TSI) + + def test_case2_no_automigrate(self, db_state6_gxy_state3_tsi_no_sam): + # Initial state: + # - all pre-alembic TSI tables exist + # - auto-migrate not enabled + # Expect: fail with appropriate message + db_url = db_state6_gxy_state3_tsi_no_sam + with pytest.raises(OutdatedDatabaseError): + with disposing_engine(db_url) as engine: + _verify_databases(engine) + + +# Test helpers + their tests, misc. fixtures + + +@pytest.fixture(autouse=True) # always override AlembicManager +def set_create_additional(monkeypatch): + monkeypatch.setattr(DatabaseStateVerifier, "_create_additional_database_objects", lambda *_: None) + + +@pytest.fixture +def set_automigrate(monkeypatch): + monkeypatch.setattr(DatabaseStateVerifier, "is_auto_migrate", True) + + +@pytest.fixture(autouse=True) # always override AlembicManager +def set_alembic_manager(monkeypatch): + monkeypatch.setattr(migrations, "get_alembic_manager", lambda engine: AlembicManagerForTests(engine)) + + +@pytest.fixture(autouse=True) # always override gxy_metadata +def set_gxy_metadata(monkeypatch, metadata_state6_gxy): + monkeypatch.setattr(migrations, "get_gxy_metadata", lambda: metadata_state6_gxy) + + +@pytest.fixture(autouse=True) # always override tsi_metadata +def set_tsi_metadata(monkeypatch, metadata_state6_tsi): + monkeypatch.setattr(migrations, "get_tsi_metadata", lambda: metadata_state6_tsi) + + +class AlembicManagerForTests(AlembicManager): + def __init__(self, engine): + path1, path2 = _get_paths_to_version_locations() + config_dict = {"version_locations": f"{path1};{path2}"} + super().__init__(engine, config_dict) + + +def _get_paths_to_version_locations(): + # One does not simply use a relative path for both tests and package tests. + basepath = os.path.abspath(os.path.dirname(__file__)) + basepath = os.path.join(basepath, "versions") + path1 = os.path.join(basepath, "db1") + path2 = os.path.join(basepath, "db2") + return path1, path2 + + +def load_sqlalchemymigrate_version(db_url, version): + with disposing_engine(db_url) as engine: + with engine.connect() as conn: + sql_delete = text(f"delete from {SQLALCHEMYMIGRATE_TABLE}") # there can be only 1 row + sql_insert = text(f"insert into {SQLALCHEMYMIGRATE_TABLE} values('_', '_', {version})") + conn.execute(sql_delete) + conn.execute(sql_insert) + + +def test_load_sqlalchemymigrate_version(url_factory, metadata_state2_gxy): # noqa F811 + db_url = url_factory() + with create_and_drop_database(db_url): + with disposing_engine(db_url) as engine: + load_metadata(metadata_state2_gxy, engine) + sql = text(f"select version from {SQLALCHEMYMIGRATE_TABLE}") + version = 42 + with engine.connect() as conn: + result = conn.execute(sql).scalar() + assert result != version + load_sqlalchemymigrate_version(db_url, version) + result = conn.execute(sql).scalar() + assert result == version + + +def test_get_last_sqlalchemymigrate_version(): + assert get_last_sqlalchemymigrate_version(GXY) == SQLALCHEMYMIGRATE_LAST_VERSION_GXY + assert get_last_sqlalchemymigrate_version(TSI) == SQLALCHEMYMIGRATE_LAST_VERSION_TSI + + +def database_is_empty(db_url): + with disposing_engine(db_url) as engine: + with engine.connect() as conn: + metadata = MetaData() + metadata.reflect(bind=conn) + return not bool(metadata.tables) + + +def test_database_is_empty(url_factory, metadata_state1_gxy): # noqa F811 + db_url = url_factory() + with create_and_drop_database(db_url): + with disposing_engine(db_url) as engine: + assert database_is_empty(db_url) + load_metadata(metadata_state1_gxy, engine) + assert not database_is_empty(db_url) + + +def database_is_up_to_date(db_url, current_state_metadata, model): + # True if the database at `db_url` has the `current_state_metadata` loaded, + # and is up-to-date with respect to `model` (has the most recent Alembic revision). + + # NOTE: Ideally, we'd determine the current metadata based on the model. However, since + # metadata is a fixture, it cannot be called directly, and instead has to be + # passed as an argument. That's why we ensure that the passed metadata is current + # (this guards againt an incorrect test). + if model == GXY: + current_tables = {"gxy_table1", "gxy_table2", "gxy_table3"} + elif model == TSI: + current_tables = {"tsi_table1", "tsi_table2", "tsi_table3"} + is_metadata_current = current_tables <= set(current_state_metadata.tables) + + with disposing_engine(db_url) as engine: + is_loaded = is_metadata_loaded(db_url, current_state_metadata) + am = AlembicManagerForTests(engine) + return is_metadata_current and is_loaded and am.is_up_to_date(model) + + +def test_database_is_up_to_date(url_factory, metadata_state6_gxy): # noqa F811 + db_url, metadata = url_factory(), metadata_state6_gxy + with create_and_drop_database(db_url): + with disposing_engine(db_url) as engine: + assert not database_is_up_to_date(db_url, metadata, GXY) + load_metadata(metadata, engine) + am = AlembicManagerForTests(engine) + am.stamp_revision("heads") + assert database_is_up_to_date(db_url, metadata, GXY) + + +def test_database_is_up_to_date_for_passed_model_only(url_factory, metadata_state6_gxy): # noqa F811 + db_url, metadata = url_factory(), metadata_state6_gxy + with create_and_drop_database(db_url): + with disposing_engine(db_url) as engine: + assert not database_is_up_to_date(db_url, metadata, GXY) + assert not database_is_up_to_date(db_url, metadata, TSI) + load_metadata(metadata, engine) + am = AlembicManagerForTests(engine) + am.stamp_revision("heads") + assert database_is_up_to_date(db_url, metadata, GXY) + assert not database_is_up_to_date(db_url, metadata, TSI) + + +def test_database_is_not_up_to_date_if_noncurrent_metadata_passed(url_factory, metadata_state5_gxy): # noqa F811 + db_url, metadata = url_factory(), metadata_state5_gxy + with create_and_drop_database(db_url): + with disposing_engine(db_url) as engine: + load_metadata(metadata, engine) + am = AlembicManagerForTests(engine) + am.stamp_revision("heads") + assert not database_is_up_to_date(db_url, metadata, GXY) + + +def test_database_is_not_up_to_date_if_metadata_not_loaded(url_factory, metadata_state6_gxy): # noqa F811 + db_url, metadata = url_factory(), metadata_state6_gxy + with create_and_drop_database(db_url): + with disposing_engine(db_url) as engine: + am = AlembicManagerForTests(engine) + am.stamp_revision("heads") + assert not database_is_up_to_date(db_url, metadata, GXY) + + +def test_database_is_not_up_to_date_if_alembic_not_added(url_factory, metadata_state6_gxy): # noqa F811 + db_url, metadata = url_factory(), metadata_state6_gxy + with create_and_drop_database(db_url): + with disposing_engine(db_url) as engine: + load_metadata(metadata, engine) + assert not database_is_up_to_date(db_url, metadata, GXY) + + +def is_metadata_loaded(db_url, metadata): + # True if the set of tables from the up-to-date state metadata (state6) + # is a subset of the metadata reflected from `db_url`. + with disposing_engine(db_url) as engine: + with engine.connect() as conn: + db_metadata = MetaData() + db_metadata.reflect(bind=conn) + tables = _get_tablenames(metadata) + return set(tables) <= set(db_metadata.tables) + + +def _get_tablenames(metadata): + metadata = listify(metadata) + tables = set() + for md in metadata: + tables |= set(md.tables) + return tables + + +def test_is_metadata_loaded(url_factory, metadata_state1_gxy): # noqa F811 + db_url, metadata = url_factory(), metadata_state1_gxy + with create_and_drop_database(db_url): + assert not is_metadata_loaded(db_url, metadata) + with disposing_engine(db_url) as engine: + with engine.connect() as conn: + metadata.create_all(bind=conn) + assert is_metadata_loaded(db_url, metadata) + + +def test_is_multiple_metadata_loaded(url_factory, metadata_state1_gxy, metadata_state1_tsi): # noqa F811 + db_url = url_factory() + metadata = [metadata_state1_gxy, metadata_state1_tsi] + with create_and_drop_database(db_url): + assert not is_metadata_loaded(db_url, metadata) + with disposing_engine(db_url) as engine: + with engine.connect() as conn: + metadata_state1_gxy.create_all(bind=conn) + metadata_state1_tsi.create_all(bind=conn) + assert is_metadata_loaded(db_url, metadata) + + +def test_load_metadata(url_factory, metadata_state1_gxy): # noqa F811 + db_url, metadata = url_factory(), metadata_state1_gxy + with create_and_drop_database(db_url): + with disposing_engine(db_url) as engine: + assert not is_metadata_loaded(db_url, metadata) + load_metadata(metadata, engine) + assert is_metadata_loaded(db_url, metadata) + + +# Fixtures: databases loaded with a given state. +# +# Each state has 3 versions: gxy, tsi, combined (see fixtures/schemas.py) +# +# Each fixture is constructed as follows: +# 1. Create a new sqlite database url. +# 2. Pass database url with state metadata fixture to a `_setup_db_url_state{state#}` function. +# 3. Inside the function, create database and load any state-specific data. + + +# state 1 +@pytest.fixture +def db_state1_gxy(url_factory, metadata_state1_gxy): # noqa F811 + yield from _setup_db_state1(url_factory(), metadata_state1_gxy) + + +@pytest.fixture +def db_state1_tsi(url_factory, metadata_state1_tsi): # noqa F811 + yield from _setup_db_state1(url_factory(), metadata_state1_tsi) + + +@pytest.fixture +def db_state1_combined(url_factory, metadata_state1_combined): # noqa F811 + yield from _setup_db_state1(url_factory(), metadata_state1_combined) + + +def _setup_db_state1(db_url, metadata): + with create_and_drop_database(db_url): + with disposing_engine(db_url) as engine: + load_metadata(metadata, engine) + yield db_url + + +# state 2 +@pytest.fixture +def db_state2_gxy(url_factory, metadata_state2_gxy): # noqa F811 + yield from _setup_db_state2(url_factory(), metadata_state2_gxy) + + +@pytest.fixture +def db_state2_tsi(url_factory, metadata_state2_tsi): # noqa F811 + yield from _setup_db_state2(url_factory(), metadata_state2_tsi) + + +@pytest.fixture +def db_state2_combined(url_factory, metadata_state2_combined): # noqa F811 + yield from _setup_db_state2(url_factory(), metadata_state2_combined) + + +def _setup_db_state2(db_url, metadata): + with create_and_drop_database(db_url): + with disposing_engine(db_url) as engine: + load_metadata(metadata, engine) + yield db_url + + +# state 3 +@pytest.fixture +def db_state3_gxy(url_factory, metadata_state3_gxy): # noqa F811 + yield from _setup_db_state3(url_factory(), metadata_state3_gxy, SQLALCHEMYMIGRATE_LAST_VERSION_GXY) + + +@pytest.fixture +def db_state3_tsi(url_factory, metadata_state3_tsi): # noqa F811 + yield from _setup_db_state3(url_factory(), metadata_state3_tsi, SQLALCHEMYMIGRATE_LAST_VERSION_TSI) + + +@pytest.fixture +def db_state3_combined(url_factory, metadata_state3_combined): # noqa F811 + # the SQLAlchemy Migrate version in a combined database is GXY (we only stored the TSI version + # if the database was separate). + yield from _setup_db_state3(url_factory(), metadata_state3_combined, SQLALCHEMYMIGRATE_LAST_VERSION_GXY) + + +def _setup_db_state3(db_url, metadata, last_version): + with create_and_drop_database(db_url): + with disposing_engine(db_url) as engine: + load_metadata(metadata, engine) + load_sqlalchemymigrate_version(db_url, last_version) + yield db_url + + +# state 4 +@pytest.fixture +def db_state4_gxy(url_factory, metadata_state4_gxy): # noqa F811 + yield from _setup_db_state4(url_factory(), metadata_state4_gxy, SQLALCHEMYMIGRATE_LAST_VERSION_GXY, GXY) + + +@pytest.fixture +def db_state4_tsi(url_factory, metadata_state4_tsi): # noqa F811 + yield from _setup_db_state4(url_factory(), metadata_state4_tsi, SQLALCHEMYMIGRATE_LAST_VERSION_TSI, TSI) + + +@pytest.fixture +def db_state4_combined(url_factory, metadata_state4_combined): # noqa F811 + # the SQLAlchemy Migrate version in a combined database is GXY (we only stored the TSI version + # if the database was separate). + yield from _setup_db_state4(url_factory(), metadata_state4_combined, SQLALCHEMYMIGRATE_LAST_VERSION_GXY) + + +def _setup_db_state4(db_url, metadata, last_version, model=None): + with create_and_drop_database(db_url): + with disposing_engine(db_url) as engine: + load_metadata(metadata, engine) + load_sqlalchemymigrate_version(db_url, last_version) + + revisions: Union[str, list] + if model == GXY: + revisions = GXY_REVISION_0 + elif model == TSI: + revisions = TSI_REVISION_0 + else: + revisions = [GXY_REVISION_0, TSI_REVISION_0] + am = AlembicManagerForTests(engine) + am.stamp_revision(revisions) + + yield db_url + + +# state 5 +@pytest.fixture +def db_state5_gxy(url_factory, metadata_state5_gxy): # noqa F811 + yield from _setup_db_state5(url_factory(), metadata_state5_gxy, GXY) + + +@pytest.fixture +def db_state5_tsi(url_factory, metadata_state5_tsi): # noqa F811 + yield from _setup_db_state5(url_factory(), metadata_state5_tsi, TSI) + + +@pytest.fixture +def db_state5_combined(url_factory, metadata_state5_combined): # noqa F811 + yield from _setup_db_state5(url_factory(), metadata_state5_combined) + + +def _setup_db_state5(db_url, metadata, model=None): + with create_and_drop_database(db_url): + with disposing_engine(db_url) as engine: + load_metadata(metadata, engine) + + revisions: Union[str, list] + if model == GXY: + revisions = GXY_REVISION_1 + elif model == TSI: + revisions = TSI_REVISION_1 + else: + revisions = [GXY_REVISION_1, TSI_REVISION_1] + am = AlembicManagerForTests(engine) + am.stamp_revision(revisions) + + yield db_url + + +# state 6 +@pytest.fixture +def db_state6_gxy(url_factory, metadata_state6_gxy): # noqa F811 + yield from _setup_db_state6(url_factory(), metadata_state6_gxy, GXY) + + +@pytest.fixture +def db_state6_tsi(url_factory, metadata_state6_tsi): # noqa F811 + yield from _setup_db_state6(url_factory(), metadata_state6_tsi, TSI) + + +@pytest.fixture +def db_state6_combined(url_factory, metadata_state6_combined): # noqa F811 + yield from _setup_db_state6(url_factory(), metadata_state6_combined) + + +def _setup_db_state6(db_url, metadata, model=None): + with create_and_drop_database(db_url): + with disposing_engine(db_url) as engine: + load_metadata(metadata, engine) + + revisions: Union[str, list] + if model == GXY: + revisions = GXY_REVISION_2 + elif model == TSI: + revisions = TSI_REVISION_2 + else: + revisions = [GXY_REVISION_2, TSI_REVISION_2] + am = AlembicManagerForTests(engine) + am.stamp_revision(revisions) + + yield db_url + + +# state 6+3 +@pytest.fixture +def db_state6_gxy_state3_tsi_no_sam(url_factory, metadata_state6_gxy_state3_tsi_no_sam): # noqa F811 + db_url = url_factory() + metadata = metadata_state6_gxy_state3_tsi_no_sam + with create_and_drop_database(db_url): + with disposing_engine(db_url) as engine: + load_metadata(metadata, engine) + am = AlembicManagerForTests(engine) + am.stamp_revision(GXY_REVISION_2) + yield db_url + + +# Test LegacyManageDb (used by script/manage_db.py) + + +@pytest.fixture(autouse=True) +def legacy_manage_db(monkeypatch): + def get_alembic_cfg(self): + path = os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, os.pardir, os.pardir, os.pardir) + path = os.path.normpath(path) + # Adjust path when running from packages + if os.path.split(path)[1] == "packages": + path = os.path.join(path, os.pardir) + path = os.path.join(path, "lib", "galaxy", "model", "migrations", "alembic.ini") + + config = Config(path) + path1, path2 = _get_paths_to_version_locations() + config.set_main_option("version_locations", f"{path1};{path2}") + return config + + monkeypatch.setattr(LegacyManageDb, "_get_alembic_cfg", get_alembic_cfg) + + +@pytest.fixture(autouse=True) +def set_db_urls(monkeypatch): + # Do not try to access galaxy config; values not needed. + def no_config_call(self): + self.gxy_db_url = "a string" + self.tsi_db_url = "a stirng" + + monkeypatch.setattr(LegacyManageDb, "_set_db_urls", no_config_call) + + +@pytest.fixture(autouse=True) # always override AlembicManager +def set_alembic_manager2(monkeypatch): + monkeypatch.setattr(scripts, "get_alembic_manager", lambda engine: AlembicManagerForTests(engine)) + + +class TestLegacyManageDbScript: + def test_get_gxy_version(self): + mdb = LegacyManageDb() + version = mdb.get_gxy_version() + assert version == GXY_REVISION_2 + + class TestState2: + # Initial state: non-empty database, SQLAlchemy Migrate version table present; however, + # the stored version is not the latest after which we could transition to Alembic. + def test_get_gxy_db_version__state2__gxy_database(self, db_state2_gxy): + # Expect: fail + db_url = db_state2_gxy + with pytest.raises(IncorrectVersionError): + mdb = LegacyManageDb() + mdb.get_gxy_db_version(db_url) + + def test_get_gxy_db_version__state2__combined_database(self, db_state2_combined): + # Expect: fail + db_url = db_state2_combined + with pytest.raises(IncorrectVersionError): + mdb = LegacyManageDb() + mdb.get_gxy_db_version(db_url) + + class TestState3: + # Initial state: non-empty database, SQLAlchemy Migrate version table contains latest version + # under SQLAlchemy Migrate. + def test_get_gxy_db_version___state3__gxy_database(self, db_state3_gxy): + # Expect: return SQLALCHEMYMIGRATE_LAST_VERSION_GXY + db_url = db_state3_gxy + mdb = LegacyManageDb() + version = mdb.get_gxy_db_version(db_url) + assert version == SQLALCHEMYMIGRATE_LAST_VERSION_GXY + + def test_get_gxy_db_version___state3__combined_database(self, db_state3_combined): + # Expect: return SQLALCHEMYMIGRATE_LAST_VERSION_GXY + db_url = db_state3_combined + mdb = LegacyManageDb() + version = mdb.get_gxy_db_version(db_url) + assert version == SQLALCHEMYMIGRATE_LAST_VERSION_GXY + + def test_upgrade__state3__combined_database(self, db_state3_combined, metadata_state6_combined): + # Expect: upgrade to current version + db_url = db_state3_combined + mdb = LegacyManageDb() + mdb.run_upgrade(db_url, db_url) + assert database_is_up_to_date(db_url, metadata_state6_combined, GXY) + assert database_is_up_to_date(db_url, metadata_state6_combined, TSI) + + def test_upgrade__state3__separate_databases( + self, db_state3_gxy, db_state3_tsi, metadata_state6_gxy, metadata_state6_tsi + ): + # Expect: upgrade to current version + db1_url, db2_url = db_state3_gxy, db_state3_tsi + mdb = LegacyManageDb() + mdb.run_upgrade(db1_url, db2_url) + assert database_is_up_to_date(db1_url, metadata_state6_gxy, GXY) + assert database_is_up_to_date(db2_url, metadata_state6_tsi, TSI) + + class TestState4: + # Initial state: non-empty database, SQLAlchemy Migrate version table present, Alembic version table present. + # Oldest Alembic revision. + # Expect: return GXY_REVISION_0 + def test_get_gxy_db_version___state4__gxy_database(self, db_state4_gxy): + db_url = db_state4_gxy + mdb = LegacyManageDb() + version = mdb.get_gxy_db_version(db_url) + assert version == GXY_REVISION_0 + + def test_get_gxy_db_version___state4__combined_database(self, db_state4_combined): + db_url = db_state4_combined + mdb = LegacyManageDb() + version = mdb.get_gxy_db_version(db_url) + assert version == GXY_REVISION_0 + + def test_upgrade__state4__combined_database(self, db_state4_combined, metadata_state6_combined): + # Expect: upgrade to current version + db_url = db_state4_combined + mdb = LegacyManageDb() + mdb.run_upgrade(db_url, db_url) + assert database_is_up_to_date(db_url, metadata_state6_combined, GXY) + assert database_is_up_to_date(db_url, metadata_state6_combined, TSI) + + def test_upgrade__state4__separate_databases( + self, db_state4_gxy, db_state4_tsi, metadata_state6_gxy, metadata_state6_tsi + ): + # Expect: upgrade to current version + db1_url, db2_url = db_state4_gxy, db_state4_tsi + mdb = LegacyManageDb() + mdb.run_upgrade(db1_url, db2_url) + assert database_is_up_to_date(db1_url, metadata_state6_gxy, GXY) + assert database_is_up_to_date(db2_url, metadata_state6_tsi, TSI) diff --git a/test/unit/data/model/migrations/test_scripts.py b/test/unit/data/model/migrations/test_scripts.py new file mode 100644 index 00000000000..216bc6a5efe --- /dev/null +++ b/test/unit/data/model/migrations/test_scripts.py @@ -0,0 +1,147 @@ +import pytest + +from galaxy.model.migrations.scripts import ( + LegacyScripts, + LegacyScriptsException, +) + + +@pytest.fixture(autouse=True) +def set_db_urls(monkeypatch): + # Do not try to access galaxy config; values not needed. + def no_config_call(self): + self.gxy_url = "a string" + self.tsi_url = "a stirng" + + monkeypatch.setattr(LegacyScripts, "load_db_urls", no_config_call) + + +@pytest.fixture(autouse=True) # set combined db for all tests +def set_combined(monkeypatch): + monkeypatch.setattr(LegacyScripts, "_is_one_database", lambda self: True) + + +@pytest.fixture +def set_separate(monkeypatch): + monkeypatch.setattr(LegacyScripts, "_is_one_database", lambda self: False) + + +class TestLegacyScripts: + @pytest.mark.parametrize("database_arg", ["galaxy", "install"]) + def test_pop_database_name(self, database_arg): + # arg_value = 'install' + argv = ["caller", "--alembic-config", "path-to-alembic", "upgrade", "--version=abc", database_arg] + ls = LegacyScripts(argv) + + ls.pop_database_argument() + assert ls.database == database_arg + assert argv == ["caller", "--alembic-config", "path-to-alembic", "upgrade", "--version=abc"] + + def test_pop_database_name_use_default(self): + argv = ["caller", "--alembic-config", "path-to-alembic", "upgrade", "--version=abc"] + ls = LegacyScripts(argv) + ls.pop_database_argument() + assert ls.database == LegacyScripts.DEFAULT_DB_ARG + assert argv == ["caller", "--alembic-config", "path-to-alembic", "upgrade", "--version=abc"] + + @pytest.mark.parametrize("arg_name", LegacyScripts.LEGACY_CONFIG_FILE_ARG_NAMES) + def test_rename_config_arg(self, arg_name): + # `-c|--config|__config-file` should be renamed to `--galaxy-config` + argv = ["caller", "--alembic-config", "path-to-alembic", arg_name, "path-to-galaxy", "upgrade", "--version=abc"] + LegacyScripts(argv).rename_config_argument() + assert argv == [ + "caller", + "--alembic-config", + "path-to-alembic", + "--galaxy-config", + "path-to-galaxy", + "upgrade", + "--version=abc", + ] + + def test_rename_config_arg_reordered_args(self): + # `-c|--config|__config-file` should be renamed to `--galaxy-config` + argv = ["caller", "--alembic-config", "path-to-alembic", "upgrade", "--version=abc", "-c", "path-to-galaxy"] + LegacyScripts(argv).rename_config_argument() + assert argv == [ + "caller", + "--alembic-config", + "path-to-alembic", + "upgrade", + "--version=abc", + "--galaxy-config", + "path-to-galaxy", + ] + + def test_rename_alembic_config_arg(self): + # `--alembic-config` should be renamed to `-c` + argv = ["caller", "--alembic-config", "path-to-alembic", "upgrade", "--version=abc"] + LegacyScripts(argv).rename_alembic_config_argument() + assert argv == ["caller", "-c", "path-to-alembic", "upgrade", "--version=abc"] + + def test_rename_alembic_config_arg_raises_error_if_c_arg_present(self): + # Ensure alembic config arg is renamed AFTER renaming the galaxy config arg. Raise error otherwise. + argv = ["caller", "--alembic-config", "path-to-alembic", "-c", "path-to-galaxy", "upgrade", "--version=abc"] + with pytest.raises(LegacyScriptsException): + LegacyScripts(argv).rename_alembic_config_argument() + + def test_convert__version_arg_1(self): + # `sh manage_db.sh upgrade --version X` >> `... upgrade X` + argv = ["caller", "--alembic-config", "path-to-alembic", "upgrade", "--version", "abc"] + LegacyScripts(argv).convert_args() + assert argv == ["caller", "-c", "path-to-alembic", "upgrade", "abc"] + + def test_convert__version_arg_2(self): + # `sh manage_db.sh upgrade --version=X` >> `... upgrade X` + argv = ["caller", "--alembic-config", "path-to-alembic", "upgrade", "--version=abc"] + LegacyScripts(argv).convert_args() + assert argv == ["caller", "-c", "path-to-alembic", "upgrade", "abc"] + + def test_convert__no_version_no_model_combined_database(self): + # `sh manage_db.sh upgrade` >> `... upgrade heads` + # No version and no model implies "upgrade the default db (which is galaxy) to its latest version". + # If it is combined, we upgrade both models: gxy and tsi. + argv = ["caller", "--alembic-config", "path-to-alembic", "upgrade"] + LegacyScripts(argv).convert_args() + assert argv == ["caller", "-c", "path-to-alembic", "upgrade", "heads"] + + def test_convert__no_version_galaxy_model_combined_database_(self): + # `sh manage_db.sh upgrade galaxy` >> `... upgrade heads` + # same as no model: if combined we upgrade the whole database + argv = ["caller", "--alembic-config", "path-to-alembic", "upgrade", "galaxy"] + LegacyScripts(argv).convert_args() + assert argv == ["caller", "-c", "path-to-alembic", "upgrade", "heads"] + + def test_convert__no_version_install_model_combined_database_(self): + # `sh manage_db.sh upgrade install` >> `... upgrade heads` + # same as no model: if combined we upgrade the whole database + argv = ["caller", "--alembic-config", "path-to-alembic", "upgrade", "install"] + LegacyScripts(argv).convert_args() + assert argv == ["caller", "-c", "path-to-alembic", "upgrade", "heads"] + + def test_convert__no_version_no_model_separate_databases(self, set_separate): + # `sh manage_db.sh upgrade` >> `... upgrade gxy@head` + # No version and no model implies "upgrade the default db (which is galaxy) to its latest version". + # Since the tsi model has its own db, we only upgrade the gxy model. + argv = ["caller", "--alembic-config", "path-to-alembic", "upgrade"] + LegacyScripts(argv).convert_args() + assert argv == ["caller", "-c", "path-to-alembic", "upgrade", "gxy@head"] + + def test_convert__no_version_galaxy_model_separate_databases(self, set_separate): + # `sh manage_db.sh upgrade galaxy` >> `... upgrade gxy@head` + # No version + a model implies "upgrade the db for the specified model to its latest version". + argv = ["caller", "--alembic-config", "path-to-alembic", "upgrade", "galaxy"] + LegacyScripts(argv).convert_args() + assert argv == ["caller", "-c", "path-to-alembic", "upgrade", "gxy@head"] + + def test_convert__no_version_install_model_separate_databases(self, set_separate): + # `sh manage_db.sh upgrade install` >> `... upgrade tsi@head` + # No version + a model implies "upgrade the db for the specified model to its latest version". + argv = ["caller", "--alembic-config", "path-to-alembic", "upgrade", "install"] + LegacyScripts(argv).convert_args() + assert argv == ["caller", "-c", "path-to-alembic", "upgrade", "tsi@head"] + + def test_downgrade_with_no_version_argument_raises_error(self): + argv = ["caller", "--alembic-config", "path-to-alembic", "downgrade"] + with pytest.raises(LegacyScriptsException): + LegacyScripts(argv).convert_args() diff --git a/test/unit/data/model/migrations/versions/db1/2e8a580bc79a_drop_sqlachemymigrate_table.py b/test/unit/data/model/migrations/versions/db1/2e8a580bc79a_drop_sqlachemymigrate_table.py new file mode 100644 index 00000000000..d80b518976b --- /dev/null +++ b/test/unit/data/model/migrations/versions/db1/2e8a580bc79a_drop_sqlachemymigrate_table.py @@ -0,0 +1,34 @@ +"""drop sqlachemymigrate table + +Revision ID: 2e8a580bc79a +Revises: 62695fac6cc0 +Create Date: 2021-11-05 16:29:19.123118 + +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "2e8a580bc79a" +down_revision = "62695fac6cc0" +branch_labels = None +depends_on = None + + +def upgrade(): + # This table exists in both schemas: gxy and tsi. With a combined database, + # this migration will be applied twice to the same database, so we ignore + # the error that happens on the second run when the table has been dropped. + try: + op.drop_table("migrate_version", must_exist=True) + except sa.exc.InvalidRequestError: + pass + + +def downgrade(): + op.create_table( + "migrate_version", + sa.Column("repository_id", sa.String(250), primary_key=True), + sa.Column("repository_path", sa.Text), + sa.Column("version", sa.Integer), + ) diff --git a/test/unit/data/model/migrations/versions/db1/62695fac6cc0_create_gxy_test_branch.py b/test/unit/data/model/migrations/versions/db1/62695fac6cc0_create_gxy_test_branch.py new file mode 100644 index 00000000000..afc3204a309 --- /dev/null +++ b/test/unit/data/model/migrations/versions/db1/62695fac6cc0_create_gxy_test_branch.py @@ -0,0 +1,22 @@ +"""create gxy test branch + +Revision ID: 62695fac6cc0 +Revises: +Create Date: 2021-11-05 16:28:30.497050 + +""" + + +# revision identifiers, used by Alembic. +revision = "62695fac6cc0" +down_revision = None +branch_labels = ("gxy",) +depends_on = None + + +def upgrade(): + pass + + +def downgrade(): + pass diff --git a/test/unit/data/model/migrations/versions/db1/e02cef55763c_add_gxy_table3.py b/test/unit/data/model/migrations/versions/db1/e02cef55763c_add_gxy_table3.py new file mode 100644 index 00000000000..37f9a596f95 --- /dev/null +++ b/test/unit/data/model/migrations/versions/db1/e02cef55763c_add_gxy_table3.py @@ -0,0 +1,26 @@ +"""add foo3 table + +Revision ID: e02cef55763c +Revises: 2e8a580bc79a +Create Date: 2021-11-05 16:30:30.521436 + +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "e02cef55763c" +down_revision = "2e8a580bc79a" +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + "gxy_table3", + sa.Column("id", sa.Integer, primary_key=True), + ) + + +def downgrade(): + op.drop_table("gxy_table3") diff --git a/test/unit/data/model/migrations/versions/db2/0e28bf2fb7b5_add_tsi_table3.py b/test/unit/data/model/migrations/versions/db2/0e28bf2fb7b5_add_tsi_table3.py new file mode 100644 index 00000000000..d692c9f4937 --- /dev/null +++ b/test/unit/data/model/migrations/versions/db2/0e28bf2fb7b5_add_tsi_table3.py @@ -0,0 +1,26 @@ +"""add bar3 table + +Revision ID: 0e28bf2fb7b5 +Revises: 8364ef1cab05 +Create Date: 2021-11-05 16:31:00.530235 + +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "0e28bf2fb7b5" +down_revision = "8364ef1cab05" +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + "tsi_table3", + sa.Column("id", sa.Integer, primary_key=True), + ) + + +def downgrade(): + op.drop_table("tsi_table3") diff --git a/test/unit/data/model/migrations/versions/db2/1bceec30363a_create_tsi_test_branch.py b/test/unit/data/model/migrations/versions/db2/1bceec30363a_create_tsi_test_branch.py new file mode 100644 index 00000000000..7209e389f8e --- /dev/null +++ b/test/unit/data/model/migrations/versions/db2/1bceec30363a_create_tsi_test_branch.py @@ -0,0 +1,22 @@ +"""create tsi test branch + +Revision ID: 1bceec30363a +Revises: +Create Date: 2021-11-05 16:28:45.450830 + +""" + + +# revision identifiers, used by Alembic. +revision = "1bceec30363a" +down_revision = None +branch_labels = ("tsi",) +depends_on = None + + +def upgrade(): + pass + + +def downgrade(): + pass diff --git a/test/unit/data/model/migrations/versions/db2/8364ef1cab05_drop_sqlachemymigrate_table.py b/test/unit/data/model/migrations/versions/db2/8364ef1cab05_drop_sqlachemymigrate_table.py new file mode 100644 index 00000000000..ac4c9a98879 --- /dev/null +++ b/test/unit/data/model/migrations/versions/db2/8364ef1cab05_drop_sqlachemymigrate_table.py @@ -0,0 +1,34 @@ +"""drop sqlachemymigrate table + +Revision ID: 8364ef1cab05 +Revises: 1bceec30363a +Create Date: 2021-11-05 16:30:51.369967 + +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "8364ef1cab05" +down_revision = "1bceec30363a" +branch_labels = None +depends_on = None + + +def upgrade(): + # This table exists in both schemas: gxy and tsi. With a combined database, + # this migration will be applied twice to the same database, so we ignore + # the error that happens on the second run when the table has been dropped. + try: + op.drop_table("migrate_version", must_exist=True) + except sa.exc.InvalidRequestError: + pass + + +def downgrade(): + op.create_table( + "migrate_version", + sa.Column("repository_id", sa.String(250), primary_key=True), + sa.Column("repository_path", sa.Text), + sa.Column("version", sa.Integer), + ) diff --git a/test/unit/data/model/test_model_discovery.py b/test/unit/data/model/test_model_discovery.py index d59fc9b1c55..db831f10baa 100644 --- a/test/unit/data/model/test_model_discovery.py +++ b/test/unit/data/model/test_model_discovery.py @@ -22,6 +22,7 @@ def test_model_create_context_persist_hdas(): "dbkey": "hg19", "name": "my file", "md5": "e5d21b1ea57fc9a31f8ea0110531bf3d", + "tags": ["name:value"], } ], } @@ -39,6 +40,9 @@ def test_model_create_context_persist_hdas(): assert imported_hda.metadata.data_lines == 2 assert len(imported_hda.dataset.hashes) == 1 assert imported_hda.dataset.hashes[0].hash_value == "e5d21b1ea57fc9a31f8ea0110531bf3d" + tags = imported_hda.tags + assert len(tags) == 1 + assert tags[0].value == "value" with open(imported_hda.file_name) as f: assert f.read().startswith("hello world\n") @@ -244,7 +248,7 @@ def _import_directory_to_history(app, target, work_directory): import_options = store.ImportOptions(allow_dataset_object_edit=True) import_model_store = store.get_import_model_store_for_directory( - target, app=app, user=u, import_options=import_options + target, app=app, user=u, import_options=import_options, tag_handler=app.tag_handler.create_tag_handler_session() ) with import_model_store.target_history(default_history=import_history): import_model_store.perform_import(import_history) diff --git a/test/unit/data/model/test_model_store.py b/test/unit/data/model/test_model_store.py index 0605e4b93fb..220ca73b7f0 100644 --- a/test/unit/data/model/test_model_store.py +++ b/test/unit/data/model/test_model_store.py @@ -44,6 +44,30 @@ def test_import_export_history_failed_job(): _assert_simple_cat_job_imported(imported_history, state="error") +def test_import_export_history_hidden_false_with_hidden_dataset(): + app = _mock_app() + + u, h, d1, d2, j = _setup_simple_cat_job(app) + d2.visible = False + app.model.session.flush() + + imported_history = _import_export_history(app, h, export_files="copy", include_hidden=False) + assert d1.dataset.get_size() == imported_history.datasets[0].get_size() + assert imported_history.datasets[1].get_size() == 0 + + +def test_import_export_history_hidden_true_with_hidden_dataset(): + app = _mock_app() + + u, h, d1, d2, j = _setup_simple_cat_job(app) + d2.visible = False + app.model.session.flush() + + imported_history = _import_export_history(app, h, export_files="copy", include_hidden=True) + assert d1.dataset.get_size() == imported_history.datasets[0].get_size() + assert d2.dataset.get_size() == imported_history.datasets[1].get_size() + + def test_import_export_bag_archive(): """Test a simple job import/export using a BagIt archive.""" dest_parent = mkdtemp() @@ -488,13 +512,13 @@ def _setup_simple_cat_job(app, state="ok"): return u, h, d1, d2, j -def _import_export_history(app, h, dest_export=None, export_files=None): +def _import_export_history(app, h, dest_export=None, export_files=None, include_hidden=False): if dest_export is None: dest_parent = mkdtemp() dest_export = os.path.join(dest_parent, "moo.tgz") with store.TarModelExportStore(dest_export, app=app, export_files=export_files) as export_store: - export_store.export_history(h) + export_store.export_history(h, include_hidden=include_hidden) imported_history = import_archive(dest_export, app, h.user) assert imported_history diff --git a/test/unit/data/test_galaxy_mapping.py b/test/unit/data/test_galaxy_mapping.py index 4a16c2dcafb..bd948b8c2a5 100644 --- a/test/unit/data/test_galaxy_mapping.py +++ b/test/unit/data/test_galaxy_mapping.py @@ -18,6 +18,10 @@ import galaxy.model.mapping as mapping from galaxy import model from galaxy.model.database_utils import create_database from galaxy.model.metadata import MetadataTempFile +from galaxy.model.orm.util import ( + add_object_to_object_session, + get_object_session, +) from galaxy.model.security import GalaxyRBACAgent datatypes_registry = galaxy.datatypes.registry.Registry() @@ -82,6 +86,7 @@ class MappingTests(BaseModelTestCase): annotated_association.annotation = "Test Annotation" annotated_association.user = u for key, value in kwds.items(): + add_object_to_object_session(annotated_association, value) setattr(annotated_association, key, value) self.persist(annotated_association) self.expunge() @@ -90,6 +95,7 @@ class MappingTests(BaseModelTestCase): assert stored_annotation.user.email == "annotator@example.com" sw = model.StoredWorkflow() + add_object_to_object_session(sw, u) sw.user = u self.persist(sw) persist_and_check_annotation(model.StoredWorkflowAnnotationAssociation, stored_workflow=sw) @@ -99,6 +105,7 @@ class MappingTests(BaseModelTestCase): self.persist(workflow) ws = model.WorkflowStep() + add_object_to_object_session(ws, workflow) ws.workflow = workflow self.persist(ws) persist_and_check_annotation(model.WorkflowStepAnnotationAssociation, workflow_step=ws) @@ -152,6 +159,7 @@ class MappingTests(BaseModelTestCase): assert stored_rating.user.email == user_email sw = model.StoredWorkflow() + add_object_to_object_session(sw, u) sw.user = u self.persist(sw) persist_and_check_rating(model.StoredWorkflowRatingAssociation, sw) @@ -433,9 +441,9 @@ class MappingTests(BaseModelTestCase): q = c2._get_nested_collection_attributes( element_attributes=("element_identifier",), hda_attributes=("extension",), dataset_attributes=("state",) ) - assert [(r.keys()) for r in q] == [ - ["element_identifier_0", "element_identifier_1", "extension", "state"], - ["element_identifier_0", "element_identifier_1", "extension", "state"], + assert [(r._fields) for r in q] == [ + ("element_identifier_0", "element_identifier_1", "extension", "state"), + ("element_identifier_0", "element_identifier_1", "extension", "state"), ] assert q.all() == [("inner_list", "forward", "bam", "new"), ("inner_list", "reverse", "txt", "new")] q = c2._get_nested_collection_attributes(return_entities=(model.HistoryDatasetAssociation,)) @@ -801,8 +809,16 @@ class MappingTests(BaseModelTestCase): def workflow_from_steps(steps): stored_workflow = model.StoredWorkflow() + add_object_to_object_session(stored_workflow, user) stored_workflow.user = user workflow = model.Workflow() + + if steps: + for step in steps: + if get_object_session(step): + add_object_to_object_session(workflow, step) + break + workflow.steps = steps workflow.stored_workflow = stored_workflow return workflow @@ -816,6 +832,7 @@ class MappingTests(BaseModelTestCase): workflow_step_2 = model.WorkflowStep() workflow_step_2.order_index = 1 workflow_step_2.type = "subworkflow" + add_object_to_object_session(workflow_step_2, child_workflow) workflow_step_2.subworkflow = child_workflow workflow_step_1.get_or_add_input("moo1") @@ -830,6 +847,7 @@ class MappingTests(BaseModelTestCase): annotation = model.WorkflowStepAnnotationAssociation() annotation.annotation = "Test Step Annotation" annotation.user = user + add_object_to_object_session(annotation, workflow_step_1) annotation.workflow_step = workflow_step_1 self.persist(annotation) @@ -840,9 +858,11 @@ class MappingTests(BaseModelTestCase): workflow_invocation = model.WorkflowInvocation() workflow_invocation.uuid = invocation_uuid + add_object_to_object_session(workflow_invocation, h1) workflow_invocation.history = h1 workflow_invocation_step1 = model.WorkflowInvocationStep() + add_object_to_object_session(workflow_invocation_step1, workflow_invocation) workflow_invocation_step1.workflow_invocation = workflow_invocation workflow_invocation_step1.workflow_step = workflow_step_1 @@ -850,6 +870,7 @@ class MappingTests(BaseModelTestCase): workflow_invocation.attach_subworkflow_invocation_for_step(workflow_step_2, subworkflow_invocation) workflow_invocation_step2 = model.WorkflowInvocationStep() + add_object_to_object_session(workflow_invocation_step2, workflow_invocation) workflow_invocation_step2.workflow_invocation = workflow_invocation workflow_invocation_step2.workflow_step = workflow_step_2 @@ -857,6 +878,7 @@ class MappingTests(BaseModelTestCase): d1 = self.new_hda(h1, name="1") workflow_request_dataset = model.WorkflowRequestToInputDatasetAssociation() + add_object_to_object_session(workflow_request_dataset, workflow_invocation) workflow_request_dataset.workflow_invocation = workflow_invocation workflow_request_dataset.workflow_step = workflow_step_1 workflow_request_dataset.dataset = d1 diff --git a/test/unit/data/test_metadata_limit.py b/test/unit/data/test_metadata_limit.py new file mode 100644 index 00000000000..e0c2121a2bc --- /dev/null +++ b/test/unit/data/test_metadata_limit.py @@ -0,0 +1,44 @@ +import pytest + +import galaxy.datatypes.registry as registry +import galaxy.model.mapping as mapping +from galaxy.model import ( + custom_types, + HistoryDatasetAssociation, + set_datatypes_registry, +) + +METADATA_LIMIT = 500 + + +@pytest.fixture(scope="module") +def datatypes_registry(): + r = registry.Registry() + r.load_datatypes() + set_datatypes_registry(r) + + +@pytest.fixture +def sa_session(datatypes_registry): + custom_types.MAX_METADATA_VALUE_SIZE = METADATA_LIMIT + return mapping.init("/tmp", "sqlite:///:memory:", create_tables=True).session + + +def create_bed_data(sa_session, string_size): + hda = HistoryDatasetAssociation(extension="bed") + big_string = "0" * string_size + sa_session.add(hda) + hda.metadata.column_names = [big_string] + assert hda.metadata.column_names + sa_session.flush() + return hda + + +def test_hda_below_limit(sa_session): + hda = create_bed_data(sa_session=sa_session, string_size=1) + assert len(hda.metadata.column_names[0]) == 1 + + +def test_hda_above_limit(sa_session): + hda = create_bed_data(sa_session=sa_session, string_size=1000) + assert not hda.metadata.column_names diff --git a/test/unit/data/test_mutable_json_column.py b/test/unit/data/test_mutable_json_column.py index 95a36ec7934..81788af4bda 100644 --- a/test/unit/data/test_mutable_json_column.py +++ b/test/unit/data/test_mutable_json_column.py @@ -9,64 +9,64 @@ class MutableColumnTest(BaseModelTestCase): item_id = item.id self.model.session.flush() self.model.session.expunge_all() - return self.model.session.query(model.Workflow).get(item_id) + return self.model.session.query(model.DynamicTool).get(item_id) def test_metadata_mutable_column(self): - w = model.Workflow() + w = model.DynamicTool() self.model.session.add(w) self.model.session.flush() - w.reports_config = {"x": "z"} + w.value = {"x": "z"} persisted = self.persist_and_reload(w) - assert persisted.reports_config == {"x": "z"} - persisted.reports_config["x"] = "1" + assert persisted.value == {"x": "z"} + persisted.value["x"] = "1" persisted = self.persist_and_reload(persisted) - assert persisted.reports_config["x"] == "1" + assert persisted.value["x"] == "1" # test string - persisted.reports_config = "abcdefg" + persisted.value = "abcdefg" persisted = self.persist_and_reload(persisted) - assert persisted.reports_config == "abcdefg" + assert persisted.value == "abcdefg" # test int - persisted.reports_config = 1 + persisted.value = 1 persisted = self.persist_and_reload(persisted) - assert persisted.reports_config == 1 + assert persisted.value == 1 # test float - persisted.reports_config = 1.1 + persisted.value = 1.1 persisted = self.persist_and_reload(persisted) - assert persisted.reports_config == 1.1 + assert persisted.value == 1.1 # test bool - persisted.reports_config = True + persisted.value = True persisted = self.persist_and_reload(persisted) - assert persisted.reports_config is True + assert persisted.value is True # Test nested dict/list - persisted.reports_config = {"list": [[1, 2, 3]]} + persisted.value = {"list": [[1, 2, 3]]} persisted = self.persist_and_reload(persisted) - assert persisted.reports_config == {"list": [[1, 2, 3]]} - copy.deepcopy(persisted.reports_config) - assert persisted.reports_config.pop("list") == [[1, 2, 3]] + assert persisted.value == {"list": [[1, 2, 3]]} + copy.deepcopy(persisted.value) + assert persisted.value.pop("list") == [[1, 2, 3]] persisted = self.persist_and_reload(persisted) - assert persisted.reports_config == {} - persisted.reports_config.update({"x": "z"}) + assert persisted.value == {} + persisted.value.update({"x": "z"}) persisted = self.persist_and_reload(persisted) - assert persisted.reports_config == {"x": "z"} - del persisted.reports_config["x"] + assert persisted.value == {"x": "z"} + del persisted.value["x"] persisted = self.persist_and_reload(persisted) - assert persisted.reports_config == {} - persisted.reports_config = {"x": {"y": "z"}} + assert persisted.value == {} + persisted.value = {"x": {"y": "z"}} persisted = self.persist_and_reload(persisted) - assert persisted.reports_config == {"x": {"y": "z"}} + assert persisted.value == {"x": {"y": "z"}} # These tests are failing ... at least since 20.09, # but nested mutable change tracking might have # never worked - # persisted.reports_config['x']['y'] = 'x' + # persisted.value['x']['y'] = 'x' # persisted = self.persist_and_reload(persisted) - # assert persisted.reports_config == {'x': {'y': 'x'}} - # persisted.reports_config[0].append(2) + # assert persisted.value == {'x': {'y': 'x'}} + # persisted.value[0].append(2) # persisted = self.persist_and_reload(persisted) - # assert persisted.reports_config[0] == [1, 2] - # persisted.reports_config[0].extend([3, 4]) + # assert persisted.value[0] == [1, 2] + # persisted.value[0].extend([3, 4]) # persisted = self.persist_and_reload(persisted) - # assert persisted.reports_config[0] == [1, 2, 3, 4] - # persisted.reports_config[0].remove(4) + # assert persisted.value[0] == [1, 2, 3, 4] + # persisted.value[0].remove(4) # persisted = self.persist_and_reload(persisted) - # assert persisted.reports_config[0] == [1, 2, 3] + # assert persisted.value[0] == [1, 2, 3] diff --git a/test/unit/tool_util/mulled/test_mulled_search.py b/test/unit/tool_util/mulled/test_mulled_search.py index 5bc52c43331..451a2af9bfa 100644 --- a/test/unit/tool_util/mulled/test_mulled_search.py +++ b/test/unit/tool_util/mulled/test_mulled_search.py @@ -1,11 +1,11 @@ import pytest from galaxy.tool_util.deps.mulled.mulled_search import ( + conda_path, CondaSearch, get_package_hash, GitHubSearch, QuaySearch, - run_command, singularity_search, ) from ..util import external_dependency_management @@ -22,24 +22,31 @@ def test_quay_search(): @external_dependency_management -@pytest.mark.skipif(run_command is None, reason="requires import from conda library") +@pytest.mark.skipif(not conda_path, reason="requires conda on path") def test_conda_search(): t = CondaSearch("bioconda") search1 = t.get_json("asdfasdf") search2 = t.get_json("bioconductor-gosemsim") assert search1 == [] - assert search2["version"] == "2.2.0" - assert search2["package"] == "bioconductor-gosemsim" - assert search2["build"] == "0" + assert all(r["package"] == "bioconductor-gosemsim" for r in search2) @external_dependency_management -def test_github_search(): +def test_github_recipe_present(): t = GitHubSearch() - search1 = t.process_json(t.get_json("adsfasdf"), "adsfasdf") - search2 = t.process_json(t.get_json("bioconductor-gosemsim"), "bioconductor-gosemsim") - assert search1 == [] - assert {"path": "recipes/bioconductor-gosemsim/meta.yaml", "name": "meta.yaml"} in search2 + + search_string2expected = { + "adsfasdf": False, + "bioconductor-gosemsim": True, + } + for search_string, expected in search_string2expected.items(): + try: + is_recipe_present = t.recipe_present(search_string) + except Exception as e: + if "API rate limit" in str(e): + pytest.skip("Hitting GitHub API rate limit") + raise + assert is_recipe_present is expected @external_dependency_management diff --git a/test/unit/tool_util/test_ontologies.py b/test/unit/tool_util/test_ontologies.py new file mode 100644 index 00000000000..3991d358c3d --- /dev/null +++ b/test/unit/tool_util/test_ontologies.py @@ -0,0 +1,68 @@ +from galaxy.tool_util.ontologies.ontology_data import expand_ontology_data +from .test_parsing import ( + get_test_tool_source, + TOOL_YAML_1, +) + +TOOL_YAML_2 = """ +name: "Bowtie Mapper" +class: GalaxyTool +id: bowtie +version: 1.0.2 +description: "The Bowtie Mapper" +xrefs: + - type: bio.tools + value: bwa +command: "bowtie --map-the-stuff" +outputs: + out1: + format: bam + from_work_dir: out1.bam +edam_operations: + - operation_0335 +edam_topics: + - topic_0102 +inputs: + - name: input1 + type: integer +""" + +TOOL_YAML_3 = """ +name: "Bowtie Mapper" +class: GalaxyTool +id: sort1 +version: 1.0.2 +description: "The Bowtie Mapper" +xrefs: + - type: bio.tools + value: bwa +command: "bowtie --map-the-stuff" +outputs: + out1: + format: bam + from_work_dir: out1.bam +inputs: + - name: input1 + type: integer +""" + + +def test_parse_edam_empty(): + test_source = get_test_tool_source(source_file_name="testtool.yml", source_contents=TOOL_YAML_1) + ontology_data = expand_ontology_data(test_source, ["bowtie"], None) + assert ontology_data.edam_operations == [] + assert ontology_data.edam_topics == [] + + +def test_parse_edam_direct(): + test_source = get_test_tool_source(source_file_name="testtool.yml", source_contents=TOOL_YAML_2) + ontology_data = expand_ontology_data(test_source, ["bowtie"], None) + assert ontology_data.edam_operations == ["operation_0335"] + assert ontology_data.edam_topics == ["topic_0102"] + + +def test_parse_edam_mapping_operations_legacy(): + test_source = get_test_tool_source(source_file_name="testtool.yml", source_contents=TOOL_YAML_3) + ontology_data = expand_ontology_data(test_source, ["sort1"], None) + assert ontology_data.edam_operations == ["operation_3802"] + assert ontology_data.edam_topics == [] diff --git a/test/unit/tool_util/test_parsing.py b/test/unit/tool_util/test_parsing.py index 09a76d7d6e0..6d3339b23ea 100644 --- a/test/unit/tool_util/test_parsing.py +++ b/test/unit/tool_util/test_parsing.py @@ -193,6 +193,26 @@ outputs: """ +def get_test_tool_source(source_file_name=None, source_contents=None, macro_contents=None, temp_directory=None): + source_directory = temp_directory or tempfile.mkdtemp() + macro_paths = [] + if not os.path.isabs(source_file_name): + path = os.path.join(source_directory, source_file_name) + with open(path, "w") as out: + out.write(source_contents) + if macro_contents: + macro_path = os.path.join(source_directory, "macros.xml") + with open(macro_path, "w") as out: + out.write(macro_contents) + macro_paths = [macro_path] + else: + path = source_file_name + tool_source = get_tool_source(path, macro_paths=macro_paths) + if temp_directory is None: + shutil.rmtree(source_directory) + return tool_source + + class BaseLoaderTestCase(unittest.TestCase): source_file_name: Optional[str] = None source_contents: Optional[str] = None @@ -208,24 +228,16 @@ class BaseLoaderTestCase(unittest.TestCase): return self._get_tool_source() def _get_tool_source(self, source_file_name=None, source_contents=None, macro_contents=None): - macro_path = None if source_file_name is None: source_file_name = self.source_file_name if source_contents is None: source_contents = self.source_contents - if not os.path.isabs(source_file_name): - path = os.path.join(self.temp_directory, source_file_name) - with open(path, "w") as out: - out.write(source_contents) - if macro_contents: - macro_path = os.path.join(self.temp_directory, "macros.xml") - with open(macro_path, "w") as out: - out.write(macro_contents) - - else: - path = source_file_name - tool_source = get_tool_source(path, macro_paths=[macro_path]) - return tool_source + return get_test_tool_source( + source_file_name, + source_contents, + macro_contents, + self.temp_directory, + ) class XmlExpressionLoaderTestCase(BaseLoaderTestCase): diff --git a/test/unit/tool_util/test_tool_linters.py b/test/unit/tool_util/test_tool_linters.py index 10b4130250d..cb0d8844679 100644 --- a/test/unit/tool_util/test_tool_linters.py +++ b/test/unit/tool_util/test_tool_linters.py @@ -421,8 +421,8 @@ OUTPUTS_UNKNOWN_TAG = """ OUTPUTS_UNNAMED_INVALID_NAME = """ - - + + """ @@ -457,6 +457,14 @@ OUTPUTS_DISCOVER_TOOL_PROVIDED_METADATA = """ """ +OUTPUTS_DUPLICATED_NAME_LABEL = """ + + + + + + +""" # tool xml for repeats linter REPEATS = """ @@ -1179,6 +1187,18 @@ def test_outputs_discover_tool_provided_metadata(lint_ctx): assert not lint_ctx.error_messages +def test_outputs_duplicated_name_label(lint_ctx): + tool_source = get_xml_tool_source(OUTPUTS_DUPLICATED_NAME_LABEL) + run_lint(lint_ctx, outputs.lint_output, tool_source) + assert "2 outputs found." in lint_ctx.info_messages + assert len(lint_ctx.info_messages) == 1 + assert not lint_ctx.valid_messages + assert not lint_ctx.warn_messages + assert "Tool output [valid_name] has duplicated name" in lint_ctx.error_messages + assert "Tool output [valid_name] uses duplicated label '${tool.name} on ${on_string}'" in lint_ctx.error_messages + assert len(lint_ctx.error_messages) == 2 + + def test_stdio_default_for_default_profile(lint_ctx): tool_source = get_xml_tool_source(STDIO_DEFAULT_FOR_DEFAULT_PROFILE) run_lint(lint_ctx, stdio.lint_stdio, XmlToolSource(tool_source)) @@ -1372,7 +1392,7 @@ COMPLETE_MACROS = """ """ -def test_tool_and_macro_xml(lint_ctx_xpath): +def test_tool_and_macro_xml(lint_ctx_xpath, lint_ctx): """ test linters (all of them via lint_tool_source_with) on a tool and macro xml file checking a list of asserts, where each assert is a 4-tuple: @@ -1391,21 +1411,31 @@ def test_tool_and_macro_xml(lint_ctx_xpath): tool_xml, _ = load_with_references(tool_path) tool_source = XmlToolSource(tool_xml) + # lint once with the lint context using XMLLintMessageXPath and XMLLintMessageLine lint_tool_source_with(lint_ctx_xpath, tool_source) + lint_tool_source_with(lint_ctx, tool_source) asserts = ( - ("Select parameter [select] has multiple options with the same value", "tool.xml", 5, "/tool/inputs/param[1]"), - ("Found param input with no name specified.", "tool.xml", 13, "/tool/inputs/param[2]"), - ("Param input [No_type] input with no type specified.", "macros.xml", 3, "/tool/inputs/param[3]"), + ("Select parameter [select] has multiple options with the same value", 5, "/tool/inputs/param[1]"), + ("Found param input with no name specified.", 13, "/tool/inputs/param[2]"), + ("Param input [No_type] input with no type specified.", 3, "/tool/inputs/param[3]"), ) for a in asserts: - message, fname, line, xpath = a + message, line, xpath = a found = False for lint_message in lint_ctx_xpath.message_list: if lint_message.message != message: continue found = True - assert lint_message.xpath == xpath, f"Assumed xpath {xpath} xpath {lint_message.xpath} for: {message}" + assert ( + lint_message.xpath == xpath + ), f"Assumed xpath {xpath}; found xpath {lint_message.xpath} for: {message}" + assert found, f"Did not find {message}" + for lint_message in lint_ctx.message_list: + if lint_message.message != message: + continue + found = True + assert lint_message.line == line, f"Assumed line {line}; found line {lint_message.line} for: {message}" assert found, f"Did not find {message}" diff --git a/test/unit/tool_util/test_tool_loader.py b/test/unit/tool_util/test_tool_loader.py index 4bc997ae064..b506ca98610 100644 --- a/test/unit/tool_util/test_tool_loader.py +++ b/test/unit/tool_util/test_tool_loader.py @@ -708,12 +708,13 @@ def test_loader_specify_nested_macro_by_token(): # test with re because loading from external macros # adds a xml:base property (containing the source path) # to the node which is printed + print(f"{xml_to_string(xml, pretty=True)}") assert re.match( r"""<\?xml version="1\.0" \?> - - + + """, xml_to_string(xml, pretty=True), re.MULTILINE, diff --git a/test/unit/util/test_fix_type_error.py b/test/unit/util/test_fix_type_error.py deleted file mode 100644 index 9ffdf03024f..00000000000 --- a/test/unit/util/test_fix_type_error.py +++ /dev/null @@ -1,20 +0,0 @@ -from galaxy.util.pastescript.loadwsgi import fix_type_error - -argtype = "" -EXP_STRING = ( - "a_func() got multiple values for%s argument 'kwarg'; got (1, 2, kwarg=...), wanted (argone, kwarg=True)" % argtype -) - - -def a_func(argone, kwarg=True): - pass - - -def test_fix_type_error(): - args = [1, 2] - kwargs = {"kwarg": False} - try: - a_func(*args, **kwargs) - except TypeError: - exc_info = fix_type_error(None, a_func, args, kwargs) - assert exc_info[1].args[0] == EXP_STRING diff --git a/test/unit/web_stack/test_application_stack.py b/test/unit/web_stack/test_application_stack.py new file mode 100644 index 00000000000..2e950dc8a84 --- /dev/null +++ b/test/unit/web_stack/test_application_stack.py @@ -0,0 +1,38 @@ +from gunicorn import SERVER_SOFTWARE + +from galaxy.model.unittest_utils import GalaxyDataTestApp +from galaxy.tool_util.verify.wait import wait_on +from galaxy.web_stack import ( + application_stack_class, + application_stack_instance, + GunicornApplicationStack, + WeblessApplicationStack, +) + + +def test_application_stack_class_factory(monkeypatch): + assert application_stack_class() == WeblessApplicationStack + monkeypatch.setenv("SERVER_SOFTWARE", SERVER_SOFTWARE) + assert application_stack_class() == GunicornApplicationStack + + +def test_application_stack_instance_factory(monkeypatch): + app = GalaxyDataTestApp() + monkeypatch.setenv("SERVER_SOFTWARE", SERVER_SOFTWARE) + stack_instance = application_stack_instance(app=app, config=app.config) + assert isinstance(stack_instance, GunicornApplicationStack) + assert stack_instance.name == "Gunicorn" + + +def test_application_stack_post_fork(monkeypatch): + app = GalaxyDataTestApp() + monkeypatch.setenv("SERVER_SOFTWARE", SERVER_SOFTWARE) + monkeypatch.setenv("GUNICORN_WORKER_ID", "100") + monkeypatch.setattr(GunicornApplicationStack, "do_post_fork", True) + stack_instance = application_stack_instance(app=app, config=app.config) + assert isinstance(stack_instance, GunicornApplicationStack) + stack_instance.register_postfork_function(lambda: stack_instance.set_postfork_server_name(app)) + assert not app.config.server_name.endswith(".100") + stack_instance.late_postfork() + stack_instance.late_postfork_event.set() + wait_on(lambda: app.config.server_name.endswith(".100") or None, desc="server name to change", timeout=1, delta=0.1) diff --git a/test/unit/webapps/api/test_configuration.py b/test/unit/webapps/api/test_configuration.py index a653cd4d670..8dd2dd6f741 100644 --- a/test/unit/webapps/api/test_configuration.py +++ b/test/unit/webapps/api/test_configuration.py @@ -1,4 +1,4 @@ -from galaxy.webapps.galaxy.api.configuration import parse_serialization_params +from galaxy.webapps.galaxy.api.common import parse_serialization_params def test_parse_serialization_params(): diff --git a/test/unit/webapps/test_request_scoped_sqlalchemy_sessions.py b/test/unit/webapps/test_request_scoped_sqlalchemy_sessions.py index d7eb3ad7b90..836662467b5 100644 --- a/test/unit/webapps/test_request_scoped_sqlalchemy_sessions.py +++ b/test/unit/webapps/test_request_scoped_sqlalchemy_sessions.py @@ -12,7 +12,7 @@ from httpx import AsyncClient from starlette_context import context as request_context from galaxy.app_unittest_utils.galaxy_mock import MockApp -from galaxy.webapps.galaxy.fast_app import add_request_id_middleware +from galaxy.webapps.base.api import add_request_id_middleware app = FastAPI() GX_APP = None diff --git a/test/unit/webapps/test_send_file.py b/test/unit/webapps/test_send_file.py index 45e92093251..6d1d0a04a25 100644 --- a/test/unit/webapps/test_send_file.py +++ b/test/unit/webapps/test_send_file.py @@ -1,8 +1,8 @@ import tempfile import pytest +from a2wsgi import WSGIMiddleware from fastapi.applications import FastAPI -from fastapi.middleware.wsgi import WSGIMiddleware from fastapi.testclient import TestClient from galaxy.util.bunch import Bunch diff --git a/test/unit/workflows/test_workflow_markdown.py b/test/unit/workflows/test_workflow_markdown.py index 74ce3576aea..11486076070 100644 --- a/test/unit/workflows/test_workflow_markdown.py +++ b/test/unit/workflows/test_workflow_markdown.py @@ -1,8 +1,6 @@ from galaxy import model -from galaxy.managers.markdown_util import ( - resolve_invocation_markdown, - validate_galaxy_markdown, -) +from galaxy.managers.markdown_parse import validate_galaxy_markdown +from galaxy.managers.markdown_util import resolve_invocation_markdown from .test_workflow_progress import TEST_WORKFLOW_YAML from .workflow_support import ( MockTrans, diff --git a/tools/filters/sff_extract.py b/tools/filters/sff_extract.py index 81d52ff035d..06eca0f556d 100644 --- a/tools/filters/sff_extract.py +++ b/tools/filters/sff_extract.py @@ -31,10 +31,6 @@ import struct import subprocess import sys import tempfile -from typing import ( - Dict, - List, -) __author__ = "Jose Blanca and Bastien Chevreux" __copyright__ = "Copyright 2008, Jose Blanca, COMAV, and Bastien Chevreux" @@ -46,9 +42,9 @@ __status__ = "beta" fake_sff_name = "fake_sff_name" # readname as key: lines with matches from SSAHA, one best match -ssahapematches: Dict[str, List] = {} +ssahapematches = {} # type: ignore # linker readname as key: length of linker sequence -linkerlengths: Dict[str, int] = {} +linkerlengths = {} # type: ignore # set to true if something really fishy is going on with the sequences stern_warning = True diff --git a/tools/filters/sff_extractor.xml b/tools/filters/sff_extractor.xml index d26b6e5ca2e..236669327dd 100644 --- a/tools/filters/sff_extractor.xml +++ b/tools/filters/sff_extractor.xml @@ -57,5 +57,3 @@ Clippings (XML) - - diff --git a/tools/interactive/interactivetool_ethercalc.xml b/tools/interactive/interactivetool_ethercalc.xml index 3b8f1c2d37d..7ae74caefde 100644 --- a/tools/interactive/interactivetool_ethercalc.xml +++ b/tools/interactive/interactivetool_ethercalc.xml @@ -1,4 +1,4 @@ - + shiltemann/ethercalc-galaxy-ie:17.05 diff --git a/tox.ini b/tox.ini index 0122b93703f..636c8ac7242 100644 --- a/tox.ini +++ b/tox.ini @@ -27,7 +27,6 @@ setenv = unit: GALAXY_VIRTUAL_ENV={envdir} unit: GALAXY_ENABLE_BETA_COMPRESSED_GENBANK_SNIFFING=1 mulled: GALAXY_TEST_INCLUDE_SLOW=1 - check_indexes: GALAXY_TEST_FORCE_DATABASE_MIGRATION=1 check_indexes: GALAXY_SKIP_CLIENT_BUILD=1 deps = lint,lint_docstring,lint_docstring_include_list,mypy: -rlib/galaxy/dependencies/pinned-lint-requirements.txt