diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..34fd55973f --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,14 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "pip" # See documentation for possible values + directory: "/src/python" # Location of package manifests + schedule: + interval: "weekly" + reviewers: + - "kpjensen" + - "wormsik" diff --git a/.gitignore b/.gitignore index 1c85c45bfc..75c6acfad4 100644 --- a/.gitignore +++ b/.gitignore @@ -71,6 +71,7 @@ dxR.Rcheck /bin/proot /bin/pbr /bin/xattr +/bin/normalizer /bin/wsdump.py /bin/dx-su-contrib /src/python/test/*.traceability.*.csv @@ -90,3 +91,5 @@ test-job-workspaces *.traceability.*.csv .vscode/ +.metals/ + diff --git a/.gitmodules b/.gitmodules index 0f1646bf44..e69de29bb2 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +0,0 @@ -[submodule "src/jq"] - path = src/jq - url = https://github.com/stedolan/jq.git - ignore = dirty \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index fe00eb8680..d637afab9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,14 +6,825 @@ Categories for each release: Added, Changed, Deprecated, Removed, Fixed, Securit ## Unreleased -## [311.0] - beta +## [388.0] - beta + +### Added + +* `dx build_asset` supports new Application Execution Environment based on Ubuntu 24.04 +* `dx build --remote` supports new Application Execution Environment based on Ubuntu 24.04 +* Nextflow support added in new region aws:me-south-1 +* `--target-file-relocation` parameter for `dx mv` and `dx cp` + +## [387.0] - 2024.12.16 + +### Added + +* `--name-mode` parameter for `dx find data` + +### Fixed + +* Nonce generation for python 3 + +## [386.0] - 2024.12.2 + +### Added + +* `--database-results-restricted` `--unset-database-results-restricted` for `dx new project` + +### Fixed + +* Remove pipes import for Python 3.13 compatibility + +## [385.0] - 2024.11.8 + +### Added + +* `nvidiaDriver` field +* `--drive` parameter for `dx new project` + +### Fixed + +* Throw an error when runSpec.distribution or runSpec.release is not present in `dx build` + +## [384.0] - 2024.10.21 + +### Fixed + +* `dx get_details` provides project-id + +## [383.1] - 2024.9.26 + +### Fixed + +* `dx describe file-xxxx` does not call /project-xxxx/listFolder + +## [382.0] - 2024.9.6 + +### Added + +* Nextflow: Support for specifying the cpus directive in Docker process definitions +* Nextaur 1.9.2: Improved error messages, mostly related to S3 + +### Fixed + +* `dx upload` always provides project-id in /file-xxxx/describe call + +## [381.0] - 2024.8.26 + +* No significant changes + +## [380.0] - 2024.7.26 + +### Fixed + +* Occasional timeout issue when building Nextflow applets + +## [379.0] - 2024.7.8 + +### Changed + +* Pin numpy < 2.0.0 in dxpy[pandas] extra requirements +* Nextflow: Limit of 20 cached sessions in the project no longer applies when using S3 as workdir +* Nextflow: Include NF workdir in context info block in job log + +## [378.0] - 2024.6.21 + +### Changed + +* Nextflow Pipeline Applet script - refactoring + +### Fixed + +* Nextaur 1.9.1: Fixed remote bin dir; currently works when docker.enabled=true +* Detecting current env for selecting Nextflow assets + +## [377.0] - 2024.6.10 + +### Added + +* Released Nextaur 1.9.0: Enables option to host the Nextflow workdir on one's own S3 bucket, with access managed using job identity tokens. + +### Changed + +* websocket-client dxpy dependency to >=1.6.0,<1.8.0 + +### Removed + +* `dx upgrade` command. `python3 -m pip install -U dxpy` is the recommended installation method + +## [376.0] - 2024.5.20 + +* No significant changes + +## [375.1] - 2024.5.14 + +### Fixed + +* `dx run --instance-type-by-executable` + +## [374.0] - 2024.4.29 + +### Added + +* Released Nextaur 1.8.0: Enables login to AWS +* AWS login for Nextflow subjobs and relogin for Nextflow headjob and subjobs +* `dx extract_assay germline --retrieve-genotype` supports germline assay datasets with reference and no-call genotypes + +## [373.0] - 2024.4.18 + +### Added + +* AWScliv2 asset to Nextflow applets + +## [372.0] - 2024.3.22 + +### Added + +* dx-jobutil-get-identity-token + +### Fixed + +* Released Nextaur 1.7.4: Fixes error when multiple Docker images with the same name and digest are cached. + +### Removed + +* For Nextflow applets, removed allProjects: VIEW access. They will have only UPLOAD access to the current project context. + +## [371.0] - 2024.3.13 + +### Fixed + +* `dx run --ssh` hanging if job has reused outputs + +## [370.2] - 2024.2.26 + +### Added + +* Python 3.12 support +* `--nextflow-pipeline-params` for `dx build --nextflow --cache-docker` +* `dx build --nextflow --cache-docker` supports the `--profile` parameter to cache images associated with specific profile + +### Fixed + +* Released Nextaur 1.7.3 +* Fix for occasional head job hang during upload, fix for "No such file or directory" errors on localized dx files +* Multiple fixes in resolving docker image digests, handling file path collision, and OOM when creating new input stream. + +## [369.1] - 2024.2.13 + +### Added + +* Additional regional resources can be specified in dxworkflow.json or using `--extra-args` when `dx build` global workflows + +## [369.0] - 2024.2.5 + +### Fixed + +* Bugfix regarding multi-assay assay selection in `dx extract_assay expression` + +## [368.1] - 2024.1.16 + +### Changed + +* Require Python >= 3.8 for dxpy +* Relaxed urllib3 dxpy dependency to >=1.25,<2.2 + +### Fixed + +* Released Nextaur 1.7.0. It contains Nextflow update to 23.10.0 and multiple minor fixes. +* Suppressing traceback when `dx extract_assay expression` attempts to access a dataset it does not have access to + +### Removed + +* Python 3.6 and 3.7 for dxpy + +## [367.0] - 2023.12.11 + +### Fixed + +* Improved error messaging for `dx extract_assay expression` +* Python docs for autodoc.dnanexus.com + +## [366.0] - 2023.12.1 + +### Added + +* certifi dxpy dependency + +### Changed + +* urllib3 dxpy dependency to >=1.26.18,<2.2 + +### Removed + +* requests, cryptography as dependencies of dxpy + +## [365.0] - 2023.11.21 + +### Changed + +* Require Python >= 3.6 for dxpy +* Update Nextflow to 23.10.0 (staging only) + +### Removed + +* Python 2.7 support + +## [364.0] - 2023.11.13 + +### Added + +* `dx extract_assay expression` +* Return `jobLogsForwardingStatus` field in `dx describe --verbose job-xxxx` +* Nextflow Docker image can be cached on the platform + +### Changed + +* Disallow Python versions <2.7 or <3.5 in setup.py for dxpy, the next release will only support Python >=3.6 + +### Fixed + +* Released Nextaur 1.6.9. It contains fixes for exception types so they are interpreted correctly by Nextflow when caching task runs. + +## [363.0] - 2023.11.6 + +### Fixed + +* Project context added for loading dataset descriptor file when using `dx extract_dataset` + +### Added + +* Support dataset and CB records with Integer and Float type global primary keys as input in `create_cohort` +* Return `jobLogsForwardingStatus` field in `dx describe --verbose job-xxxx` + +## [362.0] - 2023.10.30 + +### Added + +* archival_state param to `dxpy.bindings.search.find_data_objects()` + +## [361.0] - 2023.10.16 + +### Added + +* `--fields-file` argument in `dx extract_dataset` +* `ALT` and `alt_index` columns in `--additional-fields` of `dx extract_assay somatic` + +### Fixed + +* `dx extract_assay` error message when no valid assay is found +* In `dx extract_assay germline`, handled duplicate RSIDs, output sorting order, changed location filter range from 250M to 5M +* Handled white spaces in `dx extract` command's inputs which are of the type `string` of comma separated values +* Released Nextaur 1.6.8. It contains minor bugfixes and optimizations. +* --retrieve-genotype --sql bug in UKB RAP region +* Retry API call in object_exists_in_project() + +## [360.1] - 2023.10.16 + +### Fixed + +* Released Nextaur 1.6.7. It adds DNAnexus docker image support feature and contains errorStrategy bugfixes + +## [359.1] - 2023.10.10 + +### Changed + +* Retry `ConnectionResetError` in dxpy + +### Fixed + +* Reduce API calls in `DXFile.read()` after download URL is cached + +## [359.0] - 2023.9.29 + +### Added + +* `dx create_cohort` +* Nextflow pipeline readme file is used as a readme file of Nextflow applet +* Default optional inputs `nextflow_soft_confs` and `nextflow_params_file` to Nextflow applets to support soft configuration override and custom parameters file in `nextflow run` + +## [358.0] - 2023.9.22 + +### Changed + +* `dx find org projects --property` in Python 3 + +## [357.0] - 2023.9.15 + +### Changed + +* Remove optional output param `nextflow_log` from Nextflow pipeline applets; instead, always upload Nextflow log file to head job destination when execution completes + +### Fixed + +* Nextflow applets passes schema input params explicitly in `nextflow run` command, as parameters assigned in runtime config are not handled properly by nextflow-io +* Unexpected splitting at whitespaces inside quotes when parsing string-type input parameters of Nextflow applets + +## [356.0] - 2023.9.1 + +### Fixed + +* `dx describe analysis-xxxx --json --verbose` + +## [355.0] - 2023.8.16 + +### Added + +* Return fields in `dx describe {job/analysis}-xxxx` with `--verbose` argument: 'runSystemRequirements', 'runSystemRequirementsByExecutable', 'mergedSystemRequirementsByExecutable', 'runStageSystemRequirements' +* `--monthly-compute-limit` and `--monthly-egress-bytes-limit` for `dx new project` +* `--instance-type-by-executable` for `dx run` and `dx-jobutil-job-new` +* Parameters `system_requirements` and `system_requirements_by_executable` for `DXExecutable.run()` and `DXJob.new()` + +## [354.0] - 2023.8.1 + +### Added + +* `--try T` for `dx watch`, `dx tag/untag`, `dx set_properties/unset_properties` +* `--include-restarted` parameter for `dx find executions/jobs/analyses` +* Restarted job fields in `dx describe job-xxxx` +* `treeTurnaroundTime` fields in `dx get` and `dx describe` + +### Changed + +* dxpy User-Agent header includes Python version + +## [353.1] - 2023.7.24 + +### Added + +* Fields from `dx describe {job/analysis}-xxxx` with `--verbose` argument: 'runSystemRequirements', 'runSystemRequirementsByExecutable', 'mergedSystemRequirementsByExecutable', 'runStageSystemRequirements' +* `dx watch --metrics top` mode + +## [352.1] - 2023.7.12 + +### Added + +* `dx extract_assay somatic` + +### Fixed + +* Log line truncation for strings > 8000 bytes + +## [351.0] - 2023.7.7 + +* No significant changes + +## [350.1] - 2023.6.23 + +### Added + +* `dx watch` support for detailed job metrics (cpu, memory, network, disk io, etc every 60s) +* `--detailed-job-metrics` for `dx run` +* `--detailed-job-metrics-collect-default` for `dx update org` + +## [349.1] - 2023.6.15 + +### Added + +* `dx extract_assay` +* external_upload_restricted param for DXProject + +## [348.0] - 2023.6.9 + +### Added + +* dxpy dependencies test suite + +### Changed + +* Optimizations in Nextflow Pipeline Applet script to make fewer API calls when +concluding a subjob + +## [347.0] - 2023.5.11 + +### Changed + +* Bumped allowed `colorama` version to 0.4.6 +* Allow `requests` version up to 2.28.x + +### Removed + +* Unneeded python `gnureadline` dependency +* Unused `rlcompleter` import which may break alternative readline implementations + +## [346.0] - 2023.4.20 + +### Changed + +* Help message of the `dx make_download_url` command + +### Fixed + +* Released Nextaur 1.6.6. It includes fixes to errorStrategy handling and an update to the way AWS instance types are selected based on resource requirements in Nextflow pipelines (V2 instances are now preferred) +* `ImportError` in test_dxpy.py +* Replaced obsolete built-in `file()` method with `open()` +* Printing HTTP error codes that were hidden for API requests to cloud storage + +## [345.0] - 2023.4.13 + +### Changed + +* Bump allowed cryptography dxpy dependency version to 40.0.x +* Tab completion in interactive executions now works with `libedit` bundled in MacOS and does not require externally installed GNU `readline` +* Released Nextaur 1.6.5. It added a caching mechanism to `DxPath` file and folder resolution, which reduces number of DX API calls made during pipeline execution. It also fixes an occasional hanging of the headjob. + +### Fixed + +* Tab completion in interactive execution of `dx-app-wizard` +* `dx-app-wizard` script on Windows +* Tab completion in interactive executions on Windows + +## [344.0] - 2023.4.2 + +### Changed + +* Released Nextaur 1.6.4. It includes a fix to folder download, minor fixes and default headjob instance update (mem2_ssd1_v2_x4 for AWS, mem2_ssd1_x4 for Azure) +* Nextflow pipeline head job defaults to instance types mem2_ssd1_v2_x4 (AWS), azure:mem2_ssd1_x4 (Azure). No change to Nextflow task job instance types. + +### Fixed + +* Nextflow profiles runtime overriding fix + +### Added + +* Support for file (un)archival in DXJava +* `archivalStatus` field to DXFile describe in DXJava +* `archivalStatus` filtering support to DXSearch in DXJava +* `dx run` support for `--preserve-job-outputs` and `--preserve-job-outputs-folder` inputs +* `dx describe` for jobs and analyses outputs `Preserve Job Outputs Folder` field +* Record the dxpy version used for Nextflow build in applet's metadata and job log + +## [343.0] - 2023.3.24 + +### Changed + +* Released Nextaur 1.6.3. It includes updates to wait times for file upload and closing, and a fix to default Nextflow config path +* Upgraded Nextflow to 22.10.7 +* Nextflow assets from aws:eu-west-2 + +## [342.1] - 2023.3.8 + +### Added + +* Pretty-printing additional fields for Granular Wait Times in `dx describe` for jobs and analyses + +### Changed + +* Released Nextaur 1.6.2. It includes bugfixes and default value of maxTransferAttempts used for file downloads is set to 3 + +### Fixed + +* `dx find jobs` if stopppedRunning not in describe output + +## [341.0] - 2023.3.3 + +### Added + +* `dx ssh` to connect to job's public hostname if job is httpsApp enabled +* '--list-fields', '--list-entities', '--entities' arguments for `dx extract_dataset` + +### Changed + +* Released Nextaur 1.6.1. It includes an optimization of certain API calls and adds `docker pull` retry in Nextflow pipelines +* Increased dxpy HTTP timeout to 15 minutes + +### Fixed + +* Helpstring of '--verbose' arg + +## [340.1] - 2023.2.25 + +### Changed + +* Nextflow - updated default instance types based on destination region + +### Fixed + +* Use project ID for file-xxxx/describe API calls in dxjava DXFile +* Nextflow errorStrategy retry ends in 'failed' state if last retry fails + +## [339.0] - 2023.2.10 + +* No significant changes + +## [338.1] - 2023.1.27 + +### Added + +* Support for Granular Spot wait times in `dx run` using `--max-tree-spot-wait-time` and `--max-job-spot-wait-time` +* Printing of Spot wait times in `dx describe` for jobs and workflows +* Support for private Docker images in Nextflow pipelines on subjob level + +### Fixed + +* Feature switch check for Nextflow pipeline build in an app execution environment +* `dx get database` command reads from the API server with the API proxy interceptor +* Regex global flags in path matching to support Py3.11 +* `dx run --clone` for Nextflow jobs (clear cloned job's properties) +* Do not rewrite ubuntu repo mirror after failed execDepends install + +### Changed + +* Upgraded Nextflow plugin version to 1.5.0 + +## [337.0] - 2023.1.20 + +### Changed + +* Upgraded Nextflow plugin version to 1.4.0 +* Failed Nextflow subjobs with 'terminate' errorStrategy finish in 'failed' state +* Updated Nextflow last error message in case 'ignore' errorStrategy is applied. +* Exposed help messages for `dx build --nextflow` + +## [336.0] - 2023.1.7 + +* No significant changes + +## [335.0] - 2022.12.12 + +### Added + +* Group name for developer options in Nextflow pipeline applet + +### Fixed + +* Printing too many environment values with debug set to true +* Preserving folder structure when publishing Nextflow output files +* Missing required inputs passed to `nextflow run` + +## [334.0] - 2022.12.2 + +### Added + +* `--external-upload-restricted` flag for `dx update project` and `dx find projects` +* Support for `--destination` in `nextflow build --repository` +* `resume` and `preserve_cache` input arguments to Nextflow applets to support Nextflow resume functionality +* Support for error handling with Nextflow's errorStrategy +* `region` argument to `DXProject.new()` + +### Fixed + +* retrieving session config when no parent process exists +* an issue with describing global workflows by adding a resources container as a hint for describing underlying workflows + +## [333.0] - 2022.11.23 + +### Added + +* `nextflow run` command in the log for easier debugging + +### Fixed + +* Overriding config arguments with an empty string for Nextflow pipelines + +### Changed + +* `psutil` version to 5.9.3 which includes wheelfiles for macOS arm64 +* Set ignore reuse in the nextflow applet template +* Set `restartableEntryPoints` to "all" in the nextflow pipeline applet's `runsSpec` + + +## [332.0] - 2022.11.04 + +### Added + +* A warning for `dx build` when app(let)'s name is set both in `--extra-args` and `--destination` + +### Fixed + +* An error when setting app(let)s name in `dx build` (now the name set via `--extra-args` properly overrides the one set via `--destination`) +* `dx build --nextflow --repository` returns json instead of a simple string + +### Changed + +* Help for building Nextflow pipelines is suppressed + +## [331.0] - 2022.10.14 + +### Added + +* Added: `dx find jobs --json` and `dx describe --verbose job-xxxx` with --verbose argument return field internetUsageIPs if the caller is an org admin and the org has jobInternetUsageMonitoring enabled +* Nextflow applets no longer have default arguments and required inputs + +### Fixed + +* `dx describe user-xxxx` will not try to print the name if it is not present in the API response + +## [330.0] - 2022.10.4 + +### Added + +* Initial support for Nextflow +* pyreadline3 dependency for Windows with Python >= 3.5 + +### Fixed + +* Do not check python3 syntax with python2 and vice versa in `dx build` +* `dx build` properly verifies the applet's name given in the `extra-args` parameter + +## [329.0] - 2022.9.23 + +### Added + +* `dx extract_dataset` command +* Optional pandas dependency for dxpy + +### Changed +- `dxpy.find_one_project`, `dxpy.find_one_data_object`, `dxpy.find_one_app` raise `DXError` if `zero_ok` argument is not a `bool` + +## [328.0] - 2022.9.8 + +### Added + +* `--head-job-on-demand` argument for `dx run app(let)-xxxx` +* `--head-job-on-demand` argument for `dx-jobutil-new-job` +* `--on-behalf-of ` argument for `dx new user` + +### Changed + +* dx-toolkit never included in execDepends when building app(lets) with `dx build` + +### Deprecated + +* `--no-dx-toolkit-autodep` option for dx build + +### Fixed + +* Reduce the number of API calls for `dx run applet-xxxx` and `dx run workflow-xxxx` +* `dx upload f1 f2 --visibility hidden` now correctly marks both files as hidden +* `dx upload` retry on all types of SSL errors + +## [327.1] - 2022.8.12 + +### Fixed + +* Parsing ignoreReuse in `dx build` of workflow + +### Changed + +* DXHTTPRequest to pass ssl_context + +## [326.1] - 2022.7.7 + +### Added + +* '--rank' argument for `dx run` + +### Fixed + +* Do not use job's workspace container ID in /applet-xxxx/run for detached jobs + +## [325.1] - 2022.5.25 + +### Fixed + +* `dx describe` of executable with bundledDepends that is not an asset +* Building globalworkflow from existing workflow with `dx build --from` + +## [324.1] - 2022.5.13 + +### Fixed + +* Improvements to symlink downloading reliability by solely using `aria2c` and enhancing options around its use (removes `wget` option for downloading symlinked files, adds the ability to set max tries for aria2c, adds `-c` flag for continuing downloads, removes the `--check-certificate=false` option). +* `dx build` comparison of workflow directory to workflow name +* Set project argument for `dx run --detach` when executed from inside a job + +### Changed + +* Removed `wget` option for downloading symlinked files +* Bump allowed requests dxpy dependency version to 2.27.1 + +### Added + +* New argument `symlink_max_tries` for `dxpy.download_dxfile()` with default value of 15 + +## [323.0] - 2022.4.28 + +### Changed + +* Do not list folder contents to speed up `dx cd` + +## [322.1] - 2022.4.5 + +### Added + +* API wrappers for `dbcluster` + +### Fixed + +* Pin websocket-client to 0.54.0 to fix `dx watch` output to include job output +* Do not install pyreadline on Windows with Python 3.10 + +## [321.0] - 2022.2.23 + +### Fixed + +* KeyError in `dx-app-wizard --json` + +### Changed + +* dxjava dependencies log4j2, jackson-databind + +## [320.0] - 2022.2.1 + +### Fixed + +* Python 3.10 collections imports +* Recursive folder download `dx download -r` of folders with matching prefix + +## [319.2] - 2022.1.21 + +### Fixed + +* Incorrect setting of the `folder` input option when building global workflows +* Remove unused match_hostname urllib3 import + +### Added + +* Support for qualified workflow & applet IDs and paths when using `dx build --from` with an applet/workflow +* Setting properties when building global workflows +* '--allow-ssh' parameter to `dx ssh` +* '--no-firewall-update' parameter to `dx ssh` + +### Changed + +* Detect client IP for SSH access to job instead of `*` + +## [318.0] - 2022.1.6 + +### Fixed + +* Python 3.10 MutableMapping import + +### Added + +* `--no-temp-build-project` for single region app builds. +* `--from` option to `dx build` for building a global workflow from a project-based workflow, including a workflow built using WDL + +## [317.0] - 2021.12.8 + +### Fixed + +* Reduce file-xxxx/describe API load during `dx upload` +* `dx get` uses a region compatible with user's billTo when downloading resources + +### Changed + +* `dx run` warns users if priority is specified as low/normal when using '--watch/ssh/allow-ssh' + +## [316.0] - 2021.11.17 + +### Added + +* Support for dxpy on macOS arm64 +* Path input for `dx list database files` + +### Fixed + +* Python 3 SSH Host key output in `dx describe job-xxxx` + +### Changed + +* dxpy dependencies cryptography, websocket-client, colorama, requests + +## [315.0] - 2021.10.28 + +* No significant changes + +## [314.0] - 2021.08.27 + +### Added + +* Support FIPS enabled Python +* `dx archive` and `dx unarchive` commands + +### Fixed + +* `dx upload` part retry where file would stay in an open state +* `dx run --project/--destination/--folder` now submits analysis to given project or path + +## [313.0] - 2021.08.18 + +### Added + +* '--cost-limit' arg for `dx run` +* '--database-ui-view-only' flag for `dx new project` + +### Fixed + +* `Total price` for `dx describe` prints formatted currency based on `currency` metadata + +## [312.0] - 2021.07.06 + +* No significant changes + +## [311.0] - 2021.05.21 ### Added -* Added `--cost-limit` flag for `dx run` * `DX_WATCH_PORT` env var for supporting `dx watch` in the job execution environment -## [310.0] - 2021.05.12 stable +## [310.0] - 2021.05.12 * No significant changes @@ -239,7 +1050,7 @@ Categories for each release: Added, Changed, Deprecated, Removed, Fixed, Securit * Precise debian package build target -## [290.1] - 2019.11.21 stable +## [290.1] - 2019.11.21 ### Changed @@ -301,7 +1112,7 @@ Categories for each release: Added, Changed, Deprecated, Removed, Fixed, Securit * Only require futures package for python 2.7 * Upgrade build dependencies for pip, setuptools, and wheel -## [284.0] - 2019.06.13 stable +## [284.0] - 2019.06.13 ### Added diff --git a/COPYING b/COPYING index d645695673..162f6fbd0a 100644 --- a/COPYING +++ b/COPYING @@ -187,7 +187,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright 2013 DNAnexus® Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/Readme-osx-10.9.md b/Readme-osx.md similarity index 83% rename from Readme-osx-10.9.md rename to Readme-osx.md index a999c38fee..bd76e81e8e 100644 --- a/Readme-osx-10.9.md +++ b/Readme-osx.md @@ -1,4 +1,4 @@ -Building on OS X 10.9 +Building on OS X 10.9 & 11.4 ===================== **NOTE:** This document is intended for developers who wish to build the dx-toolkit SDK and command-line tools from source. @@ -11,6 +11,7 @@ https://documentation.dnanexus.com/downloads --------------- 1. Install Xcode and the [Command Line Tools for XCode](https://developer.apple.com/downloads/). (Free registration required with Apple) + Make sure you accept the license (either via UI or command line: `sudo xcodebuild -license`). 1. Install [MacPorts](http://www.macports.org/) for your version of OS X: @@ -33,12 +34,16 @@ https://documentation.dnanexus.com/downloads sudo port install py27-pip py27-virtualenv sudo port select --set pip pip27 sudo port select --set virtualenv virtualenv27 + sudo port install gcc11 + sudo port select --set gcc mp-gcc11 ``` 1. Clone the dx-toolkit repo, and build the SDK: ``` cd dx-toolkit export CPATH=/opt/local/include + # add following export on MacOS 11.4 (workaround for Boost lib dependency on icu4c): + export LIBRARY_PATH=${LIBRARY_PATH}:/usr/local/opt/icu4c/lib make ``` @@ -55,4 +60,4 @@ https://documentation.dnanexus.com/downloads ``` CC=clang CXX=clang++ VERSIONER_PERL_VERSION=5.16 make ua - ``` + ``` \ No newline at end of file diff --git a/Readme.md b/Readme.md index 18cdc7923c..97c548e98b 100644 --- a/Readme.md +++ b/Readme.md @@ -1,7 +1,7 @@ DNAnexus Platform SDK ===================== -* **To download pre-built packages for your platform, see https://documentation.dnanexus.com/downloads.** +**To install the `dx` CLI and the Python SDK for your platform run `python3 -m pip install dxpy`** * **Found a bug? See [Reporting Bugs](#reporting-bugs) below.** @@ -14,152 +14,51 @@ documentation. Installing the toolkit from source ---------------------------------- -First, see the section "Build dependencies" below and install the appropriate -dependencies for your platform. +The recommended way to install the Python SDK and `dx` CLI of `dx-toolkit` locally is with `python3 -m pip install -e dx-toolkit/src/python`. +Any changes made within this checkout will be reflected in the pip installed version. -To build the toolkit, simply run `make`. +### Building inside docker +To avoid lengthy installation of dependencies on your platform and simultaneous installations of development versions of `dx-toolkit` on the system, you can build `dx-toolkit` inside a docker container. -Then, initialize your environment by sourcing this file: +1. Start `python:3.9-bullseye` in the interactive mode, mounting the repo you are working on (`/dx-toolkit`): -``` -source dx-toolkit/environment -``` - -You will then be able to use `dx` (the [DNAnexus Command Line -Client](https://documentation.dnanexus.com/getting-started/tutorials/cli-quickstart#quickstart-for-cli)) and other -utilities, and you will be able to use DNAnexus API bindings in the supported -languages. + ``` + # from root folder of dx-toolkit + docker run -v `pwd`:/dx-toolkit -w /dx-toolkit -it --rm --entrypoint=/bin/bash python:3.9-bullseye + ``` +2. From the interactive shell install `dx-toolkit`. + - **A.** Using local checkout: + ``` + python3 -m pip install src/python/ --upgrade + ``` + - **B.** Using remote branch, in this example specified in "@master": + ``` + python3 -m pip install --upgrade 'git+https://github.com/dnanexus/dx-toolkit.git@master#egg=dxpy&subdirectory=src/python' + ``` +3. Log in, install dependencies(if needed) and use the container while developing. To rebuild, just save the work and run the step 2 again. Supported languages ------------------- The Platform SDK contains API language bindings for the following platforms: -* [Python](src/python/Readme.md) (requires Python 2.7) +* [Python](src/python/Readme.md) (requires Python 3.8 or higher) * C++ * [Java](src/java/Readme.md) (requires Java 7 or higher) -* [R](src/R/Readme.md) -Javascript support lives in a separate repo, -[dnanexus/dx-javascript-toolkit.git](https://github.com/dnanexus/dx-javascript-toolkit). - -Build dependencies +Build dependencies for C++ and Java ------------------ -The following packages are required to build the toolkit. You can avoid having -to install them by either downloading a compiled release from -https://documentation.dnanexus.com/downloads, or by building only a portion of the -toolkit that doesn't require them. - **Note:** There is a known incompatibility (in compiling dxcpp) when using GCC 4.7 with Boost 1.49. Please either use the GCC 4.6 series, or Boost 1.50+. -### Ubuntu 16.04 - - sudo apt install make python-setuptools python-pip python-virtualenv python-dev \ - gcc g++ cmake libboost-all-dev libcurl4-openssl-dev zlib1g-dev libbz2-dev flex bison \ - openssl libssl-dev autoconf - -If your locale is not configured properly, you might need to run following commands as well (i.e. when working from Cloud Workstation): - - export LC_ALL="en_US.UTF-8" - export LC_CTYPE="en_US.UTF-8" - sudo dpkg-reconfigure locales - -in the `dpkg-reconfigure` command, select `en_US.UTF-8 locale`. - -### Ubuntu 14.04 - - sudo apt-get install make python-setuptools python-pip python-virtualenv python-dev \ - g++ cmake libboost1.55-all-dev libcurl4-openssl-dev zlib1g-dev libbz2-dev flex bison \ - autoconf curl - -### Ubuntu 12.04 (deprecated) - - sudo apt-get install make python-setuptools python-pip python-dev \ - g++ cmake libboost1.48-all-dev libcurl4-openssl-dev zlib1g-dev libbz2-dev flex bison \ - autoconf curl - sudo pip install --upgrade virtualenv - -### Fedora - - yum install gcc gcc-c++ automake bison flex python python-pip \ - python-virtualenv boost-devel boost-static cmake openssl-devel \ - libcurl-devel bzip2-devel curl - -This package set was tested on **Fedora 20**, which has the following package -versions (abbreviated list): - -* gcc 4.8.2 -* Python 2.7.5 -* python-pip 1.4.1 -* python-virtualenv 1.10.1 -* boost 1.54.0 -* cmake 2.8.12.1 -* openssl 1.0.1e -* libcurl 7.32.0 - -### CentOS/RHEL 5.x/6.x - -Install Python 2.7. Python 2.7 is not available natively on CentOS/RHEL -5 or 6. You can use the script `build/centos_install_python27.sh`, which -installs it into `/usr/local/bin`. (Run the script as root.) - -Install boost 1.48 or higher (at least the `thread` and `regex` -libraries). This version of boost is not available natively on -CentOS/RHEL 5 or 6. You can use the script -`build/centos_install_boost.sh`, which installs it into -`/usr/local/lib`. - -Then: - - yum install cmake libcurl-devel - easy_install-2.7 pip - pip-2.7 install virtualenv - -Notes: - - - Tested on CentOS 5.4 and CentOS 6.2. - -### OS X - -Install the [Command Line Tools for XCode](https://developer.apple.com/downloads/). (Free registration required with Apple) - -Install `pip` and `virtualenv` for Python: - - easy_install-2.7 pip - pip-2.7 install virtualenv - -Install the following packages from source or via [Homebrew](http://mxcl.github.com/homebrew/), [Fink](http://www.finkproject.org/), or [MacPorts](http://www.macports.org/): - -* [CMake](http://www.cmake.org/cmake/resources/software.html) (`sudo port install cmake` or `brew install cmake`) -* Boost >= 1.49 (`sudo port install boost` or `brew install boost`) -* GCC >= 4.6 - * On MacPorts, install and select GCC with: - - ``` - sudo port install gcc47 - sudo port select --set gcc mp-gcc47 - ``` - - * On Homebrew, install and select an up-to-date version of GCC with: - - ``` - brew tap homebrew/versions - brew install gcc47 - export CC=gcc-4.7 - export CXX=g++-4.7 - ``` -* bison >= 2.7, autoconf, automake - * On Homebrew: `brew install bison autoconf automake` - * On MacPorts: `sudo port install bison autoconf automake` +### Ubuntu 22.04 -### Windows -Warning: Not all parts of the SDK are compatible with Windows. Install the following dependencies to build the Upload Agent: + sudo apt install git openjdk-11-jre-headless maven python-is-python3 python3-venv python3-dev libssl-dev libffi-dev \ + flex bison build-essential cmake libboost-all-dev curl libcurl4-openssl-dev -* [MinGW](http://www.mingw.org/), including `mingw32-libz-dev`, `mingw-zip`, and [`mingw-regex`](http://sourceforge.net/projects/mingw/files/Other/UserContributed/regex/mingw-regex-2.5.1/). -* [NSIS](http://nsis.sourceforge.net/) +### Ubuntu 20.04 -To generate the .dll dependencies required for Windows, run `make ua`, then `make pynsist_installer`, it is also possible to pass `DLL_DEPS_FOLDER=C:/folder/path/` as an argument to make + sudo apt install git make openjdk-11-jre-headless maven python-is-python3 python3-venv libssl-dev flex bison libffi-dev libboost-all-dev curl libcurl4-openssl-dev Upload Agent ------------ @@ -168,5 +67,4 @@ See the [Upload Agent Readme](https://github.com/dnanexus/dx-toolkit/blob/master Reporting Bugs -------------- -Please use [GitHub](https://github.com/dnanexus/dx-toolkit/issues) to -report bugs, post suggestions, or send us pull requests. +Please contact support@dnanexus.com for any bug reports or suggestions. diff --git a/build/Jenkinsfile b/build/Jenkinsfile index 1eedd9b525..4616eb68b0 100644 --- a/build/Jenkinsfile +++ b/build/Jenkinsfile @@ -1,8 +1,8 @@ -node{ +node('22.04-agent'){ deleteDir() stage 'Git' dir('dx-toolkit'){ - git url: 'git://github.com/dnanexus/dx-toolkit.git', branch: 'master', poll: false + git url: 'https://github.com/dnanexus/dx-toolkit.git', branch: 'master', poll: false sh "git checkout \"${commit_id}\"" sh 'git describe > commit_hash' env.commit_hash = readFile('commit_hash').trim() @@ -20,87 +20,8 @@ node{ stage 'Build' parallel ( - "source" : { - node('master'){ - deleteDir() - sh """ - commit_hash=\"${env.commit_hash}\" - working_dir=\$(pwd) - mkdir \$commit_hash - docker run -v \$working_dir/\$commit_hash:/\$commit_hash --rm dnanexus/dx-toolkit:16.04 /bin/bash -c \"git clone https://github.com/dnanexus/dx-toolkit.git; cd dx-toolkit; \\ - git checkout \$commit_hash; make toolkit_version git_submodules; rm -rf .git src/jq/.git; cd ..; \\ - tar -czf dx-toolkit-\$commit_hash-source.tar.gz dx-toolkit; \\ - tar -C dx-toolkit/src/R -czf dxR_\$commit_hash.tar.gz dxR; \\ - zip -r dx-toolkit-\$commit_hash-source.zip dx-toolkit; mv /*.{zip,tar.gz} /\$commit_hash/\" - """ - archive "${env.commit_hash}/dx*" - deleteDir() - } - }, - "16.04-amd64" : { - node('master'){ - deleteDir() - sh """ - commit_hash=\"${env.commit_hash}\" - mkdir \$commit_hash - working_dir=\$(pwd) - docker run -v \$working_dir/\$commit_hash:/\$commit_hash --rm dnanexus/dx-toolkit:16.04 /bin/bash -c \"git clone https://github.com/dnanexus/dx-toolkit.git; cd dx-toolkit; \\ - git checkout \$commit_hash; build/package.sh ubuntu-16.04-amd64; \\ - mv dx-toolkit-*.tar.gz /\$commit_hash/\" - """ - archive "${env.commit_hash}/dx-toolkit-*.tar.gz" - deleteDir() - } - }, - "20.04-amd64" : { - node('master'){ - deleteDir() - sh """ - commit_hash=\"${env.commit_hash}\" - mkdir \$commit_hash - working_dir=\$(pwd) - docker run -v \$working_dir/\$commit_hash:/\$commit_hash --rm dnanexus/dx-toolkit:20.04 /bin/bash -c \"git clone https://github.com/dnanexus/dx-toolkit.git; cd dx-toolkit; \\ - git checkout \$commit_hash; build/package.sh ubuntu-20.04-amd64; \\ - mv dx-toolkit-*.tar.gz /\$commit_hash/\" - """ - archive "${env.commit_hash}/dx-toolkit-*.tar.gz" - deleteDir() - } - }, - "centos-amd64" : { - node('master'){ - deleteDir() - sh """ - commit_hash=\"${env.commit_hash}\" - mkdir \$commit_hash - working_dir=\$(pwd) - docker run -v \$working_dir/\$commit_hash:/\$commit_hash --rm dnanexus/dx-toolkit:centos6 \\ - /bin/bash -xc \"git clone https://github.com/dnanexus/dx-toolkit.git; \\ - cd dx-toolkit; git checkout \$commit_hash; build/package.sh centos-amd64; \\ - mv dx-toolkit-*.tar.gz /\$commit_hash/\" - """ - archive "${env.commit_hash}/dx-toolkit-*.tar.gz" - deleteDir() - } - }, - "xenial-deb" : { - node('master'){ - deleteDir() - sh """ - commit_hash=\"${env.commit_hash}\" - working_dir=\$(pwd) - mkdir -p \$working_dir/\$commit_hash/xenial - docker run -v \$working_dir/\$commit_hash/xenial:/\$commit_hash/xenial --rm dnanexus/dx-toolkit:16.04 \\ - /bin/bash -xc \"git clone https://github.com/dnanexus/dx-toolkit.git; \\ - cd dx-toolkit; git checkout \$commit_hash; build/build-dx-toolkit-debs.sh; \\ - mv /*.{changes,deb,dsc,tar.xz} /\$commit_hash/xenial\" - """ - archive "${env.commit_hash}/xenial/dx*" - deleteDir() - } - }, "focal-deb" : { - node('master'){ + node('22.04-agent'){ deleteDir() sh """ commit_hash=\"${env.commit_hash}\" @@ -114,53 +35,5 @@ stage 'Build' archive "${env.commit_hash}/focal/dx*" deleteDir() } - }, - "osx-10.10" : { - node('idna'){ - deleteDir() - sh """ - git clone https://github.com/dnanexus/dx-toolkit.git - commit_hash=\"${env.commit_hash}\" - mkdir \"${env.commit_hash}\" - cd dx-toolkit - - git checkout \$commit_hash - export LC_ALL=en.UTF-8 - - export CPATH=/opt/local/include - export CC=clang - export CXX=clang++ - export CXXFLAGS="-stdlib=libc++ -mmacosx-version-min=10.7" - - # For the Python cryptography package: - export CRYPTOGRAPHY_OSX_NO_LINK_FLAGS=1 - export LDFLAGS="/opt/local/lib/libssl.a /opt/local/lib/libcrypto.a" - export CPPFLAGS="-I/opt/local/include" - - build/package.sh osx - mv dx-toolkit-*-osx.tar.gz ../\$commit_hash/ - """ - archive "${env.commit_hash}/dx-toolkit-*-osx.tar.gz" - deleteDir() - } - }, - "windows" : { - node('windowsprodbuilds') { - echo "Deleting workspace" - deleteDir() - - git credentialsId: '3c90bb4c-14c0-4745-9156-ae2b99668b6b', - url: 'git@github.com:dnanexus/dx-toolkit.git' - - bat "git checkout ${env.commit_hash}" - - bat "make DLL_DEPS_FOLDER=${dll_deps_folder} pynsist_installer" - - bat "mkdir ${env.commit_hash}" - bat "xcopy dx-toolkit-*.exe .\\${env.commit_hash}" - - archive "${env.commit_hash}\\dx-toolkit-*.exe" - deleteDir() - } } ) diff --git a/build/Prebuilt-Readme.md b/build/Prebuilt-Readme.md deleted file mode 100644 index 3dd83406aa..0000000000 --- a/build/Prebuilt-Readme.md +++ /dev/null @@ -1,35 +0,0 @@ -DNAnexus Platform SDK -===================== - -* **Found a bug? See [Reporting Bugs](#reporting-bugs) below.** - -`dx-toolkit` contains the DNAnexus API language bindings and utilities -for interacting with the DNAnexus platform. - -See https://documentation.dnanexus.com/ and http://autodoc.dnanexus.com/ for relevant -documentation. - -Using the toolkit on your system --------------------------------- - -After unpacking the toolkit, initialize your environment by sourcing this file: - -``` -source dx-toolkit/environment -``` - -You will then be able to use `dx` (the [DNAnexus Command Line -Client](https://documentation.dnanexus.com/getting-started/tutorials/cli-quickstart#quickstart-for-cli)) and other -utilities, and you will be able to use DNAnexus API bindings in the supported -languages. - -Installing the toolkit from source ----------------------------------- - -Install any missing software dependencies. View the [Readme on Github](https://github.com/dnanexus/dx-toolkit/blob/master/Readme.md#installing-the-toolkit-from-source) for a list of the dependencies needed for your OS and version. - -Reporting Bugs --------------- - -Please use [GitHub](https://github.com/dnanexus/dx-toolkit/issues) to -report bugs, post suggestions, or send us pull requests. diff --git a/build/build-dx-toolkit-debs.sh b/build/build-dx-toolkit-debs.sh deleted file mode 100755 index 4029829381..0000000000 --- a/build/build-dx-toolkit-debs.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/bin/bash -ex -# -# Copyright (C) 2013-2016 DNAnexus, Inc. -# -# This file is part of dx-toolkit (DNAnexus platform client libraries). -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may not -# use this file except in compliance with the License. You may obtain a copy -# of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -# Resolve symlinks so we can find the package root -SOURCE="${BASH_SOURCE[0]}" -while [ -h "$SOURCE" ] ; do SOURCE="$(readlink "$SOURCE")"; done -root="$( cd -P "$( dirname "$SOURCE" )" && pwd )" - -echo $root - -cd "$root/.." - -# Build dx-toolkit (stable) -git reset --hard -git clean -dxf -debuild --no-lintian --no-tgz-check -us -uc diff --git a/build/centos_install_boost.sh b/build/centos_install_boost.sh deleted file mode 100755 index 81bacbea2d..0000000000 --- a/build/centos_install_boost.sh +++ /dev/null @@ -1,52 +0,0 @@ -#!/bin/bash -ex -# -# Copyright (C) 2013-2016 DNAnexus, Inc. -# -# This file is part of dx-toolkit (DNAnexus platform client libraries). -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may not -# use this file except in compliance with the License. You may obtain a copy -# of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -# Installs boost 1.48 (required for dx C++ executables) into /usr/local. -# -# -# -# Relevant bits go into: -# /usr/local/lib/libboost_filesystem.so.1.48.0 -# /usr/local/lib/libboost_program_options.so.1.48.0 -# /usr/local/lib/libboost_regex.so.1.48.0 -# /usr/local/lib/libboost_system.so.1.48.0 -# /usr/local/lib/libboost_thread.so.1.48.0 - -# Short-circuit sudo when running as root. In a chrooted environment we are -# likely to be running as root already, and sudo may not be present on minimal -# installations. -if [ "$USER" == "root" ]; then - MAYBE_SUDO='' -else - MAYBE_SUDO='sudo' -fi - -$MAYBE_SUDO yum groupinstall -y "Development tools" - -TEMPDIR=$(mktemp -d) - -pushd $TEMPDIR -curl -O http://superb-dca2.dl.sourceforge.net/project/boost/boost/1.48.0/boost_1_48_0.tar.bz2 -tar -xjf boost_1_48_0.tar.bz2 -cd boost_1_48_0 -./bootstrap.sh --with-libraries=filesystem,program_options,regex,system,thread -# --layout=tagged installs libraries with the -mt prefix. -$MAYBE_SUDO ./b2 --layout=tagged install - -popd -# rm -rf $TEMPDIR diff --git a/build/centos_install_python27.sh b/build/centos_install_python27.sh deleted file mode 100755 index a9de4c6e68..0000000000 --- a/build/centos_install_python27.sh +++ /dev/null @@ -1,74 +0,0 @@ -#!/bin/bash -ex -# -# Copyright (C) 2013-2016 DNAnexus, Inc. -# -# This file is part of dx-toolkit (DNAnexus platform client libraries). -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may not -# use this file except in compliance with the License. You may obtain a copy -# of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -# Installs Python 2.7 (required for running dx-toolkit) into /usr/local. -# -# - -# Allow the user to download tarballs without certificate checking -MAYBE_INSECURE='' -while :; do - case $1 in - -k|--insecure) - MAYBE_INSECURE='--insecure' - ;; - *) - break - esac - shift -done - -# Short-circuit sudo when running as root. In a chrooted environment we are -# likely to be running as root already, and sudo may not be present on minimal -# installations. - -if [ "$USER" == "root" ]; then - MAYBE_SUDO='' -else - MAYBE_SUDO='sudo' -fi - -$MAYBE_SUDO yum groupinstall -y "Development tools" -$MAYBE_SUDO yum install -y zlib-devel bzip2-devel openssl-devel ncurses-devel readline - -# Install Python 2.7.9, setuptools, and pip. - -TEMPDIR=$(mktemp -d) - -pushd $TEMPDIR -curl $MAYBE_INSECURE -L -O https://www.python.org/ftp/python/2.7.9/Python-2.7.9.tar.xz -tar -xf Python-2.7.9.tar.xz -cd Python-2.7.9 -./configure --prefix=/usr/local -make -$MAYBE_SUDO make altinstall - -PYTHON=/usr/local/bin/python2.7 - -cd .. - -curl $MAYBE_INSECURE -O https://pypi.python.org/packages/source/s/setuptools/setuptools-1.1.6.tar.gz -tar -xzf setuptools-1.1.6.tar.gz -(cd setuptools-1.1.6; $MAYBE_SUDO $PYTHON setup.py install) - -curl $MAYBE_INSECURE -O https://pypi.python.org/packages/source/p/pip/pip-1.4.1.tar.gz -tar -xzf pip-1.4.1.tar.gz -(cd pip-1.4.1; $MAYBE_SUDO $PYTHON setup.py install) - -popd -# rm -rf $TEMPDIR diff --git a/build/dependencies_cross_platform_tests/.gitignore b/build/dependencies_cross_platform_tests/.gitignore new file mode 100644 index 0000000000..0245fe9697 --- /dev/null +++ b/build/dependencies_cross_platform_tests/.gitignore @@ -0,0 +1,9 @@ +*.swp +*.pyc +*~ +.DS_Store + +__pycache__/ +.pytest_cache/ + +/logs/ diff --git a/build/dependencies_cross_platform_tests/README.md b/build/dependencies_cross_platform_tests/README.md new file mode 100644 index 0000000000..eb062a97c4 --- /dev/null +++ b/build/dependencies_cross_platform_tests/README.md @@ -0,0 +1,73 @@ +# dxpy dependencies cross-platform tests + +## Usage + +### Linux + +1. Install Python and Docker: `sudo apt install --yes python3 docker.io` +2. Install Docker Python: `python3 -m pip install docker` +3. Run tests: `./run_linux.py -t -d ...` + +### Windows + +1. Run PowerShell as an administrator +2. Prepare environment (Pythons, etc.): `powershell.exe -ExecutionPolicy Unrestricted windows\prepare.ps1` +3. Run tests: `python3.11 run_windows.py -t -d ...` + +### MacOS + +1. Install dependencies: `bash macos/prepare.sh` +2. Run tests: `./run_macos.sh -t -d ...` + +## Where are libraries used + +### argcomplete + +Argument completion for `dx` commands in Unix shells. Tested usign `pexpect`. + +### colorama + +Colorized terminal output on Windows. Called directly in `dx` and `dx-app-wizard` scripts. Don't know how to test automatically. + +### pyreadline and pyreadline3 + +Used for TAB completion in interactive commands `dx run` and `dx-app-wizard` on Windows. + +### psutil + +Information about process in `DXConfig` and memory information in `dx-download-all-inputs --parallel`. + +### python-dateutil + +Date parsing in `dxpy.utils.normalize_time_input` function. + +### urllib3 + +Everything related to HTTP requests using their `PoolManager`/`ProxyManager`. Mostly used in `dxpy.__init__`. + +### websocket-client + +Used for streaming execution logs in `dx watch`. + +## Deprecated/failing environments + +### Linux + +* `*-py2-*` - Python 2.7 is not supported by dx-toolkit +* `pyenv-3.6` - Python 3.6 is not supported by dx-toolkit +* `pyenv-3.7` - Python 3.6 is not supported by dx-toolkit +* `debian-10-py3-sysdeps` - problem with psutil installation +* `dx-aee-16.04-0` - dx-toolkit is installed using distribution tarball and thus the environment is not ready for installation from the source +* `ubuntu-18.04-py3-sysdeps` - problem with psutil installation + +### Windows + +* `*-2.7` - Python 2.7 is not supported by dx-toolkit +* `*-3.6` - Python 3.6 is not supported byt dx-toolkit +* `*-3.7` - Python 3.7 is not supported byt dx-toolkit + +### MacOS + +* `*-2.7` - Python 2.7 is not supported by dx-toolkit +* `*-3.6` - Python 3.6 is not supported byt dx-toolkit +* `*-3.7` - Python 3.7 is not supported byt dx-toolkit diff --git a/build/dependencies_cross_platform_tests/applets/test-inputs/dxapp.json b/build/dependencies_cross_platform_tests/applets/test-inputs/dxapp.json new file mode 100644 index 0000000000..9f45eac2ae --- /dev/null +++ b/build/dependencies_cross_platform_tests/applets/test-inputs/dxapp.json @@ -0,0 +1,46 @@ +{ + "name": "test-inputs", + "title": "test-inputs", + "summary": "test-inputs", + "dxapi": "1.0.0", + "version": "0.0.1", + "inputSpec": [ + { + "name": "inp1", + "class": "string", + "optional": false, + "help": "" + }, + { + "name": "inp2", + "class": "file", + "optional": false, + "patterns": [ + "*" + ], + "help": "" + } + ], + "outputSpec": [], + "runSpec": { + "timeoutPolicy": { + "*": { + "hours": 1 + } + }, + "interpreter": "bash", + "file": "src/test-inputs.sh", + "distribution": "Ubuntu", + "release": "20.04", + "version": "0" + }, + "regionalOptions": { + "aws:us-east-1": { + "systemRequirements": { + "*": { + "instanceType": "mem1_ssd1_v2_x4" + } + } + } + } +} diff --git a/build/dependencies_cross_platform_tests/applets/test-inputs/src/test-inputs.sh b/build/dependencies_cross_platform_tests/applets/test-inputs/src/test-inputs.sh new file mode 100755 index 0000000000..da64904cae --- /dev/null +++ b/build/dependencies_cross_platform_tests/applets/test-inputs/src/test-inputs.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +main() { + echo "Value of inp1: '$inp1'" + echo "Value of inp2: '$inp2'" + + dx download "$inp2" -o inp2 + cat inp2 +} diff --git a/build/dependencies_cross_platform_tests/applets/test-simple/dxapp.json b/build/dependencies_cross_platform_tests/applets/test-simple/dxapp.json new file mode 100644 index 0000000000..c0495c6c6c --- /dev/null +++ b/build/dependencies_cross_platform_tests/applets/test-simple/dxapp.json @@ -0,0 +1,30 @@ +{ + "name": "test-simple", + "title": "test-simple", + "summary": "test-simple", + "dxapi": "1.0.0", + "version": "0.0.1", + "inputSpec": [], + "outputSpec": [], + "runSpec": { + "timeoutPolicy": { + "*": { + "minutes": 10 + } + }, + "interpreter": "bash", + "file": "src/test-simple.sh", + "distribution": "Ubuntu", + "release": "20.04", + "version": "0" + }, + "regionalOptions": { + "aws:us-east-1": { + "systemRequirements": { + "*": { + "instanceType": "mem1_ssd1_v2_x4" + } + } + } + } +} diff --git a/build/dependencies_cross_platform_tests/applets/test-simple/src/test-simple.sh b/build/dependencies_cross_platform_tests/applets/test-simple/src/test-simple.sh new file mode 100755 index 0000000000..41ec099d53 --- /dev/null +++ b/build/dependencies_cross_platform_tests/applets/test-simple/src/test-simple.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +main() { + echo "Started" + sleep 10s + echo "Finished" +} diff --git a/build/dependencies_cross_platform_tests/applets/test-watch/dxapp.json b/build/dependencies_cross_platform_tests/applets/test-watch/dxapp.json new file mode 100644 index 0000000000..ec17feb626 --- /dev/null +++ b/build/dependencies_cross_platform_tests/applets/test-watch/dxapp.json @@ -0,0 +1,30 @@ +{ + "name": "test-watch", + "title": "test-watch", + "summary": "test-watch", + "dxapi": "1.0.0", + "version": "0.0.1", + "inputSpec": [], + "outputSpec": [], + "runSpec": { + "timeoutPolicy": { + "*": { + "hours": 1 + } + }, + "interpreter": "bash", + "file": "src/test-watch.sh", + "distribution": "Ubuntu", + "release": "20.04", + "version": "0" + }, + "regionalOptions": { + "aws:us-east-1": { + "systemRequirements": { + "*": { + "instanceType": "mem1_ssd1_v2_x4" + } + } + } + } +} diff --git a/build/dependencies_cross_platform_tests/applets/test-watch/src/test-watch.sh b/build/dependencies_cross_platform_tests/applets/test-watch/src/test-watch.sh new file mode 100755 index 0000000000..0113019591 --- /dev/null +++ b/build/dependencies_cross_platform_tests/applets/test-watch/src/test-watch.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +main() { + echo "Started" + sleep 10s + echo "Test to stderr" 2>&1 + sleep 60s + echo "Finished" +} diff --git a/build/dependencies_cross_platform_tests/dependencies_cross_platform_tests.py b/build/dependencies_cross_platform_tests/dependencies_cross_platform_tests.py new file mode 100644 index 0000000000..e92d030a8f --- /dev/null +++ b/build/dependencies_cross_platform_tests/dependencies_cross_platform_tests.py @@ -0,0 +1,469 @@ +import json +import os +import platform +import pytest +import random +import re +import shutil +import string +import subprocess +import sys +import time + +from contextlib import contextmanager + +FILESIZE = 100 +TEST_DIR = os.path.abspath(os.path.join(__file__, os.pardir)) +IS_LINUX = platform.system() == "Linux" +IS_WINDOWS = platform.system() == "Windows" +GHA_WATCH_RETRIES = 5 +GHA_KNOWN_WATCH_ERRORS = ( + "[Errno 110] Connection timed out", "[Errno 104] Connection reset by peer", "1006: Connection is already closed.", "[Errno 32] Broken pipe", + "1006: EOF occurred in violation of protocol" +) + + +skip_on_windows = pytest.mark.skipif(IS_WINDOWS, reason="This test cannot run on Windows") +run_only_on_windows = pytest.mark.skipif(not IS_WINDOWS, reason="This test can run only on Windows") +skip_interactive_on_request = pytest.mark.skipif(os.environ.get("DXPY_TEST_SKIP_INTERACTIVE", "False").lower() == "true", reason="Requested skipping of interactive tests") + + +def _randstr(length=10): + return "".join(random.choice(string.ascii_letters) for x in range(length)) + + +def _upload_file(dir, name, content=None, platform_path=None, wait_until_closed=True): + filep = os.path.join(dir, name) + + if content is None: + if IS_WINDOWS: + res = subprocess.run(["fsutil", "file", "createnew", filep, str(FILESIZE * 1024 * 1024)], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + else: + res = subprocess.run(["dd", "if=/dev/random", "of=%s" % filep, "bs=%d" % (1024 * 1024), "count=%s" % FILESIZE], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + assert res.returncode == 0 + else: + with open(filep, 'w') as fh: + fh.write(content) + + cmd = ["dx", "upload", "--brief"] + if platform_path is not None: + cmd += ["--path", platform_path] + cmd += [filep] + + res = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) + assert res.returncode == 0 + file_id = res.stdout.strip() + assert file_id.startswith("file-") + + if wait_until_closed: + for _ in range(20): + res = subprocess.run(["dx", "describe", "--json", file_id], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) + if json.loads(res.stdout)["state"] == "closed": + return file_id, filep + time.sleep(10) + raise AssertionError("Files did not reach closed state within a time limit") + + return file_id, filep + + +def _diff_files(file1, file2): + if IS_WINDOWS: + return subprocess.run(["fc", "/B", file1, file2], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) + else: + return subprocess.run(["diff", file1, file2], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) + + +@contextmanager +def working_directory(path): + orig_wd = os.getcwd() + os.chdir(path) + try: + yield + finally: + os.chdir(orig_wd) + + +@pytest.fixture +def tmp_path_str(tmp_path): + """ + Fixture which converts tmp_path built-in from Path to str which is necessary for Python 3.5 compability. + """ + return str(tmp_path) + + +@pytest.fixture(scope="session") +def dx_python_bin(): + return os.getenv("DXPY_TEST_PYTHON_BIN").strip() + + +@pytest.fixture(scope="module", autouse=True) +def project(): + token = os.getenv("DXPY_TEST_TOKEN") + assert token is not None + env = os.getenv("DXPY_TEST_ENV") or "stg" + assert env in ["stg", "prod"] + res = subprocess.run(["dx", "login", "--noprojects", "--token", token] + (["--staging"] if env == "stg" else [])) + assert res.returncode == 0 + res = subprocess.run(["dx", "new", "project", "--select", "--brief", "dxpy-deptest-%s" % _randstr()], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) + assert res.returncode == 0 + project_id = res.stdout.strip() + assert project_id.startswith("project-") + + yield project_id + + res = subprocess.run(["dx", "rmproject", "--yes", project_id]) + assert res.returncode == 0 + + +@pytest.fixture +def applet(request): + res = subprocess.run(["dx", "build", "--force", "--brief", os.path.join(TEST_DIR, "applets", "test-%s" % request.param)], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) + assert res.returncode == 0 + applet_id = json.loads(res.stdout.strip())["id"] + assert applet_id.startswith("applet-") + + yield applet_id + + res = subprocess.run(["dx", "rm", applet_id]) + assert res.returncode == 0 + + +@pytest.fixture +def two_files(tmp_path_str): + file1 = _randstr() + content1 = "My file 1 content..." + fileid1, _ = _upload_file(tmp_path_str, file1, content1) + + file2 = _randstr() + content2 = "My file 2 content..." + fileid2, _ = _upload_file(tmp_path_str, file2, content2) + + yield ((file1, fileid1, content1), (file2, fileid2, content2)) + + res = subprocess.run(["dx", "rm", fileid1]) + assert res.returncode == 0 + res = subprocess.run(["dx", "rm", fileid2]) + assert res.returncode == 0 + + +def test_python_version(dx_python_bin): + """ + Tested libraries: none + """ + python_version = os.getenv("DXPY_TEST_PYTHON_VERSION") + assert python_version in ["3"] + res = subprocess.run([dx_python_bin, "--version"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True) + assert res.returncode == 0 + assert res.stdout.split(" ")[1][0] == python_version + + +def test_file_simple(tmp_path_str, project): + """ + Tested libraries: requests, urllib3 + """ + file = "file" + platform_path = "/test_file" + assert platform_path[0] == "/" + assert platform_path[1:] != file + file_id, filep = _upload_file(tmp_path_str, "file", platform_path=platform_path) + res = subprocess.run(["dx", "describe", "--json", file_id], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) + assert res.returncode == 0 + file_describe = json.loads(res.stdout) + assert file_describe["class"] == "file" + assert file_describe["id"] == file_id + assert file_describe["project"] == project + res = subprocess.run(["dx", "download", platform_path], cwd=tmp_path_str) + assert res.returncode == 0 + + res = _diff_files(filep, os.path.join(tmp_path_str, platform_path[1:])) + assert res.returncode == 0 + + +def test_file_nonexistent(): + """ + Tested libraries: requests, urllib3 + """ + res = subprocess.run(["dx", "download", "file-%s" % _randstr(24)], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) + assert res.returncode != 0 + assert "code 404" in res.stderr + + +def test_print_env(): + """ + Tested libraries: psutil + """ + res = subprocess.run(["dx", "env"]) + assert res.returncode == 0 + + +@skip_on_windows +def test_download_all_inputs(tmp_path_str, two_files): + """ + Tested libraries: psutil + """ + file1, fileid1, _ = two_files[0] + file2, fileid2, _ = two_files[1] + destdir = os.path.join(tmp_path_str, "dest") + os.mkdir(destdir) + with working_directory(destdir): + job_input = { + "inp1": {"$dnanexus_link": fileid1}, + "inp2": {"$dnanexus_link": fileid2}, + } + + with open("job_input.json", 'w') as fh: + json.dump(job_input, fh) + + env = os.environ.copy() + env["HOME"] = destdir + shutil.copytree(os.getenv("DX_USER_CONF_DIR", os.path.join(os.path.expanduser('~'), ".dnanexus_config")), ".dnanexus_config") + res = subprocess.run(["dx-download-all-inputs", "--parallel"], env=env) + assert res.returncode == 0 + + res = _diff_files(os.path.join(tmp_path_str, file1), os.path.join(destdir, "in", "inp1", file1)) + assert res.returncode == 0 + res = _diff_files(os.path.join(tmp_path_str, file2), os.path.join(destdir, "in", "inp2", file2)) + assert res.returncode == 0 + + +@pytest.mark.parametrize("applet", ["simple"], indirect=["applet"]) +def test_job_simple(project, applet): + """ + Tested libraries: requests, urllib3 + """ + res = subprocess.run(["dx", "run", "--yes", "--brief", "--ignore-reuse", applet], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) + assert res.returncode == 0 + job_id = res.stdout.strip() + assert job_id.startswith("job-") + res = subprocess.run(["dx", "describe", "--json", job_id], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) + assert res.returncode == 0 + job_describe = json.loads(res.stdout) + assert job_describe["id"] == job_id + assert job_describe["project"] == project + res = subprocess.run(["dx", "wait", job_id]) + assert res.returncode == 0 + res = subprocess.run(["dx", "watch", job_id], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) + assert res.returncode == 0 + assert "Started" in res.stdout + assert "Finished" in res.stdout + + +@pytest.mark.parametrize("applet", ["watch"], indirect=["applet"]) +def test_job_watch(project, applet): + """ + Tested libraries: requests, urllib3, websocket-client + """ + res = subprocess.run(["dx", "run", "--yes", "--brief", "--ignore-reuse", applet], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) + assert res.returncode == 0 + job_id = res.stdout.strip() + assert job_id.startswith("job-") + res = subprocess.run(["dx", "describe", "--json", job_id], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) + assert res.returncode == 0 + job_describe = json.loads(res.stdout) + assert job_describe["id"] == job_id + assert job_describe["project"] == project + + for i in range(GHA_WATCH_RETRIES): + res = subprocess.run(["dx", "watch", job_id], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) + + assert res.returncode == 0 + + if any(map(lambda x: x in res.stderr, GHA_KNOWN_WATCH_ERRORS)): + time.sleep(15) + continue + + assert "Started" in res.stdout + assert "Test to stderr" in res.stdout + assert "Finished" in res.stdout + return + + assert False, "Watch did not successfully finished even after %d retries" % GHA_WATCH_RETRIES + + +def test_import(dx_python_bin): + """ + Tested libraries: none + """ + res = subprocess.run([dx_python_bin, "-c", "import dxpy"]) + assert res.returncode == 0 + + +def test_normalize_time_input(dx_python_bin, two_files): + """ + Tested libraries: python-dateutil + """ + _, fileid1, _ = two_files[0] + _, fileid2, _ = two_files[1] + + res = subprocess.run(["dx", "find", "data", "--created-after=-1w"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) + assert res.returncode == 0 + assert fileid1 in res.stdout + assert fileid2 in res.stdout + + res = subprocess.run([dx_python_bin, "-c", "import sys; import dxpy.utils; sys.exit(0 if dxpy.utils.normalize_time_input('1d', default_unit='s') == 24 * 60 * 60 * 1000 else 1)"]) + assert res.returncode == 0 + + +@skip_on_windows +@pytest.mark.parametrize("applet", ["inputs"], indirect=["applet"]) +def test_run_interactive(applet, two_files): + """ + Tested libraries: readline + """ + import pexpect + file, fileid, content = two_files[0] + + inp1_val = "string value" + proc = pexpect.spawn("dx run %s" % applet) + proc.expect("inp1:") + proc.sendline(inp1_val) + proc.expect("inp2:") + proc.send("\t\t") + proc.expect(file) + proc.send(file[0:5] + "\t") + proc.expect(file) + proc.send("\n") + proc.expect("Confirm running the executable with this input \\[Y/n\\]:") + proc.sendline("Y") + proc.expect("Watch launched job now\\? \\[Y/n\\]") + job_id = re.search("(job-[a-zA-Z0-9]{24})", proc.before.decode()).group(1) + proc.sendline("n") + proc.expect(pexpect.EOF) + + assert job_id is not None + + res = subprocess.run(["dx", "wait", job_id]) + assert res.returncode == 0 + + res = subprocess.run(["dx", "watch", job_id], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) + assert res.returncode == 0 + assert inp1_val in res.stdout + assert fileid in res.stdout + assert content in res.stdout + + +@pytest.mark.skipif(IS_LINUX and sys.version_info < (3, 7), reason="Won't fix for old Python versions (see DEVEX-2258)") +@skip_on_windows +def test_dx_app_wizard_interactive(tmp_path_str): + """ + Tested libraries: readline + """ + import pexpect + + with working_directory(tmp_path_str): + app_name = "test_applet" + proc = pexpect.spawn("dx-app-wizard") + proc.expect("App Name:") + proc.sendline(app_name) + proc.expect("Title .+:") + proc.sendline() + proc.expect("Summary .+:") + proc.sendline() + proc.expect("Version .+:") + proc.sendline() + proc.expect("1st input name .+:") + proc.sendline("inp1") + proc.expect("Label .+: ") + proc.sendline() + proc.expect("Choose a class .+:") + proc.send("\t\t") + proc.expect("boolean") + proc.send("fl\t") + proc.expect("float") + proc.sendline() + proc.expect("This is an optional parameter .+:") + proc.sendline("n") + proc.expect("2nd input name .+:") + proc.sendline() + proc.expect("1st output name .+:") + proc.sendline("out1") + proc.expect("Label .+:") + proc.sendline() + proc.expect("Choose a class .+:") + proc.send("\t\t") + proc.expect("record") + proc.send("h\t") + proc.expect("hash") + proc.sendline() + proc.expect("2nd output name .+:") + proc.sendline() + proc.expect("Timeout policy .+:") + proc.sendline() + proc.expect("Programming language:") + proc.sendline("bash") + proc.expect("Will this app need access to the Internet\\? .+:") + proc.sendline() + proc.expect("Will this app need access to the parent project\\? .+:") + proc.sendline() + proc.expect("Choose an instance type for your app .+:") + proc.sendline() + proc.expect(pexpect.EOF) + + assert os.path.isdir(os.path.join(tmp_path_str, app_name)) + assert os.path.isfile(os.path.join(tmp_path_str, app_name, "dxapp.json")) + + +@skip_on_windows +def test_argcomplete(two_files): + """ + Tested libraries: argcomplete + """ + import pexpect + file, _, _ = two_files[0] + + proc = pexpect.spawn("/bin/bash") + proc.sendline('eval "$(register-python-argcomplete dx|sed \'s/-o default//\')"') + proc.send("dx \t\t") + proc.expect("generate_batch_inputs") + proc.send("new \t\t") + proc.expect("record") + proc.send("wor\t") + proc.expect("workflow") + proc.sendline('\003') + proc.send("dx describe \t\t") + proc.expect(file) + proc.sendline('\003') + proc.sendline("exit") + proc.expect(pexpect.EOF) + + +@run_only_on_windows +@skip_interactive_on_request +@pytest.mark.parametrize("applet", ["inputs"], indirect=["applet"]) +def test_dx_run_interactive_windows(tmp_path_str, applet, two_files): + """ + Tested libraries: pyreadline, pyreadline3 + """ + file, fileid, content = two_files[0] + with working_directory(tmp_path_str): + env = os.environ.copy() + env["APPLET"] = applet + env["INP1_VAL"] = "test value" + env["INP2_VAL"] = file + res = subprocess.run(["powershell", "-ExecutionPolicy", "Unrestricted", os.path.join(TEST_DIR, "windows", "test_dx_run_interactive.ps1")], timeout=30, env=env) + assert res.returncode == 0 + + +@run_only_on_windows +@skip_interactive_on_request +def test_dx_app_wizard_interactive_windows(tmp_path_str): + """ + Tested libraries: pyreadline, pyreadline3 + """ + with working_directory(tmp_path_str): + app_name = "test_applet" + res = subprocess.run(["powershell", "-ExecutionPolicy", "Unrestricted", os.path.join(TEST_DIR, "windows", "test_dx_app_wizard_interactive.ps1")], timeout=30) + assert res.returncode == 0 + + time.sleep(2) + assert os.path.isdir(os.path.join(tmp_path_str, app_name)) + assert os.path.isfile(os.path.join(tmp_path_str, app_name, "dxapp.json")) + + +@run_only_on_windows +def test_colorama(dx_python_bin): + """ + Tested libraries: colorama + """ + res = subprocess.run([dx_python_bin, "-c", "import colorama; colorama.init()"]) + assert res.returncode == 0 diff --git a/build/dependencies_cross_platform_tests/linux/dockerfiles/.dockerignore b/build/dependencies_cross_platform_tests/linux/dockerfiles/.dockerignore new file mode 100644 index 0000000000..3c10201f10 --- /dev/null +++ b/build/dependencies_cross_platform_tests/linux/dockerfiles/.dockerignore @@ -0,0 +1,2 @@ +failing/ +*.Dockerfile diff --git a/build/dependencies_cross_platform_tests/linux/dockerfiles/debian-11-py3-minimal.Dockerfile b/build/dependencies_cross_platform_tests/linux/dockerfiles/debian-11-py3-minimal.Dockerfile new file mode 100644 index 0000000000..992c1f660e --- /dev/null +++ b/build/dependencies_cross_platform_tests/linux/dockerfiles/debian-11-py3-minimal.Dockerfile @@ -0,0 +1,18 @@ +FROM --platform=amd64 debian:11 + +SHELL ["/bin/bash", "-c"] +ENV DEBIAN_FRONTEND=noninteractive +ENV DXPY_TEST_PYTHON_VERSION=3 + +RUN \ + apt-get update && \ + apt-get install -y python3 python3-pip python3-venv && \ + python3 -m venv /pytest-env + +RUN \ + source /pytest-env/bin/activate && \ + python3 -m pip install --quiet pytest pexpect + +COPY run_tests.sh / + +ENTRYPOINT [ "/run_tests.sh" ] diff --git a/build/dependencies_cross_platform_tests/linux/dockerfiles/debian-11-py3-sysdeps.Dockerfile b/build/dependencies_cross_platform_tests/linux/dockerfiles/debian-11-py3-sysdeps.Dockerfile new file mode 100644 index 0000000000..e14d44d633 --- /dev/null +++ b/build/dependencies_cross_platform_tests/linux/dockerfiles/debian-11-py3-sysdeps.Dockerfile @@ -0,0 +1,26 @@ +FROM --platform=amd64 debian:11 + +SHELL ["/bin/bash", "-c"] +ENV DEBIAN_FRONTEND=noninteractive +ENV DXPY_TEST_PYTHON_VERSION=3 + +RUN \ + apt-get update && \ + apt-get install -y python3 python3-pip python3-venv && \ + python3 -m venv /pytest-env + +RUN \ + source /pytest-env/bin/activate && \ + python3 -m pip install --quiet pytest pexpect + +RUN \ + apt-get install -y \ + python3-argcomplete \ + python3-dateutil \ + python3-psutil \ + python3-urllib3 \ + python3-websocket + +COPY run_tests.sh / + +ENTRYPOINT [ "/run_tests.sh" ] diff --git a/build/dependencies_cross_platform_tests/linux/dockerfiles/dx-aee-20.04-0.Dockerfile b/build/dependencies_cross_platform_tests/linux/dockerfiles/dx-aee-20.04-0.Dockerfile new file mode 100644 index 0000000000..a69898f5a4 --- /dev/null +++ b/build/dependencies_cross_platform_tests/linux/dockerfiles/dx-aee-20.04-0.Dockerfile @@ -0,0 +1,19 @@ +FROM --platform=amd64 ubuntu:20.04 + +SHELL ["/bin/bash", "-c"] +ENV DEBIAN_FRONTEND=noninteractive +ENV DXPY_TEST_PYTHON_VERSION=3 + +RUN \ + apt-get update && \ + apt-get install -y python3 python3-pip python3-venv && \ + python3 -m pip install --quiet pip==20.0.2 setuptools==46.1.3 wheel==0.37.1 requests==2.23.0 cryptography==36.0.2 pyOpenSSL==22.0.0 secretstorage==3.3.1 && \ + python3 -m venv /pytest-env + +RUN \ + source /pytest-env/bin/activate && \ + python3 -m pip install --quiet pytest pexpect + +COPY run_tests.sh / + +ENTRYPOINT [ "/run_tests.sh" ] diff --git a/build/dependencies_cross_platform_tests/linux/dockerfiles/failing/debian-10-py3-sysdeps.Dockerfile b/build/dependencies_cross_platform_tests/linux/dockerfiles/failing/debian-10-py3-sysdeps.Dockerfile new file mode 100644 index 0000000000..c9acad2193 --- /dev/null +++ b/build/dependencies_cross_platform_tests/linux/dockerfiles/failing/debian-10-py3-sysdeps.Dockerfile @@ -0,0 +1,28 @@ +FROM --platform=amd64 debian:10 + +SHELL ["/bin/bash", "-c"] +ENV DEBIAN_FRONTEND=noninteractive +ENV DXPY_TEST_PYTHON_VERSION=3 + +RUN \ + apt-get update && \ + apt-get install -y python3 python3-pip python3-venv && \ + python3 -m pip install --quiet --upgrade pip && \ + python3 -m venv /pytest-env + +RUN \ + source /pytest-env/bin/activate && \ + python3 -m pip install --quiet pytest pexpect + +RUN \ + apt-get install -y \ + python3-argcomplete \ + python3-cryptography \ + python3-dateutil \ + python3-psutil \ + python3-requests \ + python3-websocket + +COPY run_tests.sh / + +ENTRYPOINT [ "/run_tests.sh" ] diff --git a/build/dependencies_cross_platform_tests/linux/dockerfiles/failing/dx-aee-16.04-0.Dockerfile b/build/dependencies_cross_platform_tests/linux/dockerfiles/failing/dx-aee-16.04-0.Dockerfile new file mode 100644 index 0000000000..77731bd458 --- /dev/null +++ b/build/dependencies_cross_platform_tests/linux/dockerfiles/failing/dx-aee-16.04-0.Dockerfile @@ -0,0 +1,23 @@ +FROM ubuntu:16.04 + +SHELL ["/bin/bash", "-c"] +ENV DEBIAN_FRONTEND=noninteractive +ENV DXPY_TEST_PYTHON_VERSION=2 + +RUN \ + apt-get update && \ + apt-get install -y python python-pip python3-pip python3-venv libffi-dev && \ + python3 -m pip install --quiet pip==20.3.4 setuptools==50.3.2 wheel==0.37.1 && \ + python2 -m pip install --quiet 'packaging<21.0' 'pyparsing<=2.4.5' six appdirs && \ + python2 -m pip install --quiet --upgrade 'pip<21.0' && \ + python2 -m pip install --quiet setuptools==10.2 && \ + python3 -m venv /pytest-env + +RUN \ + source /pytest-env/bin/activate && \ + python3 -m pip install --quiet pip==20.3.4 setuptools==50.3.2 wheel==0.37.1 && \ + python3 -m pip install --quiet pytest==6.1.2 pexpect + +COPY run_tests.sh / + +ENTRYPOINT [ "/run_tests.sh" ] diff --git a/build/dependencies_cross_platform_tests/linux/dockerfiles/failing/ubuntu-18.04-py3-sysdeps.Dockerfile b/build/dependencies_cross_platform_tests/linux/dockerfiles/failing/ubuntu-18.04-py3-sysdeps.Dockerfile new file mode 100644 index 0000000000..2f9c621753 --- /dev/null +++ b/build/dependencies_cross_platform_tests/linux/dockerfiles/failing/ubuntu-18.04-py3-sysdeps.Dockerfile @@ -0,0 +1,28 @@ +FROM --platform=amd64 ubuntu:18.04 + +SHELL ["/bin/bash", "-c"] +ENV DEBIAN_FRONTEND=noninteractive +ENV DXPY_TEST_PYTHON_VERSION=3 + +RUN \ + apt-get update && \ + apt-get install -y python3 python3-pip python3-venv && \ + python3 -m pip install --upgrade pip wheel && \ + python3 -m venv /pytest-env + +RUN \ + source /pytest-env/bin/activate && \ + python3 -m pip install --quiet pytest pexpect + +RUN \ + apt-get install -y \ + python3-argcomplete \ + python3-cryptography \ + python3-dateutil \ + python3-psutil \ + python3-requests \ + python3-websocket + +COPY run_tests.sh / + +ENTRYPOINT [ "/run_tests.sh" ] diff --git a/build/dependencies_cross_platform_tests/linux/dockerfiles/fedora-36-py3-minimal.Dockerfile b/build/dependencies_cross_platform_tests/linux/dockerfiles/fedora-36-py3-minimal.Dockerfile new file mode 100644 index 0000000000..19340f5a41 --- /dev/null +++ b/build/dependencies_cross_platform_tests/linux/dockerfiles/fedora-36-py3-minimal.Dockerfile @@ -0,0 +1,16 @@ +FROM --platform=amd64 fedora:36 + +SHELL ["/bin/bash", "-c"] +ENV DXPY_TEST_PYTHON_VERSION=3 + +RUN \ + dnf install -y which diffutils python3 python3-pip && \ + python3 -m venv /pytest-env + +RUN \ + source /pytest-env/bin/activate && \ + python3 -m pip install --quiet pytest pexpect + +COPY run_tests.sh / + +ENTRYPOINT [ "/run_tests.sh" ] diff --git a/build/dependencies_cross_platform_tests/linux/dockerfiles/fedora-36-py3-sysdeps.Dockerfile b/build/dependencies_cross_platform_tests/linux/dockerfiles/fedora-36-py3-sysdeps.Dockerfile new file mode 100644 index 0000000000..fa1db09261 --- /dev/null +++ b/build/dependencies_cross_platform_tests/linux/dockerfiles/fedora-36-py3-sysdeps.Dockerfile @@ -0,0 +1,24 @@ +FROM --platform=amd64 fedora:36 + +SHELL ["/bin/bash", "-c"] +ENV DXPY_TEST_PYTHON_VERSION=3 + +RUN \ + dnf install -y which diffutils python3 python3-pip && \ + python3 -m venv /pytest-env + +RUN \ + source /pytest-env/bin/activate && \ + python3 -m pip install --quiet pytest pexpect + +RUN \ + dnf install -y \ + python3-argcomplete \ + python3-dateutil \ + python3-psutil \ + python3-urllib3 \ + python3-websocket-client + +COPY run_tests.sh / + +ENTRYPOINT [ "/run_tests.sh" ] diff --git a/build/dependencies_cross_platform_tests/linux/dockerfiles/fedora-37-py3-minimal.Dockerfile b/build/dependencies_cross_platform_tests/linux/dockerfiles/fedora-37-py3-minimal.Dockerfile new file mode 100644 index 0000000000..2892114278 --- /dev/null +++ b/build/dependencies_cross_platform_tests/linux/dockerfiles/fedora-37-py3-minimal.Dockerfile @@ -0,0 +1,16 @@ +FROM --platform=amd64 fedora:37 + +SHELL ["/bin/bash", "-c"] +ENV DXPY_TEST_PYTHON_VERSION=3 + +RUN \ + dnf install -y which diffutils python3 python3-pip && \ + python3 -m venv /pytest-env + +RUN \ + source /pytest-env/bin/activate && \ + python3 -m pip install --quiet pytest pexpect + +COPY run_tests.sh / + +ENTRYPOINT [ "/run_tests.sh" ] diff --git a/build/dependencies_cross_platform_tests/linux/dockerfiles/fedora-37-py3-sysdeps.Dockerfile b/build/dependencies_cross_platform_tests/linux/dockerfiles/fedora-37-py3-sysdeps.Dockerfile new file mode 100644 index 0000000000..bb13059209 --- /dev/null +++ b/build/dependencies_cross_platform_tests/linux/dockerfiles/fedora-37-py3-sysdeps.Dockerfile @@ -0,0 +1,24 @@ +FROM --platform=amd64 fedora:37 + +SHELL ["/bin/bash", "-c"] +ENV DXPY_TEST_PYTHON_VERSION=3 + +RUN \ + dnf install -y which diffutils python3 python3-pip && \ + python3 -m venv /pytest-env + +RUN \ + source /pytest-env/bin/activate && \ + python3 -m pip install --quiet pytest pexpect + +RUN \ + dnf install -y \ + python3-argcomplete \ + python3-dateutil \ + python3-psutil \ + python3-urllib3 \ + python3-websocket-client + +COPY run_tests.sh / + +ENTRYPOINT [ "/run_tests.sh" ] diff --git a/build/dependencies_cross_platform_tests/linux/dockerfiles/fedora-38-py3-minimal.Dockerfile b/build/dependencies_cross_platform_tests/linux/dockerfiles/fedora-38-py3-minimal.Dockerfile new file mode 100644 index 0000000000..97cac5f3c1 --- /dev/null +++ b/build/dependencies_cross_platform_tests/linux/dockerfiles/fedora-38-py3-minimal.Dockerfile @@ -0,0 +1,16 @@ +FROM --platform=amd64 fedora:38 + +SHELL ["/bin/bash", "-c"] +ENV DXPY_TEST_PYTHON_VERSION=3 + +RUN \ + dnf install -y which diffutils python3 python3-pip && \ + python3 -m venv /pytest-env + +RUN \ + source /pytest-env/bin/activate && \ + python3 -m pip install --quiet pytest pexpect + +COPY run_tests.sh / + +ENTRYPOINT [ "/run_tests.sh" ] diff --git a/build/dependencies_cross_platform_tests/linux/dockerfiles/fedora-38-py3-sysdeps.Dockerfile b/build/dependencies_cross_platform_tests/linux/dockerfiles/fedora-38-py3-sysdeps.Dockerfile new file mode 100644 index 0000000000..434d4395b2 --- /dev/null +++ b/build/dependencies_cross_platform_tests/linux/dockerfiles/fedora-38-py3-sysdeps.Dockerfile @@ -0,0 +1,24 @@ +FROM --platform=amd64 fedora:38 + +SHELL ["/bin/bash", "-c"] +ENV DXPY_TEST_PYTHON_VERSION=3 + +RUN \ + dnf install -y which diffutils python3 python3-pip && \ + python3 -m venv /pytest-env + +RUN \ + source /pytest-env/bin/activate && \ + python3 -m pip install --quiet pytest pexpect + +RUN \ + dnf install -y \ + python3-argcomplete \ + python3-dateutil \ + python3-psutil \ + python3-urllib3 \ + python3-websocket-client + +COPY run_tests.sh / + +ENTRYPOINT [ "/run_tests.sh" ] diff --git a/build/dependencies_cross_platform_tests/linux/dockerfiles/pyenv-3.10.Dockerfile b/build/dependencies_cross_platform_tests/linux/dockerfiles/pyenv-3.10.Dockerfile new file mode 100644 index 0000000000..89c7618a5f --- /dev/null +++ b/build/dependencies_cross_platform_tests/linux/dockerfiles/pyenv-3.10.Dockerfile @@ -0,0 +1,29 @@ +FROM --platform=amd64 ubuntu:22.04 + +SHELL ["/bin/bash", "-c"] +ENV DEBIAN_FRONTEND=noninteractive +ENV DXPY_TEST_PYTHON_VERSION=3 +ENV DXPY_TEST_USING_PYENV=true + +RUN \ + apt-get update && \ + apt-get install -y curl build-essential git zlib1g-dev libssl-dev libbz2-dev libffi-dev libncurses-dev libreadline-dev libsqlite3-dev liblzma-dev && \ + curl https://pyenv.run | bash + +ENV PYENV_ROOT=/root/.pyenv +ENV PATH="${PYENV_ROOT}/bin:${PATH}" +ENV PYENV_PYTHON_VERSION=3.10 + +RUN \ + eval "$(pyenv init -)" && \ + pyenv install ${PYENV_PYTHON_VERSION} && \ + pyenv global ${PYENV_PYTHON_VERSION} && \ + python3 -m venv /pytest-env + +RUN \ + source /pytest-env/bin/activate && \ + python3 -m pip install --quiet pytest pexpect + +COPY run_tests.sh / + +ENTRYPOINT [ "/run_tests.sh" ] diff --git a/build/dependencies_cross_platform_tests/linux/dockerfiles/pyenv-3.11.Dockerfile b/build/dependencies_cross_platform_tests/linux/dockerfiles/pyenv-3.11.Dockerfile new file mode 100644 index 0000000000..6e72c45ddd --- /dev/null +++ b/build/dependencies_cross_platform_tests/linux/dockerfiles/pyenv-3.11.Dockerfile @@ -0,0 +1,29 @@ +FROM --platform=amd64 ubuntu:22.04 + +SHELL ["/bin/bash", "-c"] +ENV DEBIAN_FRONTEND=noninteractive +ENV DXPY_TEST_PYTHON_VERSION=3 +ENV DXPY_TEST_USING_PYENV=true + +RUN \ + apt-get update && \ + apt-get install -y curl build-essential git zlib1g-dev libssl-dev libbz2-dev libffi-dev libncurses-dev libreadline-dev libsqlite3-dev liblzma-dev && \ + curl https://pyenv.run | bash + +ENV PYENV_ROOT=/root/.pyenv +ENV PATH="${PYENV_ROOT}/bin:${PATH}" +ENV PYENV_PYTHON_VERSION=3.11 + +RUN \ + eval "$(pyenv init -)" && \ + pyenv install ${PYENV_PYTHON_VERSION} && \ + pyenv global ${PYENV_PYTHON_VERSION} && \ + python3 -m venv /pytest-env + +RUN \ + source /pytest-env/bin/activate && \ + python3 -m pip install --quiet pytest pexpect + +COPY run_tests.sh / + +ENTRYPOINT [ "/run_tests.sh" ] diff --git a/build/dependencies_cross_platform_tests/linux/dockerfiles/pyenv-3.12.Dockerfile b/build/dependencies_cross_platform_tests/linux/dockerfiles/pyenv-3.12.Dockerfile new file mode 100644 index 0000000000..bb712e0185 --- /dev/null +++ b/build/dependencies_cross_platform_tests/linux/dockerfiles/pyenv-3.12.Dockerfile @@ -0,0 +1,29 @@ +FROM --platform=amd64 ubuntu:22.04 + +SHELL ["/bin/bash", "-c"] +ENV DEBIAN_FRONTEND=noninteractive +ENV DXPY_TEST_PYTHON_VERSION=3 +ENV DXPY_TEST_USING_PYENV=true + +RUN \ + apt-get update && \ + apt-get install -y curl build-essential git zlib1g-dev libssl-dev libbz2-dev libffi-dev libncurses-dev libreadline-dev libsqlite3-dev liblzma-dev && \ + curl https://pyenv.run | bash + +ENV PYENV_ROOT=/root/.pyenv +ENV PATH="${PYENV_ROOT}/bin:${PATH}" +ENV PYENV_PYTHON_VERSION=3.12 + +RUN \ + eval "$(pyenv init -)" && \ + pyenv install ${PYENV_PYTHON_VERSION} && \ + pyenv global ${PYENV_PYTHON_VERSION} && \ + python3 -m venv /pytest-env + +RUN \ + source /pytest-env/bin/activate && \ + python3 -m pip install --quiet pytest pexpect + +COPY run_tests.sh / + +ENTRYPOINT [ "/run_tests.sh" ] diff --git a/build/dependencies_cross_platform_tests/linux/dockerfiles/pyenv-3.8.Dockerfile b/build/dependencies_cross_platform_tests/linux/dockerfiles/pyenv-3.8.Dockerfile new file mode 100644 index 0000000000..c79ca8480c --- /dev/null +++ b/build/dependencies_cross_platform_tests/linux/dockerfiles/pyenv-3.8.Dockerfile @@ -0,0 +1,29 @@ +FROM --platform=amd64 ubuntu:22.04 + +SHELL ["/bin/bash", "-c"] +ENV DEBIAN_FRONTEND=noninteractive +ENV DXPY_TEST_PYTHON_VERSION=3 +ENV DXPY_TEST_USING_PYENV=true + +RUN \ + apt-get update && \ + apt-get install -y curl build-essential git zlib1g-dev libssl-dev libbz2-dev libffi-dev libncurses-dev libreadline-dev libsqlite3-dev liblzma-dev && \ + curl https://pyenv.run | bash + +ENV PYENV_ROOT=/root/.pyenv +ENV PATH="${PYENV_ROOT}/bin:${PATH}" +ENV PYENV_PYTHON_VERSION=3.8 + +RUN \ + eval "$(pyenv init -)" && \ + pyenv install ${PYENV_PYTHON_VERSION} && \ + pyenv global ${PYENV_PYTHON_VERSION} && \ + python3 -m venv /pytest-env + +RUN \ + source /pytest-env/bin/activate && \ + python3 -m pip install --quiet pytest pexpect + +COPY run_tests.sh / + +ENTRYPOINT [ "/run_tests.sh" ] diff --git a/build/dependencies_cross_platform_tests/linux/dockerfiles/pyenv-3.9.Dockerfile b/build/dependencies_cross_platform_tests/linux/dockerfiles/pyenv-3.9.Dockerfile new file mode 100644 index 0000000000..35128f3cd8 --- /dev/null +++ b/build/dependencies_cross_platform_tests/linux/dockerfiles/pyenv-3.9.Dockerfile @@ -0,0 +1,29 @@ +FROM --platform=amd64 ubuntu:22.04 + +SHELL ["/bin/bash", "-c"] +ENV DEBIAN_FRONTEND=noninteractive +ENV DXPY_TEST_PYTHON_VERSION=3 +ENV DXPY_TEST_USING_PYENV=true + +RUN \ + apt-get update && \ + apt-get install -y curl build-essential git zlib1g-dev libssl-dev libbz2-dev libffi-dev libncurses-dev libreadline-dev libsqlite3-dev liblzma-dev && \ + curl https://pyenv.run | bash + +ENV PYENV_ROOT=/root/.pyenv +ENV PATH="${PYENV_ROOT}/bin:${PATH}" +ENV PYENV_PYTHON_VERSION=3.9 + +RUN \ + eval "$(pyenv init -)" && \ + pyenv install ${PYENV_PYTHON_VERSION} && \ + pyenv global ${PYENV_PYTHON_VERSION} && \ + python3 -m venv /pytest-env + +RUN \ + source /pytest-env/bin/activate && \ + python3 -m pip install --quiet pytest pexpect + +COPY run_tests.sh / + +ENTRYPOINT [ "/run_tests.sh" ] diff --git a/build/dependencies_cross_platform_tests/linux/dockerfiles/rhel-8-py3-minimal.Dockerfile b/build/dependencies_cross_platform_tests/linux/dockerfiles/rhel-8-py3-minimal.Dockerfile new file mode 100644 index 0000000000..eb9f7e83ae --- /dev/null +++ b/build/dependencies_cross_platform_tests/linux/dockerfiles/rhel-8-py3-minimal.Dockerfile @@ -0,0 +1,17 @@ +FROM --platform=amd64 almalinux:8 + +SHELL ["/bin/bash", "-c"] +ENV DXPY_TEST_PYTHON_VERSION=3 + +RUN \ + dnf install -y which diffutils python38 python38-pip && \ + python3 -m pip install --quiet --upgrade pip && \ + python3 -m venv /pytest-env + +RUN \ + source /pytest-env/bin/activate && \ + python3 -m pip install --quiet pytest pexpect + +COPY run_tests.sh / + +ENTRYPOINT [ "/run_tests.sh" ] diff --git a/build/dependencies_cross_platform_tests/linux/dockerfiles/rhel-8-py3-sysdeps.Dockerfile b/build/dependencies_cross_platform_tests/linux/dockerfiles/rhel-8-py3-sysdeps.Dockerfile new file mode 100644 index 0000000000..46b6822f75 --- /dev/null +++ b/build/dependencies_cross_platform_tests/linux/dockerfiles/rhel-8-py3-sysdeps.Dockerfile @@ -0,0 +1,24 @@ +FROM --platform=amd64 almalinux:8 + +SHELL ["/bin/bash", "-c"] +ENV DXPY_TEST_PYTHON_VERSION=3 + +RUN \ + dnf install -y epel-release && \ + dnf install -y which diffutils python38 python38-pip && \ + python3 -m pip install --quiet --upgrade pip && \ + python3 -m venv /pytest-env + +RUN \ + source /pytest-env/bin/activate && \ + python3 -m pip install --quiet pytest pexpect + +RUN \ + dnf install -y \ + python38-dateutil \ + python38-psutil \ + python38-urllib3 + +COPY run_tests.sh / + +ENTRYPOINT [ "/run_tests.sh" ] diff --git a/build/dependencies_cross_platform_tests/linux/dockerfiles/rhel-9-py3-minimal.Dockerfile b/build/dependencies_cross_platform_tests/linux/dockerfiles/rhel-9-py3-minimal.Dockerfile new file mode 100644 index 0000000000..9fbd536b3c --- /dev/null +++ b/build/dependencies_cross_platform_tests/linux/dockerfiles/rhel-9-py3-minimal.Dockerfile @@ -0,0 +1,16 @@ +FROM --platform=amd64 almalinux:9 + +SHELL ["/bin/bash", "-c"] +ENV DXPY_TEST_PYTHON_VERSION=3 + +RUN \ + dnf install -y which diffutils python3 python3-pip && \ + python3 -m venv /pytest-env + +RUN \ + source /pytest-env/bin/activate && \ + python3 -m pip install --quiet pytest pexpect + +COPY run_tests.sh / + +ENTRYPOINT [ "/run_tests.sh" ] diff --git a/build/dependencies_cross_platform_tests/linux/dockerfiles/rhel-9-py3-sysdeps.Dockerfile b/build/dependencies_cross_platform_tests/linux/dockerfiles/rhel-9-py3-sysdeps.Dockerfile new file mode 100644 index 0000000000..0896a7db34 --- /dev/null +++ b/build/dependencies_cross_platform_tests/linux/dockerfiles/rhel-9-py3-sysdeps.Dockerfile @@ -0,0 +1,25 @@ +FROM --platform=amd64 almalinux:9 + +SHELL ["/bin/bash", "-c"] +ENV DXPY_TEST_PYTHON_VERSION=3 + +RUN \ + dnf install -y epel-release && \ + dnf install -y which diffutils python3 python3-pip && \ + python3 -m venv /pytest-env + +RUN \ + source /pytest-env/bin/activate && \ + python3 -m pip install --quiet pytest pexpect + +COPY run_tests.sh / + +RUN \ + dnf install -y \ + python3-argcomplete \ + python3-dateutil \ + python3-psutil \ + python3-urllib3 \ + python3-websocket-client + +ENTRYPOINT [ "/run_tests.sh" ] diff --git a/build/dependencies_cross_platform_tests/linux/dockerfiles/run_tests.sh b/build/dependencies_cross_platform_tests/linux/dockerfiles/run_tests.sh new file mode 100755 index 0000000000..e645a2a9e8 --- /dev/null +++ b/build/dependencies_cross_platform_tests/linux/dockerfiles/run_tests.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +set -e + +if [[ "$DXPY_TEST_USING_PYENV" == "true" ]]; then + eval "$(pyenv init -)" +fi + +export DXPY_TEST_PYTHON_BIN=$(which python${DXPY_TEST_PYTHON_VERSION}) + +echo "Using $($DXPY_TEST_PYTHON_BIN --version 2>&1) (${DXPY_TEST_PYTHON_BIN})" + +if [[ -z "$DXPY_TEST_PYTHON_BIN" ]]; then + echo "Cannot determine Python executable path" + exit 1 +fi + +TMPDIR=$(mktemp -d -t dx-toolkit-XXXXXX) +cp -a /dx-toolkit $TMPDIR + +if [[ -f "/extra_requirements.txt" ]]; then + echo "Installing extra requirements" + $DXPY_TEST_PYTHON_BIN -m pip install -r /extra_requirements.txt +fi + +$DXPY_TEST_PYTHON_BIN -m pip install $TMPDIR/dx-toolkit/src/python + +if [[ "$DXPY_TEST_USING_PYENV" == "true" ]]; then + pyenv rehash +fi + +source /pytest-env/bin/activate +pytest --verbose /tests/dependencies_cross_platform_tests.py $@ diff --git a/build/dependencies_cross_platform_tests/linux/dockerfiles/ubuntu-20.04-py3-minimal.Dockerfile b/build/dependencies_cross_platform_tests/linux/dockerfiles/ubuntu-20.04-py3-minimal.Dockerfile new file mode 100644 index 0000000000..f4591bf607 --- /dev/null +++ b/build/dependencies_cross_platform_tests/linux/dockerfiles/ubuntu-20.04-py3-minimal.Dockerfile @@ -0,0 +1,18 @@ +FROM --platform=amd64 ubuntu:20.04 + +SHELL ["/bin/bash", "-c"] +ENV DEBIAN_FRONTEND=noninteractive +ENV DXPY_TEST_PYTHON_VERSION=3 + +RUN \ + apt-get update && \ + apt-get install -y python3 python3-pip python3-venv && \ + python3 -m venv /pytest-env + +RUN \ + source /pytest-env/bin/activate && \ + python3 -m pip install --quiet pytest pexpect + +COPY run_tests.sh / + +ENTRYPOINT [ "/run_tests.sh" ] diff --git a/build/dependencies_cross_platform_tests/linux/dockerfiles/ubuntu-20.04-py3-sysdeps.Dockerfile b/build/dependencies_cross_platform_tests/linux/dockerfiles/ubuntu-20.04-py3-sysdeps.Dockerfile new file mode 100644 index 0000000000..c46621cc6c --- /dev/null +++ b/build/dependencies_cross_platform_tests/linux/dockerfiles/ubuntu-20.04-py3-sysdeps.Dockerfile @@ -0,0 +1,26 @@ +FROM --platform=amd64 ubuntu:22.04 + +SHELL ["/bin/bash", "-c"] +ENV DEBIAN_FRONTEND=noninteractive +ENV DXPY_TEST_PYTHON_VERSION=3 + +RUN \ + apt-get update && \ + apt-get install -y python3 python3-pip python3-venv && \ + python3 -m venv /pytest-env + +RUN \ + source /pytest-env/bin/activate && \ + python3 -m pip install --quiet pytest pexpect + +RUN \ + apt-get install -y \ + python3-argcomplete \ + python3-dateutil \ + python3-psutil \ + python3-urllib3 \ + python3-websocket + +COPY run_tests.sh / + +ENTRYPOINT [ "/run_tests.sh" ] diff --git a/build/dependencies_cross_platform_tests/linux/dockerfiles/ubuntu-22.04-py3-minimal.Dockerfile b/build/dependencies_cross_platform_tests/linux/dockerfiles/ubuntu-22.04-py3-minimal.Dockerfile new file mode 100644 index 0000000000..6f9ca1020c --- /dev/null +++ b/build/dependencies_cross_platform_tests/linux/dockerfiles/ubuntu-22.04-py3-minimal.Dockerfile @@ -0,0 +1,18 @@ +FROM --platform=amd64 ubuntu:22.04 + +SHELL ["/bin/bash", "-c"] +ENV DEBIAN_FRONTEND=noninteractive +ENV DXPY_TEST_PYTHON_VERSION=3 + +RUN \ + apt-get update && \ + apt-get install -y python3 python3-pip python3-venv && \ + python3 -m venv /pytest-env + +RUN \ + source /pytest-env/bin/activate && \ + python3 -m pip install --quiet pytest pexpect + +COPY run_tests.sh / + +ENTRYPOINT [ "/run_tests.sh" ] diff --git a/build/dependencies_cross_platform_tests/linux/dockerfiles/ubuntu-22.04-py3-sysdeps.Dockerfile b/build/dependencies_cross_platform_tests/linux/dockerfiles/ubuntu-22.04-py3-sysdeps.Dockerfile new file mode 100644 index 0000000000..c46621cc6c --- /dev/null +++ b/build/dependencies_cross_platform_tests/linux/dockerfiles/ubuntu-22.04-py3-sysdeps.Dockerfile @@ -0,0 +1,26 @@ +FROM --platform=amd64 ubuntu:22.04 + +SHELL ["/bin/bash", "-c"] +ENV DEBIAN_FRONTEND=noninteractive +ENV DXPY_TEST_PYTHON_VERSION=3 + +RUN \ + apt-get update && \ + apt-get install -y python3 python3-pip python3-venv && \ + python3 -m venv /pytest-env + +RUN \ + source /pytest-env/bin/activate && \ + python3 -m pip install --quiet pytest pexpect + +RUN \ + apt-get install -y \ + python3-argcomplete \ + python3-dateutil \ + python3-psutil \ + python3-urllib3 \ + python3-websocket + +COPY run_tests.sh / + +ENTRYPOINT [ "/run_tests.sh" ] diff --git a/build/dependencies_cross_platform_tests/macos/prepare.sh b/build/dependencies_cross_platform_tests/macos/prepare.sh new file mode 100644 index 0000000000..5a23044331 --- /dev/null +++ b/build/dependencies_cross_platform_tests/macos/prepare.sh @@ -0,0 +1,49 @@ +#!/bin/bash + +set -e + +# Install Homebrew +/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" + +if [ -f /opt/homebrew/bin/brew ]; then + BREW_BIN="/opt/homebrew/bin/brew" +else + BREW_BIN="/usr/local/bin/brew" +fi + +if [ "$SHELL" == "/bin/bash" ]; then + echo "eval \"\$($BREW_BIN shellenv)\"" >> ~/.bashrc +else + echo "eval \"\$($BREW_BIN shellenv)\"" >> ~/.zprofile +fi + +# Activate Homebrew +eval "$($BREW_BIN shellenv)" + +# Install pyenv +brew install pyenv + +# Check if Python 3 is available +if [ -f /usr/bin/python3 ]; then + PYTHON3_BIN="/usr/bin/python3" +else + brew install python@3.11 + if [ -f /opt/homebrew/bin ]; then + PYTHON3_BIN="/opt/homebrew/bin/python3.11" + else + PYTHON3_BIN="/usr/local/opt/python@3.11/bin/python3.11" + fi +fi + +# Install tests dependencies +$PYTHON3_BIN -m pip install pytest pexpect + +# Download official installation packages +mkdir python_official +pushd python_official +curl -f -O https://www.python.org/ftp/python/3.12.1/python-3.12.1-macos11.pkg +curl -f -O https://www.python.org/ftp/python/3.11.3/python-3.11.3-macos11.pkg +curl -f -O https://www.python.org/ftp/python/3.10.11/python-3.10.11-macos11.pkg +curl -f -O https://www.python.org/ftp/python/3.9.13/python-3.9.13-macos11.pkg +curl -f -O https://www.python.org/ftp/python/3.8.10/python-3.8.10-macos11.pkg +popd diff --git a/build/dependencies_cross_platform_tests/macos/run_tests.sh b/build/dependencies_cross_platform_tests/macos/run_tests.sh new file mode 100755 index 0000000000..8485970dcb --- /dev/null +++ b/build/dependencies_cross_platform_tests/macos/run_tests.sh @@ -0,0 +1,55 @@ +#!/bin/bash + +set -e + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +DX_TOOLKIT_DIR=$1 +shift +TESTENV_DIR=$1 +shift + +if [[ "$DXPY_TEST_USING_PYENV" == "true" ]]; then + eval "$(pyenv init -)" +fi + +if [[ -z "$DXPY_TEST_BASE_PYTHON_BIN" ]]; then + DXPY_TEST_BASE_PYTHON_BIN=$(which python${DXPY_TEST_PYTHON_VERSION}) +fi + +echo "Using $($DXPY_TEST_BASE_PYTHON_BIN --version 2>&1) (${DXPY_TEST_BASE_PYTHON_BIN})" + +$DXPY_TEST_BASE_PYTHON_BIN -m venv $TESTENV_DIR + +source $TESTENV_DIR/bin/activate + +export DXPY_TEST_PYTHON_BIN=$(which "python${DXPY_TEST_PYTHON_VERSION}") + +if [[ -z "$DXPY_TEST_PYTHON_BIN" ]]; then + echo "Cannot determine Python executable path" + exit 1 +fi + +PYTHON_VERSION=$($DXPY_TEST_PYTHON_BIN --version 2>&1) + +echo "Using venv with $PYTHON_VERSION ($DXPY_TEST_PYTHON_BIN)" + +if [[ ! -z "$DXPY_TEST_EXTRA_REQUIREMENTS" ]]; then + $DXPY_TEST_PYTHON_BIN -m pip install -r $DXPY_TEST_EXTRA_REQUIREMENTS +fi +$DXPY_TEST_PYTHON_BIN -m pip install $DX_TOOLKIT_DIR + +hash -r + +# We want to use system Python for running pytest +if [ -f /usr/bin/python3 ]; then + MAIN_PYTHON_BIN="/usr/bin/python3" +else + if [ -f /opt/homebrew/bin ]; then + MAIN_PYTHON_BIN="/opt/homebrew/bin/python3.11" + else + MAIN_PYTHON_BIN="/usr/local/opt/python@3.11/bin/python3.11" + fi +fi + +$MAIN_PYTHON_BIN -m pytest --verbose ${SCRIPT_DIR}/../dependencies_cross_platform_tests.py $@ \ No newline at end of file diff --git a/build/dependencies_cross_platform_tests/run_linux.py b/build/dependencies_cross_platform_tests/run_linux.py new file mode 100755 index 0000000000..c66419e4eb --- /dev/null +++ b/build/dependencies_cross_platform_tests/run_linux.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 + +import argparse +import docker +import logging +import random +import re +import sys +import tempfile +import time + +from concurrent.futures import ThreadPoolExecutor +from dataclasses import dataclass, field +from pathlib import Path +from typing import Dict, List, Optional + +from utils import EXIT_SUCCESS, init_base_argparser, init_logging, parse_common_args, extract_failed_tests, make_execution_summary, filter_pyenvs, Matcher + +ROOT_DIR = Path(__file__).parent.absolute() +DOCKERFILES_DIR = ROOT_DIR / "linux" / "dockerfiles" +PYENVS = [re.sub("\\.Dockerfile$", "", f.name) for f in DOCKERFILES_DIR.iterdir() if f.name.endswith(".Dockerfile")] + +EXIT_IMAGE_BUILD_FAILED = 1 +EXIT_TEST_EXECUTION_FAILED = 2 + +client = docker.from_env() + + +class TestExecutionFailed(Exception): + + def __init__(self, msg, failed_tests): + super().__init__(msg) + self.failed_tests = failed_tests + + +@dataclass +class DXPYTestsRunner: + dx_toolkit: Path + token: str + env: str = "stg" + pyenv_filters_inclusive: Optional[List[Matcher]] = None + pyenv_filters_exclusive: Optional[List[Matcher]] = None + extra_requirements: Optional[List[str]] = None + pytest_args: Optional[str] = None + report: Optional[str] = None + logs_dir: str = Path("logs") + workers: int = 1 + retries: int = 1 + print_logs: bool = False + print_failed_logs: bool = False + keep_images: bool = False + pull: bool = True + _test_results: Dict[str, Dict] = field(default_factory=dict, init=False) + + def run(self): + pyenvs = filter_pyenvs(PYENVS, self.pyenv_filters_inclusive, self.pyenv_filters_exclusive) + + logging.info("Python environments: " + ", ".join(pyenvs)) + + with ThreadPoolExecutor(max_workers=self.workers) as executor: + for pyenv in pyenvs: + executor.submit(self._run_pyenv, pyenv) + executor.shutdown(wait=True) + + exit_code = make_execution_summary(self._test_results, self.report) + return exit_code + + def _store_test_results(self, pyenv, code, failed_tests=None): + self._test_results[pyenv] = { + "code": code, + "failed_tests": failed_tests + } + with open(self.logs_dir / f"{pyenv}.status", 'w') as fh: + fh.write(f"{code}\n") + + def _run_pyenv(self, pyenv): + try: + image = self._build_image(pyenv) + except: + logging.exception(f"[{pyenv}] Unable to build docker image") + self._store_test_results(pyenv, EXIT_IMAGE_BUILD_FAILED) + return + + try: + for i in range(1, self.retries + 1): + try: + self._run_tests(pyenv, image) + break + except: + if i == self.retries: + raise + logging.exception(f"[{pyenv}] Tests execution failed (try {i})") + time.sleep(random.randrange(70, 90)) + except Exception as e: + logging.exception(f"[{pyenv}] Tests execution failed.") + self._store_test_results(pyenv, EXIT_TEST_EXECUTION_FAILED, e.failed_tests if isinstance(e, TestExecutionFailed) else None) + return + finally: + if not self.keep_images: + try: + client.images.remove(image.id) + except: + logging.exception(f"[{pyenv}] Unable to remove docker image") + + self._store_test_results(pyenv, EXIT_SUCCESS) + + def _build_image(self, pyenv): + logging.info(f"[{pyenv}] Building Docker image") + dockerfile = f"{pyenv}.Dockerfile" + try: + image, log_stream = client.images.build(path=str(DOCKERFILES_DIR), tag=f"dxpy-testenv:{pyenv}", dockerfile=dockerfile, pull=self.pull, rm=True) + with open(self.logs_dir / f"{pyenv}_build.log", 'w') as fh: + for msg in log_stream: + if "stream" in msg: + fh.write(msg["stream"]) + except: + logging.info(f"[{pyenv}] Docker build command failed and no logs were produced. For manual debugging, run 'docker build -f {DOCKERFILES_DIR}/{dockerfile} {DOCKERFILES_DIR}'") + raise + logging.info(f"[{pyenv}] Docker image successfully built") + return image + + def _run_tests(self, pyenv, image): + with tempfile.TemporaryDirectory() as wd: + logging.info(f"[{pyenv}] Running tests (temporary dir: '{wd}')") + wd = Path(wd) + tests_log: Path = self.logs_dir / f"{pyenv}_test.log" + volumes = { + ROOT_DIR: {'bind': '/tests', 'mode': 'ro'}, + str(self.dx_toolkit): {'bind': '/dx-toolkit/', 'mode': 'ro'} + } + + if self.extra_requirements and len(self.extra_requirements) > 0: + extra_requirements_file = wd / "extra_requirements.txt" + with open(extra_requirements_file, 'w') as fh: + fh.writelines(self.extra_requirements) + volumes[extra_requirements_file] = {'bind': '/extra-requirements.txt', 'mode': 'ro'} + + command = " ".join(self.pytest_args) if self.pytest_args is not None and len(self.pytest_args) > 0 else None + container = client.containers.run( + image.id, + command=command, + volumes=volumes, + environment={ + "DXPY_TEST_TOKEN": self.token, + "DXPY_TEST_ENV": self.env, + }, + detach=True + ) + + with open(tests_log, 'w') as fh: + for msg in container.logs(stream=True, follow=True): + fh.write(msg.decode()) + fh.flush() + + status = container.wait()["StatusCode"] + + try: + container.remove() + except: + logging.exception(f"[{pyenv}] Cannot remove container") + + if status != 0: + logging.error(f"[{pyenv}] Container exitted with non-zero return code. See log for console output: {tests_log.absolute()}") + if self.print_logs or self.print_failed_logs: + self._print_log(pyenv, tests_log) + raise TestExecutionFailed("Docker container exited with non-zero code", extract_failed_tests(tests_log)) + + logging.info(f"[{pyenv}] Tests execution successful") + if self.print_logs: + self._print_log(pyenv, tests_log) + + def _print_log(self, pyenv, log): + with open(log) as fh: + logging.info(f"[{pyenv}] Tests execution log:\n{fh.read()}") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + + init_base_argparser(parser) + + parser.add_argument("-k", "--keep-images", action="store_true", help="Do not delete docker images") + parser.add_argument("--no-docker-pull", action="store_true", help="Do NOT pull base images from Docker hub on build") + + args = parser.parse_args() + + init_logging(args.verbose) + + ret = DXPYTestsRunner( + **parse_common_args(args), + keep_images=args.keep_images, + pull=not args.no_docker_pull, + ).run() + sys.exit(ret) diff --git a/build/dependencies_cross_platform_tests/run_macos.py b/build/dependencies_cross_platform_tests/run_macos.py new file mode 100644 index 0000000000..e501e155af --- /dev/null +++ b/build/dependencies_cross_platform_tests/run_macos.py @@ -0,0 +1,208 @@ +import argparse +import logging +import os +import platform +import random +import shutil +import subprocess +import sys +import tempfile +import time + +from concurrent.futures import ThreadPoolExecutor +from dataclasses import dataclass, field +from pathlib import Path +from typing import Dict, List, Optional + +from utils import EXIT_SUCCESS, init_base_argparser, init_logging, parse_common_args, extract_failed_tests, make_execution_summary, filter_pyenvs, Matcher + +ROOT_DIR = Path(__file__).parent.absolute() + +PYENVS = \ + ["system"] + \ + [f"official-{p}" for p in ("3.8", "3.9", "3.10", "3.11", "3.12")] + \ + [f"pyenv-{p}" for p in ("3.8", "3.9", "3.10", "3.11", "3.12")] + \ + [f"brew-{p}" for p in ("3.8", "3.9", "3.10", "3.11", "3.12")] + +EXIT_TEST_EXECUTION_FAILED = 1 + + +@dataclass +class PyEnv: + name: str + _env: str = field(init=False, default=None) + _ver: str = field(init=False, default=None) + + def __post_init__(self): + p = self.name.split("-") + if len(p) > 1: + self._env = p[0] + self._ver = p[1] + else: + self._env = p[0] + if p[0] == "system": + if not Path("/usr/bin/python3").is_file(): + raise Exception("Python 2.7 is no longer supported") + self._ver = "3" + + @property + def is_system(self): + return self.name == "system" + + @property + def is_official(self): + return self._env == "official" + + @property + def is_brew(self): + return self._env == "brew" + + @property + def is_pyenv(self): + return self._env == "pyenv" + + @property + def python_version(self): + return self._ver + + +@dataclass +class DXPYTestsRunner: + dx_toolkit: Path + token: str + env: str = "stg" + pyenv_filters_inclusive: Optional[List[Matcher]] = None + pyenv_filters_exclusive: Optional[List[Matcher]] = None + extra_requirements: Optional[List[str]] = None + pytest_args: Optional[str] = None + report: Optional[str] = None + logs_dir: str = Path("logs") + workers: int = 1 + retries: int = 1 + print_logs: bool = False + print_failed_logs: bool = False + _macos_version: float = float('.'.join(platform.mac_ver()[0].split('.')[:2])) + _brew_in_opt: bool = Path("/opt/homebrew").is_dir() + _test_results: Dict[str, Dict] = field(default_factory=dict, init=False) + + def __post_init__(self): + self.dx_toolkit = self.dx_toolkit.absolute() + self.logs_dir = self.logs_dir.absolute() + logging.debug(f"Detected MacOS version {self._macos_version}") + + def run(self): + pyenvs = filter_pyenvs(PYENVS, self.pyenv_filters_inclusive, self.pyenv_filters_exclusive) + + logging.info("Python environments: " + ", ".join(pyenvs)) + + for pyenv in pyenvs: + p = PyEnv(pyenv) + if p.is_pyenv: + logging.info(f"[{pyenv}] Installing Python {p.python_version} using pyenv") + with open(self.logs_dir / f"{pyenv}_install.log", 'w') as fh: + subprocess.run(["pyenv", "install", "--skip-existing", p.python_version], check=True, stdout=fh, stderr=subprocess.STDOUT, text=True) + elif p.is_brew: + logging.info(f"[{pyenv}] Installing Python {p.python_version} using brew") + with open(self.logs_dir / f"{pyenv}_install.log", 'w') as fh: + subprocess.run(["brew", "install", "--overwrite", "--force", f"python@{p.python_version}"], check=True, stdout=fh, stderr=subprocess.STDOUT, text=True) + + with ThreadPoolExecutor(max_workers=self.workers) as executor: + for pyenv in pyenvs: + executor.submit(self._run_pyenv, pyenv) + executor.shutdown(wait=True) + + exit_code = make_execution_summary(self._test_results, self.report) + return exit_code + + def _store_test_results(self, pyenv, code, failed_tests=None): + self._test_results[pyenv] = { + "code": code, + "failed_tests": failed_tests + } + with open(self.logs_dir / f"{pyenv}.status", 'w') as fh: + fh.write(f"{code}\n") + + def _run_pyenv(self, pyenv: str): + try: + for i in range(1, self.retries + 1): + try: + self._do_run_pyenv(pyenv) + break + except: + if i == self.retries: + raise + logging.exception(f"[{pyenv}] Tests execution failed (try {i})") + time.sleep(random.randrange(70, 90)) + except: + logging.exception(f"[{pyenv} Failed running tests") + self._store_test_results(pyenv, EXIT_TEST_EXECUTION_FAILED) + + def _do_run_pyenv(self, pyenv: str): + p = PyEnv(pyenv) + with tempfile.TemporaryDirectory() as wd: + logging.info(f"[{pyenv}] Preparing for test execution (temporary dir: '{wd}')") + wd = Path(wd) + + dx_python_root = wd / "python" + shutil.copytree(self.dx_toolkit / "src" / "python", dx_python_root) + + env = os.environ.copy() + + if p.is_system: + env["DXPY_TEST_BASE_PYTHON_BIN"] = f"/usr/bin/python3" + elif p.is_official: + env["DXPY_TEST_BASE_PYTHON_BIN"] = str(Path("/Library") / "Frameworks" / "Python.framework" / "Versions" / p.python_version / "bin" / f"python{p.python_version}") + elif p.is_pyenv: + subprocess.run(f"pyenv local {p.python_version}", cwd=wd, check=True, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) + env["DXPY_TEST_USING_PYENV"] = "true" + elif p.is_brew: + if self._brew_in_opt: + env["DXPY_TEST_BASE_PYTHON_BIN"] = str(Path("/opt") / "homebrew" / "bin" / f"python{p.python_version}") + else: + env["DXPY_TEST_BASE_PYTHON_BIN"] = str(Path("/usr") / "local" / "opt" / f"python@{p.python_version}" / "bin" / f"python{p.python_version}") + + if self.extra_requirements and len(self.extra_requirements) > 0: + extra_requirements_file = wd / "extra_requirements.txt" + with open(extra_requirements_file, 'w') as fh: + fh.writelines(self.extra_requirements) + env["DXPY_TEST_EXTRA_REQUIREMENTS"] = str(extra_requirements_file) + + env_dir = wd / "testenv" + + logging.info(f"[{pyenv}] Running tests") + env["DXPY_TEST_TOKEN"] = self.token + env["DXPY_TEST_PYTHON_VERSION"] = p.python_version[0] + env["DX_USER_CONF_DIR"] = str((wd / ".dnanexus_config").absolute()) + tests_log: Path = self.logs_dir / f"{pyenv}_test.log" + with open(tests_log, 'w') as fh: + res = subprocess.run([ROOT_DIR / "macos" / "run_tests.sh", dx_python_root, env_dir] + (self.pytest_args or []), env=env, cwd=wd, stdout=fh, stderr=subprocess.STDOUT, stdin=subprocess.DEVNULL) + if res.returncode != 0: + logging.error(f"[{pyenv}] Tests exited with non-zero code. See log for console output: {tests_log.absolute()}") + if self.print_logs or self.print_failed_logs: + self._print_log(pyenv, tests_log) + self._store_test_results(pyenv, EXIT_TEST_EXECUTION_FAILED, extract_failed_tests(tests_log)) + return + + logging.info(f"[{pyenv}] Tests execution successful") + if self.print_logs: + self._print_log(pyenv, tests_log) + self._store_test_results(pyenv, EXIT_SUCCESS) + + def _print_log(self, pyenv, log): + with open(log) as fh: + logging.info(f"[{pyenv}] Tests execution log:\n{fh.read()}") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + + init_base_argparser(parser) + + args = parser.parse_args() + + init_logging(args.verbose) + + ret = DXPYTestsRunner( + **parse_common_args(args) + ).run() + sys.exit(ret) diff --git a/build/dependencies_cross_platform_tests/run_macos.sh b/build/dependencies_cross_platform_tests/run_macos.sh new file mode 100755 index 0000000000..63b321ce1b --- /dev/null +++ b/build/dependencies_cross_platform_tests/run_macos.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +set -e + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +if [ -f /usr/bin/python3 ]; then + PYTHON3_BIN="/usr/bin/python3" +else + if [ -f /opt/homebrew/bin ]; then + PYTHON3_BIN="/opt/homebrew/bin/python3.11" + else + PYTHON3_BIN="/usr/local/opt/python@3.11/bin/python3.11" + fi +fi + +$PYTHON3_BIN $SCRIPT_DIR/run_macos.py $@ diff --git a/build/dependencies_cross_platform_tests/run_windows.py b/build/dependencies_cross_platform_tests/run_windows.py new file mode 100644 index 0000000000..1fd5fef772 --- /dev/null +++ b/build/dependencies_cross_platform_tests/run_windows.py @@ -0,0 +1,205 @@ +import argparse +import logging +import os +import random +import shutil +import subprocess +import sys +import tempfile +import time + +from concurrent.futures import ThreadPoolExecutor +from dataclasses import dataclass, field +from pathlib import Path +from typing import Dict, List, Optional + +from utils import EXIT_SUCCESS, init_base_argparser, init_logging, parse_common_args, extract_failed_tests, make_execution_summary, filter_pyenvs, Matcher + +ROOT_DIR = Path(__file__).parent.absolute() + +_PYTHON_VERSIONS = ["3.8", "3.9", "3.10", "3.11", "3.12"] +PYENVS = [f"official-{p}" for p in _PYTHON_VERSIONS] + +EXIT_TEST_EXECUTION_FAILED = 1 + + +@dataclass +class DXPYTestsRunner: + dx_toolkit: Path + token: str + env: str = "stg" + pyenv_filters_inclusive: Optional[List[Matcher]] = None + pyenv_filters_exclusive: Optional[List[Matcher]] = None + extra_requirements: Optional[List[str]] = None + pytest_args: Optional[str] = None + report: Optional[str] = None + logs_dir: str = Path("logs") + workers: int = 1 + retries: int = 1 + print_logs: bool = False + print_failed_logs: bool = False + pytest_python: str = "python3.11" + skip_interactive_tests: bool = False + gha_force_python: Optional[str] = None + _test_results: Dict[str, Dict] = field(default_factory=dict, init=False) + + def __post_init__(self): + if self.workers > 1: + raise ValueError("Windows currently does not support multiple workers") + + def run(self): + if (self.pyenv_filters_inclusive is not None or self.pyenv_filters_exclusive is not None) and self.gha_force_python: + raise AssertionError("Cannot use filters with enforced Python!") + + if self.gha_force_python: + pyenvs = ["gha"] + else: + pyenvs = filter_pyenvs(PYENVS, self.pyenv_filters_inclusive, self.pyenv_filters_exclusive) + + logging.info("Python environments: " + ", ".join(pyenvs)) + + with ThreadPoolExecutor(max_workers=self.workers) as executor: + for pyenv in pyenvs: + executor.submit(self._run_pyenv, pyenv) + executor.shutdown(wait=True) + + exit_code = make_execution_summary(self._test_results, self.report) + return exit_code + + def _store_test_results(self, pyenv, code, failed_tests=None): + self._test_results[pyenv] = { + "code": code, + "failed_tests": failed_tests + } + with open(self.logs_dir / f"{pyenv}.status", 'w') as fh: + fh.write(f"{code}\n") + + def _run_pyenv(self, pyenv: str): + try: + for i in range(1, self.retries + 1): + try: + self._do_run_pyenv(pyenv) + break + except: + if i == self.retries: + raise + logging.exception(f"[{pyenv}] Tests execution failed (try {i})") + time.sleep(random.randrange(70, 90)) + except: + logging.exception(f"[{pyenv}] Failed running tests") + self._store_test_results(pyenv, EXIT_TEST_EXECUTION_FAILED) + + def _do_run_pyenv(self, pyenv: str): + with tempfile.TemporaryDirectory() as wd: + logging.info(f"[{pyenv}] Running tests (temporary dir: '{wd}')") + wd = Path(wd) + if self.gha_force_python: + python_bin = self.gha_force_python + else: + python_bin = Path("C:\\") / f"Python{pyenv.split('-')[-1].replace('.', '')}" / "python.exe" + python_version = subprocess.run([python_bin, "--version"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, check=True).stdout.split(" ")[1][0] + logging.debug(f"[{pyenv}] Running on Python {python_version}") + dx_python_root = wd / "python" + shutil.copytree(self.dx_toolkit / "src" / "python", dx_python_root) + env_dir = wd / "testenv" + try: + subprocess.run([python_bin, "-m", "venv", env_dir], check=True) + except subprocess.CalledProcessError as e: + logging.error(f"[{pyenv}] Unable to create virtual environment for test execution\n{e.output}") + self._store_test_results(pyenv, EXIT_TEST_EXECUTION_FAILED) + return + + pytest_python = self.pytest_python or sys.executable + pytest_args =' '.join(self.pytest_args) if self.pytest_args else "" + script = wd / "run_tests.ps1" + with open(script, 'w') as fh: + fh.write(f""" +$ErrorActionPreference = 'stop' +$Env:PSModulePath = $Env:PSModulePath + ";$env:UserProfile\\Documents\\PowerShell\\Modules" +{env_dir}\\Scripts\\activate.ps1 + +echo "Base Python version:" +python --version + +echo "Pytest Python path: {pytest_python}" +echo "Pytest Python version:" +{pytest_python} --version +""") + if self.extra_requirements and len(self.extra_requirements) > 0: + extra_requirements_file = wd / "extra_requirements.txt" + with open(extra_requirements_file, 'w') as fh: + fh.writelines(self.extra_requirements) + fh.write(f""" +python -m pip install -r {extra_requirements_file} + +If($LastExitCode -ne 0) +{{ + Exit 1 +}} + +""") + + fh.write(f""" +python -m pip install {dx_python_root} + +If($LastExitCode -ne 0) +{{ + Exit 1 +}} + +{pytest_python} -m pytest -v {pytest_args} {(ROOT_DIR / 'dependencies_cross_platform_tests.py').absolute()} + +If($LastExitCode -ne 0) +{{ + Exit 1 +}} + +Exit 0 +""") + + tests_log: Path = self.logs_dir / f"{pyenv}_test.log" + env = os.environ.copy() + env["DXPY_TEST_TOKEN"] = self.token + env["DXPY_TEST_PYTHON_BIN"] = str(env_dir / "Scripts" / "python") + env["DXPY_TEST_PYTHON_VERSION"] = python_version + env["DXPY_TEST_SKIP_INTERACTIVE"] = str(self.skip_interactive_tests) + env["DX_USER_CONF_DIR"] = str((wd / ".dnanexus_config").absolute()) + with open(tests_log, 'w') as fh: + res = subprocess.run(["powershell", "-ExecutionPolicy", "Unrestricted", script], env=env, stdout=fh, stderr=subprocess.STDOUT, stdin=subprocess.DEVNULL) + if res.returncode != 0: + logging.error(f"[{pyenv}] Tests exited with non-zero code. See log for console output: {tests_log.absolute()}") + if self.print_logs or self.print_failed_logs: + self._print_log(pyenv, tests_log) + self._store_test_results(pyenv, EXIT_TEST_EXECUTION_FAILED, extract_failed_tests(tests_log)) + return + + logging.info(f"[{pyenv}] Tests execution successful") + if self.print_logs: + self._print_log(pyenv, tests_log) + self._store_test_results(pyenv, EXIT_SUCCESS) + + def _print_log(self, pyenv, log): + with open(log) as fh: + logging.info(f"[{pyenv}] Tests execution log:\n{fh.read()}") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + + init_base_argparser(parser) + + parser.add_argument("--pytest-python", help="Binary used for executing Pytest. By default it uses the same Python as for executing this script.") + parser.add_argument("--skip-interactive-tests", action="store_true", help="Skip interactive tests") + parser.add_argument("--gha-force-python", help="GitHub Actions: Run only artificial pyenv with specified Python binary") + + args = parser.parse_args() + + init_logging(args.verbose) + + ret = DXPYTestsRunner( + **parse_common_args(args), + pytest_python=args.pytest_python, + skip_interactive_tests=args.skip_interactive_tests, + gha_force_python=args.gha_force_python, + ).run() + sys.exit(ret) diff --git a/build/dependencies_cross_platform_tests/utils.py b/build/dependencies_cross_platform_tests/utils.py new file mode 100644 index 0000000000..ea549613e8 --- /dev/null +++ b/build/dependencies_cross_platform_tests/utils.py @@ -0,0 +1,186 @@ +import json +import logging +import re +import subprocess +import sys + +from dataclasses import dataclass +from pathlib import Path +from typing import Dict, List + +EXIT_SUCCESS = 0 + + +class Matcher: + + def match(self, pyenv: str) -> bool: + pass + + +@dataclass +class ExactMatcher(Matcher): + pattern: re.Pattern + + def match(self, pyenv: str) -> bool: + return pyenv == self.pattern + + +@dataclass +class WildcardMatcher(Matcher): + pattern: List[str] + + def __init__(self, pattern: str): + self.pattern = pattern.split("-") + for part in self.pattern: + if "*" in part and part != "*": + raise ValueError("Wild-cards can be used only for whole sections of pyenv names!") + + def match(self, pyenv: str) -> bool: + pyenv_parts = pyenv.split("-") + if len(self.pattern) != len(pyenv_parts): + return False + for i in range(len(self.pattern)): + if self.pattern[i] != "*" and self.pattern[i] != pyenv_parts[i]: + return False + return True + + +@dataclass +class RegexpMatcher(Matcher): + pattern: re.Pattern + + def __init__(self, pattern: str): + self.pattern = re.compile(pattern) + + def match(self, pyenv: str) -> bool: + return self.pattern.match(pyenv) + + +def extract_failed_tests(log: Path) -> List[str]: + failed_tests = [] + in_block = False + with open(log) as fh: + for line in fh: + line = line.strip() + if line == "=========================== short test summary info ============================": + in_block = True + elif in_block and line.startswith("======= "): + in_block = False + break + elif in_block and line.startswith("FAILED "): + tmp = line[line.find("::") + 2:] + failed_tests.append(tmp[:tmp.find(" - ")]) + return failed_tests if len(failed_tests) > 0 else None + + +def make_execution_summary(test_results: Dict[str, Dict], report_file: Path) -> int: + logging.info("Test execution summary (%d/%d succeeded):", len([k for k, v in test_results.items() if v["code"] == EXIT_SUCCESS]), len(test_results)) + for pyenv in sorted(test_results.keys()): + code = test_results[pyenv]["code"] + msg = f" {'[ SUCCESS ]' if code == EXIT_SUCCESS else '[ FAIL ]'} {pyenv} (exit code: {code}" + if test_results[pyenv]["failed_tests"] is not None: + msg += ", failed tests: " + ", ".join(test_results[pyenv]["failed_tests"]) + msg += ")" + logging.info(msg) + + if report_file: + with open(report_file, 'w') as fh: + json.dump(test_results, fh) + + return 0 if all(map(lambda x: x["code"] == EXIT_SUCCESS, test_results.values())) else 1 + + +def init_logging(verbose: bool) -> None: + logging.basicConfig(format='%(asctime)s - %(levelname)s - %(message)s', level=logging.DEBUG if verbose else logging.INFO) + + +def init_base_argparser(parser) -> None: + parser.add_argument("-d", "--dx-toolkit", required=True, help="Path to dx-toolkit source dir") + parser.add_argument("-b", "--dx-toolkit-ref", help="dx-toolkit git reference (branch, commit, etc.) to test") + parser.add_argument("--pull", action="store_true", help="Pull dx-toolkit repo before running the tests") + parser.add_argument("-t", "--token", required=True, help="API token") + parser.add_argument("-l", "--logs", default="./logs", help="Directory where to store logs") + parser.add_argument("-e", "--env", choices=["stg", "prod"], default="stg", help="Platform") + parser.add_argument("-w", "--workers", type=int, default=1, help="Number of workers (i.e. parallelly running tests)") + parser.add_argument("-a", "--retries", type=int, default=1, help="Number of retries for failed execution to eliminate network issues") + parser.add_argument("-r", "--extra-requirement", dest="extra_requirements", action="append", help="Explicitly install this library to the virtual environment before installing dx-toolkit. Format is the same as requirements.txt file.") + parser.add_argument("-o", "--report", help="Save status report to file in JSON format") + pyenv_group = parser.add_mutually_exclusive_group() + pyenv_group.add_argument("-f", "--pyenv-filter", dest="pyenv_filters", action="append", help="Run only in environments matching the filters. Supported are wild-card character '*' (e.g. ubuntu-*-py3-*) or regular expression (when using --regexp-filters flag). Exclusive filters can be using when prefixed with '!'.") + pyenv_group.add_argument("--run-failed", metavar="REPORT", help="Load report file and run only failed environments") + parser.add_argument("--print-logs", action="store_true", help="Print logs of all executions") + parser.add_argument("--print-failed-logs", action="store_true", help="Print logs of failed executions") + parser.add_argument("--regexp-filters", action="store_true", help="Apply filters as a fully-featured regular expressions") + parser.add_argument("--pytest-matching", help="Run only tests matching given substring expression (the same as pytest -k EXPRESSION)") + parser.add_argument("--pytest-exitfirst", action="store_true", help="Exit pytest instantly on first error or failed test (the same as pytest -x)") + parser.add_argument("--pytest-tee", action="store_true", help="Also print stdout/stderr during execution (the same as pytest --capture=tee-sys)") + parser.add_argument("-v", "--verbose", action="store_true", help="Verbose logging") + + +def filter_pyenvs(all_pyenvs, filters_inclusive, filters_exclusive): + pyenvs = all_pyenvs + if filters_inclusive is not None and len(filters_inclusive) > 0: + pyenvs = [p for p in pyenvs if any(map(lambda x: x.match(p), filters_inclusive))] + if filters_exclusive is not None and len(filters_exclusive) > 0: + pyenvs = [p for p in pyenvs if all(map(lambda x: not x.match(p), filters_exclusive))] + + pyenvs.sort() + + return pyenvs + + +def parse_common_args(args) -> dict: + pytest_args = [] + if args.pytest_matching: + pytest_args += ["-k", args.pytest_matching] + if args.pytest_exitfirst: + pytest_args.append("-x") + if args.pytest_tee: + pytest_args += ["--capture", "tee-sys"] + + pyenv_filters_inclusive = None + pyenv_filters_exclusive = None + MatcherClass = RegexpMatcher if args.regexp_filters else WildcardMatcher + if args.run_failed: + with open(args.run_failed) as fh: + pyenv_filters_inclusive = [ExactMatcher(k) for k, v in json.load(fh).items() if v != 0] + elif args.pyenv_filters: + pyenv_filters_inclusive = [MatcherClass(f) for f in args.pyenv_filters if f[0] != "!"] + pyenv_filters_exclusive = [MatcherClass(f[1:]) for f in args.pyenv_filters if f[0] == "!"] + + if args.pull: + logging.debug("Pulling dx-toolkit git repository") + try: + subprocess.run(["git", "pull"], cwd=args.dx_toolkit, check=True, capture_output=True) + except: + logging.exception("Unable to pull dx-toolkit git repo") + sys.exit(1) + + if args.dx_toolkit_ref: + logging.debug(f"Checking out dx-toolkit git reference '{args.dx_toolkit_ref}'") + try: + subprocess.run(["git", "checkout", args.dx_toolkit_ref], cwd=args.dx_toolkit, check=True, capture_output=True) + except: + logging.exception("Unable to checkout dx-toolkit git reference") + sys.exit(1) + + logs_dir = Path(args.logs) + if not logs_dir.is_dir(): + logging.debug("Logs directory does not exist. Creating...") + logs_dir.mkdir() + + return dict( + dx_toolkit=Path(args.dx_toolkit), + token=args.token, + env=args.env, + pyenv_filters_inclusive=pyenv_filters_inclusive, + pyenv_filters_exclusive=pyenv_filters_exclusive, + extra_requirements=args.extra_requirements, + pytest_args=pytest_args, + report=args.report, + logs_dir=logs_dir, + print_logs=args.print_logs, + print_failed_logs=args.print_failed_logs, + workers=args.workers, + retries=args.retries, + ) diff --git a/build/dependencies_cross_platform_tests/windows/prepare.ps1 b/build/dependencies_cross_platform_tests/windows/prepare.ps1 new file mode 100644 index 0000000000..99da3d483a --- /dev/null +++ b/build/dependencies_cross_platform_tests/windows/prepare.ps1 @@ -0,0 +1,31 @@ +# Install choco +Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')) + +# Enable script execution +#Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy Unrestricted + +# Install enhanced PowerShell Await plugin +$awaitModuleDir = "$env:UserProfile\Documents\PowerShell\Modules\Await" +$destination = "await.zip" +Invoke-RestMethod -Uri https://github.com/wormsik/await/archive/refs/heads/master.zip -OutFile $destination +New-Item -Type Directory -Force $awaitModuleDir +Expand-Archive -Path $destination -DestinationPath $awaitModuleDir +Rename-Item "$awaitModuleDir\await-master" "0.9" +Remove-Item "$destination" + +# Install Windows terminal +choco install --yes --no-progress microsoft-windows-terminal + +# Install Pythons +choco install --yes --no-progress python38 +choco install --yes --no-progress python39 +choco install --yes --no-progress python310 +choco install --yes --no-progress python311 +choco install --yes --no-progress python312 +#choco install --yes miniconda3 + +# Install pytest +python3.11 -m pip install pytest + +# Refresh env vars +refreshenv diff --git a/build/dependencies_cross_platform_tests/windows/test_dx_app_wizard_interactive.ps1 b/build/dependencies_cross_platform_tests/windows/test_dx_app_wizard_interactive.ps1 new file mode 100644 index 0000000000..716bfdd378 --- /dev/null +++ b/build/dependencies_cross_platform_tests/windows/test_dx_app_wizard_interactive.ps1 @@ -0,0 +1,67 @@ +$ErrorActionPreference = 'stop' + +Import-Module Await + +Start-AwaitSession + +Send-AwaitCommand "dx-app-wizard" +Start-Sleep 1 +Wait-AwaitResponse "App Name:" +Send-AwaitCommand "test_applet" +Start-Sleep 1 +Wait-AwaitResponse "Title []:" +Send-AwaitCommand "title" +Start-Sleep 1 +Wait-AwaitResponse "Summary []:" +Send-AwaitCommand "summary" +Start-Sleep 1 +Wait-AwaitResponse "Version [0.0.1]:" +Send-AwaitCommand "0.0.1" +Start-Sleep 1 +Wait-AwaitResponse "1st input name ( to finish):" +Send-AwaitCommand "inp1" +Start-Sleep 1 +Wait-AwaitResponse "Label (optional human-readable name) []:" +Send-AwaitCommand "{ENTER}" +Start-Sleep 1 +Wait-AwaitResponse "Choose a class ( twice for choices):" +Send-AwaitCommand "{TAB}" -NoNewLine +Start-Sleep 1 +Wait-AwaitResponse "int string" +Send-AwaitCommand "float" +Start-Sleep 1 +Wait-AwaitResponse "This is an optional parameter [y/n]:" +Send-AwaitCommand "n" +Start-Sleep 1 +Wait-AwaitResponse "2nd input name ( to finish):" +Send-AwaitCommand "{ENTER}" -NoNewLine +Start-Sleep 1 +Wait-AwaitResponse "1st output name ( to finish):" +Send-AwaitCommand "out1" +Start-Sleep 1 +Wait-AwaitResponse "Label (optional human-readable name) []:" +Send-AwaitCommand " " +Start-Sleep 1 +Wait-AwaitResponse "Choose a class ( twice for choices):" +Send-AwaitCommand "{TAB}" -NoNewLine +Start-Sleep 1 +Wait-AwaitResponse "int string" +Send-AwaitCommand "float" +Start-Sleep 1 +Wait-AwaitResponse "2nd output name ( to finish):" +Send-AwaitCommand "{ENTER}" -NoNewLine +Start-Sleep 1 +Wait-AwaitResponse "Timeout policy [48h]:" +Send-AwaitCommand "1h" +Start-Sleep 1 +Wait-AwaitResponse "Programming language:" +Send-AwaitCommand "bash" +Start-Sleep 1 +Wait-AwaitResponse "Will this app need access to the Internet? [y/N]:" +Send-AwaitCommand "n" +Start-Sleep 1 +Wait-AwaitResponse "Will this app need access to the parent project? [y/N]:" +Send-AwaitCommand "n" +Start-Sleep 1 +Wait-AwaitResponse "Choose an instance type for your app [mem1_ssd1_v2_x4]:" +Send-AwaitCommand "{ENTER}" -NoNewLine diff --git a/build/dependencies_cross_platform_tests/windows/test_dx_run_interactive.ps1 b/build/dependencies_cross_platform_tests/windows/test_dx_run_interactive.ps1 new file mode 100644 index 0000000000..bc5bed76aa --- /dev/null +++ b/build/dependencies_cross_platform_tests/windows/test_dx_run_interactive.ps1 @@ -0,0 +1,28 @@ +$ErrorActionPreference = 'stop' + +Import-Module Await + +Start-AwaitSession + +Send-AwaitCommand "dx run $Env:APPLET" +Start-Sleep 1 +Wait-AwaitResponse "inp1:" +Send-AwaitCommand "$Env:INP1_VAL" +Start-Sleep 1 +Wait-AwaitResponse "inp2:" +Send-AwaitCommand "{TAB}{TAB}" -NoNewLine +Start-Sleep 1 +Wait-AwaitResponse "$Env:INP2_VAL" +Send-AwaitCommand "$Env:INP2_VAL" +Start-Sleep 1 +Wait-AwaitResponse "Confirm running the executable with this input [Y/n]:" +Send-AwaitCommand "y" +Start-Sleep 1 +$output = Wait-AwaitResponse "Watch launched job now? [Y/n]" +$matcher = select-string "job-[a-zA-Z0-9]{24}" -inputobject $output +$jobId = $matcher.Matches.groups[0] +Send-AwaitCommand "n" + +Start-Sleep 5 + +dx terminate $jobId diff --git a/build/doc_build_requirements.txt b/build/doc_build_requirements.txt index 76c175ad23..765185dcac 100644 --- a/build/doc_build_requirements.txt +++ b/build/doc_build_requirements.txt @@ -1,4 +1 @@ -docutils<=0.15.2 -sphinx==1.2.3 -requests==2.20.0 -futures==2.2.0 +sphinx==7.1.2 diff --git a/build/environment.redirector b/build/environment.redirector deleted file mode 100644 index d5b5d19cf8..0000000000 --- a/build/environment.redirector +++ /dev/null @@ -1,27 +0,0 @@ -# -*- Mode: shell-script -*- -# -# Copyright (C) 2016 DNAnexus, Inc. -# -# This file is part of dx-toolkit (DNAnexus platform client libraries). -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may not -# use this file except in compliance with the License. You may obtain a copy -# of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# -# Backwards-compatibility shim for /etc/profile.d/environment.sh file. -# $ source environment - -SOURCE="${BASH_SOURCE[0]}" -while [ -h "$SOURCE" ] ; do SOURCE="$(readlink "$SOURCE")"; done - -SOURCE=$(dirname "$SOURCE") - -source $SOURCE/dnanexus.environment.sh diff --git a/build/fix_shebang_lines.sh b/build/fix_shebang_lines.sh deleted file mode 100755 index 77e2754f5a..0000000000 --- a/build/fix_shebang_lines.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/bin/bash -e -# -# Copyright (C) 2013-2016 DNAnexus, Inc. -# -# This file is part of dx-toolkit (DNAnexus platform client libraries). -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may not -# use this file except in compliance with the License. You may obtain a copy -# of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -# Usage: fix_shebang_lines.sh DIRNAME [--debian-system-install] [interpreter] -# Rewrites shebang lines for all Python scripts in DIRNAME. - -dirname=$1 -if [[ $dirname == "" ]]; then - dirname="." -fi - -msg="Please source the environment file at the root of dx-toolkit." -if [[ $2 == "--debian-system-install" ]]; then - msg="Please source the environment file /etc/profile.d/dnanexus.environment.sh." - shift -fi - -# * Setuptools bakes the path of the Python interpreter into all -# installed Python scripts. Rewrite it back to the more portable form, -# since we don't always know where the right interpreter is on the -# target system. -interpreter="/usr/bin/env python" -if [[ $2 != "" ]]; then - interpreter=$2 -fi - -for f in "$dirname"/*; do - if head -n 1 "$f" | egrep -iq "(python|pypy)"; then - echo "Rewriting $f to use portable interpreter paths" - perl -i -pe 's|^#!/.+|'"#!$interpreter"'| if $. == 1' "$f" - fi -done diff --git a/build/lucid_install_boost.sh b/build/lucid_install_boost.sh deleted file mode 100755 index 5704ad08e7..0000000000 --- a/build/lucid_install_boost.sh +++ /dev/null @@ -1,52 +0,0 @@ -#!/bin/bash -ex -# -# Copyright (C) 2013-2016 DNAnexus, Inc. -# -# This file is part of dx-toolkit (DNAnexus platform client libraries). -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may not -# use this file except in compliance with the License. You may obtain a copy -# of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -# Installs boost 1.48 (required for dx C++ executables) into /usr/local. -# -# -# -# Relevant bits go into: -# /usr/local/lib/libboost_filesystem.so.1.48.0 -# /usr/local/lib/libboost_program_options.so.1.48.0 -# /usr/local/lib/libboost_regex.so.1.48.0 -# /usr/local/lib/libboost_system.so.1.48.0 -# /usr/local/lib/libboost_thread.so.1.48.0 - -# Short-circuit sudo when running as root. In a chrooted environment we are -# likely to be running as root already, and sudo may not be present on minimal -# installations. -if [ "$USER" == "root" ]; then - MAYBE_SUDO='' -else - MAYBE_SUDO='sudo' -fi - -$MAYBE_SUDO apt-get install --yes g++ curl - -TEMPDIR=$(mktemp -d) - -pushd $TEMPDIR -curl -O http://superb-dca2.dl.sourceforge.net/project/boost/boost/1.48.0/boost_1_48_0.tar.bz2 -tar -xjf boost_1_48_0.tar.bz2 -cd boost_1_48_0 -./bootstrap.sh --with-libraries=filesystem,program_options,regex,system,thread -# --layout=tagged installs libraries with the -mt prefix. -$MAYBE_SUDO ./b2 --layout=tagged install - -popd -# rm -rf $TEMPDIR diff --git a/build/package.sh b/build/package.sh deleted file mode 100755 index ed1f3cf868..0000000000 --- a/build/package.sh +++ /dev/null @@ -1,105 +0,0 @@ -#!/bin/bash -e -# -# Copyright (C) 2013-2016 DNAnexus, Inc. -# -# This file is part of dx-toolkit (DNAnexus platform client libraries). -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may not -# use this file except in compliance with the License. You may obtain a copy -# of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -ostype=$(uname) - -product_name=$1 -if [[ $product_name == "" ]]; then - product_name="unknown" -fi -echo "$product_name" > "$(dirname $0)"/info/target - -# Hide any existing Python packages from the build process. -export PYTHONPATH= - -#source "$(dirname $0)/../environment" - -# Get home directory location -SOURCE="${BASH_SOURCE[0]}" -while [ -h "$SOURCE" ] ; do SOURCE="$(readlink "$SOURCE")"; done -export DNANEXUS_HOME="$( cd -P "$( dirname "$SOURCE" )" && pwd )/.." - -cd "${DNANEXUS_HOME}" -make clean -make -rm Makefile -rm -rf debian src/{java,javascript,R,ua,python/build,{dx-verify-file}/build} build/*_env share/dnanexus/lib/javascript -mv build/Prebuilt-Readme.md Readme.md - -if [[ "$product_name" == *"20.04"* ]]; then - python_env="/usr/bin/env python3" -else - python_env="/usr/bin/env python2.7" -fi - -"$(dirname $0)/fix_shebang_lines.sh" bin "$python_env" - -if [[ "$ostype" == 'Linux' ]]; then - osversion=$(lsb_release -c | sed s/Codename:.//) - rhmajorversion=$(cat /etc/system-release | sed "s/^\(CentOS\) release \([0-9]\)\.[0-9]\+.*$/\2/") - # TODO: detect versions that do and don't support mktemp --suffix more - # reliably. What is known: - # - # Supports --suffix: - # Ubuntu 12.04 - # CentOS 6 - # - # Doesn't support --suffix: - # Ubuntu 10.04 - if [[ "$osversion" == 'precise' || "$rhmajorversion" == '6' ]]; then - temp_archive=$(mktemp --suffix .tar.gz) - else - temp_archive=$(mktemp -t dx-toolkit.tar.gz.XXXXXXXXXX) - fi -elif [[ "$ostype" == 'Darwin' ]]; then # Mac OS - temp_archive=$(mktemp -t dx-toolkit.tar.gz) -else - echo "Unsupported OS $ostype" - exit 1 -fi - -# TODO: what if the checkout is not named dx-toolkit? The tar commands -# below will fail. -if [[ "$ostype" == 'Linux' ]]; then - cd "${DNANEXUS_HOME}/.." - tar --exclude-vcs -czf $temp_archive dx-toolkit -elif [[ "$ostype" == 'Darwin' ]]; then # Mac OS - - # BSD tar has no --exclude-vcs, so we do the same thing ourselves in a - # temp dir. - cd "${DNANEXUS_HOME}/.." - tempdir=$(mktemp -d -t dx-packaging-workdir) - cp -a dx-toolkit $tempdir - - cd $tempdir - rm -rf dx-toolkit/.git - tar -czf $temp_archive dx-toolkit -fi - -cd "${DNANEXUS_HOME}" - -dest_tarball="${DNANEXUS_HOME}/dx-toolkit-$(git describe)-${product_name}.tar.gz" - -mv $temp_archive $dest_tarball -if [[ "$ostype" == 'Darwin' ]]; then - rm -rf $tempdir -fi - -chmod 664 "${dest_tarball}" -echo "---" -echo "--- Package in ${dest_tarball}" -echo "---" diff --git a/build/pynsist_files/cli-quickstart.url b/build/pynsist_files/cli-quickstart.url deleted file mode 100644 index a129ae5a94..0000000000 --- a/build/pynsist_files/cli-quickstart.url +++ /dev/null @@ -1,2 +0,0 @@ -[InternetShortcut] -URL=https://documentation.dnanexus.com/getting-started/tutorials/cli-quickstart#quickstart-for-cli diff --git a/build/pynsist_files/dnanexus-shell.bat b/build/pynsist_files/dnanexus-shell.bat deleted file mode 100644 index 7e85a871bd..0000000000 --- a/build/pynsist_files/dnanexus-shell.bat +++ /dev/null @@ -1,51 +0,0 @@ -@echo off -REM Copyright (C) 2013-2016 DNAnexus, Inc. -REM -REM This file is part of dx-toolkit (DNAnexus platform client libraries). -REM -REM Licensed under the Apache License, Version 2.0 (the "License"); you may not -REM use this file except in compliance with the License. You may obtain a copy -REM of the License at -REM -REM http://www.apache.org/licenses/LICENSE-2.0 -REM -REM Unless required by applicable law or agreed to in writing, software -REM distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -REM WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -REM License for the specific language governing permissions and limitations -REM under the License. -REM -REM -REM Run this file in Command Prompt to initialize DNAnexus environment vars. - -REM Set DNANEXUS_HOME to the location of this file -set "SOURCE_DIR=%~dp0" -set "DNANEXUS_HOME=%SOURCE_DIR%" - -REM Add bin dir to PATH -set "PATH=%DNANEXUS_HOME%bin;%PATH%" - -REM Check the registry for the Python27 path: -set PY27_KEY_NAME="HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Python\PythonCore\2.7\InstallPath" -FOR /F "usebackq skip=2 tokens=1,2*" %%A IN ( - `REG QUERY %PY27_KEY_NAME% /ve 2^>nul`) DO ( - set PY_INSTALL_DIR=%%C -) -if defined PY_INSTALL_DIR echo PY_INSTALL_DIR = %PY_INSTALL_DIR% -if NOT defined PY_INSTALL_DIR ( - msg %USERNAME% Python installation dir not found! - exit 1 -) - -REM Add Python27 and Python27\Scripts to PATH -set "PATH=%PY_INSTALL_DIR%;%PY_INSTALL_DIR%Scripts;%PATH%" - -REM Set PYTHONPATH so the dx-*.exe wrappers can locate dxpy -set "PYTHONPATH=%DNANEXUS_HOME%share\dnanexus\lib\python2.7\site-packages" - -REM Regenerate the dxpy console script .exe wrappers, so the .exes can -REM locate python.exe on this machine -REM python -m wheel install-scripts dxpy - -REM Bring up the interactive shell -start cmd.exe /u /k echo DNAnexus CLI initialized. For help, run: 'dx help' diff --git a/build/pynsist_files/dnanexus-shell.ps1 b/build/pynsist_files/dnanexus-shell.ps1 deleted file mode 100644 index 92ee621a17..0000000000 --- a/build/pynsist_files/dnanexus-shell.ps1 +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright (C) 2013-2016 DNAnexus, Inc. -# -# This file is part of dx-toolkit (DNAnexus platform client libraries). -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may not -# use this file except in compliance with the License. You may obtain a copy -# of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# -# -# Run this file in PowerShell to initialize DNAnexus environment vars. - -# Set DNANEXUS_HOME to the location of this file -$script:THIS_PATH = $myinvocation.mycommand.path -$script:BASE_DIR = split-path (resolve-path "$THIS_PATH") -Parent -$env:DNANEXUS_HOME = $BASE_DIR - -# Add bin and Scripts dirs to PATH so dx*.exe are available -$env:PATH = "C:/Python27;$env:DNANEXUS_HOME/bin;$env:DNANEXUS_HOME/python27/Scripts;" + $env:PATH - -# Add our custom Lib dir to PYTHONPATH so the dx*.exe wrappers can locate dxpy -$env:PYTHONPATH = "$env:DNANEXUS_HOME/python27/Lib/site-packages;" + $env:PYTHONPATH - -# Set utf-8 as default IO encoding -$env:PYTHONIOENCODING = "utf-8" - -# Print banner -"DNAnexus CLI initialized. For help, run: 'dx help'" -"" -"Client version: " + (dx --version) -"" diff --git a/build/pynsist_files/dnanexus.ico b/build/pynsist_files/dnanexus.ico deleted file mode 100644 index 9743b872b2..0000000000 Binary files a/build/pynsist_files/dnanexus.ico and /dev/null differ diff --git a/build/pynsist_files/installer.cfg.template b/build/pynsist_files/installer.cfg.template deleted file mode 100644 index 02027b8cb8..0000000000 --- a/build/pynsist_files/installer.cfg.template +++ /dev/null @@ -1,77 +0,0 @@ -[Application] -name=DNAnexus CLI -target=powershell.exe -parameters=-ExecutionPolicy Bypass -NoLogo -NoExit -File "$INSTDIR\dnanexus-shell.ps1" -#console=true -version=TEMPLATE_STRING_TOOLKIT_VERSION -icon=dnanexus.ico -# To point shortcut at dx instead: -#entry_point=dxpy.scripts.dx:main -#extra_preamble=prerun-stuff.py - -[Shortcut DNAnexus Help] -target=$INSTDIR\cli-quickstart.url -icon=dnanexus.ico - -# Shortcut for the .bat cmd.exe launcher: -#[Shortcut DNAnexus CLI (cmd)] -#target=$INSTDIR\dnanexus-shell.bat -##parameters= -#console=true - -# How to add a shortcut for a python script: -#[Shortcut dx-app-wizard] -#entry_point=dxpy.scripts.dx_app_wizard:main -#console=true -#icon=foo.ico - -[Python] -version=2.7.15 -format=installer - -[Build] -nsi_template=pyapp_installpy_dnanexus.nsi - -[Include] -# Importable packages that your application requires, one per line -#packages = dxpy -# bs4 -# colorama -# concurrent -# dateutil -# magic -# requests -# six -# wheel -# ws4py - -# Other files and folders that should be installed. -# Note that the versions for each .whl file below -# should match dx-toolkit/src/python/requirements.txt -# and requirements_windows.txt: -files = dnanexus-shell.ps1 - cli-quickstart.url - TEMPLATE_STRING_DXPY_WHEEL_FILENAME > $INSTDIR\\wheelfiles - wheelfile_depends - ../../bin/dx-verify-file.exe > $INSTDIR\\bin - ../../bin/jq.exe > $INSTDIR\\bin - DLL_DEPS_FOLDERlibboost_atomic-mt.dll > $INSTDIR\\bin - DLL_DEPS_FOLDERlibboost_chrono-mt.dll > $INSTDIR\\bin - DLL_DEPS_FOLDERlibboost_date_time-mt.dll > $INSTDIR\\bin - DLL_DEPS_FOLDERlibboost_filesystem-mt.dll > $INSTDIR\\bin - DLL_DEPS_FOLDERlibboost_iostreams-mt.dll > $INSTDIR\\bin - DLL_DEPS_FOLDERlibboost_program_options-mt.dll > $INSTDIR\\bin - DLL_DEPS_FOLDERlibboost_regex-mt.dll > $INSTDIR\\bin - DLL_DEPS_FOLDERlibboost_signals-mt.dll > $INSTDIR\\bin - DLL_DEPS_FOLDERlibboost_system-mt.dll > $INSTDIR\\bin - DLL_DEPS_FOLDERlibboost_thread-mt.dll > $INSTDIR\\bin - DLL_DEPS_FOLDERlibcares-2.dll > $INSTDIR\\bin - DLL_DEPS_FOLDERlibcurl-4.dll > $INSTDIR\\bin - DLL_DEPS_FOLDERlibeay32.dll > $INSTDIR\\bin - DLL_DEPS_FOLDERlibgcc_s_dw2-1.dll > $INSTDIR\\bin - DLL_DEPS_FOLDERlibgnurx-0.dll > $INSTDIR\\bin - DLL_DEPS_FOLDERlibmagic-1.dll > $INSTDIR\\bin - DLL_DEPS_FOLDERlibstdc++-6.dll > $INSTDIR\\bin - DLL_DEPS_FOLDERmagic.mgc > $INSTDIR\\bin - DLL_DEPS_FOLDERssleay32.dll > $INSTDIR\\bin - DLL_DEPS_FOLDERzlib1.dll > $INSTDIR\\bin diff --git a/build/pynsist_files/pyapp_dnanexus.nsi.template b/build/pynsist_files/pyapp_dnanexus.nsi.template deleted file mode 100644 index 340f772511..0000000000 --- a/build/pynsist_files/pyapp_dnanexus.nsi.template +++ /dev/null @@ -1,194 +0,0 @@ -; dx-toolkit NSIS installer config file - based on default pynsist template: -; https://github.com/takluyver/pynsist/blob/master/nsist/pyapp.nsi - -!define COMPANYNAME "DNAnexus Inc." -!define DXPY_WHEEL_FILENAME "TEMPLATE_STRING_DXPY_WHEEL_FILENAME" -!define PRODUCT_NAME "[[ib.appname]]" -!define PRODUCT_VERSION "[[ib.version]]" -!define PY_VERSION "[[ib.py_version]]" -!define PY_MAJOR_VERSION "[[ib.py_major_version]]" -!define BITNESS "[[ib.py_bitness]]" -!define ARCH_TAG "[[arch_tag]]" -!define INSTALLER_NAME "[[ib.installer_name]]" -!define PRODUCT_ICON "[[icon]]" - -SetCompressor lzma - -RequestExecutionLevel admin - -[% block modernui %] -; Modern UI installer stuff -!include "MUI2.nsh" -!define MUI_ABORTWARNING -;!define MUI_ICON "${NSISDIR}\Contrib\Graphics\Icons\modern-install.ico" -!define MUI_ICON "[[icon]]" - -; UI pages -[% block ui_pages %] -!insertmacro MUI_PAGE_WELCOME -!insertmacro MUI_PAGE_DIRECTORY -!insertmacro MUI_PAGE_INSTFILES -!insertmacro MUI_PAGE_FINISH -[% endblock ui_pages %] -!insertmacro MUI_LANGUAGE "English" -[% endblock modernui %] - -Name "${PRODUCT_NAME} ${PRODUCT_VERSION}" -OutFile "${INSTALLER_NAME}" -InstallDir "$PROGRAMFILES${BITNESS}\${PRODUCT_NAME}" -ShowInstDetails show - -Section -SETTINGS - SetOutPath "$INSTDIR" - SetOverwrite ifnewer -SectionEnd - -[% block sections %] - -Section "!${PRODUCT_NAME}" sec_app - SectionIn RO - SetShellVarContext all - File ${PRODUCT_ICON} - SetOutPath "$INSTDIR\pkgs" - File /r "pkgs\*.*" - SetOutPath "$INSTDIR" - - ; Install files - [% for destination, group in grouped_files %] - SetOutPath "[[destination]]" - [% for file in group %] - File "[[ file ]]" - [% endfor %] - [% endfor %] - - ; Install directories - [% for dir, destination in ib.install_dirs %] - SetOutPath "[[ pjoin(destination, dir) ]]" - File /r "[[dir]]\*.*" - [% endfor %] - - [% block install_shortcuts %] - ; Install shortcuts - ; The output path becomes the working directory for shortcuts - SetOutPath "%HOMEDRIVE%\%HOMEPATH%" - [% if single_shortcut %] - [% for scname, sc in ib.shortcuts.items() %] - CreateShortCut "$SMPROGRAMS\[[scname]].lnk" "[[sc['target'] ]]" \ - '[[ sc['parameters'] ]]' "$INSTDIR\[[ sc['icon'] ]]" - [% endfor %] - [% else %] - [# Multiple shortcuts: create a directory for them #] - CreateDirectory "$SMPROGRAMS\${PRODUCT_NAME}" - [% for scname, sc in ib.shortcuts.items() %] - CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\[[scname]].lnk" "[[sc['target'] ]]" \ - '[[ sc['parameters'] ]]' "$INSTDIR\[[ sc['icon'] ]]" - [% endfor %] - [% endif %] - SetOutPath "$INSTDIR" - [% endblock install_shortcuts %] - - ; Byte-compile Python files. - DetailPrint "Byte-compiling Python modules..." - nsExec::ExecToLog '[[ python ]] -m compileall -q "$INSTDIR\pkgs"' - - ; Set PYTHONPATH to the dir containing the installed dxpy wheel. - ;MessageBox MB_OK 'PYTHONPATH is "$INSTDIR\share\dnanexus\lib\python2.7\site-packages"' - ;System::Call 'Kernel32::SetEnvironmentVariable(t, t)i ("PYTHONPATH", "$INSTDIR\share\dnanexus\lib\python2.7\site-packages").r0' - ;StrCmp $0 0 error - ; ; Generate dxpy console script .exe wrappers - they'll be installed in - ; ; the user's Python27\Scripts\ dir. - ; DetailPrint "Installing wrapper executables..." - ; ExecWait '"C:\Python27\python.exe" -m wheel install-scripts dxpy' $0 - ; ;MessageBox MB_OK 'ExecWait returned "$0"' - ; Goto done - ;error: - ; MessageBox MB_OK "Can't set PYTHONPATH environment variable!" - ;done: - - DetailPrint "Installing dxpy..." - ; Install dxpy, its dependencies, and console scripts in $INSTDIR\python27 - ; Use --no-index and a local wheelfiles dir so we don't need Inet access: - ExecWait '"C:\Python27\Scripts\pip.exe" install --root "$INSTDIR" --no-index --find-links="$INSTDIR\wheelfile_depends" "$INSTDIR\wheelfiles\TEMPLATE_STRING_DXPY_WHEEL_FILENAME"' $0 - StrCmp $0 0 pass error - pass: - ;MessageBox MB_OK 'dxpy installed' - ;MessageBox MB_OK 'ExecWait returned "$0"' - Goto done - error: - MessageBox MB_OK 'Error: dxpy installation failed!' - MessageBox MB_OK 'ExecWait returned "$0"' - done: - - WriteUninstaller $INSTDIR\uninstall.exe - ; Add ourselves to Add/remove programs - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" \ - "DisplayName" "${PRODUCT_NAME}" - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" \ - "UninstallString" '"$INSTDIR\uninstall.exe"' - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" \ - "InstallLocation" "$INSTDIR" - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" \ - "DisplayIcon" "$INSTDIR\${PRODUCT_ICON}" - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" \ - "Publisher" "${COMPANYNAME}" - WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" \ - "NoModify" 1 - WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" \ - "NoRepair" 1 - - ; Check if we need to reboot - IfRebootFlag 0 noreboot - MessageBox MB_YESNO "A reboot is required to finish the installation. Do you wish to reboot now?" \ - /SD IDNO IDNO noreboot - Reboot - noreboot: -SectionEnd - -Section "Uninstall" - DetailPrint "Uninstalling dxpy root..." - RMDir /r "$INSTDIR\python27" - DetailPrint "Uninstalling bin dir..." - RMDir /r "$INSTDIR\bin" - - SetShellVarContext all - Delete $INSTDIR\uninstall.exe - Delete "$INSTDIR\${PRODUCT_ICON}" - RMDir /r "$INSTDIR\pkgs" - RMDir /r "$INSTDIR\wheelfiles" - ; Uninstall files - [% for file, destination in ib.install_files %] - Delete "[[pjoin(destination, file)]]" - [% endfor %] - ; Uninstall directories - [% for dir, destination in ib.install_dirs %] - RMDir /r "[[pjoin(destination, dir)]]" - [% endfor %] - [% block uninstall_shortcuts %] - ; Uninstall shortcuts - [% if single_shortcut %] - [% for scname in ib.shortcuts %] - Delete "$SMPROGRAMS\[[scname]].lnk" - [% endfor %] - [% else %] - RMDir /r "$SMPROGRAMS\${PRODUCT_NAME}" - [% endif %] - [% endblock uninstall_shortcuts %] - RMDir $INSTDIR - DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" -SectionEnd - -[% endblock sections %] - -; Functions - -Function .onMouseOverSection - ; Find which section the mouse is over, and set the corresponding description. - FindWindow $R0 "#32770" "" $HWNDPARENT - GetDlgItem $R0 $R0 1043 ; description item (must be added to the UI) - - [% block mouseover_messages %] - StrCmp $0 ${sec_app} "" +2 - SendMessage $R0 ${WM_SETTEXT} 0 "STR:${PRODUCT_NAME}" - - [% endblock mouseover_messages %] -FunctionEnd diff --git a/build/pynsist_files/pyapp_installpy_dnanexus.nsi b/build/pynsist_files/pyapp_installpy_dnanexus.nsi deleted file mode 100644 index 61b3e8f6c5..0000000000 --- a/build/pynsist_files/pyapp_installpy_dnanexus.nsi +++ /dev/null @@ -1,43 +0,0 @@ -; dx-toolkit NSIS installer config file - based on default pynsist template: -; https://github.com/takluyver/pynsist/blob/master/nsist/pyapp_installpy.nsi - -[% extends "pyapp_dnanexus.nsi" %] - -[% block ui_pages %] -[# We only need to add COMPONENTS, but they have to be in order #] -!insertmacro MUI_PAGE_WELCOME -!insertmacro MUI_PAGE_COMPONENTS -!insertmacro MUI_PAGE_DIRECTORY -!insertmacro MUI_PAGE_INSTFILES -!insertmacro MUI_PAGE_FINISH -[% endblock ui_pages %] - -[% block sections %] -Section "Python ${PY_VERSION}" sec_py - - DetailPrint "Installing Python ${PY_MAJOR_VERSION}, ${BITNESS} bit" - [% if ib.py_version_tuple >= (3, 5) %] - [% set filename = 'python-' ~ ib.py_version ~ ('-amd64' if ib.py_bitness==64 else '') ~ '.exe' %] - File "[[filename]]" - ExecWait '"$INSTDIR\[[filename]]" /passive Include_test=0 InstallAllUsers=1' - [% else %] - [% set filename = 'python-' ~ ib.py_version ~ ('.amd64' if ib.py_bitness==64 else '') ~ '.msi' %] - File "[[filename]]" - ;ExecWait 'msiexec /i "$INSTDIR\[[filename]]" \ - ; /qb ALLUSERS=1 TARGETDIR="$COMMONFILES${BITNESS}\Python\${PY_MAJOR_VERSION}"' - ; Allow Python to choose the default install dir (C:\Python27) - ExecWait 'msiexec /i "$INSTDIR\[[filename]]" /qb ALLUSERS=1' - [% endif %] - Delete "$INSTDIR\[[filename]]" -SectionEnd - -[[ super() ]] -[% endblock sections %] - -[% block mouseover_messages %] - StrCmp $0 ${sec_py} 0 +2 - SendMessage $R0 ${WM_SETTEXT} 0 "STR:The Python interpreter. \ - This is required for ${PRODUCT_NAME} to run." - -[[ super() ]] -[% endblock mouseover_messages %] diff --git a/build/run_java_integration_tests.py b/build/run_java_integration_tests.py index e623c7ece1..60e79dbe03 100755 --- a/build/run_java_integration_tests.py +++ b/build/run_java_integration_tests.py @@ -1,4 +1,6 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 + +from __future__ import print_function, unicode_literals import argparse import os @@ -27,10 +29,11 @@ def run(): subprocess.check_call(["make", "java"], cwd=os.path.join(TOOLKIT_ROOT_DIR, "src")) subprocess.check_call(make_cmd, cwd=os.path.join(TOOLKIT_ROOT_DIR, "src", "java")) except subprocess.CalledProcessError as e: - print "Tests failed, printing out error reports:" + print("Tests failed, printing out error reports:") for filename in os.listdir(os.path.join(TOOLKIT_ROOT_DIR, "src/java/target/surefire-reports")): if filename.startswith("com.dnanexus."): - print open(os.path.join(TOOLKIT_ROOT_DIR, "src/java/target/surefire-reports", filename)).read().strip() + with open(os.path.join(TOOLKIT_ROOT_DIR, "src/java/target/surefire-reports", filename)) as fh: + print(fh.read().strip()) raise e if __name__ == '__main__': diff --git a/build/run_python_integration_tests.py b/build/run_python_integration_tests.py index c1c7118c69..3953a918f2 100755 --- a/build/run_python_integration_tests.py +++ b/build/run_python_integration_tests.py @@ -1,8 +1,10 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 ''' Runs Python integration tests +Assumes `make python src_libs && source build/py_env/bin/activate` + If no arguments are given, all tests in src/python/test/ are run. To run a single test method, e.g. test_basic in class TestDXBashHelpers, @@ -41,17 +43,9 @@ os.environ['DNANEXUS_INSTALL_PYTHON_TEST_DEPS'] = 'yes' def run(): - # src_libs is to ensure that dx-unpack is runnable. If we had "bash unit - # tests" that were broken out separately, that would obviate this though. - # - # Note that Macs must run the make command before running this script, - # as of b9d8487 (when virtualenv was added to the Mac dx-toolkit release). - if sys.platform != "darwin": - subprocess.check_call(["make", "python", "src_libs"], cwd=TOOLKIT_ROOT_DIR) - - python_version = "python{}.{}".format(sys.version_info.major, sys.version_info.minor) + # Assumes `make python src_libs` has been run - cmd = ['python', '-m', 'unittest'] + cmd = ['python3', '-m', 'unittest'] if args.tests: cmd += ['-v'] + args.tests else: diff --git a/build/run_traceability_integration_tests.py b/build/run_traceability_integration_tests.py index d5816d961b..60af957cb8 100755 --- a/build/run_traceability_integration_tests.py +++ b/build/run_traceability_integration_tests.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 ''' Runs Python integration tests tagged for inclusion in the traceability matrix. @@ -19,13 +19,6 @@ os.environ['DX_USER_CONF_DIR'] = TOOLKIT_ROOT_DIR + "/dnanexus_config_relocated" def run(): - # src_libs is to ensure that dx-unpack is runnable. If we had "bash unit - # tests" that were broken out separately, that would obviate this though. - # - # Note that Macs must run the make command before running this script, - # as of b9d8487 (when virtualenv was added to the Mac dx-toolkit release). - if sys.platform != "darwin": - subprocess.check_call(["make", "python", "src_libs"], cwd=TOOLKIT_ROOT_DIR) cmd = ['py.test', '-vv', '-s', '-m', 'TRACEABILITY_MATRIX', 'src/python/test/'] diff --git a/build/run_traceability_integration_tests.sh b/build/run_traceability_integration_tests.sh index ca7a12ab5a..4ab073a257 100755 --- a/build/run_traceability_integration_tests.sh +++ b/build/run_traceability_integration_tests.sh @@ -8,11 +8,9 @@ export DNANEXUS_INSTALL_PYTHON_TEST_DEPS="yes" export DX_USER_CONF_DIR="${TOOLKIT_ROOT_DIR}/dnanexus_config_relocated" cd $TOOLKIT_ROOT_DIR -make python +make src_libs python -source build/py_env2.7/bin/activate -source environment +source build/py_env/bin/activate -export PYTHONPATH="${TOOLKIT_ROOT_DIR}/src/python/test:${TOOLKIT_ROOT_DIR}/share/dnanexus/lib/python2.7/site-packages" #py.test -m TRACEABILITY_MATRIX src/python/test/test_dx_bash_helpers.py::TestDXBashHelpers::test_basic -sv py.test -vv -s -m TRACEABILITY_MATRIX ${TOOLKIT_ROOT_DIR}/src/python/test/ diff --git a/build/tls12check.py b/build/tls12check.py deleted file mode 100755 index 5cb2c9a046..0000000000 --- a/build/tls12check.py +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env python - -"""Returns true if your python supports TLS v1.2, and false if not.""" - -import ssl -import sys - -# From https://github.com/boto/botocore/blob/1.5.29/botocore/handlers.py#L636 -openssl_version_tuple = ssl.OPENSSL_VERSION_INFO - -upgrade_required_msg = """ -UPGRADE REQUIRED -Your OpenSSL version: '{0}' -The OpenSSL used by your Python implementation does not support TLS 1.2. -The DNAnexus CLI requires a Python release built with OpenSSL 1.0.1 or greater. -""".format(ssl.OPENSSL_VERSION) - -upgrade_not_required_msg = """ -NO UPGRADE REQUIRED -Your OpenSSL version: '{0}' -Your Python supports TLS 1.2, and is ready to use with the DNAnexus CLI. -""".format(ssl.OPENSSL_VERSION) - -if openssl_version_tuple < (1, 0, 1, 0, 0): - print(upgrade_required_msg) - sys.exit(1) - -print(upgrade_not_required_msg) diff --git a/build/upgrade.sh b/build/upgrade.sh deleted file mode 100755 index db4711c775..0000000000 --- a/build/upgrade.sh +++ /dev/null @@ -1,79 +0,0 @@ -#!/bin/bash -e - -if [[ ! $1 ]]; then - echo "$(basename $0): Update dx-toolkit to a specified version." - echo "Usage: $(basename $0) version [build_target_name]" - exit 1 -fi - -new_version=$1 -build_target_name=$2 -build_dir=$(dirname "$0") - -function cleanup() { - echo "$(basename $0): Unable to update to version ${new_version} $@" - echo "Please visit https://documentation.dnanexus.com/downloads and download an appropriate package for your system." -} - -trap cleanup ERR - -if [[ $build_target_name == "" ]]; then - if [[ -f "${build_dir}/info/target" ]]; then - build_target_name=$(cat "${build_dir}/info/target") - else - echo "$(basename $0): Unable to determine which package to download" 1>&2 - false - fi -fi - -if [[ -f "${build_dir}/info/version" ]]; then - current_version=$(cat "${build_dir}/info/version") -else - echo "$(basename $0): Unable to determine current package version" 1>&2 - false -fi - -pkg_name="dx-toolkit-${new_version}-${build_target_name}.tar.gz" -if type -t curl >/dev/null; then - get_cmd="curl -O -L" -elif type -t wget >/dev/null; then - get_cmd="wget" -else - echo "$(basename $0): Unable to download file (either curl or wget is required)" 1>&2 - false -fi - -if [[ ! -w "${build_dir}" ]]; then - echo "${build_dir} directory not writable" 1>&2 - false -fi - -echo "Downloading $pkg_name (using ${get_cmd})..." -rm -f "${build_dir}/${pkg_name}" -(cd "$build_dir"; ${get_cmd} "https://dnanexus-sdk.s3.amazonaws.com/${pkg_name}") - -echo "Unpacking $pkg_name..." -tar -C "$build_dir" -xzf "${build_dir}/${pkg_name}" - -echo "Backing up version ${current_version}..." -rm -rf "${build_dir}/${current_version}" -mkdir "${build_dir}/${current_version}" -for i in "${build_dir}"/../*; do - if [[ $(basename "$i") != "build" ]]; then - mv "$i" "${build_dir}/${current_version}/" - fi -done - -echo "Moving version ${new_version} into place..." -for i in "${build_dir}"/dx-toolkit/*; do - if [[ $(basename "$i") != "build" ]]; then - mv "$i" "${build_dir}/../" - fi -done - -rm -rf "${build_dir}/dx-toolkit" -echo "${new_version}" > "${build_dir}/info/version" - -echo "$(basename $0): Updated to version ${new_version}. Previous version saved in ${build_dir}/${current_version}." -echo "Please close this terminal, open a new terminal, and source the environment file in the dx-toolkit folder" - diff --git a/build/upload_to_pypi.sh b/build/upload_to_pypi.sh index b8d8d47cab..6dca7114fd 100755 --- a/build/upload_to_pypi.sh +++ b/build/upload_to_pypi.sh @@ -21,4 +21,4 @@ while [ -h "$SOURCE" ] ; do SOURCE="$(readlink "$SOURCE")"; done root="$( cd -P "$( dirname "$SOURCE" )" && pwd )" make -C $root/../src api_wrappers toolkit_version -(cd $root/../src/python; /usr/bin/python setup.py sdist; twine upload dist/* -s -i 2F7B9277) +(cd $root/../src/python; /usr/bin/python setup.py sdist; twine upload dist/*) diff --git a/contrib/perl/generatePerlAPIWrappers.py b/contrib/perl/generatePerlAPIWrappers.py index 882b2bd8bf..eead5e478a 100755 --- a/contrib/perl/generatePerlAPIWrappers.py +++ b/contrib/perl/generatePerlAPIWrappers.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # # Copyright (C) 2013-2016 DNAnexus, Inc. # diff --git a/contrib/perl/lib/DNAnexus/API.pm b/contrib/perl/lib/DNAnexus/API.pm index 5a1c578afd..81763089a0 100644 --- a/contrib/perl/lib/DNAnexus/API.pm +++ b/contrib/perl/lib/DNAnexus/API.pm @@ -557,6 +557,104 @@ sub databaseListFolder($;$%) { } +sub dbclusterAddTags($;$%) { + my ($object_id, $input_params, %kwargs) = @_; + %kwargs = () unless %kwargs; + return DXHTTPRequest('/'.$object_id.'/addTags', $input_params, %kwargs); +} + + +sub dbclusterAddTypes($;$%) { + my ($object_id, $input_params, %kwargs) = @_; + %kwargs = () unless %kwargs; + return DXHTTPRequest('/'.$object_id.'/addTypes', $input_params, %kwargs); +} + + +sub dbclusterDescribe($;$%) { + my ($object_id, $input_params, %kwargs) = @_; + %kwargs = () unless %kwargs; + return DXHTTPRequest('/'.$object_id.'/describe', $input_params, %kwargs); +} + + +sub dbclusterGetDetails($;$%) { + my ($object_id, $input_params, %kwargs) = @_; + %kwargs = () unless %kwargs; + return DXHTTPRequest('/'.$object_id.'/getDetails', $input_params, %kwargs); +} + + +sub dbclusterNew(;$%) { + my ($input_params, %kwargs) = @_; + %kwargs = () unless %kwargs; + return DXHTTPRequest('/dbcluster/new', $input_params, %kwargs); +} + + +sub dbclusterRemoveTags($;$%) { + my ($object_id, $input_params, %kwargs) = @_; + %kwargs = () unless %kwargs; + return DXHTTPRequest('/'.$object_id.'/removeTags', $input_params, %kwargs); +} + + +sub dbclusterRemoveTypes($;$%) { + my ($object_id, $input_params, %kwargs) = @_; + %kwargs = () unless %kwargs; + return DXHTTPRequest('/'.$object_id.'/removeTypes', $input_params, %kwargs); +} + + +sub dbclusterRename($;$%) { + my ($object_id, $input_params, %kwargs) = @_; + %kwargs = () unless %kwargs; + return DXHTTPRequest('/'.$object_id.'/rename', $input_params, %kwargs); +} + + +sub dbclusterSetDetails($;$%) { + my ($object_id, $input_params, %kwargs) = @_; + %kwargs = () unless %kwargs; + return DXHTTPRequest('/'.$object_id.'/setDetails', $input_params, %kwargs); +} + + +sub dbclusterSetProperties($;$%) { + my ($object_id, $input_params, %kwargs) = @_; + %kwargs = () unless %kwargs; + return DXHTTPRequest('/'.$object_id.'/setProperties', $input_params, %kwargs); +} + + +sub dbclusterSetVisibility($;$%) { + my ($object_id, $input_params, %kwargs) = @_; + %kwargs = () unless %kwargs; + return DXHTTPRequest('/'.$object_id.'/setVisibility', $input_params, %kwargs); +} + + +sub dbclusterStart($;$%) { + my ($object_id, $input_params, %kwargs) = @_; + %kwargs = () unless %kwargs; + return DXHTTPRequest('/'.$object_id.'/start', $input_params, %kwargs); +} + + +sub dbclusterStop($;$%) { + my ($object_id, $input_params, %kwargs) = @_; + %kwargs = () unless %kwargs; + return DXHTTPRequest('/'.$object_id.'/stop', $input_params, %kwargs); +} + + +sub dbclusterTerminate($;$%) { + my ($object_id, $input_params, %kwargs) = @_; + %kwargs = () unless %kwargs; + return DXHTTPRequest('/'.$object_id.'/terminate', $input_params, %kwargs); +} + + sub fileAddTags($;$%) { my ($object_id, $input_params, %kwargs) = @_; %kwargs = () unless %kwargs; @@ -823,6 +921,20 @@ sub jobTerminate($;$%) { } +sub jobUpdate($;$%) { + my ($object_id, $input_params, %kwargs) = @_; + %kwargs = () unless %kwargs; + return DXHTTPRequest('/'.$object_id.'/update', $input_params, %kwargs); +} + + +sub jobGetIdentityToken($;$%) { + my ($object_id, $input_params, %kwargs) = @_; + %kwargs = () unless %kwargs; + return DXHTTPRequest('/'.$object_id.'/getIdentityToken', $input_params, %kwargs); +} + + sub jobNew(;$%) { my ($input_params, %kwargs) = @_; %kwargs = () unless %kwargs; @@ -1475,5 +1587,5 @@ sub workflowNew(;$%) { our @ISA = "Exporter"; -our @EXPORT_OK = qw(analysisAddTags analysisDescribe analysisRemoveTags analysisSetProperties analysisTerminate appAddAuthorizedUsers appAddCategories appAddDevelopers appAddTags appDelete appDescribe appGet appInstall appListAuthorizedUsers appListCategories appListDevelopers appPublish appRemoveAuthorizedUsers appRemoveCategories appRemoveDevelopers appRemoveTags appRun appValidateBatch appUninstall appUpdate appNew appletAddTags appletDescribe appletGet appletGetDetails appletListProjects appletRemoveTags appletRename appletValidateBatch appletRun appletSetProperties appletNew containerClone containerDescribe containerDestroy containerListFolder containerMove containerNewFolder containerRemoveFolder containerRemoveObjects containerRenameFolder databaseAddTags databaseAddTypes databaseClose databaseDescribe databaseGetDetails databaseListProjects databaseRelocate databaseRemoveTags databaseRemoveTypes databaseRename databaseSetDetails databaseSetProperties databaseSetVisibility databaseDownloadFile databaseListFolder fileAddTags fileAddTypes fileClose fileDescribe fileDownload fileGetDetails fileListProjects fileRemoveTags fileRemoveTypes fileRename fileSetDetails fileSetProperties fileSetVisibility fileUpload fileNew globalWorkflowAddAuthorizedUsers globalWorkflowAddCategories globalWorkflowAddDevelopers globalWorkflowAddTags globalWorkflowDelete globalWorkflowDescribe globalWorkflowListAuthorizedUsers globalWorkflowListCategories globalWorkflowListDevelopers globalWorkflowPublish globalWorkflowRemoveAuthorizedUsers globalWorkflowRemoveCategories globalWorkflowRemoveDevelopers globalWorkflowRemoveTags globalWorkflowRun globalWorkflowUpdate globalWorkflowNew jobAddTags jobDescribe jobGetLog jobRemoveTags jobSetProperties jobTerminate jobNew notificationsGet notificationsMarkRead orgDescribe orgFindMembers orgFindProjects orgFindApps orgInvite orgRemoveMember orgSetMemberAccess orgUpdate orgNew projectAddTags projectArchive projectUnarchive projectClone projectDecreasePermissions projectDescribe projectDestroy projectInvite projectLeave projectListFolder projectMove projectNewFolder projectRemoveFolder projectRemoveObjects projectRemoveTags projectRenameFolder projectSetProperties projectTransfer projectUpdate projectUpdateSponsorship projectNew recordAddTags recordAddTypes recordClose recordDescribe recordGetDetails recordListProjects recordRemoveTags recordRemoveTypes recordRename recordSetDetails recordSetProperties recordSetVisibility recordNew systemDescribeDataObjects systemDescribeExecutions systemDescribeProjects systemFindAffiliates systemFindApps systemFindDataObjects systemFindGlobalWorkflows systemResolveDataObjects systemFindExecutions systemFindAnalyses systemFindDatabases systemFindJobs systemFindProjects systemFindUsers systemFindProjectMembers systemFindOrgs systemGenerateBatchInputs systemGlobalSearch systemGreet systemHeaders systemShortenURL systemWhoami userDescribe userUpdate workflowAddStage workflowAddTags workflowAddTypes workflowClose workflowDescribe workflowDryRun workflowGetDetails workflowIsStageCompatible workflowListProjects workflowMoveStage workflowOverwrite workflowRemoveStage workflowRemoveTags workflowRemoveTypes workflowRename workflowRun workflowValidateBatch workflowSetDetails workflowSetProperties workflowSetVisibility workflowUpdate workflowUpdateStageExecutable workflowNew); +our @EXPORT_OK = qw(analysisAddTags analysisDescribe analysisRemoveTags analysisSetProperties analysisTerminate appAddAuthorizedUsers appAddCategories appAddDevelopers appAddTags appDelete appDescribe appGet appInstall appListAuthorizedUsers appListCategories appListDevelopers appPublish appRemoveAuthorizedUsers appRemoveCategories appRemoveDevelopers appRemoveTags appRun appValidateBatch appUninstall appUpdate appNew appletAddTags appletDescribe appletGet appletGetDetails appletListProjects appletRemoveTags appletRename appletValidateBatch appletRun appletSetProperties appletNew containerClone containerDescribe containerDestroy containerListFolder containerMove containerNewFolder containerRemoveFolder containerRemoveObjects containerRenameFolder databaseAddTags databaseAddTypes databaseClose databaseDescribe databaseGetDetails databaseListProjects databaseRelocate databaseRemoveTags databaseRemoveTypes databaseRename databaseSetDetails databaseSetProperties databaseSetVisibility databaseDownloadFile databaseListFolder dbclusterAddTags dbclusterAddTypes dbclusterDescribe dbclusterGetDetails dbclusterNew dbclusterRemoveTags dbclusterRemoveTypes dbclusterRename dbclusterSetDetails dbclusterSetProperties dbclusterSetVisibility dbclusterStart dbclusterStop dbclusterTerminate fileAddTags fileAddTypes fileClose fileDescribe fileDownload fileGetDetails fileListProjects fileRemoveTags fileRemoveTypes fileRename fileSetDetails fileSetProperties fileSetVisibility fileUpload fileNew globalWorkflowAddAuthorizedUsers globalWorkflowAddCategories globalWorkflowAddDevelopers globalWorkflowAddTags globalWorkflowDelete globalWorkflowDescribe globalWorkflowListAuthorizedUsers globalWorkflowListCategories globalWorkflowListDevelopers globalWorkflowPublish globalWorkflowRemoveAuthorizedUsers globalWorkflowRemoveCategories globalWorkflowRemoveDevelopers globalWorkflowRemoveTags globalWorkflowRun globalWorkflowUpdate globalWorkflowNew jobAddTags jobDescribe jobGetLog jobRemoveTags jobSetProperties jobTerminate jobUpdate jobGetIdentityToken jobNew notificationsGet notificationsMarkRead orgDescribe orgFindMembers orgFindProjects orgFindApps orgInvite orgRemoveMember orgSetMemberAccess orgUpdate orgNew projectAddTags projectArchive projectUnarchive projectClone projectDecreasePermissions projectDescribe projectDestroy projectInvite projectLeave projectListFolder projectMove projectNewFolder projectRemoveFolder projectRemoveObjects projectRemoveTags projectRenameFolder projectSetProperties projectTransfer projectUpdate projectUpdateSponsorship projectNew recordAddTags recordAddTypes recordClose recordDescribe recordGetDetails recordListProjects recordRemoveTags recordRemoveTypes recordRename recordSetDetails recordSetProperties recordSetVisibility recordNew systemDescribeDataObjects systemDescribeExecutions systemDescribeProjects systemFindAffiliates systemFindApps systemFindDataObjects systemFindGlobalWorkflows systemResolveDataObjects systemFindExecutions systemFindAnalyses systemFindDatabases systemFindJobs systemFindProjects systemFindUsers systemFindProjectMembers systemFindOrgs systemGenerateBatchInputs systemGlobalSearch systemGreet systemHeaders systemShortenURL systemWhoami userDescribe userUpdate workflowAddStage workflowAddTags workflowAddTypes workflowClose workflowDescribe workflowDryRun workflowGetDetails workflowIsStageCompatible workflowListProjects workflowMoveStage workflowOverwrite workflowRemoveStage workflowRemoveTags workflowRemoveTypes workflowRename workflowRun workflowValidateBatch workflowSetDetails workflowSetProperties workflowSetVisibility workflowUpdate workflowUpdateStageExecutable workflowNew); diff --git a/contrib/ruby/generateRubyAPIWrappers.py b/contrib/ruby/generateRubyAPIWrappers.py index 899aa5f533..e8c4922d1c 100755 --- a/contrib/ruby/generateRubyAPIWrappers.py +++ b/contrib/ruby/generateRubyAPIWrappers.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # # Copyright (C) 2013-2016 DNAnexus, Inc. # diff --git a/contrib/ruby/lib/dxruby/api.rb b/contrib/ruby/lib/dxruby/api.rb index f7d9c95346..d5667eb020 100644 --- a/contrib/ruby/lib/dxruby/api.rb +++ b/contrib/ruby/lib/dxruby/api.rb @@ -511,6 +511,118 @@ def self.database_list_folder(object_id, input_params={}, opts={}) return DX::http_request("/#{object_id}/listFolder", input_params, opts) end + # Invokes the /dbcluster-xxxx/addTags API method. + # + # For more info, see: https://documentation.dnanexus.com/developer/api/introduction-to-data-object-metadata/tags#api-method-class-xxxx-addtags + def self.dbcluster_add_tags(object_id, input_params={}, opts={}) + opts = { "always_retry" => true }.merge(opts) + return DX::http_request("/#{object_id}/addTags", input_params, opts) + end + + # Invokes the /dbcluster-xxxx/addTypes API method. + # + # For more info, see: https://documentation.dnanexus.com/developer/api/data-object-lifecycle/types#api-method-class-xxxx-addtypes + def self.dbcluster_add_types(object_id, input_params={}, opts={}) + opts = { "always_retry" => true }.merge(opts) + return DX::http_request("/#{object_id}/addTypes", input_params, opts) + end + + # Invokes the /dbcluster-xxxx/describe API method. + # + # For more info, see: https://documentation.dnanexus.com/developer/api/introduction-to-data-object-classes/dbclusters#api-method-dbcluster-xxxx-describe + def self.dbcluster_describe(object_id, input_params={}, opts={}) + opts = { "always_retry" => true }.merge(opts) + return DX::http_request("/#{object_id}/describe", input_params, opts) + end + + # Invokes the /dbcluster-xxxx/getDetails API method. + # + # For more info, see: https://documentation.dnanexus.com/developer/api/data-object-lifecycle/details-and-links#api-method-class-xxxx-getdetails + def self.dbcluster_get_details(object_id, input_params={}, opts={}) + opts = { "always_retry" => true }.merge(opts) + return DX::http_request("/#{object_id}/getDetails", input_params, opts) + end + + # Invokes the /dbcluster/new API method. + # + # For more info, see: https://documentation.dnanexus.com/developer/api/introduction-to-data-object-classes/dbclusters#api-method-dbcluster-new + def self.dbcluster_new(input_params={}, opts={}) + opts = { "always_retry" => false }.merge(opts) + return DX::http_request("/dbcluster/new", input_params, opts) + end + + # Invokes the /dbcluster-xxxx/removeTags API method. + # + # For more info, see: https://documentation.dnanexus.com/developer/api/introduction-to-data-object-metadata/tags#api-method-class-xxxx-removetags + def self.dbcluster_remove_tags(object_id, input_params={}, opts={}) + opts = { "always_retry" => true }.merge(opts) + return DX::http_request("/#{object_id}/removeTags", input_params, opts) + end + + # Invokes the /dbcluster-xxxx/removeTypes API method. + # + # For more info, see: https://documentation.dnanexus.com/developer/api/data-object-lifecycle/types#api-method-class-xxxx-removetypes + def self.dbcluster_remove_types(object_id, input_params={}, opts={}) + opts = { "always_retry" => true }.merge(opts) + return DX::http_request("/#{object_id}/removeTypes", input_params, opts) + end + + # Invokes the /dbcluster-xxxx/rename API method. + # + # For more info, see: https://documentation.dnanexus.com/developer/api/introduction-to-data-object-metadata/name#api-method-class-xxxx-rename + def self.dbcluster_rename(object_id, input_params={}, opts={}) + opts = { "always_retry" => true }.merge(opts) + return DX::http_request("/#{object_id}/rename", input_params, opts) + end + + # Invokes the /dbcluster-xxxx/setDetails API method. + # + # For more info, see: https://documentation.dnanexus.com/developer/api/data-object-lifecycle/details-and-links#api-method-class-xxxx-setdetails + def self.dbcluster_set_details(object_id, input_params={}, opts={}) + opts = { "always_retry" => true }.merge(opts) + return DX::http_request("/#{object_id}/setDetails", input_params, opts) + end + + # Invokes the /dbcluster-xxxx/setProperties API method. + # + # For more info, see: https://documentation.dnanexus.com/developer/api/introduction-to-data-object-metadata/properties#api-method-class-xxxx-setproperties + def self.dbcluster_set_properties(object_id, input_params={}, opts={}) + opts = { "always_retry" => true }.merge(opts) + return DX::http_request("/#{object_id}/setProperties", input_params, opts) + end + + # Invokes the /dbcluster-xxxx/setVisibility API method. + # + # For more info, see: https://documentation.dnanexus.com/developer/api/data-object-lifecycle/visibility#api-method-class-xxxx-setvisibility + def self.dbcluster_set_visibility(object_id, input_params={}, opts={}) + opts = { "always_retry" => true }.merge(opts) + return DX::http_request("/#{object_id}/setVisibility", input_params, opts) + end + + # Invokes the /dbcluster-xxxx/start API method. + # + # For more info, see: https://documentation.dnanexus.com/developer/api/introduction-to-data-object-classes/dbclusters#api-method-dbcluster-xxxx-start + def self.dbcluster_start(object_id, input_params={}, opts={}) + opts = { "always_retry" => true }.merge(opts) + return DX::http_request("/#{object_id}/start", input_params, opts) + end + + # Invokes the /dbcluster-xxxx/stop API method. + # + # For more info, see: https://documentation.dnanexus.com/developer/api/introduction-to-data-object-classes/dbclusters#api-method-dbcluster-xxxx-stop + def self.dbcluster_stop(object_id, input_params={}, opts={}) + opts = { "always_retry" => true }.merge(opts) + return DX::http_request("/#{object_id}/stop", input_params, opts) + end + + # Invokes the /dbcluster-xxxx/terminate API method. + # + # For more info, see: https://documentation.dnanexus.com/developer/api/introduction-to-data-object-classes/dbclusters#api-method-dbcluster-xxxx-terminate + def self.dbcluster_terminate(object_id, input_params={}, opts={}) + opts = { "always_retry" => true }.merge(opts) + return DX::http_request("/#{object_id}/terminate", input_params, opts) + end + # Invokes the /file-xxxx/addTags API method. # # For more info, see: https://documentation.dnanexus.com/developer/api/introduction-to-data-object-metadata/tags#api-method-class-xxxx-addtags @@ -815,6 +927,22 @@ def self.job_terminate(object_id, input_params={}, opts={}) return DX::http_request("/#{object_id}/terminate", input_params, opts) end + # Invokes the /job-xxxx/update API method. + # + # For more info, see: https://documentation.dnanexus.com/developer/api/running-analyses/applets-and-entry-points#api-method-job-xxxx-update + def self.job_update(object_id, input_params={}, opts={}) + opts = { "always_retry" => true }.merge(opts) + return DX::http_request("/#{object_id}/update", input_params, opts) + end + + # Invokes the /job-xxxx/getIdentityToken API method. + # + # For more info, see: https://documentation.dnanexus.com/developer/api/running-analyses/applets-and-entry-points#api-method-job-xxxx-getIdentityToken + def self.job_get_identity_token(object_id, input_params={}, opts={}) + opts = { "always_retry" => true }.merge(opts) + return DX::http_request("/#{object_id}/getIdentityToken", input_params, opts) + end + # Invokes the /job/new API method. # # For more info, see: https://documentation.dnanexus.com/developer/api/running-analyses/applets-and-entry-points#api-method-job-new diff --git a/debian/changelog b/debian/changelog index 4f50bfaacf..1ae06d0e6d 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,426 @@ +dx-toolkit (0.363.0) trusty; urgency=medium + + * Added: Support dataset and CB records with Integer and Float type global primary keys as input in create_cohort + * Fixed: Project context added for loading dataset descriptor file when using dx extract_dataset + + -- Kurt Jensen Mon, 30 Oct 2023 12:12:07 +0000 + +dx-toolkit (0.362.0) trusty; urgency=medium + + * Added: archival_state param to find_data_objects() + + -- Kurt Jensen Sat, 21 Oct 2023 12:20:15 +0000 + +dx-toolkit (0.361.0) trusty; urgency=medium + + * Added: --fields-file argument in dx extract_dataset + * Added: ALT and alt_index columns in --additional-fields of dx extract_assay somatic + * Fixed: dx extract_assay error message when no valid assay is found + * Fixed: In dx extract_assay germline, handled duplicate RSIDs, output sorting order, changed location filter range from 250M to 5M + * Fixed: Handled white spaces in dx extract command's inputs which are of the type string of comma separated values + * Fixed: Released Nextaur 1.6.8. It contains minor bugfixes and optimizations. + * Fixed: --retrieve-genotype --sql bug in UKB RAP region + * Fixed: Retry API call in object_exists_in_project() + + -- Kurt Jensen Mon, 16 Oct 2023 14:16:29 +0000 + +dx-toolkit (0.360.0) trusty; urgency=medium + + * Fixed: Released Nextaur 1.6.7. It adds DNAnexus docker image support feature and contains errorStrategy bugfixes + + -- Kurt Jensen Fri, 29 Sep 2023 15:23:46 +0000 + +dx-toolkit (0.359.0) trusty; urgency=medium + + * Added: `dx create_cohort` + * Added: Nextflow pipeline readme file is used as a readme file of Nextflow applet + * Added: Default optional inputs nextflow_soft_confs and nextflow_params_file to Nextflow applets to support soft configuration override and custom parameters file in nextflow run + + -- Kurt Jensen Fri, 22 Sep 2023 19:33:04 +0000 + +dx-toolkit (0.358.0) trusty; urgency=medium + + * Fixed: `dx find org projects --property` in Python 3 + + -- Kurt Jensen Fri, 15 Sep 2023 18:48:21 +0000 + +dx-toolkit (0.357.0) trusty; urgency=medium + + * Fixed: Nextflow applets passes schema input params explicitly in nextflow run command, as parameters assigned in runtime config are not handled properly by nextflow-io + * Fixed: Unexpected splitting at whitespaces inside quotes when parsing string-type input parameters of Nextflow applets + * Changed: Remove optional output param nextflow_log from Nextflow pipeline applets; instead, always upload Nextflow log file to head job destination when execution completes + + -- Kurt Jensen Fri, 01 Sep 2023 17:02:49 +0000 + +dx-toolkit (0.356.0) trusty; urgency=medium + + * Fixed: dx describe analysis-xxxx --json --verbose + + -- Kurt Jensen Wed, 16 Aug 2023 16:50:06 +0000 + +dx-toolkit (0.355.0) trusty; urgency=medium + + * Added: New return fields from dx describe {job/analysis}-xxxx with --verbose argument: 'runSystemRequirements', 'runSystemRequirementsByExecutable', 'mergedSystemRequirementsByExecutable', 'runStageSystemRequirements' + * Added: --monthly-compute-limit and --monthly-egress-bytes-limit for dx new project + * Added: Option '--instance-type-by-executable' for dx run and dx-jobutil-job-new + * Added: Parameters system_requirements and system_requirements_by_executable for DXExecutable.run() and DXJob.new() + + -- Kurt Jensen Tue, 01 Aug 2023 12:19:58 +0000 + +dx-toolkit (0.354.0) trusty; urgency=medium + + * Added: '--try T' for dx watch, dx tag/untag, dx set_properties/unset_properties + * Added: '--include-restarted' for dx find executions/jobs/analyses + * Added: Fields related to restarted jobs to dx describe job-xxxx + * Changed: dxpy User-Agent header to include Python version + + -- Kurt Jensen Mon, 24 Jul 2023 15:40:58 +0000 + +dx-toolkit (0.353.0) trusty; urgency=medium + + * Added: New argument '--instance-type-by-executable' arg for dx run and dx-jobutil-job-new + * Added: New return fields from dx describe {job/analysis}-xxxx with --verbose argument: 'runSystemRequirements', 'runSystemRequirementsByExecutable', 'mergedSystemRequirementsByExecutable', 'runStageSystemRequirements' + * Added: `dx watch --metrics top` + + -- Kurt Jensen Thu, 13 Jul 2023 02:39:19 +0000 + +dx-toolkit (0.352.0) trusty; urgency=medium + + * Added: `dx extract_assay somatic` + * Fixed: Log truncation for strings > 8000 bytes + + -- Kurt Jensen Fri, 07 Jul 2023 17:49:36 +0000 + +dx-toolkit (0.351.0) trusty; urgency=medium + + * No significant changes + + -- Kurt Jensen Fri, 23 Jun 2023 17:14:34 +0000 + +dx-toolkit (0.350.0) trusty; urgency=medium + + * Added: `dx watch` support for detailed job metrics (cpu, memory, network, disk io, etc every 60s) + * Added: '--detailed-job-metrics-collect-default' for `dx update org` + * Added: '--detailed-job-metrics' for `dx run` + + -- Kurt Jensen Thu, 15 Jun 2023 21:47:25 +0000 + +dx-toolkit (0.349.0) trusty; urgency=medium + + * Added: external_upload_restricted to DXProject + * Added: `dx extract_assay` for genomic assay model data + + -- Kurt Jensen Fri, 09 Jun 2023 03:39:28 +0000 + +dx-toolkit (0.348.0) trusty; urgency=medium + + * Added: dxpy dependencies test suite + * Changed: Optimizations in Nextflow Pipeline Applet script to make fewer API calls when concluding a subjob + + -- Kurt Jensen Thu, 11 May 2023 14:35:53 +0000 + +dx-toolkit (0.347.0) trusty; urgency=medium + + * Changed: Removed unneeded gnureadline dependency + * Changed: Bumped allowed colorama version to 0.4.6 + * Fixed: Removed unused rlcompleter import which may break alternative readline implementations + + -- Kurt Jensen Thu, 20 Apr 2023 16:28:33 +0000 + +dx-toolkit (0.346.0) trusty; urgency=medium + + * Fixed: Released Nextaur 1.6.6. It includes fixes to errorStrategy handling and an update to the way AWS instance types are selected based on resource requirements in Nextflow pipelines (V2 instances are now preferred) + * Fixed: ImportError in test_dxpy.py + * Fixed: Replaced obsolete built-in file() method with open() + * Fixed: Printing HTTP error codes and messages that were hidden + * Changed: Help message of the dx make_download_url command + + -- Kurt Jensen Thu, 13 Apr 2023 14:00:12 +0000 + +dx-toolkit (0.345.0) trusty; urgency=medium + + * Fixed: Tab completion in interactive execution of dx-app-wizard + * Fixed: dx-app-wizard script on Windows + * Fixed: Tab completion in interactive executions on Windows + * Changed: Released Nextaur 1.6.5. It added a caching mechanism to DxPath file and folder resolution, which reduces number of DX API calls made during pipeline execution. It also fixes an occasional hanging of the headjob. + * Changed: Tab completion in interactive executions now works with libedit bundled in MacOS and does not require externally installed GNU readline + * Changed: Bump allowed cryptography dxpy dependency version to 40.0.x + + -- Kurt Jensen Mon, 03 Apr 2023 04:17:30 +0000 + +dx-toolkit (0.344.0) trusty; urgency=medium + + * Added: Support for file (un)archival in DXJava + * Added: archivalStatus field to DXFile describe in DXJava + * Added: archivalStatus filtering support to DXSearch in DXJava + * Added: `dx run` support for '--preserve-job-outputs' and '--preserve-job-outputs' parameter + * Added: `dx describe` for jobs and alayses outputs 'Preserve Job Outputs Folder' field + * Added: Record the dxpy version used for the Nextflow build in applet's metadata and job log + * Fixed: Nextflow profiles runtime overriding fix + * Changed: Released Nextaur 1.6.4. It includes a fix to folder download, minor fixes and default headjob instance update (mem2_ssd1_v2_x4 for AWS, mem2_ssd1_x4 for Azure) + + -- Kurt Jensen Fri, 24 Mar 2023 21:10:14 +0000 + +dx-toolkit (0.343.0) trusty; urgency=medium + + * Fixed: Do not print job runtime if stopppedRunning not in describe + * Changed: Released Nextaur 1.6.3 (includes updated wait times for file upload and close) + * Changed: Upgraded Nextflow to 22.10.7 + * Changed: Removed Nextflow assets from aws:eu-west-2 + + -- Kurt Jensen Wed, 08 Mar 2023 21:33:21 +0000 + +dx-toolkit (0.342.0) trusty; urgency=medium + + * Added: Pretty-print spotCostSavings field in dx describe`job-xxxx` + * Changed: Released Nextaur 1.6.2. It includes bugfixes and default value of maxTransferAttempts is set to 3 + + -- Kurt Jensen Fri, 03 Mar 2023 21:49:32 +0000 + +dx-toolkit (0.341.0) trusty; urgency=medium + + * Added: '--list-fields', '--list-entities', '--entities' arguments for `dx extract_dataset` + * Added: `dx ssh` to connect to job's public hostname if job is httpsApp enabled + * Fixed: Helpstring of '--verbose' arg + * Changed: Released Nextaur 1.6.1. It includes an optimization of certain API calls and adds `docker pull` retry in Nextflow pipelines + * Changed: Increased dxpy HTTP timeout to 15 minutes + + -- Kurt Jensen Sat, 25 Feb 2023 13:43:02 +0000 + +dx-toolkit (0.340.0) trusty; urgency=medium + + * Changed: Upgraded Nextflow plugin version to 1.6.0 + * Changed: Nextflow - updated default instance types based on destination region + * Fixed: Nextflow errorStrategy retry ends in 'failed' state if last retry fails + + -- Kurt Jensen Fri, 10 Feb 2023 19:00:03 +0000 + +dx-toolkit (0.339.0) trusty; urgency=medium + + * No significant changes + + -- Kurt Jensen Fri, 27 Jan 2023 17:32:58 +0000 + +dx-toolkit (0.338.0) trusty; urgency=medium + + * Added: Support for Granular Spot wait times in dx run using --max-tree-spot-wait-time and --max-job-spot-wait-time + * Added: Printing of Spot wait times in dx describe for jobs and workflows + * Added: Support for private Docker images in Nextflow pipelines on subjob level + * Fixed: How dx get database command reads from the API server with the API proxy interceptor + * Fixed: Regex global flags in path matching to support Python 3.11 + * Fixed: dx run --clone for Nextflow jobs (clear cloned job's properties) + * Changed: Upgraded Nextflow plugin version to 1.5.0 + + + -- Kurt Jensen Sat, 21 Jan 2023 04:17:21 +0000 + +dx-toolkit (0.337.0) trusty; urgency=medium + + * Changed: Upgraded Nextflow plugin version to 1.4.0 + * Changed: Failed Nextflow subjobs with 'terminate' errorStrategy finish in 'failed' state + * Changed: Updated Nextflow last error message in case 'ignore' errorStrategy is applied. + * Changed: Exposed help messages for `dx build --nextflow` + + -- Kurt Jensen Sat, 07 Jan 2023 13:19:29 +0000 + +dx-toolkit (0.336.0) trusty; urgency=medium + + * No significant changes + + -- Kurt Jensen Mon, 12 Dec 2022 22:01:13 +0000 + +dx-toolkit (0.335.0) trusty; urgency=medium + + * Added: Group name for developer options in Nextflow pipeline applet + * Fixed: Printing too many environment values with debug set to true (Nextflow) + * Fixed: Missing required inputs passed to nextflow run + * Fixed: Preserving folder structure when publishing Nextflow output files + + -- Kurt Jensen Fri, 02 Dec 2022 22:23:03 +0000 + +dx-toolkit (0.334.0) trusty; urgency=medium + + * Added: '--external-upload-restricted' flag for `dx update project` and `dx find projects` + * Added: Support for '--destination' in `nextflow build --repository` + * Added: `resume` and `preserve_cache` input arguments to Nextflow applets + * Added: Error handling with Nextflow's errorStrategy + * Added: `region` argument to `DXProject.new()` + * Fixed: Retrieving session config when no parent process exists + * Fixed: Error describing global workflows by adding a resources container as a hint + + -- Kurt Jensen Wed, 23 Nov 2022 21:53:17 +0000 + +dx-toolkit (0.333.0) trusty; urgency=medium + + * Added: `nextflow run` command in the log for debugging + * Fixed: Overriding config arguments with an empty string for nextflow pipelines + * Changed: dxpy psutil requirement to >= 5.9.3 for macOS arm + * Changed: Set `ignoreReuse` in the nextflow applet template + * Set `restartableEntryPoints` to 'all' in the nextflow pipele applet's runSpec + + -- Kurt Jensen Fri, 04 Nov 2022 14:58:04 +0000 + +dx-toolkit (0.332.0) trusty; urgency=medium + + * Added: A warning for `dx build` when app(let)'s name is set both in --extra-args and --destination + * Fixed: An error when setting app(let)s name in `dx build` (now the name set via --extra-args properly overrides the one set via --destination) + * Fixed: `dx build --nextflow --repository` returns json instead of a simple string + * Changed: Help for building Nextflow pipelines is suppressed + + -- Kurt Jensen Fri, 14 Oct 2022 22:21:11 +0000 + +dx-toolkit (0.331.0) trusty; urgency=medium + + * Added: `dx find jobs --json` and `dx describe --verbose job-xxxx` with --verbose argument return field internetUsageIPs if the caller an org admin and has enabled jobInternetUsageMonitoring license feature + * Added: Nextflow applets no longer have default arguments and required inputs + * Fixed: dx `describe user-xxxx` will not try to print the name if it is not present in the API response + + -- Kurt Jensen Tue, 04 Oct 2022 12:41:05 +0000 + +dx-toolkit (0.330.0) trusty; urgency=medium + + * Added: Initial nextflow support + * Fixed: Do not check python3 syntax with python2 and vice versa in dx `build` + * Fixed: `dx build` properly verifies the applet's name given in the extra-args parameter + + -- Kurt Jensen Sat, 24 Sep 2022 01:28:01 +0000 + +dx-toolkit (0.329.0) trusty; urgency=medium + + * Added: `dx extract_dataset` command + * Added: Optional pandas dependency for dxpy + * Changed: dxpy.find_one_project, dxpy.find_one_data_object, dxpy.find_one_app raise DXError if zero_ok argument is not a bool + + -- Kurt Jensen Thu, 08 Sep 2022 19:39:40 +0000 + +dx-toolkit (0.328.0) trusty; urgency=medium + + * Added: --head-job-on-demand argument for dx run app(let)-xxxx + * Added: --head-job-on-demand argument for dx-jobutil-new-job + * Added: --on-behalf-of argument for dx new user + * Changed: dx-toolkit never included in execDepends when building app(lets) with dx build + * Fixed: Reduce the number of API calls for dx run applet-xxxx and dx run workflow-xxxx + * Fixed: dx upload f1 f2 --visibility hidden now correctly marks both files as hidden + * Fixed: `dx upload` retry on all types of SSL errors + * Deprecated: --no-dx-toolkit-autodep option for dx build + + -- Kurt Jensen Fri, 12 Aug 2022 15:07:33 +0000 + +dx-toolkit (0.327.0) trusty; urgency=medium + + * Added: `dx extract_dataset` command + * Added: pandas requirement for dxpy + * Fixed: Parsing ignoreReuse in `dx build` of workflow + * Changed: DXHTTPRequest to pass ssl_context + + -- Kurt Jensen Fri, 08 Jul 2022 00:19:26 +0000 + +dx-toolkit (0.326.0) trusty; urgency=medium + + * Added: '--rank' arg for `dx run` + + -- Kurt Jensen Wed, 25 May 2022 18:34:17 +0000 + +dx-toolkit (0.325.0) trusty; urgency=medium + + * Fixed: `dx describe` of executable with bundledDepends that is not an asset + + -- Kurt Jensen Fri, 13 May 2022 15:01:33 +0000 + +dx-toolkit (0.324.0) trusty; urgency=medium + + * Fixed: `dx build` comparison of workflow directory to workflow name + * Fixed: Set projejct argument for `dx run --detach` from inside a job + * Fixed: Reliability of symlink downloads with `aria2c` + * Changed: Removed `wget` as an option for downloading symlinked files + * Changed: Bump allowed requests dxpy dependency version to 2.27.1 + + -- Kurt Jensen Thu, 28 Apr 2022 21:10:40 +0000 + +dx-toolkit (0.323.0) trusty; urgency=medium + + * Changed: Do not list folder objects during `dx cd` + + -- Kurt Jensen Tue, 05 Apr 2022 20:10:51 +0000 + +dx-toolkit (0.322.0) trusty; urgency=medium + + * Added: API wrappers for dbcluster + + -- Kurt Jensen Wed, 23 Feb 2022 18:24:26 +0000 + +dx-toolkit (0.321.0) trusty; urgency=medium + + * Fixed: KeyError in `dx-app-wizard --json` + * Changed: dxjava dependencies log4j2, jackson-databind + + -- Kurt Jensen Tue, 01 Feb 2022 18:45:23 +0000 + +dx-toolkit (0.320.0) trusty; urgency=medium + + * Fixed: Python 3.10 collections imports + * Fixed: Recursive folder download `dx download -r` of folders with matching prefix + + -- Kurt Jensen Sat, 22 Jan 2022 00:44:30 +0000 + +dx-toolkit (0.319.0) trusty; urgency=medium + + * Fixed: Incorrect setting of the folder input option when building global workflows + * Added: Setting properties when building global workflows + * Added: '--allow-ssh' parameter to dx ssh + * Added: '--no-firewall-update' parameter to dx ssh + * Changed: Detect client IP for SSH access to job instead of'*' + + -- Kurt Jensen Thu, 06 Jan 2022 19:14:57 +0000 + +dx-toolkit (0.318.0) trusty; urgency=medium + + * Fixed: Python 3.10 MutableMapping import + * Added: '--no-temp-build-project' for single region app builds + * Added: '--from' option to `dx build` for building a global workflow from a project-based workflow, including a workflow built using WDL + + -- Kurt Jensen Tue, 07 Dec 2021 23:54:38 +0000 + +dx-toolkit (0.317.0) trusty; urgency=medium + + * Fixed: Reduce file-xxxx/describe API load during dx upload + * Fixed: `dx get` uses a region compatible with user's billTo when downloading resources + * Changed: `dx run` warns users if priority is specified as low/normal when using '--watch/ssh/allow-ssh' + + -- Kurt Jensen Wed, 17 Nov 2021 23:41:19 +0000 + +dx-toolkit (0.316.0) trusty; urgency=medium + + * Fixed: Python 3 SSH Host key output in `dx describe job-xxxx` + * Added: Support for dxpy on macOS arm64 + * Added: Path input for `dx list database files` + * Changed: dxpy dependencies cryptography, websocket-client, colorama, requests + + -- Kurt Jensen Thu, 28 Oct 2021 22:05:36 +0000 + +dx-toolkit (0.315.0) trusty; urgency=medium + + * No significant changes + + -- Kurt Jensen Fri, 27 Aug 2021 22:20:17 +0000 + +dx-toolkit (0.314.0) trusty; urgency=medium + + * Fixed: Retry failed part uploads + * Fixed: `dx run --project/--destination/--folder` now submits analysis to given project or path + * Added: Support FIPS enabled Python + * Added: `dx archive` and `dx unarchive` commands + + -- Kurt Jensen Wed, 18 Aug 2021 05:58:15 +0000 + +dx-toolkit (0.313.0) trusty; urgency=medium + + * Added: '--cost-limit' arg for `dx run` + * Added: '--database-ui-view-only' flag for `dx new project` + * Added: Currency formatting in `dx describe` + + -- Kurt Jensen Tue, 06 Jul 2021 20:07:00 +0000 + dx-toolkit (0.312.0) trusty; urgency=medium * No significant changes diff --git a/debian/control b/debian/control index 1988d9bfd4..4bf085a84d 100644 --- a/debian/control +++ b/debian/control @@ -1,17 +1,9 @@ Source: dx-toolkit -Maintainer: Phil Sung +Maintainer: support@dnanexus.com Section: science Priority: optional Standards-Version: 3.9.3 -Build-Depends: debhelper (>= 8), python-virtualenv | python3-virtualenv, libboost1.58-all-dev | libboost-all-dev | libboost1.55-all-dev | libboost1.48-all-dev, openjdk-7-jdk | openjdk-8-jdk-headless | openjdk-11-jdk-headless | openjdk-8-jdk, maven2 | maven - -Package: dx-toolkit -Architecture: any -Depends: ${shlibs:Depends}, ${misc:Depends}, ${python:Depends} -Conflicts: jq, python-argcomplete -Description: DNAnexus client libraries and tools - Bindings for interacting with the DNAnexus platform and common tools for - developer use (inside or outside of the DNAnexus execution environment). +Build-Depends: openjdk-7-jdk | openjdk-8-jdk-headless | openjdk-11-jdk-headless | openjdk-8-jdk, maven2 | maven Package: dx-java-bindings Architecture: any diff --git a/debian/rules b/debian/rules index 30221657d5..8068436a9d 100755 --- a/debian/rules +++ b/debian/rules @@ -5,7 +5,6 @@ override_dh_auto_build: override_dh_auto_install: - DESTDIR="$(CURDIR)/debian/dx-toolkit" PREFIX="/usr" $(MAKE) -C src debian_install DESTDIR="$(CURDIR)/debian/dx-java-bindings" PREFIX="/usr" $(MAKE) -C src debian_java_install override_dh_auto_test: diff --git a/doc/examples/dx-apps/python_trimmer_example/dxapp.json b/doc/examples/dx-apps/python_trimmer_example/dxapp.json index 8aa4edfc3d..dcd6b01ca6 100644 --- a/doc/examples/dx-apps/python_trimmer_example/dxapp.json +++ b/doc/examples/dx-apps/python_trimmer_example/dxapp.json @@ -20,8 +20,8 @@ ], "runSpec": { "interpreter": "python3", - "release": "16.04", - "version": "1", + "release": "20.04", + "version": "0", "distribution": "Ubuntu", "file": "src/python_trimmer_example.py" } diff --git a/src/Makefile b/src/Makefile index e2d2bcf482..839ff9312c 100644 --- a/src/Makefile +++ b/src/Makefile @@ -18,7 +18,7 @@ include mk/config.mk include mk/Makefile.${PLATFORM} -all: api_wrappers cpp src_libs python dx-verify-file jq dx-docker +all: api_wrappers cpp src_libs python dx-verify-file dx-docker python: api_wrappers toolkit_version $(DNANEXUS_HOME)/bin/dx @@ -136,18 +136,10 @@ install: base_install doc: doc_python doc_dxcpp doc_dxjson doc_java -setup_doc_virtualenv: .setup_doc_virtualenv - -.setup_doc_virtualenv: ../build/doc_build_requirements.txt - @if ! which pip > /dev/null; then echo "pip not found, please run apt-get install python-pip"; exit 1; fi - @if ! which virtualenv > /dev/null; then echo "virtualenv not found, please run apt-get install python-virtualenv"; exit 1; fi - (unset PYTHONPATH; $(VIRTUAL_ENV) ../build/doc_env; source ../build/doc_env/$(ACTIVATE); pip install --find-links=http://dnanexus-pypi2.s3.amazonaws.com/index.html --requirement=../build/doc_build_requirements.txt) - $(VIRTUAL_ENV) --relocatable ../build/doc_env - touch .setup_doc_virtualenv - -doc_python: python setup_doc_virtualenv +doc_python: python rm -rf ../doc/python/* - source ../build/doc_env/$(ACTIVATE); source "../environment"; export PYTHONPATH="$${PYTHONPATH}:../lib"; $(MAKE) -C python/doc html + ${PIP} install --requirement=../build/doc_build_requirements.txt + $(MAKE) -C python/doc html doc_dxcpp: mkdir -p ../doc/cpp/dxcpp @@ -298,7 +290,7 @@ CARES_PV=1.13.0 CARES_SHA=dde50284cc3d505fb2463ff6276e61d5531b1d68 c-ares/stage/lib/libcares.la: shasum mkdir -p c-ares - curl https://c-ares.haxx.se/download/c-ares-${CARES_PV}.tar.gz > c-ares-${CARES_PV}.tar.gz + curl https://c-ares.org/archive/c-ares-${CARES_PV}.tar.gz > c-ares-${CARES_PV}.tar.gz [[ $$(shasum c-ares-${CARES_PV}.tar.gz|cut -f 1 -d ' ') == $(CARES_SHA) ]] tar -xzf c-ares-${CARES_PV}.tar.gz -C c-ares --strip-components=1 cd c-ares; ./configure --prefix=/ @@ -328,18 +320,14 @@ git_submodules: fi) -jq: git_submodules ../bin/jq - - # Clean # ===== distclean: clean -clean: clean_jq +clean: $(MAKE) -C dx-verify-file clean $(MAKE) -C ua clean - -rm -f ../bin/jq -find ../bin -type f \( -name dx -or -name 'dx-*' \) -not -name 'dx-unpack*' -not -name 'dx-su-*' -delete -rm -f ../bin/docker2aci ../bin/proot -rm -rf python/{build,*.{egg,egg-info}} @@ -350,12 +338,4 @@ clean: clean_jq -rm -rf "$(DX_PY_ENV)" -rm -rf boost c-ares curl file openssl -# Check that the 'jq' makefile exists, before trying to clean up. -# We checkout and build the 'jq' package, it is an external -# package. -clean_jq: - (if [ -e jq/Makefile ]; then \ - $(MAKE) -C jq clean; \ - fi) - -.PHONY: all toolkit_version api_wrappers src_libs python pynsist_installer java ua test test_python install base_install debian_install doc setup_doc_build_virtualenv doc_python doc_dxcpp doc_dxjson doc_java R doc_R boost boost_build install_sysdeps +.PHONY: all toolkit_version api_wrappers src_libs python java ua test test_python install base_install debian_install doc setup_doc_build_virtualenv doc_python doc_dxcpp doc_dxjson doc_java R doc_R boost boost_build install_sysdeps diff --git a/src/R/dxR/DESCRIPTION b/src/R/dxR/DESCRIPTION index a4267450e2..9a7c6cd4ef 100644 --- a/src/R/dxR/DESCRIPTION +++ b/src/R/dxR/DESCRIPTION @@ -1,7 +1,7 @@ Package: dxR Type: Package Title: DNAnexus R Client Library -Version: 0.312.0 +Version: 0.388.0 Author: Katherine Lai Maintainer: Katherine Lai Description: dxR is an R extension containing API wrapper functions for diff --git a/src/R/dxR/R/api.R b/src/R/dxR/R/api.R index 033ce02159..8a1f8b1265 100644 --- a/src/R/dxR/R/api.R +++ b/src/R/dxR/R/api.R @@ -2163,6 +2163,462 @@ databaseListFolder <- function(objectID, alwaysRetry=alwaysRetry) } +##' dbclusterAddTags API wrapper +##' +##' This function makes an API call to the \code{/dbcluster-xxxx/addTags} API +##' method; it is a simple wrapper around the \code{\link{dxHTTPRequest}} +##' function which makes POST HTTP requests to the API server. +##' +##' +##' @param objectID DNAnexus object ID +##' @param inputParams Either an R object that will be converted into JSON +##' using \code{RJSONIO::toJSON} to be used as the input to the API call. If +##' providing the JSON string directly, you must set \code{jsonifyData} to +##' \code{FALSE}. +##' @param jsonifyData Whether to call \code{RJSONIO::toJSON} on +##' \code{inputParams} to create the JSON string or pass through the value of +##' \code{inputParams} directly. (Default is \code{TRUE}.) +##' @param alwaysRetry Whether to always retry even when no response is +##' received from the API server +##' @return If the API call is successful, the parsed JSON of the API server +##' response is returned (using \code{RJSONIO::fromJSON}). +##' @export +##' @seealso \code{\link{dxHTTPRequest}} +##' @references API spec documentation: \url{https://documentation.dnanexus.com/developer/api/introduction-to-data-object-metadata/tags#api-method-class-xxxx-addtags} +dbclusterAddTags <- function(objectID, + inputParams=emptyNamedList, + jsonifyData=TRUE, + alwaysRetry=TRUE) { + resource <- paste('/', objectID, '/', 'addTags', sep='') + dxHTTPRequest(resource, + inputParams, + jsonifyData=jsonifyData, + alwaysRetry=alwaysRetry) +} + +##' dbclusterAddTypes API wrapper +##' +##' This function makes an API call to the \code{/dbcluster-xxxx/addTypes} API +##' method; it is a simple wrapper around the \code{\link{dxHTTPRequest}} +##' function which makes POST HTTP requests to the API server. +##' +##' +##' @param objectID DNAnexus object ID +##' @param inputParams Either an R object that will be converted into JSON +##' using \code{RJSONIO::toJSON} to be used as the input to the API call. If +##' providing the JSON string directly, you must set \code{jsonifyData} to +##' \code{FALSE}. +##' @param jsonifyData Whether to call \code{RJSONIO::toJSON} on +##' \code{inputParams} to create the JSON string or pass through the value of +##' \code{inputParams} directly. (Default is \code{TRUE}.) +##' @param alwaysRetry Whether to always retry even when no response is +##' received from the API server +##' @return If the API call is successful, the parsed JSON of the API server +##' response is returned (using \code{RJSONIO::fromJSON}). +##' @export +##' @seealso \code{\link{dxHTTPRequest}} +##' @references API spec documentation: \url{https://documentation.dnanexus.com/developer/api/data-object-lifecycle/types#api-method-class-xxxx-addtypes} +dbclusterAddTypes <- function(objectID, + inputParams=emptyNamedList, + jsonifyData=TRUE, + alwaysRetry=TRUE) { + resource <- paste('/', objectID, '/', 'addTypes', sep='') + dxHTTPRequest(resource, + inputParams, + jsonifyData=jsonifyData, + alwaysRetry=alwaysRetry) +} + +##' dbclusterDescribe API wrapper +##' +##' This function makes an API call to the \code{/dbcluster-xxxx/describe} API +##' method; it is a simple wrapper around the \code{\link{dxHTTPRequest}} +##' function which makes POST HTTP requests to the API server. +##' +##' +##' @param objectID DNAnexus object ID +##' @param inputParams Either an R object that will be converted into JSON +##' using \code{RJSONIO::toJSON} to be used as the input to the API call. If +##' providing the JSON string directly, you must set \code{jsonifyData} to +##' \code{FALSE}. +##' @param jsonifyData Whether to call \code{RJSONIO::toJSON} on +##' \code{inputParams} to create the JSON string or pass through the value of +##' \code{inputParams} directly. (Default is \code{TRUE}.) +##' @param alwaysRetry Whether to always retry even when no response is +##' received from the API server +##' @return If the API call is successful, the parsed JSON of the API server +##' response is returned (using \code{RJSONIO::fromJSON}). +##' @export +##' @seealso \code{\link{dxHTTPRequest}} +##' @references API spec documentation: \url{https://documentation.dnanexus.com/developer/api/introduction-to-data-object-classes/dbclusters#api-method-dbcluster-xxxx-describe} +dbclusterDescribe <- function(objectID, + inputParams=emptyNamedList, + jsonifyData=TRUE, + alwaysRetry=TRUE) { + resource <- paste('/', objectID, '/', 'describe', sep='') + dxHTTPRequest(resource, + inputParams, + jsonifyData=jsonifyData, + alwaysRetry=alwaysRetry) +} + +##' dbclusterGetDetails API wrapper +##' +##' This function makes an API call to the \code{/dbcluster-xxxx/getDetails} API +##' method; it is a simple wrapper around the \code{\link{dxHTTPRequest}} +##' function which makes POST HTTP requests to the API server. +##' +##' +##' @param objectID DNAnexus object ID +##' @param inputParams Either an R object that will be converted into JSON +##' using \code{RJSONIO::toJSON} to be used as the input to the API call. If +##' providing the JSON string directly, you must set \code{jsonifyData} to +##' \code{FALSE}. +##' @param jsonifyData Whether to call \code{RJSONIO::toJSON} on +##' \code{inputParams} to create the JSON string or pass through the value of +##' \code{inputParams} directly. (Default is \code{TRUE}.) +##' @param alwaysRetry Whether to always retry even when no response is +##' received from the API server +##' @return If the API call is successful, the parsed JSON of the API server +##' response is returned (using \code{RJSONIO::fromJSON}). +##' @export +##' @seealso \code{\link{dxHTTPRequest}} +##' @references API spec documentation: \url{https://documentation.dnanexus.com/developer/api/data-object-lifecycle/details-and-links#api-method-class-xxxx-getdetails} +dbclusterGetDetails <- function(objectID, + inputParams=emptyNamedList, + jsonifyData=TRUE, + alwaysRetry=TRUE) { + resource <- paste('/', objectID, '/', 'getDetails', sep='') + dxHTTPRequest(resource, + inputParams, + jsonifyData=jsonifyData, + alwaysRetry=alwaysRetry) +} + +##' dbclusterNew API wrapper +##' +##' This function makes an API call to the \code{/dbcluster/new} API +##' method; it is a simple wrapper around the \code{\link{dxHTTPRequest}} +##' function which makes POST HTTP requests to the API server. +##' +##' +##' @param inputParams Either an R object that will be converted into JSON +##' using \code{RJSONIO::toJSON} to be used as the input to the API call. If +##' providing the JSON string directly, you must set \code{jsonifyData} to +##' \code{FALSE}. +##' @param jsonifyData Whether to call \code{RJSONIO::toJSON} on +##' \code{inputParams} to create the JSON string or pass through the value of +##' \code{inputParams} directly. (Default is \code{TRUE}.) +##' @param alwaysRetry Whether to always retry even when no response is +##' received from the API server +##' @return If the API call is successful, the parsed JSON of the API server +##' response is returned (using \code{RJSONIO::fromJSON}). +##' @export +##' @seealso \code{\link{dxHTTPRequest}} +##' @references API spec documentation: \url{https://documentation.dnanexus.com/developer/api/introduction-to-data-object-classes/dbclusters#api-method-dbcluster-new} +dbclusterNew <- function(inputParams=emptyNamedList, + jsonifyData=TRUE, + alwaysRetry=FALSE) { + dxHTTPRequest('/dbcluster/new', inputParams, jsonifyData=jsonifyData, alwaysRetry=alwaysRetry) +} + +##' dbclusterRemoveTags API wrapper +##' +##' This function makes an API call to the \code{/dbcluster-xxxx/removeTags} API +##' method; it is a simple wrapper around the \code{\link{dxHTTPRequest}} +##' function which makes POST HTTP requests to the API server. +##' +##' +##' @param objectID DNAnexus object ID +##' @param inputParams Either an R object that will be converted into JSON +##' using \code{RJSONIO::toJSON} to be used as the input to the API call. If +##' providing the JSON string directly, you must set \code{jsonifyData} to +##' \code{FALSE}. +##' @param jsonifyData Whether to call \code{RJSONIO::toJSON} on +##' \code{inputParams} to create the JSON string or pass through the value of +##' \code{inputParams} directly. (Default is \code{TRUE}.) +##' @param alwaysRetry Whether to always retry even when no response is +##' received from the API server +##' @return If the API call is successful, the parsed JSON of the API server +##' response is returned (using \code{RJSONIO::fromJSON}). +##' @export +##' @seealso \code{\link{dxHTTPRequest}} +##' @references API spec documentation: \url{https://documentation.dnanexus.com/developer/api/introduction-to-data-object-metadata/tags#api-method-class-xxxx-removetags} +dbclusterRemoveTags <- function(objectID, + inputParams=emptyNamedList, + jsonifyData=TRUE, + alwaysRetry=TRUE) { + resource <- paste('/', objectID, '/', 'removeTags', sep='') + dxHTTPRequest(resource, + inputParams, + jsonifyData=jsonifyData, + alwaysRetry=alwaysRetry) +} + +##' dbclusterRemoveTypes API wrapper +##' +##' This function makes an API call to the \code{/dbcluster-xxxx/removeTypes} API +##' method; it is a simple wrapper around the \code{\link{dxHTTPRequest}} +##' function which makes POST HTTP requests to the API server. +##' +##' +##' @param objectID DNAnexus object ID +##' @param inputParams Either an R object that will be converted into JSON +##' using \code{RJSONIO::toJSON} to be used as the input to the API call. If +##' providing the JSON string directly, you must set \code{jsonifyData} to +##' \code{FALSE}. +##' @param jsonifyData Whether to call \code{RJSONIO::toJSON} on +##' \code{inputParams} to create the JSON string or pass through the value of +##' \code{inputParams} directly. (Default is \code{TRUE}.) +##' @param alwaysRetry Whether to always retry even when no response is +##' received from the API server +##' @return If the API call is successful, the parsed JSON of the API server +##' response is returned (using \code{RJSONIO::fromJSON}). +##' @export +##' @seealso \code{\link{dxHTTPRequest}} +##' @references API spec documentation: \url{https://documentation.dnanexus.com/developer/api/data-object-lifecycle/types#api-method-class-xxxx-removetypes} +dbclusterRemoveTypes <- function(objectID, + inputParams=emptyNamedList, + jsonifyData=TRUE, + alwaysRetry=TRUE) { + resource <- paste('/', objectID, '/', 'removeTypes', sep='') + dxHTTPRequest(resource, + inputParams, + jsonifyData=jsonifyData, + alwaysRetry=alwaysRetry) +} + +##' dbclusterRename API wrapper +##' +##' This function makes an API call to the \code{/dbcluster-xxxx/rename} API +##' method; it is a simple wrapper around the \code{\link{dxHTTPRequest}} +##' function which makes POST HTTP requests to the API server. +##' +##' +##' @param objectID DNAnexus object ID +##' @param inputParams Either an R object that will be converted into JSON +##' using \code{RJSONIO::toJSON} to be used as the input to the API call. If +##' providing the JSON string directly, you must set \code{jsonifyData} to +##' \code{FALSE}. +##' @param jsonifyData Whether to call \code{RJSONIO::toJSON} on +##' \code{inputParams} to create the JSON string or pass through the value of +##' \code{inputParams} directly. (Default is \code{TRUE}.) +##' @param alwaysRetry Whether to always retry even when no response is +##' received from the API server +##' @return If the API call is successful, the parsed JSON of the API server +##' response is returned (using \code{RJSONIO::fromJSON}). +##' @export +##' @seealso \code{\link{dxHTTPRequest}} +##' @references API spec documentation: \url{https://documentation.dnanexus.com/developer/api/introduction-to-data-object-metadata/name#api-method-class-xxxx-rename} +dbclusterRename <- function(objectID, + inputParams=emptyNamedList, + jsonifyData=TRUE, + alwaysRetry=TRUE) { + resource <- paste('/', objectID, '/', 'rename', sep='') + dxHTTPRequest(resource, + inputParams, + jsonifyData=jsonifyData, + alwaysRetry=alwaysRetry) +} + +##' dbclusterSetDetails API wrapper +##' +##' This function makes an API call to the \code{/dbcluster-xxxx/setDetails} API +##' method; it is a simple wrapper around the \code{\link{dxHTTPRequest}} +##' function which makes POST HTTP requests to the API server. +##' +##' +##' @param objectID DNAnexus object ID +##' @param inputParams Either an R object that will be converted into JSON +##' using \code{RJSONIO::toJSON} to be used as the input to the API call. If +##' providing the JSON string directly, you must set \code{jsonifyData} to +##' \code{FALSE}. +##' @param jsonifyData Whether to call \code{RJSONIO::toJSON} on +##' \code{inputParams} to create the JSON string or pass through the value of +##' \code{inputParams} directly. (Default is \code{TRUE}.) +##' @param alwaysRetry Whether to always retry even when no response is +##' received from the API server +##' @return If the API call is successful, the parsed JSON of the API server +##' response is returned (using \code{RJSONIO::fromJSON}). +##' @export +##' @seealso \code{\link{dxHTTPRequest}} +##' @references API spec documentation: \url{https://documentation.dnanexus.com/developer/api/data-object-lifecycle/details-and-links#api-method-class-xxxx-setdetails} +dbclusterSetDetails <- function(objectID, + inputParams=emptyNamedList, + jsonifyData=TRUE, + alwaysRetry=TRUE) { + resource <- paste('/', objectID, '/', 'setDetails', sep='') + dxHTTPRequest(resource, + inputParams, + jsonifyData=jsonifyData, + alwaysRetry=alwaysRetry) +} + +##' dbclusterSetProperties API wrapper +##' +##' This function makes an API call to the \code{/dbcluster-xxxx/setProperties} API +##' method; it is a simple wrapper around the \code{\link{dxHTTPRequest}} +##' function which makes POST HTTP requests to the API server. +##' +##' +##' @param objectID DNAnexus object ID +##' @param inputParams Either an R object that will be converted into JSON +##' using \code{RJSONIO::toJSON} to be used as the input to the API call. If +##' providing the JSON string directly, you must set \code{jsonifyData} to +##' \code{FALSE}. +##' @param jsonifyData Whether to call \code{RJSONIO::toJSON} on +##' \code{inputParams} to create the JSON string or pass through the value of +##' \code{inputParams} directly. (Default is \code{TRUE}.) +##' @param alwaysRetry Whether to always retry even when no response is +##' received from the API server +##' @return If the API call is successful, the parsed JSON of the API server +##' response is returned (using \code{RJSONIO::fromJSON}). +##' @export +##' @seealso \code{\link{dxHTTPRequest}} +##' @references API spec documentation: \url{https://documentation.dnanexus.com/developer/api/introduction-to-data-object-metadata/properties#api-method-class-xxxx-setproperties} +dbclusterSetProperties <- function(objectID, + inputParams=emptyNamedList, + jsonifyData=TRUE, + alwaysRetry=TRUE) { + resource <- paste('/', objectID, '/', 'setProperties', sep='') + dxHTTPRequest(resource, + inputParams, + jsonifyData=jsonifyData, + alwaysRetry=alwaysRetry) +} + +##' dbclusterSetVisibility API wrapper +##' +##' This function makes an API call to the \code{/dbcluster-xxxx/setVisibility} API +##' method; it is a simple wrapper around the \code{\link{dxHTTPRequest}} +##' function which makes POST HTTP requests to the API server. +##' +##' +##' @param objectID DNAnexus object ID +##' @param inputParams Either an R object that will be converted into JSON +##' using \code{RJSONIO::toJSON} to be used as the input to the API call. If +##' providing the JSON string directly, you must set \code{jsonifyData} to +##' \code{FALSE}. +##' @param jsonifyData Whether to call \code{RJSONIO::toJSON} on +##' \code{inputParams} to create the JSON string or pass through the value of +##' \code{inputParams} directly. (Default is \code{TRUE}.) +##' @param alwaysRetry Whether to always retry even when no response is +##' received from the API server +##' @return If the API call is successful, the parsed JSON of the API server +##' response is returned (using \code{RJSONIO::fromJSON}). +##' @export +##' @seealso \code{\link{dxHTTPRequest}} +##' @references API spec documentation: \url{https://documentation.dnanexus.com/developer/api/data-object-lifecycle/visibility#api-method-class-xxxx-setvisibility} +dbclusterSetVisibility <- function(objectID, + inputParams=emptyNamedList, + jsonifyData=TRUE, + alwaysRetry=TRUE) { + resource <- paste('/', objectID, '/', 'setVisibility', sep='') + dxHTTPRequest(resource, + inputParams, + jsonifyData=jsonifyData, + alwaysRetry=alwaysRetry) +} + +##' dbclusterStart API wrapper +##' +##' This function makes an API call to the \code{/dbcluster-xxxx/start} API +##' method; it is a simple wrapper around the \code{\link{dxHTTPRequest}} +##' function which makes POST HTTP requests to the API server. +##' +##' +##' @param objectID DNAnexus object ID +##' @param inputParams Either an R object that will be converted into JSON +##' using \code{RJSONIO::toJSON} to be used as the input to the API call. If +##' providing the JSON string directly, you must set \code{jsonifyData} to +##' \code{FALSE}. +##' @param jsonifyData Whether to call \code{RJSONIO::toJSON} on +##' \code{inputParams} to create the JSON string or pass through the value of +##' \code{inputParams} directly. (Default is \code{TRUE}.) +##' @param alwaysRetry Whether to always retry even when no response is +##' received from the API server +##' @return If the API call is successful, the parsed JSON of the API server +##' response is returned (using \code{RJSONIO::fromJSON}). +##' @export +##' @seealso \code{\link{dxHTTPRequest}} +##' @references API spec documentation: \url{https://documentation.dnanexus.com/developer/api/introduction-to-data-object-classes/dbclusters#api-method-dbcluster-xxxx-start} +dbclusterStart <- function(objectID, + inputParams=emptyNamedList, + jsonifyData=TRUE, + alwaysRetry=TRUE) { + resource <- paste('/', objectID, '/', 'start', sep='') + dxHTTPRequest(resource, + inputParams, + jsonifyData=jsonifyData, + alwaysRetry=alwaysRetry) +} + +##' dbclusterStop API wrapper +##' +##' This function makes an API call to the \code{/dbcluster-xxxx/stop} API +##' method; it is a simple wrapper around the \code{\link{dxHTTPRequest}} +##' function which makes POST HTTP requests to the API server. +##' +##' +##' @param objectID DNAnexus object ID +##' @param inputParams Either an R object that will be converted into JSON +##' using \code{RJSONIO::toJSON} to be used as the input to the API call. If +##' providing the JSON string directly, you must set \code{jsonifyData} to +##' \code{FALSE}. +##' @param jsonifyData Whether to call \code{RJSONIO::toJSON} on +##' \code{inputParams} to create the JSON string or pass through the value of +##' \code{inputParams} directly. (Default is \code{TRUE}.) +##' @param alwaysRetry Whether to always retry even when no response is +##' received from the API server +##' @return If the API call is successful, the parsed JSON of the API server +##' response is returned (using \code{RJSONIO::fromJSON}). +##' @export +##' @seealso \code{\link{dxHTTPRequest}} +##' @references API spec documentation: \url{https://documentation.dnanexus.com/developer/api/introduction-to-data-object-classes/dbclusters#api-method-dbcluster-xxxx-stop} +dbclusterStop <- function(objectID, + inputParams=emptyNamedList, + jsonifyData=TRUE, + alwaysRetry=TRUE) { + resource <- paste('/', objectID, '/', 'stop', sep='') + dxHTTPRequest(resource, + inputParams, + jsonifyData=jsonifyData, + alwaysRetry=alwaysRetry) +} + +##' dbclusterTerminate API wrapper +##' +##' This function makes an API call to the \code{/dbcluster-xxxx/terminate} API +##' method; it is a simple wrapper around the \code{\link{dxHTTPRequest}} +##' function which makes POST HTTP requests to the API server. +##' +##' +##' @param objectID DNAnexus object ID +##' @param inputParams Either an R object that will be converted into JSON +##' using \code{RJSONIO::toJSON} to be used as the input to the API call. If +##' providing the JSON string directly, you must set \code{jsonifyData} to +##' \code{FALSE}. +##' @param jsonifyData Whether to call \code{RJSONIO::toJSON} on +##' \code{inputParams} to create the JSON string or pass through the value of +##' \code{inputParams} directly. (Default is \code{TRUE}.) +##' @param alwaysRetry Whether to always retry even when no response is +##' received from the API server +##' @return If the API call is successful, the parsed JSON of the API server +##' response is returned (using \code{RJSONIO::fromJSON}). +##' @export +##' @seealso \code{\link{dxHTTPRequest}} +##' @references API spec documentation: \url{https://documentation.dnanexus.com/developer/api/introduction-to-data-object-classes/dbclusters#api-method-dbcluster-xxxx-terminate} +dbclusterTerminate <- function(objectID, + inputParams=emptyNamedList, + jsonifyData=TRUE, + alwaysRetry=TRUE) { + resource <- paste('/', objectID, '/', 'terminate', sep='') + dxHTTPRequest(resource, + inputParams, + jsonifyData=jsonifyData, + alwaysRetry=alwaysRetry) +} + ##' fileAddTags API wrapper ##' ##' This function makes an API call to the \code{/file-xxxx/addTags} API @@ -3405,6 +3861,72 @@ jobTerminate <- function(objectID, alwaysRetry=alwaysRetry) } +##' jobUpdate API wrapper +##' +##' This function makes an API call to the \code{/job-xxxx/update} API +##' method; it is a simple wrapper around the \code{\link{dxHTTPRequest}} +##' function which makes POST HTTP requests to the API server. +##' +##' +##' @param objectID DNAnexus object ID +##' @param inputParams Either an R object that will be converted into JSON +##' using \code{RJSONIO::toJSON} to be used as the input to the API call. If +##' providing the JSON string directly, you must set \code{jsonifyData} to +##' \code{FALSE}. +##' @param jsonifyData Whether to call \code{RJSONIO::toJSON} on +##' \code{inputParams} to create the JSON string or pass through the value of +##' \code{inputParams} directly. (Default is \code{TRUE}.) +##' @param alwaysRetry Whether to always retry even when no response is +##' received from the API server +##' @return If the API call is successful, the parsed JSON of the API server +##' response is returned (using \code{RJSONIO::fromJSON}). +##' @export +##' @seealso \code{\link{dxHTTPRequest}} +##' @references API spec documentation: \url{https://documentation.dnanexus.com/developer/api/running-analyses/applets-and-entry-points#api-method-job-xxxx-update} +jobUpdate <- function(objectID, + inputParams=emptyNamedList, + jsonifyData=TRUE, + alwaysRetry=TRUE) { + resource <- paste('/', objectID, '/', 'update', sep='') + dxHTTPRequest(resource, + inputParams, + jsonifyData=jsonifyData, + alwaysRetry=alwaysRetry) +} + +##' jobGetIdentityToken API wrapper +##' +##' This function makes an API call to the \code{/job-xxxx/getIdentityToken} API +##' method; it is a simple wrapper around the \code{\link{dxHTTPRequest}} +##' function which makes POST HTTP requests to the API server. +##' +##' +##' @param objectID DNAnexus object ID +##' @param inputParams Either an R object that will be converted into JSON +##' using \code{RJSONIO::toJSON} to be used as the input to the API call. If +##' providing the JSON string directly, you must set \code{jsonifyData} to +##' \code{FALSE}. +##' @param jsonifyData Whether to call \code{RJSONIO::toJSON} on +##' \code{inputParams} to create the JSON string or pass through the value of +##' \code{inputParams} directly. (Default is \code{TRUE}.) +##' @param alwaysRetry Whether to always retry even when no response is +##' received from the API server +##' @return If the API call is successful, the parsed JSON of the API server +##' response is returned (using \code{RJSONIO::fromJSON}). +##' @export +##' @seealso \code{\link{dxHTTPRequest}} +##' @references API spec documentation: \url{https://documentation.dnanexus.com/developer/api/running-analyses/applets-and-entry-points#api-method-job-xxxx-getIdentityToken} +jobGetIdentityToken <- function(objectID, + inputParams=emptyNamedList, + jsonifyData=TRUE, + alwaysRetry=TRUE) { + resource <- paste('/', objectID, '/', 'getIdentityToken', sep='') + dxHTTPRequest(resource, + inputParams, + jsonifyData=jsonifyData, + alwaysRetry=alwaysRetry) +} + ##' jobNew API wrapper ##' ##' This function makes an API call to the \code{/job/new} API diff --git a/src/R/dxR/R/dxR-package.R b/src/R/dxR/R/dxR-package.R index 049aaae637..d67d996edc 100644 --- a/src/R/dxR/R/dxR-package.R +++ b/src/R/dxR/R/dxR-package.R @@ -4,7 +4,7 @@ ##' the new DNAnexus platform. ##' ##' \tabular{ll}{ Package: \tab dxR\cr Type: \tab Package\cr Version: \tab -##' 0.312.0\cr License: \tab Apache License (== 2.0)\cr +##' 0.388.0\cr License: \tab Apache License (== 2.0)\cr ##' } ##' ##' @name dxR-package diff --git a/src/R/dxR/man/dxR-package.Rd b/src/R/dxR/man/dxR-package.Rd index 710f4dbbaf..b2a5cdb797 100644 --- a/src/R/dxR/man/dxR-package.Rd +++ b/src/R/dxR/man/dxR-package.Rd @@ -9,7 +9,7 @@ } \details{ \tabular{ll}{ Package: \tab dxR\cr Type: \tab Package\cr - Version: \tab 0.312.0\cr License: \tab Apache License (== + Version: \tab 0.388.0\cr License: \tab Apache License (== 2.0)\cr } } \author{ diff --git a/src/Readme.md b/src/Readme.md index 6f266f2d93..bc536af65f 100644 --- a/src/Readme.md +++ b/src/Readme.md @@ -21,6 +21,8 @@ Environment Variable | Tests included `DXTEST_TCSH` | Run tests that require `tcsh` to be installed `DXTEST_WITH_AUTHSERVER` | Run tests that require a running authserver `DX_RUN_NEXT_TESTS` | Run tests that require synchronous updates to backend +`DXTEST_NF_DOCKER` | Run tests that require docker for Nextflow + Python and Java tests recognize these environment variables and enable or disable tests accordingly. diff --git a/src/api_wrappers/generateCppAPICCWrappers.py b/src/api_wrappers/generateCppAPICCWrappers.py index 9e0d93e51c..438ac65f89 100755 --- a/src/api_wrappers/generateCppAPICCWrappers.py +++ b/src/api_wrappers/generateCppAPICCWrappers.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # # Copyright (C) 2013-2016 DNAnexus, Inc. # diff --git a/src/api_wrappers/generateCppAPIHWrappers.py b/src/api_wrappers/generateCppAPIHWrappers.py index ee175d4058..072668a4f1 100755 --- a/src/api_wrappers/generateCppAPIHWrappers.py +++ b/src/api_wrappers/generateCppAPIHWrappers.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # # Copyright (C) 2013-2016 DNAnexus, Inc. # diff --git a/src/api_wrappers/generateJavaAPIWrappers.py b/src/api_wrappers/generateJavaAPIWrappers.py index b304aa13ce..87bee22e50 100755 --- a/src/api_wrappers/generateJavaAPIWrappers.py +++ b/src/api_wrappers/generateJavaAPIWrappers.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # # Copyright (C) 2013-2016 DNAnexus, Inc. # diff --git a/src/api_wrappers/generatePythonAPIWrappers.py b/src/api_wrappers/generatePythonAPIWrappers.py index c15e5a0e2e..e0c465b5de 100755 --- a/src/api_wrappers/generatePythonAPIWrappers.py +++ b/src/api_wrappers/generatePythonAPIWrappers.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # # Copyright (C) 2013-2016 DNAnexus, Inc. # diff --git a/src/api_wrappers/generateRAPIWrappers.py b/src/api_wrappers/generateRAPIWrappers.py index 4cb58274cd..9ba7f47fc2 100755 --- a/src/api_wrappers/generateRAPIWrappers.py +++ b/src/api_wrappers/generateRAPIWrappers.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # # Copyright (C) 2013-2016 DNAnexus, Inc. # diff --git a/src/api_wrappers/wrapper_table.json b/src/api_wrappers/wrapper_table.json index 110920e777..6d399bbb18 100644 --- a/src/api_wrappers/wrapper_table.json +++ b/src/api_wrappers/wrapper_table.json @@ -552,6 +552,132 @@ "wikiLink": "https://documentation.dnanexus.com/developer/api/data-containers/folders-and-deletion#api-method-class-xxxx-listfolder" } ], + [ + "/dbcluster-xxxx/addTags", + "dbclusterAddTags(req, objectId)", + { + "objectMethod": true, + "retryable": true, + "wikiLink": "https://documentation.dnanexus.com/developer/api/introduction-to-data-object-metadata/tags#api-method-class-xxxx-addtags" + } + ], + [ + "/dbcluster-xxxx/addTypes", + "dbclusterAddTypes(req, objectId)", + { + "objectMethod": true, + "retryable": true, + "wikiLink": "https://documentation.dnanexus.com/developer/api/data-object-lifecycle/types#api-method-class-xxxx-addtypes" + } + ], + [ + "/dbcluster-xxxx/describe", + "dbclusterDescribe(req, objectId)", + { + "objectMethod": true, + "retryable": true, + "wikiLink": "https://documentation.dnanexus.com/developer/api/introduction-to-data-object-classes/dbclusters#api-method-dbcluster-xxxx-describe" + } + ], + [ + "/dbcluster-xxxx/getDetails", + "dbclusterGetDetails(req, objectId)", + { + "objectMethod": true, + "retryable": true, + "wikiLink": "https://documentation.dnanexus.com/developer/api/data-object-lifecycle/details-and-links#api-method-class-xxxx-getdetails" + } + ], + [ + "/dbcluster/new", + "dbclusterNew(req)", + { + "objectMethod": false, + "retryable": false, + "wikiLink": "https://documentation.dnanexus.com/developer/api/introduction-to-data-object-classes/dbclusters#api-method-dbcluster-new" + } + ], + [ + "/dbcluster-xxxx/removeTags", + "dbclusterRemoveTags(req, objectId)", + { + "objectMethod": true, + "retryable": true, + "wikiLink": "https://documentation.dnanexus.com/developer/api/introduction-to-data-object-metadata/tags#api-method-class-xxxx-removetags" + } + ], + [ + "/dbcluster-xxxx/removeTypes", + "dbclusterRemoveTypes(req, objectId)", + { + "objectMethod": true, + "retryable": true, + "wikiLink": "https://documentation.dnanexus.com/developer/api/data-object-lifecycle/types#api-method-class-xxxx-removetypes" + } + ], + [ + "/dbcluster-xxxx/rename", + "dbclusterRename(req, objectId)", + { + "objectMethod": true, + "retryable": true, + "wikiLink": "https://documentation.dnanexus.com/developer/api/introduction-to-data-object-metadata/name#api-method-class-xxxx-rename" + } + ], + [ + "/dbcluster-xxxx/setDetails", + "dbclusterSetDetails(req, objectId)", + { + "objectMethod": true, + "retryable": true, + "wikiLink": "https://documentation.dnanexus.com/developer/api/data-object-lifecycle/details-and-links#api-method-class-xxxx-setdetails" + } + ], + [ + "/dbcluster-xxxx/setProperties", + "dbclusterSetProperties(req, objectId)", + { + "objectMethod": true, + "retryable": true, + "wikiLink": "https://documentation.dnanexus.com/developer/api/introduction-to-data-object-metadata/properties#api-method-class-xxxx-setproperties" + } + ], + [ + "/dbcluster-xxxx/setVisibility", + "dbclusterSetVisibility(req, objectId)", + { + "objectMethod": true, + "retryable": true, + "wikiLink": "https://documentation.dnanexus.com/developer/api/data-object-lifecycle/visibility#api-method-class-xxxx-setvisibility" + } + ], + [ + "/dbcluster-xxxx/start", + "dbclusterStart(req, objectId)", + { + "objectMethod": true, + "retryable": true, + "wikiLink": "https://documentation.dnanexus.com/developer/api/introduction-to-data-object-classes/dbclusters#api-method-dbcluster-xxxx-start" + } + ], + [ + "/dbcluster-xxxx/stop", + "dbclusterStop(req, objectId)", + { + "objectMethod": true, + "retryable": true, + "wikiLink": "https://documentation.dnanexus.com/developer/api/introduction-to-data-object-classes/dbclusters#api-method-dbcluster-xxxx-stop" + } + ], + [ + "/dbcluster-xxxx/terminate", + "dbclusterTerminate(req, objectId)", + { + "objectMethod": true, + "retryable": true, + "wikiLink": "https://documentation.dnanexus.com/developer/api/introduction-to-data-object-classes/dbclusters#api-method-dbcluster-xxxx-terminate" + } + ], [ "/file-xxxx/addTags", "fileAddTags(req, objectId)", @@ -897,6 +1023,24 @@ "wikiLink": "https://documentation.dnanexus.com/developer/api/running-analyses/applets-and-entry-points#api-method-job-xxxx-terminate" } ], + [ + "/job-xxxx/update", + "jobUpdate(req, objectId)", + { + "objectMethod": true, + "retryable": true, + "wikiLink": "https://documentation.dnanexus.com/developer/api/running-analyses/applets-and-entry-points#api-method-job-xxxx-update" + } + ], + [ + "/job-xxxx/getIdentityToken", + "jobGetIdentityToken(req, objectId)", + { + "retryable": true, + "objectMethod": true, + "wikiLink": "https://documentation.dnanexus.com/developer/api/running-analyses/applets-and-entry-points#api-method-job-xxxx-getIdentityToken" + } + ], [ "/job/new", "jobNew(req)", diff --git a/src/cpp/dxcpp/api.cc b/src/cpp/dxcpp/api.cc index 7117bd61f0..1610a6711a 100644 --- a/src/cpp/dxcpp/api.cc +++ b/src/cpp/dxcpp/api.cc @@ -659,6 +659,118 @@ namespace dx { return databaseListFolder(object_id, input_params.toString(), safe_to_retry); } + JSON dbclusterAddTags(const std::string &object_id, const std::string &input_params, const bool safe_to_retry) { + return DXHTTPRequest(std::string("/") + object_id + std::string("/addTags"), input_params, safe_to_retry); + } + + JSON dbclusterAddTags(const std::string &object_id, const JSON &input_params, const bool safe_to_retry) { + return dbclusterAddTags(object_id, input_params.toString(), safe_to_retry); + } + + JSON dbclusterAddTypes(const std::string &object_id, const std::string &input_params, const bool safe_to_retry) { + return DXHTTPRequest(std::string("/") + object_id + std::string("/addTypes"), input_params, safe_to_retry); + } + + JSON dbclusterAddTypes(const std::string &object_id, const JSON &input_params, const bool safe_to_retry) { + return dbclusterAddTypes(object_id, input_params.toString(), safe_to_retry); + } + + JSON dbclusterDescribe(const std::string &object_id, const std::string &input_params, const bool safe_to_retry) { + return DXHTTPRequest(std::string("/") + object_id + std::string("/describe"), input_params, safe_to_retry); + } + + JSON dbclusterDescribe(const std::string &object_id, const JSON &input_params, const bool safe_to_retry) { + return dbclusterDescribe(object_id, input_params.toString(), safe_to_retry); + } + + JSON dbclusterGetDetails(const std::string &object_id, const std::string &input_params, const bool safe_to_retry) { + return DXHTTPRequest(std::string("/") + object_id + std::string("/getDetails"), input_params, safe_to_retry); + } + + JSON dbclusterGetDetails(const std::string &object_id, const JSON &input_params, const bool safe_to_retry) { + return dbclusterGetDetails(object_id, input_params.toString(), safe_to_retry); + } + + JSON dbclusterNew(const std::string &input_params, const bool safe_to_retry) { + return DXHTTPRequest("/dbcluster/new", input_params, safe_to_retry); + } + + JSON dbclusterNew(const JSON &input_params, const bool safe_to_retry) { + return dbclusterNew(input_params.toString(), safe_to_retry); + } + + JSON dbclusterRemoveTags(const std::string &object_id, const std::string &input_params, const bool safe_to_retry) { + return DXHTTPRequest(std::string("/") + object_id + std::string("/removeTags"), input_params, safe_to_retry); + } + + JSON dbclusterRemoveTags(const std::string &object_id, const JSON &input_params, const bool safe_to_retry) { + return dbclusterRemoveTags(object_id, input_params.toString(), safe_to_retry); + } + + JSON dbclusterRemoveTypes(const std::string &object_id, const std::string &input_params, const bool safe_to_retry) { + return DXHTTPRequest(std::string("/") + object_id + std::string("/removeTypes"), input_params, safe_to_retry); + } + + JSON dbclusterRemoveTypes(const std::string &object_id, const JSON &input_params, const bool safe_to_retry) { + return dbclusterRemoveTypes(object_id, input_params.toString(), safe_to_retry); + } + + JSON dbclusterRename(const std::string &object_id, const std::string &input_params, const bool safe_to_retry) { + return DXHTTPRequest(std::string("/") + object_id + std::string("/rename"), input_params, safe_to_retry); + } + + JSON dbclusterRename(const std::string &object_id, const JSON &input_params, const bool safe_to_retry) { + return dbclusterRename(object_id, input_params.toString(), safe_to_retry); + } + + JSON dbclusterSetDetails(const std::string &object_id, const std::string &input_params, const bool safe_to_retry) { + return DXHTTPRequest(std::string("/") + object_id + std::string("/setDetails"), input_params, safe_to_retry); + } + + JSON dbclusterSetDetails(const std::string &object_id, const JSON &input_params, const bool safe_to_retry) { + return dbclusterSetDetails(object_id, input_params.toString(), safe_to_retry); + } + + JSON dbclusterSetProperties(const std::string &object_id, const std::string &input_params, const bool safe_to_retry) { + return DXHTTPRequest(std::string("/") + object_id + std::string("/setProperties"), input_params, safe_to_retry); + } + + JSON dbclusterSetProperties(const std::string &object_id, const JSON &input_params, const bool safe_to_retry) { + return dbclusterSetProperties(object_id, input_params.toString(), safe_to_retry); + } + + JSON dbclusterSetVisibility(const std::string &object_id, const std::string &input_params, const bool safe_to_retry) { + return DXHTTPRequest(std::string("/") + object_id + std::string("/setVisibility"), input_params, safe_to_retry); + } + + JSON dbclusterSetVisibility(const std::string &object_id, const JSON &input_params, const bool safe_to_retry) { + return dbclusterSetVisibility(object_id, input_params.toString(), safe_to_retry); + } + + JSON dbclusterStart(const std::string &object_id, const std::string &input_params, const bool safe_to_retry) { + return DXHTTPRequest(std::string("/") + object_id + std::string("/start"), input_params, safe_to_retry); + } + + JSON dbclusterStart(const std::string &object_id, const JSON &input_params, const bool safe_to_retry) { + return dbclusterStart(object_id, input_params.toString(), safe_to_retry); + } + + JSON dbclusterStop(const std::string &object_id, const std::string &input_params, const bool safe_to_retry) { + return DXHTTPRequest(std::string("/") + object_id + std::string("/stop"), input_params, safe_to_retry); + } + + JSON dbclusterStop(const std::string &object_id, const JSON &input_params, const bool safe_to_retry) { + return dbclusterStop(object_id, input_params.toString(), safe_to_retry); + } + + JSON dbclusterTerminate(const std::string &object_id, const std::string &input_params, const bool safe_to_retry) { + return DXHTTPRequest(std::string("/") + object_id + std::string("/terminate"), input_params, safe_to_retry); + } + + JSON dbclusterTerminate(const std::string &object_id, const JSON &input_params, const bool safe_to_retry) { + return dbclusterTerminate(object_id, input_params.toString(), safe_to_retry); + } + JSON fileAddTags(const std::string &object_id, const std::string &input_params, const bool safe_to_retry) { return DXHTTPRequest(std::string("/") + object_id + std::string("/addTags"), input_params, safe_to_retry); } @@ -966,6 +1078,22 @@ namespace dx { return jobTerminate(object_id, input_params.toString(), safe_to_retry); } + JSON jobUpdate(const std::string &object_id, const std::string &input_params, const bool safe_to_retry) { + return DXHTTPRequest(std::string("/") + object_id + std::string("/update"), input_params, safe_to_retry); + } + + JSON jobUpdate(const std::string &object_id, const JSON &input_params, const bool safe_to_retry) { + return jobUpdate(object_id, input_params.toString(), safe_to_retry); + } + + JSON jobGetIdentityToken(const std::string &object_id, const std::string &input_params, const bool safe_to_retry) { + return DXHTTPRequest(std::string("/") + object_id + std::string("/getIdentityToken"), input_params, safe_to_retry); + } + + JSON jobGetIdentityToken(const std::string &object_id, const JSON &input_params, const bool safe_to_retry) { + return jobGetIdentityToken(object_id, input_params.toString(), safe_to_retry); + } + JSON jobNew(const std::string &input_params, const bool safe_to_retry) { return DXHTTPRequest("/job/new", input_params, safe_to_retry); } diff --git a/src/cpp/dxcpp/api.h b/src/cpp/dxcpp/api.h index 34ef31e312..741cb6f0a0 100644 --- a/src/cpp/dxcpp/api.h +++ b/src/cpp/dxcpp/api.h @@ -249,6 +249,48 @@ namespace dx { JSON databaseListFolder(const std::string &object_id, const std::string &input_params="{}", const bool safe_to_retry=true); JSON databaseListFolder(const std::string &object_id, const dx::JSON &input_params, const bool safe_to_retry=true); + JSON dbclusterAddTags(const std::string &object_id, const std::string &input_params="{}", const bool safe_to_retry=true); + JSON dbclusterAddTags(const std::string &object_id, const dx::JSON &input_params, const bool safe_to_retry=true); + + JSON dbclusterAddTypes(const std::string &object_id, const std::string &input_params="{}", const bool safe_to_retry=true); + JSON dbclusterAddTypes(const std::string &object_id, const dx::JSON &input_params, const bool safe_to_retry=true); + + JSON dbclusterDescribe(const std::string &object_id, const std::string &input_params="{}", const bool safe_to_retry=true); + JSON dbclusterDescribe(const std::string &object_id, const dx::JSON &input_params, const bool safe_to_retry=true); + + JSON dbclusterGetDetails(const std::string &object_id, const std::string &input_params="{}", const bool safe_to_retry=true); + JSON dbclusterGetDetails(const std::string &object_id, const dx::JSON &input_params, const bool safe_to_retry=true); + + JSON dbclusterNew(const std::string &input_params="{}", const bool safe_to_retry=false); + JSON dbclusterNew(const dx::JSON &input_params, const bool safe_to_retry=false); + + JSON dbclusterRemoveTags(const std::string &object_id, const std::string &input_params="{}", const bool safe_to_retry=true); + JSON dbclusterRemoveTags(const std::string &object_id, const dx::JSON &input_params, const bool safe_to_retry=true); + + JSON dbclusterRemoveTypes(const std::string &object_id, const std::string &input_params="{}", const bool safe_to_retry=true); + JSON dbclusterRemoveTypes(const std::string &object_id, const dx::JSON &input_params, const bool safe_to_retry=true); + + JSON dbclusterRename(const std::string &object_id, const std::string &input_params="{}", const bool safe_to_retry=true); + JSON dbclusterRename(const std::string &object_id, const dx::JSON &input_params, const bool safe_to_retry=true); + + JSON dbclusterSetDetails(const std::string &object_id, const std::string &input_params="{}", const bool safe_to_retry=true); + JSON dbclusterSetDetails(const std::string &object_id, const dx::JSON &input_params, const bool safe_to_retry=true); + + JSON dbclusterSetProperties(const std::string &object_id, const std::string &input_params="{}", const bool safe_to_retry=true); + JSON dbclusterSetProperties(const std::string &object_id, const dx::JSON &input_params, const bool safe_to_retry=true); + + JSON dbclusterSetVisibility(const std::string &object_id, const std::string &input_params="{}", const bool safe_to_retry=true); + JSON dbclusterSetVisibility(const std::string &object_id, const dx::JSON &input_params, const bool safe_to_retry=true); + + JSON dbclusterStart(const std::string &object_id, const std::string &input_params="{}", const bool safe_to_retry=true); + JSON dbclusterStart(const std::string &object_id, const dx::JSON &input_params, const bool safe_to_retry=true); + + JSON dbclusterStop(const std::string &object_id, const std::string &input_params="{}", const bool safe_to_retry=true); + JSON dbclusterStop(const std::string &object_id, const dx::JSON &input_params, const bool safe_to_retry=true); + + JSON dbclusterTerminate(const std::string &object_id, const std::string &input_params="{}", const bool safe_to_retry=true); + JSON dbclusterTerminate(const std::string &object_id, const dx::JSON &input_params, const bool safe_to_retry=true); + JSON fileAddTags(const std::string &object_id, const std::string &input_params="{}", const bool safe_to_retry=true); JSON fileAddTags(const std::string &object_id, const dx::JSON &input_params, const bool safe_to_retry=true); @@ -363,6 +405,12 @@ namespace dx { JSON jobTerminate(const std::string &object_id, const std::string &input_params="{}", const bool safe_to_retry=true); JSON jobTerminate(const std::string &object_id, const dx::JSON &input_params, const bool safe_to_retry=true); + JSON jobUpdate(const std::string &object_id, const std::string &input_params="{}", const bool safe_to_retry=true); + JSON jobUpdate(const std::string &object_id, const dx::JSON &input_params, const bool safe_to_retry=true); + + JSON jobGetIdentityToken(const std::string &object_id, const std::string &input_params="{}", const bool safe_to_retry=true); + JSON jobGetIdentityToken(const std::string &object_id, const dx::JSON &input_params, const bool safe_to_retry=true); + JSON jobNew(const std::string &input_params="{}", const bool safe_to_retry=false); JSON jobNew(const dx::JSON &input_params, const bool safe_to_retry=false); diff --git a/src/dx-verify-file/Makefile.mw b/src/dx-verify-file/Makefile.mw deleted file mode 100644 index 67ea55c666..0000000000 --- a/src/dx-verify-file/Makefile.mw +++ /dev/null @@ -1,72 +0,0 @@ -# -*- mode: Makefile -*- -# -# Copyright (C) 2013-2016 DNAnexus, Inc. -# -# This file is part of dx-toolkit (DNAnexus platform client libraries). -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may not -# use this file except in compliance with the License. You may obtain a copy -# of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -# curl_build_dir := $(shell mktemp -d --tmpdir=/tmp curl-build-XXXXXX) - -# curl: -# $(DNANEXUS_HOME)/src/dx-verify-file/build_curl.sh $(curl_build_dir) -UNAME := $(shell uname) - -ifneq ($(UNAME), MINGW32_NT-6.2) - $(error This Makefile should be used only with MINGW32_NT-6.2) -endif - -DXTOOLKIT_GITVERSION := $(shell git describe|sed 's/-/+/') -VERSION = 0.0.1 - -sw_dir = $(HOME)/sw/local -curl_dir = $(sw_dir)/curl -cpp_dir = $(DNANEXUS_HOME)/src/cpp -dxjson_dir = $(cpp_dir)/dxjson -dxhttp_dir = $(cpp_dir)/SimpleHttpLib -dxcpp_dir = $(cpp_dir)/dxcpp -dx-verify-file_dir = $(DNANEXUS_HOME)/src/dx-verify-file -zlib_dir = $(sw_dir)/zlib-1.2.3-lib -lmagic_dir = $(sw_dir)/file-5.03-lib -boost_dir = $(sw_dir)/boost_1_51_0 -VPATH = $(dxjson_dir):$(dxhttp_dir):$(dxcpp_dir):$(dx-verify-file_dir) - -CFLAGS = -g -Wall -Wextra -I/include -I$(zlib_dir)/include -CXXFLAGS = -DSTATIC_BUILD -DWINDOWS_BUILD -DBOOST_THREAD_USE_LIB -D_FILE_OFFSET_BITS=64 -DDX_VERIFY_FILE_VERSION=\"$(VERSION)\" -DDXTOOLKIT_GITVERSION=\"$(DXTOOLKIT_GITVERSION)\" -g -Wall -Wextra -Werror=return-type -std=gnu++0x -I$(curl_dir)/include -I$(cpp_dir) -I$(dxhttp_dir) -I$(dxjson_dir) -I$(dxcpp_dir) -I$(dx-verify-file_dir) -I$(boost_dir) -I$(zlib_dir)/include -I/include - -LDFLAGS := -static-libstdc++ -static-libgcc -DBOOST_THREAD_USE_LIB -L$(boost_dir)/stage/lib -L$(curl_dir)/lib -L/lib $(LDFLAGS) -L$(zlib_dir)/lib -lboost_program_options-mgw47-mt-1_51 -lboost_filesystem-mgw47-mt-1_51 -lboost_regex-mgw47-mt-1_51 -lboost_system-mgw47-mt-1_51 -lcurl -lcrypto -lz -lboost_thread-mgw47-mt-1_51 -lboost_chrono-mgw47-mt-1_51 - -dxjson_objs = dxjson.o -dxhttp_objs = SimpleHttp.o SimpleHttpHeaders.o Utility.o -dxcpp_objs = api.o dxcpp.o SSLThreads.o utils.o dxlog.o -dx-verify-file_objs = options.o log.o chunk.o main.o File.o - -dxjson: $(dxjson_objs) -dxhttp: $(dxhttp_objs) -dxcpp: $(dxcpp_objs) -dx-verify-file: $(dx-verify-file_objs) - -all: dxjson dxhttp dxcpp dx-verify-file - g++ *.o $(LDFLAGS) -o dx-verify-file - -dist: all - rm -rf dx-verify-file-$(VERSION)-windows dx-verify-file-$(VERSION)-windows.zip - mkdir -p dx-verify-file-$(VERSION)-windows - mv dx-verify-file.exe dx-verify-file-$(VERSION)-windows/ - ../windows_build/collect_dx-verify-file_dlls.sh dx-verify-file-$(VERSION)-windows - cd dx-verify-file-$(VERSION)-windows && zip -9 -r ../dx-verify-file-$(VERSION)-windows.zip . - rm -rf dx-verify-file-$(VERSION)-windows -clean: - rm -v *.o dx-verify-file - -.PHONY: all dxjson dxhttp dxcpp dx-verify-file diff --git a/src/java/Readme.md b/src/java/Readme.md index 84a2d8a049..2b19b5aa06 100644 --- a/src/java/Readme.md +++ b/src/java/Readme.md @@ -12,13 +12,13 @@ Development ### Build dependencies -* Maven (`apt-get install maven2`) +* Maven (`apt-get install maven`) ### Building From dx-toolkit, run: - make && make java + make java To create a project for Eclipse development: diff --git a/src/java/pom.xml b/src/java/pom.xml index 85c5e04654..99900cc5ce 100644 --- a/src/java/pom.xml +++ b/src/java/pom.xml @@ -41,12 +41,17 @@ + - org.slf4j - slf4j-log4j12 - 1.7.25 + org.apache.logging.log4j + log4j-core + 2.17.1 + + + org.apache.logging.log4j + log4j-api + 2.17.1 - org.apache.httpcomponents httpclient @@ -77,7 +82,7 @@ com.fasterxml.jackson.core jackson-databind - 2.9.10.7 + 2.9.10.8 diff --git a/src/java/src/main/java/com/dnanexus/ArchivalState.java b/src/java/src/main/java/com/dnanexus/ArchivalState.java new file mode 100644 index 0000000000..0b1b651b38 --- /dev/null +++ b/src/java/src/main/java/com/dnanexus/ArchivalState.java @@ -0,0 +1,57 @@ +package com.dnanexus; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; + +import java.util.Map; + +/** + * Archival states of file object. + */ +public enum ArchivalState { + /** + * The file is in archival storage, such as AWS S3 Glacier or Azure Blob ARCHIVE. + */ + ARCHIVED("archived"), + /** + * The file is in standard storage, such as AWS S3 or Azure Blob. + */ + LIVE("live"), + /** + * Archival requested on the current file, but other copies of the same file are in the live state in multiple + * projects with the same billTo entity. The file is still in standard storage. + */ + ARCHIVAL("archival"), + /** + * Unarchival requested on the current file. The file is in transition from archival storage to standard storage. + */ + UNARCHIVING("unarchiving"); + + private static Map createMap; + + static { + Map result = Maps.newHashMap(); + for (ArchivalState state : ArchivalState.values()) { + result.put(state.getValue(), state); + } + createMap = ImmutableMap.copyOf(result); + } + + @JsonCreator + private static ArchivalState create(String value) { + return createMap.get(value); + } + + private String value; + + private ArchivalState(String value) { + this.value = value; + } + + @JsonValue + private String getValue() { + return this.value; + } +} \ No newline at end of file diff --git a/src/java/src/main/java/com/dnanexus/DXAPI.java b/src/java/src/main/java/com/dnanexus/DXAPI.java index fa5fbb64dd..0d60bbf3a2 100644 --- a/src/java/src/main/java/com/dnanexus/DXAPI.java +++ b/src/java/src/main/java/com/dnanexus/DXAPI.java @@ -11317,7 +11317,7 @@ public static JsonNode databaseListFolder(String objectId, JsonNode inputParams, } /** - * Invokes the fileAddTags method with an empty input, deserializing to an object of the specified class. + * Invokes the dbclusterAddTags method with an empty input, deserializing to an object of the specified class. * *

For more information about this method, see the API specification. * @@ -11333,11 +11333,11 @@ public static JsonNode databaseListFolder(String objectId, JsonNode inputParams, * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileAddTags(String objectId, Class outputClass) { - return fileAddTags(objectId, mapper.createObjectNode(), outputClass); + public static T dbclusterAddTags(String objectId, Class outputClass) { + return dbclusterAddTags(objectId, mapper.createObjectNode(), outputClass); } /** - * Invokes the fileAddTags method with the given input, deserializing to an object of the specified class. + * Invokes the dbclusterAddTags method with the given input, deserializing to an object of the specified class. * *

For more information about this method, see the API specification. * @@ -11354,14 +11354,14 @@ public static T fileAddTags(String objectId, Class outputClass) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileAddTags(String objectId, Object inputObject, Class outputClass) { + public static T dbclusterAddTags(String objectId, Object inputObject, Class outputClass) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( new DXHTTPRequest().request("/" + objectId + "/" + "addTags", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the fileAddTags method with an empty input using the given environment, deserializing to an object of the specified class. + * Invokes the dbclusterAddTags method with an empty input using the given environment, deserializing to an object of the specified class. * *

For more information about this method, see the API specification. * @@ -11378,11 +11378,11 @@ public static T fileAddTags(String objectId, Object inputObject, Class ou * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileAddTags(String objectId, Class outputClass, DXEnvironment env) { - return fileAddTags(objectId, mapper.createObjectNode(), outputClass, env); + public static T dbclusterAddTags(String objectId, Class outputClass, DXEnvironment env) { + return dbclusterAddTags(objectId, mapper.createObjectNode(), outputClass, env); } /** - * Invokes the fileAddTags method with the given input using the given environment, deserializing to an object of the specified class. + * Invokes the dbclusterAddTags method with the given input using the given environment, deserializing to an object of the specified class. * *

For more information about this method, see the API specification. * @@ -11400,7 +11400,7 @@ public static T fileAddTags(String objectId, Class outputClass, DXEnviron * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileAddTags(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { + public static T dbclusterAddTags(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( new DXHTTPRequest(env).request("/" + objectId + "/" + "addTags", @@ -11408,7 +11408,7 @@ public static T fileAddTags(String objectId, Object inputObject, Class ou } /** - * Invokes the fileAddTags method. + * Invokes the dbclusterAddTags method. * *

For more information about this method, see the API specification. * @@ -11423,14 +11423,14 @@ public static T fileAddTags(String objectId, Object inputObject, Class ou * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileAddTags(String, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #dbclusterAddTags(String, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileAddTags(String objectId) { - return fileAddTags(objectId, mapper.createObjectNode()); + public static JsonNode dbclusterAddTags(String objectId) { + return dbclusterAddTags(objectId, mapper.createObjectNode()); } /** - * Invokes the fileAddTags method with the specified parameters. + * Invokes the dbclusterAddTags method with the specified parameters. * *

For more information about this method, see the API specification. * @@ -11446,15 +11446,15 @@ public static JsonNode fileAddTags(String objectId) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileAddTags(String, Object, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #dbclusterAddTags(String, Object, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileAddTags(String objectId, JsonNode inputParams) { + public static JsonNode dbclusterAddTags(String objectId, JsonNode inputParams) { return new DXHTTPRequest().request("/" + objectId + "/" + "addTags", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the fileAddTags method with the specified environment. + * Invokes the dbclusterAddTags method with the specified environment. * *

For more information about this method, see the API specification. * @@ -11470,14 +11470,14 @@ public static JsonNode fileAddTags(String objectId, JsonNode inputParams) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileAddTags(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #dbclusterAddTags(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileAddTags(String objectId, DXEnvironment env) { - return fileAddTags(objectId, mapper.createObjectNode(), env); + public static JsonNode dbclusterAddTags(String objectId, DXEnvironment env) { + return dbclusterAddTags(objectId, mapper.createObjectNode(), env); } /** - * Invokes the fileAddTags method with the specified environment and parameters. + * Invokes the dbclusterAddTags method with the specified environment and parameters. * *

For more information about this method, see the API specification. * @@ -11494,16 +11494,16 @@ public static JsonNode fileAddTags(String objectId, DXEnvironment env) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileAddTags(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #dbclusterAddTags(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileAddTags(String objectId, JsonNode inputParams, DXEnvironment env) { + public static JsonNode dbclusterAddTags(String objectId, JsonNode inputParams, DXEnvironment env) { return new DXHTTPRequest(env).request("/" + objectId + "/" + "addTags", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the fileAddTypes method with an empty input, deserializing to an object of the specified class. + * Invokes the dbclusterAddTypes method with an empty input, deserializing to an object of the specified class. * *

For more information about this method, see the API specification. * @@ -11519,11 +11519,11 @@ public static JsonNode fileAddTags(String objectId, JsonNode inputParams, DXEnvi * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileAddTypes(String objectId, Class outputClass) { - return fileAddTypes(objectId, mapper.createObjectNode(), outputClass); + public static T dbclusterAddTypes(String objectId, Class outputClass) { + return dbclusterAddTypes(objectId, mapper.createObjectNode(), outputClass); } /** - * Invokes the fileAddTypes method with the given input, deserializing to an object of the specified class. + * Invokes the dbclusterAddTypes method with the given input, deserializing to an object of the specified class. * *

For more information about this method, see the API specification. * @@ -11540,14 +11540,14 @@ public static T fileAddTypes(String objectId, Class outputClass) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileAddTypes(String objectId, Object inputObject, Class outputClass) { + public static T dbclusterAddTypes(String objectId, Object inputObject, Class outputClass) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( new DXHTTPRequest().request("/" + objectId + "/" + "addTypes", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the fileAddTypes method with an empty input using the given environment, deserializing to an object of the specified class. + * Invokes the dbclusterAddTypes method with an empty input using the given environment, deserializing to an object of the specified class. * *

For more information about this method, see the API specification. * @@ -11564,11 +11564,11 @@ public static T fileAddTypes(String objectId, Object inputObject, Class o * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileAddTypes(String objectId, Class outputClass, DXEnvironment env) { - return fileAddTypes(objectId, mapper.createObjectNode(), outputClass, env); + public static T dbclusterAddTypes(String objectId, Class outputClass, DXEnvironment env) { + return dbclusterAddTypes(objectId, mapper.createObjectNode(), outputClass, env); } /** - * Invokes the fileAddTypes method with the given input using the given environment, deserializing to an object of the specified class. + * Invokes the dbclusterAddTypes method with the given input using the given environment, deserializing to an object of the specified class. * *

For more information about this method, see the API specification. * @@ -11586,7 +11586,7 @@ public static T fileAddTypes(String objectId, Class outputClass, DXEnviro * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileAddTypes(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { + public static T dbclusterAddTypes(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( new DXHTTPRequest(env).request("/" + objectId + "/" + "addTypes", @@ -11594,7 +11594,7 @@ public static T fileAddTypes(String objectId, Object inputObject, Class o } /** - * Invokes the fileAddTypes method. + * Invokes the dbclusterAddTypes method. * *

For more information about this method, see the API specification. * @@ -11609,14 +11609,14 @@ public static T fileAddTypes(String objectId, Object inputObject, Class o * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileAddTypes(String, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #dbclusterAddTypes(String, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileAddTypes(String objectId) { - return fileAddTypes(objectId, mapper.createObjectNode()); + public static JsonNode dbclusterAddTypes(String objectId) { + return dbclusterAddTypes(objectId, mapper.createObjectNode()); } /** - * Invokes the fileAddTypes method with the specified parameters. + * Invokes the dbclusterAddTypes method with the specified parameters. * *

For more information about this method, see the API specification. * @@ -11632,15 +11632,15 @@ public static JsonNode fileAddTypes(String objectId) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileAddTypes(String, Object, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #dbclusterAddTypes(String, Object, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileAddTypes(String objectId, JsonNode inputParams) { + public static JsonNode dbclusterAddTypes(String objectId, JsonNode inputParams) { return new DXHTTPRequest().request("/" + objectId + "/" + "addTypes", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the fileAddTypes method with the specified environment. + * Invokes the dbclusterAddTypes method with the specified environment. * *

For more information about this method, see the API specification. * @@ -11656,14 +11656,14 @@ public static JsonNode fileAddTypes(String objectId, JsonNode inputParams) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileAddTypes(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #dbclusterAddTypes(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileAddTypes(String objectId, DXEnvironment env) { - return fileAddTypes(objectId, mapper.createObjectNode(), env); + public static JsonNode dbclusterAddTypes(String objectId, DXEnvironment env) { + return dbclusterAddTypes(objectId, mapper.createObjectNode(), env); } /** - * Invokes the fileAddTypes method with the specified environment and parameters. + * Invokes the dbclusterAddTypes method with the specified environment and parameters. * *

For more information about this method, see the API specification. * @@ -11680,18 +11680,18 @@ public static JsonNode fileAddTypes(String objectId, DXEnvironment env) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileAddTypes(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #dbclusterAddTypes(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileAddTypes(String objectId, JsonNode inputParams, DXEnvironment env) { + public static JsonNode dbclusterAddTypes(String objectId, JsonNode inputParams, DXEnvironment env) { return new DXHTTPRequest(env).request("/" + objectId + "/" + "addTypes", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the fileClose method with an empty input, deserializing to an object of the specified class. + * Invokes the dbclusterDescribe method with an empty input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -11705,13 +11705,13 @@ public static JsonNode fileAddTypes(String objectId, JsonNode inputParams, DXEnv * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileClose(String objectId, Class outputClass) { - return fileClose(objectId, mapper.createObjectNode(), outputClass); + public static T dbclusterDescribe(String objectId, Class outputClass) { + return dbclusterDescribe(objectId, mapper.createObjectNode(), outputClass); } /** - * Invokes the fileClose method with the given input, deserializing to an object of the specified class. + * Invokes the dbclusterDescribe method with the given input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -11726,16 +11726,16 @@ public static T fileClose(String objectId, Class outputClass) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileClose(String objectId, Object inputObject, Class outputClass) { + public static T dbclusterDescribe(String objectId, Object inputObject, Class outputClass) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest().request("/" + objectId + "/" + "close", + new DXHTTPRequest().request("/" + objectId + "/" + "describe", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the fileClose method with an empty input using the given environment, deserializing to an object of the specified class. + * Invokes the dbclusterDescribe method with an empty input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -11750,13 +11750,13 @@ public static T fileClose(String objectId, Object inputObject, Class outp * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileClose(String objectId, Class outputClass, DXEnvironment env) { - return fileClose(objectId, mapper.createObjectNode(), outputClass, env); + public static T dbclusterDescribe(String objectId, Class outputClass, DXEnvironment env) { + return dbclusterDescribe(objectId, mapper.createObjectNode(), outputClass, env); } /** - * Invokes the fileClose method with the given input using the given environment, deserializing to an object of the specified class. + * Invokes the dbclusterDescribe method with the given input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -11772,17 +11772,17 @@ public static T fileClose(String objectId, Class outputClass, DXEnvironme * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileClose(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { + public static T dbclusterDescribe(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest(env).request("/" + objectId + "/" + "close", + new DXHTTPRequest(env).request("/" + objectId + "/" + "describe", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the fileClose method. + * Invokes the dbclusterDescribe method. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @@ -11795,16 +11795,16 @@ public static T fileClose(String objectId, Object inputObject, Class outp * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileClose(String, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #dbclusterDescribe(String, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileClose(String objectId) { - return fileClose(objectId, mapper.createObjectNode()); + public static JsonNode dbclusterDescribe(String objectId) { + return dbclusterDescribe(objectId, mapper.createObjectNode()); } /** - * Invokes the fileClose method with the specified parameters. + * Invokes the dbclusterDescribe method with the specified parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -11818,17 +11818,17 @@ public static JsonNode fileClose(String objectId) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileClose(String, Object, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #dbclusterDescribe(String, Object, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileClose(String objectId, JsonNode inputParams) { - return new DXHTTPRequest().request("/" + objectId + "/" + "close", inputParams, + public static JsonNode dbclusterDescribe(String objectId, JsonNode inputParams) { + return new DXHTTPRequest().request("/" + objectId + "/" + "describe", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the fileClose method with the specified environment. + * Invokes the dbclusterDescribe method with the specified environment. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param env environment object specifying the auth token and remote server and protocol @@ -11842,16 +11842,16 @@ public static JsonNode fileClose(String objectId, JsonNode inputParams) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileClose(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #dbclusterDescribe(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileClose(String objectId, DXEnvironment env) { - return fileClose(objectId, mapper.createObjectNode(), env); + public static JsonNode dbclusterDescribe(String objectId, DXEnvironment env) { + return dbclusterDescribe(objectId, mapper.createObjectNode(), env); } /** - * Invokes the fileClose method with the specified environment and parameters. + * Invokes the dbclusterDescribe method with the specified environment and parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -11866,18 +11866,18 @@ public static JsonNode fileClose(String objectId, DXEnvironment env) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileClose(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #dbclusterDescribe(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileClose(String objectId, JsonNode inputParams, DXEnvironment env) { - return new DXHTTPRequest(env).request("/" + objectId + "/" + "close", inputParams, + public static JsonNode dbclusterDescribe(String objectId, JsonNode inputParams, DXEnvironment env) { + return new DXHTTPRequest(env).request("/" + objectId + "/" + "describe", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the fileDescribe method with an empty input, deserializing to an object of the specified class. + * Invokes the dbclusterGetDetails method with an empty input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -11891,13 +11891,13 @@ public static JsonNode fileClose(String objectId, JsonNode inputParams, DXEnviro * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileDescribe(String objectId, Class outputClass) { - return fileDescribe(objectId, mapper.createObjectNode(), outputClass); + public static T dbclusterGetDetails(String objectId, Class outputClass) { + return dbclusterGetDetails(objectId, mapper.createObjectNode(), outputClass); } /** - * Invokes the fileDescribe method with the given input, deserializing to an object of the specified class. + * Invokes the dbclusterGetDetails method with the given input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -11912,16 +11912,16 @@ public static T fileDescribe(String objectId, Class outputClass) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileDescribe(String objectId, Object inputObject, Class outputClass) { + public static T dbclusterGetDetails(String objectId, Object inputObject, Class outputClass) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest().request("/" + objectId + "/" + "describe", + new DXHTTPRequest().request("/" + objectId + "/" + "getDetails", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the fileDescribe method with an empty input using the given environment, deserializing to an object of the specified class. + * Invokes the dbclusterGetDetails method with an empty input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -11936,13 +11936,13 @@ public static T fileDescribe(String objectId, Object inputObject, Class o * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileDescribe(String objectId, Class outputClass, DXEnvironment env) { - return fileDescribe(objectId, mapper.createObjectNode(), outputClass, env); + public static T dbclusterGetDetails(String objectId, Class outputClass, DXEnvironment env) { + return dbclusterGetDetails(objectId, mapper.createObjectNode(), outputClass, env); } /** - * Invokes the fileDescribe method with the given input using the given environment, deserializing to an object of the specified class. + * Invokes the dbclusterGetDetails method with the given input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -11958,17 +11958,17 @@ public static T fileDescribe(String objectId, Class outputClass, DXEnviro * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileDescribe(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { + public static T dbclusterGetDetails(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest(env).request("/" + objectId + "/" + "describe", + new DXHTTPRequest(env).request("/" + objectId + "/" + "getDetails", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the fileDescribe method. + * Invokes the dbclusterGetDetails method. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @@ -11981,16 +11981,16 @@ public static T fileDescribe(String objectId, Object inputObject, Class o * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileDescribe(String, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #dbclusterGetDetails(String, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileDescribe(String objectId) { - return fileDescribe(objectId, mapper.createObjectNode()); + public static JsonNode dbclusterGetDetails(String objectId) { + return dbclusterGetDetails(objectId, mapper.createObjectNode()); } /** - * Invokes the fileDescribe method with the specified parameters. + * Invokes the dbclusterGetDetails method with the specified parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -12004,17 +12004,17 @@ public static JsonNode fileDescribe(String objectId) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileDescribe(String, Object, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #dbclusterGetDetails(String, Object, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileDescribe(String objectId, JsonNode inputParams) { - return new DXHTTPRequest().request("/" + objectId + "/" + "describe", inputParams, + public static JsonNode dbclusterGetDetails(String objectId, JsonNode inputParams) { + return new DXHTTPRequest().request("/" + objectId + "/" + "getDetails", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the fileDescribe method with the specified environment. + * Invokes the dbclusterGetDetails method with the specified environment. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param env environment object specifying the auth token and remote server and protocol @@ -12028,16 +12028,16 @@ public static JsonNode fileDescribe(String objectId, JsonNode inputParams) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileDescribe(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #dbclusterGetDetails(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileDescribe(String objectId, DXEnvironment env) { - return fileDescribe(objectId, mapper.createObjectNode(), env); + public static JsonNode dbclusterGetDetails(String objectId, DXEnvironment env) { + return dbclusterGetDetails(objectId, mapper.createObjectNode(), env); } /** - * Invokes the fileDescribe method with the specified environment and parameters. + * Invokes the dbclusterGetDetails method with the specified environment and parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -12052,23 +12052,22 @@ public static JsonNode fileDescribe(String objectId, DXEnvironment env) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileDescribe(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #dbclusterGetDetails(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileDescribe(String objectId, JsonNode inputParams, DXEnvironment env) { - return new DXHTTPRequest(env).request("/" + objectId + "/" + "describe", inputParams, + public static JsonNode dbclusterGetDetails(String objectId, JsonNode inputParams, DXEnvironment env) { + return new DXHTTPRequest(env).request("/" + objectId + "/" + "getDetails", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the fileDownload method with an empty input, deserializing to an object of the specified class. + * Invokes the dbclusterNew method with an empty input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * - * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to * - * @return Response object + * @return Server response parsed from JSON * * @throws DXAPIException * If the server returns a complete response with an HTTP status @@ -12077,19 +12076,18 @@ public static JsonNode fileDescribe(String objectId, JsonNode inputParams, DXEnv * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileDownload(String objectId, Class outputClass) { - return fileDownload(objectId, mapper.createObjectNode(), outputClass); + public static T dbclusterNew(Class outputClass) { + return dbclusterNew(mapper.createObjectNode(), outputClass); } /** - * Invokes the fileDownload method with the given input, deserializing to an object of the specified class. + * Invokes the dbclusterNew method with an empty input using the specified environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * - * @param objectId ID of the object to operate on - * @param inputObject input object (to be JSON serialized to an input hash) * @param outputClass class to deserialize the server reponse to + * @param env environment object specifying the auth token and remote server and protocol * - * @return Response object + * @return Server response parsed from JSON * * @throws DXAPIException * If the server returns a complete response with an HTTP status @@ -12098,22 +12096,18 @@ public static T fileDownload(String objectId, Class outputClass) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileDownload(String objectId, Object inputObject, Class outputClass) { - JsonNode input = mapper.valueToTree(inputObject); - return DXJSON.safeTreeToValue( - new DXHTTPRequest().request("/" + objectId + "/" + "download", - input, RetryStrategy.SAFE_TO_RETRY), outputClass); + public static T dbclusterNew(Class outputClass, DXEnvironment env) { + return dbclusterNew(mapper.createObjectNode(), outputClass, env); } /** - * Invokes the fileDownload method with an empty input using the given environment, deserializing to an object of the specified class. + * Invokes the dbclusterNew method with the specified input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * - * @param objectId ID of the object to operate on + * @param inputObject input object (to be JSON serialized to an input hash) * @param outputClass class to deserialize the server reponse to - * @param env environment object specifying the auth token and remote server and protocol * - * @return Response object + * @return Server response parsed from JSON * * @throws DXAPIException * If the server returns a complete response with an HTTP status @@ -12122,20 +12116,22 @@ public static T fileDownload(String objectId, Object inputObject, Class o * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileDownload(String objectId, Class outputClass, DXEnvironment env) { - return fileDownload(objectId, mapper.createObjectNode(), outputClass, env); + public static T dbclusterNew(Object inputObject, Class outputClass) { + JsonNode input = mapper.valueToTree(inputObject); + return DXJSON.safeTreeToValue( + new DXHTTPRequest().request("/dbcluster/new", input, RetryStrategy.UNSAFE_TO_RETRY), + outputClass); } /** - * Invokes the fileDownload method with the given input using the given environment, deserializing to an object of the specified class. + * Invokes the dbclusterNew method with the specified input using the specified environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * - * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) * @param outputClass class to deserialize the server reponse to * @param env environment object specifying the auth token and remote server and protocol * - * @return Response object + * @return Server response parsed from JSON * * @throws DXAPIException * If the server returns a complete response with an HTTP status @@ -12144,19 +12140,17 @@ public static T fileDownload(String objectId, Class outputClass, DXEnviro * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileDownload(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { + public static T dbclusterNew(Object inputObject, Class outputClass, DXEnvironment env) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest(env).request("/" + objectId + "/" + "download", - input, RetryStrategy.SAFE_TO_RETRY), outputClass); + new DXHTTPRequest(env).request("/dbcluster/new", input, RetryStrategy.UNSAFE_TO_RETRY), + outputClass); } /** - * Invokes the fileDownload method. - * - *

For more information about this method, see the API specification. + * Invokes the dbclusterNew method. * - * @param objectId ID of the object to operate on + *

For more information about this method, see the API specification. * * @return Server response parsed from JSON * @@ -12167,18 +12161,17 @@ public static T fileDownload(String objectId, Object inputObject, Class o * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileDownload(String, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #dbclusterNew(Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileDownload(String objectId) { - return fileDownload(objectId, mapper.createObjectNode()); + public static JsonNode dbclusterNew() { + return dbclusterNew(mapper.createObjectNode()); } /** - * Invokes the fileDownload method with the specified parameters. + * Invokes the dbclusterNew method with the specified input parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * - * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call * * @return Server response parsed from JSON @@ -12190,19 +12183,17 @@ public static JsonNode fileDownload(String objectId) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileDownload(String, Object, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #dbclusterNew(Object, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileDownload(String objectId, JsonNode inputParams) { - return new DXHTTPRequest().request("/" + objectId + "/" + "download", inputParams, - RetryStrategy.SAFE_TO_RETRY); + public static JsonNode dbclusterNew(JsonNode inputParams) { + return new DXHTTPRequest().request("/dbcluster/new", inputParams, RetryStrategy.UNSAFE_TO_RETRY); } /** - * Invokes the fileDownload method with the specified environment. + * Invokes the dbclusterNew method with the specified environment. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * - * @param objectId ID of the object to operate on * @param env environment object specifying the auth token and remote server and protocol * * @return Server response parsed from JSON @@ -12214,18 +12205,17 @@ public static JsonNode fileDownload(String objectId, JsonNode inputParams) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileDownload(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #dbclusterNew(Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileDownload(String objectId, DXEnvironment env) { - return fileDownload(objectId, mapper.createObjectNode(), env); + public static JsonNode dbclusterNew(DXEnvironment env) { + return dbclusterNew(mapper.createObjectNode(), env); } /** - * Invokes the fileDownload method with the specified environment and parameters. + * Invokes the dbclusterNew method with the specified environment and input parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * - * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call * @param env environment object specifying the auth token and remote server and protocol * @@ -12238,18 +12228,17 @@ public static JsonNode fileDownload(String objectId, DXEnvironment env) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileDownload(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #dbclusterNew(Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileDownload(String objectId, JsonNode inputParams, DXEnvironment env) { - return new DXHTTPRequest(env).request("/" + objectId + "/" + "download", inputParams, - RetryStrategy.SAFE_TO_RETRY); + public static JsonNode dbclusterNew(JsonNode inputParams, DXEnvironment env) { + return new DXHTTPRequest(env).request("/dbcluster/new", inputParams, RetryStrategy.UNSAFE_TO_RETRY); } /** - * Invokes the fileGetDetails method with an empty input, deserializing to an object of the specified class. + * Invokes the dbclusterRemoveTags method with an empty input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -12263,13 +12252,13 @@ public static JsonNode fileDownload(String objectId, JsonNode inputParams, DXEnv * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileGetDetails(String objectId, Class outputClass) { - return fileGetDetails(objectId, mapper.createObjectNode(), outputClass); + public static T dbclusterRemoveTags(String objectId, Class outputClass) { + return dbclusterRemoveTags(objectId, mapper.createObjectNode(), outputClass); } /** - * Invokes the fileGetDetails method with the given input, deserializing to an object of the specified class. + * Invokes the dbclusterRemoveTags method with the given input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -12284,16 +12273,16 @@ public static T fileGetDetails(String objectId, Class outputClass) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileGetDetails(String objectId, Object inputObject, Class outputClass) { + public static T dbclusterRemoveTags(String objectId, Object inputObject, Class outputClass) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest().request("/" + objectId + "/" + "getDetails", + new DXHTTPRequest().request("/" + objectId + "/" + "removeTags", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the fileGetDetails method with an empty input using the given environment, deserializing to an object of the specified class. + * Invokes the dbclusterRemoveTags method with an empty input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -12308,13 +12297,13 @@ public static T fileGetDetails(String objectId, Object inputObject, Class * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileGetDetails(String objectId, Class outputClass, DXEnvironment env) { - return fileGetDetails(objectId, mapper.createObjectNode(), outputClass, env); + public static T dbclusterRemoveTags(String objectId, Class outputClass, DXEnvironment env) { + return dbclusterRemoveTags(objectId, mapper.createObjectNode(), outputClass, env); } /** - * Invokes the fileGetDetails method with the given input using the given environment, deserializing to an object of the specified class. + * Invokes the dbclusterRemoveTags method with the given input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -12330,17 +12319,17 @@ public static T fileGetDetails(String objectId, Class outputClass, DXEnvi * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileGetDetails(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { + public static T dbclusterRemoveTags(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest(env).request("/" + objectId + "/" + "getDetails", + new DXHTTPRequest(env).request("/" + objectId + "/" + "removeTags", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the fileGetDetails method. + * Invokes the dbclusterRemoveTags method. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @@ -12353,16 +12342,16 @@ public static T fileGetDetails(String objectId, Object inputObject, Class * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileGetDetails(String, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #dbclusterRemoveTags(String, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileGetDetails(String objectId) { - return fileGetDetails(objectId, mapper.createObjectNode()); + public static JsonNode dbclusterRemoveTags(String objectId) { + return dbclusterRemoveTags(objectId, mapper.createObjectNode()); } /** - * Invokes the fileGetDetails method with the specified parameters. + * Invokes the dbclusterRemoveTags method with the specified parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -12376,17 +12365,17 @@ public static JsonNode fileGetDetails(String objectId) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileGetDetails(String, Object, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #dbclusterRemoveTags(String, Object, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileGetDetails(String objectId, JsonNode inputParams) { - return new DXHTTPRequest().request("/" + objectId + "/" + "getDetails", inputParams, + public static JsonNode dbclusterRemoveTags(String objectId, JsonNode inputParams) { + return new DXHTTPRequest().request("/" + objectId + "/" + "removeTags", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the fileGetDetails method with the specified environment. + * Invokes the dbclusterRemoveTags method with the specified environment. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param env environment object specifying the auth token and remote server and protocol @@ -12400,16 +12389,16 @@ public static JsonNode fileGetDetails(String objectId, JsonNode inputParams) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileGetDetails(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #dbclusterRemoveTags(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileGetDetails(String objectId, DXEnvironment env) { - return fileGetDetails(objectId, mapper.createObjectNode(), env); + public static JsonNode dbclusterRemoveTags(String objectId, DXEnvironment env) { + return dbclusterRemoveTags(objectId, mapper.createObjectNode(), env); } /** - * Invokes the fileGetDetails method with the specified environment and parameters. + * Invokes the dbclusterRemoveTags method with the specified environment and parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -12424,18 +12413,18 @@ public static JsonNode fileGetDetails(String objectId, DXEnvironment env) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileGetDetails(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #dbclusterRemoveTags(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileGetDetails(String objectId, JsonNode inputParams, DXEnvironment env) { - return new DXHTTPRequest(env).request("/" + objectId + "/" + "getDetails", inputParams, + public static JsonNode dbclusterRemoveTags(String objectId, JsonNode inputParams, DXEnvironment env) { + return new DXHTTPRequest(env).request("/" + objectId + "/" + "removeTags", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the fileListProjects method with an empty input, deserializing to an object of the specified class. + * Invokes the dbclusterRemoveTypes method with an empty input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -12449,13 +12438,13 @@ public static JsonNode fileGetDetails(String objectId, JsonNode inputParams, DXE * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileListProjects(String objectId, Class outputClass) { - return fileListProjects(objectId, mapper.createObjectNode(), outputClass); + public static T dbclusterRemoveTypes(String objectId, Class outputClass) { + return dbclusterRemoveTypes(objectId, mapper.createObjectNode(), outputClass); } /** - * Invokes the fileListProjects method with the given input, deserializing to an object of the specified class. + * Invokes the dbclusterRemoveTypes method with the given input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -12470,16 +12459,16 @@ public static T fileListProjects(String objectId, Class outputClass) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileListProjects(String objectId, Object inputObject, Class outputClass) { + public static T dbclusterRemoveTypes(String objectId, Object inputObject, Class outputClass) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest().request("/" + objectId + "/" + "listProjects", + new DXHTTPRequest().request("/" + objectId + "/" + "removeTypes", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the fileListProjects method with an empty input using the given environment, deserializing to an object of the specified class. + * Invokes the dbclusterRemoveTypes method with an empty input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -12494,13 +12483,13 @@ public static T fileListProjects(String objectId, Object inputObject, Class< * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileListProjects(String objectId, Class outputClass, DXEnvironment env) { - return fileListProjects(objectId, mapper.createObjectNode(), outputClass, env); + public static T dbclusterRemoveTypes(String objectId, Class outputClass, DXEnvironment env) { + return dbclusterRemoveTypes(objectId, mapper.createObjectNode(), outputClass, env); } /** - * Invokes the fileListProjects method with the given input using the given environment, deserializing to an object of the specified class. + * Invokes the dbclusterRemoveTypes method with the given input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -12516,17 +12505,17 @@ public static T fileListProjects(String objectId, Class outputClass, DXEn * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileListProjects(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { + public static T dbclusterRemoveTypes(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest(env).request("/" + objectId + "/" + "listProjects", + new DXHTTPRequest(env).request("/" + objectId + "/" + "removeTypes", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the fileListProjects method. + * Invokes the dbclusterRemoveTypes method. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @@ -12539,16 +12528,16 @@ public static T fileListProjects(String objectId, Object inputObject, Class< * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileListProjects(String, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #dbclusterRemoveTypes(String, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileListProjects(String objectId) { - return fileListProjects(objectId, mapper.createObjectNode()); + public static JsonNode dbclusterRemoveTypes(String objectId) { + return dbclusterRemoveTypes(objectId, mapper.createObjectNode()); } /** - * Invokes the fileListProjects method with the specified parameters. + * Invokes the dbclusterRemoveTypes method with the specified parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -12562,17 +12551,17 @@ public static JsonNode fileListProjects(String objectId) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileListProjects(String, Object, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #dbclusterRemoveTypes(String, Object, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileListProjects(String objectId, JsonNode inputParams) { - return new DXHTTPRequest().request("/" + objectId + "/" + "listProjects", inputParams, + public static JsonNode dbclusterRemoveTypes(String objectId, JsonNode inputParams) { + return new DXHTTPRequest().request("/" + objectId + "/" + "removeTypes", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the fileListProjects method with the specified environment. + * Invokes the dbclusterRemoveTypes method with the specified environment. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param env environment object specifying the auth token and remote server and protocol @@ -12586,16 +12575,16 @@ public static JsonNode fileListProjects(String objectId, JsonNode inputParams) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileListProjects(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #dbclusterRemoveTypes(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileListProjects(String objectId, DXEnvironment env) { - return fileListProjects(objectId, mapper.createObjectNode(), env); + public static JsonNode dbclusterRemoveTypes(String objectId, DXEnvironment env) { + return dbclusterRemoveTypes(objectId, mapper.createObjectNode(), env); } /** - * Invokes the fileListProjects method with the specified environment and parameters. + * Invokes the dbclusterRemoveTypes method with the specified environment and parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -12610,18 +12599,18 @@ public static JsonNode fileListProjects(String objectId, DXEnvironment env) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileListProjects(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #dbclusterRemoveTypes(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileListProjects(String objectId, JsonNode inputParams, DXEnvironment env) { - return new DXHTTPRequest(env).request("/" + objectId + "/" + "listProjects", inputParams, + public static JsonNode dbclusterRemoveTypes(String objectId, JsonNode inputParams, DXEnvironment env) { + return new DXHTTPRequest(env).request("/" + objectId + "/" + "removeTypes", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the fileRemoveTags method with an empty input, deserializing to an object of the specified class. + * Invokes the dbclusterRename method with an empty input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -12635,13 +12624,13 @@ public static JsonNode fileListProjects(String objectId, JsonNode inputParams, D * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileRemoveTags(String objectId, Class outputClass) { - return fileRemoveTags(objectId, mapper.createObjectNode(), outputClass); + public static T dbclusterRename(String objectId, Class outputClass) { + return dbclusterRename(objectId, mapper.createObjectNode(), outputClass); } /** - * Invokes the fileRemoveTags method with the given input, deserializing to an object of the specified class. + * Invokes the dbclusterRename method with the given input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -12656,16 +12645,16 @@ public static T fileRemoveTags(String objectId, Class outputClass) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileRemoveTags(String objectId, Object inputObject, Class outputClass) { + public static T dbclusterRename(String objectId, Object inputObject, Class outputClass) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest().request("/" + objectId + "/" + "removeTags", + new DXHTTPRequest().request("/" + objectId + "/" + "rename", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the fileRemoveTags method with an empty input using the given environment, deserializing to an object of the specified class. + * Invokes the dbclusterRename method with an empty input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -12680,13 +12669,13 @@ public static T fileRemoveTags(String objectId, Object inputObject, Class * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileRemoveTags(String objectId, Class outputClass, DXEnvironment env) { - return fileRemoveTags(objectId, mapper.createObjectNode(), outputClass, env); + public static T dbclusterRename(String objectId, Class outputClass, DXEnvironment env) { + return dbclusterRename(objectId, mapper.createObjectNode(), outputClass, env); } /** - * Invokes the fileRemoveTags method with the given input using the given environment, deserializing to an object of the specified class. + * Invokes the dbclusterRename method with the given input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -12702,17 +12691,17 @@ public static T fileRemoveTags(String objectId, Class outputClass, DXEnvi * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileRemoveTags(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { + public static T dbclusterRename(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest(env).request("/" + objectId + "/" + "removeTags", + new DXHTTPRequest(env).request("/" + objectId + "/" + "rename", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the fileRemoveTags method. + * Invokes the dbclusterRename method. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @@ -12725,16 +12714,16 @@ public static T fileRemoveTags(String objectId, Object inputObject, Class * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileRemoveTags(String, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #dbclusterRename(String, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileRemoveTags(String objectId) { - return fileRemoveTags(objectId, mapper.createObjectNode()); + public static JsonNode dbclusterRename(String objectId) { + return dbclusterRename(objectId, mapper.createObjectNode()); } /** - * Invokes the fileRemoveTags method with the specified parameters. + * Invokes the dbclusterRename method with the specified parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -12748,17 +12737,17 @@ public static JsonNode fileRemoveTags(String objectId) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileRemoveTags(String, Object, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #dbclusterRename(String, Object, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileRemoveTags(String objectId, JsonNode inputParams) { - return new DXHTTPRequest().request("/" + objectId + "/" + "removeTags", inputParams, + public static JsonNode dbclusterRename(String objectId, JsonNode inputParams) { + return new DXHTTPRequest().request("/" + objectId + "/" + "rename", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the fileRemoveTags method with the specified environment. + * Invokes the dbclusterRename method with the specified environment. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param env environment object specifying the auth token and remote server and protocol @@ -12772,16 +12761,16 @@ public static JsonNode fileRemoveTags(String objectId, JsonNode inputParams) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileRemoveTags(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #dbclusterRename(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileRemoveTags(String objectId, DXEnvironment env) { - return fileRemoveTags(objectId, mapper.createObjectNode(), env); + public static JsonNode dbclusterRename(String objectId, DXEnvironment env) { + return dbclusterRename(objectId, mapper.createObjectNode(), env); } /** - * Invokes the fileRemoveTags method with the specified environment and parameters. + * Invokes the dbclusterRename method with the specified environment and parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -12796,18 +12785,18 @@ public static JsonNode fileRemoveTags(String objectId, DXEnvironment env) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileRemoveTags(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #dbclusterRename(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileRemoveTags(String objectId, JsonNode inputParams, DXEnvironment env) { - return new DXHTTPRequest(env).request("/" + objectId + "/" + "removeTags", inputParams, + public static JsonNode dbclusterRename(String objectId, JsonNode inputParams, DXEnvironment env) { + return new DXHTTPRequest(env).request("/" + objectId + "/" + "rename", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the fileRemoveTypes method with an empty input, deserializing to an object of the specified class. + * Invokes the dbclusterSetDetails method with an empty input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -12821,13 +12810,13 @@ public static JsonNode fileRemoveTags(String objectId, JsonNode inputParams, DXE * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileRemoveTypes(String objectId, Class outputClass) { - return fileRemoveTypes(objectId, mapper.createObjectNode(), outputClass); + public static T dbclusterSetDetails(String objectId, Class outputClass) { + return dbclusterSetDetails(objectId, mapper.createObjectNode(), outputClass); } /** - * Invokes the fileRemoveTypes method with the given input, deserializing to an object of the specified class. + * Invokes the dbclusterSetDetails method with the given input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -12842,16 +12831,16 @@ public static T fileRemoveTypes(String objectId, Class outputClass) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileRemoveTypes(String objectId, Object inputObject, Class outputClass) { + public static T dbclusterSetDetails(String objectId, Object inputObject, Class outputClass) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest().request("/" + objectId + "/" + "removeTypes", + new DXHTTPRequest().request("/" + objectId + "/" + "setDetails", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the fileRemoveTypes method with an empty input using the given environment, deserializing to an object of the specified class. + * Invokes the dbclusterSetDetails method with an empty input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -12866,13 +12855,13 @@ public static T fileRemoveTypes(String objectId, Object inputObject, Class T fileRemoveTypes(String objectId, Class outputClass, DXEnvironment env) { - return fileRemoveTypes(objectId, mapper.createObjectNode(), outputClass, env); + public static T dbclusterSetDetails(String objectId, Class outputClass, DXEnvironment env) { + return dbclusterSetDetails(objectId, mapper.createObjectNode(), outputClass, env); } /** - * Invokes the fileRemoveTypes method with the given input using the given environment, deserializing to an object of the specified class. + * Invokes the dbclusterSetDetails method with the given input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -12888,17 +12877,17 @@ public static T fileRemoveTypes(String objectId, Class outputClass, DXEnv * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileRemoveTypes(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { + public static T dbclusterSetDetails(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest(env).request("/" + objectId + "/" + "removeTypes", + new DXHTTPRequest(env).request("/" + objectId + "/" + "setDetails", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the fileRemoveTypes method. + * Invokes the dbclusterSetDetails method. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @@ -12911,16 +12900,16 @@ public static T fileRemoveTypes(String objectId, Object inputObject, ClassFor more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -12934,17 +12923,17 @@ public static JsonNode fileRemoveTypes(String objectId) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileRemoveTypes(String, Object, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #dbclusterSetDetails(String, Object, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileRemoveTypes(String objectId, JsonNode inputParams) { - return new DXHTTPRequest().request("/" + objectId + "/" + "removeTypes", inputParams, + public static JsonNode dbclusterSetDetails(String objectId, JsonNode inputParams) { + return new DXHTTPRequest().request("/" + objectId + "/" + "setDetails", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the fileRemoveTypes method with the specified environment. + * Invokes the dbclusterSetDetails method with the specified environment. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param env environment object specifying the auth token and remote server and protocol @@ -12958,16 +12947,16 @@ public static JsonNode fileRemoveTypes(String objectId, JsonNode inputParams) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileRemoveTypes(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #dbclusterSetDetails(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileRemoveTypes(String objectId, DXEnvironment env) { - return fileRemoveTypes(objectId, mapper.createObjectNode(), env); + public static JsonNode dbclusterSetDetails(String objectId, DXEnvironment env) { + return dbclusterSetDetails(objectId, mapper.createObjectNode(), env); } /** - * Invokes the fileRemoveTypes method with the specified environment and parameters. + * Invokes the dbclusterSetDetails method with the specified environment and parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -12982,18 +12971,18 @@ public static JsonNode fileRemoveTypes(String objectId, DXEnvironment env) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileRemoveTypes(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #dbclusterSetDetails(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileRemoveTypes(String objectId, JsonNode inputParams, DXEnvironment env) { - return new DXHTTPRequest(env).request("/" + objectId + "/" + "removeTypes", inputParams, + public static JsonNode dbclusterSetDetails(String objectId, JsonNode inputParams, DXEnvironment env) { + return new DXHTTPRequest(env).request("/" + objectId + "/" + "setDetails", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the fileRename method with an empty input, deserializing to an object of the specified class. + * Invokes the dbclusterSetProperties method with an empty input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -13007,13 +12996,13 @@ public static JsonNode fileRemoveTypes(String objectId, JsonNode inputParams, DX * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileRename(String objectId, Class outputClass) { - return fileRename(objectId, mapper.createObjectNode(), outputClass); + public static T dbclusterSetProperties(String objectId, Class outputClass) { + return dbclusterSetProperties(objectId, mapper.createObjectNode(), outputClass); } /** - * Invokes the fileRename method with the given input, deserializing to an object of the specified class. + * Invokes the dbclusterSetProperties method with the given input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -13028,16 +13017,16 @@ public static T fileRename(String objectId, Class outputClass) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileRename(String objectId, Object inputObject, Class outputClass) { + public static T dbclusterSetProperties(String objectId, Object inputObject, Class outputClass) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest().request("/" + objectId + "/" + "rename", + new DXHTTPRequest().request("/" + objectId + "/" + "setProperties", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the fileRename method with an empty input using the given environment, deserializing to an object of the specified class. + * Invokes the dbclusterSetProperties method with an empty input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -13052,13 +13041,13 @@ public static T fileRename(String objectId, Object inputObject, Class out * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileRename(String objectId, Class outputClass, DXEnvironment env) { - return fileRename(objectId, mapper.createObjectNode(), outputClass, env); + public static T dbclusterSetProperties(String objectId, Class outputClass, DXEnvironment env) { + return dbclusterSetProperties(objectId, mapper.createObjectNode(), outputClass, env); } /** - * Invokes the fileRename method with the given input using the given environment, deserializing to an object of the specified class. + * Invokes the dbclusterSetProperties method with the given input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -13074,17 +13063,17 @@ public static T fileRename(String objectId, Class outputClass, DXEnvironm * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileRename(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { + public static T dbclusterSetProperties(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest(env).request("/" + objectId + "/" + "rename", + new DXHTTPRequest(env).request("/" + objectId + "/" + "setProperties", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the fileRename method. + * Invokes the dbclusterSetProperties method. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @@ -13097,16 +13086,16 @@ public static T fileRename(String objectId, Object inputObject, Class out * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileRename(String, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #dbclusterSetProperties(String, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileRename(String objectId) { - return fileRename(objectId, mapper.createObjectNode()); + public static JsonNode dbclusterSetProperties(String objectId) { + return dbclusterSetProperties(objectId, mapper.createObjectNode()); } /** - * Invokes the fileRename method with the specified parameters. + * Invokes the dbclusterSetProperties method with the specified parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -13120,17 +13109,17 @@ public static JsonNode fileRename(String objectId) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileRename(String, Object, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #dbclusterSetProperties(String, Object, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileRename(String objectId, JsonNode inputParams) { - return new DXHTTPRequest().request("/" + objectId + "/" + "rename", inputParams, + public static JsonNode dbclusterSetProperties(String objectId, JsonNode inputParams) { + return new DXHTTPRequest().request("/" + objectId + "/" + "setProperties", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the fileRename method with the specified environment. + * Invokes the dbclusterSetProperties method with the specified environment. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param env environment object specifying the auth token and remote server and protocol @@ -13144,16 +13133,16 @@ public static JsonNode fileRename(String objectId, JsonNode inputParams) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileRename(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #dbclusterSetProperties(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileRename(String objectId, DXEnvironment env) { - return fileRename(objectId, mapper.createObjectNode(), env); + public static JsonNode dbclusterSetProperties(String objectId, DXEnvironment env) { + return dbclusterSetProperties(objectId, mapper.createObjectNode(), env); } /** - * Invokes the fileRename method with the specified environment and parameters. + * Invokes the dbclusterSetProperties method with the specified environment and parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -13168,18 +13157,18 @@ public static JsonNode fileRename(String objectId, DXEnvironment env) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileRename(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #dbclusterSetProperties(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileRename(String objectId, JsonNode inputParams, DXEnvironment env) { - return new DXHTTPRequest(env).request("/" + objectId + "/" + "rename", inputParams, + public static JsonNode dbclusterSetProperties(String objectId, JsonNode inputParams, DXEnvironment env) { + return new DXHTTPRequest(env).request("/" + objectId + "/" + "setProperties", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the fileSetDetails method with an empty input, deserializing to an object of the specified class. + * Invokes the dbclusterSetVisibility method with an empty input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -13193,13 +13182,13 @@ public static JsonNode fileRename(String objectId, JsonNode inputParams, DXEnvir * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileSetDetails(String objectId, Class outputClass) { - return fileSetDetails(objectId, mapper.createObjectNode(), outputClass); + public static T dbclusterSetVisibility(String objectId, Class outputClass) { + return dbclusterSetVisibility(objectId, mapper.createObjectNode(), outputClass); } /** - * Invokes the fileSetDetails method with the given input, deserializing to an object of the specified class. + * Invokes the dbclusterSetVisibility method with the given input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -13214,16 +13203,16 @@ public static T fileSetDetails(String objectId, Class outputClass) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileSetDetails(String objectId, Object inputObject, Class outputClass) { + public static T dbclusterSetVisibility(String objectId, Object inputObject, Class outputClass) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest().request("/" + objectId + "/" + "setDetails", + new DXHTTPRequest().request("/" + objectId + "/" + "setVisibility", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the fileSetDetails method with an empty input using the given environment, deserializing to an object of the specified class. + * Invokes the dbclusterSetVisibility method with an empty input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -13238,13 +13227,13 @@ public static T fileSetDetails(String objectId, Object inputObject, Class * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileSetDetails(String objectId, Class outputClass, DXEnvironment env) { - return fileSetDetails(objectId, mapper.createObjectNode(), outputClass, env); + public static T dbclusterSetVisibility(String objectId, Class outputClass, DXEnvironment env) { + return dbclusterSetVisibility(objectId, mapper.createObjectNode(), outputClass, env); } /** - * Invokes the fileSetDetails method with the given input using the given environment, deserializing to an object of the specified class. + * Invokes the dbclusterSetVisibility method with the given input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -13260,17 +13249,17 @@ public static T fileSetDetails(String objectId, Class outputClass, DXEnvi * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileSetDetails(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { + public static T dbclusterSetVisibility(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest(env).request("/" + objectId + "/" + "setDetails", + new DXHTTPRequest(env).request("/" + objectId + "/" + "setVisibility", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the fileSetDetails method. + * Invokes the dbclusterSetVisibility method. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @@ -13283,16 +13272,16 @@ public static T fileSetDetails(String objectId, Object inputObject, Class * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileSetDetails(String, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #dbclusterSetVisibility(String, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileSetDetails(String objectId) { - return fileSetDetails(objectId, mapper.createObjectNode()); + public static JsonNode dbclusterSetVisibility(String objectId) { + return dbclusterSetVisibility(objectId, mapper.createObjectNode()); } /** - * Invokes the fileSetDetails method with the specified parameters. + * Invokes the dbclusterSetVisibility method with the specified parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -13306,17 +13295,17 @@ public static JsonNode fileSetDetails(String objectId) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileSetDetails(String, Object, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #dbclusterSetVisibility(String, Object, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileSetDetails(String objectId, JsonNode inputParams) { - return new DXHTTPRequest().request("/" + objectId + "/" + "setDetails", inputParams, + public static JsonNode dbclusterSetVisibility(String objectId, JsonNode inputParams) { + return new DXHTTPRequest().request("/" + objectId + "/" + "setVisibility", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the fileSetDetails method with the specified environment. + * Invokes the dbclusterSetVisibility method with the specified environment. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param env environment object specifying the auth token and remote server and protocol @@ -13330,16 +13319,16 @@ public static JsonNode fileSetDetails(String objectId, JsonNode inputParams) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileSetDetails(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #dbclusterSetVisibility(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileSetDetails(String objectId, DXEnvironment env) { - return fileSetDetails(objectId, mapper.createObjectNode(), env); + public static JsonNode dbclusterSetVisibility(String objectId, DXEnvironment env) { + return dbclusterSetVisibility(objectId, mapper.createObjectNode(), env); } /** - * Invokes the fileSetDetails method with the specified environment and parameters. + * Invokes the dbclusterSetVisibility method with the specified environment and parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -13354,18 +13343,18 @@ public static JsonNode fileSetDetails(String objectId, DXEnvironment env) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileSetDetails(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #dbclusterSetVisibility(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileSetDetails(String objectId, JsonNode inputParams, DXEnvironment env) { - return new DXHTTPRequest(env).request("/" + objectId + "/" + "setDetails", inputParams, + public static JsonNode dbclusterSetVisibility(String objectId, JsonNode inputParams, DXEnvironment env) { + return new DXHTTPRequest(env).request("/" + objectId + "/" + "setVisibility", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the fileSetProperties method with an empty input, deserializing to an object of the specified class. + * Invokes the dbclusterStart method with an empty input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -13379,13 +13368,13 @@ public static JsonNode fileSetDetails(String objectId, JsonNode inputParams, DXE * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileSetProperties(String objectId, Class outputClass) { - return fileSetProperties(objectId, mapper.createObjectNode(), outputClass); + public static T dbclusterStart(String objectId, Class outputClass) { + return dbclusterStart(objectId, mapper.createObjectNode(), outputClass); } /** - * Invokes the fileSetProperties method with the given input, deserializing to an object of the specified class. + * Invokes the dbclusterStart method with the given input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -13400,16 +13389,16 @@ public static T fileSetProperties(String objectId, Class outputClass) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileSetProperties(String objectId, Object inputObject, Class outputClass) { + public static T dbclusterStart(String objectId, Object inputObject, Class outputClass) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest().request("/" + objectId + "/" + "setProperties", + new DXHTTPRequest().request("/" + objectId + "/" + "start", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the fileSetProperties method with an empty input using the given environment, deserializing to an object of the specified class. + * Invokes the dbclusterStart method with an empty input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -13424,13 +13413,13 @@ public static T fileSetProperties(String objectId, Object inputObject, Class * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileSetProperties(String objectId, Class outputClass, DXEnvironment env) { - return fileSetProperties(objectId, mapper.createObjectNode(), outputClass, env); + public static T dbclusterStart(String objectId, Class outputClass, DXEnvironment env) { + return dbclusterStart(objectId, mapper.createObjectNode(), outputClass, env); } /** - * Invokes the fileSetProperties method with the given input using the given environment, deserializing to an object of the specified class. + * Invokes the dbclusterStart method with the given input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -13446,17 +13435,17 @@ public static T fileSetProperties(String objectId, Class outputClass, DXE * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileSetProperties(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { + public static T dbclusterStart(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest(env).request("/" + objectId + "/" + "setProperties", + new DXHTTPRequest(env).request("/" + objectId + "/" + "start", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the fileSetProperties method. + * Invokes the dbclusterStart method. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @@ -13469,16 +13458,16 @@ public static T fileSetProperties(String objectId, Object inputObject, Class * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileSetProperties(String, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #dbclusterStart(String, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileSetProperties(String objectId) { - return fileSetProperties(objectId, mapper.createObjectNode()); + public static JsonNode dbclusterStart(String objectId) { + return dbclusterStart(objectId, mapper.createObjectNode()); } /** - * Invokes the fileSetProperties method with the specified parameters. + * Invokes the dbclusterStart method with the specified parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -13492,17 +13481,17 @@ public static JsonNode fileSetProperties(String objectId) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileSetProperties(String, Object, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #dbclusterStart(String, Object, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileSetProperties(String objectId, JsonNode inputParams) { - return new DXHTTPRequest().request("/" + objectId + "/" + "setProperties", inputParams, + public static JsonNode dbclusterStart(String objectId, JsonNode inputParams) { + return new DXHTTPRequest().request("/" + objectId + "/" + "start", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the fileSetProperties method with the specified environment. + * Invokes the dbclusterStart method with the specified environment. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param env environment object specifying the auth token and remote server and protocol @@ -13516,16 +13505,16 @@ public static JsonNode fileSetProperties(String objectId, JsonNode inputParams) * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileSetProperties(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #dbclusterStart(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileSetProperties(String objectId, DXEnvironment env) { - return fileSetProperties(objectId, mapper.createObjectNode(), env); + public static JsonNode dbclusterStart(String objectId, DXEnvironment env) { + return dbclusterStart(objectId, mapper.createObjectNode(), env); } /** - * Invokes the fileSetProperties method with the specified environment and parameters. + * Invokes the dbclusterStart method with the specified environment and parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -13540,18 +13529,18 @@ public static JsonNode fileSetProperties(String objectId, DXEnvironment env) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileSetProperties(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #dbclusterStart(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileSetProperties(String objectId, JsonNode inputParams, DXEnvironment env) { - return new DXHTTPRequest(env).request("/" + objectId + "/" + "setProperties", inputParams, + public static JsonNode dbclusterStart(String objectId, JsonNode inputParams, DXEnvironment env) { + return new DXHTTPRequest(env).request("/" + objectId + "/" + "start", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the fileSetVisibility method with an empty input, deserializing to an object of the specified class. + * Invokes the dbclusterStop method with an empty input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -13565,13 +13554,13 @@ public static JsonNode fileSetProperties(String objectId, JsonNode inputParams, * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileSetVisibility(String objectId, Class outputClass) { - return fileSetVisibility(objectId, mapper.createObjectNode(), outputClass); + public static T dbclusterStop(String objectId, Class outputClass) { + return dbclusterStop(objectId, mapper.createObjectNode(), outputClass); } /** - * Invokes the fileSetVisibility method with the given input, deserializing to an object of the specified class. + * Invokes the dbclusterStop method with the given input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -13586,16 +13575,16 @@ public static T fileSetVisibility(String objectId, Class outputClass) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileSetVisibility(String objectId, Object inputObject, Class outputClass) { + public static T dbclusterStop(String objectId, Object inputObject, Class outputClass) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest().request("/" + objectId + "/" + "setVisibility", + new DXHTTPRequest().request("/" + objectId + "/" + "stop", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the fileSetVisibility method with an empty input using the given environment, deserializing to an object of the specified class. + * Invokes the dbclusterStop method with an empty input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -13610,13 +13599,13 @@ public static T fileSetVisibility(String objectId, Object inputObject, Class * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileSetVisibility(String objectId, Class outputClass, DXEnvironment env) { - return fileSetVisibility(objectId, mapper.createObjectNode(), outputClass, env); + public static T dbclusterStop(String objectId, Class outputClass, DXEnvironment env) { + return dbclusterStop(objectId, mapper.createObjectNode(), outputClass, env); } /** - * Invokes the fileSetVisibility method with the given input using the given environment, deserializing to an object of the specified class. + * Invokes the dbclusterStop method with the given input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -13632,17 +13621,17 @@ public static T fileSetVisibility(String objectId, Class outputClass, DXE * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileSetVisibility(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { + public static T dbclusterStop(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest(env).request("/" + objectId + "/" + "setVisibility", + new DXHTTPRequest(env).request("/" + objectId + "/" + "stop", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the fileSetVisibility method. + * Invokes the dbclusterStop method. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @@ -13655,16 +13644,16 @@ public static T fileSetVisibility(String objectId, Object inputObject, Class * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileSetVisibility(String, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #dbclusterStop(String, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileSetVisibility(String objectId) { - return fileSetVisibility(objectId, mapper.createObjectNode()); + public static JsonNode dbclusterStop(String objectId) { + return dbclusterStop(objectId, mapper.createObjectNode()); } /** - * Invokes the fileSetVisibility method with the specified parameters. + * Invokes the dbclusterStop method with the specified parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -13678,17 +13667,17 @@ public static JsonNode fileSetVisibility(String objectId) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileSetVisibility(String, Object, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #dbclusterStop(String, Object, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileSetVisibility(String objectId, JsonNode inputParams) { - return new DXHTTPRequest().request("/" + objectId + "/" + "setVisibility", inputParams, + public static JsonNode dbclusterStop(String objectId, JsonNode inputParams) { + return new DXHTTPRequest().request("/" + objectId + "/" + "stop", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the fileSetVisibility method with the specified environment. + * Invokes the dbclusterStop method with the specified environment. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param env environment object specifying the auth token and remote server and protocol @@ -13702,16 +13691,16 @@ public static JsonNode fileSetVisibility(String objectId, JsonNode inputParams) * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileSetVisibility(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #dbclusterStop(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileSetVisibility(String objectId, DXEnvironment env) { - return fileSetVisibility(objectId, mapper.createObjectNode(), env); + public static JsonNode dbclusterStop(String objectId, DXEnvironment env) { + return dbclusterStop(objectId, mapper.createObjectNode(), env); } /** - * Invokes the fileSetVisibility method with the specified environment and parameters. + * Invokes the dbclusterStop method with the specified environment and parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -13726,18 +13715,18 @@ public static JsonNode fileSetVisibility(String objectId, DXEnvironment env) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileSetVisibility(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #dbclusterStop(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileSetVisibility(String objectId, JsonNode inputParams, DXEnvironment env) { - return new DXHTTPRequest(env).request("/" + objectId + "/" + "setVisibility", inputParams, + public static JsonNode dbclusterStop(String objectId, JsonNode inputParams, DXEnvironment env) { + return new DXHTTPRequest(env).request("/" + objectId + "/" + "stop", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the fileUpload method with an empty input, deserializing to an object of the specified class. + * Invokes the dbclusterTerminate method with an empty input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -13751,13 +13740,13 @@ public static JsonNode fileSetVisibility(String objectId, JsonNode inputParams, * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileUpload(String objectId, Class outputClass) { - return fileUpload(objectId, mapper.createObjectNode(), outputClass); + public static T dbclusterTerminate(String objectId, Class outputClass) { + return dbclusterTerminate(objectId, mapper.createObjectNode(), outputClass); } /** - * Invokes the fileUpload method with the given input, deserializing to an object of the specified class. + * Invokes the dbclusterTerminate method with the given input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -13772,16 +13761,16 @@ public static T fileUpload(String objectId, Class outputClass) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileUpload(String objectId, Object inputObject, Class outputClass) { + public static T dbclusterTerminate(String objectId, Object inputObject, Class outputClass) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest().request("/" + objectId + "/" + "upload", + new DXHTTPRequest().request("/" + objectId + "/" + "terminate", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the fileUpload method with an empty input using the given environment, deserializing to an object of the specified class. + * Invokes the dbclusterTerminate method with an empty input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -13796,13 +13785,13 @@ public static T fileUpload(String objectId, Object inputObject, Class out * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileUpload(String objectId, Class outputClass, DXEnvironment env) { - return fileUpload(objectId, mapper.createObjectNode(), outputClass, env); + public static T dbclusterTerminate(String objectId, Class outputClass, DXEnvironment env) { + return dbclusterTerminate(objectId, mapper.createObjectNode(), outputClass, env); } /** - * Invokes the fileUpload method with the given input using the given environment, deserializing to an object of the specified class. + * Invokes the dbclusterTerminate method with the given input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -13818,17 +13807,17 @@ public static T fileUpload(String objectId, Class outputClass, DXEnvironm * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileUpload(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { + public static T dbclusterTerminate(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest(env).request("/" + objectId + "/" + "upload", + new DXHTTPRequest(env).request("/" + objectId + "/" + "terminate", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the fileUpload method. + * Invokes the dbclusterTerminate method. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @@ -13841,16 +13830,16 @@ public static T fileUpload(String objectId, Object inputObject, Class out * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileUpload(String, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #dbclusterTerminate(String, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileUpload(String objectId) { - return fileUpload(objectId, mapper.createObjectNode()); + public static JsonNode dbclusterTerminate(String objectId) { + return dbclusterTerminate(objectId, mapper.createObjectNode()); } /** - * Invokes the fileUpload method with the specified parameters. + * Invokes the dbclusterTerminate method with the specified parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -13864,17 +13853,17 @@ public static JsonNode fileUpload(String objectId) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileUpload(String, Object, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #dbclusterTerminate(String, Object, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileUpload(String objectId, JsonNode inputParams) { - return new DXHTTPRequest().request("/" + objectId + "/" + "upload", inputParams, + public static JsonNode dbclusterTerminate(String objectId, JsonNode inputParams) { + return new DXHTTPRequest().request("/" + objectId + "/" + "terminate", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the fileUpload method with the specified environment. + * Invokes the dbclusterTerminate method with the specified environment. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param env environment object specifying the auth token and remote server and protocol @@ -13888,16 +13877,16 @@ public static JsonNode fileUpload(String objectId, JsonNode inputParams) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileUpload(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #dbclusterTerminate(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileUpload(String objectId, DXEnvironment env) { - return fileUpload(objectId, mapper.createObjectNode(), env); + public static JsonNode dbclusterTerminate(String objectId, DXEnvironment env) { + return dbclusterTerminate(objectId, mapper.createObjectNode(), env); } /** - * Invokes the fileUpload method with the specified environment and parameters. + * Invokes the dbclusterTerminate method with the specified environment and parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -13912,22 +13901,23 @@ public static JsonNode fileUpload(String objectId, DXEnvironment env) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileUpload(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #dbclusterTerminate(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileUpload(String objectId, JsonNode inputParams, DXEnvironment env) { - return new DXHTTPRequest(env).request("/" + objectId + "/" + "upload", inputParams, + public static JsonNode dbclusterTerminate(String objectId, JsonNode inputParams, DXEnvironment env) { + return new DXHTTPRequest(env).request("/" + objectId + "/" + "terminate", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the fileNew method with an empty input, deserializing to an object of the specified class. + * Invokes the fileAddTags method with an empty input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * + * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to * - * @return Server response parsed from JSON + * @return Response object * * @throws DXAPIException * If the server returns a complete response with an HTTP status @@ -13936,18 +13926,19 @@ public static JsonNode fileUpload(String objectId, JsonNode inputParams, DXEnvir * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileNew(Class outputClass) { - return fileNew(mapper.createObjectNode(), outputClass); + public static T fileAddTags(String objectId, Class outputClass) { + return fileAddTags(objectId, mapper.createObjectNode(), outputClass); } /** - * Invokes the fileNew method with an empty input using the specified environment, deserializing to an object of the specified class. + * Invokes the fileAddTags method with the given input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * + * @param objectId ID of the object to operate on + * @param inputObject input object (to be JSON serialized to an input hash) * @param outputClass class to deserialize the server reponse to - * @param env environment object specifying the auth token and remote server and protocol * - * @return Server response parsed from JSON + * @return Response object * * @throws DXAPIException * If the server returns a complete response with an HTTP status @@ -13956,18 +13947,22 @@ public static T fileNew(Class outputClass) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileNew(Class outputClass, DXEnvironment env) { - return fileNew(mapper.createObjectNode(), outputClass, env); + public static T fileAddTags(String objectId, Object inputObject, Class outputClass) { + JsonNode input = mapper.valueToTree(inputObject); + return DXJSON.safeTreeToValue( + new DXHTTPRequest().request("/" + objectId + "/" + "addTags", + input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the fileNew method with the specified input, deserializing to an object of the specified class. + * Invokes the fileAddTags method with an empty input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * - * @param inputObject input object (to be JSON serialized to an input hash) + * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to + * @param env environment object specifying the auth token and remote server and protocol * - * @return Server response parsed from JSON + * @return Response object * * @throws DXAPIException * If the server returns a complete response with an HTTP status @@ -13976,22 +13971,20 @@ public static T fileNew(Class outputClass, DXEnvironment env) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileNew(Object inputObject, Class outputClass) { - JsonNode input = Nonce.updateNonce(mapper.valueToTree(inputObject)); - return DXJSON.safeTreeToValue( - new DXHTTPRequest().request("/file/new", input, RetryStrategy.SAFE_TO_RETRY), - outputClass); + public static T fileAddTags(String objectId, Class outputClass, DXEnvironment env) { + return fileAddTags(objectId, mapper.createObjectNode(), outputClass, env); } /** - * Invokes the fileNew method with the specified input using the specified environment, deserializing to an object of the specified class. + * Invokes the fileAddTags method with the given input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * + * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) * @param outputClass class to deserialize the server reponse to * @param env environment object specifying the auth token and remote server and protocol * - * @return Server response parsed from JSON + * @return Response object * * @throws DXAPIException * If the server returns a complete response with an HTTP status @@ -14000,17 +13993,19 @@ public static T fileNew(Object inputObject, Class outputClass) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T fileNew(Object inputObject, Class outputClass, DXEnvironment env) { - JsonNode input = Nonce.updateNonce(mapper.valueToTree(inputObject)); + public static T fileAddTags(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { + JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest(env).request("/file/new", input, RetryStrategy.SAFE_TO_RETRY), - outputClass); + new DXHTTPRequest(env).request("/" + objectId + "/" + "addTags", + input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the fileNew method. + * Invokes the fileAddTags method. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on * * @return Server response parsed from JSON * @@ -14021,17 +14016,18 @@ public static T fileNew(Object inputObject, Class outputClass, DXEnvironm * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileNew(Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileAddTags(String, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileNew() { - return fileNew(mapper.createObjectNode()); + public static JsonNode fileAddTags(String objectId) { + return fileAddTags(objectId, mapper.createObjectNode()); } /** - * Invokes the fileNew method with the specified input parameters. + * Invokes the fileAddTags method with the specified parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * + * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call * * @return Server response parsed from JSON @@ -14043,17 +14039,19 @@ public static JsonNode fileNew() { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileNew(Object, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileAddTags(String, Object, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileNew(JsonNode inputParams) { - return new DXHTTPRequest().request("/file/new", inputParams, RetryStrategy.UNSAFE_TO_RETRY); + public static JsonNode fileAddTags(String objectId, JsonNode inputParams) { + return new DXHTTPRequest().request("/" + objectId + "/" + "addTags", inputParams, + RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the fileNew method with the specified environment. + * Invokes the fileAddTags method with the specified environment. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * + * @param objectId ID of the object to operate on * @param env environment object specifying the auth token and remote server and protocol * * @return Server response parsed from JSON @@ -14065,17 +14063,18 @@ public static JsonNode fileNew(JsonNode inputParams) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileNew(Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileAddTags(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileNew(DXEnvironment env) { - return fileNew(mapper.createObjectNode(), env); + public static JsonNode fileAddTags(String objectId, DXEnvironment env) { + return fileAddTags(objectId, mapper.createObjectNode(), env); } /** - * Invokes the fileNew method with the specified environment and input parameters. + * Invokes the fileAddTags method with the specified environment and parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * + * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call * @param env environment object specifying the auth token and remote server and protocol * @@ -14088,17 +14087,18 @@ public static JsonNode fileNew(DXEnvironment env) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #fileNew(Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileAddTags(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode fileNew(JsonNode inputParams, DXEnvironment env) { - return new DXHTTPRequest(env).request("/file/new", inputParams, RetryStrategy.UNSAFE_TO_RETRY); + public static JsonNode fileAddTags(String objectId, JsonNode inputParams, DXEnvironment env) { + return new DXHTTPRequest(env).request("/" + objectId + "/" + "addTags", inputParams, + RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the globalWorkflowAddAuthorizedUsers method with an empty input, deserializing to an object of the specified class. + * Invokes the fileAddTypes method with an empty input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -14112,13 +14112,13 @@ public static JsonNode fileNew(JsonNode inputParams, DXEnvironment env) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowAddAuthorizedUsers(String objectId, Class outputClass) { - return globalWorkflowAddAuthorizedUsers(objectId, mapper.createObjectNode(), outputClass); + public static T fileAddTypes(String objectId, Class outputClass) { + return fileAddTypes(objectId, mapper.createObjectNode(), outputClass); } /** - * Invokes the globalWorkflowAddAuthorizedUsers method with the given input, deserializing to an object of the specified class. + * Invokes the fileAddTypes method with the given input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -14133,16 +14133,16 @@ public static T globalWorkflowAddAuthorizedUsers(String objectId, Class o * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowAddAuthorizedUsers(String objectId, Object inputObject, Class outputClass) { + public static T fileAddTypes(String objectId, Object inputObject, Class outputClass) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest().request("/" + objectId + "/" + "addAuthorizedUsers", + new DXHTTPRequest().request("/" + objectId + "/" + "addTypes", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the globalWorkflowAddAuthorizedUsers method with an empty input using the given environment, deserializing to an object of the specified class. + * Invokes the fileAddTypes method with an empty input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -14157,13 +14157,13 @@ public static T globalWorkflowAddAuthorizedUsers(String objectId, Object inp * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowAddAuthorizedUsers(String objectId, Class outputClass, DXEnvironment env) { - return globalWorkflowAddAuthorizedUsers(objectId, mapper.createObjectNode(), outputClass, env); + public static T fileAddTypes(String objectId, Class outputClass, DXEnvironment env) { + return fileAddTypes(objectId, mapper.createObjectNode(), outputClass, env); } /** - * Invokes the globalWorkflowAddAuthorizedUsers method with the given input using the given environment, deserializing to an object of the specified class. + * Invokes the fileAddTypes method with the given input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -14179,17 +14179,17 @@ public static T globalWorkflowAddAuthorizedUsers(String objectId, Class o * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowAddAuthorizedUsers(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { + public static T fileAddTypes(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest(env).request("/" + objectId + "/" + "addAuthorizedUsers", + new DXHTTPRequest(env).request("/" + objectId + "/" + "addTypes", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the globalWorkflowAddAuthorizedUsers method. + * Invokes the fileAddTypes method. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @@ -14202,16 +14202,16 @@ public static T globalWorkflowAddAuthorizedUsers(String objectId, Object inp * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowAddAuthorizedUsers(String, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileAddTypes(String, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowAddAuthorizedUsers(String objectId) { - return globalWorkflowAddAuthorizedUsers(objectId, mapper.createObjectNode()); + public static JsonNode fileAddTypes(String objectId) { + return fileAddTypes(objectId, mapper.createObjectNode()); } /** - * Invokes the globalWorkflowAddAuthorizedUsers method with the specified parameters. + * Invokes the fileAddTypes method with the specified parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -14225,17 +14225,17 @@ public static JsonNode globalWorkflowAddAuthorizedUsers(String objectId) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowAddAuthorizedUsers(String, Object, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileAddTypes(String, Object, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowAddAuthorizedUsers(String objectId, JsonNode inputParams) { - return new DXHTTPRequest().request("/" + objectId + "/" + "addAuthorizedUsers", inputParams, + public static JsonNode fileAddTypes(String objectId, JsonNode inputParams) { + return new DXHTTPRequest().request("/" + objectId + "/" + "addTypes", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the globalWorkflowAddAuthorizedUsers method with the specified environment. + * Invokes the fileAddTypes method with the specified environment. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param env environment object specifying the auth token and remote server and protocol @@ -14249,16 +14249,16 @@ public static JsonNode globalWorkflowAddAuthorizedUsers(String objectId, JsonNod * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowAddAuthorizedUsers(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileAddTypes(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowAddAuthorizedUsers(String objectId, DXEnvironment env) { - return globalWorkflowAddAuthorizedUsers(objectId, mapper.createObjectNode(), env); + public static JsonNode fileAddTypes(String objectId, DXEnvironment env) { + return fileAddTypes(objectId, mapper.createObjectNode(), env); } /** - * Invokes the globalWorkflowAddAuthorizedUsers method with the specified environment and parameters. + * Invokes the fileAddTypes method with the specified environment and parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -14273,18 +14273,18 @@ public static JsonNode globalWorkflowAddAuthorizedUsers(String objectId, DXEnvir * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowAddAuthorizedUsers(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileAddTypes(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowAddAuthorizedUsers(String objectId, JsonNode inputParams, DXEnvironment env) { - return new DXHTTPRequest(env).request("/" + objectId + "/" + "addAuthorizedUsers", inputParams, + public static JsonNode fileAddTypes(String objectId, JsonNode inputParams, DXEnvironment env) { + return new DXHTTPRequest(env).request("/" + objectId + "/" + "addTypes", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the globalWorkflowAddCategories method with an empty input, deserializing to an object of the specified class. + * Invokes the fileClose method with an empty input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -14298,13 +14298,13 @@ public static JsonNode globalWorkflowAddAuthorizedUsers(String objectId, JsonNod * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowAddCategories(String objectId, Class outputClass) { - return globalWorkflowAddCategories(objectId, mapper.createObjectNode(), outputClass); + public static T fileClose(String objectId, Class outputClass) { + return fileClose(objectId, mapper.createObjectNode(), outputClass); } /** - * Invokes the globalWorkflowAddCategories method with the given input, deserializing to an object of the specified class. + * Invokes the fileClose method with the given input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -14319,16 +14319,16 @@ public static T globalWorkflowAddCategories(String objectId, Class output * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowAddCategories(String objectId, Object inputObject, Class outputClass) { + public static T fileClose(String objectId, Object inputObject, Class outputClass) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest().request("/" + objectId + "/" + "addCategories", + new DXHTTPRequest().request("/" + objectId + "/" + "close", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the globalWorkflowAddCategories method with an empty input using the given environment, deserializing to an object of the specified class. + * Invokes the fileClose method with an empty input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -14343,13 +14343,13 @@ public static T globalWorkflowAddCategories(String objectId, Object inputObj * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowAddCategories(String objectId, Class outputClass, DXEnvironment env) { - return globalWorkflowAddCategories(objectId, mapper.createObjectNode(), outputClass, env); + public static T fileClose(String objectId, Class outputClass, DXEnvironment env) { + return fileClose(objectId, mapper.createObjectNode(), outputClass, env); } /** - * Invokes the globalWorkflowAddCategories method with the given input using the given environment, deserializing to an object of the specified class. + * Invokes the fileClose method with the given input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -14365,17 +14365,17 @@ public static T globalWorkflowAddCategories(String objectId, Class output * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowAddCategories(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { + public static T fileClose(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest(env).request("/" + objectId + "/" + "addCategories", + new DXHTTPRequest(env).request("/" + objectId + "/" + "close", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the globalWorkflowAddCategories method. + * Invokes the fileClose method. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @@ -14388,16 +14388,16 @@ public static T globalWorkflowAddCategories(String objectId, Object inputObj * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowAddCategories(String, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileClose(String, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowAddCategories(String objectId) { - return globalWorkflowAddCategories(objectId, mapper.createObjectNode()); + public static JsonNode fileClose(String objectId) { + return fileClose(objectId, mapper.createObjectNode()); } /** - * Invokes the globalWorkflowAddCategories method with the specified parameters. + * Invokes the fileClose method with the specified parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -14411,17 +14411,17 @@ public static JsonNode globalWorkflowAddCategories(String objectId) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowAddCategories(String, Object, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileClose(String, Object, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowAddCategories(String objectId, JsonNode inputParams) { - return new DXHTTPRequest().request("/" + objectId + "/" + "addCategories", inputParams, + public static JsonNode fileClose(String objectId, JsonNode inputParams) { + return new DXHTTPRequest().request("/" + objectId + "/" + "close", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the globalWorkflowAddCategories method with the specified environment. + * Invokes the fileClose method with the specified environment. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param env environment object specifying the auth token and remote server and protocol @@ -14435,16 +14435,16 @@ public static JsonNode globalWorkflowAddCategories(String objectId, JsonNode inp * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowAddCategories(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileClose(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowAddCategories(String objectId, DXEnvironment env) { - return globalWorkflowAddCategories(objectId, mapper.createObjectNode(), env); + public static JsonNode fileClose(String objectId, DXEnvironment env) { + return fileClose(objectId, mapper.createObjectNode(), env); } /** - * Invokes the globalWorkflowAddCategories method with the specified environment and parameters. + * Invokes the fileClose method with the specified environment and parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -14459,18 +14459,18 @@ public static JsonNode globalWorkflowAddCategories(String objectId, DXEnvironmen * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowAddCategories(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileClose(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowAddCategories(String objectId, JsonNode inputParams, DXEnvironment env) { - return new DXHTTPRequest(env).request("/" + objectId + "/" + "addCategories", inputParams, + public static JsonNode fileClose(String objectId, JsonNode inputParams, DXEnvironment env) { + return new DXHTTPRequest(env).request("/" + objectId + "/" + "close", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the globalWorkflowAddDevelopers method with an empty input, deserializing to an object of the specified class. + * Invokes the fileDescribe method with an empty input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -14484,13 +14484,13 @@ public static JsonNode globalWorkflowAddCategories(String objectId, JsonNode inp * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowAddDevelopers(String objectId, Class outputClass) { - return globalWorkflowAddDevelopers(objectId, mapper.createObjectNode(), outputClass); + public static T fileDescribe(String objectId, Class outputClass) { + return fileDescribe(objectId, mapper.createObjectNode(), outputClass); } /** - * Invokes the globalWorkflowAddDevelopers method with the given input, deserializing to an object of the specified class. + * Invokes the fileDescribe method with the given input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -14505,16 +14505,16 @@ public static T globalWorkflowAddDevelopers(String objectId, Class output * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowAddDevelopers(String objectId, Object inputObject, Class outputClass) { + public static T fileDescribe(String objectId, Object inputObject, Class outputClass) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest().request("/" + objectId + "/" + "addDevelopers", + new DXHTTPRequest().request("/" + objectId + "/" + "describe", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the globalWorkflowAddDevelopers method with an empty input using the given environment, deserializing to an object of the specified class. + * Invokes the fileDescribe method with an empty input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -14529,13 +14529,13 @@ public static T globalWorkflowAddDevelopers(String objectId, Object inputObj * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowAddDevelopers(String objectId, Class outputClass, DXEnvironment env) { - return globalWorkflowAddDevelopers(objectId, mapper.createObjectNode(), outputClass, env); + public static T fileDescribe(String objectId, Class outputClass, DXEnvironment env) { + return fileDescribe(objectId, mapper.createObjectNode(), outputClass, env); } /** - * Invokes the globalWorkflowAddDevelopers method with the given input using the given environment, deserializing to an object of the specified class. + * Invokes the fileDescribe method with the given input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -14551,17 +14551,17 @@ public static T globalWorkflowAddDevelopers(String objectId, Class output * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowAddDevelopers(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { + public static T fileDescribe(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest(env).request("/" + objectId + "/" + "addDevelopers", + new DXHTTPRequest(env).request("/" + objectId + "/" + "describe", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the globalWorkflowAddDevelopers method. + * Invokes the fileDescribe method. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @@ -14574,16 +14574,16 @@ public static T globalWorkflowAddDevelopers(String objectId, Object inputObj * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowAddDevelopers(String, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileDescribe(String, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowAddDevelopers(String objectId) { - return globalWorkflowAddDevelopers(objectId, mapper.createObjectNode()); + public static JsonNode fileDescribe(String objectId) { + return fileDescribe(objectId, mapper.createObjectNode()); } /** - * Invokes the globalWorkflowAddDevelopers method with the specified parameters. + * Invokes the fileDescribe method with the specified parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -14597,17 +14597,17 @@ public static JsonNode globalWorkflowAddDevelopers(String objectId) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowAddDevelopers(String, Object, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileDescribe(String, Object, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowAddDevelopers(String objectId, JsonNode inputParams) { - return new DXHTTPRequest().request("/" + objectId + "/" + "addDevelopers", inputParams, + public static JsonNode fileDescribe(String objectId, JsonNode inputParams) { + return new DXHTTPRequest().request("/" + objectId + "/" + "describe", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the globalWorkflowAddDevelopers method with the specified environment. + * Invokes the fileDescribe method with the specified environment. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param env environment object specifying the auth token and remote server and protocol @@ -14621,16 +14621,16 @@ public static JsonNode globalWorkflowAddDevelopers(String objectId, JsonNode inp * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowAddDevelopers(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileDescribe(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowAddDevelopers(String objectId, DXEnvironment env) { - return globalWorkflowAddDevelopers(objectId, mapper.createObjectNode(), env); + public static JsonNode fileDescribe(String objectId, DXEnvironment env) { + return fileDescribe(objectId, mapper.createObjectNode(), env); } /** - * Invokes the globalWorkflowAddDevelopers method with the specified environment and parameters. + * Invokes the fileDescribe method with the specified environment and parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -14645,18 +14645,18 @@ public static JsonNode globalWorkflowAddDevelopers(String objectId, DXEnvironmen * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowAddDevelopers(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileDescribe(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowAddDevelopers(String objectId, JsonNode inputParams, DXEnvironment env) { - return new DXHTTPRequest(env).request("/" + objectId + "/" + "addDevelopers", inputParams, + public static JsonNode fileDescribe(String objectId, JsonNode inputParams, DXEnvironment env) { + return new DXHTTPRequest(env).request("/" + objectId + "/" + "describe", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the globalWorkflowAddTags method with an empty input, deserializing to an object of the specified class. + * Invokes the fileDownload method with an empty input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -14670,13 +14670,13 @@ public static JsonNode globalWorkflowAddDevelopers(String objectId, JsonNode inp * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowAddTags(String objectId, Class outputClass) { - return globalWorkflowAddTags(objectId, mapper.createObjectNode(), outputClass); + public static T fileDownload(String objectId, Class outputClass) { + return fileDownload(objectId, mapper.createObjectNode(), outputClass); } /** - * Invokes the globalWorkflowAddTags method with the given input, deserializing to an object of the specified class. + * Invokes the fileDownload method with the given input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -14691,16 +14691,16 @@ public static T globalWorkflowAddTags(String objectId, Class outputClass) * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowAddTags(String objectId, Object inputObject, Class outputClass) { + public static T fileDownload(String objectId, Object inputObject, Class outputClass) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest().request("/" + objectId + "/" + "addTags", + new DXHTTPRequest().request("/" + objectId + "/" + "download", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the globalWorkflowAddTags method with an empty input using the given environment, deserializing to an object of the specified class. + * Invokes the fileDownload method with an empty input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -14715,13 +14715,13 @@ public static T globalWorkflowAddTags(String objectId, Object inputObject, C * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowAddTags(String objectId, Class outputClass, DXEnvironment env) { - return globalWorkflowAddTags(objectId, mapper.createObjectNode(), outputClass, env); + public static T fileDownload(String objectId, Class outputClass, DXEnvironment env) { + return fileDownload(objectId, mapper.createObjectNode(), outputClass, env); } /** - * Invokes the globalWorkflowAddTags method with the given input using the given environment, deserializing to an object of the specified class. + * Invokes the fileDownload method with the given input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -14737,17 +14737,17 @@ public static T globalWorkflowAddTags(String objectId, Class outputClass, * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowAddTags(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { + public static T fileDownload(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest(env).request("/" + objectId + "/" + "addTags", + new DXHTTPRequest(env).request("/" + objectId + "/" + "download", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the globalWorkflowAddTags method. + * Invokes the fileDownload method. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @@ -14760,16 +14760,16 @@ public static T globalWorkflowAddTags(String objectId, Object inputObject, C * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowAddTags(String, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileDownload(String, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowAddTags(String objectId) { - return globalWorkflowAddTags(objectId, mapper.createObjectNode()); + public static JsonNode fileDownload(String objectId) { + return fileDownload(objectId, mapper.createObjectNode()); } /** - * Invokes the globalWorkflowAddTags method with the specified parameters. + * Invokes the fileDownload method with the specified parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -14783,17 +14783,17 @@ public static JsonNode globalWorkflowAddTags(String objectId) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowAddTags(String, Object, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileDownload(String, Object, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowAddTags(String objectId, JsonNode inputParams) { - return new DXHTTPRequest().request("/" + objectId + "/" + "addTags", inputParams, + public static JsonNode fileDownload(String objectId, JsonNode inputParams) { + return new DXHTTPRequest().request("/" + objectId + "/" + "download", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the globalWorkflowAddTags method with the specified environment. + * Invokes the fileDownload method with the specified environment. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param env environment object specifying the auth token and remote server and protocol @@ -14807,16 +14807,16 @@ public static JsonNode globalWorkflowAddTags(String objectId, JsonNode inputPara * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowAddTags(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileDownload(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowAddTags(String objectId, DXEnvironment env) { - return globalWorkflowAddTags(objectId, mapper.createObjectNode(), env); + public static JsonNode fileDownload(String objectId, DXEnvironment env) { + return fileDownload(objectId, mapper.createObjectNode(), env); } /** - * Invokes the globalWorkflowAddTags method with the specified environment and parameters. + * Invokes the fileDownload method with the specified environment and parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -14831,18 +14831,18 @@ public static JsonNode globalWorkflowAddTags(String objectId, DXEnvironment env) * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowAddTags(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileDownload(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowAddTags(String objectId, JsonNode inputParams, DXEnvironment env) { - return new DXHTTPRequest(env).request("/" + objectId + "/" + "addTags", inputParams, + public static JsonNode fileDownload(String objectId, JsonNode inputParams, DXEnvironment env) { + return new DXHTTPRequest(env).request("/" + objectId + "/" + "download", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the globalWorkflowDelete method with an empty input, deserializing to an object of the specified class. + * Invokes the fileGetDetails method with an empty input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -14856,13 +14856,13 @@ public static JsonNode globalWorkflowAddTags(String objectId, JsonNode inputPara * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowDelete(String objectId, Class outputClass) { - return globalWorkflowDelete(objectId, mapper.createObjectNode(), outputClass); + public static T fileGetDetails(String objectId, Class outputClass) { + return fileGetDetails(objectId, mapper.createObjectNode(), outputClass); } /** - * Invokes the globalWorkflowDelete method with the given input, deserializing to an object of the specified class. + * Invokes the fileGetDetails method with the given input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -14877,16 +14877,16 @@ public static T globalWorkflowDelete(String objectId, Class outputClass) * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowDelete(String objectId, Object inputObject, Class outputClass) { + public static T fileGetDetails(String objectId, Object inputObject, Class outputClass) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest().request("/" + objectId + "/" + "delete", + new DXHTTPRequest().request("/" + objectId + "/" + "getDetails", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the globalWorkflowDelete method with an empty input using the given environment, deserializing to an object of the specified class. + * Invokes the fileGetDetails method with an empty input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -14901,13 +14901,13 @@ public static T globalWorkflowDelete(String objectId, Object inputObject, Cl * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowDelete(String objectId, Class outputClass, DXEnvironment env) { - return globalWorkflowDelete(objectId, mapper.createObjectNode(), outputClass, env); + public static T fileGetDetails(String objectId, Class outputClass, DXEnvironment env) { + return fileGetDetails(objectId, mapper.createObjectNode(), outputClass, env); } /** - * Invokes the globalWorkflowDelete method with the given input using the given environment, deserializing to an object of the specified class. + * Invokes the fileGetDetails method with the given input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -14923,17 +14923,17 @@ public static T globalWorkflowDelete(String objectId, Class outputClass, * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowDelete(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { + public static T fileGetDetails(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest(env).request("/" + objectId + "/" + "delete", + new DXHTTPRequest(env).request("/" + objectId + "/" + "getDetails", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the globalWorkflowDelete method. + * Invokes the fileGetDetails method. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @@ -14946,16 +14946,16 @@ public static T globalWorkflowDelete(String objectId, Object inputObject, Cl * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowDelete(String, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileGetDetails(String, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowDelete(String objectId) { - return globalWorkflowDelete(objectId, mapper.createObjectNode()); + public static JsonNode fileGetDetails(String objectId) { + return fileGetDetails(objectId, mapper.createObjectNode()); } /** - * Invokes the globalWorkflowDelete method with the specified parameters. + * Invokes the fileGetDetails method with the specified parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -14969,17 +14969,17 @@ public static JsonNode globalWorkflowDelete(String objectId) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowDelete(String, Object, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileGetDetails(String, Object, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowDelete(String objectId, JsonNode inputParams) { - return new DXHTTPRequest().request("/" + objectId + "/" + "delete", inputParams, + public static JsonNode fileGetDetails(String objectId, JsonNode inputParams) { + return new DXHTTPRequest().request("/" + objectId + "/" + "getDetails", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the globalWorkflowDelete method with the specified environment. + * Invokes the fileGetDetails method with the specified environment. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param env environment object specifying the auth token and remote server and protocol @@ -14993,16 +14993,16 @@ public static JsonNode globalWorkflowDelete(String objectId, JsonNode inputParam * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowDelete(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileGetDetails(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowDelete(String objectId, DXEnvironment env) { - return globalWorkflowDelete(objectId, mapper.createObjectNode(), env); + public static JsonNode fileGetDetails(String objectId, DXEnvironment env) { + return fileGetDetails(objectId, mapper.createObjectNode(), env); } /** - * Invokes the globalWorkflowDelete method with the specified environment and parameters. + * Invokes the fileGetDetails method with the specified environment and parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -15017,18 +15017,18 @@ public static JsonNode globalWorkflowDelete(String objectId, DXEnvironment env) * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowDelete(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileGetDetails(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowDelete(String objectId, JsonNode inputParams, DXEnvironment env) { - return new DXHTTPRequest(env).request("/" + objectId + "/" + "delete", inputParams, + public static JsonNode fileGetDetails(String objectId, JsonNode inputParams, DXEnvironment env) { + return new DXHTTPRequest(env).request("/" + objectId + "/" + "getDetails", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the globalWorkflowDescribe method with an empty input, deserializing to an object of the specified class. + * Invokes the fileListProjects method with an empty input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -15042,13 +15042,13 @@ public static JsonNode globalWorkflowDelete(String objectId, JsonNode inputParam * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowDescribe(String objectId, Class outputClass) { - return globalWorkflowDescribe(objectId, mapper.createObjectNode(), outputClass); + public static T fileListProjects(String objectId, Class outputClass) { + return fileListProjects(objectId, mapper.createObjectNode(), outputClass); } /** - * Invokes the globalWorkflowDescribe method with the given input, deserializing to an object of the specified class. + * Invokes the fileListProjects method with the given input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -15063,16 +15063,16 @@ public static T globalWorkflowDescribe(String objectId, Class outputClass * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowDescribe(String objectId, Object inputObject, Class outputClass) { + public static T fileListProjects(String objectId, Object inputObject, Class outputClass) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest().request("/" + objectId + "/" + "describe", + new DXHTTPRequest().request("/" + objectId + "/" + "listProjects", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the globalWorkflowDescribe method with an empty input using the given environment, deserializing to an object of the specified class. + * Invokes the fileListProjects method with an empty input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -15087,13 +15087,13 @@ public static T globalWorkflowDescribe(String objectId, Object inputObject, * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowDescribe(String objectId, Class outputClass, DXEnvironment env) { - return globalWorkflowDescribe(objectId, mapper.createObjectNode(), outputClass, env); + public static T fileListProjects(String objectId, Class outputClass, DXEnvironment env) { + return fileListProjects(objectId, mapper.createObjectNode(), outputClass, env); } /** - * Invokes the globalWorkflowDescribe method with the given input using the given environment, deserializing to an object of the specified class. + * Invokes the fileListProjects method with the given input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -15109,17 +15109,17 @@ public static T globalWorkflowDescribe(String objectId, Class outputClass * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowDescribe(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { + public static T fileListProjects(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest(env).request("/" + objectId + "/" + "describe", + new DXHTTPRequest(env).request("/" + objectId + "/" + "listProjects", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the globalWorkflowDescribe method. + * Invokes the fileListProjects method. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @@ -15132,16 +15132,16 @@ public static T globalWorkflowDescribe(String objectId, Object inputObject, * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowDescribe(String, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileListProjects(String, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowDescribe(String objectId) { - return globalWorkflowDescribe(objectId, mapper.createObjectNode()); + public static JsonNode fileListProjects(String objectId) { + return fileListProjects(objectId, mapper.createObjectNode()); } /** - * Invokes the globalWorkflowDescribe method with the specified parameters. + * Invokes the fileListProjects method with the specified parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -15155,17 +15155,17 @@ public static JsonNode globalWorkflowDescribe(String objectId) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowDescribe(String, Object, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileListProjects(String, Object, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowDescribe(String objectId, JsonNode inputParams) { - return new DXHTTPRequest().request("/" + objectId + "/" + "describe", inputParams, + public static JsonNode fileListProjects(String objectId, JsonNode inputParams) { + return new DXHTTPRequest().request("/" + objectId + "/" + "listProjects", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the globalWorkflowDescribe method with the specified environment. + * Invokes the fileListProjects method with the specified environment. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param env environment object specifying the auth token and remote server and protocol @@ -15179,16 +15179,16 @@ public static JsonNode globalWorkflowDescribe(String objectId, JsonNode inputPar * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowDescribe(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileListProjects(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowDescribe(String objectId, DXEnvironment env) { - return globalWorkflowDescribe(objectId, mapper.createObjectNode(), env); + public static JsonNode fileListProjects(String objectId, DXEnvironment env) { + return fileListProjects(objectId, mapper.createObjectNode(), env); } /** - * Invokes the globalWorkflowDescribe method with the specified environment and parameters. + * Invokes the fileListProjects method with the specified environment and parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -15203,18 +15203,18 @@ public static JsonNode globalWorkflowDescribe(String objectId, DXEnvironment env * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowDescribe(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileListProjects(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowDescribe(String objectId, JsonNode inputParams, DXEnvironment env) { - return new DXHTTPRequest(env).request("/" + objectId + "/" + "describe", inputParams, + public static JsonNode fileListProjects(String objectId, JsonNode inputParams, DXEnvironment env) { + return new DXHTTPRequest(env).request("/" + objectId + "/" + "listProjects", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the globalWorkflowListAuthorizedUsers method with an empty input, deserializing to an object of the specified class. + * Invokes the fileRemoveTags method with an empty input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -15228,13 +15228,13 @@ public static JsonNode globalWorkflowDescribe(String objectId, JsonNode inputPar * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowListAuthorizedUsers(String objectId, Class outputClass) { - return globalWorkflowListAuthorizedUsers(objectId, mapper.createObjectNode(), outputClass); + public static T fileRemoveTags(String objectId, Class outputClass) { + return fileRemoveTags(objectId, mapper.createObjectNode(), outputClass); } /** - * Invokes the globalWorkflowListAuthorizedUsers method with the given input, deserializing to an object of the specified class. + * Invokes the fileRemoveTags method with the given input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -15249,16 +15249,16 @@ public static T globalWorkflowListAuthorizedUsers(String objectId, Class * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowListAuthorizedUsers(String objectId, Object inputObject, Class outputClass) { + public static T fileRemoveTags(String objectId, Object inputObject, Class outputClass) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest().request("/" + objectId + "/" + "listAuthorizedUsers", + new DXHTTPRequest().request("/" + objectId + "/" + "removeTags", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the globalWorkflowListAuthorizedUsers method with an empty input using the given environment, deserializing to an object of the specified class. + * Invokes the fileRemoveTags method with an empty input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -15273,13 +15273,13 @@ public static T globalWorkflowListAuthorizedUsers(String objectId, Object in * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowListAuthorizedUsers(String objectId, Class outputClass, DXEnvironment env) { - return globalWorkflowListAuthorizedUsers(objectId, mapper.createObjectNode(), outputClass, env); + public static T fileRemoveTags(String objectId, Class outputClass, DXEnvironment env) { + return fileRemoveTags(objectId, mapper.createObjectNode(), outputClass, env); } /** - * Invokes the globalWorkflowListAuthorizedUsers method with the given input using the given environment, deserializing to an object of the specified class. + * Invokes the fileRemoveTags method with the given input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -15295,17 +15295,17 @@ public static T globalWorkflowListAuthorizedUsers(String objectId, Class * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowListAuthorizedUsers(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { + public static T fileRemoveTags(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest(env).request("/" + objectId + "/" + "listAuthorizedUsers", + new DXHTTPRequest(env).request("/" + objectId + "/" + "removeTags", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the globalWorkflowListAuthorizedUsers method. + * Invokes the fileRemoveTags method. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @@ -15318,16 +15318,16 @@ public static T globalWorkflowListAuthorizedUsers(String objectId, Object in * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowListAuthorizedUsers(String, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileRemoveTags(String, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowListAuthorizedUsers(String objectId) { - return globalWorkflowListAuthorizedUsers(objectId, mapper.createObjectNode()); + public static JsonNode fileRemoveTags(String objectId) { + return fileRemoveTags(objectId, mapper.createObjectNode()); } /** - * Invokes the globalWorkflowListAuthorizedUsers method with the specified parameters. + * Invokes the fileRemoveTags method with the specified parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -15341,17 +15341,17 @@ public static JsonNode globalWorkflowListAuthorizedUsers(String objectId) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowListAuthorizedUsers(String, Object, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileRemoveTags(String, Object, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowListAuthorizedUsers(String objectId, JsonNode inputParams) { - return new DXHTTPRequest().request("/" + objectId + "/" + "listAuthorizedUsers", inputParams, + public static JsonNode fileRemoveTags(String objectId, JsonNode inputParams) { + return new DXHTTPRequest().request("/" + objectId + "/" + "removeTags", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the globalWorkflowListAuthorizedUsers method with the specified environment. + * Invokes the fileRemoveTags method with the specified environment. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param env environment object specifying the auth token and remote server and protocol @@ -15365,16 +15365,16 @@ public static JsonNode globalWorkflowListAuthorizedUsers(String objectId, JsonNo * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowListAuthorizedUsers(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileRemoveTags(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowListAuthorizedUsers(String objectId, DXEnvironment env) { - return globalWorkflowListAuthorizedUsers(objectId, mapper.createObjectNode(), env); + public static JsonNode fileRemoveTags(String objectId, DXEnvironment env) { + return fileRemoveTags(objectId, mapper.createObjectNode(), env); } /** - * Invokes the globalWorkflowListAuthorizedUsers method with the specified environment and parameters. + * Invokes the fileRemoveTags method with the specified environment and parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -15389,18 +15389,18 @@ public static JsonNode globalWorkflowListAuthorizedUsers(String objectId, DXEnvi * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowListAuthorizedUsers(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileRemoveTags(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowListAuthorizedUsers(String objectId, JsonNode inputParams, DXEnvironment env) { - return new DXHTTPRequest(env).request("/" + objectId + "/" + "listAuthorizedUsers", inputParams, + public static JsonNode fileRemoveTags(String objectId, JsonNode inputParams, DXEnvironment env) { + return new DXHTTPRequest(env).request("/" + objectId + "/" + "removeTags", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the globalWorkflowListCategories method with an empty input, deserializing to an object of the specified class. + * Invokes the fileRemoveTypes method with an empty input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -15414,13 +15414,13 @@ public static JsonNode globalWorkflowListAuthorizedUsers(String objectId, JsonNo * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowListCategories(String objectId, Class outputClass) { - return globalWorkflowListCategories(objectId, mapper.createObjectNode(), outputClass); + public static T fileRemoveTypes(String objectId, Class outputClass) { + return fileRemoveTypes(objectId, mapper.createObjectNode(), outputClass); } /** - * Invokes the globalWorkflowListCategories method with the given input, deserializing to an object of the specified class. + * Invokes the fileRemoveTypes method with the given input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -15435,16 +15435,16 @@ public static T globalWorkflowListCategories(String objectId, Class outpu * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowListCategories(String objectId, Object inputObject, Class outputClass) { + public static T fileRemoveTypes(String objectId, Object inputObject, Class outputClass) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest().request("/" + objectId + "/" + "listCategories", + new DXHTTPRequest().request("/" + objectId + "/" + "removeTypes", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the globalWorkflowListCategories method with an empty input using the given environment, deserializing to an object of the specified class. + * Invokes the fileRemoveTypes method with an empty input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -15459,13 +15459,13 @@ public static T globalWorkflowListCategories(String objectId, Object inputOb * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowListCategories(String objectId, Class outputClass, DXEnvironment env) { - return globalWorkflowListCategories(objectId, mapper.createObjectNode(), outputClass, env); + public static T fileRemoveTypes(String objectId, Class outputClass, DXEnvironment env) { + return fileRemoveTypes(objectId, mapper.createObjectNode(), outputClass, env); } /** - * Invokes the globalWorkflowListCategories method with the given input using the given environment, deserializing to an object of the specified class. + * Invokes the fileRemoveTypes method with the given input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -15481,17 +15481,17 @@ public static T globalWorkflowListCategories(String objectId, Class outpu * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowListCategories(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { + public static T fileRemoveTypes(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest(env).request("/" + objectId + "/" + "listCategories", + new DXHTTPRequest(env).request("/" + objectId + "/" + "removeTypes", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the globalWorkflowListCategories method. + * Invokes the fileRemoveTypes method. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @@ -15504,16 +15504,16 @@ public static T globalWorkflowListCategories(String objectId, Object inputOb * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowListCategories(String, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileRemoveTypes(String, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowListCategories(String objectId) { - return globalWorkflowListCategories(objectId, mapper.createObjectNode()); + public static JsonNode fileRemoveTypes(String objectId) { + return fileRemoveTypes(objectId, mapper.createObjectNode()); } /** - * Invokes the globalWorkflowListCategories method with the specified parameters. + * Invokes the fileRemoveTypes method with the specified parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -15527,17 +15527,17 @@ public static JsonNode globalWorkflowListCategories(String objectId) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowListCategories(String, Object, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileRemoveTypes(String, Object, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowListCategories(String objectId, JsonNode inputParams) { - return new DXHTTPRequest().request("/" + objectId + "/" + "listCategories", inputParams, + public static JsonNode fileRemoveTypes(String objectId, JsonNode inputParams) { + return new DXHTTPRequest().request("/" + objectId + "/" + "removeTypes", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the globalWorkflowListCategories method with the specified environment. + * Invokes the fileRemoveTypes method with the specified environment. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param env environment object specifying the auth token and remote server and protocol @@ -15551,16 +15551,16 @@ public static JsonNode globalWorkflowListCategories(String objectId, JsonNode in * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowListCategories(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileRemoveTypes(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowListCategories(String objectId, DXEnvironment env) { - return globalWorkflowListCategories(objectId, mapper.createObjectNode(), env); + public static JsonNode fileRemoveTypes(String objectId, DXEnvironment env) { + return fileRemoveTypes(objectId, mapper.createObjectNode(), env); } /** - * Invokes the globalWorkflowListCategories method with the specified environment and parameters. + * Invokes the fileRemoveTypes method with the specified environment and parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -15575,18 +15575,18 @@ public static JsonNode globalWorkflowListCategories(String objectId, DXEnvironme * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowListCategories(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileRemoveTypes(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowListCategories(String objectId, JsonNode inputParams, DXEnvironment env) { - return new DXHTTPRequest(env).request("/" + objectId + "/" + "listCategories", inputParams, + public static JsonNode fileRemoveTypes(String objectId, JsonNode inputParams, DXEnvironment env) { + return new DXHTTPRequest(env).request("/" + objectId + "/" + "removeTypes", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the globalWorkflowListDevelopers method with an empty input, deserializing to an object of the specified class. + * Invokes the fileRename method with an empty input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -15600,13 +15600,13 @@ public static JsonNode globalWorkflowListCategories(String objectId, JsonNode in * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowListDevelopers(String objectId, Class outputClass) { - return globalWorkflowListDevelopers(objectId, mapper.createObjectNode(), outputClass); + public static T fileRename(String objectId, Class outputClass) { + return fileRename(objectId, mapper.createObjectNode(), outputClass); } /** - * Invokes the globalWorkflowListDevelopers method with the given input, deserializing to an object of the specified class. + * Invokes the fileRename method with the given input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -15621,16 +15621,16 @@ public static T globalWorkflowListDevelopers(String objectId, Class outpu * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowListDevelopers(String objectId, Object inputObject, Class outputClass) { + public static T fileRename(String objectId, Object inputObject, Class outputClass) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest().request("/" + objectId + "/" + "listDevelopers", + new DXHTTPRequest().request("/" + objectId + "/" + "rename", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the globalWorkflowListDevelopers method with an empty input using the given environment, deserializing to an object of the specified class. + * Invokes the fileRename method with an empty input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -15645,13 +15645,13 @@ public static T globalWorkflowListDevelopers(String objectId, Object inputOb * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowListDevelopers(String objectId, Class outputClass, DXEnvironment env) { - return globalWorkflowListDevelopers(objectId, mapper.createObjectNode(), outputClass, env); + public static T fileRename(String objectId, Class outputClass, DXEnvironment env) { + return fileRename(objectId, mapper.createObjectNode(), outputClass, env); } /** - * Invokes the globalWorkflowListDevelopers method with the given input using the given environment, deserializing to an object of the specified class. + * Invokes the fileRename method with the given input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -15667,17 +15667,17 @@ public static T globalWorkflowListDevelopers(String objectId, Class outpu * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowListDevelopers(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { + public static T fileRename(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest(env).request("/" + objectId + "/" + "listDevelopers", + new DXHTTPRequest(env).request("/" + objectId + "/" + "rename", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the globalWorkflowListDevelopers method. + * Invokes the fileRename method. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @@ -15690,16 +15690,16 @@ public static T globalWorkflowListDevelopers(String objectId, Object inputOb * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowListDevelopers(String, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileRename(String, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowListDevelopers(String objectId) { - return globalWorkflowListDevelopers(objectId, mapper.createObjectNode()); + public static JsonNode fileRename(String objectId) { + return fileRename(objectId, mapper.createObjectNode()); } /** - * Invokes the globalWorkflowListDevelopers method with the specified parameters. + * Invokes the fileRename method with the specified parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -15713,17 +15713,17 @@ public static JsonNode globalWorkflowListDevelopers(String objectId) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowListDevelopers(String, Object, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileRename(String, Object, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowListDevelopers(String objectId, JsonNode inputParams) { - return new DXHTTPRequest().request("/" + objectId + "/" + "listDevelopers", inputParams, + public static JsonNode fileRename(String objectId, JsonNode inputParams) { + return new DXHTTPRequest().request("/" + objectId + "/" + "rename", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the globalWorkflowListDevelopers method with the specified environment. + * Invokes the fileRename method with the specified environment. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param env environment object specifying the auth token and remote server and protocol @@ -15737,16 +15737,16 @@ public static JsonNode globalWorkflowListDevelopers(String objectId, JsonNode in * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowListDevelopers(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileRename(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowListDevelopers(String objectId, DXEnvironment env) { - return globalWorkflowListDevelopers(objectId, mapper.createObjectNode(), env); + public static JsonNode fileRename(String objectId, DXEnvironment env) { + return fileRename(objectId, mapper.createObjectNode(), env); } /** - * Invokes the globalWorkflowListDevelopers method with the specified environment and parameters. + * Invokes the fileRename method with the specified environment and parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -15761,18 +15761,18 @@ public static JsonNode globalWorkflowListDevelopers(String objectId, DXEnvironme * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowListDevelopers(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileRename(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowListDevelopers(String objectId, JsonNode inputParams, DXEnvironment env) { - return new DXHTTPRequest(env).request("/" + objectId + "/" + "listDevelopers", inputParams, + public static JsonNode fileRename(String objectId, JsonNode inputParams, DXEnvironment env) { + return new DXHTTPRequest(env).request("/" + objectId + "/" + "rename", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the globalWorkflowPublish method with an empty input, deserializing to an object of the specified class. + * Invokes the fileSetDetails method with an empty input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -15786,13 +15786,13 @@ public static JsonNode globalWorkflowListDevelopers(String objectId, JsonNode in * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowPublish(String objectId, Class outputClass) { - return globalWorkflowPublish(objectId, mapper.createObjectNode(), outputClass); + public static T fileSetDetails(String objectId, Class outputClass) { + return fileSetDetails(objectId, mapper.createObjectNode(), outputClass); } /** - * Invokes the globalWorkflowPublish method with the given input, deserializing to an object of the specified class. + * Invokes the fileSetDetails method with the given input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -15807,16 +15807,16 @@ public static T globalWorkflowPublish(String objectId, Class outputClass) * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowPublish(String objectId, Object inputObject, Class outputClass) { + public static T fileSetDetails(String objectId, Object inputObject, Class outputClass) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest().request("/" + objectId + "/" + "publish", + new DXHTTPRequest().request("/" + objectId + "/" + "setDetails", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the globalWorkflowPublish method with an empty input using the given environment, deserializing to an object of the specified class. + * Invokes the fileSetDetails method with an empty input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -15831,13 +15831,13 @@ public static T globalWorkflowPublish(String objectId, Object inputObject, C * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowPublish(String objectId, Class outputClass, DXEnvironment env) { - return globalWorkflowPublish(objectId, mapper.createObjectNode(), outputClass, env); + public static T fileSetDetails(String objectId, Class outputClass, DXEnvironment env) { + return fileSetDetails(objectId, mapper.createObjectNode(), outputClass, env); } /** - * Invokes the globalWorkflowPublish method with the given input using the given environment, deserializing to an object of the specified class. + * Invokes the fileSetDetails method with the given input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -15853,17 +15853,17 @@ public static T globalWorkflowPublish(String objectId, Class outputClass, * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowPublish(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { + public static T fileSetDetails(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest(env).request("/" + objectId + "/" + "publish", + new DXHTTPRequest(env).request("/" + objectId + "/" + "setDetails", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the globalWorkflowPublish method. + * Invokes the fileSetDetails method. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @@ -15876,16 +15876,16 @@ public static T globalWorkflowPublish(String objectId, Object inputObject, C * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowPublish(String, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileSetDetails(String, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowPublish(String objectId) { - return globalWorkflowPublish(objectId, mapper.createObjectNode()); + public static JsonNode fileSetDetails(String objectId) { + return fileSetDetails(objectId, mapper.createObjectNode()); } /** - * Invokes the globalWorkflowPublish method with the specified parameters. + * Invokes the fileSetDetails method with the specified parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -15899,17 +15899,17 @@ public static JsonNode globalWorkflowPublish(String objectId) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowPublish(String, Object, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileSetDetails(String, Object, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowPublish(String objectId, JsonNode inputParams) { - return new DXHTTPRequest().request("/" + objectId + "/" + "publish", inputParams, + public static JsonNode fileSetDetails(String objectId, JsonNode inputParams) { + return new DXHTTPRequest().request("/" + objectId + "/" + "setDetails", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the globalWorkflowPublish method with the specified environment. + * Invokes the fileSetDetails method with the specified environment. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param env environment object specifying the auth token and remote server and protocol @@ -15923,16 +15923,16 @@ public static JsonNode globalWorkflowPublish(String objectId, JsonNode inputPara * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowPublish(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileSetDetails(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowPublish(String objectId, DXEnvironment env) { - return globalWorkflowPublish(objectId, mapper.createObjectNode(), env); + public static JsonNode fileSetDetails(String objectId, DXEnvironment env) { + return fileSetDetails(objectId, mapper.createObjectNode(), env); } /** - * Invokes the globalWorkflowPublish method with the specified environment and parameters. + * Invokes the fileSetDetails method with the specified environment and parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -15947,18 +15947,18 @@ public static JsonNode globalWorkflowPublish(String objectId, DXEnvironment env) * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowPublish(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileSetDetails(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowPublish(String objectId, JsonNode inputParams, DXEnvironment env) { - return new DXHTTPRequest(env).request("/" + objectId + "/" + "publish", inputParams, + public static JsonNode fileSetDetails(String objectId, JsonNode inputParams, DXEnvironment env) { + return new DXHTTPRequest(env).request("/" + objectId + "/" + "setDetails", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the globalWorkflowRemoveAuthorizedUsers method with an empty input, deserializing to an object of the specified class. + * Invokes the fileSetProperties method with an empty input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -15972,13 +15972,13 @@ public static JsonNode globalWorkflowPublish(String objectId, JsonNode inputPara * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowRemoveAuthorizedUsers(String objectId, Class outputClass) { - return globalWorkflowRemoveAuthorizedUsers(objectId, mapper.createObjectNode(), outputClass); + public static T fileSetProperties(String objectId, Class outputClass) { + return fileSetProperties(objectId, mapper.createObjectNode(), outputClass); } /** - * Invokes the globalWorkflowRemoveAuthorizedUsers method with the given input, deserializing to an object of the specified class. + * Invokes the fileSetProperties method with the given input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -15993,16 +15993,16 @@ public static T globalWorkflowRemoveAuthorizedUsers(String objectId, Class T globalWorkflowRemoveAuthorizedUsers(String objectId, Object inputObject, Class outputClass) { + public static T fileSetProperties(String objectId, Object inputObject, Class outputClass) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest().request("/" + objectId + "/" + "removeAuthorizedUsers", + new DXHTTPRequest().request("/" + objectId + "/" + "setProperties", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the globalWorkflowRemoveAuthorizedUsers method with an empty input using the given environment, deserializing to an object of the specified class. + * Invokes the fileSetProperties method with an empty input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -16017,13 +16017,13 @@ public static T globalWorkflowRemoveAuthorizedUsers(String objectId, Object * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowRemoveAuthorizedUsers(String objectId, Class outputClass, DXEnvironment env) { - return globalWorkflowRemoveAuthorizedUsers(objectId, mapper.createObjectNode(), outputClass, env); + public static T fileSetProperties(String objectId, Class outputClass, DXEnvironment env) { + return fileSetProperties(objectId, mapper.createObjectNode(), outputClass, env); } /** - * Invokes the globalWorkflowRemoveAuthorizedUsers method with the given input using the given environment, deserializing to an object of the specified class. + * Invokes the fileSetProperties method with the given input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -16039,17 +16039,17 @@ public static T globalWorkflowRemoveAuthorizedUsers(String objectId, Class T globalWorkflowRemoveAuthorizedUsers(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { + public static T fileSetProperties(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest(env).request("/" + objectId + "/" + "removeAuthorizedUsers", + new DXHTTPRequest(env).request("/" + objectId + "/" + "setProperties", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the globalWorkflowRemoveAuthorizedUsers method. + * Invokes the fileSetProperties method. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @@ -16062,16 +16062,16 @@ public static T globalWorkflowRemoveAuthorizedUsers(String objectId, Object * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowRemoveAuthorizedUsers(String, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileSetProperties(String, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowRemoveAuthorizedUsers(String objectId) { - return globalWorkflowRemoveAuthorizedUsers(objectId, mapper.createObjectNode()); + public static JsonNode fileSetProperties(String objectId) { + return fileSetProperties(objectId, mapper.createObjectNode()); } /** - * Invokes the globalWorkflowRemoveAuthorizedUsers method with the specified parameters. + * Invokes the fileSetProperties method with the specified parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -16085,17 +16085,17 @@ public static JsonNode globalWorkflowRemoveAuthorizedUsers(String objectId) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowRemoveAuthorizedUsers(String, Object, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileSetProperties(String, Object, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowRemoveAuthorizedUsers(String objectId, JsonNode inputParams) { - return new DXHTTPRequest().request("/" + objectId + "/" + "removeAuthorizedUsers", inputParams, + public static JsonNode fileSetProperties(String objectId, JsonNode inputParams) { + return new DXHTTPRequest().request("/" + objectId + "/" + "setProperties", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the globalWorkflowRemoveAuthorizedUsers method with the specified environment. + * Invokes the fileSetProperties method with the specified environment. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param env environment object specifying the auth token and remote server and protocol @@ -16109,16 +16109,16 @@ public static JsonNode globalWorkflowRemoveAuthorizedUsers(String objectId, Json * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowRemoveAuthorizedUsers(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileSetProperties(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowRemoveAuthorizedUsers(String objectId, DXEnvironment env) { - return globalWorkflowRemoveAuthorizedUsers(objectId, mapper.createObjectNode(), env); + public static JsonNode fileSetProperties(String objectId, DXEnvironment env) { + return fileSetProperties(objectId, mapper.createObjectNode(), env); } /** - * Invokes the globalWorkflowRemoveAuthorizedUsers method with the specified environment and parameters. + * Invokes the fileSetProperties method with the specified environment and parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -16133,18 +16133,18 @@ public static JsonNode globalWorkflowRemoveAuthorizedUsers(String objectId, DXEn * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowRemoveAuthorizedUsers(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileSetProperties(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowRemoveAuthorizedUsers(String objectId, JsonNode inputParams, DXEnvironment env) { - return new DXHTTPRequest(env).request("/" + objectId + "/" + "removeAuthorizedUsers", inputParams, + public static JsonNode fileSetProperties(String objectId, JsonNode inputParams, DXEnvironment env) { + return new DXHTTPRequest(env).request("/" + objectId + "/" + "setProperties", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the globalWorkflowRemoveCategories method with an empty input, deserializing to an object of the specified class. + * Invokes the fileSetVisibility method with an empty input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -16158,13 +16158,13 @@ public static JsonNode globalWorkflowRemoveAuthorizedUsers(String objectId, Json * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowRemoveCategories(String objectId, Class outputClass) { - return globalWorkflowRemoveCategories(objectId, mapper.createObjectNode(), outputClass); + public static T fileSetVisibility(String objectId, Class outputClass) { + return fileSetVisibility(objectId, mapper.createObjectNode(), outputClass); } /** - * Invokes the globalWorkflowRemoveCategories method with the given input, deserializing to an object of the specified class. + * Invokes the fileSetVisibility method with the given input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -16179,16 +16179,16 @@ public static T globalWorkflowRemoveCategories(String objectId, Class out * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowRemoveCategories(String objectId, Object inputObject, Class outputClass) { + public static T fileSetVisibility(String objectId, Object inputObject, Class outputClass) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest().request("/" + objectId + "/" + "removeCategories", + new DXHTTPRequest().request("/" + objectId + "/" + "setVisibility", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the globalWorkflowRemoveCategories method with an empty input using the given environment, deserializing to an object of the specified class. + * Invokes the fileSetVisibility method with an empty input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -16203,13 +16203,13 @@ public static T globalWorkflowRemoveCategories(String objectId, Object input * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowRemoveCategories(String objectId, Class outputClass, DXEnvironment env) { - return globalWorkflowRemoveCategories(objectId, mapper.createObjectNode(), outputClass, env); + public static T fileSetVisibility(String objectId, Class outputClass, DXEnvironment env) { + return fileSetVisibility(objectId, mapper.createObjectNode(), outputClass, env); } /** - * Invokes the globalWorkflowRemoveCategories method with the given input using the given environment, deserializing to an object of the specified class. + * Invokes the fileSetVisibility method with the given input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -16225,17 +16225,17 @@ public static T globalWorkflowRemoveCategories(String objectId, Class out * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowRemoveCategories(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { + public static T fileSetVisibility(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest(env).request("/" + objectId + "/" + "removeCategories", + new DXHTTPRequest(env).request("/" + objectId + "/" + "setVisibility", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the globalWorkflowRemoveCategories method. + * Invokes the fileSetVisibility method. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @@ -16248,16 +16248,16 @@ public static T globalWorkflowRemoveCategories(String objectId, Object input * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowRemoveCategories(String, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileSetVisibility(String, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowRemoveCategories(String objectId) { - return globalWorkflowRemoveCategories(objectId, mapper.createObjectNode()); + public static JsonNode fileSetVisibility(String objectId) { + return fileSetVisibility(objectId, mapper.createObjectNode()); } /** - * Invokes the globalWorkflowRemoveCategories method with the specified parameters. + * Invokes the fileSetVisibility method with the specified parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -16271,17 +16271,17 @@ public static JsonNode globalWorkflowRemoveCategories(String objectId) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowRemoveCategories(String, Object, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileSetVisibility(String, Object, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowRemoveCategories(String objectId, JsonNode inputParams) { - return new DXHTTPRequest().request("/" + objectId + "/" + "removeCategories", inputParams, + public static JsonNode fileSetVisibility(String objectId, JsonNode inputParams) { + return new DXHTTPRequest().request("/" + objectId + "/" + "setVisibility", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the globalWorkflowRemoveCategories method with the specified environment. + * Invokes the fileSetVisibility method with the specified environment. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param env environment object specifying the auth token and remote server and protocol @@ -16295,16 +16295,16 @@ public static JsonNode globalWorkflowRemoveCategories(String objectId, JsonNode * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowRemoveCategories(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileSetVisibility(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowRemoveCategories(String objectId, DXEnvironment env) { - return globalWorkflowRemoveCategories(objectId, mapper.createObjectNode(), env); + public static JsonNode fileSetVisibility(String objectId, DXEnvironment env) { + return fileSetVisibility(objectId, mapper.createObjectNode(), env); } /** - * Invokes the globalWorkflowRemoveCategories method with the specified environment and parameters. + * Invokes the fileSetVisibility method with the specified environment and parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -16319,18 +16319,18 @@ public static JsonNode globalWorkflowRemoveCategories(String objectId, DXEnviron * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowRemoveCategories(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileSetVisibility(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowRemoveCategories(String objectId, JsonNode inputParams, DXEnvironment env) { - return new DXHTTPRequest(env).request("/" + objectId + "/" + "removeCategories", inputParams, + public static JsonNode fileSetVisibility(String objectId, JsonNode inputParams, DXEnvironment env) { + return new DXHTTPRequest(env).request("/" + objectId + "/" + "setVisibility", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the globalWorkflowRemoveDevelopers method with an empty input, deserializing to an object of the specified class. + * Invokes the fileUpload method with an empty input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -16344,13 +16344,13 @@ public static JsonNode globalWorkflowRemoveCategories(String objectId, JsonNode * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowRemoveDevelopers(String objectId, Class outputClass) { - return globalWorkflowRemoveDevelopers(objectId, mapper.createObjectNode(), outputClass); + public static T fileUpload(String objectId, Class outputClass) { + return fileUpload(objectId, mapper.createObjectNode(), outputClass); } /** - * Invokes the globalWorkflowRemoveDevelopers method with the given input, deserializing to an object of the specified class. + * Invokes the fileUpload method with the given input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -16365,16 +16365,16 @@ public static T globalWorkflowRemoveDevelopers(String objectId, Class out * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowRemoveDevelopers(String objectId, Object inputObject, Class outputClass) { + public static T fileUpload(String objectId, Object inputObject, Class outputClass) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest().request("/" + objectId + "/" + "removeDevelopers", + new DXHTTPRequest().request("/" + objectId + "/" + "upload", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the globalWorkflowRemoveDevelopers method with an empty input using the given environment, deserializing to an object of the specified class. + * Invokes the fileUpload method with an empty input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -16389,13 +16389,13 @@ public static T globalWorkflowRemoveDevelopers(String objectId, Object input * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowRemoveDevelopers(String objectId, Class outputClass, DXEnvironment env) { - return globalWorkflowRemoveDevelopers(objectId, mapper.createObjectNode(), outputClass, env); + public static T fileUpload(String objectId, Class outputClass, DXEnvironment env) { + return fileUpload(objectId, mapper.createObjectNode(), outputClass, env); } /** - * Invokes the globalWorkflowRemoveDevelopers method with the given input using the given environment, deserializing to an object of the specified class. + * Invokes the fileUpload method with the given input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -16411,17 +16411,17 @@ public static T globalWorkflowRemoveDevelopers(String objectId, Class out * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowRemoveDevelopers(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { + public static T fileUpload(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest(env).request("/" + objectId + "/" + "removeDevelopers", + new DXHTTPRequest(env).request("/" + objectId + "/" + "upload", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the globalWorkflowRemoveDevelopers method. + * Invokes the fileUpload method. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @@ -16434,16 +16434,16 @@ public static T globalWorkflowRemoveDevelopers(String objectId, Object input * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowRemoveDevelopers(String, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileUpload(String, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowRemoveDevelopers(String objectId) { - return globalWorkflowRemoveDevelopers(objectId, mapper.createObjectNode()); + public static JsonNode fileUpload(String objectId) { + return fileUpload(objectId, mapper.createObjectNode()); } /** - * Invokes the globalWorkflowRemoveDevelopers method with the specified parameters. + * Invokes the fileUpload method with the specified parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -16457,17 +16457,17 @@ public static JsonNode globalWorkflowRemoveDevelopers(String objectId) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowRemoveDevelopers(String, Object, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileUpload(String, Object, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowRemoveDevelopers(String objectId, JsonNode inputParams) { - return new DXHTTPRequest().request("/" + objectId + "/" + "removeDevelopers", inputParams, + public static JsonNode fileUpload(String objectId, JsonNode inputParams) { + return new DXHTTPRequest().request("/" + objectId + "/" + "upload", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the globalWorkflowRemoveDevelopers method with the specified environment. + * Invokes the fileUpload method with the specified environment. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param env environment object specifying the auth token and remote server and protocol @@ -16481,16 +16481,16 @@ public static JsonNode globalWorkflowRemoveDevelopers(String objectId, JsonNode * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowRemoveDevelopers(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileUpload(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowRemoveDevelopers(String objectId, DXEnvironment env) { - return globalWorkflowRemoveDevelopers(objectId, mapper.createObjectNode(), env); + public static JsonNode fileUpload(String objectId, DXEnvironment env) { + return fileUpload(objectId, mapper.createObjectNode(), env); } /** - * Invokes the globalWorkflowRemoveDevelopers method with the specified environment and parameters. + * Invokes the fileUpload method with the specified environment and parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -16505,23 +16505,22 @@ public static JsonNode globalWorkflowRemoveDevelopers(String objectId, DXEnviron * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowRemoveDevelopers(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileUpload(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowRemoveDevelopers(String objectId, JsonNode inputParams, DXEnvironment env) { - return new DXHTTPRequest(env).request("/" + objectId + "/" + "removeDevelopers", inputParams, + public static JsonNode fileUpload(String objectId, JsonNode inputParams, DXEnvironment env) { + return new DXHTTPRequest(env).request("/" + objectId + "/" + "upload", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the globalWorkflowRemoveTags method with an empty input, deserializing to an object of the specified class. + * Invokes the fileNew method with an empty input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * - * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to * - * @return Response object + * @return Server response parsed from JSON * * @throws DXAPIException * If the server returns a complete response with an HTTP status @@ -16530,19 +16529,18 @@ public static JsonNode globalWorkflowRemoveDevelopers(String objectId, JsonNode * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowRemoveTags(String objectId, Class outputClass) { - return globalWorkflowRemoveTags(objectId, mapper.createObjectNode(), outputClass); + public static T fileNew(Class outputClass) { + return fileNew(mapper.createObjectNode(), outputClass); } /** - * Invokes the globalWorkflowRemoveTags method with the given input, deserializing to an object of the specified class. + * Invokes the fileNew method with an empty input using the specified environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * - * @param objectId ID of the object to operate on - * @param inputObject input object (to be JSON serialized to an input hash) * @param outputClass class to deserialize the server reponse to + * @param env environment object specifying the auth token and remote server and protocol * - * @return Response object + * @return Server response parsed from JSON * * @throws DXAPIException * If the server returns a complete response with an HTTP status @@ -16551,22 +16549,18 @@ public static T globalWorkflowRemoveTags(String objectId, Class outputCla * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowRemoveTags(String objectId, Object inputObject, Class outputClass) { - JsonNode input = mapper.valueToTree(inputObject); - return DXJSON.safeTreeToValue( - new DXHTTPRequest().request("/" + objectId + "/" + "removeTags", - input, RetryStrategy.SAFE_TO_RETRY), outputClass); + public static T fileNew(Class outputClass, DXEnvironment env) { + return fileNew(mapper.createObjectNode(), outputClass, env); } /** - * Invokes the globalWorkflowRemoveTags method with an empty input using the given environment, deserializing to an object of the specified class. + * Invokes the fileNew method with the specified input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * - * @param objectId ID of the object to operate on + * @param inputObject input object (to be JSON serialized to an input hash) * @param outputClass class to deserialize the server reponse to - * @param env environment object specifying the auth token and remote server and protocol * - * @return Response object + * @return Server response parsed from JSON * * @throws DXAPIException * If the server returns a complete response with an HTTP status @@ -16575,20 +16569,22 @@ public static T globalWorkflowRemoveTags(String objectId, Object inputObject * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowRemoveTags(String objectId, Class outputClass, DXEnvironment env) { - return globalWorkflowRemoveTags(objectId, mapper.createObjectNode(), outputClass, env); + public static T fileNew(Object inputObject, Class outputClass) { + JsonNode input = Nonce.updateNonce(mapper.valueToTree(inputObject)); + return DXJSON.safeTreeToValue( + new DXHTTPRequest().request("/file/new", input, RetryStrategy.SAFE_TO_RETRY), + outputClass); } /** - * Invokes the globalWorkflowRemoveTags method with the given input using the given environment, deserializing to an object of the specified class. + * Invokes the fileNew method with the specified input using the specified environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * - * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) * @param outputClass class to deserialize the server reponse to * @param env environment object specifying the auth token and remote server and protocol * - * @return Response object + * @return Server response parsed from JSON * * @throws DXAPIException * If the server returns a complete response with an HTTP status @@ -16597,19 +16593,17 @@ public static T globalWorkflowRemoveTags(String objectId, Class outputCla * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowRemoveTags(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { - JsonNode input = mapper.valueToTree(inputObject); + public static T fileNew(Object inputObject, Class outputClass, DXEnvironment env) { + JsonNode input = Nonce.updateNonce(mapper.valueToTree(inputObject)); return DXJSON.safeTreeToValue( - new DXHTTPRequest(env).request("/" + objectId + "/" + "removeTags", - input, RetryStrategy.SAFE_TO_RETRY), outputClass); + new DXHTTPRequest(env).request("/file/new", input, RetryStrategy.SAFE_TO_RETRY), + outputClass); } /** - * Invokes the globalWorkflowRemoveTags method. - * - *

For more information about this method, see the API specification. + * Invokes the fileNew method. * - * @param objectId ID of the object to operate on + *

For more information about this method, see the API specification. * * @return Server response parsed from JSON * @@ -16620,18 +16614,17 @@ public static T globalWorkflowRemoveTags(String objectId, Object inputObject * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowRemoveTags(String, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileNew(Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowRemoveTags(String objectId) { - return globalWorkflowRemoveTags(objectId, mapper.createObjectNode()); + public static JsonNode fileNew() { + return fileNew(mapper.createObjectNode()); } /** - * Invokes the globalWorkflowRemoveTags method with the specified parameters. + * Invokes the fileNew method with the specified input parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * - * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call * * @return Server response parsed from JSON @@ -16643,19 +16636,17 @@ public static JsonNode globalWorkflowRemoveTags(String objectId) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowRemoveTags(String, Object, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileNew(Object, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowRemoveTags(String objectId, JsonNode inputParams) { - return new DXHTTPRequest().request("/" + objectId + "/" + "removeTags", inputParams, - RetryStrategy.SAFE_TO_RETRY); + public static JsonNode fileNew(JsonNode inputParams) { + return new DXHTTPRequest().request("/file/new", inputParams, RetryStrategy.UNSAFE_TO_RETRY); } /** - * Invokes the globalWorkflowRemoveTags method with the specified environment. + * Invokes the fileNew method with the specified environment. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * - * @param objectId ID of the object to operate on * @param env environment object specifying the auth token and remote server and protocol * * @return Server response parsed from JSON @@ -16667,18 +16658,17 @@ public static JsonNode globalWorkflowRemoveTags(String objectId, JsonNode inputP * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowRemoveTags(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileNew(Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowRemoveTags(String objectId, DXEnvironment env) { - return globalWorkflowRemoveTags(objectId, mapper.createObjectNode(), env); + public static JsonNode fileNew(DXEnvironment env) { + return fileNew(mapper.createObjectNode(), env); } /** - * Invokes the globalWorkflowRemoveTags method with the specified environment and parameters. + * Invokes the fileNew method with the specified environment and input parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * - * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call * @param env environment object specifying the auth token and remote server and protocol * @@ -16691,18 +16681,17 @@ public static JsonNode globalWorkflowRemoveTags(String objectId, DXEnvironment e * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowRemoveTags(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #fileNew(Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowRemoveTags(String objectId, JsonNode inputParams, DXEnvironment env) { - return new DXHTTPRequest(env).request("/" + objectId + "/" + "removeTags", inputParams, - RetryStrategy.SAFE_TO_RETRY); + public static JsonNode fileNew(JsonNode inputParams, DXEnvironment env) { + return new DXHTTPRequest(env).request("/file/new", inputParams, RetryStrategy.UNSAFE_TO_RETRY); } /** - * Invokes the globalWorkflowRun method with an empty input, deserializing to an object of the specified class. + * Invokes the globalWorkflowAddAuthorizedUsers method with an empty input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -16716,13 +16705,13 @@ public static JsonNode globalWorkflowRemoveTags(String objectId, JsonNode inputP * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowRun(String objectId, Class outputClass) { - return globalWorkflowRun(objectId, mapper.createObjectNode(), outputClass); + public static T globalWorkflowAddAuthorizedUsers(String objectId, Class outputClass) { + return globalWorkflowAddAuthorizedUsers(objectId, mapper.createObjectNode(), outputClass); } /** - * Invokes the globalWorkflowRun method with the given input, deserializing to an object of the specified class. + * Invokes the globalWorkflowAddAuthorizedUsers method with the given input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -16737,16 +16726,16 @@ public static T globalWorkflowRun(String objectId, Class outputClass) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowRun(String objectId, Object inputObject, Class outputClass) { - JsonNode input = Nonce.updateNonce(mapper.valueToTree(inputObject)); + public static T globalWorkflowAddAuthorizedUsers(String objectId, Object inputObject, Class outputClass) { + JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest().request("/" + objectId + "/" + "run", + new DXHTTPRequest().request("/" + objectId + "/" + "addAuthorizedUsers", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the globalWorkflowRun method with an empty input using the given environment, deserializing to an object of the specified class. + * Invokes the globalWorkflowAddAuthorizedUsers method with an empty input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -16761,13 +16750,13 @@ public static T globalWorkflowRun(String objectId, Object inputObject, Class * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowRun(String objectId, Class outputClass, DXEnvironment env) { - return globalWorkflowRun(objectId, mapper.createObjectNode(), outputClass, env); + public static T globalWorkflowAddAuthorizedUsers(String objectId, Class outputClass, DXEnvironment env) { + return globalWorkflowAddAuthorizedUsers(objectId, mapper.createObjectNode(), outputClass, env); } /** - * Invokes the globalWorkflowRun method with the given input using the given environment, deserializing to an object of the specified class. + * Invokes the globalWorkflowAddAuthorizedUsers method with the given input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -16783,17 +16772,17 @@ public static T globalWorkflowRun(String objectId, Class outputClass, DXE * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowRun(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { - JsonNode input = Nonce.updateNonce(mapper.valueToTree(inputObject)); + public static T globalWorkflowAddAuthorizedUsers(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { + JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest(env).request("/" + objectId + "/" + "run", + new DXHTTPRequest(env).request("/" + objectId + "/" + "addAuthorizedUsers", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the globalWorkflowRun method. + * Invokes the globalWorkflowAddAuthorizedUsers method. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @@ -16806,16 +16795,16 @@ public static T globalWorkflowRun(String objectId, Object inputObject, Class * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowRun(String, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #globalWorkflowAddAuthorizedUsers(String, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowRun(String objectId) { - return globalWorkflowRun(objectId, mapper.createObjectNode()); + public static JsonNode globalWorkflowAddAuthorizedUsers(String objectId) { + return globalWorkflowAddAuthorizedUsers(objectId, mapper.createObjectNode()); } /** - * Invokes the globalWorkflowRun method with the specified parameters. + * Invokes the globalWorkflowAddAuthorizedUsers method with the specified parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -16829,17 +16818,17 @@ public static JsonNode globalWorkflowRun(String objectId) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowRun(String, Object, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #globalWorkflowAddAuthorizedUsers(String, Object, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowRun(String objectId, JsonNode inputParams) { - return new DXHTTPRequest().request("/" + objectId + "/" + "run", inputParams, - RetryStrategy.UNSAFE_TO_RETRY); + public static JsonNode globalWorkflowAddAuthorizedUsers(String objectId, JsonNode inputParams) { + return new DXHTTPRequest().request("/" + objectId + "/" + "addAuthorizedUsers", inputParams, + RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the globalWorkflowRun method with the specified environment. + * Invokes the globalWorkflowAddAuthorizedUsers method with the specified environment. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param env environment object specifying the auth token and remote server and protocol @@ -16853,16 +16842,16 @@ public static JsonNode globalWorkflowRun(String objectId, JsonNode inputParams) * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowRun(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #globalWorkflowAddAuthorizedUsers(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowRun(String objectId, DXEnvironment env) { - return globalWorkflowRun(objectId, mapper.createObjectNode(), env); + public static JsonNode globalWorkflowAddAuthorizedUsers(String objectId, DXEnvironment env) { + return globalWorkflowAddAuthorizedUsers(objectId, mapper.createObjectNode(), env); } /** - * Invokes the globalWorkflowRun method with the specified environment and parameters. + * Invokes the globalWorkflowAddAuthorizedUsers method with the specified environment and parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -16877,18 +16866,18 @@ public static JsonNode globalWorkflowRun(String objectId, DXEnvironment env) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowRun(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #globalWorkflowAddAuthorizedUsers(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowRun(String objectId, JsonNode inputParams, DXEnvironment env) { - return new DXHTTPRequest(env).request("/" + objectId + "/" + "run", inputParams, - RetryStrategy.UNSAFE_TO_RETRY); + public static JsonNode globalWorkflowAddAuthorizedUsers(String objectId, JsonNode inputParams, DXEnvironment env) { + return new DXHTTPRequest(env).request("/" + objectId + "/" + "addAuthorizedUsers", inputParams, + RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the globalWorkflowUpdate method with an empty input, deserializing to an object of the specified class. + * Invokes the globalWorkflowAddCategories method with an empty input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -16902,13 +16891,13 @@ public static JsonNode globalWorkflowRun(String objectId, JsonNode inputParams, * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowUpdate(String objectId, Class outputClass) { - return globalWorkflowUpdate(objectId, mapper.createObjectNode(), outputClass); + public static T globalWorkflowAddCategories(String objectId, Class outputClass) { + return globalWorkflowAddCategories(objectId, mapper.createObjectNode(), outputClass); } /** - * Invokes the globalWorkflowUpdate method with the given input, deserializing to an object of the specified class. + * Invokes the globalWorkflowAddCategories method with the given input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -16923,16 +16912,16 @@ public static T globalWorkflowUpdate(String objectId, Class outputClass) * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowUpdate(String objectId, Object inputObject, Class outputClass) { + public static T globalWorkflowAddCategories(String objectId, Object inputObject, Class outputClass) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest().request("/" + objectId + "/" + "update", + new DXHTTPRequest().request("/" + objectId + "/" + "addCategories", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the globalWorkflowUpdate method with an empty input using the given environment, deserializing to an object of the specified class. + * Invokes the globalWorkflowAddCategories method with an empty input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -16947,13 +16936,13 @@ public static T globalWorkflowUpdate(String objectId, Object inputObject, Cl * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowUpdate(String objectId, Class outputClass, DXEnvironment env) { - return globalWorkflowUpdate(objectId, mapper.createObjectNode(), outputClass, env); + public static T globalWorkflowAddCategories(String objectId, Class outputClass, DXEnvironment env) { + return globalWorkflowAddCategories(objectId, mapper.createObjectNode(), outputClass, env); } /** - * Invokes the globalWorkflowUpdate method with the given input using the given environment, deserializing to an object of the specified class. + * Invokes the globalWorkflowAddCategories method with the given input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -16969,17 +16958,17 @@ public static T globalWorkflowUpdate(String objectId, Class outputClass, * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowUpdate(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { + public static T globalWorkflowAddCategories(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest(env).request("/" + objectId + "/" + "update", + new DXHTTPRequest(env).request("/" + objectId + "/" + "addCategories", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the globalWorkflowUpdate method. + * Invokes the globalWorkflowAddCategories method. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @@ -16992,16 +16981,16 @@ public static T globalWorkflowUpdate(String objectId, Object inputObject, Cl * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowUpdate(String, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #globalWorkflowAddCategories(String, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowUpdate(String objectId) { - return globalWorkflowUpdate(objectId, mapper.createObjectNode()); + public static JsonNode globalWorkflowAddCategories(String objectId) { + return globalWorkflowAddCategories(objectId, mapper.createObjectNode()); } /** - * Invokes the globalWorkflowUpdate method with the specified parameters. + * Invokes the globalWorkflowAddCategories method with the specified parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -17015,17 +17004,17 @@ public static JsonNode globalWorkflowUpdate(String objectId) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowUpdate(String, Object, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #globalWorkflowAddCategories(String, Object, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowUpdate(String objectId, JsonNode inputParams) { - return new DXHTTPRequest().request("/" + objectId + "/" + "update", inputParams, + public static JsonNode globalWorkflowAddCategories(String objectId, JsonNode inputParams) { + return new DXHTTPRequest().request("/" + objectId + "/" + "addCategories", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the globalWorkflowUpdate method with the specified environment. + * Invokes the globalWorkflowAddCategories method with the specified environment. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param env environment object specifying the auth token and remote server and protocol @@ -17039,16 +17028,16 @@ public static JsonNode globalWorkflowUpdate(String objectId, JsonNode inputParam * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowUpdate(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #globalWorkflowAddCategories(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowUpdate(String objectId, DXEnvironment env) { - return globalWorkflowUpdate(objectId, mapper.createObjectNode(), env); + public static JsonNode globalWorkflowAddCategories(String objectId, DXEnvironment env) { + return globalWorkflowAddCategories(objectId, mapper.createObjectNode(), env); } /** - * Invokes the globalWorkflowUpdate method with the specified environment and parameters. + * Invokes the globalWorkflowAddCategories method with the specified environment and parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -17063,22 +17052,23 @@ public static JsonNode globalWorkflowUpdate(String objectId, DXEnvironment env) * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowUpdate(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #globalWorkflowAddCategories(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowUpdate(String objectId, JsonNode inputParams, DXEnvironment env) { - return new DXHTTPRequest(env).request("/" + objectId + "/" + "update", inputParams, + public static JsonNode globalWorkflowAddCategories(String objectId, JsonNode inputParams, DXEnvironment env) { + return new DXHTTPRequest(env).request("/" + objectId + "/" + "addCategories", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the globalWorkflowNew method with an empty input, deserializing to an object of the specified class. + * Invokes the globalWorkflowAddDevelopers method with an empty input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * + * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to * - * @return Server response parsed from JSON + * @return Response object * * @throws DXAPIException * If the server returns a complete response with an HTTP status @@ -17087,18 +17077,19 @@ public static JsonNode globalWorkflowUpdate(String objectId, JsonNode inputParam * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowNew(Class outputClass) { - return globalWorkflowNew(mapper.createObjectNode(), outputClass); + public static T globalWorkflowAddDevelopers(String objectId, Class outputClass) { + return globalWorkflowAddDevelopers(objectId, mapper.createObjectNode(), outputClass); } /** - * Invokes the globalWorkflowNew method with an empty input using the specified environment, deserializing to an object of the specified class. + * Invokes the globalWorkflowAddDevelopers method with the given input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * + * @param objectId ID of the object to operate on + * @param inputObject input object (to be JSON serialized to an input hash) * @param outputClass class to deserialize the server reponse to - * @param env environment object specifying the auth token and remote server and protocol * - * @return Server response parsed from JSON + * @return Response object * * @throws DXAPIException * If the server returns a complete response with an HTTP status @@ -17107,18 +17098,22 @@ public static T globalWorkflowNew(Class outputClass) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowNew(Class outputClass, DXEnvironment env) { - return globalWorkflowNew(mapper.createObjectNode(), outputClass, env); + public static T globalWorkflowAddDevelopers(String objectId, Object inputObject, Class outputClass) { + JsonNode input = mapper.valueToTree(inputObject); + return DXJSON.safeTreeToValue( + new DXHTTPRequest().request("/" + objectId + "/" + "addDevelopers", + input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the globalWorkflowNew method with the specified input, deserializing to an object of the specified class. + * Invokes the globalWorkflowAddDevelopers method with an empty input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * - * @param inputObject input object (to be JSON serialized to an input hash) + * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to + * @param env environment object specifying the auth token and remote server and protocol * - * @return Server response parsed from JSON + * @return Response object * * @throws DXAPIException * If the server returns a complete response with an HTTP status @@ -17127,22 +17122,20 @@ public static T globalWorkflowNew(Class outputClass, DXEnvironment env) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowNew(Object inputObject, Class outputClass) { - JsonNode input = Nonce.updateNonce(mapper.valueToTree(inputObject)); - return DXJSON.safeTreeToValue( - new DXHTTPRequest().request("/globalworkflow/new", input, RetryStrategy.SAFE_TO_RETRY), - outputClass); + public static T globalWorkflowAddDevelopers(String objectId, Class outputClass, DXEnvironment env) { + return globalWorkflowAddDevelopers(objectId, mapper.createObjectNode(), outputClass, env); } /** - * Invokes the globalWorkflowNew method with the specified input using the specified environment, deserializing to an object of the specified class. + * Invokes the globalWorkflowAddDevelopers method with the given input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * + * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) * @param outputClass class to deserialize the server reponse to * @param env environment object specifying the auth token and remote server and protocol * - * @return Server response parsed from JSON + * @return Response object * * @throws DXAPIException * If the server returns a complete response with an HTTP status @@ -17151,17 +17144,19 @@ public static T globalWorkflowNew(Object inputObject, Class outputClass) * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T globalWorkflowNew(Object inputObject, Class outputClass, DXEnvironment env) { - JsonNode input = Nonce.updateNonce(mapper.valueToTree(inputObject)); + public static T globalWorkflowAddDevelopers(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { + JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest(env).request("/globalworkflow/new", input, RetryStrategy.SAFE_TO_RETRY), - outputClass); + new DXHTTPRequest(env).request("/" + objectId + "/" + "addDevelopers", + input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the globalWorkflowNew method. + * Invokes the globalWorkflowAddDevelopers method. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on * * @return Server response parsed from JSON * @@ -17172,17 +17167,18 @@ public static T globalWorkflowNew(Object inputObject, Class outputClass, * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowNew(Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #globalWorkflowAddDevelopers(String, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowNew() { - return globalWorkflowNew(mapper.createObjectNode()); + public static JsonNode globalWorkflowAddDevelopers(String objectId) { + return globalWorkflowAddDevelopers(objectId, mapper.createObjectNode()); } /** - * Invokes the globalWorkflowNew method with the specified input parameters. + * Invokes the globalWorkflowAddDevelopers method with the specified parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * + * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call * * @return Server response parsed from JSON @@ -17194,17 +17190,19 @@ public static JsonNode globalWorkflowNew() { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowNew(Object, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #globalWorkflowAddDevelopers(String, Object, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowNew(JsonNode inputParams) { - return new DXHTTPRequest().request("/globalworkflow/new", inputParams, RetryStrategy.UNSAFE_TO_RETRY); + public static JsonNode globalWorkflowAddDevelopers(String objectId, JsonNode inputParams) { + return new DXHTTPRequest().request("/" + objectId + "/" + "addDevelopers", inputParams, + RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the globalWorkflowNew method with the specified environment. + * Invokes the globalWorkflowAddDevelopers method with the specified environment. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * + * @param objectId ID of the object to operate on * @param env environment object specifying the auth token and remote server and protocol * * @return Server response parsed from JSON @@ -17216,17 +17214,18 @@ public static JsonNode globalWorkflowNew(JsonNode inputParams) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowNew(Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #globalWorkflowAddDevelopers(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowNew(DXEnvironment env) { - return globalWorkflowNew(mapper.createObjectNode(), env); + public static JsonNode globalWorkflowAddDevelopers(String objectId, DXEnvironment env) { + return globalWorkflowAddDevelopers(objectId, mapper.createObjectNode(), env); } /** - * Invokes the globalWorkflowNew method with the specified environment and input parameters. + * Invokes the globalWorkflowAddDevelopers method with the specified environment and parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * + * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call * @param env environment object specifying the auth token and remote server and protocol * @@ -17239,17 +17238,18 @@ public static JsonNode globalWorkflowNew(DXEnvironment env) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #globalWorkflowNew(Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #globalWorkflowAddDevelopers(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode globalWorkflowNew(JsonNode inputParams, DXEnvironment env) { - return new DXHTTPRequest(env).request("/globalworkflow/new", inputParams, RetryStrategy.UNSAFE_TO_RETRY); + public static JsonNode globalWorkflowAddDevelopers(String objectId, JsonNode inputParams, DXEnvironment env) { + return new DXHTTPRequest(env).request("/" + objectId + "/" + "addDevelopers", inputParams, + RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the jobAddTags method with an empty input, deserializing to an object of the specified class. + * Invokes the globalWorkflowAddTags method with an empty input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -17263,13 +17263,13 @@ public static JsonNode globalWorkflowNew(JsonNode inputParams, DXEnvironment env * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T jobAddTags(String objectId, Class outputClass) { - return jobAddTags(objectId, mapper.createObjectNode(), outputClass); + public static T globalWorkflowAddTags(String objectId, Class outputClass) { + return globalWorkflowAddTags(objectId, mapper.createObjectNode(), outputClass); } /** - * Invokes the jobAddTags method with the given input, deserializing to an object of the specified class. + * Invokes the globalWorkflowAddTags method with the given input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -17284,16 +17284,16 @@ public static T jobAddTags(String objectId, Class outputClass) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T jobAddTags(String objectId, Object inputObject, Class outputClass) { + public static T globalWorkflowAddTags(String objectId, Object inputObject, Class outputClass) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( new DXHTTPRequest().request("/" + objectId + "/" + "addTags", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the jobAddTags method with an empty input using the given environment, deserializing to an object of the specified class. + * Invokes the globalWorkflowAddTags method with an empty input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -17308,13 +17308,13 @@ public static T jobAddTags(String objectId, Object inputObject, Class out * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T jobAddTags(String objectId, Class outputClass, DXEnvironment env) { - return jobAddTags(objectId, mapper.createObjectNode(), outputClass, env); + public static T globalWorkflowAddTags(String objectId, Class outputClass, DXEnvironment env) { + return globalWorkflowAddTags(objectId, mapper.createObjectNode(), outputClass, env); } /** - * Invokes the jobAddTags method with the given input using the given environment, deserializing to an object of the specified class. + * Invokes the globalWorkflowAddTags method with the given input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -17330,7 +17330,7 @@ public static T jobAddTags(String objectId, Class outputClass, DXEnvironm * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T jobAddTags(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { + public static T globalWorkflowAddTags(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( new DXHTTPRequest(env).request("/" + objectId + "/" + "addTags", @@ -17338,9 +17338,9 @@ public static T jobAddTags(String objectId, Object inputObject, Class out } /** - * Invokes the jobAddTags method. + * Invokes the globalWorkflowAddTags method. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @@ -17353,16 +17353,16 @@ public static T jobAddTags(String objectId, Object inputObject, Class out * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #jobAddTags(String, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #globalWorkflowAddTags(String, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode jobAddTags(String objectId) { - return jobAddTags(objectId, mapper.createObjectNode()); + public static JsonNode globalWorkflowAddTags(String objectId) { + return globalWorkflowAddTags(objectId, mapper.createObjectNode()); } /** - * Invokes the jobAddTags method with the specified parameters. + * Invokes the globalWorkflowAddTags method with the specified parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -17376,17 +17376,17 @@ public static JsonNode jobAddTags(String objectId) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #jobAddTags(String, Object, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #globalWorkflowAddTags(String, Object, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode jobAddTags(String objectId, JsonNode inputParams) { + public static JsonNode globalWorkflowAddTags(String objectId, JsonNode inputParams) { return new DXHTTPRequest().request("/" + objectId + "/" + "addTags", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the jobAddTags method with the specified environment. + * Invokes the globalWorkflowAddTags method with the specified environment. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param env environment object specifying the auth token and remote server and protocol @@ -17400,16 +17400,16 @@ public static JsonNode jobAddTags(String objectId, JsonNode inputParams) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #jobAddTags(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #globalWorkflowAddTags(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode jobAddTags(String objectId, DXEnvironment env) { - return jobAddTags(objectId, mapper.createObjectNode(), env); + public static JsonNode globalWorkflowAddTags(String objectId, DXEnvironment env) { + return globalWorkflowAddTags(objectId, mapper.createObjectNode(), env); } /** - * Invokes the jobAddTags method with the specified environment and parameters. + * Invokes the globalWorkflowAddTags method with the specified environment and parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -17424,18 +17424,18 @@ public static JsonNode jobAddTags(String objectId, DXEnvironment env) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #jobAddTags(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #globalWorkflowAddTags(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode jobAddTags(String objectId, JsonNode inputParams, DXEnvironment env) { + public static JsonNode globalWorkflowAddTags(String objectId, JsonNode inputParams, DXEnvironment env) { return new DXHTTPRequest(env).request("/" + objectId + "/" + "addTags", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the jobDescribe method with an empty input, deserializing to an object of the specified class. + * Invokes the globalWorkflowDelete method with an empty input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -17449,13 +17449,13 @@ public static JsonNode jobAddTags(String objectId, JsonNode inputParams, DXEnvir * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T jobDescribe(String objectId, Class outputClass) { - return jobDescribe(objectId, mapper.createObjectNode(), outputClass); + public static T globalWorkflowDelete(String objectId, Class outputClass) { + return globalWorkflowDelete(objectId, mapper.createObjectNode(), outputClass); } /** - * Invokes the jobDescribe method with the given input, deserializing to an object of the specified class. + * Invokes the globalWorkflowDelete method with the given input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -17470,16 +17470,202 @@ public static T jobDescribe(String objectId, Class outputClass) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T jobDescribe(String objectId, Object inputObject, Class outputClass) { + public static T globalWorkflowDelete(String objectId, Object inputObject, Class outputClass) { + JsonNode input = mapper.valueToTree(inputObject); + return DXJSON.safeTreeToValue( + new DXHTTPRequest().request("/" + objectId + "/" + "delete", + input, RetryStrategy.SAFE_TO_RETRY), outputClass); + } + /** + * Invokes the globalWorkflowDelete method with an empty input using the given environment, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param outputClass class to deserialize the server reponse to + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T globalWorkflowDelete(String objectId, Class outputClass, DXEnvironment env) { + return globalWorkflowDelete(objectId, mapper.createObjectNode(), outputClass, env); + } + /** + * Invokes the globalWorkflowDelete method with the given input using the given environment, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputObject input object (to be JSON serialized to an input hash) + * @param outputClass class to deserialize the server reponse to + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T globalWorkflowDelete(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { + JsonNode input = mapper.valueToTree(inputObject); + return DXJSON.safeTreeToValue( + new DXHTTPRequest(env).request("/" + objectId + "/" + "delete", + input, RetryStrategy.SAFE_TO_RETRY), outputClass); + } + + /** + * Invokes the globalWorkflowDelete method. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #globalWorkflowDelete(String, Class)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode globalWorkflowDelete(String objectId) { + return globalWorkflowDelete(objectId, mapper.createObjectNode()); + } + /** + * Invokes the globalWorkflowDelete method with the specified parameters. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputParams input parameters to the API call + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #globalWorkflowDelete(String, Object, Class)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode globalWorkflowDelete(String objectId, JsonNode inputParams) { + return new DXHTTPRequest().request("/" + objectId + "/" + "delete", inputParams, + RetryStrategy.SAFE_TO_RETRY); + } + /** + * Invokes the globalWorkflowDelete method with the specified environment. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #globalWorkflowDelete(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode globalWorkflowDelete(String objectId, DXEnvironment env) { + return globalWorkflowDelete(objectId, mapper.createObjectNode(), env); + } + /** + * Invokes the globalWorkflowDelete method with the specified environment and parameters. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputParams input parameters to the API call + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #globalWorkflowDelete(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode globalWorkflowDelete(String objectId, JsonNode inputParams, DXEnvironment env) { + return new DXHTTPRequest(env).request("/" + objectId + "/" + "delete", inputParams, + RetryStrategy.SAFE_TO_RETRY); + } + + /** + * Invokes the globalWorkflowDescribe method with an empty input, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param outputClass class to deserialize the server reponse to + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T globalWorkflowDescribe(String objectId, Class outputClass) { + return globalWorkflowDescribe(objectId, mapper.createObjectNode(), outputClass); + } + /** + * Invokes the globalWorkflowDescribe method with the given input, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputObject input object (to be JSON serialized to an input hash) + * @param outputClass class to deserialize the server reponse to + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T globalWorkflowDescribe(String objectId, Object inputObject, Class outputClass) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( new DXHTTPRequest().request("/" + objectId + "/" + "describe", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the jobDescribe method with an empty input using the given environment, deserializing to an object of the specified class. + * Invokes the globalWorkflowDescribe method with an empty input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -17494,13 +17680,13 @@ public static T jobDescribe(String objectId, Object inputObject, Class ou * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T jobDescribe(String objectId, Class outputClass, DXEnvironment env) { - return jobDescribe(objectId, mapper.createObjectNode(), outputClass, env); + public static T globalWorkflowDescribe(String objectId, Class outputClass, DXEnvironment env) { + return globalWorkflowDescribe(objectId, mapper.createObjectNode(), outputClass, env); } /** - * Invokes the jobDescribe method with the given input using the given environment, deserializing to an object of the specified class. + * Invokes the globalWorkflowDescribe method with the given input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -17516,7 +17702,7 @@ public static T jobDescribe(String objectId, Class outputClass, DXEnviron * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T jobDescribe(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { + public static T globalWorkflowDescribe(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( new DXHTTPRequest(env).request("/" + objectId + "/" + "describe", @@ -17524,9 +17710,9 @@ public static T jobDescribe(String objectId, Object inputObject, Class ou } /** - * Invokes the jobDescribe method. + * Invokes the globalWorkflowDescribe method. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @@ -17539,16 +17725,2795 @@ public static T jobDescribe(String objectId, Object inputObject, Class ou * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #jobDescribe(String, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #globalWorkflowDescribe(String, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode jobDescribe(String objectId) { - return jobDescribe(objectId, mapper.createObjectNode()); + public static JsonNode globalWorkflowDescribe(String objectId) { + return globalWorkflowDescribe(objectId, mapper.createObjectNode()); } /** - * Invokes the jobDescribe method with the specified parameters. + * Invokes the globalWorkflowDescribe method with the specified parameters. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputParams input parameters to the API call + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #globalWorkflowDescribe(String, Object, Class)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode globalWorkflowDescribe(String objectId, JsonNode inputParams) { + return new DXHTTPRequest().request("/" + objectId + "/" + "describe", inputParams, + RetryStrategy.SAFE_TO_RETRY); + } + /** + * Invokes the globalWorkflowDescribe method with the specified environment. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #globalWorkflowDescribe(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode globalWorkflowDescribe(String objectId, DXEnvironment env) { + return globalWorkflowDescribe(objectId, mapper.createObjectNode(), env); + } + /** + * Invokes the globalWorkflowDescribe method with the specified environment and parameters. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputParams input parameters to the API call + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #globalWorkflowDescribe(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode globalWorkflowDescribe(String objectId, JsonNode inputParams, DXEnvironment env) { + return new DXHTTPRequest(env).request("/" + objectId + "/" + "describe", inputParams, + RetryStrategy.SAFE_TO_RETRY); + } + + /** + * Invokes the globalWorkflowListAuthorizedUsers method with an empty input, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param outputClass class to deserialize the server reponse to + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T globalWorkflowListAuthorizedUsers(String objectId, Class outputClass) { + return globalWorkflowListAuthorizedUsers(objectId, mapper.createObjectNode(), outputClass); + } + /** + * Invokes the globalWorkflowListAuthorizedUsers method with the given input, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputObject input object (to be JSON serialized to an input hash) + * @param outputClass class to deserialize the server reponse to + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T globalWorkflowListAuthorizedUsers(String objectId, Object inputObject, Class outputClass) { + JsonNode input = mapper.valueToTree(inputObject); + return DXJSON.safeTreeToValue( + new DXHTTPRequest().request("/" + objectId + "/" + "listAuthorizedUsers", + input, RetryStrategy.SAFE_TO_RETRY), outputClass); + } + /** + * Invokes the globalWorkflowListAuthorizedUsers method with an empty input using the given environment, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param outputClass class to deserialize the server reponse to + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T globalWorkflowListAuthorizedUsers(String objectId, Class outputClass, DXEnvironment env) { + return globalWorkflowListAuthorizedUsers(objectId, mapper.createObjectNode(), outputClass, env); + } + /** + * Invokes the globalWorkflowListAuthorizedUsers method with the given input using the given environment, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputObject input object (to be JSON serialized to an input hash) + * @param outputClass class to deserialize the server reponse to + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T globalWorkflowListAuthorizedUsers(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { + JsonNode input = mapper.valueToTree(inputObject); + return DXJSON.safeTreeToValue( + new DXHTTPRequest(env).request("/" + objectId + "/" + "listAuthorizedUsers", + input, RetryStrategy.SAFE_TO_RETRY), outputClass); + } + + /** + * Invokes the globalWorkflowListAuthorizedUsers method. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #globalWorkflowListAuthorizedUsers(String, Class)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode globalWorkflowListAuthorizedUsers(String objectId) { + return globalWorkflowListAuthorizedUsers(objectId, mapper.createObjectNode()); + } + /** + * Invokes the globalWorkflowListAuthorizedUsers method with the specified parameters. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputParams input parameters to the API call + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #globalWorkflowListAuthorizedUsers(String, Object, Class)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode globalWorkflowListAuthorizedUsers(String objectId, JsonNode inputParams) { + return new DXHTTPRequest().request("/" + objectId + "/" + "listAuthorizedUsers", inputParams, + RetryStrategy.SAFE_TO_RETRY); + } + /** + * Invokes the globalWorkflowListAuthorizedUsers method with the specified environment. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #globalWorkflowListAuthorizedUsers(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode globalWorkflowListAuthorizedUsers(String objectId, DXEnvironment env) { + return globalWorkflowListAuthorizedUsers(objectId, mapper.createObjectNode(), env); + } + /** + * Invokes the globalWorkflowListAuthorizedUsers method with the specified environment and parameters. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputParams input parameters to the API call + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #globalWorkflowListAuthorizedUsers(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode globalWorkflowListAuthorizedUsers(String objectId, JsonNode inputParams, DXEnvironment env) { + return new DXHTTPRequest(env).request("/" + objectId + "/" + "listAuthorizedUsers", inputParams, + RetryStrategy.SAFE_TO_RETRY); + } + + /** + * Invokes the globalWorkflowListCategories method with an empty input, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param outputClass class to deserialize the server reponse to + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T globalWorkflowListCategories(String objectId, Class outputClass) { + return globalWorkflowListCategories(objectId, mapper.createObjectNode(), outputClass); + } + /** + * Invokes the globalWorkflowListCategories method with the given input, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputObject input object (to be JSON serialized to an input hash) + * @param outputClass class to deserialize the server reponse to + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T globalWorkflowListCategories(String objectId, Object inputObject, Class outputClass) { + JsonNode input = mapper.valueToTree(inputObject); + return DXJSON.safeTreeToValue( + new DXHTTPRequest().request("/" + objectId + "/" + "listCategories", + input, RetryStrategy.SAFE_TO_RETRY), outputClass); + } + /** + * Invokes the globalWorkflowListCategories method with an empty input using the given environment, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param outputClass class to deserialize the server reponse to + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T globalWorkflowListCategories(String objectId, Class outputClass, DXEnvironment env) { + return globalWorkflowListCategories(objectId, mapper.createObjectNode(), outputClass, env); + } + /** + * Invokes the globalWorkflowListCategories method with the given input using the given environment, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputObject input object (to be JSON serialized to an input hash) + * @param outputClass class to deserialize the server reponse to + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T globalWorkflowListCategories(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { + JsonNode input = mapper.valueToTree(inputObject); + return DXJSON.safeTreeToValue( + new DXHTTPRequest(env).request("/" + objectId + "/" + "listCategories", + input, RetryStrategy.SAFE_TO_RETRY), outputClass); + } + + /** + * Invokes the globalWorkflowListCategories method. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #globalWorkflowListCategories(String, Class)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode globalWorkflowListCategories(String objectId) { + return globalWorkflowListCategories(objectId, mapper.createObjectNode()); + } + /** + * Invokes the globalWorkflowListCategories method with the specified parameters. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputParams input parameters to the API call + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #globalWorkflowListCategories(String, Object, Class)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode globalWorkflowListCategories(String objectId, JsonNode inputParams) { + return new DXHTTPRequest().request("/" + objectId + "/" + "listCategories", inputParams, + RetryStrategy.SAFE_TO_RETRY); + } + /** + * Invokes the globalWorkflowListCategories method with the specified environment. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #globalWorkflowListCategories(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode globalWorkflowListCategories(String objectId, DXEnvironment env) { + return globalWorkflowListCategories(objectId, mapper.createObjectNode(), env); + } + /** + * Invokes the globalWorkflowListCategories method with the specified environment and parameters. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputParams input parameters to the API call + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #globalWorkflowListCategories(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode globalWorkflowListCategories(String objectId, JsonNode inputParams, DXEnvironment env) { + return new DXHTTPRequest(env).request("/" + objectId + "/" + "listCategories", inputParams, + RetryStrategy.SAFE_TO_RETRY); + } + + /** + * Invokes the globalWorkflowListDevelopers method with an empty input, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param outputClass class to deserialize the server reponse to + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T globalWorkflowListDevelopers(String objectId, Class outputClass) { + return globalWorkflowListDevelopers(objectId, mapper.createObjectNode(), outputClass); + } + /** + * Invokes the globalWorkflowListDevelopers method with the given input, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputObject input object (to be JSON serialized to an input hash) + * @param outputClass class to deserialize the server reponse to + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T globalWorkflowListDevelopers(String objectId, Object inputObject, Class outputClass) { + JsonNode input = mapper.valueToTree(inputObject); + return DXJSON.safeTreeToValue( + new DXHTTPRequest().request("/" + objectId + "/" + "listDevelopers", + input, RetryStrategy.SAFE_TO_RETRY), outputClass); + } + /** + * Invokes the globalWorkflowListDevelopers method with an empty input using the given environment, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param outputClass class to deserialize the server reponse to + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T globalWorkflowListDevelopers(String objectId, Class outputClass, DXEnvironment env) { + return globalWorkflowListDevelopers(objectId, mapper.createObjectNode(), outputClass, env); + } + /** + * Invokes the globalWorkflowListDevelopers method with the given input using the given environment, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputObject input object (to be JSON serialized to an input hash) + * @param outputClass class to deserialize the server reponse to + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T globalWorkflowListDevelopers(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { + JsonNode input = mapper.valueToTree(inputObject); + return DXJSON.safeTreeToValue( + new DXHTTPRequest(env).request("/" + objectId + "/" + "listDevelopers", + input, RetryStrategy.SAFE_TO_RETRY), outputClass); + } + + /** + * Invokes the globalWorkflowListDevelopers method. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #globalWorkflowListDevelopers(String, Class)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode globalWorkflowListDevelopers(String objectId) { + return globalWorkflowListDevelopers(objectId, mapper.createObjectNode()); + } + /** + * Invokes the globalWorkflowListDevelopers method with the specified parameters. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputParams input parameters to the API call + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #globalWorkflowListDevelopers(String, Object, Class)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode globalWorkflowListDevelopers(String objectId, JsonNode inputParams) { + return new DXHTTPRequest().request("/" + objectId + "/" + "listDevelopers", inputParams, + RetryStrategy.SAFE_TO_RETRY); + } + /** + * Invokes the globalWorkflowListDevelopers method with the specified environment. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #globalWorkflowListDevelopers(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode globalWorkflowListDevelopers(String objectId, DXEnvironment env) { + return globalWorkflowListDevelopers(objectId, mapper.createObjectNode(), env); + } + /** + * Invokes the globalWorkflowListDevelopers method with the specified environment and parameters. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputParams input parameters to the API call + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #globalWorkflowListDevelopers(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode globalWorkflowListDevelopers(String objectId, JsonNode inputParams, DXEnvironment env) { + return new DXHTTPRequest(env).request("/" + objectId + "/" + "listDevelopers", inputParams, + RetryStrategy.SAFE_TO_RETRY); + } + + /** + * Invokes the globalWorkflowPublish method with an empty input, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param outputClass class to deserialize the server reponse to + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T globalWorkflowPublish(String objectId, Class outputClass) { + return globalWorkflowPublish(objectId, mapper.createObjectNode(), outputClass); + } + /** + * Invokes the globalWorkflowPublish method with the given input, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputObject input object (to be JSON serialized to an input hash) + * @param outputClass class to deserialize the server reponse to + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T globalWorkflowPublish(String objectId, Object inputObject, Class outputClass) { + JsonNode input = mapper.valueToTree(inputObject); + return DXJSON.safeTreeToValue( + new DXHTTPRequest().request("/" + objectId + "/" + "publish", + input, RetryStrategy.SAFE_TO_RETRY), outputClass); + } + /** + * Invokes the globalWorkflowPublish method with an empty input using the given environment, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param outputClass class to deserialize the server reponse to + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T globalWorkflowPublish(String objectId, Class outputClass, DXEnvironment env) { + return globalWorkflowPublish(objectId, mapper.createObjectNode(), outputClass, env); + } + /** + * Invokes the globalWorkflowPublish method with the given input using the given environment, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputObject input object (to be JSON serialized to an input hash) + * @param outputClass class to deserialize the server reponse to + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T globalWorkflowPublish(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { + JsonNode input = mapper.valueToTree(inputObject); + return DXJSON.safeTreeToValue( + new DXHTTPRequest(env).request("/" + objectId + "/" + "publish", + input, RetryStrategy.SAFE_TO_RETRY), outputClass); + } + + /** + * Invokes the globalWorkflowPublish method. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #globalWorkflowPublish(String, Class)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode globalWorkflowPublish(String objectId) { + return globalWorkflowPublish(objectId, mapper.createObjectNode()); + } + /** + * Invokes the globalWorkflowPublish method with the specified parameters. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputParams input parameters to the API call + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #globalWorkflowPublish(String, Object, Class)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode globalWorkflowPublish(String objectId, JsonNode inputParams) { + return new DXHTTPRequest().request("/" + objectId + "/" + "publish", inputParams, + RetryStrategy.SAFE_TO_RETRY); + } + /** + * Invokes the globalWorkflowPublish method with the specified environment. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #globalWorkflowPublish(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode globalWorkflowPublish(String objectId, DXEnvironment env) { + return globalWorkflowPublish(objectId, mapper.createObjectNode(), env); + } + /** + * Invokes the globalWorkflowPublish method with the specified environment and parameters. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputParams input parameters to the API call + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #globalWorkflowPublish(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode globalWorkflowPublish(String objectId, JsonNode inputParams, DXEnvironment env) { + return new DXHTTPRequest(env).request("/" + objectId + "/" + "publish", inputParams, + RetryStrategy.SAFE_TO_RETRY); + } + + /** + * Invokes the globalWorkflowRemoveAuthorizedUsers method with an empty input, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param outputClass class to deserialize the server reponse to + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T globalWorkflowRemoveAuthorizedUsers(String objectId, Class outputClass) { + return globalWorkflowRemoveAuthorizedUsers(objectId, mapper.createObjectNode(), outputClass); + } + /** + * Invokes the globalWorkflowRemoveAuthorizedUsers method with the given input, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputObject input object (to be JSON serialized to an input hash) + * @param outputClass class to deserialize the server reponse to + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T globalWorkflowRemoveAuthorizedUsers(String objectId, Object inputObject, Class outputClass) { + JsonNode input = mapper.valueToTree(inputObject); + return DXJSON.safeTreeToValue( + new DXHTTPRequest().request("/" + objectId + "/" + "removeAuthorizedUsers", + input, RetryStrategy.SAFE_TO_RETRY), outputClass); + } + /** + * Invokes the globalWorkflowRemoveAuthorizedUsers method with an empty input using the given environment, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param outputClass class to deserialize the server reponse to + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T globalWorkflowRemoveAuthorizedUsers(String objectId, Class outputClass, DXEnvironment env) { + return globalWorkflowRemoveAuthorizedUsers(objectId, mapper.createObjectNode(), outputClass, env); + } + /** + * Invokes the globalWorkflowRemoveAuthorizedUsers method with the given input using the given environment, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputObject input object (to be JSON serialized to an input hash) + * @param outputClass class to deserialize the server reponse to + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T globalWorkflowRemoveAuthorizedUsers(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { + JsonNode input = mapper.valueToTree(inputObject); + return DXJSON.safeTreeToValue( + new DXHTTPRequest(env).request("/" + objectId + "/" + "removeAuthorizedUsers", + input, RetryStrategy.SAFE_TO_RETRY), outputClass); + } + + /** + * Invokes the globalWorkflowRemoveAuthorizedUsers method. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #globalWorkflowRemoveAuthorizedUsers(String, Class)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode globalWorkflowRemoveAuthorizedUsers(String objectId) { + return globalWorkflowRemoveAuthorizedUsers(objectId, mapper.createObjectNode()); + } + /** + * Invokes the globalWorkflowRemoveAuthorizedUsers method with the specified parameters. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputParams input parameters to the API call + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #globalWorkflowRemoveAuthorizedUsers(String, Object, Class)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode globalWorkflowRemoveAuthorizedUsers(String objectId, JsonNode inputParams) { + return new DXHTTPRequest().request("/" + objectId + "/" + "removeAuthorizedUsers", inputParams, + RetryStrategy.SAFE_TO_RETRY); + } + /** + * Invokes the globalWorkflowRemoveAuthorizedUsers method with the specified environment. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #globalWorkflowRemoveAuthorizedUsers(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode globalWorkflowRemoveAuthorizedUsers(String objectId, DXEnvironment env) { + return globalWorkflowRemoveAuthorizedUsers(objectId, mapper.createObjectNode(), env); + } + /** + * Invokes the globalWorkflowRemoveAuthorizedUsers method with the specified environment and parameters. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputParams input parameters to the API call + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #globalWorkflowRemoveAuthorizedUsers(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode globalWorkflowRemoveAuthorizedUsers(String objectId, JsonNode inputParams, DXEnvironment env) { + return new DXHTTPRequest(env).request("/" + objectId + "/" + "removeAuthorizedUsers", inputParams, + RetryStrategy.SAFE_TO_RETRY); + } + + /** + * Invokes the globalWorkflowRemoveCategories method with an empty input, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param outputClass class to deserialize the server reponse to + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T globalWorkflowRemoveCategories(String objectId, Class outputClass) { + return globalWorkflowRemoveCategories(objectId, mapper.createObjectNode(), outputClass); + } + /** + * Invokes the globalWorkflowRemoveCategories method with the given input, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputObject input object (to be JSON serialized to an input hash) + * @param outputClass class to deserialize the server reponse to + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T globalWorkflowRemoveCategories(String objectId, Object inputObject, Class outputClass) { + JsonNode input = mapper.valueToTree(inputObject); + return DXJSON.safeTreeToValue( + new DXHTTPRequest().request("/" + objectId + "/" + "removeCategories", + input, RetryStrategy.SAFE_TO_RETRY), outputClass); + } + /** + * Invokes the globalWorkflowRemoveCategories method with an empty input using the given environment, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param outputClass class to deserialize the server reponse to + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T globalWorkflowRemoveCategories(String objectId, Class outputClass, DXEnvironment env) { + return globalWorkflowRemoveCategories(objectId, mapper.createObjectNode(), outputClass, env); + } + /** + * Invokes the globalWorkflowRemoveCategories method with the given input using the given environment, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputObject input object (to be JSON serialized to an input hash) + * @param outputClass class to deserialize the server reponse to + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T globalWorkflowRemoveCategories(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { + JsonNode input = mapper.valueToTree(inputObject); + return DXJSON.safeTreeToValue( + new DXHTTPRequest(env).request("/" + objectId + "/" + "removeCategories", + input, RetryStrategy.SAFE_TO_RETRY), outputClass); + } + + /** + * Invokes the globalWorkflowRemoveCategories method. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #globalWorkflowRemoveCategories(String, Class)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode globalWorkflowRemoveCategories(String objectId) { + return globalWorkflowRemoveCategories(objectId, mapper.createObjectNode()); + } + /** + * Invokes the globalWorkflowRemoveCategories method with the specified parameters. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputParams input parameters to the API call + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #globalWorkflowRemoveCategories(String, Object, Class)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode globalWorkflowRemoveCategories(String objectId, JsonNode inputParams) { + return new DXHTTPRequest().request("/" + objectId + "/" + "removeCategories", inputParams, + RetryStrategy.SAFE_TO_RETRY); + } + /** + * Invokes the globalWorkflowRemoveCategories method with the specified environment. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #globalWorkflowRemoveCategories(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode globalWorkflowRemoveCategories(String objectId, DXEnvironment env) { + return globalWorkflowRemoveCategories(objectId, mapper.createObjectNode(), env); + } + /** + * Invokes the globalWorkflowRemoveCategories method with the specified environment and parameters. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputParams input parameters to the API call + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #globalWorkflowRemoveCategories(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode globalWorkflowRemoveCategories(String objectId, JsonNode inputParams, DXEnvironment env) { + return new DXHTTPRequest(env).request("/" + objectId + "/" + "removeCategories", inputParams, + RetryStrategy.SAFE_TO_RETRY); + } + + /** + * Invokes the globalWorkflowRemoveDevelopers method with an empty input, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param outputClass class to deserialize the server reponse to + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T globalWorkflowRemoveDevelopers(String objectId, Class outputClass) { + return globalWorkflowRemoveDevelopers(objectId, mapper.createObjectNode(), outputClass); + } + /** + * Invokes the globalWorkflowRemoveDevelopers method with the given input, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputObject input object (to be JSON serialized to an input hash) + * @param outputClass class to deserialize the server reponse to + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T globalWorkflowRemoveDevelopers(String objectId, Object inputObject, Class outputClass) { + JsonNode input = mapper.valueToTree(inputObject); + return DXJSON.safeTreeToValue( + new DXHTTPRequest().request("/" + objectId + "/" + "removeDevelopers", + input, RetryStrategy.SAFE_TO_RETRY), outputClass); + } + /** + * Invokes the globalWorkflowRemoveDevelopers method with an empty input using the given environment, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param outputClass class to deserialize the server reponse to + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T globalWorkflowRemoveDevelopers(String objectId, Class outputClass, DXEnvironment env) { + return globalWorkflowRemoveDevelopers(objectId, mapper.createObjectNode(), outputClass, env); + } + /** + * Invokes the globalWorkflowRemoveDevelopers method with the given input using the given environment, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputObject input object (to be JSON serialized to an input hash) + * @param outputClass class to deserialize the server reponse to + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T globalWorkflowRemoveDevelopers(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { + JsonNode input = mapper.valueToTree(inputObject); + return DXJSON.safeTreeToValue( + new DXHTTPRequest(env).request("/" + objectId + "/" + "removeDevelopers", + input, RetryStrategy.SAFE_TO_RETRY), outputClass); + } + + /** + * Invokes the globalWorkflowRemoveDevelopers method. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #globalWorkflowRemoveDevelopers(String, Class)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode globalWorkflowRemoveDevelopers(String objectId) { + return globalWorkflowRemoveDevelopers(objectId, mapper.createObjectNode()); + } + /** + * Invokes the globalWorkflowRemoveDevelopers method with the specified parameters. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputParams input parameters to the API call + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #globalWorkflowRemoveDevelopers(String, Object, Class)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode globalWorkflowRemoveDevelopers(String objectId, JsonNode inputParams) { + return new DXHTTPRequest().request("/" + objectId + "/" + "removeDevelopers", inputParams, + RetryStrategy.SAFE_TO_RETRY); + } + /** + * Invokes the globalWorkflowRemoveDevelopers method with the specified environment. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #globalWorkflowRemoveDevelopers(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode globalWorkflowRemoveDevelopers(String objectId, DXEnvironment env) { + return globalWorkflowRemoveDevelopers(objectId, mapper.createObjectNode(), env); + } + /** + * Invokes the globalWorkflowRemoveDevelopers method with the specified environment and parameters. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputParams input parameters to the API call + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #globalWorkflowRemoveDevelopers(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode globalWorkflowRemoveDevelopers(String objectId, JsonNode inputParams, DXEnvironment env) { + return new DXHTTPRequest(env).request("/" + objectId + "/" + "removeDevelopers", inputParams, + RetryStrategy.SAFE_TO_RETRY); + } + + /** + * Invokes the globalWorkflowRemoveTags method with an empty input, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param outputClass class to deserialize the server reponse to + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T globalWorkflowRemoveTags(String objectId, Class outputClass) { + return globalWorkflowRemoveTags(objectId, mapper.createObjectNode(), outputClass); + } + /** + * Invokes the globalWorkflowRemoveTags method with the given input, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputObject input object (to be JSON serialized to an input hash) + * @param outputClass class to deserialize the server reponse to + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T globalWorkflowRemoveTags(String objectId, Object inputObject, Class outputClass) { + JsonNode input = mapper.valueToTree(inputObject); + return DXJSON.safeTreeToValue( + new DXHTTPRequest().request("/" + objectId + "/" + "removeTags", + input, RetryStrategy.SAFE_TO_RETRY), outputClass); + } + /** + * Invokes the globalWorkflowRemoveTags method with an empty input using the given environment, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param outputClass class to deserialize the server reponse to + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T globalWorkflowRemoveTags(String objectId, Class outputClass, DXEnvironment env) { + return globalWorkflowRemoveTags(objectId, mapper.createObjectNode(), outputClass, env); + } + /** + * Invokes the globalWorkflowRemoveTags method with the given input using the given environment, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputObject input object (to be JSON serialized to an input hash) + * @param outputClass class to deserialize the server reponse to + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T globalWorkflowRemoveTags(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { + JsonNode input = mapper.valueToTree(inputObject); + return DXJSON.safeTreeToValue( + new DXHTTPRequest(env).request("/" + objectId + "/" + "removeTags", + input, RetryStrategy.SAFE_TO_RETRY), outputClass); + } + + /** + * Invokes the globalWorkflowRemoveTags method. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #globalWorkflowRemoveTags(String, Class)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode globalWorkflowRemoveTags(String objectId) { + return globalWorkflowRemoveTags(objectId, mapper.createObjectNode()); + } + /** + * Invokes the globalWorkflowRemoveTags method with the specified parameters. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputParams input parameters to the API call + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #globalWorkflowRemoveTags(String, Object, Class)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode globalWorkflowRemoveTags(String objectId, JsonNode inputParams) { + return new DXHTTPRequest().request("/" + objectId + "/" + "removeTags", inputParams, + RetryStrategy.SAFE_TO_RETRY); + } + /** + * Invokes the globalWorkflowRemoveTags method with the specified environment. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #globalWorkflowRemoveTags(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode globalWorkflowRemoveTags(String objectId, DXEnvironment env) { + return globalWorkflowRemoveTags(objectId, mapper.createObjectNode(), env); + } + /** + * Invokes the globalWorkflowRemoveTags method with the specified environment and parameters. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputParams input parameters to the API call + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #globalWorkflowRemoveTags(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode globalWorkflowRemoveTags(String objectId, JsonNode inputParams, DXEnvironment env) { + return new DXHTTPRequest(env).request("/" + objectId + "/" + "removeTags", inputParams, + RetryStrategy.SAFE_TO_RETRY); + } + + /** + * Invokes the globalWorkflowRun method with an empty input, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param outputClass class to deserialize the server reponse to + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T globalWorkflowRun(String objectId, Class outputClass) { + return globalWorkflowRun(objectId, mapper.createObjectNode(), outputClass); + } + /** + * Invokes the globalWorkflowRun method with the given input, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputObject input object (to be JSON serialized to an input hash) + * @param outputClass class to deserialize the server reponse to + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T globalWorkflowRun(String objectId, Object inputObject, Class outputClass) { + JsonNode input = Nonce.updateNonce(mapper.valueToTree(inputObject)); + return DXJSON.safeTreeToValue( + new DXHTTPRequest().request("/" + objectId + "/" + "run", + input, RetryStrategy.SAFE_TO_RETRY), outputClass); + } + /** + * Invokes the globalWorkflowRun method with an empty input using the given environment, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param outputClass class to deserialize the server reponse to + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T globalWorkflowRun(String objectId, Class outputClass, DXEnvironment env) { + return globalWorkflowRun(objectId, mapper.createObjectNode(), outputClass, env); + } + /** + * Invokes the globalWorkflowRun method with the given input using the given environment, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputObject input object (to be JSON serialized to an input hash) + * @param outputClass class to deserialize the server reponse to + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T globalWorkflowRun(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { + JsonNode input = Nonce.updateNonce(mapper.valueToTree(inputObject)); + return DXJSON.safeTreeToValue( + new DXHTTPRequest(env).request("/" + objectId + "/" + "run", + input, RetryStrategy.SAFE_TO_RETRY), outputClass); + } + + /** + * Invokes the globalWorkflowRun method. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #globalWorkflowRun(String, Class)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode globalWorkflowRun(String objectId) { + return globalWorkflowRun(objectId, mapper.createObjectNode()); + } + /** + * Invokes the globalWorkflowRun method with the specified parameters. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputParams input parameters to the API call + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #globalWorkflowRun(String, Object, Class)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode globalWorkflowRun(String objectId, JsonNode inputParams) { + return new DXHTTPRequest().request("/" + objectId + "/" + "run", inputParams, + RetryStrategy.UNSAFE_TO_RETRY); + } + /** + * Invokes the globalWorkflowRun method with the specified environment. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #globalWorkflowRun(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode globalWorkflowRun(String objectId, DXEnvironment env) { + return globalWorkflowRun(objectId, mapper.createObjectNode(), env); + } + /** + * Invokes the globalWorkflowRun method with the specified environment and parameters. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputParams input parameters to the API call + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #globalWorkflowRun(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode globalWorkflowRun(String objectId, JsonNode inputParams, DXEnvironment env) { + return new DXHTTPRequest(env).request("/" + objectId + "/" + "run", inputParams, + RetryStrategy.UNSAFE_TO_RETRY); + } + + /** + * Invokes the globalWorkflowUpdate method with an empty input, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param outputClass class to deserialize the server reponse to + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T globalWorkflowUpdate(String objectId, Class outputClass) { + return globalWorkflowUpdate(objectId, mapper.createObjectNode(), outputClass); + } + /** + * Invokes the globalWorkflowUpdate method with the given input, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputObject input object (to be JSON serialized to an input hash) + * @param outputClass class to deserialize the server reponse to + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T globalWorkflowUpdate(String objectId, Object inputObject, Class outputClass) { + JsonNode input = mapper.valueToTree(inputObject); + return DXJSON.safeTreeToValue( + new DXHTTPRequest().request("/" + objectId + "/" + "update", + input, RetryStrategy.SAFE_TO_RETRY), outputClass); + } + /** + * Invokes the globalWorkflowUpdate method with an empty input using the given environment, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param outputClass class to deserialize the server reponse to + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T globalWorkflowUpdate(String objectId, Class outputClass, DXEnvironment env) { + return globalWorkflowUpdate(objectId, mapper.createObjectNode(), outputClass, env); + } + /** + * Invokes the globalWorkflowUpdate method with the given input using the given environment, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputObject input object (to be JSON serialized to an input hash) + * @param outputClass class to deserialize the server reponse to + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T globalWorkflowUpdate(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { + JsonNode input = mapper.valueToTree(inputObject); + return DXJSON.safeTreeToValue( + new DXHTTPRequest(env).request("/" + objectId + "/" + "update", + input, RetryStrategy.SAFE_TO_RETRY), outputClass); + } + + /** + * Invokes the globalWorkflowUpdate method. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #globalWorkflowUpdate(String, Class)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode globalWorkflowUpdate(String objectId) { + return globalWorkflowUpdate(objectId, mapper.createObjectNode()); + } + /** + * Invokes the globalWorkflowUpdate method with the specified parameters. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputParams input parameters to the API call + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #globalWorkflowUpdate(String, Object, Class)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode globalWorkflowUpdate(String objectId, JsonNode inputParams) { + return new DXHTTPRequest().request("/" + objectId + "/" + "update", inputParams, + RetryStrategy.SAFE_TO_RETRY); + } + /** + * Invokes the globalWorkflowUpdate method with the specified environment. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #globalWorkflowUpdate(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode globalWorkflowUpdate(String objectId, DXEnvironment env) { + return globalWorkflowUpdate(objectId, mapper.createObjectNode(), env); + } + /** + * Invokes the globalWorkflowUpdate method with the specified environment and parameters. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputParams input parameters to the API call + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #globalWorkflowUpdate(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode globalWorkflowUpdate(String objectId, JsonNode inputParams, DXEnvironment env) { + return new DXHTTPRequest(env).request("/" + objectId + "/" + "update", inputParams, + RetryStrategy.SAFE_TO_RETRY); + } + + /** + * Invokes the globalWorkflowNew method with an empty input, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param outputClass class to deserialize the server reponse to + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T globalWorkflowNew(Class outputClass) { + return globalWorkflowNew(mapper.createObjectNode(), outputClass); + } + /** + * Invokes the globalWorkflowNew method with an empty input using the specified environment, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param outputClass class to deserialize the server reponse to + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T globalWorkflowNew(Class outputClass, DXEnvironment env) { + return globalWorkflowNew(mapper.createObjectNode(), outputClass, env); + } + /** + * Invokes the globalWorkflowNew method with the specified input, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param inputObject input object (to be JSON serialized to an input hash) + * @param outputClass class to deserialize the server reponse to + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T globalWorkflowNew(Object inputObject, Class outputClass) { + JsonNode input = Nonce.updateNonce(mapper.valueToTree(inputObject)); + return DXJSON.safeTreeToValue( + new DXHTTPRequest().request("/globalworkflow/new", input, RetryStrategy.SAFE_TO_RETRY), + outputClass); + } + /** + * Invokes the globalWorkflowNew method with the specified input using the specified environment, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param inputObject input object (to be JSON serialized to an input hash) + * @param outputClass class to deserialize the server reponse to + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T globalWorkflowNew(Object inputObject, Class outputClass, DXEnvironment env) { + JsonNode input = Nonce.updateNonce(mapper.valueToTree(inputObject)); + return DXJSON.safeTreeToValue( + new DXHTTPRequest(env).request("/globalworkflow/new", input, RetryStrategy.SAFE_TO_RETRY), + outputClass); + } + + /** + * Invokes the globalWorkflowNew method. + * + *

For more information about this method, see the API specification. + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #globalWorkflowNew(Class)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode globalWorkflowNew() { + return globalWorkflowNew(mapper.createObjectNode()); + } + /** + * Invokes the globalWorkflowNew method with the specified input parameters. + * + *

For more information about this method, see the API specification. + * + * @param inputParams input parameters to the API call + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #globalWorkflowNew(Object, Class)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode globalWorkflowNew(JsonNode inputParams) { + return new DXHTTPRequest().request("/globalworkflow/new", inputParams, RetryStrategy.UNSAFE_TO_RETRY); + } + /** + * Invokes the globalWorkflowNew method with the specified environment. + * + *

For more information about this method, see the API specification. + * + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #globalWorkflowNew(Class, DXEnvironment)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode globalWorkflowNew(DXEnvironment env) { + return globalWorkflowNew(mapper.createObjectNode(), env); + } + /** + * Invokes the globalWorkflowNew method with the specified environment and input parameters. + * + *

For more information about this method, see the API specification. + * + * @param inputParams input parameters to the API call + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #globalWorkflowNew(Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode globalWorkflowNew(JsonNode inputParams, DXEnvironment env) { + return new DXHTTPRequest(env).request("/globalworkflow/new", inputParams, RetryStrategy.UNSAFE_TO_RETRY); + } + + /** + * Invokes the jobAddTags method with an empty input, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param outputClass class to deserialize the server reponse to + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T jobAddTags(String objectId, Class outputClass) { + return jobAddTags(objectId, mapper.createObjectNode(), outputClass); + } + /** + * Invokes the jobAddTags method with the given input, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputObject input object (to be JSON serialized to an input hash) + * @param outputClass class to deserialize the server reponse to + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T jobAddTags(String objectId, Object inputObject, Class outputClass) { + JsonNode input = mapper.valueToTree(inputObject); + return DXJSON.safeTreeToValue( + new DXHTTPRequest().request("/" + objectId + "/" + "addTags", + input, RetryStrategy.SAFE_TO_RETRY), outputClass); + } + /** + * Invokes the jobAddTags method with an empty input using the given environment, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param outputClass class to deserialize the server reponse to + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T jobAddTags(String objectId, Class outputClass, DXEnvironment env) { + return jobAddTags(objectId, mapper.createObjectNode(), outputClass, env); + } + /** + * Invokes the jobAddTags method with the given input using the given environment, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputObject input object (to be JSON serialized to an input hash) + * @param outputClass class to deserialize the server reponse to + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T jobAddTags(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { + JsonNode input = mapper.valueToTree(inputObject); + return DXJSON.safeTreeToValue( + new DXHTTPRequest(env).request("/" + objectId + "/" + "addTags", + input, RetryStrategy.SAFE_TO_RETRY), outputClass); + } + + /** + * Invokes the jobAddTags method. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #jobAddTags(String, Class)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode jobAddTags(String objectId) { + return jobAddTags(objectId, mapper.createObjectNode()); + } + /** + * Invokes the jobAddTags method with the specified parameters. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputParams input parameters to the API call + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #jobAddTags(String, Object, Class)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode jobAddTags(String objectId, JsonNode inputParams) { + return new DXHTTPRequest().request("/" + objectId + "/" + "addTags", inputParams, + RetryStrategy.SAFE_TO_RETRY); + } + /** + * Invokes the jobAddTags method with the specified environment. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #jobAddTags(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode jobAddTags(String objectId, DXEnvironment env) { + return jobAddTags(objectId, mapper.createObjectNode(), env); + } + /** + * Invokes the jobAddTags method with the specified environment and parameters. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputParams input parameters to the API call + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #jobAddTags(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode jobAddTags(String objectId, JsonNode inputParams, DXEnvironment env) { + return new DXHTTPRequest(env).request("/" + objectId + "/" + "addTags", inputParams, + RetryStrategy.SAFE_TO_RETRY); + } + + /** + * Invokes the jobDescribe method with an empty input, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param outputClass class to deserialize the server reponse to + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T jobDescribe(String objectId, Class outputClass) { + return jobDescribe(objectId, mapper.createObjectNode(), outputClass); + } + /** + * Invokes the jobDescribe method with the given input, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputObject input object (to be JSON serialized to an input hash) + * @param outputClass class to deserialize the server reponse to + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T jobDescribe(String objectId, Object inputObject, Class outputClass) { + JsonNode input = mapper.valueToTree(inputObject); + return DXJSON.safeTreeToValue( + new DXHTTPRequest().request("/" + objectId + "/" + "describe", + input, RetryStrategy.SAFE_TO_RETRY), outputClass); + } + /** + * Invokes the jobDescribe method with an empty input using the given environment, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param outputClass class to deserialize the server reponse to + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T jobDescribe(String objectId, Class outputClass, DXEnvironment env) { + return jobDescribe(objectId, mapper.createObjectNode(), outputClass, env); + } + /** + * Invokes the jobDescribe method with the given input using the given environment, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputObject input object (to be JSON serialized to an input hash) + * @param outputClass class to deserialize the server reponse to + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T jobDescribe(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { + JsonNode input = mapper.valueToTree(inputObject); + return DXJSON.safeTreeToValue( + new DXHTTPRequest(env).request("/" + objectId + "/" + "describe", + input, RetryStrategy.SAFE_TO_RETRY), outputClass); + } + + /** + * Invokes the jobDescribe method. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #jobDescribe(String, Class)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode jobDescribe(String objectId) { + return jobDescribe(objectId, mapper.createObjectNode()); + } + /** + * Invokes the jobDescribe method with the specified parameters. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputParams input parameters to the API call + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #jobDescribe(String, Object, Class)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode jobDescribe(String objectId, JsonNode inputParams) { + return new DXHTTPRequest().request("/" + objectId + "/" + "describe", inputParams, + RetryStrategy.SAFE_TO_RETRY); + } + /** + * Invokes the jobDescribe method with the specified environment. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #jobDescribe(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode jobDescribe(String objectId, DXEnvironment env) { + return jobDescribe(objectId, mapper.createObjectNode(), env); + } + /** + * Invokes the jobDescribe method with the specified environment and parameters. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputParams input parameters to the API call + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #jobDescribe(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode jobDescribe(String objectId, JsonNode inputParams, DXEnvironment env) { + return new DXHTTPRequest(env).request("/" + objectId + "/" + "describe", inputParams, + RetryStrategy.SAFE_TO_RETRY); + } + + /** + * Invokes the jobGetLog method with an empty input, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param outputClass class to deserialize the server reponse to + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T jobGetLog(String objectId, Class outputClass) { + return jobGetLog(objectId, mapper.createObjectNode(), outputClass); + } + /** + * Invokes the jobGetLog method with the given input, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputObject input object (to be JSON serialized to an input hash) + * @param outputClass class to deserialize the server reponse to + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T jobGetLog(String objectId, Object inputObject, Class outputClass) { + JsonNode input = mapper.valueToTree(inputObject); + return DXJSON.safeTreeToValue( + new DXHTTPRequest().request("/" + objectId + "/" + "getLog", + input, RetryStrategy.UNSAFE_TO_RETRY), outputClass); + } + /** + * Invokes the jobGetLog method with an empty input using the given environment, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param outputClass class to deserialize the server reponse to + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T jobGetLog(String objectId, Class outputClass, DXEnvironment env) { + return jobGetLog(objectId, mapper.createObjectNode(), outputClass, env); + } + /** + * Invokes the jobGetLog method with the given input using the given environment, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputObject input object (to be JSON serialized to an input hash) + * @param outputClass class to deserialize the server reponse to + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T jobGetLog(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { + JsonNode input = mapper.valueToTree(inputObject); + return DXJSON.safeTreeToValue( + new DXHTTPRequest(env).request("/" + objectId + "/" + "getLog", + input, RetryStrategy.UNSAFE_TO_RETRY), outputClass); + } + + /** + * Invokes the jobGetLog method. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #jobGetLog(String, Class)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode jobGetLog(String objectId) { + return jobGetLog(objectId, mapper.createObjectNode()); + } + /** + * Invokes the jobGetLog method with the specified parameters. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputParams input parameters to the API call + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #jobGetLog(String, Object, Class)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode jobGetLog(String objectId, JsonNode inputParams) { + return new DXHTTPRequest().request("/" + objectId + "/" + "getLog", inputParams, + RetryStrategy.UNSAFE_TO_RETRY); + } + /** + * Invokes the jobGetLog method with the specified environment. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #jobGetLog(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode jobGetLog(String objectId, DXEnvironment env) { + return jobGetLog(objectId, mapper.createObjectNode(), env); + } + /** + * Invokes the jobGetLog method with the specified environment and parameters. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputParams input parameters to the API call + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #jobGetLog(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode jobGetLog(String objectId, JsonNode inputParams, DXEnvironment env) { + return new DXHTTPRequest(env).request("/" + objectId + "/" + "getLog", inputParams, + RetryStrategy.UNSAFE_TO_RETRY); + } + + /** + * Invokes the jobRemoveTags method with an empty input, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param outputClass class to deserialize the server reponse to + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T jobRemoveTags(String objectId, Class outputClass) { + return jobRemoveTags(objectId, mapper.createObjectNode(), outputClass); + } + /** + * Invokes the jobRemoveTags method with the given input, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputObject input object (to be JSON serialized to an input hash) + * @param outputClass class to deserialize the server reponse to + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T jobRemoveTags(String objectId, Object inputObject, Class outputClass) { + JsonNode input = mapper.valueToTree(inputObject); + return DXJSON.safeTreeToValue( + new DXHTTPRequest().request("/" + objectId + "/" + "removeTags", + input, RetryStrategy.SAFE_TO_RETRY), outputClass); + } + /** + * Invokes the jobRemoveTags method with an empty input using the given environment, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param outputClass class to deserialize the server reponse to + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T jobRemoveTags(String objectId, Class outputClass, DXEnvironment env) { + return jobRemoveTags(objectId, mapper.createObjectNode(), outputClass, env); + } + /** + * Invokes the jobRemoveTags method with the given input using the given environment, deserializing to an object of the specified class. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * @param inputObject input object (to be JSON serialized to an input hash) + * @param outputClass class to deserialize the server reponse to + * @param env environment object specifying the auth token and remote server and protocol + * + * @return Response object + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + */ + public static T jobRemoveTags(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { + JsonNode input = mapper.valueToTree(inputObject); + return DXJSON.safeTreeToValue( + new DXHTTPRequest(env).request("/" + objectId + "/" + "removeTags", + input, RetryStrategy.SAFE_TO_RETRY), outputClass); + } + + /** + * Invokes the jobRemoveTags method. + * + *

For more information about this method, see the API specification. + * + * @param objectId ID of the object to operate on + * + * @return Server response parsed from JSON + * + * @throws DXAPIException + * If the server returns a complete response with an HTTP status + * code other than 200 (OK). + * @throws DXHTTPException + * If an error occurs while making the HTTP request or obtaining + * the response (includes HTTP protocol errors). + * + * @deprecated Use {@link #jobRemoveTags(String, Class)} instead and supply your own class to deserialize to. + */ + @Deprecated + public static JsonNode jobRemoveTags(String objectId) { + return jobRemoveTags(objectId, mapper.createObjectNode()); + } + /** + * Invokes the jobRemoveTags method with the specified parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -17562,17 +20527,17 @@ public static JsonNode jobDescribe(String objectId) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #jobDescribe(String, Object, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #jobRemoveTags(String, Object, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode jobDescribe(String objectId, JsonNode inputParams) { - return new DXHTTPRequest().request("/" + objectId + "/" + "describe", inputParams, + public static JsonNode jobRemoveTags(String objectId, JsonNode inputParams) { + return new DXHTTPRequest().request("/" + objectId + "/" + "removeTags", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the jobDescribe method with the specified environment. + * Invokes the jobRemoveTags method with the specified environment. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param env environment object specifying the auth token and remote server and protocol @@ -17586,16 +20551,16 @@ public static JsonNode jobDescribe(String objectId, JsonNode inputParams) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #jobDescribe(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #jobRemoveTags(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode jobDescribe(String objectId, DXEnvironment env) { - return jobDescribe(objectId, mapper.createObjectNode(), env); + public static JsonNode jobRemoveTags(String objectId, DXEnvironment env) { + return jobRemoveTags(objectId, mapper.createObjectNode(), env); } /** - * Invokes the jobDescribe method with the specified environment and parameters. + * Invokes the jobRemoveTags method with the specified environment and parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -17610,18 +20575,18 @@ public static JsonNode jobDescribe(String objectId, DXEnvironment env) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #jobDescribe(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #jobRemoveTags(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode jobDescribe(String objectId, JsonNode inputParams, DXEnvironment env) { - return new DXHTTPRequest(env).request("/" + objectId + "/" + "describe", inputParams, + public static JsonNode jobRemoveTags(String objectId, JsonNode inputParams, DXEnvironment env) { + return new DXHTTPRequest(env).request("/" + objectId + "/" + "removeTags", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the jobGetLog method with an empty input, deserializing to an object of the specified class. + * Invokes the jobSetProperties method with an empty input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -17635,13 +20600,13 @@ public static JsonNode jobDescribe(String objectId, JsonNode inputParams, DXEnvi * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T jobGetLog(String objectId, Class outputClass) { - return jobGetLog(objectId, mapper.createObjectNode(), outputClass); + public static T jobSetProperties(String objectId, Class outputClass) { + return jobSetProperties(objectId, mapper.createObjectNode(), outputClass); } /** - * Invokes the jobGetLog method with the given input, deserializing to an object of the specified class. + * Invokes the jobSetProperties method with the given input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -17656,16 +20621,16 @@ public static T jobGetLog(String objectId, Class outputClass) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T jobGetLog(String objectId, Object inputObject, Class outputClass) { + public static T jobSetProperties(String objectId, Object inputObject, Class outputClass) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest().request("/" + objectId + "/" + "getLog", - input, RetryStrategy.UNSAFE_TO_RETRY), outputClass); + new DXHTTPRequest().request("/" + objectId + "/" + "setProperties", + input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the jobGetLog method with an empty input using the given environment, deserializing to an object of the specified class. + * Invokes the jobSetProperties method with an empty input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -17680,13 +20645,13 @@ public static T jobGetLog(String objectId, Object inputObject, Class outp * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T jobGetLog(String objectId, Class outputClass, DXEnvironment env) { - return jobGetLog(objectId, mapper.createObjectNode(), outputClass, env); + public static T jobSetProperties(String objectId, Class outputClass, DXEnvironment env) { + return jobSetProperties(objectId, mapper.createObjectNode(), outputClass, env); } /** - * Invokes the jobGetLog method with the given input using the given environment, deserializing to an object of the specified class. + * Invokes the jobSetProperties method with the given input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -17702,17 +20667,17 @@ public static T jobGetLog(String objectId, Class outputClass, DXEnvironme * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T jobGetLog(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { + public static T jobSetProperties(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest(env).request("/" + objectId + "/" + "getLog", - input, RetryStrategy.UNSAFE_TO_RETRY), outputClass); + new DXHTTPRequest(env).request("/" + objectId + "/" + "setProperties", + input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the jobGetLog method. + * Invokes the jobSetProperties method. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @@ -17725,16 +20690,16 @@ public static T jobGetLog(String objectId, Object inputObject, Class outp * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #jobGetLog(String, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #jobSetProperties(String, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode jobGetLog(String objectId) { - return jobGetLog(objectId, mapper.createObjectNode()); + public static JsonNode jobSetProperties(String objectId) { + return jobSetProperties(objectId, mapper.createObjectNode()); } /** - * Invokes the jobGetLog method with the specified parameters. + * Invokes the jobSetProperties method with the specified parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -17748,17 +20713,17 @@ public static JsonNode jobGetLog(String objectId) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #jobGetLog(String, Object, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #jobSetProperties(String, Object, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode jobGetLog(String objectId, JsonNode inputParams) { - return new DXHTTPRequest().request("/" + objectId + "/" + "getLog", inputParams, - RetryStrategy.UNSAFE_TO_RETRY); + public static JsonNode jobSetProperties(String objectId, JsonNode inputParams) { + return new DXHTTPRequest().request("/" + objectId + "/" + "setProperties", inputParams, + RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the jobGetLog method with the specified environment. + * Invokes the jobSetProperties method with the specified environment. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param env environment object specifying the auth token and remote server and protocol @@ -17772,16 +20737,16 @@ public static JsonNode jobGetLog(String objectId, JsonNode inputParams) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #jobGetLog(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #jobSetProperties(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode jobGetLog(String objectId, DXEnvironment env) { - return jobGetLog(objectId, mapper.createObjectNode(), env); + public static JsonNode jobSetProperties(String objectId, DXEnvironment env) { + return jobSetProperties(objectId, mapper.createObjectNode(), env); } /** - * Invokes the jobGetLog method with the specified environment and parameters. + * Invokes the jobSetProperties method with the specified environment and parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -17796,18 +20761,18 @@ public static JsonNode jobGetLog(String objectId, DXEnvironment env) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #jobGetLog(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #jobSetProperties(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode jobGetLog(String objectId, JsonNode inputParams, DXEnvironment env) { - return new DXHTTPRequest(env).request("/" + objectId + "/" + "getLog", inputParams, - RetryStrategy.UNSAFE_TO_RETRY); + public static JsonNode jobSetProperties(String objectId, JsonNode inputParams, DXEnvironment env) { + return new DXHTTPRequest(env).request("/" + objectId + "/" + "setProperties", inputParams, + RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the jobRemoveTags method with an empty input, deserializing to an object of the specified class. + * Invokes the jobTerminate method with an empty input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -17821,13 +20786,13 @@ public static JsonNode jobGetLog(String objectId, JsonNode inputParams, DXEnviro * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T jobRemoveTags(String objectId, Class outputClass) { - return jobRemoveTags(objectId, mapper.createObjectNode(), outputClass); + public static T jobTerminate(String objectId, Class outputClass) { + return jobTerminate(objectId, mapper.createObjectNode(), outputClass); } /** - * Invokes the jobRemoveTags method with the given input, deserializing to an object of the specified class. + * Invokes the jobTerminate method with the given input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -17842,16 +20807,16 @@ public static T jobRemoveTags(String objectId, Class outputClass) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T jobRemoveTags(String objectId, Object inputObject, Class outputClass) { + public static T jobTerminate(String objectId, Object inputObject, Class outputClass) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest().request("/" + objectId + "/" + "removeTags", + new DXHTTPRequest().request("/" + objectId + "/" + "terminate", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the jobRemoveTags method with an empty input using the given environment, deserializing to an object of the specified class. + * Invokes the jobTerminate method with an empty input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -17866,13 +20831,13 @@ public static T jobRemoveTags(String objectId, Object inputObject, Class * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T jobRemoveTags(String objectId, Class outputClass, DXEnvironment env) { - return jobRemoveTags(objectId, mapper.createObjectNode(), outputClass, env); + public static T jobTerminate(String objectId, Class outputClass, DXEnvironment env) { + return jobTerminate(objectId, mapper.createObjectNode(), outputClass, env); } /** - * Invokes the jobRemoveTags method with the given input using the given environment, deserializing to an object of the specified class. + * Invokes the jobTerminate method with the given input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -17888,17 +20853,17 @@ public static T jobRemoveTags(String objectId, Class outputClass, DXEnvir * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T jobRemoveTags(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { + public static T jobTerminate(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest(env).request("/" + objectId + "/" + "removeTags", + new DXHTTPRequest(env).request("/" + objectId + "/" + "terminate", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the jobRemoveTags method. + * Invokes the jobTerminate method. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @@ -17911,16 +20876,16 @@ public static T jobRemoveTags(String objectId, Object inputObject, Class * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #jobRemoveTags(String, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #jobTerminate(String, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode jobRemoveTags(String objectId) { - return jobRemoveTags(objectId, mapper.createObjectNode()); + public static JsonNode jobTerminate(String objectId) { + return jobTerminate(objectId, mapper.createObjectNode()); } /** - * Invokes the jobRemoveTags method with the specified parameters. + * Invokes the jobTerminate method with the specified parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -17934,17 +20899,17 @@ public static JsonNode jobRemoveTags(String objectId) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #jobRemoveTags(String, Object, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #jobTerminate(String, Object, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode jobRemoveTags(String objectId, JsonNode inputParams) { - return new DXHTTPRequest().request("/" + objectId + "/" + "removeTags", inputParams, + public static JsonNode jobTerminate(String objectId, JsonNode inputParams) { + return new DXHTTPRequest().request("/" + objectId + "/" + "terminate", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the jobRemoveTags method with the specified environment. + * Invokes the jobTerminate method with the specified environment. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param env environment object specifying the auth token and remote server and protocol @@ -17958,16 +20923,16 @@ public static JsonNode jobRemoveTags(String objectId, JsonNode inputParams) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #jobRemoveTags(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #jobTerminate(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode jobRemoveTags(String objectId, DXEnvironment env) { - return jobRemoveTags(objectId, mapper.createObjectNode(), env); + public static JsonNode jobTerminate(String objectId, DXEnvironment env) { + return jobTerminate(objectId, mapper.createObjectNode(), env); } /** - * Invokes the jobRemoveTags method with the specified environment and parameters. + * Invokes the jobTerminate method with the specified environment and parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -17982,18 +20947,18 @@ public static JsonNode jobRemoveTags(String objectId, DXEnvironment env) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #jobRemoveTags(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #jobTerminate(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode jobRemoveTags(String objectId, JsonNode inputParams, DXEnvironment env) { - return new DXHTTPRequest(env).request("/" + objectId + "/" + "removeTags", inputParams, + public static JsonNode jobTerminate(String objectId, JsonNode inputParams, DXEnvironment env) { + return new DXHTTPRequest(env).request("/" + objectId + "/" + "terminate", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the jobSetProperties method with an empty input, deserializing to an object of the specified class. + * Invokes the jobUpdate method with an empty input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -18007,13 +20972,13 @@ public static JsonNode jobRemoveTags(String objectId, JsonNode inputParams, DXEn * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T jobSetProperties(String objectId, Class outputClass) { - return jobSetProperties(objectId, mapper.createObjectNode(), outputClass); + public static T jobUpdate(String objectId, Class outputClass) { + return jobUpdate(objectId, mapper.createObjectNode(), outputClass); } /** - * Invokes the jobSetProperties method with the given input, deserializing to an object of the specified class. + * Invokes the jobUpdate method with the given input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -18028,16 +20993,16 @@ public static T jobSetProperties(String objectId, Class outputClass) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T jobSetProperties(String objectId, Object inputObject, Class outputClass) { + public static T jobUpdate(String objectId, Object inputObject, Class outputClass) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest().request("/" + objectId + "/" + "setProperties", + new DXHTTPRequest().request("/" + objectId + "/" + "update", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the jobSetProperties method with an empty input using the given environment, deserializing to an object of the specified class. + * Invokes the jobUpdate method with an empty input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -18052,13 +21017,13 @@ public static T jobSetProperties(String objectId, Object inputObject, Class< * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T jobSetProperties(String objectId, Class outputClass, DXEnvironment env) { - return jobSetProperties(objectId, mapper.createObjectNode(), outputClass, env); + public static T jobUpdate(String objectId, Class outputClass, DXEnvironment env) { + return jobUpdate(objectId, mapper.createObjectNode(), outputClass, env); } /** - * Invokes the jobSetProperties method with the given input using the given environment, deserializing to an object of the specified class. + * Invokes the jobUpdate method with the given input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -18074,17 +21039,17 @@ public static T jobSetProperties(String objectId, Class outputClass, DXEn * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T jobSetProperties(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { + public static T jobUpdate(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest(env).request("/" + objectId + "/" + "setProperties", + new DXHTTPRequest(env).request("/" + objectId + "/" + "update", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the jobSetProperties method. + * Invokes the jobUpdate method. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @@ -18097,16 +21062,16 @@ public static T jobSetProperties(String objectId, Object inputObject, Class< * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #jobSetProperties(String, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #jobUpdate(String, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode jobSetProperties(String objectId) { - return jobSetProperties(objectId, mapper.createObjectNode()); + public static JsonNode jobUpdate(String objectId) { + return jobUpdate(objectId, mapper.createObjectNode()); } /** - * Invokes the jobSetProperties method with the specified parameters. + * Invokes the jobUpdate method with the specified parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -18120,17 +21085,17 @@ public static JsonNode jobSetProperties(String objectId) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #jobSetProperties(String, Object, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #jobUpdate(String, Object, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode jobSetProperties(String objectId, JsonNode inputParams) { - return new DXHTTPRequest().request("/" + objectId + "/" + "setProperties", inputParams, + public static JsonNode jobUpdate(String objectId, JsonNode inputParams) { + return new DXHTTPRequest().request("/" + objectId + "/" + "update", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the jobSetProperties method with the specified environment. + * Invokes the jobUpdate method with the specified environment. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param env environment object specifying the auth token and remote server and protocol @@ -18144,16 +21109,16 @@ public static JsonNode jobSetProperties(String objectId, JsonNode inputParams) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #jobSetProperties(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #jobUpdate(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode jobSetProperties(String objectId, DXEnvironment env) { - return jobSetProperties(objectId, mapper.createObjectNode(), env); + public static JsonNode jobUpdate(String objectId, DXEnvironment env) { + return jobUpdate(objectId, mapper.createObjectNode(), env); } /** - * Invokes the jobSetProperties method with the specified environment and parameters. + * Invokes the jobUpdate method with the specified environment and parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -18168,18 +21133,18 @@ public static JsonNode jobSetProperties(String objectId, DXEnvironment env) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #jobSetProperties(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #jobUpdate(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode jobSetProperties(String objectId, JsonNode inputParams, DXEnvironment env) { - return new DXHTTPRequest(env).request("/" + objectId + "/" + "setProperties", inputParams, + public static JsonNode jobUpdate(String objectId, JsonNode inputParams, DXEnvironment env) { + return new DXHTTPRequest(env).request("/" + objectId + "/" + "update", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the jobTerminate method with an empty input, deserializing to an object of the specified class. + * Invokes the jobGetIdentityToken method with an empty input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -18193,13 +21158,13 @@ public static JsonNode jobSetProperties(String objectId, JsonNode inputParams, D * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T jobTerminate(String objectId, Class outputClass) { - return jobTerminate(objectId, mapper.createObjectNode(), outputClass); + public static T jobGetIdentityToken(String objectId, Class outputClass) { + return jobGetIdentityToken(objectId, mapper.createObjectNode(), outputClass); } /** - * Invokes the jobTerminate method with the given input, deserializing to an object of the specified class. + * Invokes the jobGetIdentityToken method with the given input, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -18214,16 +21179,16 @@ public static T jobTerminate(String objectId, Class outputClass) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T jobTerminate(String objectId, Object inputObject, Class outputClass) { + public static T jobGetIdentityToken(String objectId, Object inputObject, Class outputClass) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest().request("/" + objectId + "/" + "terminate", + new DXHTTPRequest().request("/" + objectId + "/" + "getIdentityToken", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the jobTerminate method with an empty input using the given environment, deserializing to an object of the specified class. + * Invokes the jobGetIdentityToken method with an empty input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param outputClass class to deserialize the server reponse to @@ -18238,13 +21203,13 @@ public static T jobTerminate(String objectId, Object inputObject, Class o * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T jobTerminate(String objectId, Class outputClass, DXEnvironment env) { - return jobTerminate(objectId, mapper.createObjectNode(), outputClass, env); + public static T jobGetIdentityToken(String objectId, Class outputClass, DXEnvironment env) { + return jobGetIdentityToken(objectId, mapper.createObjectNode(), outputClass, env); } /** - * Invokes the jobTerminate method with the given input using the given environment, deserializing to an object of the specified class. + * Invokes the jobGetIdentityToken method with the given input using the given environment, deserializing to an object of the specified class. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputObject input object (to be JSON serialized to an input hash) @@ -18260,17 +21225,17 @@ public static T jobTerminate(String objectId, Class outputClass, DXEnviro * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). */ - public static T jobTerminate(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { + public static T jobGetIdentityToken(String objectId, Object inputObject, Class outputClass, DXEnvironment env) { JsonNode input = mapper.valueToTree(inputObject); return DXJSON.safeTreeToValue( - new DXHTTPRequest(env).request("/" + objectId + "/" + "terminate", + new DXHTTPRequest(env).request("/" + objectId + "/" + "getIdentityToken", input, RetryStrategy.SAFE_TO_RETRY), outputClass); } /** - * Invokes the jobTerminate method. + * Invokes the jobGetIdentityToken method. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @@ -18283,16 +21248,16 @@ public static T jobTerminate(String objectId, Object inputObject, Class o * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #jobTerminate(String, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #jobGetIdentityToken(String, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode jobTerminate(String objectId) { - return jobTerminate(objectId, mapper.createObjectNode()); + public static JsonNode jobGetIdentityToken(String objectId) { + return jobGetIdentityToken(objectId, mapper.createObjectNode()); } /** - * Invokes the jobTerminate method with the specified parameters. + * Invokes the jobGetIdentityToken method with the specified parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -18306,17 +21271,17 @@ public static JsonNode jobTerminate(String objectId) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #jobTerminate(String, Object, Class)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #jobGetIdentityToken(String, Object, Class)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode jobTerminate(String objectId, JsonNode inputParams) { - return new DXHTTPRequest().request("/" + objectId + "/" + "terminate", inputParams, + public static JsonNode jobGetIdentityToken(String objectId, JsonNode inputParams) { + return new DXHTTPRequest().request("/" + objectId + "/" + "getIdentityToken", inputParams, RetryStrategy.SAFE_TO_RETRY); } /** - * Invokes the jobTerminate method with the specified environment. + * Invokes the jobGetIdentityToken method with the specified environment. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param env environment object specifying the auth token and remote server and protocol @@ -18330,16 +21295,16 @@ public static JsonNode jobTerminate(String objectId, JsonNode inputParams) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #jobTerminate(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #jobGetIdentityToken(String, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode jobTerminate(String objectId, DXEnvironment env) { - return jobTerminate(objectId, mapper.createObjectNode(), env); + public static JsonNode jobGetIdentityToken(String objectId, DXEnvironment env) { + return jobGetIdentityToken(objectId, mapper.createObjectNode(), env); } /** - * Invokes the jobTerminate method with the specified environment and parameters. + * Invokes the jobGetIdentityToken method with the specified environment and parameters. * - *

For more information about this method, see the API specification. + *

For more information about this method, see the API specification. * * @param objectId ID of the object to operate on * @param inputParams input parameters to the API call @@ -18354,11 +21319,11 @@ public static JsonNode jobTerminate(String objectId, DXEnvironment env) { * If an error occurs while making the HTTP request or obtaining * the response (includes HTTP protocol errors). * - * @deprecated Use {@link #jobTerminate(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. + * @deprecated Use {@link #jobGetIdentityToken(String, Object, Class, DXEnvironment)} instead and supply your own class to deserialize to. */ @Deprecated - public static JsonNode jobTerminate(String objectId, JsonNode inputParams, DXEnvironment env) { - return new DXHTTPRequest(env).request("/" + objectId + "/" + "terminate", inputParams, + public static JsonNode jobGetIdentityToken(String objectId, JsonNode inputParams, DXEnvironment env) { + return new DXHTTPRequest(env).request("/" + objectId + "/" + "getIdentityToken", inputParams, RetryStrategy.SAFE_TO_RETRY); } diff --git a/src/java/src/main/java/com/dnanexus/DXEnvironment.java b/src/java/src/main/java/com/dnanexus/DXEnvironment.java index 50d528006a..fbe33103e2 100644 --- a/src/java/src/main/java/com/dnanexus/DXEnvironment.java +++ b/src/java/src/main/java/com/dnanexus/DXEnvironment.java @@ -31,8 +31,8 @@ import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.databind.JsonNode; @@ -67,7 +67,7 @@ public static class Builder { /** * Creates a Builder object using the JSON config in the file - * ~/.dnanexus_config/environment.json. + * ~/.dnanexus_config/environment.json. * * @return new Builder object */ @@ -115,7 +115,7 @@ public static Builder fromFile(File environmentJsonFile) { /** * Initializes a Builder object using JSON config in the file - * ~/.dnanexus_config/environment.json. + * ~/.dnanexus_config/environment.json. * * @deprecated Use {@link #fromDefaults()} instead */ @@ -387,8 +387,6 @@ public Builder setWorkspace(DXContainer workspace) { /** * Disables automatic retry of HTTP requests. * - * @param disableRetryLogic boolean - * * @return the same Builder object */ public Builder disableRetry() { @@ -766,7 +764,7 @@ public void close() throws IOException { httpclient.close(); } - private static final Logger LOG = LoggerFactory.getLogger(DXEnvironment.class); + private static final Logger LOG = LogManager.getLogger(DXEnvironment.class); private static boolean isDebug() { return LOG.isDebugEnabled(); diff --git a/src/java/src/main/java/com/dnanexus/DXFile.java b/src/java/src/main/java/com/dnanexus/DXFile.java index d93e0c85c0..eed96a6a82 100644 --- a/src/java/src/main/java/com/dnanexus/DXFile.java +++ b/src/java/src/main/java/com/dnanexus/DXFile.java @@ -40,7 +40,6 @@ import org.apache.http.client.methods.HttpPut; import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.entity.ByteArrayEntity; -import org.apache.http.impl.client.HttpClientBuilder; import com.dnanexus.DXHTTPRequest.RetryStrategy; import com.fasterxml.jackson.annotation.JsonCreator; @@ -176,6 +175,8 @@ private static class PartValue { private Map parts; @JsonProperty private Long size; + @JsonProperty + private ArchivalState archivalState; private Describe() { super(); @@ -247,6 +248,17 @@ public long getSize() { "file size is not accessible because it was not retrieved with the describe call"); return size; } + + /** + * Returns the archival state of the file. + * + * @return archival state of file + */ + public ArchivalState getArchivalState() { + Preconditions.checkState(this.archivalState != null, + "archival state is not accessible because it was not retrieved with the describe call"); + return archivalState; + } } private class FileApiInputStream extends InputStream { @@ -276,10 +288,12 @@ private class FileApiInputStream extends InputStream { private final long readStart; // Counter used for ramping private int request = 1; + // File size + private final long fileSize; private FileApiInputStream(long readStart, long readEnd, PartDownloader downloader) { // API call returns URL and headers for HTTP GET requests - JsonNode output = apiCallOnObject("download", MAPPER.valueToTree(new FileDownloadRequest(true)), + JsonNode output = apiCallOnObject("download", MAPPER.valueToTree(new FileDownloadRequest(true, getProjectReference().getId())), RetryStrategy.SAFE_TO_RETRY); try { apiResponse = MAPPER.treeToValue(output, FileDownloadResponse.class); @@ -287,13 +301,15 @@ private FileApiInputStream(long readStart, long readEnd, PartDownloader download throw new RuntimeException(e); } - partsMetadata = describe(DXDataObject.DescribeOptions.get().withCustomFields("parts")); + partsMetadata = describe(DXDataObject.DescribeOptions.get().withCustomFields("parts", "size")); + // Get file size to avoid additional describe calls + fileSize = partsMetadata.getSize(); // Get a sorted list of file parts fileParts = partsMetadata.getFilePartsList(); if (readEnd == -1) { - readEnd = describe().getSize(); + readEnd = fileSize; } Preconditions.checkArgument(readEnd >= readStart, "The start byte cannot be larger than the end byte"); this.readStart = readStart; @@ -372,7 +388,7 @@ public int read(byte[] b, int off, int numBytes) throws IOException { long chunkSize = getNextChunkSize(); // API request to download bytes - long endRange = Math.min(nextByteFromApi + chunkSize, describe().getSize()); + long endRange = Math.min(nextByteFromApi + chunkSize, fileSize); byte[] bytesFromApiCall = this.downloader.get(apiResponse.url, nextByteFromApi, endRange - 1); // Stream of bytes retrieved from the API call pre-checksum @@ -497,9 +513,12 @@ public void write(int b) throws IOException { private static class FileDownloadRequest { @JsonProperty("preauthenticated") private boolean preauth; + @JsonProperty("project") + private String project; - private FileDownloadRequest(boolean preauth) { + private FileDownloadRequest(boolean preauth, String project) { this.preauth = preauth; + this.project = project; } } @@ -748,6 +767,14 @@ private static void sleep(int seconds) { } } + /* + Project or container reference in case it was not provided during initialisation. + + Necessary to decrease workload of API server for file-xxx/describe and file-xxx/download calls which are expensive + when project/container ID is not provided in a request. + */ + private DXContainer localProjectReference; + // Variables for download private final int maxDownloadChunkSize = 16 * 1024 * 1024; private final int minDownloadChunkSize = 64 * 1024; @@ -769,6 +796,19 @@ private DXFile(String fileId, DXEnvironment env) { super(fileId, "file", env, null); } + private DXContainer getProjectReference() { + if (getProject() != null) { + return getProject(); + } + + if (localProjectReference == null) { + DescribeOptions options = DXDataObject.DescribeOptions.get().withCustomFields("project"); + this.localProjectReference = DXJSON.safeTreeToValue(apiCallOnObject("describe", MAPPER.valueToTree(options), RetryStrategy.SAFE_TO_RETRY), Describe.class).getProject(); + } + + return this.localProjectReference; + } + @Override public DXFile close() { super.close(); @@ -783,11 +823,13 @@ public DXFile closeAndWait() { @Override public Describe describe() { - return DXJSON.safeTreeToValue(apiCallOnObject("describe", RetryStrategy.SAFE_TO_RETRY), Describe.class); + DescribeOptions options = DXDataObject.DescribeOptions.get().inProject(getProjectReference()); + return DXJSON.safeTreeToValue(apiCallOnObject("describe", MAPPER.valueToTree(options), RetryStrategy.SAFE_TO_RETRY), Describe.class); } @Override public Describe describe(DXDataObject.DescribeOptions options) { + options = options.inProject(getProjectReference()); return DXJSON.safeTreeToValue( apiCallOnObject("describe", MAPPER.valueToTree(options), RetryStrategy.SAFE_TO_RETRY), Describe.class); } diff --git a/src/java/src/main/java/com/dnanexus/DXJSON.java b/src/java/src/main/java/com/dnanexus/DXJSON.java index a0333fc401..287dfb85d4 100644 --- a/src/java/src/main/java/com/dnanexus/DXJSON.java +++ b/src/java/src/main/java/com/dnanexus/DXJSON.java @@ -65,7 +65,7 @@ public static JsonNode parseJson(String stringified) throws IOException { * .addAllStrings(ImmutableList.of("Bar", "Baz")) * .build()} * - * when serialized, produces the JSON array ["Foo", "Bar", "Baz"]. + * when serialized, produces the JSON array ["Foo", "Bar", "Baz"]. */ public static class ArrayBuilder { private final boolean isEmpty; @@ -165,7 +165,7 @@ public ArrayNode build() { * .put("key2", 12321) * .build()} * - * when serialized, produces the JSON object {"key1": "a-string", "key2": 12321}. + * when serialized, produces the JSON object {"key1": "a-string", "key2": 12321}. */ public static class ObjectBuilder { private final boolean isEmpty; diff --git a/src/java/src/main/java/com/dnanexus/DXProject.java b/src/java/src/main/java/com/dnanexus/DXProject.java index 6c7df49d67..7329d2981a 100644 --- a/src/java/src/main/java/com/dnanexus/DXProject.java +++ b/src/java/src/main/java/com/dnanexus/DXProject.java @@ -24,6 +24,10 @@ import com.fasterxml.jackson.databind.JsonNode; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; +import com.google.common.collect.Lists; + +import java.util.Collection; +import java.util.List; /** * A project (a container providing features for data sharing and collaboration). @@ -219,6 +223,458 @@ public void destroy(boolean terminateJobs) { RetryStrategy.SAFE_TO_RETRY); } + /** + * Request specified files or folder to be archived. + * + *

+ * Example use: + *

+ * + *
+     * // Archive using file id
+     * DXFile f1 = DXFile.getInstance("file-xxxx");
+     * DXFile f2 = DXFile.getInstance("file-yyyy");
+     * ArchiveResults r = project.archive().addFiles(f1, f2).execute();
+     * // Archive folder
+     * ArchiveResults r = project.archive().setFolder("/folder", true).execute();
+     * 
+ * + * @return a newly initialized {@code ArchiveRequestBuilder} + */ + public ArchiveRequestBuilder archive() { + return new ArchiveRequestBuilder(); + } + + /** + * Request specified files or folder to be unarchived. + * + *

+ * Example use: + *

+ * + *
+     * // Unarchive using file id
+     * DXFile f1 = DXFile.getInstance("file-xxxx");
+     * DXFile f2 = DXFile.getInstance("file-yyyy");
+     * UnarchiveResults r = project.unarchive().addFiles(f1, f2).execute();
+     * // Unarchive folder
+     * UnarchiveResults r = project.unarchive().setFolder("/folder", true).execute();
+     * 
+ * + * @return a newly initialized {@code UnarchiveRequestBuilder} + */ + public UnarchiveRequestBuilder unarchive() { + return new UnarchiveRequestBuilder(); + } + + @JsonInclude(Include.NON_NULL) + private static class ArchiveRequest { + @JsonProperty + private final List files; + @JsonProperty + private final String folder; + @JsonProperty + private final Boolean recurse; + @JsonProperty + private final Boolean allCopies; + + public ArchiveRequest(ArchiveRequestBuilder b) { + this.files = b.files.isEmpty() ? null : b.files; + this.folder = b.folder; + this.recurse = b.recurse; + this.allCopies = b.allCopies; + } + + } + + /** + * Builder for archive requests. + */ + public class ArchiveRequestBuilder { + private static final String FILES_LIST_NULL_ERR = "files collection may not be null"; + private static final String FILES_FOLDER_NONEXCLUSIVE_ERR = "Files and folder params are mutually exclusive"; + private final List files = Lists.newArrayList(); + private String folder; + private Boolean recurse; + private Boolean allCopies; + + /** + * Adds the file to the list of files for archival. + * + *

+ * This method may be called multiple times during the construction of a request, and is mutually exclusive + * with {@link #setFolder(String)} and {@link #setFolder(String, Boolean)}. + *

+ * + * @param file {@code DXFile} instance to be archived + * + * @return the same builder object + */ + public ArchiveRequestBuilder addFile(DXFile file) { + Preconditions.checkState(this.folder == null, FILES_FOLDER_NONEXCLUSIVE_ERR); + files.add(Preconditions.checkNotNull( + Preconditions.checkNotNull(file, "file may not be null").getId(), + "file id may not be null")); + return this; + } + + /** + * Adds the files to the list of files for archival. + * + *

+ * This method may be called multiple times during the construction of a request, and is mutually exclusive + * with {@link #setFolder(String)} and {@link #setFolder(String, Boolean)}. + *

+ * + * @param files list of {@code DXFile} instances to be archived + * + * @return the same builder object + */ + public ArchiveRequestBuilder addFiles(DXFile... files) { + Preconditions.checkNotNull(files, FILES_LIST_NULL_ERR); + return addFiles(Lists.newArrayList(files)); + } + + /** + * Adds the files to the list of files for archival. + * + *

+ * This method may be called multiple times during the construction of a request, and is mutually exclusive + * with {@link #setFolder(String)} and {@link #setFolder(String, Boolean)}. + *

+ * + * @param files collection of {@code DXFile} instances to be archived + * + * @return the same builder object + */ + public ArchiveRequestBuilder addFiles(Collection files) { + Preconditions.checkNotNull(files, FILES_LIST_NULL_ERR); + for (DXFile file : files) { + addFile(file); + } + return this; + } + + /** + * Sets folder for archival. + * + *

+ * This method may only be called once during the construction of a request, and is mutually exclusive with + * {@link #addFile(DXFile)}, {@link #addFiles(DXFile...)}, and {@link #addFiles(Collection)}. + *

+ * + * @param folder path to folder to be archived + * + * @return the same builder object + */ + public ArchiveRequestBuilder setFolder(String folder) { + return setFolder(folder, null); + } + + /** + * Sets folder for archival. + * + *

+ * This method may only be called once during the construction of a request, and is mutually exclusive with + * {@link #addFile(DXFile)}, {@link #addFiles(DXFile...)}, and {@link #addFiles(Collection)}. + *

+ * + * @param folder path to folder to be archived + * @param recurse whether to archive all files in subfolders of {@code folder} + * + * @return the same builder object + */ + public ArchiveRequestBuilder setFolder(String folder, Boolean recurse) { + Preconditions.checkState(this.files.isEmpty(), FILES_FOLDER_NONEXCLUSIVE_ERR); + Preconditions.checkState(this.folder == null, "Cannot call setFolder more than once"); + this.folder = Preconditions.checkNotNull(folder, "folder may not be null"); + this.recurse = recurse; + return this; + } + + /** + * Sets flag to enforce the transition of files into {@link ArchivalState#ARCHIVED} state. If true, archive all + * the copies of files in projects with the same {@code billTo} org. If false, archive only the copy of the file + * in the current project, while other copies of the file in the rest projects with the same {@code billTo} org + * will stay in the live state. + * + * @param allCopies whether to enforce archival of all copies of files within the same {@code billTo} org + * + * @return the same builder object + */ + public ArchiveRequestBuilder setAllCopies(Boolean allCopies) { + Preconditions.checkState(this.allCopies == null, + "Cannot call setAllCopies more than once"); + this.allCopies = allCopies; + return this; + } + + /** + * Executes the request. + * + * @return execution results + */ + public ArchiveResults execute() { + return DXJSON.safeTreeToValue(apiCallOnObject("archive", + MAPPER.valueToTree(new ArchiveRequest(this)), + RetryStrategy.SAFE_TO_RETRY), ArchiveResults.class); + } + + @VisibleForTesting + JsonNode buildRequestHash() { + // Use this method to test the JSON hash created by a particular + // builder call without actually executing the request. + return MAPPER.valueToTree(new ArchiveRequest(this)); + } + + } + + /** + * Results of archive request. + */ + @JsonIgnoreProperties(ignoreUnknown = true) + public static class ArchiveResults { + @JsonProperty + private int count; + + /** + * Returns number of files tagged for archival. + * + * @return number of files tagged for archival + */ + public int getCount() { + return count; + } + + } + + @JsonInclude(Include.NON_NULL) + private static class UnarchiveRequest { + @JsonProperty + private final List files; + @JsonProperty + private final String folder; + @JsonProperty + private final Boolean recurse; + @JsonProperty + private final UnarchivingRate rate; + @JsonProperty + private final Boolean dryRun; + + public UnarchiveRequest(UnarchiveRequestBuilder b) { + this.files = b.files.isEmpty() ? null : b.files; + this.folder = b.folder; + this.recurse = b.recurse; + this.rate = b.rate; + this.dryRun = b.dryRun; + } + + } + + /** + * Builder for unarchive requests. + */ + public class UnarchiveRequestBuilder { + private static final String FILES_LIST_NULL_ERR = "files collection may not be null"; + private static final String FILES_FOLDER_NONEXCLUSIVE_ERR = "Files and folder params are mutually exclusive"; + private final List files = Lists.newArrayList(); + private String folder; + private Boolean recurse; + private UnarchivingRate rate; + private Boolean dryRun; + + /** + * Adds the file to the list of files for unarchiving. + * + *

+ * This method may be called multiple times during the construction of a request, and is mutually exclusive + * with {@link #setFolder(String)} and {@link #setFolder(String, Boolean)}. + *

+ * + * @param file {@code DXFile} instance to be unarchived + * + * @return the same builder object + */ + public UnarchiveRequestBuilder addFile(DXFile file) { + Preconditions.checkState(this.folder == null, FILES_FOLDER_NONEXCLUSIVE_ERR); + files.add(Preconditions.checkNotNull( + Preconditions.checkNotNull(file, "file may not be null").getId(), + "file id may not be null")); + return this; + } + + /** + * Adds the files to the list of files for unarchiving. + * + *

+ * This method may be called multiple times during the construction of a request, and is mutually exclusive + * with {@link #setFolder(String)} and {@link #setFolder(String, Boolean)}. + *

+ * + * @param files list of {@code DXFile} instances to be unarchived + * + * @return the same builder object + */ + public UnarchiveRequestBuilder addFiles(DXFile... files) { + Preconditions.checkNotNull(files, FILES_LIST_NULL_ERR); + return addFiles(Lists.newArrayList(files)); + } + + /** + * Adds the files to the list of files for unarchiving. + * + *

+ * This method may be called multiple times during the construction of a request, and is mutually exclusive + * with {@link #setFolder(String)} and {@link #setFolder(String, Boolean)}. + *

+ * + * @param files collection of {@code DXFile} instances to be unarchived + * + * @return the same builder object + */ + public UnarchiveRequestBuilder addFiles(Collection files) { + Preconditions.checkNotNull(files, FILES_LIST_NULL_ERR); + for (DXFile file : files) { + addFile(file); + } + return this; + } + + /** + * Sets folder for unarchiving. + * + *

+ * This method may only be called once during the construction of a request, and is mutually exclusive with + * {@link #addFile(DXFile)}, {@link #addFiles(DXFile...)}, and {@link #addFiles(Collection)}. + *

+ * + * @param folder path to folder to be unarchived + * + * @return the same builder object + */ + public UnarchiveRequestBuilder setFolder(String folder) { + return setFolder(folder, null); + } + + /** + * Sets folder for unarchiving. + * + *

+ * This method may only be called once during the construction of a request, and is mutually exclusive with + * {@link #addFile(DXFile)}, {@link #addFiles(DXFile...)}, and {@link #addFiles(Collection)}. + *

+ * + * @param folder path to folder to be unarchived + * @param recurse whether to unarchive all files in subfolders of {@code folder} + * + * @return the same builder object + */ + public UnarchiveRequestBuilder setFolder(String folder, Boolean recurse) { + Preconditions.checkState(this.files.isEmpty(), FILES_FOLDER_NONEXCLUSIVE_ERR); + Preconditions.checkState(this.folder == null, "Cannot call setFolder more than once"); + this.folder = Preconditions.checkNotNull(folder, "folder may not be null"); + this.recurse = recurse; + return this; + } + + /** + * Sets the speed at which the files in this request are unarchived. + * + *

+ * Valid only for AWS. + *

+ * + * @param rate speed of unarchiving + * + * @return the same builder object + */ + public UnarchiveRequestBuilder setRate(UnarchivingRate rate) { + Preconditions.checkState(this.rate == null, + "Cannot call setRate more than once"); + this.rate = rate; + return this; + } + + /** + * Sets dry-run mode. If true, only display the output of the API call without executing + * the unarchival process. + * + * @param dryRun whether the unarchival process should be actually executed or not + * + * @return the same builder object + */ + + public UnarchiveRequestBuilder setDryRun(Boolean dryRun) { + Preconditions.checkState(this.dryRun == null, + "Cannot call setDryRun more than once"); + this.dryRun = dryRun; + return this; + } + + /** + * Executes the request. + * + * @return execution results + */ + public UnarchiveResults execute() { + return DXJSON.safeTreeToValue(apiCallOnObject("unarchive", + MAPPER.valueToTree(new UnarchiveRequest(this)), + RetryStrategy.SAFE_TO_RETRY), UnarchiveResults.class); + } + + @VisibleForTesting + JsonNode buildRequestHash() { + // Use this method to test the JSON hash created by a particular + // builder call without actually executing the request. + return MAPPER.valueToTree(new UnarchiveRequest(this)); + } + + } + + /** + * Results of unarchive request. + */ + @JsonIgnoreProperties(ignoreUnknown = true) + public static class UnarchiveResults { + @JsonProperty + private int files; + @JsonProperty + private int size; + @JsonProperty + private float cost; + + protected UnarchiveResults() { + } + + /** + * Returns the number of files that will be unarchived. + * + * @return number of files that will be unarchived + */ + public int getFiles() { + return files; + } + + /** + * Returns the size of the data (in GB) that will be unarchived. + * + * @return size of the data that will be unarchived + */ + public int getSize() { + return size; + } + + /** + * Returns total cost (in millidollars) that will be charged for the unarchival request. + * + * @return total cost that will be charged for the unarchival request + */ + public float getCost() { + return cost; + } + + } + // The following unimplemented methods are sorted in approximately // decreasing order of usefulness to Java clients. diff --git a/src/java/src/main/java/com/dnanexus/DXSearch.java b/src/java/src/main/java/com/dnanexus/DXSearch.java index d638aa71c5..0882063e72 100644 --- a/src/java/src/main/java/com/dnanexus/DXSearch.java +++ b/src/java/src/main/java/com/dnanexus/DXSearch.java @@ -125,6 +125,8 @@ private ScopeQuery(String projectId, String folder, Boolean recurse) { private final TimeIntervalQuery modified; @JsonProperty private final TimeIntervalQuery created; + @JsonProperty + private final ArchivalState archivalState; @JsonProperty private final DescribeParameters describe; @@ -159,6 +161,7 @@ private FindDataObjectsRequest(FindDataObjectsRequest previousQuery, JsonNode st this.level = previousQuery.level; this.modified = previousQuery.modified; this.created = previousQuery.created; + this.archivalState = previousQuery.archivalState; this.describe = previousQuery.describe; this.starting = (starting == null || starting.isNull()) ? null : starting; @@ -197,6 +200,7 @@ private FindDataObjectsRequest(FindDataObjectsRequestBuilder builder, JsonNod this.scope = builder.scopeQuery; this.sortBy = builder.sortByQuery; this.level = builder.level; + this.archivalState = builder.archivalState; if (builder.modifiedBefore != null || builder.modifiedAfter != null) { this.modified = @@ -243,6 +247,7 @@ public static class FindDataObjectsRequestBuilder { private Date createdBefore; private Date createdAfter; private DescribeParameters describe; + private ArchivalState archivalState; private final DXEnvironment env; @@ -579,6 +584,28 @@ public FindDataObjectsRequestBuilder nameMatchesRegexp(String regexp, return this; } + /** + * Only returns files with the specified archival state. If not + * specified, the default is to return all objects regardless their archival state. + * + *

+ * Please note archival states are supported only for "file" class (use {@link #withClassFile()}). This filter + * requires project and folder (can be root "/") to be specified (use either {@link #inFolder(DXContainer, String)} or + * {@link #inFolderOrSubfolders(DXContainer, String)}). + *

+ * + * @param archivalState enum value specifying what files archival states can be returned + * + * @return the same builder object + */ + public FindDataObjectsRequestBuilder withArchivalState(ArchivalState archivalState) { + Preconditions.checkState(this.archivalState == null, + "Cannot call withArchivalState more than once"); + this.archivalState = + Preconditions.checkNotNull(archivalState, "archivalState may not be null"); + return this; + } + /** * Only returns applets (filters out data objects of all other classes). * @@ -1080,7 +1107,7 @@ public FindResultPage getFirstPage(int pageSize) { /** * Returns a subsequent page of the {@code findDataObjects} results starting from the specified item. * - * @param starting result of {@link DXSearch.FindDataObjectsResult.Page#getNext()} call on previous page + * @param starting result of {@link DXSearch.FindDataObjectsResult.FindDataObjectsResultPage#getNext()} call on previous page * @param pageSize number of elements to retrieve * * @return result set diff --git a/src/java/src/main/java/com/dnanexus/RunSpecification.java b/src/java/src/main/java/com/dnanexus/RunSpecification.java index b6c3b3bf24..703a7bdac3 100644 --- a/src/java/src/main/java/com/dnanexus/RunSpecification.java +++ b/src/java/src/main/java/com/dnanexus/RunSpecification.java @@ -41,7 +41,7 @@ public static class Builder { private Builder(String interpreter, String code, String distribution, String release) { this.interpreter = interpreter; this.code = code; - this. distribution = distribution; + this.distribution = distribution; this.release = release; } @@ -59,7 +59,7 @@ public RunSpecification build() { /** * Returns a builder initialized to create a run specification with the given interpreter, - * entry point code, distribution and releae. + * entry point code, distribution and release. * * @param interpreter interpreter name, e.g. "bash" or "python2.7" * @param code entry point code diff --git a/src/java/src/main/java/com/dnanexus/UnarchivingRate.java b/src/java/src/main/java/com/dnanexus/UnarchivingRate.java new file mode 100644 index 0000000000..6252473f8c --- /dev/null +++ b/src/java/src/main/java/com/dnanexus/UnarchivingRate.java @@ -0,0 +1,40 @@ +package com.dnanexus; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; + +import java.util.Map; + +public enum UnarchivingRate { + EXPEDITED("Expedited"), + STANDARD("Standard"), + BULK("Bulk"); + + private static Map createMap; + + static { + Map result = Maps.newHashMap(); + for (UnarchivingRate state : UnarchivingRate.values()) { + result.put(state.getValue(), state); + } + createMap = ImmutableMap.copyOf(result); + } + + @JsonCreator + private static UnarchivingRate create(String value) { + return createMap.get(value); + } + + private String value; + + private UnarchivingRate(String value) { + this.value = value; + } + + @JsonValue + private String getValue() { + return this.value; + } +} diff --git a/src/java/src/test/java/com/dnanexus/DXFileTest.java b/src/java/src/test/java/com/dnanexus/DXFileTest.java index 8a8ab62a71..8b73c02b4f 100644 --- a/src/java/src/test/java/com/dnanexus/DXFileTest.java +++ b/src/java/src/test/java/com/dnanexus/DXFileTest.java @@ -621,4 +621,22 @@ public void testUploadStreamDownloadStream() throws IOException { Assert.assertArrayEquals(uploadBytes, bytesFromDownloadStream); } + + @Test + public void testDownloadFileWithoutProject() throws IOException { + // Initialize file + byte[] uploadBytes = new byte[5 * 1024 * 1024]; + new Random().nextBytes(uploadBytes); + + DXFile f = DXFile.newFile().setProject(testProject).build(); + f = DXFile.newFile().setProject(testProject).build(); + f.uploadChunkSize = 5 * 1024 * 1024; + f.upload(uploadBytes); + f.closeAndWait(); + // Initialize file without project ID and download + DXFile g = DXFile.getInstance(f.dxId); + byte[] downloadBytes = g.downloadBytes(); + Assert.assertArrayEquals(uploadBytes, downloadBytes); + } + } diff --git a/src/java/src/test/java/com/dnanexus/DXProjectTest.java b/src/java/src/test/java/com/dnanexus/DXProjectTest.java index 3fb3b04b53..219c4a9102 100644 --- a/src/java/src/test/java/com/dnanexus/DXProjectTest.java +++ b/src/java/src/test/java/com/dnanexus/DXProjectTest.java @@ -17,7 +17,12 @@ package com.dnanexus; import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.List; +import com.google.common.base.Strings; +import com.google.common.collect.Lists; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -49,6 +54,40 @@ public void tearDown() { } } + private DXFile uploadMinimalFile(String name) { + return uploadMinimalFile(name, null); + } + + private DXFile uploadMinimalFile(String name, String folder) { + DXFile.Builder fileBuilder = DXFile.newFile() + .setProject(testProject) + .setName(name); + + if (folder != null) { + fileBuilder.setFolder(folder); + } + + DXFile file = fileBuilder.build(); + try { + file.upload("content".getBytes()); + } catch(Exception ex) { + Assert.fail("Creation of test file " + (folder != null ? folder : "/") + "/" + name + " failed!"); + } + file.closeAndWait(); + return file; + } + + /** + * Delayes execution by i milliseconds. + */ + private void sleep(int i) { + try { + Thread.sleep(i); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + // External tests @Test @@ -175,6 +214,330 @@ public void testCreateProject() { // System.out.println(p.getId()); } + @Test + public void testArchive() throws IOException { + this.testProject = DXProject.newProject().setName("DXProjectTest").build(); + + final DXFile fakeFile = DXFile.getInstance("file-" + Strings.repeat("x", 24)); + List fakeFiles = Lists.newArrayList(); + for (int i = 0; i < 10; ++i) { + fakeFiles.add(DXFile.getInstance("file-" + Strings.padStart(String.valueOf(i), 24, '0'))); + } + String fakeJson = "{\"files\": ["; + for (DXFile f : fakeFiles) { + fakeJson += "\"" + f.getId() + "\","; + } + fakeJson = fakeJson.substring(0, fakeJson.length() - 1) + "]}"; + + // Test all possible methods and their repetitive calls + Assert.assertEquals(DXJSON.parseJson(fakeJson), testProject.archive() + .addFile(fakeFiles.get(0)) + .addFiles(fakeFiles.get(1), fakeFiles.get(2)) + .addFiles(ImmutableList.of(fakeFiles.get(3), fakeFiles.get(4))) + .addFile(fakeFiles.get(5)) + .addFiles(fakeFiles.get(6), fakeFiles.get(7)) + .addFiles(ImmutableList.of(fakeFiles.get(8), fakeFiles.get(9))) + .buildRequestHash()); + + Assert.assertEquals(DXJSON.parseJson("{\"folder\": \"/folder\"}"), testProject.archive() + .setFolder("/folder").buildRequestHash()); + Assert.assertEquals(DXJSON.parseJson("{\"folder\": \"/folder\", \"recurse\": true}"), testProject.archive() + .setFolder("/folder", true).buildRequestHash()); + Assert.assertEquals(DXJSON.parseJson("{\"folder\": \"/folder\", \"recurse\": false}"), testProject.archive() + .setFolder("/folder", false).buildRequestHash()); + Assert.assertEquals(DXJSON.parseJson("{\"allCopies\": true}"), testProject.archive() + .setAllCopies(true).buildRequestHash()); + Assert.assertEquals(DXJSON.parseJson("{\"allCopies\": false}"), testProject.archive() + .setAllCopies(false).buildRequestHash()); + + // null params + try { + testProject.archive().addFile(null); + Assert.fail("Expected archival to fail due to null file"); + } catch (NullPointerException ex) { + // Expected + } + + try { + testProject.archive().addFiles(fakeFile, null); + Assert.fail("Expected archival to fail due to null file"); + } catch (NullPointerException ex) { + // Expected + } + + try { + testProject.archive().addFiles(ImmutableList.of(fakeFile, null)); + Assert.fail("Expected archival to fail due to null file"); + } catch (NullPointerException ex) { + // Expected + } + + try { + testProject.archive().setFolder(null); + Assert.fail("Expected archival to fail due to null folder"); + } catch (NullPointerException ex) { + // Expected + } + + try { + testProject.archive().setFolder(null, true); + Assert.fail("Expected archival to fail due to null folder"); + } catch (NullPointerException ex) { + // Expected + } + + // files and folder mutual exclusivity + try { + testProject.archive().addFile(fakeFile).setFolder("/folder"); + Assert.fail("Expected archival to fail due to the mutual exclusivity of files and folder"); + } catch (IllegalStateException ex) { + // Expected + } + + try { + testProject.archive().addFile(fakeFile).setFolder("/folder", true); + Assert.fail("Expected archival to fail due to the mutual exclusivity of files and folder"); + } catch (IllegalStateException ex) { + // Expected + } + + try { + testProject.archive().setFolder("/folder").addFile(fakeFile); + Assert.fail("Expected archival to fail due to the mutual exclusivity of files and folder"); + } catch (IllegalStateException ex) { + // Expected + } + + try { + testProject.archive().setFolder("/folder", true).addFile(fakeFile); + Assert.fail("Expected archival to fail due to the mutual exclusivity of files and folder"); + } catch (IllegalStateException ex) { + // Expected + } + + // setFolder single call restriction + try { + testProject.archive().setFolder("/folder").setFolder("/folder2"); + Assert.fail("Expected archival to fail because setFolder should be callable only once"); + } catch (IllegalStateException ex) { + // Expected + } + + try { + testProject.archive().setFolder("/folder", true).setFolder("/folder2"); + Assert.fail("Expected archival to fail because setFolder should be callable only once"); + } catch (IllegalStateException ex) { + // Expected + } + + try { + testProject.archive().setFolder("/folder").setFolder("/folder2", true); + Assert.fail("Expected archival to fail because setFolder should be callable only once"); + } catch (IllegalStateException ex) { + // Expected + } + + try { + testProject.archive().setFolder("/folder", true).setFolder("/folder2", true); + Assert.fail("Expected archival to fail because setFolder should be callable only once"); + } catch (IllegalStateException ex) { + // Expected + } + + // setAllCopies single call restriction + try { + testProject.archive().setAllCopies(true).setAllCopies(true); + Assert.fail("Expected archival to fail because setAllCopies should be callable only once"); + } catch (IllegalStateException ex) { + // Expected + } + + uploadMinimalFile("archiveFile1"); + uploadMinimalFile("archiveFile2"); + testProject.newFolder("/folder"); + testProject.newFolder("/folder/subfolder"); + uploadMinimalFile("archiveFile10", "/folder"); + uploadMinimalFile("archiveFile11", "/folder/subfolder"); + + Assert.assertEquals(2, testProject.archive().setFolder("/folder", true).execute().getCount()); + } + + @Test + public void testUnarchive() throws IOException, InvocationTargetException, IllegalAccessException, NoSuchMethodException { + this.testProject = DXProject.newProject().setName("DXProjectTest").build(); + + final DXFile fakeFile = DXFile.getInstance("file-" + Strings.repeat("x", 24)); + List fakeFiles = Lists.newArrayList(); + for (int i = 0; i < 10; ++i) { + fakeFiles.add(DXFile.getInstance("file-" + Strings.padStart(String.valueOf(i), 24, '0'))); + } + String fakeJson = "{\"files\": ["; + for (DXFile f : fakeFiles) { + fakeJson += "\"" + f.getId() + "\","; + } + fakeJson = fakeJson.substring(0, fakeJson.length() - 1) + "]}"; + + // Test all possible methods and their repetitive calls + Assert.assertEquals(DXJSON.parseJson(fakeJson), testProject.unarchive() + .addFile(fakeFiles.get(0)) + .addFiles(fakeFiles.get(1), fakeFiles.get(2)) + .addFiles(ImmutableList.of(fakeFiles.get(3), fakeFiles.get(4))) + .addFile(fakeFiles.get(5)) + .addFiles(fakeFiles.get(6), fakeFiles.get(7)) + .addFiles(ImmutableList.of(fakeFiles.get(8), fakeFiles.get(9))) + .buildRequestHash()); + + Assert.assertEquals(DXJSON.parseJson("{\"folder\": \"/folder\"}"), testProject.unarchive() + .setFolder("/folder").buildRequestHash()); + Assert.assertEquals(DXJSON.parseJson("{\"folder\": \"/folder\", \"recurse\": true}"), testProject.unarchive() + .setFolder("/folder", true).buildRequestHash()); + Assert.assertEquals(DXJSON.parseJson("{\"folder\": \"/folder\", \"recurse\": false}"), testProject.unarchive() + .setFolder("/folder", false).buildRequestHash()); + Assert.assertEquals(DXJSON.parseJson("{\"dryRun\": true}"), testProject.unarchive() + .setDryRun(true).buildRequestHash()); + Assert.assertEquals(DXJSON.parseJson("{\"dryRun\": false}"), testProject.unarchive() + .setDryRun(false).buildRequestHash()); + Method rateGetValue = UnarchivingRate.class.getDeclaredMethod("getValue"); + rateGetValue.setAccessible(true); + for (UnarchivingRate rate : UnarchivingRate.values()) { + Assert.assertEquals(DXJSON.parseJson("{\"rate\": \"" + rateGetValue.invoke(rate) + "\"}"), testProject.unarchive() + .setRate(rate).buildRequestHash()); + } + + // null params + try { + testProject.unarchive().addFile(null); + Assert.fail("Expected archival to fail due to null file"); + } catch (NullPointerException ex) { + // Expected + } + + try { + testProject.unarchive().addFiles(fakeFile, null); + Assert.fail("Expected archival to fail due to null file"); + } catch (NullPointerException ex) { + // Expected + } + + try { + testProject.unarchive().addFiles(ImmutableList.of(fakeFile, null)); + Assert.fail("Expected archival to fail due to null file"); + } catch (NullPointerException ex) { + // Expected + } + + try { + testProject.unarchive().setFolder(null); + Assert.fail("Expected archival to fail due to null folder"); + } catch (NullPointerException ex) { + // Expected + } + + try { + testProject.unarchive().setFolder(null, true); + Assert.fail("Expected archival to fail due to null folder"); + } catch (NullPointerException ex) { + // Expected + } + + // files and folder mutual exclusivity + try { + testProject.unarchive().addFile(fakeFile).setFolder("/folder"); + Assert.fail("Expected archival to fail due to the mutual exclusivity of files and folder"); + } catch (IllegalStateException ex) { + // Expected + } + + try { + testProject.unarchive().addFile(fakeFile).setFolder("/folder", true); + Assert.fail("Expected archival to fail due to the mutual exclusivity of files and folder"); + } catch (IllegalStateException ex) { + // Expected + } + + try { + testProject.unarchive().setFolder("/folder").addFile(fakeFile); + Assert.fail("Expected archival to fail due to the mutual exclusivity of files and folder"); + } catch (IllegalStateException ex) { + // Expected + } + + try { + testProject.unarchive().setFolder("/folder", true).addFile(fakeFile); + Assert.fail("Expected archival to fail due to the mutual exclusivity of files and folder"); + } catch (IllegalStateException ex) { + // Expected + } + + // setFolder single call restriction + try { + testProject.unarchive().setFolder("/folder").setFolder("/folder2"); + Assert.fail("Expected archival to fail because setFolder should be callable only once"); + } catch (IllegalStateException ex) { + // Expected + } + + try { + testProject.unarchive().setFolder("/folder", true).setFolder("/folder2"); + Assert.fail("Expected archival to fail because setFolder should be callable only once"); + } catch (IllegalStateException ex) { + // Expected + } + + try { + testProject.unarchive().setFolder("/folder").setFolder("/folder2", true); + Assert.fail("Expected archival to fail because setFolder should be callable only once"); + } catch (IllegalStateException ex) { + // Expected + } + + try { + testProject.unarchive().setFolder("/folder", true).setFolder("/folder2", true); + Assert.fail("Expected archival to fail because setFolder should be callable only once"); + } catch (IllegalStateException ex) { + // Expected + } + + // setRate single call restriction + try { + testProject.unarchive().setRate(UnarchivingRate.EXPEDITED).setRate(UnarchivingRate.EXPEDITED); + Assert.fail("Expected archival to fail because setRate should be callable only once"); + } catch (IllegalStateException ex) { + // Expected + } + + // setDryRun single call restriction + try { + testProject.unarchive().setDryRun(true).setDryRun(true); + Assert.fail("Expected archival to fail because setDryRun should be callable only once"); + } catch (IllegalStateException ex) { + // Expected + } + + uploadMinimalFile("archiveFile1"); + uploadMinimalFile("archiveFile2"); + testProject.newFolder("/folder"); + testProject.newFolder("/folder/subfolder"); + DXFile file1 = uploadMinimalFile("archiveFile10", "/folder"); + DXFile file2 = uploadMinimalFile("archiveFile11", "/folder/subfolder"); + testProject.archive().setFolder("/", true).execute(); + + // Wait for archival to complete + final int maxRetries = 24; + for (int i = 1; i <= maxRetries; ++i) { + if (file1.describe().getArchivalState() == ArchivalState.ARCHIVED && file2.describe().getArchivalState() == ArchivalState.ARCHIVED) { + break; + } + if (i == maxRetries) { + Assert.fail("Could not archive test files. Test cannot proceed..."); + } + sleep(5000); + } + + sleep(5000); + Assert.assertEquals(2, testProject.unarchive().setFolder("/folder", true).execute().getFiles()); + } + // Internal tests @Test diff --git a/src/java/src/test/java/com/dnanexus/DXSearchTest.java b/src/java/src/test/java/com/dnanexus/DXSearchTest.java index e4a5267a23..ba9df49a2f 100644 --- a/src/java/src/test/java/com/dnanexus/DXSearchTest.java +++ b/src/java/src/test/java/com/dnanexus/DXSearchTest.java @@ -107,6 +107,21 @@ private DXApplet createMinimalApplet() { return applet; } + private DXFile uploadMinimalFile(String name) { + DXFile file = DXFile.newFile() + .setProject(testProject) + .setName(name) + .build(); + + try { + file.upload("content".getBytes()); + } catch (Exception ex) { + Assert.fail("Creation of test file '" + name + "' failed!"); + } + file.closeAndWait(); + return file; + } + /** * Delayes execution by i milliseconds. */ @@ -338,6 +353,55 @@ public void testFindDataObjects() { assertEqualsAnyOrder(DXSearch.findDataObjects().withIdsIn(ImmutableList.of(moo, invisible)) .withVisibility(VisibilityQuery.EITHER).execute().asList(), moo, invisible); + // withArchivalState + + DXFile fileLive1 = uploadMinimalFile("fileLive1"); + DXFile fileLive2 = uploadMinimalFile("fileLive2"); + DXFile fileArchived = uploadMinimalFile("fileArchived"); + testProject.archive().addFile(fileArchived).execute(); + + // Wait for archival to complete + final int maxRetries = 24; + for (int i = 1; i <= maxRetries; ++i) { + if (fileArchived.describe().getArchivalState() == ArchivalState.ARCHIVED) { + break; + } + if (i == maxRetries) { + Assert.fail("Could not archive test file. Test cannot proceed..."); + } + sleep(5000); + } + + // Empty archival state does not affect file retrieval + assertEqualsAnyOrder(DXSearch.findDataObjects().withClassFile().inFolder(testProject, "/") + .execute().asList(), fileLive1, fileLive2, fileArchived); + assertEqualsAnyOrder(DXSearch.findDataObjects().withClassFile().inFolder(testProject, "/") + .withArchivalState(ArchivalState.LIVE).execute().asList(), fileLive1, fileLive2); + assertEqualsAnyOrder(DXSearch.findDataObjects().withClassFile().inFolder(testProject, "/") + .withArchivalState(ArchivalState.ARCHIVED).execute().asList(), fileArchived); + + // We may search by any archival state + for (ArchivalState state : ArchivalState.values()) { + DXSearch.findDataObjects().withClassFile().inFolder(testProject, "/").withArchivalState(state) + .execute().asList(); + } + + try { + DXSearch.findDataObjects().inFolder(testProject, "/") + .withArchivalState(ArchivalState.LIVE).execute().asList(); + Assert.fail("Expected search to fail due to missing 'class=File'"); + } catch (Exception ex) { + // Expected + } + + try { + DXSearch.findDataObjects().withClassFile() + .withArchivalState(ArchivalState.LIVE).execute().asList(); + Assert.fail("Expected search to fail due to missing 'project' and 'folder' fields"); + } catch (Exception ex) { + // Expected + } + // TODO: withLinkTo, withMinimumAccessLevel } diff --git a/src/jq b/src/jq deleted file mode 160000 index 1cdaabf2dd..0000000000 --- a/src/jq +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 1cdaabf2dd41ea811c6b1e0cb2820052627bba89 diff --git a/src/mk/Makefile.linux b/src/mk/Makefile.linux index 31f233dc53..100ebcb025 100644 --- a/src/mk/Makefile.linux +++ b/src/mk/Makefile.linux @@ -15,60 +15,24 @@ # under the License. $(DNANEXUS_HOME)/bin/dx: $(shell find python/{dxpy,scripts,requirements*,setup*} -not -name toolkit_version*) - python -c 'import sys; exit("dx-toolkit is not compatible with Python < 2.7" if sys.version_info < (2, 7) else 0)' - rm -rf "$(PYTHON_LIBDIR)" "$(DX_PY_ENV)" python/dist - mkdir -p "$$(dirname '$(PYTHON_LIBDIR)')" + python3 -c 'import sys; exit("dx-toolkit is not compatible with Python < 3.8" if sys.version_info < (3, 8) else 0)' + rm -rf "$(DX_PY_ENV)" $(VIRTUAL_ENV) "$(DX_PY_ENV)" -# Install setuptools and other fundamental packages unset PYTHONPATH; source "$(DX_PY_ENV)/$(ACTIVATE)"; ${PIP} install --upgrade -r python/requirements_setuptools.txt -# Build the dxpy wheel and move it into place - unset PYTHONPATH; source "$(DX_PY_ENV)/$(ACTIVATE)"; cd python; python setup.py bdist_wheel - - export DXPY_WHEEL_FILENAME=$$(basename python/dist/dxpy-*.whl) ; \ - unset PYTHONPATH; source "$(DX_PY_ENV)/$(ACTIVATE)"; ${PIP} install --ignore-installed --prefix="$(DNANEXUS_HOME)" python/dist/$${DXPY_WHEEL_FILENAME}[xattr] - mv "$(DNANEXUS_HOME)"/lib/python?.?/site-packages "$(PYTHON_LIBDIR)" + # Build the dxpy wheel and move it into place + unset PYTHONPATH; source "$(DX_PY_ENV)/$(ACTIVATE)"; ${PIP} install python/ # Installation # ============ -# debian specific installation targets -debian_install: base_install - cp -a $(DNANEXUS_HOME)/share $(DESTDIR)/$(PREFIX) # core libraries - mkdir -p $(DESTDIR)/$(PREFIX)/share/dnanexus/build - cp -a $(DNANEXUS_HOME)/build/info $(DESTDIR)/$(PREFIX)/share/dnanexus/build/ - rm -f $(DESTDIR)/$(PREFIX)/share/dnanexus/src/cpp/*/.gitignore - cp -a $(DNANEXUS_HOME)/doc $(DESTDIR)/$(PREFIX)/share/dnanexus # docs - - ../build/fix_shebang_lines.sh $(DESTDIR)/$(PREFIX)/bin --debian-system-install - - mkdir -p $(DESTDIR)/etc/profile.d # Install environment file into etc - install -v -m0644 $(DNANEXUS_HOME)/build/environment.redirector $(DESTDIR)/etc/profile.d/dnanexus.environment - install -v -m0644 $(DNANEXUS_HOME)/environment $(DESTDIR)/etc/profile.d/dnanexus.environment.sh - cp -a $(DNANEXUS_HOME)/share/dnanexus/lib/python${PYTHON_VERSION_NUMBER}/site-packages/* $(DESTDIR)/$(PREFIX)/share/dnanexus/lib/python${PYTHON_VERSION_NUMBER}/site-packages/ - ls $(DNANEXUS_HOME)/share/dnanexus/lib/python${PYTHON_VERSION_NUMBER}/site-packages | grep dxpy > $(DESTDIR)/$(PREFIX)/share/dnanexus/lib/python${PYTHON_VERSION_NUMBER}/site-packages/dxpy.pth - debian_java_install: java mkdir -p $(DESTDIR)/$(PREFIX)/share/java $(eval CLEANED_JAR_NAME := `cd "$(DNANEXUS_HOME)"/lib/java; ls *.jar | sed "s/dnanexus-api-\([0-9]\+\.[0-9]\+\.[0-9]\+\)-.*/dnanexus-api-\1.jar/g"`) (cd "$(DNANEXUS_HOME)"/lib/java; cp -a *.jar $(DESTDIR)/$(PREFIX)/share/java/"$(CLEANED_JAR_NAME)") -debian_r_install: R - mkdir -p $(DESTDIR)/$(PREFIX)/lib/R/site-library - cp -a $(DNANEXUS_HOME)/lib/R/{RCurl,RJSONIO} $(DESTDIR)/$(PREFIX)/lib/R/site-library/ - cp -a $(DNANEXUS_HOME)/lib/R/dxR $(DESTDIR)/$(PREFIX)/lib/R/site-library/ - - -# Bundled utilities -# ================= - -../bin/jq: jq/ - (cd jq; autoreconf --install; ./configure) - $(MAKE) -C jq -j PATH=./bin:$$PATH - cp -f jq/jq ../bin/ - # System dependencies # =================== diff --git a/src/mk/Makefile.osx b/src/mk/Makefile.osx index e3e8b809ab..ff6c4e8c34 100644 --- a/src/mk/Makefile.osx +++ b/src/mk/Makefile.osx @@ -15,31 +15,14 @@ # under the License. $(DNANEXUS_HOME)/bin/dx: $(shell find python/{dxpy,scripts,requirements*,setup*} -not -name toolkit_version*) - python -c 'import sys; exit("dx-toolkit is not compatible with Python < 2.7" if sys.version_info < (2, 7) else 0)' - rm -rf "$(PYTHON_LIBDIR)" "$(DX_PY_ENV)" python/dist - mkdir -p "$$(dirname '$(PYTHON_LIBDIR)')" + python3 -c 'import sys; exit("dx-toolkit is not compatible with Python < 3.8" if sys.version_info < (3, 8) else 0)' + rm -rf "$(DX_PY_ENV)" python/dist $(VIRTUAL_ENV) "$(DX_PY_ENV)" unset PYTHONPATH; source "$(DX_PY_ENV)/$(ACTIVATE)"; ${PIP} install --upgrade -r python/requirements_setuptools.txt # Build the dxpy wheel and move it into place - unset PYTHONPATH; source "$(DX_PY_ENV)/$(ACTIVATE)"; cd python; python setup.py bdist_wheel - unset PYTHONPATH; source "$(DX_PY_ENV)/$(ACTIVATE)"; pip install --ignore-installed --prefix="$(DNANEXUS_HOME)" python/dist/*.whl - mv "$(DNANEXUS_HOME)"/lib/python?.?/site-packages "${PYTHON_LIBDIR}" - rm -f "$(DNANEXUS_HOME)/bin/xattr" - - -# Bundled utilities -# ================= - -jq: git_submodules ../bin/jq - -../bin/jq: jq/ - (cd jq; autoreconf --install; ./configure) - (if [[ ! "$(CC)" == clang ]] && ! (gcc --version | grep -q LLVM); then export LDFLAGS="$$LDFLAGS -lgcc_eh"; fi; \ - cd jq; ./configure --disable-docs) - $(MAKE) -C jq -j PATH=$$(brew --prefix bison || echo .)/bin:$$PATH - cp -f jq/jq ../bin/ + unset PYTHONPATH; source "$(DX_PY_ENV)/$(ACTIVATE)"; ls -l; ${PIP} install python/ # System dependencies # =================== diff --git a/src/mk/Makefile.windows b/src/mk/Makefile.windows deleted file mode 100644 index b33ea6a6f8..0000000000 --- a/src/mk/Makefile.windows +++ /dev/null @@ -1,90 +0,0 @@ -# Copyright (C) 2013-2016 DNAnexus, Inc. -# -# This file is part of dx-toolkit (DNAnexus platform client libraries). -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may not -# use this file except in compliance with the License. You may obtain a copy -# of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - - -# Get the short toolkit version string, resembling v0.195.0 -GIT_TOOLKIT_VERSION_SHORT := $(shell git describe --abbrev=0) -# IF upload agent is built, dlls will be placed here, can be overriden with argument -# e.g. `make DLL_DEPS_FOLDER=C:\\toolkit-deps\\ pynsist_installer` -DLL_DEPS_FOLDER = '../../src/ua/dist/' - -pynsist_installer: toolkit_version $(DNANEXUS_HOME)/bin/dx dx-verify-file jq - - # Create installer.cfg and insert the toolkit version string - export GIT_TOOLKIT_VERSION_SHORT=$(GIT_TOOLKIT_VERSION_SHORT) ; \ - sed s/TEMPLATE_STRING_TOOLKIT_VERSION/$${GIT_TOOLKIT_VERSION_SHORT}/ "$(DNANEXUS_HOME)"/build/pynsist_files/installer.cfg.template > "$(DNANEXUS_HOME)"/build/pynsist_files/installer.cfg - # Insert dll foldername - export DLL_DEPS_FOLDER=$(DLL_DEPS_FOLDER) ; \ - sed --in-place "s~DLL_DEPS_FOLDER~$${DLL_DEPS_FOLDER}~" "$(DNANEXUS_HOME)"/build/pynsist_files/installer.cfg - - # Copy wheel file into place without changing its filename - export DXPY_WHEEL_FILENAME=$$(basename $(DNANEXUS_HOME)/src/python/dist/dxpy-*.whl) ; \ - cp "$(DNANEXUS_HOME)"/src/python/dist/$${DXPY_WHEEL_FILENAME} "$(DNANEXUS_HOME)"/build/pynsist_files/ - - # Download the .whl files for each of dxpy's Python dependencies, to bundle with NSIS installer - unset PYTHONPATH; source "$(DX_PY_ENV)/$(ACTIVATE)"; mkdir -p "$(DNANEXUS_HOME)"/build/pynsist_files/wheelfile_depends; python -m pip download --dest "$(DNANEXUS_HOME)"/build/pynsist_files/wheelfile_depends -r python/requirements.txt -r python/requirements_backports.txt -r python/requirements_windows.txt - - # Insert the wheel filename into installer.cfg - export DXPY_WHEEL_FILENAME=$$(basename $(DNANEXUS_HOME)/src/python/dist/dxpy-*.whl) ; \ - sed --in-place s/TEMPLATE_STRING_DXPY_WHEEL_FILENAME/$${DXPY_WHEEL_FILENAME}/ "$(DNANEXUS_HOME)"/build/pynsist_files/installer.cfg - - # Create pyapp_dnanexus.nsi and insert the wheel filename into it - export DXPY_WHEEL_FILENAME=$$(basename $(DNANEXUS_HOME)/src/python/dist/dxpy-*.whl) ; \ - sed s/TEMPLATE_STRING_DXPY_WHEEL_FILENAME/$${DXPY_WHEEL_FILENAME}/ "$(DNANEXUS_HOME)"/build/pynsist_files/pyapp_dnanexus.nsi.template > "$(DNANEXUS_HOME)"/build/pynsist_files/pyapp_dnanexus.nsi - - # Install the pynsist package and use it to build a Windows installer - unset PYTHONPATH; source "$(DX_PY_ENV)/$(ACTIVATE)"; python -m pip install pynsist==1.12 - unset PYTHONPATH; source "$(DX_PY_ENV)/$(ACTIVATE)"; pynsist "$(DNANEXUS_HOME)"/build/pynsist_files/installer.cfg - # Copy .exe installer to dx-toolkit root once it's ready - cp "$(DNANEXUS_HOME)"/build/pynsist_files/build/nsis/DNAnexus_CLI_"$(GIT_TOOLKIT_VERSION_SHORT)".exe "$(DNANEXUS_HOME)"/dx-toolkit-"$(GIT_TOOLKIT_VERSION_SHORT)".exe - -$(DNANEXUS_HOME)/bin/dx: $(shell find python/{dxpy,scripts,requirements*,setup*} -not -name toolkit_version*) - python -c 'import sys; exit("dx-toolkit is not compatible with Python < 2.7" if sys.version_info < (2, 7) else 0)' - rm -rf "$(PYTHON_LIBDIR)" "$(DX_PY_ENV)" python/dist - mkdir -p "$$(dirname '$(PYTHON_LIBDIR)')" - virtualenv --python=python "$(DX_PY_ENV)" - - # Install setuptools and build the dxpy wheel - unset PYTHONPATH; source "$(DX_PY_ENV)/$(ACTIVATE)"; python -m pip install --upgrade -r python/requirements_setuptools.txt - - # Build the dxpy wheel and move it into place - unset PYTHONPATH; source "$(DX_PY_ENV)/$(ACTIVATE)"; cd python; python setup.py bdist_wheel - - # Install the dxpy .whl and wheel itself to the temp dir. Note wheel is only - # needed because we need to bundle it with the pynsist installer. - unset PYTHONPATH; source "$(DX_PY_ENV)/$(ACTIVATE)"; python -m pip install --ignore-installed --prefix="$(WHEEL_TEMPDIR)" python/dist/*.whl - unset PYTHONPATH; source "$(DX_PY_ENV)/$(ACTIVATE)"; python -m pip install --ignore-installed --prefix="$(WHEEL_TEMPDIR)" wheel - # Move the staging dir artifacts into their final homes - mv "$(DNANEXUS_HOME)"/src/$(WHEEL_TEMPDIR)/Lib/site-packages "$(PYTHON_LIBDIR)" - mv "$(DNANEXUS_HOME)"/src/$(WHEEL_TEMPDIR)/Scripts/* "$(DNANEXUS_HOME)"/bin - rm -f "$(DNANEXUS_HOME)/bin/xattr" - - -# Bundled utilities -# ================= - -jq: git_submodules ../bin/jq - -../bin/jq: jq/ - (cd jq; autoreconf --install; ./configure) - $(MAKE) -C jq -j 1 - cp -f jq/jq ../bin/ - -# System dependencies -# =================== - -install_sysdeps: - echo "No dependencies for windows currently" diff --git a/src/mk/config.mk b/src/mk/config.mk index 3fd7e00c67..132f54f34d 100644 --- a/src/mk/config.mk +++ b/src/mk/config.mk @@ -43,8 +43,8 @@ endif # Extract the two most significant digits the python distribution # -PYTHON_VERSION_NUMBER:=$(shell python -c 'import sys; print("{}.{}".format(sys.version_info[0], sys.version_info[1]))') -PYTHON_MAJOR_VERSION:=$(shell python -c 'import sys; print(sys.version_info[0])') +PYTHON_VERSION_NUMBER:=$(shell python3 -c 'import sys; print("{}.{}".format(sys.version_info[0], sys.version_info[1]))') +PYTHON_MAJOR_VERSION:=$(shell python3 -c 'import sys; print(sys.version_info[0])') ifeq (${PYTHON_MAJOR_VERSION}, 2) PIP=pip @@ -69,7 +69,7 @@ endif export DNANEXUS_HOME := $(CURDIR)/.. export PATH := $(DNANEXUS_HOME)/build/bin:$(PATH) -export DX_PY_ENV := $(DNANEXUS_HOME)/build/py_env${PYTHON_VERSION_NUMBER} +export DX_PY_ENV := $(DNANEXUS_HOME)/build/py_env export DNANEXUS_LIBDIR := $(DNANEXUS_HOME)/share/dnanexus/lib # Short-circuit sudo when running as root. In a chrooted environment we are @@ -81,8 +81,6 @@ else MAYBE_SUDO='sudo' endif -PYTHON_LIBDIR = $(DNANEXUS_LIBDIR)/python${PYTHON_VERSION_NUMBER}/site-packages - ifeq ($(PLATFORM), windows) ACTIVATE=Scripts/activate diff --git a/src/python/Readme.md b/src/python/Readme.md index 4983179752..3caf2402db 100644 --- a/src/python/Readme.md +++ b/src/python/Readme.md @@ -1,7 +1,8 @@ dxpy: DNAnexus Python API ========================= +[DNAnexus Documentation](https://documentation.dnanexus.com/) -[API Documentation](http://autodoc.dnanexus.com/bindings/python/current/) +[dxpy API Documentation](http://autodoc.dnanexus.com/bindings/python/current/) Building -------- @@ -9,7 +10,7 @@ Building From the dx-toolkit root directory: ``` -make python +python3 -m pip install -e src/python ``` Debugging @@ -26,6 +27,14 @@ Example: $ _DX_DEBUG=1 dx ls ``` +### Debugging inside the IDE (PyCharm) +To be able to debug dx-toolkit (dx commands) directly in the IDE, 'Run/Debug Configurations' needs to be changed. +1. Go to Run → Edit Configurations... +2. Add New Configuration (Python) +3. Change script to module (dxpy.scripts.dx) +4. To Script parameters field write dx command you want to run (eg 'ls' runs 'dx ls') +5. Apply and OK (now it is possible to start debugging via main() function in dx.py) + Python coding style ------------------- @@ -56,30 +65,7 @@ Other useful resources: Python version compatibility ---------------------------- -dxpy is supported on Python 2 (2.7+) and Python 3 (3.5+) - -Code going into the Python codebase should be written in Python 3.5 style, and should be compatible with Python 2.7. Python 2.7 support will end on March 1, 2021. - -To facilitate Python 2 compatibility, we have the compat module in https://github.com/dnanexus/dx-toolkit/blob/master/src/python/dxpy/compat.py. Also, the following boilerplate should be -inserted into all Python source files: - -``` -from __future__ import absolute_import, division, print_function, unicode_literals -``` - -- `dxpy.compat` has some simple shims that mirror Python 3 builtins and redirect them to Python 2.7 equivalents when on 2.7. Most critically, `from dxpy.compat import str` will import the `unicode` builtin on 2.7 and the `str` builtin on python 3. Use `str` wherever you would have used `unicode`. To convert unicode strings to bytes, use `.encode('utf-8')`. -- Use `from __future__ import print_function` and use print as a function. Instead of `print >>sys.stderr`, write `print(..., file=sys.stderr)`. -- The next most troublesome gotcha after the bytes/unicode conversions is that many iterables operators return generators in Python 3. For example, `map()` returns a generator. This breaks places that expect a list, and requires either explicit casting with `list()`, or the use of list comprehensions (usually preferred). -- Instead of `raw_input`, use `from dxpy.compat import input`. -- Instead of `.iteritems()`, use `.items()`. If this is a performance concern on 2.7, introduce a shim in compat.py. -- Instead of `StringIO.StringIO`, use `from dxpy.compat import BytesIO` (which is StringIO on 2.7). -- Instead of `.next()`, use `next()`. -- Instead of `x.has_key(y)`, use `y in x`. -- Instead of `sort(x, cmp=lambda x, y: ...)`, use `x=sorted(x, key=lambda x: ...)`. - -Other useful resources: -* [The Hitchhiker's Guide to Python](http://docs.python-guide.org/en/latest/index.html) -* http://lucumr.pocoo.org/2013/5/21/porting-to-python-3-redux/ +dxpy is supported on Python 3 (3.8+) Convention for Python scripts that are also modules --------------------------------------------------- diff --git a/src/python/doc/conf.py b/src/python/doc/conf.py index 753897b7d6..6bf14404ef 100644 --- a/src/python/doc/conf.py +++ b/src/python/doc/conf.py @@ -109,12 +109,14 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'default' +html_theme = 'classic' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +html_theme_options = { + "body_max_width": None +} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] @@ -138,7 +140,6 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. diff --git a/src/python/doc/dxpy.rst b/src/python/doc/dxpy.rst index ad72c1a749..dcedb01b88 100644 --- a/src/python/doc/dxpy.rst +++ b/src/python/doc/dxpy.rst @@ -1,7 +1,7 @@ :mod:`dxpy` Package =================== -This Python 2.7 package includes three key modules: +This package includes three key modules: * :mod:`dxpy.bindings`: Contains useful Pythonic bindings for interacting with remote objects via the DNAnexus API server. For @@ -14,16 +14,6 @@ This Python 2.7 package includes three key modules: * :mod:`dxpy.exceptions`: Contains exceptions used in the other :mod:`dxpy` modules. -It has the following external dependencies: - -* :mod:`requests`: To install on Linux, use ``sudo pip install - requests``. Other installation options can be found at - http://docs.python-requests.org/en/latest/user/install - -* :mod:`futures`: To install on Linux, use ``sudo pip install futures``. - Other installation options can be found at - http://code.google.com/p/pythonfutures/ - Package Configuration --------------------- diff --git a/src/python/doc/dxpy_bindings_intro.rst b/src/python/doc/dxpy_bindings_intro.rst index e6b650662b..ee87edf5bb 100644 --- a/src/python/doc/dxpy_bindings_intro.rst +++ b/src/python/doc/dxpy_bindings_intro.rst @@ -4,7 +4,7 @@ Documentation on classes and methods: .. toctree:: - :maxdepth: 9 + :maxdepth: 1 dxpy_bindings dxpy_functions diff --git a/src/python/doc/index.rst b/src/python/doc/index.rst index a50365c576..ef203b7e90 100644 --- a/src/python/doc/index.rst +++ b/src/python/doc/index.rst @@ -31,14 +31,16 @@ Table of Contents .. toctree:: :numbered: - :maxdepth: 9 + :titlesonly: + :maxdepth: 2 dxpy dxpy_bindings_intro dxpy_app_builder - dxpy_utils dxpy_api dxpy_exceptions + dxpy_utils + Indices and tables ================== diff --git a/src/python/dxpy/__init__.py b/src/python/dxpy/__init__.py index e7c4d8ddfd..38d7a007c9 100644 --- a/src/python/dxpy/__init__.py +++ b/src/python/dxpy/__init__.py @@ -20,8 +20,7 @@ 1. Environment variables 2. Values stored in ``~/.dnanexus_config/environment`` -3. Values stored in ``/opt/dnanexus/environment`` -4. Hardcoded defaults +3. Hardcoded defaults The bindings are configured by the following environment variables: @@ -133,31 +132,19 @@ import os, sys, json, time, platform, ssl, traceback import errno import math -import mmap -import requests import socket import threading -import subprocess - +import certifi from collections import namedtuple from . import exceptions -from .compat import USING_PYTHON2, BadStatusLine, StringIO, bytes, Repr +from .compat import BadStatusLine, StringIO, bytes, Repr from .utils.printing import BOLD, BLUE, YELLOW, GREEN, RED, WHITE from random import randint -from requests.auth import AuthBase -from requests.packages import urllib3 -from requests.packages.urllib3.packages.ssl_match_hostname import match_hostname +import urllib3 from threading import Lock -from . import ssh_tunnel_app_support - -try: - # python-3 - from urllib.parse import urlsplit -except ImportError: - # python-2 - from urlparse import urlsplit +from urllib.parse import urlsplit sequence_number_mutex = threading.Lock() counter = 0 @@ -173,15 +160,7 @@ def _get_sequence_number(): def configure_urllib3(): # Disable verbose urllib3 warnings and log messages urllib3.disable_warnings(category=urllib3.exceptions.InsecurePlatformWarning) - logging.getLogger('dxpy.packages.requests.packages.urllib3.connectionpool').setLevel(logging.ERROR) - - # Trust DNAnexus S3 upload tunnel - def _match_hostname(cert, hostname): - if hostname == "ul.cn.dnanexus.com": - hostname = "s3.amazonaws.com" - match_hostname(cert, hostname) - - urllib3.connection.match_hostname = _match_hostname + logging.getLogger('urllib3.connectionpool').setLevel(logging.ERROR) configure_urllib3() @@ -201,18 +180,24 @@ def _match_hostname(cert, hostname): APISERVER_PORT = DEFAULT_APISERVER_PORT DEFAULT_RETRIES = 6 -DEFAULT_TIMEOUT = 600 +DEFAULT_TIMEOUT = 905 _DEBUG = 0 # debug verbosity level _UPGRADE_NOTIFY = True INCOMPLETE_READS_NUM_SUBCHUNKS = 8 -USER_AGENT = "{name}/{version} ({platform})".format(name=__name__, +USER_AGENT = "{name}/{version} ({platform}) Python/{python_version}".format(name=__name__, version=TOOLKIT_VERSION, - platform=platform.platform()) -_default_certs = requests.certs.where() -_default_headers = requests.utils.default_headers() + platform=platform.platform(), + python_version=platform.python_version()) +_default_certs = certifi.where() +_default_headers = { + "User-Agent": USER_AGENT, + "Accept-Encoding": "gzip, deflate", + "Accept": "*/*", + "Connection": "keep-alive", + } _default_timeout = urllib3.util.timeout.Timeout(connect=DEFAULT_TIMEOUT, read=DEFAULT_TIMEOUT) _RequestForAuth = namedtuple('_RequestForAuth', 'method url headers') _expected_exceptions = (exceptions.network_exceptions, exceptions.DXAPIError, BadStatusLine, exceptions.BadJSONInReply, @@ -256,7 +241,7 @@ def _get_env_var_proxy(print_proxy=False): file=sys.stderr) return proxy -def _get_pool_manager(verify, cert_file, key_file): +def _get_pool_manager(verify, cert_file, key_file, ssl_context=None): global _pool_manager default_pool_args = dict(maxsize=32, cert_reqs=ssl.CERT_REQUIRED, @@ -284,7 +269,8 @@ def _get_pool_manager(verify, cert_file, key_file): pool_args = dict(default_pool_args, cert_file=cert_file, key_file=key_file, - ca_certs=verify or os.environ.get('DX_CA_CERT') or requests.certs.where()) + ssl_context=ssl_context, + ca_certs=verify or os.environ.get('DX_CA_CERT') or certifi.where()) if verify is False or os.environ.get('DX_CA_CERT') == 'NOVERIFY': pool_args.update(cert_reqs=ssl.CERT_NONE, ca_certs=None) urllib3.disable_warnings() @@ -302,15 +288,7 @@ def _process_method_url_headers(method, url, headers): _headers.update(headers) else: _url, _headers = url, headers - # When *data* is bytes but *headers* contains Unicode text, httplib tries to concatenate them and decode - # *data*, which should not be done. Also, per HTTP/1.1 headers must be encoded with MIME, but we'll - # disregard that here, and just encode them with the Python default (ascii) and fail for any non-ascii - # content. See http://tools.ietf.org/html/rfc3987 for a discussion of encoding URLs. - # TODO: ascertain whether this is a problem in Python 3/make test - if USING_PYTHON2: - return method.encode(), _url.encode('utf-8'), {k.encode(): v.encode() for k, v in _headers.items()} - else: - return method, _url, _headers + return method, _url, _headers # When any of the following errors are indicated, we are sure that the @@ -322,6 +300,8 @@ def _process_method_url_headers(method, url, headers): errno.ECONNREFUSED # A remote host refused to allow the network connection } +_RETRYABLE_WITH_RESPONSE = (exceptions.ContentLengthError, BadStatusLine, exceptions.BadJSONInReply, + ConnectionResetError, urllib3.exceptions.ProtocolError, exceptions.UrllibInternalError) def _is_retryable_exception(e): """Returns True if the exception is always safe to retry. @@ -335,17 +315,19 @@ def _is_retryable_exception(e): """ if isinstance(e, urllib3.exceptions.ProtocolError): - e = e.args[1] + return True + if isinstance(e, ConnectionResetError): + return True if isinstance(e, (socket.gaierror, socket.herror)): return True if isinstance(e, socket.error) and e.errno in _RETRYABLE_SOCKET_ERRORS: return True if isinstance(e, urllib3.exceptions.NewConnectionError): return True - if isinstance(e, requests.exceptions.SSLError): - errmsg = str(e) - if "EOF occurred in violation of protocol" in errmsg: - return True + if isinstance(e, urllib3.exceptions.SSLError): + return True + if isinstance(e, ssl.SSLError): + return True return False def _extract_msg_from_last_exception(): @@ -358,7 +340,7 @@ def _extract_msg_from_last_exception(): # '}') return last_error.error_message() else: - return traceback.format_exc().splitlines()[-1].strip() + return traceback.format_exception_only(last_exc_type, last_error)[-1].strip() def _calculate_retry_delay(response, num_attempts): @@ -474,20 +456,6 @@ def _debug_print_response(debug_level, seq_num, time_started, req_id, response_s content_to_print, file=sys.stderr) - -def _test_tls_version(): - tls12_check_script = os.path.join(os.getenv("DNANEXUS_HOME"), "build", "tls12check.py") - if not os.path.exists(tls12_check_script): - return - - try: - subprocess.check_output(['python', tls12_check_script]) - except subprocess.CalledProcessError as e: - if e.returncode == 1: - print (e.output) - raise exceptions.InvalidTLSProtocol - - def DXHTTPRequest(resource, data, method='POST', headers=None, auth=True, timeout=DEFAULT_TIMEOUT, use_compression=None, jsonify_data=True, want_full_response=False, @@ -509,13 +477,11 @@ def DXHTTPRequest(resource, data, method='POST', headers=None, auth=True, :type auth: tuple, object, True (default), or None :param timeout: HTTP request timeout, in seconds :type timeout: float - :param config: *config* value to pass through to :meth:`requests.request` - :type config: dict :param use_compression: Deprecated :type use_compression: string or None :param jsonify_data: If True, *data* is converted from a Python list or dict to a JSON string :type jsonify_data: boolean - :param want_full_response: If True, the full :class:`requests.Response` object is returned (otherwise, only the content of the response body is returned) + :param want_full_response: If True, the full :class:`urllib3.response.HTTPResponse` object is returned (otherwise, only the content of the response body is returned) :type want_full_response: boolean :param decode_response_body: If True (and *want_full_response* is False), the response body is decoded and, if it is a JSON string, deserialized. Otherwise, the response body is uncompressed if transport compression is on, and returned raw. :type decode_response_body: boolean @@ -536,9 +502,9 @@ def DXHTTPRequest(resource, data, method='POST', headers=None, auth=True, :type always_retry: boolean :returns: Response from API server in the format indicated by *want_full_response* and *decode_response_body*. - :raises: :exc:`exceptions.DXAPIError` or a subclass if the server returned a non-200 status code; :exc:`requests.exceptions.HTTPError` if an invalid response was received from the server; or :exc:`requests.exceptions.ConnectionError` if a connection cannot be established. + :raises: :exc:`exceptions.DXAPIError` or a subclass if the server returned a non-200 status code; :exc:`urllib3.exceptions.HTTPError` if an invalid response was received from the server; or :exc:`urllib3.exceptions.ConnectionError` if a connection cannot be established. - Wrapper around :meth:`requests.request()` that makes an HTTP + Wrapper around :meth:`urllib3.request()` that makes an HTTP request, inserting authentication headers and (by default) converting *data* to JSON. @@ -564,7 +530,7 @@ def DXHTTPRequest(resource, data, method='POST', headers=None, auth=True, if auth: auth(_RequestForAuth(method, url, headers)) - pool_args = {arg: kwargs.pop(arg, None) for arg in ("verify", "cert_file", "key_file")} + pool_args = {arg: kwargs.pop(arg, None) for arg in ("verify", "cert_file", "key_file", "ssl_context")} test_retry = kwargs.pop("_test_retry_http_request", False) # data is a sequence/buffer or a dict @@ -594,6 +560,7 @@ def DXHTTPRequest(resource, data, method='POST', headers=None, auth=True, retried_responses = [] _url = None + redirect_url = None while True: success, time_started = True, None response = None @@ -620,32 +587,29 @@ def ensure_ascii(i): return i _headers = {ensure_ascii(k): ensure_ascii(v) for k, v in _headers.items()} - if USING_PYTHON2: + + # This is needed for python 3 urllib + _headers.pop(b'host', None) + _headers.pop(b'content-length', None) + _headers.pop(b'Content-Length', None) + + # The libraries downstream (http client) require elimination of non-ascii + # chars from URL. + # We check if the URL contains non-ascii characters to see if we need to + # quote it. It is important not to always quote the path (here: parts[2]) + # since it might contain elements (e.g. HMAC for api proxy) containing + # special characters that should not be quoted. + try: + ensure_ascii(_url) encoded_url = _url - else: - # This is needed for python 3 urllib - _headers.pop(b'host', None) - _headers.pop(b'content-length', None) - _headers.pop(b'Content-Length', None) - - # The libraries downstream (http client) require elimination of non-ascii - # chars from URL. - # We check if the URL contains non-ascii characters to see if we need to - # quote it. It is important not to always quote the path (here: parts[2]) - # since it might contain elements (e.g. HMAC for api proxy) containing - # special characters that should not be quoted. - try: - ensure_ascii(_url) - encoded_url = _url - except UnicodeEncodeError: - import urllib.parse - parts = list(urllib.parse.urlparse(_url)) - parts[2] = urllib.parse.quote(parts[2]) - encoded_url = urllib.parse.urlunparse(parts) + except UnicodeEncodeError: + import urllib.parse + parts = list(urllib.parse.urlparse(_url)) + parts[2] = urllib.parse.quote(parts[2]) + encoded_url = urllib.parse.urlunparse(parts) response = pool_manager.request(_method, encoded_url, headers=_headers, body=body, timeout=timeout, retries=False, **kwargs) - except urllib3.exceptions.ClosedPoolError: # If another thread closed the pool before the request was # started, will throw ClosedPoolError @@ -659,12 +623,19 @@ def ensure_ascii(i): and '_ARGCOMPLETE' not in os.environ): logger.info(response.headers['x-upgrade-info']) try: - with file(_UPGRADE_NOTIFY, 'a'): + with open(_UPGRADE_NOTIFY, 'a'): os.utime(_UPGRADE_NOTIFY, None) except: pass _UPGRADE_NOTIFY = False + # Handle redirection manually for symlink files + if response.status // 100 == 3: + redirect_url = response.headers.get('Location') + if not redirect_url: + raise exceptions.UrllibInternalError("Location not found in redirect response", response.status) + break + # If an HTTP code that is not in the 200 series is received and the content is JSON, parse it and throw the # appropriate error. Otherwise, raise the usual exception. if response.status // 100 != 2: @@ -682,18 +653,17 @@ def ensure_ascii(i): try: error_class = getattr(exceptions, content["error"]["type"], exceptions.DXAPIError) except (KeyError, AttributeError, TypeError): - raise exceptions.HTTPError(response.status, content) + raise exceptions.HTTPErrorWithContent("Appropriate error class not found. [HTTPCode=%s]" % response.status, content) raise error_class(content, response.status, time_started, req_id) else: try: content = response.data.decode('utf-8') except AttributeError: raise exceptions.UrllibInternalError("Content is none", response.status) - raise exceptions.HTTPError("{} {} [Time={} RequestID={}]\n{}".format(response.status, + raise exceptions.HTTPErrorWithContent("{} {} [Time={} RequestID={}]".format(response.status, response.reason, time_started, - req_id, - content)) + req_id), content.strip()) if want_full_response: return response @@ -757,8 +727,7 @@ def ensure_ascii(i): # BadStatusLine --- server did not return anything # BadJSONInReply --- server returned JSON that didn't parse properly if (response is None - or isinstance(e, (exceptions.ContentLengthError, BadStatusLine, exceptions.BadJSONInReply, - urllib3.exceptions.ProtocolError, exceptions.UrllibInternalError))): + or isinstance(e, _RETRYABLE_WITH_RESPONSE)): ok_to_retry = is_retryable else: ok_to_retry = 500 <= response.status < 600 @@ -766,8 +735,10 @@ def ensure_ascii(i): # The server has closed the connection prematurely if (response is not None and response.status == 400 and is_retryable and method == 'PUT' - and isinstance(e, requests.exceptions.HTTPError)): - if 'RequestTimeout' in exception_msg: + and isinstance(e, urllib3.exceptions.HTTPError)): + request_timeout_str = 'RequestTimeout' + if (request_timeout_str in exception_msg + or (isinstance(e, exceptions.HTTPErrorWithContent) and request_timeout_str in e.content)): logger.info("Retrying 400 HTTP error, due to slow data transfer. " + "Request Time=%f Request ID=%s", time_started, req_id) else: @@ -778,7 +749,6 @@ def ensure_ascii(i): # Unprocessable entity, request has semantical errors if response is not None and response.status == 422: ok_to_retry = False - if ok_to_retry: if rewind_input_buffer_offset is not None: data.seek(rewind_input_buffer_offset) @@ -792,8 +762,11 @@ def ensure_ascii(i): waiting_msg = 'Waiting %d seconds before retry %d of %d...' % ( delay, try_index + 1, max_retries) - logger.warning("[%s] %s %s: %s. %s %s", - time.ctime(), method, _url, exception_msg, waiting_msg, range_str) + log_msg = "[%s] %s %s: %s. %s %s" % (time.ctime(), method, _url, exception_msg, waiting_msg, range_str) + if isinstance(e, exceptions.HTTPErrorWithContent): + log_msg += "\n%s" % e.content + + logger.warning(log_msg) time.sleep(delay) try_index_including_503 += 1 if response is None or response.status != 503: @@ -803,13 +776,10 @@ def ensure_ascii(i): # All retries have been exhausted OR the error is deemed not # retryable. Print the latest error and propagate it back to the caller. if not isinstance(e, exceptions.DXAPIError): - logger.error("[%s] %s %s: %s.", time.ctime(), method, _url, exception_msg) - - if isinstance(e, urllib3.exceptions.ProtocolError) and \ - 'Connection reset by peer' in exception_msg: - # If the protocol error is 'connection reset by peer', most likely it is an - # error in the ssl handshake due to unsupported TLS protocol. - _test_tls_version() + log_msg = "[%s] %s %s: %s." % (time.ctime(), method, _url, exception_msg) + if isinstance(e, exceptions.HTTPErrorWithContent): + log_msg += "\n%s" % e.content + logger.error(log_msg) # Retries have been exhausted, and we are unable to get a full # buffer from the data source. Raise a special exception. @@ -822,10 +792,19 @@ def ensure_ascii(i): logger.info("[%s] %s %s: Recovered after %d retries", time.ctime(), method, _url, try_index) raise AssertionError('Should never reach this line: should have attempted a retry or reraised by now') + + # Make a new request to the URL specified in the Location header if we got a redirect_url + if redirect_url: + return DXHTTPRequest(redirect_url, body, method=method, headers=headers, auth=auth, timeout=timeout, + use_compression=use_compression, jsonify_data=jsonify_data, + want_full_response=want_full_response, + decode_response_body=decode_response_body, prepend_srv=prepend_srv, + session_handler=session_handler, + max_retries=max_retries, always_retry=always_retry, **kwargs) raise AssertionError('Should never reach this line: should never break out of loop') -class DXHTTPOAuth2(AuthBase): +class DXHTTPOAuth2(): def __init__(self, security_context): self.security_context = security_context @@ -1042,7 +1021,7 @@ def append_underlying_workflow_describe(globalworkflow_desc): for region, config in globalworkflow_desc['regionalOptions'].items(): workflow_id = config['workflow'] - workflow_desc = dxpy.api.workflow_describe(workflow_id) + workflow_desc = dxpy.api.workflow_describe(workflow_id, input_params={"project": config["resources"]}) globalworkflow_desc['regionalOptions'][region]['workflowDescribe'] = workflow_desc return globalworkflow_desc diff --git a/src/python/dxpy/api.py b/src/python/dxpy/api.py index d995192b9a..2e2e47c4c9 100644 --- a/src/python/dxpy/api.py +++ b/src/python/dxpy/api.py @@ -518,6 +518,118 @@ def database_list_folder(object_id, input_params={}, always_retry=True, **kwargs """ return DXHTTPRequest('/%s/listFolder' % object_id, input_params, always_retry=always_retry, **kwargs) +def dbcluster_add_tags(object_id, input_params={}, always_retry=True, **kwargs): + """ + Invokes the /dbcluster-xxxx/addTags API method. + + For more info, see: https://documentation.dnanexus.com/developer/api/introduction-to-data-object-metadata/tags#api-method-class-xxxx-addtags + """ + return DXHTTPRequest('/%s/addTags' % object_id, input_params, always_retry=always_retry, **kwargs) + +def dbcluster_add_types(object_id, input_params={}, always_retry=True, **kwargs): + """ + Invokes the /dbcluster-xxxx/addTypes API method. + + For more info, see: https://documentation.dnanexus.com/developer/api/data-object-lifecycle/types#api-method-class-xxxx-addtypes + """ + return DXHTTPRequest('/%s/addTypes' % object_id, input_params, always_retry=always_retry, **kwargs) + +def dbcluster_describe(object_id, input_params={}, always_retry=True, **kwargs): + """ + Invokes the /dbcluster-xxxx/describe API method. + + For more info, see: https://documentation.dnanexus.com/developer/api/introduction-to-data-object-classes/dbclusters#api-method-dbcluster-xxxx-describe + """ + return DXHTTPRequest('/%s/describe' % object_id, input_params, always_retry=always_retry, **kwargs) + +def dbcluster_get_details(object_id, input_params={}, always_retry=True, **kwargs): + """ + Invokes the /dbcluster-xxxx/getDetails API method. + + For more info, see: https://documentation.dnanexus.com/developer/api/data-object-lifecycle/details-and-links#api-method-class-xxxx-getdetails + """ + return DXHTTPRequest('/%s/getDetails' % object_id, input_params, always_retry=always_retry, **kwargs) + +def dbcluster_new(input_params={}, always_retry=False, **kwargs): + """ + Invokes the /dbcluster/new API method. + + For more info, see: https://documentation.dnanexus.com/developer/api/introduction-to-data-object-classes/dbclusters#api-method-dbcluster-new + """ + return DXHTTPRequest('/dbcluster/new', input_params, always_retry=always_retry, **kwargs) + +def dbcluster_remove_tags(object_id, input_params={}, always_retry=True, **kwargs): + """ + Invokes the /dbcluster-xxxx/removeTags API method. + + For more info, see: https://documentation.dnanexus.com/developer/api/introduction-to-data-object-metadata/tags#api-method-class-xxxx-removetags + """ + return DXHTTPRequest('/%s/removeTags' % object_id, input_params, always_retry=always_retry, **kwargs) + +def dbcluster_remove_types(object_id, input_params={}, always_retry=True, **kwargs): + """ + Invokes the /dbcluster-xxxx/removeTypes API method. + + For more info, see: https://documentation.dnanexus.com/developer/api/data-object-lifecycle/types#api-method-class-xxxx-removetypes + """ + return DXHTTPRequest('/%s/removeTypes' % object_id, input_params, always_retry=always_retry, **kwargs) + +def dbcluster_rename(object_id, input_params={}, always_retry=True, **kwargs): + """ + Invokes the /dbcluster-xxxx/rename API method. + + For more info, see: https://documentation.dnanexus.com/developer/api/introduction-to-data-object-metadata/name#api-method-class-xxxx-rename + """ + return DXHTTPRequest('/%s/rename' % object_id, input_params, always_retry=always_retry, **kwargs) + +def dbcluster_set_details(object_id, input_params={}, always_retry=True, **kwargs): + """ + Invokes the /dbcluster-xxxx/setDetails API method. + + For more info, see: https://documentation.dnanexus.com/developer/api/data-object-lifecycle/details-and-links#api-method-class-xxxx-setdetails + """ + return DXHTTPRequest('/%s/setDetails' % object_id, input_params, always_retry=always_retry, **kwargs) + +def dbcluster_set_properties(object_id, input_params={}, always_retry=True, **kwargs): + """ + Invokes the /dbcluster-xxxx/setProperties API method. + + For more info, see: https://documentation.dnanexus.com/developer/api/introduction-to-data-object-metadata/properties#api-method-class-xxxx-setproperties + """ + return DXHTTPRequest('/%s/setProperties' % object_id, input_params, always_retry=always_retry, **kwargs) + +def dbcluster_set_visibility(object_id, input_params={}, always_retry=True, **kwargs): + """ + Invokes the /dbcluster-xxxx/setVisibility API method. + + For more info, see: https://documentation.dnanexus.com/developer/api/data-object-lifecycle/visibility#api-method-class-xxxx-setvisibility + """ + return DXHTTPRequest('/%s/setVisibility' % object_id, input_params, always_retry=always_retry, **kwargs) + +def dbcluster_start(object_id, input_params={}, always_retry=True, **kwargs): + """ + Invokes the /dbcluster-xxxx/start API method. + + For more info, see: https://documentation.dnanexus.com/developer/api/introduction-to-data-object-classes/dbclusters#api-method-dbcluster-xxxx-start + """ + return DXHTTPRequest('/%s/start' % object_id, input_params, always_retry=always_retry, **kwargs) + +def dbcluster_stop(object_id, input_params={}, always_retry=True, **kwargs): + """ + Invokes the /dbcluster-xxxx/stop API method. + + For more info, see: https://documentation.dnanexus.com/developer/api/introduction-to-data-object-classes/dbclusters#api-method-dbcluster-xxxx-stop + """ + return DXHTTPRequest('/%s/stop' % object_id, input_params, always_retry=always_retry, **kwargs) + +def dbcluster_terminate(object_id, input_params={}, always_retry=True, **kwargs): + """ + Invokes the /dbcluster-xxxx/terminate API method. + + For more info, see: https://documentation.dnanexus.com/developer/api/introduction-to-data-object-classes/dbclusters#api-method-dbcluster-xxxx-terminate + """ + return DXHTTPRequest('/%s/terminate' % object_id, input_params, always_retry=always_retry, **kwargs) + def file_add_tags(object_id, input_params={}, always_retry=True, **kwargs): """ Invokes the /file-xxxx/addTags API method. @@ -841,6 +953,22 @@ def job_terminate(object_id, input_params={}, always_retry=True, **kwargs): """ return DXHTTPRequest('/%s/terminate' % object_id, input_params, always_retry=always_retry, **kwargs) +def job_update(object_id, input_params={}, always_retry=True, **kwargs): + """ + Invokes the /job-xxxx/update API method. + + For more info, see: https://documentation.dnanexus.com/developer/api/running-analyses/applets-and-entry-points#api-method-job-xxxx-update + """ + return DXHTTPRequest('/%s/update' % object_id, input_params, always_retry=always_retry, **kwargs) + +def job_get_identity_token(object_id, input_params={}, always_retry=True, **kwargs): + """ + Invokes the /job-xxxx/getIdentityToken API method. + + For more info, see: https://documentation.dnanexus.com/developer/api/running-analyses/applets-and-entry-points#api-method-job-xxxx-getIdentityToken + """ + return DXHTTPRequest('/%s/getIdentityToken' % object_id, input_params, always_retry=always_retry, **kwargs) + def job_new(input_params={}, always_retry=True, **kwargs): """ Invokes the /job/new API method. diff --git a/src/python/dxpy/app_builder.py b/src/python/dxpy/app_builder.py index 6f4a90686d..b37c7d79ea 100644 --- a/src/python/dxpy/app_builder.py +++ b/src/python/dxpy/app_builder.py @@ -192,7 +192,7 @@ def _fix_perm_filter(tar_obj): return tar_obj -def upload_resources(src_dir, project=None, folder='/', ensure_upload=False, force_symlinks=False, brief=False): +def upload_resources(src_dir, project=None, folder='/', ensure_upload=False, force_symlinks=False, brief=False, resources_dir=None, worker_resources_subpath=""): """ :param ensure_upload: If True, will bypass checksum of resources directory and upload resources bundle unconditionally; @@ -207,15 +207,24 @@ def upload_resources(src_dir, project=None, folder='/', ensure_upload=False, for result in a broken link within the resource directory unless you really know what you're doing. :type force_symlinks: boolean + :param resources_dir: Directory with resources to be archived and uploaded. If not given, uses `resources/`. + :type resources_dir: str + :param worker_resources_subpath: Path that will be prepended to the default directory where files are extracted on the worker. + Default is empty string, therefore files would be extracted directly to the root folder. + Example: If "home/dnanexus" is given, files will be extracted into /home/dnanexus. + :type worker_resources_subpath: str :returns: A list (possibly empty) of references to the generated archive(s) :rtype: list - If it exists, archives and uploads the contents of the - ``resources/`` subdirectory of *src_dir* to a new remote file + If resources_dir exists, archives and uploads the contents of the resources_dir + (usually ``resources/``) subdirectory of *src_dir* to a new remote file object, and returns a list describing a single bundled dependency in the form expected by the ``bundledDepends`` field of a run specification. Returns an empty list, if no archive was created. """ + if not resources_dir: + resources_dir = os.path.join(src_dir, "resources") + applet_spec = _get_applet_spec(src_dir) if project is None: @@ -224,7 +233,6 @@ def upload_resources(src_dir, project=None, folder='/', ensure_upload=False, for dest_project = project applet_spec['project'] = project - resources_dir = os.path.join(src_dir, "resources") if os.path.exists(resources_dir) and len(os.listdir(resources_dir)) > 0: target_folder = applet_spec['folder'] if 'folder' in applet_spec else folder @@ -273,8 +281,7 @@ def upload_resources(src_dir, project=None, folder='/', ensure_upload=False, for # add an entry in the tar file for the current directory, but # do not recurse! - tar_fh.add(dirname, arcname='.' + relative_dirname, recursive=False, filter=_fix_perm_filter) - + tar_fh.add(dirname, arcname=worker_resources_subpath + relative_dirname, recursive=False, filter=_fix_perm_filter) # Canonicalize the order of subdirectories; this is the order in # which they will be visited by os.walk subdirs.sort() @@ -337,9 +344,7 @@ def upload_resources(src_dir, project=None, folder='/', ensure_upload=False, for # If we are to dereference, use the target fn if deref_link: true_filename = os.path.realpath(true_filename) - - tar_fh.add(true_filename, arcname='.' + relative_filename, filter=_fix_perm_filter) - + tar_fh.add(true_filename, arcname=worker_resources_subpath + relative_filename, filter=_fix_perm_filter) # end for filename in sorted(files) # end for dirname, subdirs, files in os.walk(resources_dir): @@ -426,7 +431,7 @@ def upload_resources(src_dir, project=None, folder='/', ensure_upload=False, for def upload_applet(src_dir, uploaded_resources, check_name_collisions=True, overwrite=False, archive=False, - project=None, override_folder=None, override_name=None, dx_toolkit_autodep="stable", + project=None, override_folder=None, override_name=None, dry_run=False, brief=False, **kwargs): """ Creates a new applet object. @@ -437,11 +442,6 @@ def upload_applet(src_dir, uploaded_resources, check_name_collisions=True, overw :type override_folder: str :param override_name: name for the resulting applet which, if specified, overrides that given in dxapp.json :type override_name: str - :param dx_toolkit_autodep: What type of dx-toolkit dependency to - inject if none is present. "stable" for the APT package; "git" - for HEAD of dx-toolkit master branch; or False for no - dependency. - :type dx_toolkit_autodep: boolean or string """ applet_spec = _get_applet_spec(src_dir) @@ -503,7 +503,8 @@ def upload_applet(src_dir, uploaded_resources, check_name_collisions=True, overw # ----- # Override various fields from the pristine dxapp.json - + # TODO: should merge extra args into app spec before processing + # see line https://github.com/dnanexus/dx-toolkit/blob/f2ebe53a9e49b3213f8252e7ac1cd35d99913333/src/python/dxpy/app_builder.py#L616 # Carry region-specific values from regionalOptions into the main # runSpec applet_spec["runSpec"].setdefault("bundledDepends", []) @@ -613,34 +614,6 @@ def upload_applet(src_dir, uploaded_resources, check_name_collisions=True, overw raise AppBuilderException("No asset bundle was found that matched the specification %s" % (json.dumps(asset))) - # Include the DNAnexus client libraries as an execution dependency, if they are not already - # there - if dx_toolkit_autodep == "git": - dx_toolkit_dep = {"name": "dx-toolkit", - "package_manager": "git", - "url": "git://github.com/dnanexus/dx-toolkit.git", - "tag": "master", - "build_commands": "make install DESTDIR=/ PREFIX=/opt/dnanexus"} - elif dx_toolkit_autodep == "stable": - dx_toolkit_dep = {"name": "dx-toolkit", "package_manager": "apt"} - elif dx_toolkit_autodep: - raise AppBuilderException("dx_toolkit_autodep must be one of 'stable', 'git', or False; got %r instead" % (dx_toolkit_autodep,)) - - if dx_toolkit_autodep: - applet_spec["runSpec"].setdefault("execDepends", []) - exec_depends = applet_spec["runSpec"]["execDepends"] - if type(exec_depends) is not list or any(type(dep) is not dict for dep in exec_depends): - raise AppBuilderException("Expected runSpec.execDepends to be an array of objects") - dx_toolkit_dep_found = any(dep.get('name') in DX_TOOLKIT_PKGS or dep.get('url') in DX_TOOLKIT_GIT_URLS for dep in exec_depends) - if not dx_toolkit_dep_found: - exec_depends.append(dx_toolkit_dep) - if dx_toolkit_autodep == "git": - applet_spec.setdefault("access", {}) - applet_spec["access"].setdefault("network", []) - # Note: this can be set to "github.com" instead of "*" if the build doesn't download any deps - if "*" not in applet_spec["access"]["network"]: - applet_spec["access"]["network"].append("*") - merge(applet_spec, kwargs) # ----- @@ -904,7 +877,7 @@ def _create_app(applet_or_regional_options, app_name, src_dir, publish=False, se if authorized_users_to_add: dxpy.api.app_add_authorized_users(app_id, input_params={'authorizedUsers': list(authorized_users_to_add)}) if skip_adding_public: - logger.warn('the app was NOT made public as requested in the dxapp.json. To make it so, run "dx add users app-%s PUBLIC".' % (app_spec["name"],)) + logger.warn('the app was NOT made public as requested in the app spec. To make it so, run "dx add users app-%s PUBLIC".' % (app_spec["name"],)) if authorized_users_to_remove: dxpy.api.app_remove_authorized_users(app_id, input_params={'authorizedUsers': list(authorized_users_to_remove)}) @@ -912,7 +885,7 @@ def _create_app(applet_or_regional_options, app_name, src_dir, publish=False, se elif not len(existing_authorized_users) and not brief: # Apps that had authorized users added by any other means will # not have this message printed. - logger.warn('authorizedUsers is missing from the dxapp.json. No one will be able to view or run the app except the app\'s developers.') + logger.warn('authorizedUsers is missing from the app spec. No one will be able to view or run the app except the app\'s developers.') if publish: dxpy.api.app_publish(app_id, input_params={'makeDefault': set_default}) diff --git a/src/python/dxpy/asset_builder.py b/src/python/dxpy/asset_builder.py index 88e5c9e002..cdf26218ce 100644 --- a/src/python/dxpy/asset_builder.py +++ b/src/python/dxpy/asset_builder.py @@ -35,7 +35,15 @@ ASSET_BUILDER_XENIAL = "app-create_asset_xenial" ASSET_BUILDER_XENIAL_V1 = "app-create_asset_xenial_v1" ASSET_BUILDER_FOCAL = "app-create_asset_focal" - +ASSET_BUILDER_NOBLE = "app-create_asset_noble" +ASSET_BUILDERS = { + '12.04': ASSET_BUILDER_PRECISE, + '14.04': ASSET_BUILDER_TRUSTY, + '16.04': ASSET_BUILDER_XENIAL, + '16.04_v1': ASSET_BUILDER_XENIAL_V1, + '20.04': ASSET_BUILDER_FOCAL, + '24.04': ASSET_BUILDER_NOBLE, +} class AssetBuilderException(Exception): @@ -85,8 +93,8 @@ def validate_conf(asset_conf): raise AssetBuilderException('The asset configuration does not contain the required field "name".') # Validate runSpec - if 'release' not in asset_conf or asset_conf['release'] not in ["20.04", "16.04", "14.04", "12.04"]: - raise AssetBuilderException('The "release" field value should be either "20.04", "16.04", "14.04" (DEPRECATED), or "12.04" (DEPRECATED)') + if 'release' not in asset_conf or asset_conf['release'] not in ["24.04", "20.04", "16.04", "14.04", "12.04"]: + raise AssetBuilderException('The "release" field value should be either "24.04", "20.04", "16.04" (DEPRECATED), "14.04" (DEPRECATED), or "12.04" (DEPRECATED)') if 'runSpecVersion' in asset_conf: if asset_conf['runSpecVersion'] not in ["0", "1"]: raise AssetBuilderException('The "runSpecVersion" field should be either "0", or "1"') @@ -228,16 +236,11 @@ def build_asset(args): builder_run_options["systemRequirements"] = {"*": {"instanceType": asset_conf["instanceType"]}} if dest_folder_name: builder_run_options["folder"] = dest_folder_name - if asset_conf['release'] == "12.04": - app_run_result = dxpy.api.app_run(ASSET_BUILDER_PRECISE, input_params=builder_run_options) - elif asset_conf['release'] == "14.04": - app_run_result = dxpy.api.app_run(ASSET_BUILDER_TRUSTY, input_params=builder_run_options) - elif asset_conf['release'] == "16.04" and asset_conf['runSpecVersion'] == '1': - app_run_result = dxpy.api.app_run(ASSET_BUILDER_XENIAL_V1, input_params=builder_run_options) - elif asset_conf['release'] == "16.04": - app_run_result = dxpy.api.app_run(ASSET_BUILDER_XENIAL, input_params=builder_run_options) - elif asset_conf['release'] == "20.04": - app_run_result = dxpy.api.app_run(ASSET_BUILDER_FOCAL, input_params=builder_run_options) + + release = asset_conf['release'] + if asset_conf['runSpecVersion'] == '1': + release += '_v1' + app_run_result = dxpy.api.app_run(ASSET_BUILDERS[release], input_params=builder_run_options) job_id = app_run_result["id"] diff --git a/src/python/dxpy/bindings/__init__.py b/src/python/dxpy/bindings/__init__.py index 2b729968c4..63d86b0d33 100644 --- a/src/python/dxpy/bindings/__init__.py +++ b/src/python/dxpy/bindings/__init__.py @@ -549,10 +549,10 @@ def close(self, **kwargs): def list_projects(self, **kwargs): """ - :rtype: list of strings + :rtype: dict - Returns a list of project IDs of the projects that contain this - object and are visible to the requesting user. + Returns a dict of project IDs of the projects that contain this + object and the permission level of the requesting user. """ @@ -657,6 +657,30 @@ def _wait_on_close(self, timeout=3600*24*1, **kwargs): i += 1 elapsed += wait + def _wait_until_parts_uploaded(self, timeout=255, **kwargs): + elapsed = 0 + i = 0 + describe_input = {"fields": {"parts": True, "state": True}} + if self._proj is not None: + describe_input["project"] = self._proj + while True: + describe = self._describe(self._dxid, describe_input, **kwargs) + state, parts = describe["state"], describe["parts"] + if state == "closed" or not parts: + # parts of closed files must have been uploaded successfully + break + else: + is_uploaded = not any(parts[key].get("state", "complete") != "complete" for key in parts) + if is_uploaded: + break + if elapsed >= timeout or elapsed < 0: + raise DXError("Reached timeout while waiting for parts of the file ({}) to be uploaded".format(self.get_id())) + + wait = min(2**7, 2**i) + time.sleep(wait) + i += 1 + elapsed += wait + from .dxfile import DXFile, DXFILE_HTTP_THREADS, DEFAULT_BUFFER_SIZE from .dxdatabase import DXDatabase, DXFILE_HTTP_THREADS, DEFAULT_BUFFER_SIZE from .download_all_inputs import download_all_inputs diff --git a/src/python/test/__init__.py b/src/python/dxpy/bindings/apollo/__init__.py similarity index 100% rename from src/python/test/__init__.py rename to src/python/dxpy/bindings/apollo/__init__.py diff --git a/src/python/dxpy/bindings/apollo/cmd_line_options_validator.py b/src/python/dxpy/bindings/apollo/cmd_line_options_validator.py new file mode 100644 index 0000000000..97c45548c6 --- /dev/null +++ b/src/python/dxpy/bindings/apollo/cmd_line_options_validator.py @@ -0,0 +1,177 @@ +from __future__ import print_function + + +class ArgsValidator: + """ + InputsValidator class. Checks for invalid input combinations set by a JSON schema. + + The schema is a dictionary with the following structure: + + { + "schema_version": "1.0", + "parser_args":[ + "path" + "...", + ] + "condition_1": { + "properties": { + "items": ["path", "json_help"], + }, + "condition": "at_least_one_required", + "error_message": { + "message": "..." + }, + }, + "condition_2": { + "properties": { + "main_key": "path", + "items": ["output","delim"] + }, + "condition": "with_at_least_one_required", + "error_message": { + "message": "...", + "type": "warning" + }, + + """ + + def __init__( + self, + parser_dict, + schema, + error_handler=print, + warning_handler=print, + ): + self.parser_dict = parser_dict + self.schema = schema + self.error_handler = error_handler + self.warning_handler = warning_handler + + self.schema_version = schema.get("schema_version") + self.conditions_funcs = { + "exclusive": "interpret_exclusive", + "exclusive_with_exceptions": "interpret_exclusive_with_exceptions", + "required": "interpret_required", + "at_least_one_required": "interpret_at_least_one_required", + "with_at_least_one_required": "interpret_with_at_least_one_required", + "with_none_of": "interpret_with_none_of", + "mutually_exclusive_group": "interpret_mutually_exclusive_group", + } + + ### Schema methods ### + def validate_schema_conditions(self): + # Checking if all conditions exist + present_conditions = [ + value.get("condition") + for key, value in self.schema.items() + if key != "schema_version" and key != "parser_args" + ] + not_found = set(present_conditions) - set(self.conditions_funcs) + if len(not_found) != 0: + self.error_handler("{} schema condition is not defined".format(not_found)) + + ### Checking general methods ### + def interpret_conditions(self): + for key, value in self.schema.items(): + if key != "schema_version" and key != "parser_args": + condition = value.get("condition") + method_to_call = self.conditions_funcs.get(condition) + getattr(self, method_to_call)(key) + + def throw_schema_message(self, check): + message_type = self.schema.get(check).get("error_message").get("type") + message = self.schema.get(check).get("error_message").get("message") + if message_type == "warning": + self.throw_warning(message) + elif isinstance(message_type, type(None)) or (message_type == "error"): + self.throw_exit_error(message) + else: + self.error_handler( + 'Unkown error message in schema: "{}" for key "{}"'.format(type, check) + ) + + def throw_exit_error(self, message): + self.error_handler(message) + + def throw_warning(self, message): + self.warning_handler(message) + + def get_parser_values(self, params): + values = [self.parser_dict.get(p) for p in params] + return values + + def get_main_key(self, check): + return self.schema.get(check).get("properties").get("main_key") + + def get_items(self, check): + return self.schema.get(check).get("properties").get("items") + + def get_items_values(self, check): + items = self.get_items(check) + return [self.parser_dict[i] for i in items] + + def get_exceptions(self, check): + return self.schema.get(check).get("properties").get("exceptions") + + def remove_exceptions_from_list(self, check, list): + exceptions_list = self.get_exceptions(check) + for e in exceptions_list: + list.remove(e) + return list + + ### Checking specific methods ### + def interpret_exclusive(self, check): + self.interpret_exclusive_with_exceptions(check, exception_present=False) + + def interpret_exclusive_with_exceptions(self, check, exception_present=True): + main_key = self.get_main_key(check) + + # Defining args to check and its values + args_to_check = self.schema.get("parser_args")[:] + args_to_check.remove(main_key) + if exception_present: + args_to_check = self.remove_exceptions_from_list(check, args_to_check) + args_to_check_values = self.get_parser_values(args_to_check) + + # True check + if self.parser_dict.get(main_key) and any(args_to_check_values): + self.throw_schema_message(check) + + def interpret_with_none_of(self, check): + main_key = self.get_main_key(check) + args_to_check_values = self.get_items_values(check) + if self.parser_dict.get(main_key) and any(args_to_check_values): + self.throw_schema_message(check) + + def interpret_required(self, check): + self.interpret_at_least_one_required(check) + + def interpret_at_least_one_required(self, check, main_key=None): + args_to_check_values = self.get_items_values(check) + + if main_key: + if self.parser_dict.get(main_key) and not any(args_to_check_values): + self.throw_schema_message(check) + else: + if not any(args_to_check_values): + self.throw_schema_message(check) + + def interpret_with_at_least_one_required(self, check): + main_key = self.get_main_key(check) + self.interpret_at_least_one_required(check, main_key) + + def interpret_mutually_exclusive_group(self, check): + args_to_check_values = self.get_items_values(check) + present_args_count = 0 + + for arg in args_to_check_values: + if arg: + present_args_count += 1 + + if present_args_count > 1: + self.throw_schema_message(check) + + # VALIDATION + def validate_input_combination(self): + self.validate_schema_conditions() + self.interpret_conditions() diff --git a/src/python/dxpy/bindings/apollo/data_transformations.py b/src/python/dxpy/bindings/apollo/data_transformations.py new file mode 100644 index 0000000000..f7aa8207d2 --- /dev/null +++ b/src/python/dxpy/bindings/apollo/data_transformations.py @@ -0,0 +1,57 @@ + +def transform_to_expression_matrix(list_of_dicts): + """ + list_of_dicts: list of dictionaries, each of the form + { + "feature_id":, + "sample_id":, + "expression": + } + Additional key:value pairs are possible, but are not included in the transformed output + + How it works: + First, create a dictionary where each key is a sample id, and the values are the feature_id:expression pairs + Then, convert it to a list of dictionaries (the format that output handling is expecting) + Each item in the list is a dictionary containing one "sample_id":sample_id pair, and + multiple feature_id:expression pairs + + Returns: + A list of dictionaries, each of the form: + { + "sample_id":, + : + : + ... + : + } + """ + transformed_dict = {} + # create a dict of the form + # { + # :{ + # "sample_id":, + # :, + # :, + # etc. + # } + # + # } + colnames = set() + for entry in list_of_dicts: + # Keep track of all seen feature_ids, they will become the columns of our final table + colnames.add(entry["feature_id"]) + if entry["sample_id"] not in transformed_dict: + transformed_dict[entry["sample_id"]] = {"sample_id":entry["sample_id"],entry["feature_id"]:entry["expression"]} + else: + transformed_dict[entry["sample_id"]][entry["feature_id"]] = entry["expression"] + colnames = sorted(list(colnames)) + colnames.insert(0,"sample_id") + + # We want the output to be a list of dictionaries, rather than a single dicitonary keyed on sample_id + dict_list = list(transformed_dict.values()) + for dict_row in dict_list: + for colname in colnames: + if colname not in dict_row: + dict_row[colname] = None + + return (dict_list,colnames) \ No newline at end of file diff --git a/src/python/dxpy/bindings/apollo/dataset.py b/src/python/dxpy/bindings/apollo/dataset.py new file mode 100644 index 0000000000..9ff44a118a --- /dev/null +++ b/src/python/dxpy/bindings/apollo/dataset.py @@ -0,0 +1,125 @@ +import sys +import gzip +from dxpy import DXHTTPRequest +from dxpy.bindings import DXRecord, DXFile +import json + + +class Dataset(DXRecord): + def __init__(self, dataset_id, detail_describe_dict=None): + super(DXRecord, self).__init__(dataset_id) + self.dataset_id = dataset_id + self._detail_describe = detail_describe_dict + self._visualize_info = None + + if detail_describe_dict: + if "details" not in detail_describe_dict: + raise ValueError("detail is expected key in detail_describe_dict") + + @staticmethod + def resolve_cohort_to_dataset(record_obj): + record_obj_desc = record_obj.describe( + default_fields=True, fields={"properties", "details"} + ) + cohort_info = None + is_cohort = "CohortBrowser" in record_obj_desc["types"] + + if is_cohort: + cohort_info = record_obj_desc + dataset_id = record_obj_desc["details"]["dataset"]["$dnanexus_link"] + dataset_obj = Dataset(dataset_id) + else: + dataset_obj = Dataset(record_obj.id, record_obj_desc) + + return dataset_obj, cohort_info + + @property + def descriptor_file(self): + return self.detail_describe["details"]["descriptor"]["$dnanexus_link"] + + @property + def descriptor_file_dict(self): + is_python2 = sys.version_info.major == 2 + content = DXFile( + self.descriptor_file, mode="rb", project=self.project_id + ).read() + + if is_python2: + import StringIO + + x = StringIO.StringIO(content) + file_obj = gzip.GzipFile(fileobj=x) + content = file_obj.read() + + else: + content = gzip.decompress(content) + + return json.loads(content) + + @property + def visualize_info(self): + if self._visualize_info is None: + self._visualize_info = DXHTTPRequest( + "/" + self.dataset_id + "/visualize", + {"project": self.project_id, "cohortBrowser": False}, + ) + return self._visualize_info + + @property + def vizserver_url(self): + vis_info = self.visualize_info + return vis_info.get("url") + + @property + def project_id(self): + return self.detail_describe.get("project") + + @property + def version(self): + return self.detail_describe.get("details").get("version") + + @property + def detail_describe(self): + if self._detail_describe is None: + self._detail_describe = self.describe( + default_fields=True, fields={"properties", "details"} + ) + return self._detail_describe + + @property + def assays_info_dict(self): + assays = self.descriptor_file_dict["assays"] + assays_info_dict = {} + + for index in range(len(assays)): + model = assays[index]["generalized_assay_model"] + assay_dict = { + "name": assays[index]["name"], + "index": index, + "uuid": assays[index]["uuid"], + "reference": assays[index].get("reference"), + } + + if model not in assays_info_dict.keys(): + assays_info_dict[model] = [] + + assays_info_dict[model].append(assay_dict) + + return assays_info_dict + + def assay_names_list(self, assay_type): + assay_names_list = [] + if self.assays_info_dict.get(assay_type): + for assay in self.assays_info_dict.get(assay_type): + assay_names_list.append(assay["name"]) + return assay_names_list + + def is_assay_name_valid(self, assay_name, assay_type): + return assay_name in self.assay_names_list(assay_type) + + def assay_index(self, assay_name): + assay_lists_per_model = self.assays_info_dict.values() + for model_assays in assay_lists_per_model: + for assay in model_assays: + if assay["name"] == assay_name: + return assay["index"] diff --git a/src/python/dxpy/bindings/apollo/json_validation_by_schema.py b/src/python/dxpy/bindings/apollo/json_validation_by_schema.py new file mode 100644 index 0000000000..af049dc2dc --- /dev/null +++ b/src/python/dxpy/bindings/apollo/json_validation_by_schema.py @@ -0,0 +1,241 @@ +from __future__ import print_function + + +class JSONValidator(object): + """ + JSON validator class to validate a JSON against a schema. + + The schema is a dictionary with the following structure: + + { + "key1": { + "type": dict, + "properties": { + "name": {"type": str, "required": True}, + "attribute": {"type": str} + } + }, + "key2": { + "type": dict, + "properties": { + "xyz": {"type": str}, + "abc": {"type": list} + }, + "conflicting_keys": [["xyz", "abc"]] + }, + "list_key_example": { + "type": list, + "items": { + "type": dict, + "properties": { + "nested_key1": {"type": str, "required": True}, + "nested_key2": {"type": str} + } + } + }, + "id": { + "type": list, + }, + "conflicting_keys": [["key1", "key2"]], + "dependent_conditional_keys": { + "list_key_example": ["key1", "key2"] + } + } + + Key types can be defined via the "type" key + + Required keys can be defined via the "required" key + + Conflicting keys can be defined via the "conflicting_keys" key, where the value is a list of lists of keys that are mutually exclusive. + Conflicting keys can be defined at the level of the schema for the entire JSON or at key-level. + In the example above, within "key2" the keys "xyz" and "abc" are mutually exclusive and cannot be present together. + Similarly, at the level of the whole JSON, "key1" and "key2" are mutually exclusive and cannot be present together. + + Conditional key combinations can be defined via the dependent_conditional_keys at the level of the schema. These should be defined as a dict where the value is a list. + In the example above, dependent_conditional_keys is defined as {"list_key_example": ["key1", "key2"]}, which means whenever list_key_example is present, either "key1" + or "key2" has to be also present. + + Currently the maximum level of nestedness supported for validation is as deep as shown in the example above via the "list_key_example" key. + + This is an example of a valid JSON that satisfies all the conditions defined in the schema above: + + { + "key1": { + "name": "test", + }, + "list_key_example": [ + {"nested_key1": "1", "nested_key2": "2"}, + {"nested_key1": "3", "nested_key2": "4"}, + {"nested_key1": "5"} + ], + "id": ["id_1", "id_2"] + } + + """ + + def __init__(self, schema, error_handler=print): + self.schema = schema + self.error_handler = error_handler + + if not isinstance(schema, dict) or not schema: + error_handler("Schema must be a non-empty dict.") + + def validate(self, input_json): + if not isinstance(input_json, dict) or not input_json: + self.error_handler("Input JSON must be a non-empty dict.") + + self.error_on_invalid_keys(input_json) + + self.validate_types(input_json) + + for key, value in self.schema.items(): + if key in input_json: + self.validate_properties(value.get("properties", {}), input_json[key]) + + if value.get("type") == list: + self.validate_list_items( + value.get("items", {}), input_json[key], key + ) + + # Check for incompatible/conflicting subkeys defined at the key-level + if value.get("conflicting_keys"): + self.check_incompatible_subkeys(input_json[key], key) + + self.check_incompatible_keys(input_json) + self.check_dependent_key_combinations(input_json) + + def validate_types(self, input_dict): + for key, value in input_dict.items(): + if not isinstance(value, self.schema.get(key, {}).get("type")): + self.error_handler( + "Key '{}' has an invalid type. Expected {} but got {}".format( + key, self.schema.get(key, {}).get("type"), type(value) + ) + ) + + def validate_properties(self, properties, input_dict): + for key, value in properties.items(): + if key not in input_dict and value.get("required"): + self.error_handler( + "Required key '{}' was not found in the input JSON.".format(key) + ) + if key in input_dict and not isinstance(input_dict[key], value.get("type")): + self.error_handler( + "Key '{}' has an invalid type. Expected {} but got {}".format( + key, value.get("type"), type(input_dict[key]) + ) + ) + + def validate_list_items(self, item_schema, input_list, key_name): + item_type = item_schema.get("type") + if item_type: + for item in input_list: + if not isinstance(item, item_type): + self.error_handler( + "Expected list items within '{}' to be of type {} but got {} instead.".format( + key_name, item_type, type(item) + ) + ) + self.validate_properties(item_schema.get("properties", {}), item) + else: + if not isinstance(input_list, list): + self.error_handler( + "Expected list but got {} for {}".format(type(input_list), key_name) + ) + for item in input_list: + if not isinstance(item, str): + self.error_handler( + "Expected list items to be of type string for {}.".format( + key_name + ) + ) + + def check_incompatible_subkeys(self, input_json, current_key): + for keys in self.schema.get(current_key, {}).get("conflicting_keys", []): + if all(k in input_json for k in keys): + self.error_handler( + "For {}, exactly one of {} must be provided in the supplied JSON object.".format( + current_key, " or ".join(keys) + ) + ) + + def check_incompatible_keys(self, input_json): + for keys in self.schema.get("conflicting_keys", []): + if all(key in input_json for key in keys): + self.error_handler( + "Exactly one of {} must be provided in the supplied JSON object.".format( + " or ".join(keys) + ) + ) + + def check_dependent_key_combinations( + self, input_json, enforce_one_associated_key=False + ): + mandatory_combinations = self.schema.get("dependent_conditional_keys", {}) + for main_key, associated_keys in mandatory_combinations.items(): + if main_key in input_json: + present_associated_keys = [ + key for key in associated_keys if key in input_json + ] + if len(present_associated_keys) == 0: + self.error_handler( + "When {} is present, one of the following keys must be also present: {}.".format( + main_key, ", ".join(associated_keys) + ) + ) + if len(present_associated_keys) > 1 and enforce_one_associated_key: + self.error_handler( + "Only one of the associated keys {} can be present for main key {}".format( + ", ".join(associated_keys) + ) + ) + + def error_on_invalid_keys(self, input_json): + invalid_keys = [] + for key in input_json: + if key not in self.schema: + invalid_keys.append(key) + + if invalid_keys: + self.error_handler( + "Found following invalid filters: {}".format(invalid_keys) + ) + + def are_list_items_within_range( + self, + input_json, + key, + start_subkey, + end_subkey, + window_width=250000000, + check_each_separately=False, + ): + """ + This won't run by default when calling .validate() on a JSONValidator object. Run it separately when necessary + """ + + for item in input_json[key]: + if int(item[end_subkey]) <= int(item[start_subkey]): + self.error_handler( + "{} cannot be less than or equal to the {}".format( + end_subkey, start_subkey + ) + ) + + if check_each_separately: + if item[end_subkey] - item[start_subkey] > window_width: + self.error_handler( + "Range of {} and {} cannot be greater than {} for each item in {}".format( + start_subkey, end_subkey, window_width, key + ) + ) + if not check_each_separately: + # Check will be done across all items in the list + # the entire search would be limited to window_width + start_values = [int(item[start_subkey]) for item in input_json[key]] + end_values = [int(item[end_subkey]) for item in input_json[key]] + + if max(end_values) - min(start_values) > window_width: + self.error_handler( + "Range cannot be greater than {} for {}".format(window_width, key) + ) diff --git a/src/python/dxpy/bindings/apollo/schemas/__init__.py b/src/python/dxpy/bindings/apollo/schemas/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/python/dxpy/bindings/apollo/schemas/assay_filtering_conditions.py b/src/python/dxpy/bindings/apollo/schemas/assay_filtering_conditions.py new file mode 100644 index 0000000000..6ae2115756 --- /dev/null +++ b/src/python/dxpy/bindings/apollo/schemas/assay_filtering_conditions.py @@ -0,0 +1,77 @@ +EXTRACT_ASSAY_EXPRESSION_FILTERING_CONDITIONS = { + "version": "1.0", + "output_fields_mapping": { + "default": [ + {"feature_id": "expression$feature_id"}, + {"sample_id": "expression$sample_id"}, + {"expression": "expression$value"}, + ], + "additional": [ + {"feature_name": "expr_annotation$gene_name"}, + {"chrom": "expr_annotation$chr"}, + {"start": "expr_annotation$start"}, + {"end": "expr_annotation$end"}, + {"strand": "expr_annotation$strand"}, + ], + }, + "filtering_conditions": { + "location": { + "items_combination_operator": "or", + "filters_combination_operator": "and", + "max_item_limit": 10, + "properties": [ + { + "key": "chromosome", + "condition": "is", + "table_column": "expr_annotation$chr", + }, + { + "keys": ["starting_position", "ending_position"], + "condition": "genobin_partial_overlap", + "max_range": "250", + "table_column": { + "starting_position": "expr_annotation$start", + "ending_position": "expr_annotation$end", + }, + }, + ], + }, + "annotation": { + "properties": { + "feature_id": { + "max_item_limit": 100, + "condition": "in", + "table_column": "expr_annotation$feature_id", + }, + "feature_name": { + "max_item_limit": 100, + "condition": "in", + "table_column": "expr_annotation$gene_name", + }, + } + }, + "expression": { + "filters_combination_operator": "and", + "properties": { + "min_value": { + "condition": "greater-than-eq", + "table_column": "expression$value", + }, + "max_value": { + "condition": "less-than-eq", + "table_column": "expression$value", + }, + }, + }, + "sample_id": { + "max_item_limit": 100, + "condition": "in", + "table_column": "expression$sample_id", + }, + }, + "filters_combination_operator": "and", + "order_by": [ + {"feature_id": "asc"}, + {"sample_id": "asc"}, + ], +} diff --git a/src/python/dxpy/bindings/apollo/schemas/assay_filtering_json_schemas.py b/src/python/dxpy/bindings/apollo/schemas/assay_filtering_json_schemas.py new file mode 100644 index 0000000000..cd758592ae --- /dev/null +++ b/src/python/dxpy/bindings/apollo/schemas/assay_filtering_json_schemas.py @@ -0,0 +1,30 @@ +EXTRACT_ASSAY_EXPRESSION_JSON_SCHEMA = { + "annotation": { + "type": dict, + "properties": { + "feature_name": {"type": list, "required": False}, + "feature_id": {"type": list, "required": False}, + }, + "conflicting_keys": [["feature_name", "feature_id"]], + }, + "expression": { + "type": dict, + "properties": {"min_value": {"type": (int, float)}, "max_value": {"type": (int, float)}}, + }, + "location": { + "type": list, + "items": { + "type": dict, + "properties": { + "chromosome": {"type": str, "required": True}, + "starting_position": {"type": str, "required": True}, + "ending_position": {"type": str, "required": True}, + }, + }, + }, + "sample_id": { + "type": list, + }, + "conflicting_keys": [["location", "annotation"]], + "dependent_conditional_keys": {"expression": ["annotation", "location"]}, +} diff --git a/src/python/dxpy/bindings/apollo/schemas/input_arguments_validation_schemas.py b/src/python/dxpy/bindings/apollo/schemas/input_arguments_validation_schemas.py new file mode 100644 index 0000000000..4458752b83 --- /dev/null +++ b/src/python/dxpy/bindings/apollo/schemas/input_arguments_validation_schemas.py @@ -0,0 +1,123 @@ +# -*- coding: utf-8 -*- + +EXTRACT_ASSAY_EXPRESSION_INPUT_ARGS_SCHEMA = { + "schema_version": "1.0", + "parser_args": [ + "path", + "assay_name", + "list_assays", + "output", + "retrieve_expression", + "additional_fields", + "additional_fields_help", + "delim", + "filter_json_file", + "sql", + "expression_matrix", + "json_help", + "filter_json", + ], + "1_path_or_json_help-at_least_one_required": { + "properties": { + "items": ["path", "json_help"], + }, + "condition": "at_least_one_required", + "error_message": { + "message": 'At least one of the following arguments is required: "Path", "--json-help"' + }, + }, + "2_path_with_no_args-with_at_least_one_required": { + "properties": { + "main_key": "path", + "items": [ + "list_assays", + "retrieve_expression", + "additional_fields_help", + "json_help", + ], + }, + "condition": "with_at_least_one_required", + "error_message": { + "message": 'One of the arguments "--retrieve-expression", "--list-assays", "--additional-fields-help", "--json-help" is required.' + }, + }, + "3_list_assays_exclusive_with_exceptions": { + "properties": { + "main_key": "list_assays", + "exceptions": ["path"], + }, + "condition": "exclusive_with_exceptions", + "error_message": { + "message": '"--list-assays" cannot be presented with other options' + }, + }, + "4_retrieve_expression_with_at_least_one_required": { + "properties": { + "main_key": "retrieve_expression", + "items": [ + "filter_json", + "filter_json_file", + "json_help", + "additional_fields_help", + ], + }, + "condition": "with_at_least_one_required", + "error_message": { + "message": 'The flag "--retrieve_expression" must be followed by "--filter-json", "--filter-json-file", "--json-help", or "--additional-fields-help".' + }, + }, + "5_json_help_exclusive_with_exceptions": { + "properties": { + "main_key": "json_help", + "exceptions": [ + "path", + "retrieve_expression", + ], + }, + "condition": "exclusive_with_exceptions", + "error_message": { + "message": '"--json-help" cannot be passed with any option other than "--retrieve-expression".' + }, + }, + "6_additional_fields_help_exclusive_with_exceptions": { + "properties": { + "main_key": "additional_fields_help", + "exceptions": [ + "path", + "retrieve_expression", + ], + }, + "condition": "exclusive_with_exceptions", + "error_message": { + "message": '"--additional-fields-help" cannot be passed with any option other than "--retrieve-expression".' + }, + }, + "7_json_inputs-mutually_exclusive": { + "properties": { + "items": ["filter_json", "filter_json_file"], + }, + "condition": "mutually_exclusive_group", + "error_message": { + "message": 'The arguments "--filter-json" and "--filter-json-file" are not allowed together.' + }, + }, + "8_expression_matrix-with_at_least_one_required": { + "properties": { + "main_key": "expression_matrix", + "items": ["retrieve_expression"], + }, + "condition": "with_at_least_one_required", + "error_message": { + "message": '"--expression-matrix" cannot be passed with any argument other than "--retrieve-expression".' + }, + }, + "9_em_sql-mutually_exclusive": { + "properties": { + "items": ["expression_matrix", "sql"], + }, + "condition": "mutually_exclusive_group", + "error_message": { + "message": '"--expression-matrix"/"-em" cannot be passed with the flag, "--sql".' + }, + }, +} diff --git a/src/python/dxpy/bindings/apollo/vizclient.py b/src/python/dxpy/bindings/apollo/vizclient.py new file mode 100644 index 0000000000..5f3f6a48e8 --- /dev/null +++ b/src/python/dxpy/bindings/apollo/vizclient.py @@ -0,0 +1,36 @@ +from __future__ import print_function +import dxpy + +class VizClient(object): + def __init__(self, url, project_id, error_handler=print): + self.url = url + self.project_id = project_id + self.error_handler = error_handler + + def _get_response(self, payload, resource_url): + try: + response = dxpy.DXHTTPRequest( + resource=resource_url, data=payload, prepend_srv=False + ) + if "error" in response: + if response["error"]["type"] == "InvalidInput": + err_message = ( + "Insufficient permissions due to the project policy.\n" + + response["error"]["message"] + ) + elif response["error"]["type"] == "QueryTimeOut": + err_message = "Please consider using --sql option to generate the SQL query and execute query via a private compute cluster." + else: + err_message = response["error"] + self.error_handler(str(err_message)) + return response + except Exception as details: + self.error_handler(str(details)) + + def get_data(self, payload, record_id): + resource_url = "{}/data/3.0/{}/raw".format(self.url, record_id) + return self._get_response(payload, resource_url) + + def get_raw_sql(self, payload, record_id): + resource_url = "{}/viz-query/3.0/{}/raw-query".format(self.url, record_id) + return self._get_response(payload, resource_url) diff --git a/src/python/dxpy/bindings/apollo/vizserver_filters_from_json_parser.py b/src/python/dxpy/bindings/apollo/vizserver_filters_from_json_parser.py new file mode 100644 index 0000000000..f4a6e7e2fb --- /dev/null +++ b/src/python/dxpy/bindings/apollo/vizserver_filters_from_json_parser.py @@ -0,0 +1,523 @@ +from __future__ import print_function + + +class JSONFiltersValidator(object): + """ + A specialized class that parses user input JSON according to a schema to prepare vizserver-compliant compound filters. + + See assay_filtering_conditions.py for current schemas. + + Filters must be defined in schema["filtering_conditions"] + + There are currently three ways to define filtering_conditions when version is 1.0: + 1. Basic use-case: no "properties" are defined. Only "condition", "table_column" and optionally "max_item_limit" are defined. + In this case, there are no sub-keys for the current key in input_json. + (see 'sample_id' in 'assay_filtering_conditions.EXTRACT_ASSAY_EXPRESSION_FILTERING_CONDITIONS') + 2. Properties defined as dict of dicts (see 'annotation' and 'expression' in EXTRACT_ASSAY_EXPRESSION_FILTERING_CONDITIONS) + 2.1. If filters_combination_operator is not defined, the assumption is that there is only one sub-key in input_json + + 3. Complex use-case: Properties defined as list of dicts (more advanced, special use-case with complex conditional logics that needs translation) + Filtering conditions defined this way indicate that input_json may contain more than one item for the current key, and elements are stored in a list. + In this case, the schema must define "items_combination_operator" and "filters_combination_operator". + items_combination_operator: how to combine filters for list items in input_json + filters_combination_operator: how to combine filters within each item + (see 'location' in EXTRACT_ASSAY_EXPRESSION_FILTERING_CONDITIONS) + + Within 'properties' if "key" is defined, then a generic one-key filter is built. + If "keys" is defined, then more complex use-cases are handled via special conditions that are defined in condition_function_mapping. + + """ + + def __init__(self, input_json, schema, error_handler=print): + self.input_json = input_json + self.schema = schema + self.error_handler = error_handler + self.condition_function_mapping = { + "genobin_partial_overlap": self.build_partial_overlap_genobin_filters, + } + self.SUPPORTED_VIZSERVER_CONDITIONS = [ + "contains", + "exists", + "not-exists", + "any", + "not-any", + "all", + "not-empty", + "in", + "not-in", + "is", + "is-not", + "greater-than", + "less-than", + "greater-than-eq", + "less-than-eq", + "between", + "between-ex", + "between-left-inc", + "between-right-inc", + "not-between", + "not-between-ex", + "not-between-left-inc", + "not-between-right-inc", + "compare-before", + "compare-after", + "compare-within", + ] + + def parse(self): + self.is_valid_json(self.schema) + if self.get_schema_version(self.schema) == "1.0": + return self.parse_v1() + else: + raise NotImplementedError + + def parse_v1(self): + """ + Parse input_json according to schema version 1.0, and build vizserver compound filters. + """ + + try: + # Get the general structure of vizserver-compliant compound filter dict + vizserver_compound_filters = self.get_vizserver_basic_filter_structure() + + # Get 'filtering_conditions' from the schema + all_filters = self.collect_filtering_conditions(self.schema) + + # Go through the input_json (iterate through keys and values in user input JSON) + for filter_key, filter_values in self.input_json.items(): + # Example: if schema is EXTRACT_ASSAY_EXPRESSION_FILTERING_CONDITIONS + # Then input JSON would probably contain location or annotation + # In this case: + # filter_key -> location + # filter_values -> list of dicts (where each dict contains "chromosome", "starting_position", "ending_position") + if filter_key not in all_filters: + self.error_handler( + "No filtering condition was defined for {} in the schema.".format( + filter_key + ) + ) + + # Get the filtering conditions for the current "key" in input_json + current_filters = all_filters[filter_key] + + # Get the properties if any (this might be None, + # if so we will just execute the basic use case defined in the docstring of the class) + current_properties = current_filters.get("properties") + + # Validate max number of allowed items + # if max_item_limit is defined at the top level within this key + # It will be later validated for each property as well + + self.validate_max_item_limit(current_filters, filter_values, filter_key) + + # There are several ways filtering_conditions can be defined + # 1. Basic use-case: no properties, just condition (see 'sample_id' in 'EXTRACT_ASSAY_EXPRESSION_FILTERING_CONDITIONS') + # 2. Properties defined as dict of dicts (see 'annotation' and 'expression') + # 3. Properties defined as list of dicts (more advanced, special use-case with complex conditional logics that needs translation) + # For more information see the docstring of the class + # The following if conditions will go through each of the aforementioned scenarios + + if isinstance(current_properties, list) and isinstance( + filter_values, list + ): + filters = self.parse_list_v1( + current_filters, + filter_values, + current_properties, + ) + + if filters is not None: + vizserver_compound_filters["compound"].append(filters) + filters = None + + if isinstance(current_properties, dict): + for k, v in current_properties.items(): + if k in filter_values: + self.validate_max_item_limit(v, filter_values[k], k) + + filters = self.parse_dict_v1( + current_filters, filter_values, current_properties + ) + if filters is not None: + vizserver_compound_filters["compound"].append(filters) + filters = None + + if current_properties is None: + # no properties, so just apply conditions + # In other words .get("properties") returns None + # Therefore we are dealing with a basic use-case scenario + # (See 'sample_id' in 'EXTRACT_ASSAY_EXPRESSION_FILTERING_CONDITIONS' for an example) + filters = self.build_one_key_generic_filter( + current_filters.get("table_column"), + current_filters.get("condition"), + filter_values, + ) + + if filters is not None: + vizserver_compound_filters["compound"].append(filters) + filters = None + + return vizserver_compound_filters + + except Exception as e: + self.error_handler(str(e)) + + def parse_list_v1(self, current_filters, filter_values, current_properties): + if current_properties is None: + self.error_handler("Expected properties to be defined within schema.") + + # There are two important aspects: + # Filters (if more than one) must be compounded for each item with filters_combination_operator logic + # All items must be compounded together as one large compounded filter with the logic defined in items_combination_operator + + full_filter_for_all_items = { + "logic": current_filters.get("items_combination_operator"), + "compound": [ + # {base_filter_for_each_item_1}, + # {base_filter_for_each_item_2}, + # {etc.} + # as many dicts as there are "items" in input_json[filter_key] + ], + } + + ### current_properties is a list of dicts and each dict is a filter + ### therefore, we need to iterate over each dict and build a filter for each one + ### However, also remember than inside filter_values from input_json we have a list of dicts + ### therefore there might be more than one element within that list + ### so we need to build the filters for each element in the list as well + ### and then append them to the compound list of dicts + + # Build filters for each item in the list (input_json[filter_values]) + for current_list_item in filter_values: + # Consider keeping track of input_json keys and properties evaluated and used so far + + current_compound_filter = { + "logic": current_filters.get("filters_combination_operator"), + "compound": [ + # {}, + # {}, + # here there will be as many dicts inside as there are qualifying [key] conditions + ], + } + + ## properties['key'] -> simple case + ## properties['keys'] -> reserved for complex, special cases + ## specifically for those cases where SUPPORTED_VIZSERVER_CONDITIONS is not sufficient + + for item in current_properties: + if item.get("key"): + # Consider a separate check for list type + # Currently not necessary + temp_filter = self.build_one_key_generic_filter( + item["table_column"], + item["condition"], + current_list_item[item.get("key")], + ) + + current_compound_filter["compound"].append(temp_filter) + + if item.get("keys"): + if len(item.get("keys")) == 2: + if ( + item.get("condition") + not in self.SUPPORTED_VIZSERVER_CONDITIONS + ): + special_filtering_function = ( + self.condition_function_mapping[item.get("condition")] + ) + temp_filter = special_filtering_function( + item, current_list_item + ) + current_compound_filter["compound"].append(temp_filter) + + # Consider checking if current_compound_filter contains any new elements before appending + full_filter_for_all_items["compound"].append(current_compound_filter) + + return full_filter_for_all_items + + def parse_dict_v1(self, current_filters, filter_values, current_properties): + if current_properties is None: + self.error_handler("Expected properties to be defined within schema.") + + # if no filters_combination_operator is specified, then single filter assumed + + filtering_logic = current_filters.get("filters_combination_operator") + + # if filtering_logic isn't defined at this level then there must be only one 'key' to filter on + if filtering_logic: + if len(filter_values) == 1: + temp_key = next(iter(filter_values.keys())) + matched_filter = current_properties[temp_key] + temp_filter = self.build_one_key_generic_filter( + matched_filter.get("table_column"), + matched_filter.get("condition"), + filter_values.get(temp_key), + ) + return temp_filter + + if len(filter_values) == 2: + temp_keys = list(filter_values.keys()) + first_key = current_properties[temp_keys[0]] + second_key = current_properties[temp_keys[1]] + + # check if there are two filtering conditions that need to be applied on the same table_column + # check if both of those are defined in input_json + # an example of such a case is 'expression' in EXTRACT_ASSAY_EXPRESSION_FILTERING_CONDITIONS + # where we might have a `max_value` and a `min_value` + # however providing both is not mandatory + + if first_key.get("table_column") == second_key.get("table_column"): + if filtering_logic == "and": + # where possible we will use "between" operator + # instead of defining two separate conditions with greater-than AND less-than + temp_condition = self.convert_to_between_operator( + [ + first_key.get("condition"), + second_key.get("condition"), + ] + ) + if temp_condition in ["between", "between-ex"]: + temp_values = [ + filter_values.get(temp_keys[0]), + filter_values.get(temp_keys[1]), + ] + temp_values.sort() + temp_filter = self.build_one_key_generic_filter( + first_key.get("table_column"), + temp_condition, + temp_values, + ) + return temp_filter + + else: + # A more complex conversion scenario + # that may not involve greater-than/less-than operators + # This is currently neither implemented nor needed + raise NotImplementedError + + if filtering_logic == "or": + # A special edge case that should probably never be used + # It is also not supported by vizserver + # In other words, 'or' logic is not supported for + # values for the same key in the filter. + # If "or" logic needs to be applied to two filters + # both with the same 'key', consider constructing two separate + # filters and then compounding them with "or" logic + raise NotImplementedError + + else: + # This is currently not needed anywhere + raise NotImplementedError + + else: + # There's no filtering logic, in other words, filters_combination_operator is not defined at this level + # (see 'annotation' in 'EXTRACT_ASSAY_EXPRESSION_FILTERING_CONDITIONS' for an example) + if len(current_properties) > 1: + if len(filter_values) > 1: + # if there are also more than 1 in input_json + self.error_handler( + "More than one filter found at this level, but no filters_combination_operator was specified." + ) + else: + temp_key = next(iter(filter_values.keys())) + matched_filter = current_properties[temp_key] + temp_filter = self.build_one_key_generic_filter( + matched_filter.get("table_column"), + matched_filter.get("condition"), + filter_values.get(temp_key), + ) + return temp_filter + + def get_vizserver_basic_filter_structure(self): + return { + "logic": self.get_toplevel_filtering_logic(), + "compound": [], + } + + def collect_input_filters(self): + return self.input_json.keys() + + def get_toplevel_filtering_logic(self): + return self.schema.get("filters_combination_operator") + + def collect_output_fields_mappings(self, schema): + return schema.get("output_fields_mapping") + + def collect_filtering_conditions(self, schema): + return schema.get("filtering_conditions") + + def validate_max_item_limit(self, current, input_json_values, field_name): + max_item_limit = current.get("max_item_limit") + if not max_item_limit: + return True + + if len(input_json_values) > max_item_limit: + self.error_handler( + "Too many items given in field {}, maximum is {}.".format( + field_name, max_item_limit + ) + ) + + def is_valid_json(self, schema): + if not isinstance(schema, dict) or not schema: + self.error_handler("Schema must be a non-empty dict.") + + def get_schema_version(self, schema): + return schema.get("version") + + def get_number_of_filters(self): + return len(self.input_json) + + def convert_to_between_operator(self, operators_list): + if len(operators_list) != 2: + self.error_handler("Expected exactly two operators") + + if set(operators_list) == {"greater-than-eq", "less-than-eq"}: + return "between" + + if set(operators_list) == {"greater-than", "less-than"}: + return "between-ex" + + def build_one_key_generic_filter( + self, table_column_mapping, condition, value, return_complete_filter=True + ): + """ + { + "filters": {"expr_annotation$chr": [{"condition": "is", "values": 5}]}, + } + """ + base_filter = { + table_column_mapping: [{"condition": condition, "values": value}] + } + + if return_complete_filter: + return {"filters": base_filter} + else: + return base_filter + + def build_partial_overlap_genobin_filters( + self, filtering_condition, input_json_item + ): + """ + + Returns a nested compound filter in the following format: + + filter = { + "logic": "or", + "compound": [ + { + "filters": { + db_table_column_start: [ + { + "condition": "between", + "values": [input_start_value, input_end_value], + } + ], + db_table_column_end: [ + { + "condition": "between", + "values": [input_start_value, input_end_value], + } + ], + }, + "logic": "or", + }, + { + "filters": { + db_table_column_start: [ + {"condition": "less-than", "values": input_start_value} + ], + db_table_column_end: [ + {"condition": "greater-than", "values": input_end_value} + ], + }, + "logic": "and", + }, + ], + } + + """ + if ( + "starting_position" not in filtering_condition["keys"] + or "ending_position" not in filtering_condition["keys"] + or "starting_position" not in input_json_item + or "ending_position" not in input_json_item + ): + self.error_handler( + "Error in location filtering. starting_position and ending_position must be defined in filtering conditions" + ) + + db_table_column_start = filtering_condition["table_column"]["starting_position"] + db_table_column_end = filtering_condition["table_column"]["ending_position"] + + input_start_value = int(input_json_item["starting_position"]) + input_end_value = int(input_json_item["ending_position"]) + + start_filter = self.build_one_key_generic_filter( + db_table_column_start, + "between", + [input_start_value, input_end_value], + return_complete_filter=False, + ) + end_filter = self.build_one_key_generic_filter( + db_table_column_end, + "between", + [input_start_value, input_end_value], + return_complete_filter=False, + ) + compound_start_filter = self.build_one_key_generic_filter( + db_table_column_start, + "less-than-eq", + input_start_value, + return_complete_filter=False, + ) + + compound_end_filter = self.build_one_key_generic_filter( + db_table_column_end, + "greater-than-eq", + input_end_value, + return_complete_filter=False, + ) + + filter_structure = { + "logic": "or", + "compound": [ + { + "filters": {}, + "logic": "or", + }, + { + "filters": {}, + "logic": "and", + }, + ], + } + + filter_structure["compound"][0]["filters"].update(start_filter) + filter_structure["compound"][0]["filters"].update(end_filter) + filter_structure["compound"][1]["filters"].update(compound_start_filter) + filter_structure["compound"][1]["filters"].update(compound_end_filter) + + # In Python 3, this can be simply done via: + + # return { + # "logic": "or", + # "compound": [ + # { + # "filters": { + # **start_filter, + # **end_filter, + # }, + # "logic": "or", + # }, + # { + # "filters": { + # **compound_start_filter, + # **compound_end_filter, + # }, + # "logic": "and", + # }, + # ], + # } + + return filter_structure diff --git a/src/python/dxpy/bindings/apollo/vizserver_payload_builder.py b/src/python/dxpy/bindings/apollo/vizserver_payload_builder.py new file mode 100644 index 0000000000..3c16b31eea --- /dev/null +++ b/src/python/dxpy/bindings/apollo/vizserver_payload_builder.py @@ -0,0 +1,155 @@ +import sys + + +class VizPayloadBuilder(object): + """ + + 'filters' and/or 'raw_filters' can be built with the help of vizserver_filters_from_json_parser.JSONFiltersValidator + + assemble_assay_raw_filters is a helper method to build a complete raw_filters structure for a single assay, + if raw_filters does not already have this information. + + Example usage: + + payload = VizPayloadBuilder( + "project-xyz", + { + "feature_id": "expr_annotation$feature_id", + "sample_id": "expression$sample_id", + "expression": "expression$value", + }, + error_handler=print, + ) + + payload.assemble_assay_raw_filters( + assay_name="xyz", + assay_id="a-b1-2c-f-xyz-test", + filters={ + "logic": "or", + "compound": [ + { + "test": "1", + }, + {"exmaple": 3}, + ], + }, + ) + + # Hint: Use vizserver_filters_from_json_parser.JSONFiltersValidator to build "filters" + + final_payload = payload.build() + + + """ + + def __init__( + self, + project_context, + output_fields_mapping, + raw_filters=None, + filters=None, + order_by=None, + limit=None, + base_sql=None, + is_cohort=False, + return_query=False, + error_handler=None, + ): + self.project_context = project_context + self.output_fields_mapping = output_fields_mapping + self.raw_filters = raw_filters + self.filters = filters + self.order_by = order_by + self.limit = limit + self.base_sql = base_sql + self.is_cohort = is_cohort + self.return_query = return_query + self.error_handler = error_handler + + if self.error_handler is None: + raise Exception("error_handler must be defined") + + def build(self): + payload = self.get_vizserver_payload_structure() + + if self.is_cohort and self.base_sql: + self.validate_base_sql() + payload.update({"base_sql": self.base_sql, "is_cohort": self.is_cohort}) + + if self.base_sql and not self.is_cohort: + self.error_handler( + "base_sql is only allowed for cohorts. is_cohort must be set to True" + ) + + if self.limit: + self.validate_returned_records_limit() + payload.update({"limit": self.limit}) + + if self.raw_filters: + payload.update(self.raw_filters) + + if self.filters: + payload.update(self.filters) + + if self.order_by: + payload.update({"order_by": self.order_by}) + + return payload + + def assemble_assay_raw_filters(self, assay_name, assay_id, filters): + """ + Helper method to build a complete raw_filters structure for a single assay + if raw_filters does not already have this information. + + filters may be a dict with the following structure: + { + "logic": "or", + "compound": [ + { + ... + } + ] + } + + or any other structure that is accepted by vizserver within assay_filters, e.g. + { + "filters": { + ... + } + } + """ + raw_filters = { + "raw_filters": { + "assay_filters": { + "name": assay_name, + "id": assay_id, + } + } + } + + raw_filters["raw_filters"]["assay_filters"].update(filters) + + self.raw_filters = raw_filters + + def get_vizserver_payload_structure(self): + return { + "project_context": self.project_context, + "fields": self.output_fields_mapping, + "return_query": self.return_query, + } + + def validate_base_sql(self): + THROW_ERROR = False + if sys.version_info.major == 2: + if not isinstance(self.base_sql, (str, unicode)) or self.base_sql == "": + THROW_ERROR = True + else: + if not isinstance(self.base_sql, str) or "".__eq__(self.base_sql): + THROW_ERROR = True + + if THROW_ERROR: + self.error_handler("base_sql is either not a string or is empty") + + def validate_returned_records_limit(self): + if not isinstance(self.limit, int) or self.limit <= 0: + self.error_handler("limit must be a positive integer") diff --git a/src/python/dxpy/bindings/dxapp.py b/src/python/dxpy/bindings/dxapp.py index 08bab0f70c..da78f62743 100644 --- a/src/python/dxpy/bindings/dxapp.py +++ b/src/python/dxpy/bindings/dxapp.py @@ -61,7 +61,7 @@ 'developers', 'authorizedUsers', 'regionalOptions'] # These are optional keys for apps, not sure what to do with them -_app_optional_keys = ['details', 'categories', 'access', 'ignoreReuse'] +_app_optional_keys = ['details', 'categories', 'access', 'ignoreReuse', 'treeTurnaroundTimeThreshold'] _app_describe_output_keys = [] diff --git a/src/python/dxpy/bindings/dxapplet.py b/src/python/dxpy/bindings/dxapplet.py index 92189b8728..3be1ab9cc9 100644 --- a/src/python/dxpy/bindings/dxapplet.py +++ b/src/python/dxpy/bindings/dxapplet.py @@ -29,6 +29,7 @@ import dxpy from . import DXDataObject, DXJob from ..utils import merge +from ..utils.resolver import is_project_id from ..system_requirements import SystemRequirementsDict from ..exceptions import DXError from ..compat import basestring @@ -36,8 +37,9 @@ class DXExecutable: '''Methods in :class:`!DXExecutable` are used by :class:`~dxpy.bindings.dxapp.DXApp`, - :class:`~dxpy.bindings.dxapplet.DXApplet`, and - :class:`~dxpy.bindings.dxworkflow.DXWorkflow` + :class:`~dxpy.bindings.dxapplet.DXApplet`, + :class:`~dxpy.bindings.dxworkflow.DXWorkflow`, and + :class:`~dxpy.bindings.dxworkflow.DXGlobalWorkflow` ''' def __init__(self, *args, **kwargs): raise NotImplementedError("This class is a mix-in. Use DXApp or DXApplet instead.") @@ -56,10 +58,18 @@ def _get_run_input_common_fields(executable_input, **kwargs): if kwargs.get(arg) is not None: run_input[arg] = kwargs[arg] - if kwargs.get('instance_type') is not None or kwargs.get('cluster_spec') is not None: + if any(kwargs.get(key) is not None for key in ['instance_type', 'cluster_spec', 'fpga_driver', 'nvidia_driver']): instance_type_srd = SystemRequirementsDict.from_instance_type(kwargs.get('instance_type')) cluster_spec_srd = SystemRequirementsDict(kwargs.get('cluster_spec')) - run_input["systemRequirements"] = (instance_type_srd + cluster_spec_srd).as_dict() + fpga_driver_srd = SystemRequirementsDict(kwargs.get('fpga_driver')) + nvidia_driver_srd = SystemRequirementsDict(kwargs.get('nvidia_driver')) + run_input["systemRequirements"] = (instance_type_srd + cluster_spec_srd + fpga_driver_srd + nvidia_driver_srd).as_dict() + + if kwargs.get('system_requirements') is not None: + run_input["systemRequirements"] = kwargs.get('system_requirements') + + if kwargs.get('system_requirements_by_executable') is not None: + run_input["systemRequirementsByExecutable"] = kwargs.get('system_requirements_by_executable') if kwargs.get('depends_on') is not None: run_input["dependsOn"] = [] @@ -91,7 +101,7 @@ def _get_run_input_common_fields(executable_input, **kwargs): if kwargs.get('ignore_reuse') is not None: run_input["ignoreReuse"] = kwargs['ignore_reuse'] - if dxpy.JOB_ID is None: + if dxpy.JOB_ID is None or (kwargs.get('detach') is True and project is not None and is_project_id(project)): run_input["project"] = project if kwargs.get('extra_args') is not None: @@ -103,6 +113,22 @@ def _get_run_input_common_fields(executable_input, **kwargs): if kwargs.get('cost_limit') is not None: run_input["costLimit"] = kwargs['cost_limit'] + if kwargs.get('rank') is not None: + run_input["rank"] = kwargs['rank'] + + if kwargs.get('max_tree_spot_wait_time') is not None: + run_input["maxTreeSpotWaitTime"] = kwargs['max_tree_spot_wait_time'] + + if kwargs.get('max_job_spot_wait_time') is not None: + run_input["maxJobSpotWaitTime"] = kwargs['max_job_spot_wait_time'] + + if kwargs.get('detailed_job_metrics') is not None: + run_input["detailedJobMetrics"] = kwargs['detailed_job_metrics'] + + preserve_job_outputs = kwargs.get('preserve_job_outputs') + if preserve_job_outputs is not None and preserve_job_outputs != False: + run_input["preserveJobOutputs"] = {} if preserve_job_outputs == True else preserve_job_outputs + return run_input @staticmethod @@ -117,7 +143,11 @@ def _get_run_input_fields_for_applet(executable_input, **kwargs): if kwargs.get(unsupported_arg): raise DXError(unsupported_arg + ' is not supported for applets (only workflows)') - return DXExecutable._get_run_input_common_fields(executable_input, **kwargs) + run_input = DXExecutable._get_run_input_common_fields(executable_input, **kwargs) + + if kwargs.get('head_job_on_demand') is not None: + run_input["headJobOnDemand"] = kwargs['head_job_on_demand'] + return run_input def _run_impl(self, run_input, **kwargs): """ @@ -163,8 +193,10 @@ def _get_cleanup_keys(self): def run(self, executable_input, project=None, folder=None, name=None, tags=None, properties=None, details=None, instance_type=None, stage_instance_types=None, stage_folders=None, rerun_stages=None, cluster_spec=None, - depends_on=None, allow_ssh=None, debug=None, delay_workspace_destruction=None, priority=None, - ignore_reuse=None, ignore_reuse_stages=None, detach=None, cost_limit=None, extra_args=None, **kwargs): + depends_on=None, allow_ssh=None, debug=None, delay_workspace_destruction=None, priority=None, head_job_on_demand=None, + ignore_reuse=None, ignore_reuse_stages=None, detach=None, cost_limit=None, rank=None, max_tree_spot_wait_time=None, + max_job_spot_wait_time=None, preserve_job_outputs=None, detailed_job_metrics=None, extra_args=None, + fpga_driver=None, system_requirements=None, system_requirements_by_executable=None, nvidia_driver=None, **kwargs): ''' :param executable_input: Hash of the executable's input arguments :type executable_input: dict @@ -190,8 +222,10 @@ def run(self, executable_input, project=None, folder=None, name=None, tags=None, :type debug: dict :param delay_workspace_destruction: Whether to keep the job's temporary workspace around for debugging purposes for 3 days after it succeeds or fails :type delay_workspace_destruction: boolean - :param priority: Priority level to request for all jobs created in the execution tree, either "normal" or "high" + :param priority: Priority level to request for all jobs created in the execution tree, "low", "normal", or "high" :type priority: string + :param head_job_on_demand: If true, the job will be run on a demand instance. + :type head_job_on_demand: bool :param ignore_reuse: Disable job reuse for this execution :type ignore_reuse: boolean :param ignore_reuse_stages: Stages of a workflow (IDs, names, or indices) or "*" for which job reuse should be disabled @@ -200,16 +234,34 @@ def run(self, executable_input, project=None, folder=None, name=None, tags=None, :type detach: boolean :param cost_limit: Maximum cost of the job before termination. :type cost_limit: float + :param rank: Rank of execution + :type rank: int + :param max_tree_spot_wait_time: Number of seconds allocated to each path in the root execution's tree to wait for Spot + :type max_tree_spot_wait_time: int + :param max_job_spot_wait_time: Number of seconds allocated to each job in the root execution's tree to wait for Spot + :type max_job_spot_wait_time: int + :param preserve_job_outputs: Copy cloneable outputs of every non-reused job entering "done" state in this root execution to a folder in the project. If value is True it will place job outputs into the "intermediateJobOutputs" subfolder under the output folder for the root execution. If the value is dict, it may contains "folder" key with desired folder path. If the folder path starts with '/' it refers to an absolute path within the project, otherwise, it refers to a subfolder under root execution's output folder. + :type preserve_job_outputs: boolean or dict + :param detailed_job_metrics: Enable detailed job metrics for this root execution + :type detailed_job_metrics: boolean :param extra_args: If provided, a hash of options that will be merged into the underlying JSON given for the API call :type extra_args: dict :returns: Object handler of the newly created job + :param fpga_driver: a dict mapping function names to fpga driver requests + :type fpga_driver: dict + :param system_requirements: System requirement single mapping + :type system_requirements: dict + :param system_requirements_by_executable: System requirement by executable double mapping + :type system_requirements_by_executable: dict + :param nvidia_driver: a dict mapping function names to nvidia driver requests + :type nvidia_driver: dict :rtype: :class:`~dxpy.bindings.dxjob.DXJob` Creates a new job that executes the function "main" of this executable with the given input *executable_input*. ''' - # stage_instance_types, stage_folders, and rerun_stages are + # stage_instance_types, stage_folders, rerun_stages and ignore_reuse_stages are # only supported for workflows, but we include them # here. Applet-based executables should detect when they # receive a truthy workflow-specific value and raise an error. @@ -232,9 +284,19 @@ def run(self, executable_input, project=None, folder=None, name=None, tags=None, debug=debug, delay_workspace_destruction=delay_workspace_destruction, priority=priority, + head_job_on_demand = head_job_on_demand, detach=detach, cost_limit=cost_limit, - extra_args=extra_args) + rank=rank, + max_tree_spot_wait_time=max_tree_spot_wait_time, + max_job_spot_wait_time=max_job_spot_wait_time, + preserve_job_outputs=preserve_job_outputs, + detailed_job_metrics=detailed_job_metrics, + extra_args=extra_args, + fpga_driver=fpga_driver, + system_requirements=system_requirements, + system_requirements_by_executable=system_requirements_by_executable, + nvidia_driver=nvidia_driver) return self._run_impl(run_input, **kwargs) @@ -244,7 +306,7 @@ def run(self, executable_input, project=None, folder=None, name=None, tags=None, _applet_required_keys = ['name', 'title', 'summary', 'types', 'tags', 'httpsApp', 'properties', 'dxapi', 'inputSpec', 'outputSpec', 'runSpec', 'access', 'details'] -_applet_optional_keys = ['ignoreReuse'] +_applet_optional_keys = ['ignoreReuse', 'treeTurnaroundTimeThreshold'] _applet_describe_output_keys = ['properties', 'details'] _applet_cleanup_keys = ['name', 'title', 'summary', 'types', 'tags', 'properties', 'runSpec', 'access', 'details'] diff --git a/src/python/dxpy/bindings/dxdatabase_functions.py b/src/python/dxpy/bindings/dxdatabase_functions.py index 22298fea8c..4a774b3416 100644 --- a/src/python/dxpy/bindings/dxdatabase_functions.py +++ b/src/python/dxpy/bindings/dxdatabase_functions.py @@ -36,7 +36,7 @@ from . import dxfile, DXFile from . import dxdatabase, DXDatabase from .dxfile import FILE_REQUEST_TIMEOUT -from ..compat import open, USING_PYTHON2 +from ..compat import open, USING_PYTHON2, md5_hasher from ..exceptions import DXFileError, DXChecksumMismatchError, DXIncompleteReadsError, err_exit from ..utils import response_iterator import subprocess @@ -220,10 +220,7 @@ def get_chunk(part_id_to_get, start, end): url, headers = dxdatabase.get_download_url(src_filename=src_filename, project=project, **kwargs) # No sub ranges for database file downloads sub_range = False - data_url = dxpy._dxhttp_read_range(url, headers, start, end, FILE_REQUEST_TIMEOUT, sub_range) - do_debug("dxdatabase_functions.py get_chunk - data_url = {}".format(data_url)) - # 'data_url' is the s3 URL, so read again, just like in DNAxFileSystem - data = dxpy._dxhttp_read_range(data_url, headers, start, end, FILE_REQUEST_TIMEOUT, sub_range) + data = dxpy._dxhttp_read_range(url, headers, start, end, FILE_REQUEST_TIMEOUT, sub_range) return part_id_to_get, data def chunk_requests(): @@ -257,7 +254,8 @@ def verify_part(_part_id, got_bytes, hasher): if chunk_part != cur_part: # TODO: remove permanently if we don't find use for this # verify_part(cur_part, got_bytes, hasher) - cur_part, got_bytes, hasher = chunk_part, 0, hashlib.md5() + + cur_part, got_bytes, hasher = chunk_part, 0, md5_hasher() got_bytes += len(chunk_data) hasher.update(chunk_data) fh.write(chunk_data) diff --git a/src/python/dxpy/bindings/dxfile.py b/src/python/dxpy/bindings/dxfile.py index 15d732a25f..1d31324faf 100644 --- a/src/python/dxpy/bindings/dxfile.py +++ b/src/python/dxpy/bindings/dxfile.py @@ -34,7 +34,7 @@ from ..exceptions import DXFileError, DXIncompleteReadsError from ..utils import warn from ..utils.resolver import object_exists_in_project -from ..compat import BytesIO, basestring, USING_PYTHON2 +from ..compat import BytesIO, basestring, USING_PYTHON2, md5_hasher DXFILE_HTTP_THREADS = min(cpu_count(), 8) @@ -658,7 +658,7 @@ def upload_part(self, data, index=None, display_progress=False, report_progress_ :type display_progress: boolean :param report_progress_fn: Optional: a function to call that takes in two arguments (self, # bytes transmitted) :type report_progress_fn: function or None - :raises: :exc:`dxpy.exceptions.DXFileError` if *index* is given and is not in the correct range, :exc:`requests.exceptions.HTTPError` if upload fails + :raises: :exc:`dxpy.exceptions.DXFileError` if *index* is given and is not in the correct range, :exc:`urllib3.exceptions.HTTPError` if upload fails Uploads the data in *data* as part number *index* for the associated file. If no value for *index* is given, *index* @@ -673,7 +673,7 @@ def upload_part(self, data, index=None, display_progress=False, report_progress_ if index is not None: req_input["index"] = int(index) - md5 = hashlib.md5() + md5 = md5_hasher() if hasattr(data, 'seek') and hasattr(data, 'tell'): # data is a buffer; record initial position (so we can rewind back) rewind_input_buffer_offset = data.tell() @@ -709,6 +709,7 @@ def get_upload_url_and_headers(): # The file upload API requires us to get a pre-authenticated upload URL (and headers for it) every time we # attempt an upload. Because DXHTTPRequest will retry requests under retryable conditions, we give it a callback # to ask us for a new upload URL every time it attempts a request (instead of giving them directly). + dxpy.DXHTTPRequest(get_upload_url_and_headers, data, jsonify_data=False, @@ -726,6 +727,10 @@ def get_upload_url_and_headers(): if report_progress_fn is not None: report_progress_fn(self, len(data)) + def wait_until_parts_uploaded(self, **kwargs): + self._wait_until_parts_uploaded(**kwargs) + + def get_download_url(self, duration=None, preauthenticated=False, filename=None, project=None, **kwargs): """ :param duration: number of seconds for which the generated URL will be @@ -759,39 +764,39 @@ def get_download_url(self, duration=None, preauthenticated=False, filename=None, file. """ - args = {"preauthenticated": preauthenticated} - - if duration is not None: - args["duration"] = duration - if filename is not None: - args["filename"] = filename - - # If project=None, we fall back to the project attached to this handler - # (if any). If this is supplied, it's treated as a hint: if it's a - # project in which this file exists, it's passed on to the - # apiserver. Otherwise, NO hint is supplied. In principle supplying a - # project in the handler that doesn't contain this file ought to be an - # error, but it's this way for backwards compatibility. We don't know - # who might be doing downloads and creating handlers without being - # careful that the project encoded in the handler contains the file - # being downloaded. They may now rely on such behavior. - if project is None and 'DX_JOB_ID' not in os.environ: - project_from_handler = self.get_proj_id() - if project_from_handler and object_exists_in_project(self.get_id(), project_from_handler): - project = project_from_handler - - if project is not None and project is not DXFile.NO_PROJECT_HINT: - args["project"] = project - - # Test hook to write 'project' argument passed to API call to a - # local file - if '_DX_DUMP_BILLED_PROJECT' in os.environ: - with open(os.environ['_DX_DUMP_BILLED_PROJECT'], "w") as fd: - if project is not None and project != DXFile.NO_PROJECT_HINT: - fd.write(project) - with self._url_download_mutex: + # Only generate URL if not already cached or expired if self._download_url is None or self._download_url_expires < time.time(): + args = {"preauthenticated": preauthenticated} + if duration is not None: + args["duration"] = duration + if filename is not None: + args["filename"] = filename + + # If project=None, we fall back to the project attached to this handler + # (if any). If this is supplied, it's treated as a hint: if it's a + # project in which this file exists, it's passed on to the + # apiserver. Otherwise, NO hint is supplied. In principle supplying a + # project in the handler that doesn't contain this file ought to be an + # error, but it's this way for backwards compatibility. We don't know + # who might be doing downloads and creating handlers without being + # careful that the project encoded in the handler contains the file + # being downloaded. They may now rely on such behavior. + if project is None and 'DX_JOB_ID' not in os.environ: + project_from_handler = self.get_proj_id() + # object_exists_in_project will call /file-xxxx/describe, which is skipped if the URL is cached + if project_from_handler and object_exists_in_project(self.get_id(), project_from_handler): + project = project_from_handler + + if project is not None and project is not DXFile.NO_PROJECT_HINT: + args["project"] = project + + # Test hook to write 'project' argument passed to API call to a + # local file + if '_DX_DUMP_BILLED_PROJECT' in os.environ: + with open(os.environ['_DX_DUMP_BILLED_PROJECT'], "w") as fd: + if project is not None and project != DXFile.NO_PROJECT_HINT: + fd.write(project) # The idea here is to cache a download URL for the entire file, that will # be good for a few minutes. This avoids each thread having to ask the # server for a URL, increasing server load. diff --git a/src/python/dxpy/bindings/dxfile_functions.py b/src/python/dxpy/bindings/dxfile_functions.py index c6e3331501..7719470052 100644 --- a/src/python/dxpy/bindings/dxfile_functions.py +++ b/src/python/dxpy/bindings/dxfile_functions.py @@ -37,8 +37,8 @@ from .. import logger from . import dxfile, DXFile from .dxfile import FILE_REQUEST_TIMEOUT -from ..compat import open, USING_PYTHON2 -from ..exceptions import DXFileError, DXPartLengthMismatchError, DXChecksumMismatchError, DXIncompleteReadsError, err_exit +from ..exceptions import DXError, DXFileError, DXPartLengthMismatchError, DXChecksumMismatchError, DXIncompleteReadsError, err_exit +from ..compat import open, md5_hasher, USING_PYTHON2 from ..utils import response_iterator import subprocess @@ -97,7 +97,7 @@ def new_dxfile(mode=None, write_buffer_size=dxfile.DEFAULT_BUFFER_SIZE, expected def download_dxfile(dxid, filename, chunksize=dxfile.DEFAULT_BUFFER_SIZE, append=False, show_progress=False, - project=None, describe_output=None, **kwargs): + project=None, describe_output=None, symlink_max_tries=15, **kwargs): ''' :param dxid: DNAnexus file ID or DXFile (file handler) object :type dxid: string or DXFile @@ -114,6 +114,8 @@ def download_dxfile(dxid, filename, chunksize=dxfile.DEFAULT_BUFFER_SIZE, append It should contain the default fields of the describe API call output and the "parts" field, not included in the output by default. :type describe_output: dict or None + :param symlink_max_tries: Maximum amount of tries when downloading a symlink with aria2c. + :type symlink_max_tries: int or None Downloads the remote file referenced by *dxid* and saves it to *filename*. @@ -134,6 +136,7 @@ def download_dxfile(dxid, filename, chunksize=dxfile.DEFAULT_BUFFER_SIZE, append show_progress=show_progress, project=project, describe_output=describe_output, + symlink_max_tries=symlink_max_tries, **kwargs) @@ -173,67 +176,59 @@ def _verify(filename, md5digest): err_exit("Checksum doesn't match " + str(actual_md5) + " expected:" + str(md5digest)) print("Checksum correct") + # [dxid] is a symbolic link. Create a preauthenticated URL, # and download it -def _download_symbolic_link(dxid, md5digest, project, dest_filename): - dxfile = dxpy.DXFile(dxid) - url, _headers = dxfile.get_download_url(preauthenticated=True, - duration=6*3600, - project=project) +def _download_symbolic_link(dxid, md5digest, project, dest_filename, symlink_max_tries=15): + if symlink_max_tries < 1: + raise dxpy.exceptions.DXError("symlink_max_tries argument has to be positive integer") - def call_cmd(cmd, max_retries=6, num_attempts=0): - try: - if aria2c_exe is not None: - print("Downloading symbolic link with aria2c") - else: - print("Downloading symbolic link with wget") - subprocess.check_call(cmd, stderr=subprocess.STDOUT) - except subprocess.CalledProcessError as e: - msg = "" - if e and e.output: - msg = e.output.strip() - if e.returncode == 22 and num_attempts <= max_retries: # hotfix, DEVEX-1779 - time_to_wait = 1 if num_attempts <= 2 else min(randint(2 ** (num_attempts - 2), 2 ** (num_attempts - 1)), 60) - print("Download failed with code 22. Retrying after {} seconds... Error details: cmd: {}\nmsg: {}\n".format(str(time_to_wait), str(cmd), msg)) - sleep(time_to_wait) - call_cmd(cmd, max_retries, num_attempts + 1) - err_exit("Failed to call download: {cmd}\n{msg}\n".format(cmd=str(cmd), msg=msg)) - - # Follow the redirection - print('Following redirect for ' + url) - - # Check if aria2 present - # Use that instead of wget + # Check if aria2 present, if not, error. aria2c_exe = _which("aria2c") + if aria2c_exe is None: - wget_exe = _which("wget") - if wget_exe is None: - err_exit("wget is not installed on this system") - - cmd = ["wget", "--tries=20", "--quiet"] - if os.path.isfile(dxid): - # file already exists, resume upload. - cmd += ["--continue"] - cmd += ["-O", dest_filename, url] + err_exit("aria2c must be installed on this system to download this data. " + \ + "Please see the documentation at https://aria2.github.io/.") + return + + if isinstance(dxid, DXFile): + dxf = dxid else: - print("aria2c found in path so using that instead of wget \n") - # aria2c does not allow more than 16 connections per server - max_connections = min(16, multiprocessing.cpu_count()) - cmd = ["aria2c", "--check-certificate=false", "-s", str(max_connections), "-x", str(max_connections), "--retry-wait=10", "--max-tries=15"] - # Split path properly for aria2c - # If '-d' arg not provided, aria2c uses current working directory - cwd = os.getcwd() - directory, filename = os.path.split(dest_filename) - directory = cwd if directory in ["", cwd] else directory - cmd += ["-o", filename, "-d", os.path.abspath(directory), url] - call_cmd(cmd) + dxf = dxpy.DXFile(dxid) + + url, _headers = dxf.get_download_url(preauthenticated=True, + duration=6*3600, + project=project) + + # aria2c does not allow more than 16 connections per server + max_connections = min(16, multiprocessing.cpu_count()) + cmd = [ + "aria2c", + "-c", # continue downloading a partially downloaded file + "-s", str(max_connections), # number of concurrent downloads (split file) + "-x", str(max_connections), # maximum number of connections to one server for each download + "--retry-wait=10" # time to wait before retrying + ] + cmd.extend(["-m", str(symlink_max_tries)]) + + # Split path properly for aria2c + # If '-d' arg not provided, aria2c uses current working directory + cwd = os.getcwd() + directory, filename = os.path.split(dest_filename) + directory = cwd if directory in ["", cwd] else directory + cmd += ["-o", filename, "-d", os.path.abspath(directory), url] + + try: + subprocess.check_call(cmd, stderr=subprocess.STDOUT) + except subprocess.CalledProcessError as e: + err_exit("Failed to call download: {cmd}\n{msg}\n".format(cmd=str(cmd), msg=e)) if md5digest is not None: _verify(dest_filename, md5digest) def _download_dxfile(dxid, filename, part_retry_counter, chunksize=dxfile.DEFAULT_BUFFER_SIZE, append=False, show_progress=False, - project=None, describe_output=None, **kwargs): + project=None, describe_output=None, symlink_max_tries=15, **kwargs): ''' Core of download logic. Download file-id *dxid* and store it in a local file *filename*. @@ -279,13 +274,15 @@ def print_progress(bytes_downloaded, file_size, action="Downloaded"): dxfile_desc = describe_output else: dxfile_desc = dxfile.describe(fields={"parts"}, default_fields=True, **kwargs) - if 'drive' in dxfile_desc: - # A symbolic link. Get the MD5 checksum, if we have it + + # handling of symlinked files. + if 'drive' in dxfile_desc and 'parts' not in dxfile_desc \ + or 'drive' in dxfile_desc and dxfile_desc["drive"] == "drive-PUBLISHED": if 'md5' in dxfile_desc: md5 = dxfile_desc['md5'] else: md5 = None - _download_symbolic_link(dxid, md5, project, filename) + _download_symbolic_link(dxid, md5, project, filename, symlink_max_tries=symlink_max_tries) return True parts = dxfile_desc["parts"] @@ -350,7 +347,7 @@ def verify_part(_part_id, got_bytes, hasher): if "md5" not in part_info: raise DXFileError("File {} does not contain part md5 checksums".format(dxfile.get_id())) bytes_to_read = part_info["size"] - hasher = hashlib.md5() + hasher = md5_hasher() while bytes_to_read > 0: chunk = fh.read(min(max_verify_chunk_size, bytes_to_read)) if len(chunk) < min(max_verify_chunk_size, bytes_to_read): @@ -384,7 +381,7 @@ def verify_part(_part_id, got_bytes, hasher): do_first_task_sequentially=get_first_chunk_sequentially): if chunk_part != cur_part: verify_part(cur_part, got_bytes, hasher) - cur_part, got_bytes, hasher = chunk_part, 0, hashlib.md5() + cur_part, got_bytes, hasher = chunk_part, 0, md5_hasher() got_bytes += len(chunk_data) hasher.update(chunk_data) fh.write(chunk_data) @@ -452,49 +449,11 @@ def upload_local_file(filename=None, file=None, media_type=None, keep_open=False ''' fd = file if filename is None else open(filename, 'rb') - try: file_size = os.fstat(fd.fileno()).st_size except: file_size = 0 - file_is_mmapd = hasattr(fd, "fileno") - - if write_buffer_size is None: - write_buffer_size=dxfile.DEFAULT_BUFFER_SIZE - - if use_existing_dxfile: - handler = use_existing_dxfile - else: - # Set a reasonable name for the file if none has been set - # already - creation_kwargs = kwargs.copy() - if 'name' not in kwargs: - if filename is not None: - creation_kwargs['name'] = os.path.basename(filename) - else: - # Try to get filename from file-like object - try: - local_file_name = file.name - except AttributeError: - pass - else: - creation_kwargs['name'] = os.path.basename(local_file_name) - - # Use 'a' mode because we will be responsible for closing the file - # ourselves later (if requested). - handler = new_dxfile(mode='a', media_type=media_type, write_buffer_size=write_buffer_size, - expected_file_size=file_size, file_is_mmapd=file_is_mmapd, **creation_kwargs) - - # For subsequent API calls, don't supply the dataobject metadata - # parameters that are only needed at creation time. - _, remaining_kwargs = dxpy.DXDataObject._get_creation_params(kwargs) - - num_ticks = 60 - offset = 0 - - handler._ensure_write_bufsize(**remaining_kwargs) - def can_be_mmapd(fd): if not hasattr(fd, "fileno"): return False @@ -510,15 +469,13 @@ def read(num_bytes): # If file cannot be mmap'd (e.g. is stdin, or a fifo), fall back # to doing an actual read from the file. if not can_be_mmapd(fd): - return fd.read(handler._write_bufsize) + return fd.read(num_bytes) bytes_available = max(file_size - offset, 0) if bytes_available == 0: return b"" - return mmap.mmap(fd.fileno(), min(handler._write_bufsize, bytes_available), offset=offset, access=mmap.ACCESS_READ) - - handler._num_bytes_transmitted = 0 + return mmap.mmap(fd.fileno(), min(num_bytes, bytes_available), offset=offset, access=mmap.ACCESS_READ) def report_progress(handler, num_bytes): handler._num_bytes_transmitted += num_bytes @@ -537,29 +494,87 @@ def report_progress(handler, num_bytes): sys.stderr.write("\r") sys.stderr.flush() - if show_progress: - report_progress(handler, 0) + def get_new_handler(filename): + # Set a reasonable name for the file if none has been set + # already + creation_kwargs = kwargs.copy() + if 'name' not in kwargs: + if filename is not None: + creation_kwargs['name'] = os.path.basename(filename) + else: + # Try to get filename from file-like object + try: + local_file_name = file.name + except AttributeError: + pass + else: + creation_kwargs['name'] = os.path.basename(local_file_name) - while True: - buf = read(handler._write_bufsize) - offset += len(buf) + # Use 'a' mode because we will be responsible for closing the file + # ourselves later (if requested). + return new_dxfile(mode='a', media_type=media_type, write_buffer_size=write_buffer_size, + expected_file_size=file_size, file_is_mmapd=file_is_mmapd, **creation_kwargs) - if len(buf) == 0: - break + retries = 0 + max_retries = 2 + file_is_mmapd = hasattr(fd, "fileno") - handler.write(buf, - report_progress_fn=report_progress if show_progress else None, - multithread=multithread, - **remaining_kwargs) + if write_buffer_size is None: + write_buffer_size=dxfile.DEFAULT_BUFFER_SIZE + + # APPS-650 file upload would occasionally fail due to some parts not being uploaded correctly. This will try to re-upload in case this happens. + while retries <= max_retries: + retries += 1 - if filename is not None: - fd.close() + if use_existing_dxfile: + handler = use_existing_dxfile + else: + handler = get_new_handler(filename) - handler.flush(report_progress_fn=report_progress if show_progress else None, **remaining_kwargs) + # For subsequent API calls, don't supply the dataobject metadata + # parameters that are only needed at creation time. + _, remaining_kwargs = dxpy.DXDataObject._get_creation_params(kwargs) - if show_progress: - sys.stderr.write("\n") - sys.stderr.flush() + num_ticks = 60 + offset = 0 + + handler._ensure_write_bufsize(**remaining_kwargs) + + handler._num_bytes_transmitted = 0 + + if show_progress: + report_progress(handler, 0) + + while True: + buf = read(handler._write_bufsize) + offset += len(buf) + + if len(buf) == 0: + break + + handler.write(buf, + report_progress_fn=report_progress if show_progress else None, + multithread=multithread, + **remaining_kwargs) + + handler.flush(report_progress_fn=report_progress if show_progress else None, **remaining_kwargs) + + if show_progress: + sys.stderr.write("\n") + sys.stderr.flush() + try: + handler.wait_until_parts_uploaded() + except DXError: + if show_progress: + logger.warning("File {} was not uploaded correctly!".format(filename)) + if retries > max_retries: + raise + if show_progress: + logger.warning("Retrying...({}/{})".format(retries, max_retries)) + continue + if filename is not None: + fd.close() + break if not keep_open: handler.close(block=wait_on_close, report_progress_fn=report_progress if show_progress else None, **remaining_kwargs) @@ -619,10 +634,10 @@ def list_subfolders(project, path, recurse=True): ''' project_folders = dxpy.get_handler(project).describe(input_params={'folders': True})['folders'] - # TODO: support shell-style path globbing (i.e. /a*/c matches /ab/c but not /a/b/c) - # return pathmatch.filter(project_folders, os.path.join(path, '*')) + # If path is '/', return all folders + # If path is '/foo', return '/foo' and all subfolders of '/foo/' if recurse: - return (f for f in project_folders if f.startswith(path)) + return (f for f in project_folders if path == '/' or (f == path or f.startswith(path + '/'))) else: return (f for f in project_folders if f.startswith(path) and '/' not in f[len(path)+1:]) diff --git a/src/python/dxpy/bindings/dxglobalworkflow.py b/src/python/dxpy/bindings/dxglobalworkflow.py index 4a7f8cf54c..cf998ea820 100644 --- a/src/python/dxpy/bindings/dxglobalworkflow.py +++ b/src/python/dxpy/bindings/dxglobalworkflow.py @@ -262,7 +262,7 @@ def _get_run_input(self, workflow_input, project=None, **kwargs): region = dxpy.api.project_describe(project, input_params={"fields": {"region": True}})["region"] dxworkflow = self.get_underlying_workflow(region) - return dxworkflow._get_run_input(workflow_input, **kwargs) + return dxworkflow._get_run_input(workflow_input, project=project, **kwargs) def _run_impl(self, run_input, **kwargs): if self._dxid is not None: diff --git a/src/python/dxpy/bindings/dxjob.py b/src/python/dxpy/bindings/dxjob.py index e90f7640f1..bcb09b63c7 100644 --- a/src/python/dxpy/bindings/dxjob.py +++ b/src/python/dxpy/bindings/dxjob.py @@ -38,13 +38,15 @@ from ..utils.local_exec_utils import queue_entry_point from ..compat import basestring + ######### # DXJob # ######### -def new_dxjob(fn_input, fn_name, name=None, tags=None, properties=None, details=None, - instance_type=None, depends_on=None, - **kwargs): + +def new_dxjob(fn_input, fn_name, name=None, tags=None, properties=None, details=None, instance_type=None, + depends_on=None, cluster_spec=None, fpga_driver=None, system_requirements=None, + system_requirements_by_executable=None, nvidia_driver=None, **kwargs): ''' :param fn_input: Function input :type fn_input: dict @@ -62,6 +64,16 @@ def new_dxjob(fn_input, fn_name, name=None, tags=None, properties=None, details= :type instance_type: string or dict :param depends_on: List of data objects or jobs to wait that need to enter the "closed" or "done" states, respectively, before the new job will be run; each element in the list can either be a dxpy handler or a string ID :type depends_on: list + :param cluster_spec: a dict mapping function names to cluster spec requests + :type cluster_spec: dict + :param fpga_driver: a dict mapping function names to fpga driver requests + :type fpga_driver: dict + :param system_requirements: System requirement single mapping + :type system_requirements: dict + :param system_requirements_by_executable: System requirement by executable double mapping + :type system_requirements_by_executable: dict + :param nvidia_driver: a dict mapping function names to nvidia driver requests + :type nvidia_driver: dict :rtype: :class:`~dxpy.bindings.dxjob.DXJob` Creates and enqueues a new job that will execute a particular @@ -85,10 +97,13 @@ def new_dxjob(fn_input, fn_name, name=None, tags=None, properties=None, details= ''' dxjob = DXJob() - dxjob.new(fn_input, fn_name, name=name, tags=tags, properties=properties, - details=details, instance_type=instance_type, depends_on=depends_on, **kwargs) + dxjob.new(fn_input, fn_name, name=name, tags=tags, properties=properties, details=details, + instance_type=instance_type, depends_on=depends_on, cluster_spec=cluster_spec, fpga_driver=fpga_driver, + system_requirements=system_requirements, system_requirements_by_executable=system_requirements_by_executable, + nvidia_driver=nvidia_driver, **kwargs) return dxjob + class DXJob(DXObject): ''' Remote job object handler. @@ -101,9 +116,9 @@ def __init__(self, dxid=None): DXObject.__init__(self, dxid=dxid) self.set_id(dxid) - def new(self, fn_input, fn_name, name=None, tags=None, properties=None, details=None, - instance_type=None, depends_on=None, - **kwargs): + def new(self, fn_input, fn_name, name=None, tags=None, properties=None, details=None, instance_type=None, + depends_on=None, cluster_spec=None, fpga_driver=None, system_requirements=None, + system_requirements_by_executable=None, nvidia_driver=None, **kwargs): ''' :param fn_input: Function input :type fn_input: dict @@ -121,6 +136,16 @@ def new(self, fn_input, fn_name, name=None, tags=None, properties=None, details= :type instance_type: string or dict :param depends_on: List of data objects or jobs to wait that need to enter the "closed" or "done" states, respectively, before the new job will be run; each element in the list can either be a dxpy handler or a string ID :type depends_on: list + :param cluster_spec: a dict mapping function names to cluster spec requests + :type cluster_spec: dict + :param fpga_driver: a dict mapping function names to fpga driver requests + :type fpga_driver: dict + :param system_requirements: System requirement single mapping + :type system_requirements: dict + :param system_requirements_by_executable: System requirement by executable double mapping + :type system_requirements_by_executable: dict + :param nvidia_driver: a dict mapping function names to nvidia driver requests + :type nvidia_driver: dict Creates and enqueues a new job that will execute a particular function (from the same app or applet as the one the current job @@ -159,8 +184,16 @@ def new(self, fn_input, fn_name, name=None, tags=None, properties=None, details= req_input["tags"] = tags if properties is not None: req_input["properties"] = properties - if instance_type is not None: - req_input["systemRequirements"] = SystemRequirementsDict.from_instance_type(instance_type, fn_name).as_dict() + if any(requirement is not None for requirement in [instance_type, cluster_spec, fpga_driver, nvidia_driver]): + instance_type_srd = SystemRequirementsDict.from_instance_type(instance_type, fn_name) + cluster_spec_srd = SystemRequirementsDict(cluster_spec) + fpga_driver_srd = SystemRequirementsDict(fpga_driver) + nvidia_driver_srd = SystemRequirementsDict(nvidia_driver) + req_input["systemRequirements"] = (instance_type_srd + cluster_spec_srd + fpga_driver_srd + nvidia_driver_srd).as_dict() + if system_requirements is not None: + req_input["systemRequirements"] = system_requirements + if system_requirements_by_executable is not None: + req_input["systemRequirementsByExecutable"] = system_requirements_by_executable if depends_on is not None: req_input["dependsOn"] = final_depends_on if details is not None: @@ -187,12 +220,14 @@ def set_id(self, dxid): verify_string_dxid(dxid, self._class) self._dxid = dxid - def describe(self, fields=None, io=None, **kwargs): + def describe(self, fields=None, defaultFields=None, io=None, **kwargs): """ :param fields: dict where the keys are field names that should be returned, and values should be set to True (by default, all fields are returned) :type fields: dict + :param defaultFields: include default fields when fields is supplied + :type defaultFields: bool :param io: Include input and output fields in description; cannot be provided with *fields*; default is True if *fields* is not provided (deprecated) @@ -213,6 +248,8 @@ def describe(self, fields=None, io=None, **kwargs): describe_input = {} if fields is not None: describe_input['fields'] = fields + if defaultFields is not None: + describe_input['defaultFields'] = defaultFields if io is not None: describe_input['io'] = io self._desc = dxpy.api.job_describe(self._dxid, describe_input, **kwargs) @@ -242,6 +279,18 @@ def remove_tags(self, tags, **kwargs): dxpy.api.job_remove_tags(self._dxid, {"tags": tags}, **kwargs) + def update(self, allow_ssh, **kwargs): + """ + :param allow_ssh: Allowable IP ranges to set for SSH access to the job + :type allow_ssh: list of strings + + Updates a job's allowSSH field, overwrites existing values + + """ + + dxpy.api.job_update(self._dxid, {"allowSSH": allow_ssh}, **kwargs) + + def set_properties(self, properties, **kwargs): """ :param properties: Property names and values given as key-value pairs of strings diff --git a/src/python/dxpy/bindings/dxproject.py b/src/python/dxpy/bindings/dxproject.py index 98d2de5481..0bf210cb14 100644 --- a/src/python/dxpy/bindings/dxproject.py +++ b/src/python/dxpy/bindings/dxproject.py @@ -281,9 +281,11 @@ class DXProject(DXContainer): _class = "project" - def new(self, name, summary=None, description=None, protected=None, - restricted=None, download_restricted=None, contains_phi=None, + def new(self, name, summary=None, description=None, region=None, protected=None, + restricted=None, download_restricted=None, contains_phi=None, tags=None, properties=None, bill_to=None, database_ui_view_only=None, + external_upload_restricted=None, default_symlink=None, + database_results_restricted=None, **kwargs): """ :param name: The name of the project @@ -292,11 +294,13 @@ def new(self, name, summary=None, description=None, protected=None, :type summary: string :param description: If provided, the new project description :type name: string + :param region: If provided, the region that this project will be created in. The region must be among the permitted regions of the project's billTo + :type name: string :param protected: If provided, whether the project should be protected :type protected: boolean :param restricted: If provided, whether the project should be restricted :type restricted: boolean - :param download_restricted: If provided, whether external downloads should be restricted + :param download_restricted: If provided, whether external file downloads and external access to database objects should be restricted :type download_restricted: boolean :param contains_phi: If provided, whether the project should be marked as containing protected health information (PHI) :type contains_phi: boolean @@ -308,6 +312,12 @@ def new(self, name, summary=None, description=None, protected=None, :type bill_to: string :param database_ui_view_only: If provided, whether the viewers on the project can access the database data directly :type database_ui_view_only: boolean + :param external_upload_restricted: If provided, whether project members can upload data to project from external sources, e.g. outside of job + :type external_upload_restricted: boolean + :param database_results_restricted: If provided, minimum amount of data that project members with VIEW access can see from databases in the project + :type database_results_restricted: int + :param default_symlink: If provided, the details needed to have writable symlinks in the project. Dict must include drive, container, and optional prefix. + :type default_symlink: dict Creates a new project. Initially only the user performing this action will be in the permissions/member list, with ADMINISTER access. @@ -316,12 +326,15 @@ def new(self, name, summary=None, description=None, protected=None, method for more info. """ + input_hash = {} input_hash["name"] = name if summary is not None: input_hash["summary"] = summary if description is not None: input_hash["description"] = description + if region is not None: + input_hash["region"] = region if protected is not None: input_hash["protected"] = protected if restricted is not None: @@ -334,19 +347,27 @@ def new(self, name, summary=None, description=None, protected=None, input_hash["billTo"] = bill_to if database_ui_view_only is not None: input_hash["databaseUIViewOnly"] = database_ui_view_only + if external_upload_restricted is not None: + input_hash["externalUploadRestricted"] = external_upload_restricted + if database_results_restricted is not None: + input_hash["databaseResultsRestricted"] = database_results_restricted if tags is not None: input_hash["tags"] = tags if properties is not None: input_hash["properties"] = properties + if default_symlink is not None: + input_hash["defaultSymlink"] = default_symlink self.set_id(dxpy.api.project_new(input_hash, **kwargs)["id"]) self._desc = {} return self._dxid def update(self, name=None, summary=None, description=None, protected=None, - restricted=None, download_restricted=None, version=None, - allowed_executables=None, unset_allowed_executables=None, - database_ui_view_only=None, **kwargs): + restricted=None, download_restricted=None, version=None, + allowed_executables=None, unset_allowed_executables=None, + database_ui_view_only=None, external_upload_restricted=None, + database_results_restricted=None, unset_database_results_restricted=None, + **kwargs): """ :param name: If provided, the new project name :type name: string @@ -362,8 +383,16 @@ def update(self, name=None, summary=None, description=None, protected=None, :type download_restricted: boolean :param allowed_executables: If provided, these are the only executable ID(s) allowed to run as root executions in this project :type allowed_executables: list + :param unset_allowed_executables: If provided, removes any restrictions set by allowed_executables + :type unset_allowed_executables: boolean :param database_ui_view_only: If provided, whether the viewers on the project can access the database data directly :type database_ui_view_only: boolean + :param external_upload_restricted: If provided, whether project members can upload data to project from external sources, e.g. outside of job + :type external_upload_restricted: boolean + :param database_results_restricted: If provided, minimum amount of data that project members with VIEW access can see from databases in the project + :type database_results_restricted: int + :param unset_database_results_restricted: If provided, removes any restrictions set by database_results_restricted + :type unset_database_results_restricted: boolean :param version: If provided, the update will only occur if the value matches the current project's version number :type version: int @@ -395,6 +424,12 @@ def update(self, name=None, summary=None, description=None, protected=None, update_hash["allowedExecutables"] = None if database_ui_view_only is not None: update_hash["databaseUIViewOnly"] = database_ui_view_only + if external_upload_restricted is not None: + update_hash["externalUploadRestricted"] = external_upload_restricted + if database_results_restricted is not None: + update_hash["databaseResultsRestricted"] = database_results_restricted + if unset_database_results_restricted is not None: + update_hash["databaseResultsRestricted"] = None dxpy.api.project_update(self._dxid, update_hash, **kwargs) def invite(self, invitee, level, send_email=True, **kwargs): diff --git a/src/python/dxpy/bindings/search.py b/src/python/dxpy/bindings/search.py index 636bf34b96..09344ca738 100644 --- a/src/python/dxpy/bindings/search.py +++ b/src/python/dxpy/bindings/search.py @@ -114,7 +114,7 @@ def find_data_objects(classname=None, state=None, visibility=None, modified_after=None, modified_before=None, created_after=None, created_before=None, describe=False, limit=None, level=None, region=None, - return_handler=False, first_page_size=100, + archival_state=None, return_handler=False, first_page_size=100, **kwargs): """ :param classname: @@ -160,9 +160,11 @@ def find_data_objects(classname=None, state=None, visibility=None, things, be used to customize the set of fields that is returned) :type describe: bool or dict :param level: The minimum permissions level for which results should be returned (one of "VIEW", "UPLOAD", "CONTRIBUTE", or "ADMINISTER") + :type level: string :param region: Filter on result set by the given region(s). :type region: string or list of strings - :type level: string + :param archival_state: Filter by the given archival state (one of "archived", "live", "archival", "unarchiving", or "any"). Requires classname="file", project, and folder arguments to be provided. + :type archival_state: string :param limit: The maximum number of results to be returned (if not specified, the number of results is unlimited) :type limit: int :param first_page_size: The number of results that the initial API call will return. Subsequent calls will raise this by multiplying by 2 up to a maximum of 1000. @@ -260,6 +262,8 @@ def find_data_objects(classname=None, state=None, visibility=None, query['level'] = level if region is not None: query['region'] = region + if archival_state is not None: + query['archivalState'] = archival_state if limit is not None: query["limit"] = limit @@ -272,7 +276,7 @@ def find_executions(classname=None, launched_by=None, executable=None, project=N created_after=None, created_before=None, describe=False, name=None, name_mode="exact", tags=None, properties=None, limit=None, first_page_size=100, return_handler=False, include_subjobs=True, - **kwargs): + include_restarted=None, **kwargs): ''' :param classname: Class with which to restrict the search, i.e. one of "job", @@ -326,6 +330,8 @@ def find_executions(classname=None, launched_by=None, executable=None, project=N :type return_handler: boolean :param include_subjobs: If False, no subjobs will be returned by the API :type include_subjobs: boolean + :param include_restarted: If True, API response will include restarted jobs and job trees rooted in restarted jobs + :type include_restarted: boolean :rtype: generator Returns a generator that yields all executions (jobs or analyses) that match the query. It transparently handles @@ -412,6 +418,8 @@ def find_executions(classname=None, launched_by=None, executable=None, project=N query['properties'] = properties if include_subjobs is not True: query["includeSubjobs"] = include_subjobs + if include_restarted is not None: + query["includeRestarted"] = include_restarted if limit is not None: query["limit"] = limit @@ -434,7 +442,7 @@ def find_analyses(*args, **kwargs): def find_projects(name=None, name_mode='exact', properties=None, tags=None, level=None, describe=False, explicit_perms=None, region=None, public=None, created_after=None, created_before=None, billed_to=None, - limit=None, return_handler=False, first_page_size=100, containsPHI=None, **kwargs): + limit=None, return_handler=False, first_page_size=100, containsPHI=None, externalUploadRestricted=None, **kwargs): """ :param name: Name of the project (also see *name_mode*) :type name: string @@ -476,6 +484,9 @@ def find_projects(name=None, name_mode='exact', properties=None, tags=None, :param containsPHI: If set to true, only returns projects that contain PHI. If set to false, only returns projects that do not contain PHI. :type containsPHI: boolean + :param externalUploadRestricted: If set to true, only returns projects with externalUploadRestricted enabled. + If set to false, only returns projects that do not have externalUploadRestricted enabled. + :type externalUploadRestricted: boolean :rtype: generator Returns a generator that yields all projects that match the query. @@ -523,6 +534,8 @@ def find_projects(name=None, name_mode='exact', properties=None, tags=None, query["limit"] = limit if containsPHI is not None: query["containsPHI"] = containsPHI + if externalUploadRestricted is not None: + query["externalUploadRestricted"] = externalUploadRestricted return _find(dxpy.api.system_find_projects, query, limit, return_handler, first_page_size, **kwargs) @@ -662,6 +675,9 @@ def find_global_workflows(name=None, name_mode='exact', category=None, first_page_size=first_page_size, **kwargs) def _find_one(method, zero_ok=False, more_ok=True, **kwargs): + # users often incorrectly pass strings to zero_ok, fail fast in that case + if not isinstance(zero_ok, bool): + raise DXError('_find_one: Unexpected value found for argument zero_ok, it should be a bool') kwargs["limit"] = 1 if more_ok else 2 response = method(**kwargs) result = next(response, None) @@ -683,6 +699,7 @@ def find_one_data_object(zero_ok=False, more_ok=True, **kwargs): If False (default), :class:`~dxpy.exceptions.DXSearchError` is raised if the search has 0 results; if True, returns None if the search has 0 results + If not boolean, :class:`~dxpy.exceptions.DXError` is raised :type zero_ok: bool :param more_ok: If False, :class:`~dxpy.exceptions.DXSearchError` is raised if @@ -703,6 +720,7 @@ def find_one_project(zero_ok=False, more_ok=True, **kwargs): If False (default), :class:`~dxpy.exceptions.DXSearchError` is raised if the search has 0 results; if True, returns None if the search has 0 results + If not boolean, :class:`~dxpy.exceptions.DXError` is raised :type zero_ok: bool :param more_ok: If False, :class:`~dxpy.exceptions.DXSearchError` is raised if @@ -723,6 +741,7 @@ def find_one_app(zero_ok=False, more_ok=True, **kwargs): If False (default), :class:`~dxpy.exceptions.DXSearchError` is raised if the search has 0 results; if True, returns None if the search has 0 results + If not boolean, :class:`~dxpy.exceptions.DXError` is raised :type zero_ok: bool :param more_ok: If False, :class:`~dxpy.exceptions.DXSearchError` is raised if @@ -843,7 +862,7 @@ def org_find_projects(org_id=None, name=None, name_mode='exact', ids=None, prope if len(properties.keys()) == 1: query["properties"] = properties else: - query["properties"] = {"$and": [{k: v} for (k, v) in properties.iteritems()]} + query["properties"] = {"$and": [{k: v} for (k, v) in properties.items()]} if tags is not None: if len(tags) == 1: query["tags"] = tags[0] diff --git a/src/python/dxpy/cli/cp.py b/src/python/dxpy/cli/cp.py index 09f05729b7..81ad71ffb0 100644 --- a/src/python/dxpy/cli/cp.py +++ b/src/python/dxpy/cli/cp.py @@ -24,9 +24,8 @@ from __future__ import print_function, unicode_literals, division, absolute_import import dxpy -import requests from ..utils.resolver import (resolve_existing_path, resolve_path, is_hashid, get_last_pos_of_char) -from ..exceptions import (err_exit, DXCLIError) +from ..exceptions import (err_exit, DXCLIError, ResourceNotFound) from . import try_call from dxpy.utils.printing import (fill) @@ -47,8 +46,8 @@ def cp_to_noexistent_destination(args, dest_path, dx_dest, dest_proj): dest_name = dest_path[last_slash_pos + 1:].replace('\/', '/') try: dx_dest.list_folder(folder=dest_folder, only='folders') - except dxpy.DXAPIError as details: - if details.code == requests.codes['not_found']: + except dxpy.DXAPIError as e: + if isinstance(e, ResourceNotFound): raise DXCLIError('The destination folder does not exist') else: raise @@ -152,7 +151,8 @@ def cp(args): {"objects": src_objects, "folders": src_folders, "project": dest_proj, - "destination": dest_path})['exists'] + "destination": dest_path, + "targetFileRelocation": args.target_file_relocation})['exists'] if len(exists) > 0: print(fill('The following objects already existed in the destination container ' + 'and were left alone:') + '\n ' + '\n '.join(exists)) diff --git a/src/python/dxpy/cli/dataset_utilities.py b/src/python/dxpy/cli/dataset_utilities.py new file mode 100644 index 0000000000..dc47698c14 --- /dev/null +++ b/src/python/dxpy/cli/dataset_utilities.py @@ -0,0 +1,2265 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (C) 2022 DNAnexus, Inc. +# +# This file is part of dx-toolkit (DNAnexus platform client libraries). +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy +# of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from __future__ import print_function, unicode_literals, division, absolute_import, annotations + +import sys +import collections +import json +import os +import re +import csv +import dxpy +import codecs +import subprocess +from functools import reduce +from ..utils.printing import fill +from ..bindings import DXRecord +from ..bindings.dxdataobject_functions import is_dxlink, describe +from ..bindings.dxfile import DXFile +from ..utils.resolver import resolve_existing_path, is_hashid, ResolutionError, resolve_path, check_folder_exists +from ..utils.file_handle import as_handle +from ..utils.describe import print_desc +from ..compat import USING_PYTHON2 +from ..exceptions import ( + err_exit, + PermissionDenied, + InvalidInput, + InvalidState, + ResourceNotFound, + default_expected_exceptions, +) + +from ..dx_extract_utils.filter_to_payload import validate_JSON, final_payload +from ..dx_extract_utils.germline_utils import ( + get_genotype_only_types, + add_germline_base_sql, + sort_germline_variant, + harmonize_germline_sql, + harmonize_germline_results, + get_germline_ref_payload, + get_germline_loci_payload, + update_genotype_only_ref, + get_genotype_types, + infer_genotype_type, + get_types_to_filter_out_when_infering, + filter_results +) +from ..dx_extract_utils.input_validation import inference_validation +from ..dx_extract_utils.input_validation_somatic import validate_somatic_filter +from ..dx_extract_utils.somatic_filter_payload import somatic_final_payload +from ..dx_extract_utils.cohort_filter_payload import cohort_filter_payload, cohort_final_payload + +from ..bindings.apollo.dataset import Dataset + +from ..bindings.apollo.cmd_line_options_validator import ArgsValidator +from ..bindings.apollo.json_validation_by_schema import JSONValidator + +from ..bindings.apollo.schemas.input_arguments_validation_schemas import EXTRACT_ASSAY_EXPRESSION_INPUT_ARGS_SCHEMA +from ..bindings.apollo.schemas.assay_filtering_json_schemas import EXTRACT_ASSAY_EXPRESSION_JSON_SCHEMA +from ..bindings.apollo.schemas.assay_filtering_conditions import EXTRACT_ASSAY_EXPRESSION_FILTERING_CONDITIONS + +from ..bindings.apollo.vizserver_filters_from_json_parser import JSONFiltersValidator +from ..bindings.apollo.vizserver_payload_builder import VizPayloadBuilder +from ..bindings.apollo.vizclient import VizClient + +from ..bindings.apollo.data_transformations import transform_to_expression_matrix +from .output_handling import write_expression_output, pretty_print_json + +from .help_messages import EXTRACT_ASSAY_EXPRESSION_JSON_HELP, EXTRACT_ASSAY_EXPRESSION_ADDITIONAL_FIELDS_HELP + +database_unique_name_regex = re.compile("^database_\w{24}__\w+$") +database_id_regex = re.compile("^database-\\w{24}$") + + +def resolve_validate_record_path(path): + + project, folder_path, entity_result = resolve_existing_path(path) + + if project is None: + raise ResolutionError( + 'Unable to resolve "' + + path + + '" to a data object or folder name in a project' + ) + elif project != entity_result["describe"]["project"]: + raise ResolutionError( + 'Unable to resolve "' + + path + + "\" to a data object or folder name in '" + + project + + "'" + ) + + if entity_result["describe"]["class"] != "record": + err_exit( + "{}: Invalid path. The path must point to a record type of cohort or dataset".format(path) + + ) + + try: + resp = dxpy.DXHTTPRequest( + "/" + entity_result["id"] + "/visualize", + {"project": project, "cohortBrowser": False}, + ) + except PermissionDenied: + err_exit("Insufficient permissions", expected_exceptions=(PermissionDenied,)) + except (InvalidInput, InvalidState): + err_exit( + "%s : Invalid cohort or dataset" % entity_result["id"], + expected_exceptions=( + InvalidInput, + InvalidState, + ), + ) + except Exception as details: + err_exit(str(details)) + + if resp["datasetVersion"] != "3.0": + err_exit( + "%s : Invalid version of cohort or dataset. Version must be 3.0" + % resp["datasetVersion"] + ) + + if ("Dataset" in resp["recordTypes"]) or ("CohortBrowser" in resp["recordTypes"]): + dataset_project = resp["datasetRecordProject"] + else: + err_exit( + "%s : Invalid path. The path must point to a record type of cohort or dataset" + % resp["recordTypes"] + ) + + return project, entity_result, resp, dataset_project + + +def viz_query_api_call(resp, payload, route): + resource_val = resp["url"] + "/viz-query/3.0/" + resp["dataset"] + "/" + route + try: + resp_query = dxpy.DXHTTPRequest( + resource=resource_val, data=payload, prepend_srv=False + ) + + except Exception as details: + err_exit(str(details)) + sql_results = resp_query["sql"] + ";" + return sql_results + + +def raw_query_api_call(resp, payload): + return viz_query_api_call(resp, payload, 'raw-query') + + +def raw_cohort_query_api_call(resp, payload): + return viz_query_api_call(resp, payload, 'raw-cohort-query') + + +def raw_api_call(resp, payload, sql_message=True): + resource_val = resp["url"] + "/data/3.0/" + resp["dataset"] + "/raw" + try: + resp_raw = dxpy.DXHTTPRequest( + resource=resource_val, data=payload, prepend_srv=False + ) + if "error" in resp_raw.keys(): + if resp_raw["error"]["type"] == "InvalidInput": + err_message = "Insufficient permissions due to the project policy.\n" + resp_raw["error"]["message"] + elif sql_message and resp_raw["error"]["type"] == "QueryTimeOut": + err_message = "Please consider using `--sql` option to generate the SQL query and query via a private compute cluster.\n" + resp_raw["error"]["message"] + elif resp_raw["error"]["type"] == "QueryBuilderError" and resp_raw["error"]["details"] == "rsid exists in request filters without rsid entries in rsid_lookup_table.": + err_message = "At least one rsID provided in the filter is not present in the provided dataset or cohort" + elif resp_raw["error"]["type"] == "DxApiError": + err_message = resp_raw["error"]["message"] + else: + err_message = resp_raw["error"] + err_exit(str(err_message)) + except Exception as details: + err_exit(str(details)) + return resp_raw + + +def extract_dataset(args): + """ + Retrieves the data or generates SQL to retrieve the data from a dataset or cohort for a set of entity.fields. Additionally, the dataset's dictionary can be extracted independently or in conjunction with data. + """ + if ( + not args.dump_dataset_dictionary + and not args.list_fields + and not args.list_entities + and args.fields is None + and args.fields_file is None + ): + err_exit( + "Must provide at least one of the following options: --fields, --fields-file, --dump-dataset-dictionary, --list-fields, --list-entities" + ) + + if ( + args.fields is not None + and args.fields_file is not None + ): + err_exit( + "dx extract_dataset: error: only one of the arguments, --fields or --fields-file, may be supplied at a given time" + ) + + listing_restricted = { + "dump_dataset_dictionary": False, + "sql": False, + "fields": None, + "fields_file": None, + "output": None, + "delim": ",", + } + + def check_options(args, restricted): + error_list = [] + for option, value in restricted.items(): + if args.__dict__[option] != value: + error_list.append("--{}".format(option.replace("_", "-"))) + return error_list + + if args.list_fields: + listing_restricted["list_entities"] = False + error_list = check_options(args, listing_restricted) + if error_list: + err_exit("--list-fields cannot be specified with: {}".format(error_list)) + + if args.list_entities: + listing_restricted["list_fields"] = False + listing_restricted["entities"] = None + error_list = check_options(args, listing_restricted) + if error_list: + err_exit("--list-entities cannot be specified with: {}".format(error_list)) + + delimiter = str(codecs.decode(args.delim, "unicode_escape")) + if len(delimiter) == 1 and delimiter != '"': + if delimiter == ",": + out_extension = ".csv" + elif delimiter == "\t": + out_extension = ".tsv" + else: + out_extension = ".txt" + else: + err_exit("Invalid delimiter specified") + + project, entity_result, resp, dataset_project = resolve_validate_record_path(args.path) + + dataset_id = resp["dataset"] + out_directory = "" + out_file_field = "" + print_to_stdout = False + files_to_check = [] + file_already_exist = [] + + def _check_system_python_version(): + python_version = sys.version_info[:3] + # Set python version range + # python_range = 0 for python_version>="3.7" + # python_range = 1 for python_version>="3.5.3" and python_version<"3.7" + # python_range = 2 for python_version<"3.5.3" + if python_version >= (3, 7): + python_range = "0" + elif python_version >= (3, 5, 3): + python_range = "1" + else: + python_range = "2" + return python_range + + def _check_pandas_version( + python_range, current_pandas_version, pandas_version_range + ): + # Valid pandas versions based on python versions + # python_range = 0; python_version>="3.7"; Valid pandas version: pandas==1.3.5 + # python_range = 1; python_version>="3.5.3" and python_version<"3.7"; Valid pandas version: pandas>=0.23.3,<=0.25.3 + # python_range = 2; python_version<"3.5.3"; Valid pandas version: pandas>=0.23.3,< 0.25.0 + system_pandas_version = tuple(map(int, current_pandas_version.split("."))) + if ( + (python_range == "0" and system_pandas_version == (1, 3, 5)) + or ( + python_range == "1" + and ((0, 25, 3) >= system_pandas_version >= (0, 23, 3)) + ) + or ( + python_range == "2" + and ((0, 25, 0) > system_pandas_version >= (0, 23, 3)) + ) + ): + pass + else: + print( + "Warning: For '-ddd' usage, the recommended pandas version is {}. The installed version of pandas is {}. It is recommended to update pandas. For example, 'pip/pip3 install -I pandas==X.X.X' where X.X.X is {}.".format( + pandas_version_range, current_pandas_version, pandas_version_range + ) + ) + + if args.dump_dataset_dictionary: + global pd + pandas_version_dictionary = { + "0": "'1.3.5'", + "1": ">= '0.23.3' and <= '0.25.3'", + "2": ">= '0.23.3' and < '0.25.0'", + } + python_range = _check_system_python_version() + try: + import pandas as pd + + current_pandas_version = pd.__version__ + _check_pandas_version( + python_range, + current_pandas_version, + pandas_version_dictionary[python_range], + ) + except ImportError as e: + err_exit( + "'-ddd' requires the use of pandas, which is not currently installed. Please install pandas to a version {}. For example, 'pip/pip3 install -I pandas==X.X.X' where X.X.X is {}.".format( + pandas_version_dictionary[python_range], + pandas_version_dictionary[python_range], + ) + ) + + if args.output is None: + out_directory = os.getcwd() + elif args.output == "-": + print_to_stdout = True + elif os.path.exists(args.output): + if os.path.isdir(args.output): + out_directory = args.output + else: + err_exit( + "Error: When using -ddd, --output must be an existing directory" + ) + else: + err_exit("Error: When using -ddd, --output must be an existing directory") + + if print_to_stdout: + output_file_data = sys.stdout + output_file_coding = sys.stdout + output_file_entity = sys.stdout + else: + output_file_data = os.path.join( + out_directory, resp["recordName"] + ".data_dictionary" + out_extension + ) + output_file_coding = os.path.join( + out_directory, resp["recordName"] + ".codings" + out_extension + ) + output_file_entity = os.path.join( + out_directory, resp["recordName"] + ".entity_dictionary" + out_extension + ) + files_to_check = [output_file_data, output_file_coding, output_file_entity] + + if args.fields or args.fields_file: + if args.sql: + file_name_suffix = ".data.sql" + else: + file_name_suffix = out_extension + + if args.output is None: + out_directory = os.getcwd() + out_file_field = os.path.join( + out_directory, resp["recordName"] + file_name_suffix + ) + files_to_check.append(out_file_field) + elif args.output == "-": + print_to_stdout = True + elif os.path.exists(args.output): + if os.path.isdir(args.output): + out_directory = args.output + out_file_field = os.path.join( + out_directory, resp["recordName"] + file_name_suffix + ) + files_to_check.append(out_file_field) + else: + file_already_exist.append(args.output) + elif os.path.exists(os.path.dirname(args.output)) or not os.path.dirname( + args.output + ): + out_file_field = args.output + else: + err_exit( + "Error: {path} could not be found".format( + path=os.path.dirname(args.output) + ) + ) + + for file in files_to_check: + if os.path.exists(file): + file_already_exist.append(file) + + if file_already_exist: + err_exit("Error: path already exists {path}".format(path=file_already_exist)) + + rec_descriptor = DXDataset(dataset_id, project=dataset_project).get_descriptor() + + fields_list = [] + if args.fields is not None: + fields_list = [field.strip() for field in args.fields.split(",")] + elif args.fields_file is not None: + if os.path.isfile(args.fields_file): + with open(args.fields_file, "r") as infile: + for line in infile: + fields_list.append(line.strip("\n")) + else: + err_exit( + "The file, {input_fields_file}, supplied using --fields-file could not be found".format( + input_fields_file=args.fields_file + ) + ) + + if fields_list: + error_list = [] + for entry in fields_list: + entity_field = entry.split(".") + if len(entity_field) < 2: + error_list.append(entry) + elif ( + entity_field[0] not in rec_descriptor.model["entities"].keys() + or entity_field[1] + not in rec_descriptor.model["entities"][entity_field[0]][ + "fields" + ].keys() + ): + error_list.append(entry) + + if error_list: + err_exit("The following fields cannot be found: %s" % error_list) + + payload = { + "project_context": project, + "fields": [{item: "$".join(item.split("."))} for item in fields_list], + } + if "CohortBrowser" in resp["recordTypes"]: + if resp.get("baseSql"): + payload["base_sql"] = resp.get("baseSql") + payload["filters"] = resp["filters"] + + if args.sql: + sql_results = raw_query_api_call(resp, payload) + if print_to_stdout: + print(sql_results) + else: + with open(out_file_field, "w") as f: + print(sql_results, file=f) + else: + resp_raw = raw_api_call(resp, payload) + csv_from_json( + out_file_name=out_file_field, + print_to_stdout=print_to_stdout, + sep=delimiter, + raw_results=resp_raw["results"], + column_names=fields_list, + ) + + elif args.sql: + err_exit("`--sql` passed without `--fields` or `--fields-file") + + if args.dump_dataset_dictionary: + rec_dict = rec_descriptor.get_dictionary() + write_ot = rec_dict.write( + output_file_data=output_file_data, + output_file_entity=output_file_entity, + output_file_coding=output_file_coding, + sep=delimiter, + ) + + # Listing section + if args.list_entities or args.list_fields: + # Retrieve entity names, titles and main entity + entity_names_and_titles, _main_entity = retrieve_entities(rec_descriptor.model) + # List entities + if args.list_entities: + print("\n".join(entity_names_and_titles)) + # List fields + if args.list_fields: + list_fields(rec_descriptor.model, _main_entity, args) + + +def get_assay_info(rec_descriptor, assay_type): + assay_list = rec_descriptor.assays + selected_type_assays = [] + other_assays = [] + if not assay_list: + err_exit("No valid assays in the dataset.") + else: + for a in assay_list: + if a["generalized_assay_model"] == assay_type: + selected_type_assays.append(a) + else: + other_assays.append(a) + return (selected_type_assays, other_assays) + + +#### Validate json filters #### +def json_validation_function(filter_type, args): + filter_arg = "args.retrieve_" + filter_type + filter_value = str(vars(args)["retrieve_" + filter_type]) + filter = {} + if filter_value.endswith(".json"): + if os.path.isfile(filter_value): + if os.stat(filter_value).st_size == 0: + err_exit( + 'No filter given for --retrieve-{filter_type} or JSON for "--retrieve-{filter_type}" does not contain valid filter information.'.format( + filter_type=filter_type + ) + ) + else: + with open(filter_value, "r") as json_file: + try: + filter = json.load(json_file) + except Exception as json_error: + err_exit( + "JSON for variant filters is malformatted.", + expected_exceptions=default_expected_exceptions, + ) + else: + err_exit( + "JSON file {filter_json} provided does not exist".format( + filter_json=filter_value + ) + ) + else: + if filter_value == "{}": + err_exit( + 'No filter given for --retrieve-{filter_type} or JSON for "--retrieve-{filter_type}" does not contain valid filter information.'.format( + filter_type=filter_type + ) + ) + else: + try: + filter = json.loads(filter_value) + except Exception as json_error: + err_exit( + "JSON for variant filters is malformatted.", + expected_exceptions=default_expected_exceptions, + ) + + if filter_type in ["allele", "annotation", "genotype"]: + validate_JSON(filter, filter_type) + elif filter_type in ["variant"]: + validate_somatic_filter(filter, filter_type) + + return filter + + +def retrieve_meta_info(resp, project_id, assay_id, assay_name, print_to_stdout, out_file_name): + table = "vcf_meta_information_unique" + columns = ["Field", "ID", "Type", "Number", "Description"] + payload = { + "project_context": project_id, + "fields": [ + {column: "$".join((table, column))} for column in columns + ], + "is_cohort": False, + "variant_browser": { + "name": assay_name, + "id": assay_id, + }, + } + resp_raw = raw_api_call(resp, payload, sql_message=False) + + csv_from_json( + out_file_name=out_file_name, + print_to_stdout=print_to_stdout, + sep="\t", + raw_results=resp_raw["results"], + column_names=columns, + ) + + +def assign_output_method(args, record_name, friendly_assay_type): + #### Decide output method based on --output and --sql #### + if args.sql: + file_name_suffix = ".data.sql" + elif friendly_assay_type == 'somatic' and args.retrieve_meta_info: + file_name_suffix = ".vcf_meta_info.txt" + else: + file_name_suffix = ".tsv" + file_already_exist = [] + files_to_check = [] + out_file = "" + + print_to_stdout = False + if args.output is None: + out_directory = os.getcwd() + out_file = os.path.join(out_directory, record_name + file_name_suffix) + files_to_check.append(out_file) + elif args.output == "-": + print_to_stdout = True + elif os.path.exists(args.output): + if os.path.isdir(args.output): + err_exit("--output should be a file, not a directory.") + else: + file_already_exist.append(args.output) + elif os.path.exists(os.path.dirname(args.output)) or not os.path.dirname( + args.output + ): + out_file = args.output + else: + err_exit( + "Error: {path} could not be found".format(path=os.path.dirname(args.output)) + ) + + for file in files_to_check: + if os.path.exists(file): + file_already_exist.append(file) + + if file_already_exist: + err_exit("Cannot specify the output to be an existing file.") + return out_file, print_to_stdout + +def get_assay_name_info( + list_assays, assay_name, path, friendly_assay_type, rec_descriptor +): + """ + Generalized function for determining assay name and reference genome + """ + if friendly_assay_type == "germline": + assay_type = "genetic_variant" + elif friendly_assay_type == "somatic": + assay_type = "somatic_variant" + + #### Get names of genetic assays #### + if list_assays: + (target_assays, other_assays) = get_assay_info( + rec_descriptor, assay_type=assay_type + ) + if not target_assays: + err_exit("There's no {} assay in the dataset provided.".format(friendly_assay_type)) + else: + for a in target_assays: + print(a["name"]) + sys.exit(0) + + #### Decide which assay is to be queried and which ref genome is to be used #### + (target_assays, other_assays) = get_assay_info( + rec_descriptor, assay_type=assay_type + ) + + target_assay_names = [ga["name"] for ga in target_assays] + target_assay_ids = [ga["uuid"] for ga in target_assays] + other_assay_names = [oa["name"] for oa in other_assays] + # other_assay_ids = [oa["uuid"] for oa in other_assays] + + if target_assay_names and target_assay_ids: + selected_assay_name = target_assay_names[0] + selected_assay_id = target_assay_ids[0] + else: + err_exit("There's no {} assay in the dataset provided.".format(friendly_assay_type)) + if assay_name: + if assay_name not in list(target_assay_names): + if assay_name in list(other_assay_names): + err_exit( + "This is not a valid assay. For valid assays accepted by the function, `extract_assay {}`, please use the --list-assays flag.".format( + friendly_assay_type + ) + ) + else: + err_exit( + "Assay {assay_name} does not exist in the {path}.".format( + assay_name=assay_name, path=path + ) + ) + else: + selected_assay_name = assay_name + for ga in target_assays: + if ga["name"] == assay_name: + selected_assay_id = ga["uuid"] + + additional_descriptor_info = {} + + if friendly_assay_type == "germline": + selected_ref_genome = "GRCh38.92" + for a in target_assays: + if a["name"] == selected_assay_name: + if a["reference_genome"]: + selected_ref_genome = a["reference_genome"]["name"].split(".", 1)[1] + additional_descriptor_info["genotype_type_table"] = a["entities"]["genotype"]["fields"]["type"]["mapping"]["table"] + for exclude_genotype in ("exclude_refdata", "exclude_halfref", "exclude_nocall"): + if exclude_genotype in a: + additional_descriptor_info[exclude_genotype] = a[exclude_genotype] + elif friendly_assay_type == "somatic": + selected_ref_genome = "" + for a in target_assays: + if a["name"] == selected_assay_name and a["reference"]: + selected_ref_genome = a["reference"]["name"] + "." + a["reference"]["annotation_source_version"] + + return(selected_assay_name, selected_assay_id, selected_ref_genome, additional_descriptor_info) + + +def comment_fill(string, comment_string='# ', **kwargs): + width_adjustment = kwargs.pop('width_adjustment', 0) - len(comment_string) + return re.sub('^', comment_string, fill(string, width_adjustment=width_adjustment, **kwargs), flags=re.MULTILINE) + + +def retrieve_samples(resp: dict, assay_name: str, assay_id: str) -> list: + """ + Get the list of sample_ids from the sample table for the selected assay. + """ + sample_payload = { + "project_context": resp["datasetRecordProject"], + "fields": [{"sample_id": "sample$sample_id"}], + "raw_filters": {"assay_filters": {"name": assay_name, "id": assay_id}}, + } + return [_["sample_id"] for _ in raw_api_call(resp, sample_payload)["results"]] + + +def extract_assay_germline(args): + """ + Retrieve the selected data or generate SQL to retrieve the data from an genetic variant assay in a dataset or cohort based on provided rules. + """ + ######## Input combination validation ######## + filter_given = False + if args.retrieve_allele or args.retrieve_annotation or args.retrieve_genotype: + filter_given = True + #### Check if valid options are passed with the --json-help flag #### + if args.json_help: + if not filter_given: + err_exit( + 'Please specify one of the following: --retrieve-allele, --retrieve-genotype or --retrieve-annotation" for details on the corresponding JSON template and filter definition.' + ) + elif args.list_assays or args.assay_name or args.sql or args.output: + err_exit( + "Please check to make sure the parameters are set properly. --json-help cannot be specified with options other than --retrieve-annotation/--retrieve-allele/--retrieve-genotype." + ) + #### Validate that other arguments are not passed with --list-assays #### + if args.list_assays: + if args.sql: + err_exit("The flag, --sql, cannot be used with --list-assays.") + elif args.output: + err_exit( + 'When --list-assays is specified, output is to STDOUT. "--output" may not be supplied.' + ) + elif filter_given: + err_exit("--list-assays cannot be presented with other options.") + + #### Validate that a retrieve options infer_ref or infer_nocall are not passed with retrieve_allele or retrieve_annotation #### + if args.infer_ref or args.infer_nocall: + if args.retrieve_allele or args.retrieve_annotation or args.sql or args.list_assays: + err_exit( + "The flags, --infer-ref and --infer-nocall, can only be used with --retrieve-genotype." + ) + + #### Check if the retrieve options are passed correctly, print help if needed #### + if args.retrieve_allele: + if args.json_help: + print( + comment_fill('Filters and respective definitions', comment_string='# ') + '\n#\n' + + comment_fill('rsid: rsID associated with an allele or set of alleles. If multiple values are provided, the conditional search will be, "OR." For example, ["rs1111", "rs2222"], will search for alleles which match either "rs1111" or "rs2222". String match is case sensitive. Duplicate values are permitted and will be handled silently.') + '\n#\n' + + comment_fill('type: Type of allele. Accepted values are "SNP", "Ins", "Del", "Mixed". If multiple values are provided, the conditional search will be, "OR." For example, ["SNP", "Ins"], will search for variants which match either "SNP" or "Ins". String match is case sensitive.') + '\n#\n' + + comment_fill('dataset_alt_af: Dataset alternate allele frequency, a json object with empty content or two sets of key/value pair: {min: 0.1, max:0.5}. Accepted numeric value for each key is between and including 0 and 1. If a user does not want to apply this filter but still wants this information in the output, an empty json object should be provided.') + '\n#\n' + + comment_fill('gnomad_alt_af: gnomAD alternate allele frequency. a json object with empty content or two sets of key/value pair: {min: 0.1, max:0.5}. Accepted value for each key is between 0 and 1. If a user does not want to apply this filter but still wants this information in the output, an empty json object should be provided.') + '\n#\n' + + comment_fill('location: Genomic range in the reference genome where the starting position of alleles fall into. If multiple values are provided in the list, the conditional search will be, "OR." String match is case sensitive.') + '\n#\n' + + comment_fill('JSON filter template for --retrieve-allele', comment_string='# ') + '\n' + +'{\n "rsid": ["rs11111", "rs22222"],\n "type": ["SNP", "Del", "Ins"],\n "dataset_alt_af": {"min": 0.001, "max": 0.05},\n "gnomad_alt_af": {"min": 0.001, "max": 0.05},\n "location": [\n {\n "chromosome": "1",\n "starting_position": "10000",\n "ending_position": "20000"\n },\n {\n "chromosome": "X",\n "starting_position": "500",\n "ending_position": "1700"\n }\n ]\n}' + ) + sys.exit(0) + elif args.retrieve_annotation: + if args.json_help: + print( + comment_fill('Filters and respective definitions', comment_string='# ') + '\n#\n' + + comment_fill('allele_id: ID of an allele for which annotations should be returned. If multiple values are provided, annotations for any alleles that match one of the values specified will be listed. For example, ["1_1000_A_T", "1_1010_C_T"], will search for annotations of alleles which match either "1_1000_A_T" or ""1_1010_C_T". String match is case insensitive.') + '\n#\n' + + comment_fill('gene_name: Gene name of the annotation. A list of gene names whose annotations will be returned. If multiple values are provided, the conditional search will be, "OR." For example, ["BRCA2", "ASPM"], will search for annotations which match either "BRCA2" or "ASPM". String match is case insensitive.') + '\n#\n' + + comment_fill('gene_id: Ensembl gene ID (ENSG) of the annotation. If multiple values are provided, the conditional search will be, "OR." For example, ["ENSG00000302118", "ENSG00004000504"], will search for annotations which match either "ENSG00000302118" or "ENSG00004000504". String match is case insensitive.') + '\n#\n' + + comment_fill('feature_id: Ensembl feature id (ENST) where the range overlaps with the variant. Currently, only coding transcript IDs are searched. If multiple values are provided, the conditional search will be, "OR." For example, ["ENST00000302118.5", "ENST00004000504.1"], will search for annotations which match either "ENST00000302118.5" or "ENST00004000504.1". String match is case insensitive.') + '\n#\n' + + comment_fill('consequences: Consequence as recorded in the annotation. If multiple values are provided, the conditional search will be, "OR." For example, ["5_prime_UTR_variant", "3_prime_UTR_variant"], will search for annotations which match either "5 prime UTR variant" or "3 prime UTR variant". String match is case sensitive. For all supported consequences terms, please refer to snpeff: http://pcingola.github.io/SnpEff/se_inputoutput/#effect-prediction-details (Effect Seq. Ontology column). This filter cannot be specified by itself, and must be included with at least one of the following filters: "gene_id", "gene_name",or "feature_id".') + '\n#\n' + + comment_fill('putative_impact: Putative impact as recorded in the annotation. Possible values are [ "HIGH", "MODERATE", "LOW", "MODIFIER"]. If multiple values are provided, the conditional search will be, "OR." For example, ["MODIFIER", "HIGH"], will search for annotations which match either "MODIFIER" or "HIGH". String match is case insensitive. For all supported terms, please refer to snpeff: http://pcingola.github.io/SnpEff/se_inputoutput/#impact-prediction. This filter cannot be specified by itself, and must be included with at least one of the following filters: "gene_id", "gene_name", or "transcript_id".') + '\n#\n' + + comment_fill('hgvs_c: HGVS (DNA) code of the annotation. If multiple values are provided, the conditional search will be, "OR." For example, ["c.-49A>G", "c.-20T>G"], will search for annotations which match either "c.-49A>G" or "c.-20T>G". String match is case sensitive.') + '\n#\n' + + comment_fill('hgvs_p: HGVS (Protein) code of the annotation. If multiple values are provided, the conditional search will be, "OR." For example, ["p.Gly2Asp", "p.Aps2Gly"], will search for annotations which match either "p.Gly2Asp" or "p.Aps2Gly". String match is case sensitive.') + '\n#\n' + + comment_fill('JSON filter template for --retrieve-annotation', comment_string='# ') + '\n' + + '{\n "allele_id":["1_1000_A_T","2_1000_G_C"],\n "gene_name": ["BRCA2"],\n "gene_id": ["ENST00000302118"],\n "feature_id": ["ENST00000302118.5"],\n "consequences": ["5 prime UTR variant"],\n "putative_impact": ["MODIFIER"],\n "hgvs_c": ["c.-49A>G"],\n "hgvs_p": ["p.Gly2Asp"]\n}' + ) + sys.exit(0) + elif args.retrieve_genotype: + if args.json_help: + print( + comment_fill('Filters and respective definitions', comment_string='# ') + '\n#\n' + + comment_fill('allele_id: ID(s) of one or more alleles for which sample genotypes will be returned. If multiple values are provided, any samples having at least one allele that match any of the values specified will be listed. For example, ["1_1000_A_T", "1_1010_C_T"], will search for samples with at least one allele matching either "1_1000_A_T" or "1_1010_C_T". String match is case insensitive.') + '\n#\n' + + comment_fill('location: Genomic position in the reference genome of the starting position of the alleles. If multiple values are provided in the list, the conditional search will be, "OR." String match is case sensitive.') + '\n#\n' + + comment_fill('allele_id and location are mutually exclusive filters.') + '\n#\n' + + comment_fill('sample_id: Optional, one or more sample IDs for which sample genotypes will be returned. If the provided object is a cohort, this further intersects the sample ids. If a user has a list of samples more than 1,000, it is recommended to use a cohort id containing all the samples.') + '\n#\n' + + comment_fill('genotype_type: Optional, one or more genotype types for which sample genotype types will be returned.') + '\n' + + comment_fill('One of:') + '\n' + + comment_fill('\tref\t(homozygous for the reference allele\t\t\te.g. 0/0)') + '\n' + + comment_fill('\thet-ref\t(heterozygous for the ref allele and alt allele\t\te.g. 0/1)') + '\n' + + comment_fill('\thom\t(homozygous for the non-ref allele\t\t\te.g. 1/1)') + '\n' + + comment_fill('\thet-alt\t(heterozygous with two distinct alt alleles\t\te.g. 1/2)') + '\n' + + comment_fill('\thalf\t(only one allele is known, second allele is unknown\te.g. ./1)') + '\n' + + comment_fill('\tno-call\t(both alleles are unknown\t\t\t\te.g. ./.)') + '\n#\n' + + comment_fill('JSON filter templates for --retrieve-genotype', comment_string='# ') + '\n#\n' + + comment_fill('Example using location:', comment_string='# ') + '\n' + + pretty_print_json( + { + "sample_id": ["s1", "s2"], + "location": [ + {"chromosome": "1", "starting_position": "10000"}, + {"chromosome": "X", "starting_position": "500"}, + ], + "genotype_type": ["ref", "het-ref", "hom", "het-alt", "half", "no-call"], + } + ) + '\n' + + comment_fill('Example using allele_id:', comment_string='# ') + '\n' + + pretty_print_json({ + "sample_id": ["s1", "s2"], + "allele_id": ["1_1000_A_T", "2_1000_G_C"], + "genotype_type": ["het-ref", "hom", "het-alt", "half"], + } + ) + ) + sys.exit(0) + + if args.retrieve_allele: + filter_dict = json_validation_function("allele", args) + elif args.retrieve_annotation: + filter_dict = json_validation_function("annotation", args) + elif args.retrieve_genotype: + filter_dict = json_validation_function("genotype", args) + + #### Validate that a retrieve option is passed with --assay-name #### + if args.assay_name: + if not filter_given: + err_exit( + "--assay-name must be used with one of --retrieve-allele,--retrieve-annotation, --retrieve-genotype." + ) + + #### Validate that a retrieve option is passed with --sql #### + if args.sql: + if not filter_given: + err_exit( + "When --sql provided, must also provide at least one of the three options: --retrieve-allele ; --retrieve-genotype ; --retrieve-annotation ." + ) + + ######## Data Processing ######## + project, entity_result, resp, dataset_project = resolve_validate_record_path(args.path) + + if "CohortBrowser" in resp["recordTypes"] and any( + [args.list_assays, args.assay_name] + ): + err_exit( + "Currently --assay-name and --list-assays may not be used with a CohortBrowser record (Cohort Object) as input. To select a specific assay or to list assays, please use a Dataset Object as input." + ) + dataset_id = resp["dataset"] + rec_descriptor = DXDataset(dataset_id, project=dataset_project).get_descriptor() + + selected_assay_name, selected_assay_id, selected_ref_genome, additional_descriptor_info = get_assay_name_info( + args.list_assays, args.assay_name, args.path, "germline", rec_descriptor + ) + + out_file, print_to_stdout = assign_output_method(args, resp["recordName"], "germline") + + filter_type = None + if args.retrieve_allele: + filter_type = "allele" + elif args.retrieve_annotation: + filter_type = "annotation" + + if filter_type and filter_given: + payload, fields_list = final_payload( + full_input_dict=filter_dict, + name=selected_assay_name, + id=selected_assay_id, + project_context=project, + genome_reference=selected_ref_genome, + filter_type=filter_type, + ) + + add_germline_base_sql(resp, payload) + + if args.sql: + sql_results = raw_query_api_call(resp, payload) + + if print_to_stdout: + print(sql_results) + else: + with open(out_file, "w") as sql_file: + print(sql_results, file=sql_file) + else: + resp_raw = raw_api_call(resp, payload) + ordered_results = sorted(resp_raw["results"], key=sort_germline_variant) + + csv_from_json( + out_file_name=out_file, + print_to_stdout=print_to_stdout, + sep="\t", + raw_results=ordered_results, + column_names=fields_list, + quote_char=str("|"), + ) + + if args.retrieve_genotype and filter_given: + exclude_refdata: bool = additional_descriptor_info.get("exclude_refdata") + exclude_halfref: bool = additional_descriptor_info.get("exclude_halfref") + exclude_nocall: bool = additional_descriptor_info.get("exclude_nocall") + inference_validation( + args.infer_nocall, + args.infer_ref, + filter_dict, + exclude_nocall, + exclude_refdata, + exclude_halfref + ) + # in case of infer flags, we query all the genotypes and do the filtering post query + if args.infer_ref or args.infer_nocall: + types_to_filter_out = get_types_to_filter_out_when_infering(filter_dict.get("genotype_type", [])) + filter_dict["genotype_type"] = [] + + # get a list of requested genotype types for the genotype table only queries + if "allele_id" in filter_dict: + genotype_only_types = [] + else: + genotype_only_types = get_genotype_only_types(filter_dict, + exclude_refdata, exclude_halfref, exclude_nocall) + + # get the payload for the genotype/allele table query for alternate genotype types + genotype_payload, fields_list = final_payload( + full_input_dict=filter_dict, + name=selected_assay_name, + id=selected_assay_id, + project_context=project, + genome_reference=selected_ref_genome, + filter_type="genotype", + order=not genotype_only_types, + ) + + add_germline_base_sql(resp, genotype_payload) + + genotype_only_payloads = [] + if genotype_only_types: + # get the payloads for the genotype table only query + # assay_filter does not support "or" so there is a separate query for each partition + for i, genotype_only_type in enumerate(genotype_only_types): + genotype_only_filter_dict = filter_dict.copy() + if genotype_only_type == "ref": + genotype_only_filter_dict["ref_yn"] = True + elif genotype_only_type == "half": + genotype_only_filter_dict["halfref_yn"] = True + elif genotype_only_type == "no-call": + genotype_only_filter_dict["nocall_yn"] = True + + genotype_only_payload, _ = final_payload( + full_input_dict=genotype_only_filter_dict, + name=selected_assay_name, + id=selected_assay_id, + project_context=project, + genome_reference=selected_ref_genome, + filter_type="genotype_only", + exclude_refdata=genotype_only_type != "ref", + exclude_halfref=genotype_only_type != "half", + exclude_nocall=genotype_only_type != "no-call", + order=i == len(genotype_only_types) - 1, + ) + + add_germline_base_sql(resp, genotype_only_payload) + + genotype_only_payloads.append(genotype_only_payload) + + # get the list of requested genotype types for the genotype/allele table query + genotype_types = get_genotype_types(filter_dict) + + if args.sql: + sql_queries = [] + if genotype_types: + # get the genotype/allele table query + genotype_sql_query = raw_query_api_call(resp, genotype_payload)[:-1] + try: + geno_table_regex = r"\b" + additional_descriptor_info["genotype_type_table"] + r"\w+" + re.search(geno_table_regex, genotype_sql_query).group() + except Exception: + err_exit("Failed to find the table, {}, in the generated SQL".format( + additional_descriptor_info["genotype_type_table"]), expected_exceptions=(AttributeError,)) + sql_queries.append(genotype_sql_query) + + # get the genotype table only query + # assay_filter does not support "or" so there is a separate query for each partition + for genotype_only_payload in genotype_only_payloads: + # join the allele table to get the ref, will join on locus_id + genotype_only_payload["fields"].append({"ref": "allele$ref"}) + genotype_only_sql_query = raw_query_api_call(resp, genotype_only_payload)[:-1] + # update the query to add column in the genotype/allele table query and join on locus_id + sql_queries.append(harmonize_germline_sql(genotype_only_sql_query)) + + # combine the queries into a single query + sql_results = " UNION ".join(sql_queries) + ";" + + if print_to_stdout: + print(sql_results) + else: + with open(out_file, "w") as sql_file: + print(sql_results, file=sql_file) + else: + # get the list of dictionary results for the genotype/allele table query + ordered_results = [] + if genotype_types: + genotype_resp_raw = raw_api_call(resp, genotype_payload) + ordered_results.extend(genotype_resp_raw["results"]) + + # get the list of dictionary results for each genotype table only query + for genotype_only_payload in genotype_only_payloads: + genotype_only_resp_raw = raw_api_call(resp, genotype_only_payload) + # add missing keys that are in the allele table part of the genotype/allele table query + ordered_results.extend(harmonize_germline_results(genotype_only_resp_raw["results"], fields_list)) + + if genotype_only_types: + # get the ref value from the allele table using locus ids + # ingestion of VCFs lines missing ALT is unsupported so the locus_id will exist in the allele table + # normalized ref values in the locus_id will match the ref value for missing ALT lines if they + # were ingested and locus_id could be parsed for the ref value + ref_payload = get_germline_ref_payload(ordered_results, genotype_payload) + if ref_payload: + locus_id_refs = raw_api_call(resp, ref_payload) + update_genotype_only_ref(ordered_results, locus_id_refs) + + if args.infer_ref or args.infer_nocall: + samples = retrieve_samples(resp, selected_assay_name, selected_assay_id) + selected_samples = set(filter_dict.get("sample_id", [])) + if selected_samples: + samples = list(selected_samples.intersection(samples)) + loci_payload = get_germline_loci_payload(filter_dict["location"], genotype_payload) + loci = [locus for locus in raw_api_call(resp, loci_payload)["results"]] + type_to_infer = "ref" if args.infer_ref else "no-call" + ordered_results = infer_genotype_type(samples, loci, ordered_results, type_to_infer) + # Filter out not requested genotypes + if len(types_to_filter_out) > 0: + ordered_results = filter_results(ordered_results, "genotype_type", types_to_filter_out) + + ordered_results.sort(key=sort_germline_variant) + + csv_from_json( + out_file_name=out_file, + print_to_stdout=print_to_stdout, + sep="\t", + raw_results=ordered_results, + column_names=fields_list, + quote_char=str("|"), + ) + + +def retrieve_entities(model): + """ + Retrieves the entities in form of \t and identifies main entity + """ + entity_names_and_titles = [] + for entity in sorted(model["entities"].keys()): + entity_names_and_titles.append( + "{}\t{}".format(entity, model["entities"][entity]["entity_title"]) + ) + if model["entities"][entity]["is_main_entity"] is True: + main_entity = entity + return entity_names_and_titles, main_entity + + +def list_fields(model, main_entity, args): + """ + Listing fileds in the model in form at .\t for specified list of entities + """ + present_entities = model["entities"].keys() + entities_to_list_fields = [model["entities"][main_entity]] + if args.entities: + entities_to_list_fields = [] + error_list = [] + for entity in sorted(args.entities.split(",")): + if entity in present_entities: + entities_to_list_fields.append(model["entities"][entity]) + else: + error_list.append(entity) + if error_list: + err_exit("The following entity/entities cannot be found: %s" % error_list) + fields = [] + for entity in entities_to_list_fields: + for field in sorted(entity["fields"].keys()): + fields.append( + "{}.{}\t{}".format( + entity["name"], field, entity["fields"][field]["title"] + ) + ) + print("\n".join(fields)) + + +def csv_from_json( + out_file_name="", + print_to_stdout=False, + sep=",", + raw_results=[], + column_names=[], + quote_char=str('"'), + quoting=csv.QUOTE_MINIMAL, +): + if print_to_stdout: + fields_output = sys.stdout + else: + fields_output = open(out_file_name, "w") + + csv_writer = csv.DictWriter( + fields_output, + delimiter=str(sep), + doublequote=True, + escapechar=None, + lineterminator="\n", + quotechar=quote_char, + quoting=quoting, + skipinitialspace=False, + strict=False, + fieldnames=column_names, + ) + csv_writer.writeheader() + for entry in raw_results: + csv_writer.writerow(entry) + + if not print_to_stdout: + fields_output.close() + + +def extract_assay_somatic(args): + """ + Retrieve the selected data or generate SQL to retrieve the data from an somatic variant assay in a dataset or cohort based on provided rules. + """ + + ######## Input combination validation and print help######## + invalid_combo_args = any([args.include_normal_sample, args.additional_fields, args.json_help, args.sql]) + + if args.retrieve_meta_info and invalid_combo_args: + err_exit( + "The flag, --retrieve-meta-info cannot be used with arguments other than --assay-name, --output." + ) + + if args.list_assays and any([args.assay_name, args.output, invalid_combo_args]): + err_exit( + '--list-assays cannot be presented with other options.' + ) + + if args.json_help: + if any([args.assay_name, args.output, args.include_normal_sample, args.additional_fields, args.sql]): + err_exit( + "--json-help cannot be passed with any of --assay-name, --sql, --additional-fields, --additional-fields-help, --output." + ) + elif args.retrieve_variant is None: + err_exit("--json-help cannot be passed without --retrieve-variant.") + else: + print( + comment_fill('Filters and respective definitions', comment_string='# ') + '\n#\n' + + comment_fill('location: "location" filters variants based on having an allele_id which has a corresponding annotation row which matches the supplied "chromosome" with CHROM and where the start position (POS) of the allele_id is between and including the supplied "starting_position" and "ending_position". If multiple values are provided in the list, the conditional search will be, "OR". String match is case sensitive.') + '\n#\n' + + comment_fill('symbol: "symbol" filters variants based on having an allele_id which has a corresponding annotation row which has a matching symbol (gene) name. If multiple values are provided, the conditional search will be, "OR". For example, ["BRCA2", "ASPM"], will search for variants which match either "BRCA2" or "ASPM". String match is case sensitive.') + '\n#\n' + + comment_fill('gene: "gene" filters variants based on having an allele_id which has a corresponding annotation row which has a matching gene ID of the variant. If multiple values are provided, the conditional search will be, "OR". For example, ["ENSG00000302118", "ENSG00004000504"], will search for variants which match either "ENSG00000302118" or "ENSG00004000504". String match is case insensitive.') + '\n#\n' + + comment_fill('feature: "feature" filters variants based on having an allele_id which has a corresponding annotation row which has a matching feature ID. The most common Feature ID is a transcript_id. If multiple values are provided, the conditional search will be, "OR". For example, ["ENST00000302118", "ENST00004000504"], will search for variants which match either "ENST00000302118" or "ENST00004000504". String match is case insensitive.') + '\n#\n' + + comment_fill('hgvsc: "hgvsc" filters variants based on having an allele_id which has a corresponding annotation row which has a matching HGVSc. If multiple values are provided, the conditional search will be, "OR". For example, ["c.-49A>G", "c.-20T>G"], will search for alleles which match either "c.-49A>G" or "c.-20T>G". String match is case sensitive.') + '\n#\n' + + comment_fill('hgvsp: "hgvsp" filters variants based on having an allele_id which has a corresponding annotation row which has a matching HGVSp. If multiple values are provided, the conditional search will be, "OR". For example, ["p.Gly2Asp", "p.Aps2Gly"], will search for variants which match either "p.Gly2Asp" or "p.Aps2Gly". String match is case sensitive.') + '\n#\n' + + comment_fill('allele_id: "allele_id" filters variants based on allele_id match. If multiple values are provided, anymatch will be returned. For example, ["1_1000_A_T", "1_1010_C_T"], will search for allele_ids which match either "1_1000_A_T" or ""1_1010_C_T". String match is case sensitive/exact match.') + '\n#\n' + + comment_fill('variant_type: Type of allele. Accepted values are "SNP", "INS", "DEL", "DUP", "INV", "CNV", "CNV:TR", "BND", "DUP:TANDEM", "DEL:ME", "INS:ME", "MISSING", "MISSING:DEL", "UNSPECIFIED", "REF" or "OTHER". If multiple values are provided, the conditional search will be, "OR". For example, ["SNP", "INS"], will search for variants which match either "SNP" or ""INS". String match is case insensitive.') + '\n#\n' + + comment_fill('sample_id: "sample_id" filters either a pair of tumor-normal samples based on having sample_id which has a corresponding sample row which has a matching sample_id. If a user has more than 500 IDs, it is recommended to either retrieve multiple times, or use a cohort id containing all desired individuals, providing the full set of sample_ids.') + '\n#\n' + + comment_fill('assay_sample_id: "assay_sample_id" filters either a tumor or normal sample based on having an assay_sample_id which has a corresponding sample row which has a matching assay_sample_id. If a user has a list of more than 1,000 IDs, it is recommended to either retrieve multiple times, or use a cohort id containing all desired individuals, providing the full set of assay_sample_ids.') + '\n#\n' + + comment_fill('JSON filter template for --retrieve-variant', comment_string='# ') + '\n' + + '{\n "location": [\n {\n "chromosome": "1",\n "starting_position": "10000",\n "ending_position": "20000"\n },\n {\n "chromosome": "X",\n "starting_position": "500",\n "ending_position": "1700"\n }\n ],\n "annotation": {\n "symbol": ["BRCA2"],\n "gene": ["ENST00000302118"],\n "feature": ["ENST00000302118.5"],\n "hgvsc": ["c.-49A>G"],\n "hgvsp": ["p.Gly2Asp"]\n },\n "allele" : {\n "allele_id":["1_1000_A_T","2_1000_G_C"],\n "variant_type" : ["SNP", "INS"]\n },\n "sample": {\n "sample_id": ["Sample1", "Sample2"],\n "assay_sample_id" : ["Sample1_tumt", "Sample1_nor"]\n }\n}' + ) + sys.exit(0) + + if args.additional_fields_help: + if any([args.assay_name, args.output, invalid_combo_args]): + err_exit( + '--additional-fields-help cannot be presented with other options.' + ) + else: + def print_fields(fields): + for row in fields: + fields_string = "{: <22} {: <22} ".format(*row[:2]) + width = len(fields_string) + fields_string += re.sub('\n', '\n' + ' ' * width, fill(row[2], width_adjustment=-width)) + print(fields_string) + print(fill('The following fields will always be returned by default:') + '\n') + fixed_fields = [['NAME', 'TITLE', 'DESCRIPTION'], + ['assay_sample_id', 'Assay Sample ID', 'A unique identifier for the tumor or normal sample. Populated from the sample columns of the VCF header.'], + ['allele_id', 'Allele ID', 'An unique identification of the allele'], + ['CHROM', 'Chromosome', 'Chromosome of variant, verbatim from original VCF'], + ['POS', 'Position', 'Starting position of variant, verbatim from original VCF'], + ['REF', 'Reference Allele', 'Reference allele of locus, verbatim from original VCF'], + ['allele', 'Allele', 'Sequence of the allele']] + print_fields(fixed_fields) + print('\n' + fill('The following fields may be added to the output by using option --additional-fields. If multiple fields are specified, use a comma to separate each entry. For example, "sample_id,tumor_normal"') + '\n') + additional_fields = [['NAME', 'TITLE', 'DESCRIPTION'], + ['sample_id', 'Sample ID', 'Unique ID of the pair of tumor-normal samples'], + ['tumor_normal', 'Tumor-Normal', 'One of ["tumor", "normal"] to describe source sample type'], + ['ID', 'ID', 'Comma separated list of associated IDs for the variant from the original VCF'], + ['QUAL', 'QUAL', 'Quality of locus, verbatim from original VCF'], + ['FILTER', 'FILTER', 'Comma separated list of filters for locus from the original VCF'], + ['reference_source', 'Reference Source', 'One of ["GRCh37", "GRCh38"] or the allele_sample_id of the respective normal sample'], + ['variant_type', 'Variant Type', 'The type of allele, with respect to reference'], + ['symbolic_type', 'Symbolic Type', 'One of ["precise", "imprecise"]. Non-symbolic alleles are always "precise"'], + ['file_id', 'Source File ID', 'DNAnexus platform file-id of original source file'], + ['INFO', 'INFO', 'INFO section, verbatim from original VCF'], + ['FORMAT', 'FORMAT', 'FORMAT section, verbatim from original VCF'], + ['SYMBOL', 'Symbol', 'A list of gene name associated with the variant'], + ['GENOTYPE', 'GENOTYPE', 'GENOTYPE section, as described by FORMAT section, verbatim from original VCF'], + ['normal_assay_sample_id', 'Normal Assay Sample ID', 'Assay Sample ID of respective "normal" sample, if exists'], + ['normal_allele_ids', 'Normal Allele IDs', 'Allele ID(s) of respective "normal" sample, if exists'], + ['Gene', 'Gene ID', 'A list of gene IDs, associated with the variant'], + ['Feature', 'Feature ID', 'A list of feature IDs, associated with the variant'], + ['HGVSc', 'HGVSc', 'A list of sequence variants in HGVS nomenclature, for DNA'], + ['HGVSp', 'HGVSp', 'A list of sequence variants in HGVS nomenclature, for protein'], + ['CLIN_SIG', 'Clinical Significance', 'A list of allele specific clinical significance terms'], + ['ALT', 'ALT', 'Alternate allele(s) at locus, comma separated if more than one, verbatim from original VCF'], + ['alt_index', 'ALT allele_index', 'Order of the allele, as represented in the ALT field. If the allele is missing (i.e, "./0", "0/." or "./.") then the alt_index will be empty']] + print_fields(additional_fields) + sys.exit(0) + + # Validate additional fields + + if args.additional_fields is not None: + additional_fields_input = [additional_field.strip() for additional_field in args.additional_fields.split(",")] + accepted_additional_fields = ['sample_id', 'tumor_normal', 'ID', 'QUAL', 'FILTER', 'reference_source', 'variant_type', 'symbolic_type', 'file_id', 'INFO', 'FORMAT', 'SYMBOL', 'GENOTYPE', 'normal_assay_sample_id', 'normal_allele_ids', 'Gene', 'Feature', 'HGVSc', 'HGVSp', 'CLIN_SIG', 'ALT', 'alt_index'] + for field in additional_fields_input: + if field not in accepted_additional_fields: + err_exit("One or more of the supplied fields using --additional-fields are invalid. Please run --additional-fields-help for a list of valid fields") + + ######## Data Processing ######## + project, entity_result, resp, dataset_project = resolve_validate_record_path(args.path) + if "CohortBrowser" in resp["recordTypes"] and any([args.list_assays,args.assay_name]): + err_exit( + "Currently --assay-name and --list-assays may not be used with a CohortBrowser record (Cohort Object) as input. To select a specific assay or to list assays, please use a Dataset Object as input." + ) + dataset_id = resp["dataset"] + rec_descriptor = DXDataset(dataset_id, project=dataset_project).get_descriptor() + + selected_assay_name, selected_assay_id, selected_ref_genome, additional_descriptor_info = get_assay_name_info( + args.list_assays, args.assay_name, args.path, "somatic", rec_descriptor + ) + + out_file, print_to_stdout = assign_output_method(args, resp["recordName"], "somatic") + + if args.retrieve_meta_info: + retrieve_meta_info(resp, project, selected_assay_id, selected_assay_name, print_to_stdout, out_file) + sys.exit(0) + + if args.retrieve_variant: + filter_dict = json_validation_function("variant", args) + + if args.additional_fields: + payload, fields_list = somatic_final_payload( + full_input_dict=filter_dict, + name=selected_assay_name, + id=selected_assay_id, + project_context=project, + genome_reference=selected_ref_genome, + additional_fields=additional_fields_input, + include_normal=args.include_normal_sample, + ) + else: + payload, fields_list = somatic_final_payload( + full_input_dict=filter_dict, + name=selected_assay_name, + id=selected_assay_id, + project_context=project, + genome_reference=selected_ref_genome, + include_normal=args.include_normal_sample, + ) + + if "CohortBrowser" in resp["recordTypes"]: + if resp.get("baseSql"): + payload["base_sql"] = resp.get("baseSql") + payload["filters"] = resp["filters"] + + #### Run api call to get sql or extract data #### + + if args.sql: + sql_results = raw_query_api_call(resp, payload) + if print_to_stdout: + print(sql_results) + else: + with open(out_file, "w") as sql_file: + print(sql_results, file=sql_file) + else: + resp_raw = raw_api_call(resp, payload) + + csv_from_json( + out_file_name=out_file, + print_to_stdout=print_to_stdout, + sep="\t", + raw_results=resp_raw["results"], + column_names=fields_list, + quote_char=str("\t"), + quoting=csv.QUOTE_NONE, + ) + +def extract_assay_expression(args): + """ + `dx extract_assay expression` + + Retrieve the selected data or generate SQL to retrieve the data from an expression assay in a dataset or cohort based on provided rules. + """ + + # Validating input arguments (argparse) combinations + parser_dict = vars(args) + input_validator = ArgsValidator( + parser_dict=parser_dict, + schema=EXTRACT_ASSAY_EXPRESSION_INPUT_ARGS_SCHEMA, + error_handler=err_exit, + ) + input_validator.validate_input_combination() + + if args.json_help: + print(EXTRACT_ASSAY_EXPRESSION_JSON_HELP) + sys.exit(0) + + if args.additional_fields_help: + print(EXTRACT_ASSAY_EXPRESSION_ADDITIONAL_FIELDS_HELP) + sys.exit(0) + + # Resolving `path` argument + if args.path: + # entity_result contains `id` and `describe` + project, folder_path, entity_result = resolve_existing_path(args.path) + if entity_result is None: + err_exit( + 'Unable to resolve "{}" to a data object in {}.'.format( + args.path, project + ) + ) + else: + entity_describe = entity_result.get("describe") + + # Is object in current project? + if project != entity_result["describe"]["project"]: + err_exit( + 'Unable to resolve "{}" to a data object or folder name in {}. Please make sure the object is in the selected project.'.format( + args.path, project + ) + ) + + # Is object a cohort or a dataset? + EXPECTED_TYPES = ["Dataset", "CohortBrowser"] + _record_types = entity_result["describe"]["types"] + if entity_result["describe"]["class"] != "record" or all( + x not in _record_types for x in EXPECTED_TYPES + ): + err_exit( + "{} Invalid path. The path must point to a record type of cohort or dataset and not a {} object.".format( + entity_result["id"], _record_types + ) + ) + + # Cohort/Dataset handling + record = DXRecord(entity_describe["id"]) + dataset, cohort_info = Dataset.resolve_cohort_to_dataset(record) + + if cohort_info: + if args.assay_name or args.list_assays: + err_exit( + 'Currently "--assay-name" and "--list-assays" may not be used with a CohortBrowser record (Cohort Object) as input. To select a specific assay or to list assays, please use a Dataset object as input.' + ) + if float(cohort_info["details"]["version"]) < 3.0: + err_exit( + "{}: Version of the cohort is too old. Version must be at least 3.0.".format( + cohort_info["id"] + ) + ) + + BASE_SQL = cohort_info.get("details").get("baseSql") + COHORT_FILTERS = cohort_info.get("details").get("filters") + IS_COHORT = True + else: + BASE_SQL = None + COHORT_FILTERS = None + IS_COHORT = False + if float(dataset.version) < 3.0: + err_exit( + "{}: Version of the dataset is too old. Version must be at least 3.0.".format( + dataset.id + ) + ) + + if args.list_assays: + print(*dataset.assay_names_list("molecular_expression"), sep="\n") + sys.exit(0) + + # Check whether assay_name is valid + # If no assay_name is provided, the first molecular_expression assay in the dataset must be selected + if args.assay_name: + if not dataset.is_assay_name_valid(args.assay_name, "molecular_expression"): + err_exit( + "Assay {} does not exist in {}, or the assay name provided cannot be recognized as a molecular expression assay. For valid assays accepted by the function, `extract_assay expression`, please use the --list-assays flag.".format( + args.assay_name, dataset.id + ) + ) + elif not args.assay_name: + if dataset.assays_info_dict.get("molecular_expression") is None or len(dataset.assays_info_dict.get("molecular_expression")) == 0: + err_exit("No molecular expression assays found in the dataset") + + # Load args.filter_json or args.filter_json_file into a dict + if sys.version_info.major == 2: + # In Python 2 JSONDecodeError does not exist + json.JSONDecodeError = ValueError + + if args.filter_json: + try: + user_filters_json = json.loads(args.filter_json) + except json.JSONDecodeError as e: + json_reading_error = ( + "JSON provided for --retrieve-expression is malformatted." + + "\n" + + str(e) + ) + err_exit(json_reading_error) + except Exception as e: + err_exit(str(e)) + + elif args.filter_json_file: + try: + with open(args.filter_json_file) as f: + user_filters_json = json.load(f) + except json.JSONDecodeError as e: + json_reading_error = ( + "JSON provided for --retrieve-expression is malformatted." + + "\n" + + str(e) + ) + err_exit(json_reading_error) + except OSError as e: + if "No such file or directory" in str(e): + err_exit( + "JSON file {} provided to --retrieve-expression does not exist".format( + e.filename + ) + ) + else: + err_exit(str(e)) + except Exception as e: + err_exit(str(e)) + + if user_filters_json == {}: + err_exit( + "No filter JSON is passed with --retrieve-expression or input JSON for --retrieve-expression does not contain valid filter information." + ) + + if args.expression_matrix: + if "expression" in user_filters_json: + err_exit( + 'Expression filters are not compatible with --expression-matrix argument. Please remove "expression" from filters JSON.' + ) + + if args.additional_fields: + err_exit( + "--additional-fields cannot be used with --expression-matrix argument." + ) + + # Replace 'str' with 'unicode' when checking types in Python 2 + if sys.version_info.major == 2: + EXTRACT_ASSAY_EXPRESSION_JSON_SCHEMA["location"]["items"]["properties"][ + "chromosome" + ]["type"] = unicode + EXTRACT_ASSAY_EXPRESSION_JSON_SCHEMA["location"]["items"]["properties"][ + "starting_position" + ]["type"] = unicode + EXTRACT_ASSAY_EXPRESSION_JSON_SCHEMA["location"]["items"]["properties"][ + "ending_position" + ]["type"] = unicode + + # Validate filters JSON provided by the user according to a predefined schema + input_json_validator = JSONValidator( + schema=EXTRACT_ASSAY_EXPRESSION_JSON_SCHEMA, error_handler=err_exit + ) + input_json_validator.validate(input_json=user_filters_json) + + if "location" in user_filters_json: + if args.sql: + EXTRACT_ASSAY_EXPRESSION_FILTERING_CONDITIONS["filtering_conditions"][ + "location" + ]["max_item_limit"] = None + + else: + # Genomic range adding together across multiple contigs should be smaller than 250 Mbps + input_json_validator.are_list_items_within_range( + input_json=user_filters_json, + key="location", + start_subkey="starting_position", + end_subkey="ending_position", + window_width=250000000, + check_each_separately=False, + ) + + input_json_parser = JSONFiltersValidator( + input_json=user_filters_json, + schema=EXTRACT_ASSAY_EXPRESSION_FILTERING_CONDITIONS, + error_handler=err_exit, + ) + vizserver_raw_filters = input_json_parser.parse() + + _db_columns_list = EXTRACT_ASSAY_EXPRESSION_FILTERING_CONDITIONS[ + "output_fields_mapping" + ].get("default") + + if args.additional_fields: + # All three of the following should work: + # --additional_fields field1 field2 + # --additional_fields field1,field2 + # --additional_fields field1, field2 (note the space char) + # In the first case, the arg will look like ["field1", "field2"] + # In the second case: ["field1,field2"] + # In the third case: ["field1,", "field2"] + additional_fields = [] + for item in args.additional_fields: + field = [x.strip() for x in item.split(",") if x.strip()] + additional_fields.extend(field) + + all_additional_cols = EXTRACT_ASSAY_EXPRESSION_FILTERING_CONDITIONS[ + "output_fields_mapping" + ].get("additional") + incorrect_cols = set(additional_fields) - set( + {k for d in all_additional_cols for k in d.keys()} + ) + if len(incorrect_cols) != 0: + err_exit( + "One or more of the supplied fields using --additional-fields are invalid. Please run --additional-fields-help for a list of valid fields" + ) + user_additional_cols = [ + i for i in all_additional_cols if set(i.keys()) & set(additional_fields) + ] + _db_columns_list.extend(user_additional_cols) + + viz = VizPayloadBuilder( + project_context=project, + output_fields_mapping=_db_columns_list, + filters={"filters": COHORT_FILTERS} if IS_COHORT else None, + order_by=EXTRACT_ASSAY_EXPRESSION_FILTERING_CONDITIONS["order_by"], + limit=None, + base_sql=BASE_SQL, + is_cohort=IS_COHORT, + error_handler=err_exit, + ) + + DATASET_DESCRIPTOR = dataset.descriptor_file_dict + ASSAY_NAME = ( + args.assay_name if args.assay_name else dataset.assay_names_list("molecular_expression")[0] + ) + + if args.assay_name: + # Assumption: assay names are unique in a dataset descriptor + # i.e. there are never two assays of the same type with the same name in the same dataset + for molecular_assay in dataset.assays_info_dict["molecular_expression"]: + if molecular_assay["name"] == args.assay_name: + ASSAY_ID = molecular_assay["uuid"] + break + else: + ASSAY_ID = dataset.assays_info_dict["molecular_expression"][0]["uuid"] + + viz.assemble_assay_raw_filters( + assay_name=ASSAY_NAME, assay_id=ASSAY_ID, filters=vizserver_raw_filters + ) + vizserver_payload = viz.build() + + # Get the record ID and vizserver URL from the Dataset object + record_id = dataset.detail_describe["id"] + url = dataset.vizserver_url + + # Create VizClient object and get data from vizserver using generated payload + client = VizClient(url, project, err_exit) + if args.sql: + vizserver_response = client.get_raw_sql(vizserver_payload, record_id) + else: + vizserver_response = client.get_data(vizserver_payload, record_id) + + colnames = None + + # Output is on the "sql" key rather than the "results" key when sql is requested + output_data = ( + vizserver_response["sql"] if args.sql else vizserver_response["results"] + ) + + # Output data (from vizserver_response["results"]) will be an empty list if no data is returned for the given filters + if args.expression_matrix and output_data: + transformed_response, colnames = transform_to_expression_matrix( + vizserver_response["results"] + ) + output_data = transformed_response + + if not output_data: + # write_expression_output expects a list of dicts + output_data = [{}] + + write_expression_output( + args.output, + args.delim, + args.sql, + output_data, + save_uncommon_delim_to_txt=True, + output_file_name=dataset.detail_describe["name"], + colnames=colnames, + ) + + + +#### CREATE COHORT #### +def resolve_validate_dx_path(path): + """ + Resolves dx path into project, folder and name. Fails if non existing folder is provided. + """ + project, folder, name = resolve_path(path) + err_msg, folder_exists = None, None + if folder != "/": + folder_name = os.path.basename(folder) + folder_path = os.path.dirname(folder) + try: + folder_exists = check_folder_exists(project, folder_path, folder_name) + except ResolutionError as e: + if "folder could not be found" in str(e): + folder_exists = False + else: + raise e + if not folder_exists: + err_msg = "The folder: {} could not be found in the project: {}".format( + folder, project + ) + + return project, folder, name, err_msg + +class VizserverError(Exception): + pass + +def validate_cohort_ids(descriptor, project, resp, ids): + # Usually the name of the table + entity_name = descriptor.model["global_primary_key"]["entity"] + # The name of the column or field in the table + field_name = descriptor.model["global_primary_key"]["field"] + + # Get data type of global primay key field + gpk_type = descriptor.model["entities"][entity_name]["fields"][field_name]["mapping"]["column_sql_type"] + # Prepare a payload to find entries matching the input ids in the dataset + if gpk_type in ["integer", "bigint"]: + if USING_PYTHON2: + lambda_for_list_conv = lambda a, b: a+[long(b)] + else: + lambda_for_list_conv = lambda a, b: a+[int(b)] + elif gpk_type in ["float", "double"]: + lambda_for_list_conv = lambda a, b: a+[float(b)] + elif gpk_type in ["string"]: + lambda_for_list_conv = lambda a, b: a+[str(b)] + else: + err_msg = "Invalid input record. Cohort ID field in the input dataset or cohortbrowser record is of type, {type}. Support is currently only available for Cohort ID fields having one of the following types; string, integer and float".format(type = gpk_type) + raise ValueError(err_msg) + id_list = reduce(lambda_for_list_conv, ids, []) + + entity_field_name = "{}${}".format(entity_name, field_name) + fields_list = [{field_name: entity_field_name}] + + # Note that pheno filters do not need name or id fields + payload = { + "project_context": project, + "fields": fields_list, + "filters":{ + "pheno_filters": { + "filters": { + entity_field_name: [ + {"condition": "in", "values": id_list} + ] + } + } + } + } + + # Use the dxpy raw_api_function to send a POST request to the server with our payload + try: + resp_raw = raw_api_call(resp, payload) + except Exception as exc: + raise VizserverError( + "Exception caught while validating cohort ids. Bad response from Vizserver." + "Original Error message:\n{}" + ).format(str(exc)) + # Order of samples doesn't matter so using set here + discovered_ids = set() + # Parse the results objects for the cohort ids + for result in resp_raw["results"]: + discovered_ids.add(result[field_name]) + + # Compare the discovered cohort ids to the user-provided cohort ids + if discovered_ids != set(id_list): + # Find which given samples are not present in the dataset + missing_ids = set(id_list).difference(discovered_ids) + err_msg = "The following supplied IDs do not match IDs in the main entity of dataset, {dataset_name}: {ids}".format(dataset_name = resp["dataset"], ids = missing_ids) + raise ValueError(err_msg) + + return id_list, lambda_for_list_conv + + +def has_access_level(project, access_level): + """ + Validates that issuing user has required access level. + Args: + project: str: tasked project_id + access_level: str: minimum requested level + Retuns: boolean + """ + level_rank = ["VIEW", "UPLOAD", "CONTRIBUTE", "ADMINISTER"] + access_level_idx = level_rank.index(access_level) + try: + project_describe = describe(project) + except PermissionDenied: + return False + if level_rank.index(project_describe["level"]) < access_level_idx: + return False + return True + + +def validate_project_access(project, access_level="UPLOAD"): + """ + Validates that project has requested access. + Args: + project: str: tasked project_id + access_level: str: minimum requested level Default at least UPLOAD + Returns: + Error message + + """ + if not has_access_level(project, access_level): + return "At least {} permission is required to create a record in a project".format( + access_level + ) + + +def create_cohort(args): + """ + Create a cohort from dataset/cohort and specified list of samples. + """ + #### Validation #### + # Validate and resolve 'PATH' input + + # default path values + path_project = dxpy.WORKSPACE_ID + path_folder = dxpy.config.get('DX_CLI_WD', '/') + path_name = None + if args.PATH: + path_project, path_folder, path_name, err_msg = resolve_validate_dx_path( + args.PATH + ) + if err_msg: + err_exit(err_msg) + err_msg = validate_project_access(path_project) + if err_msg: + err_exit(err_msg) + # validate and resolve 'from' input + FROM = args.__dict__.get("from") + from_project, entity_result, resp, dataset_project = resolve_validate_record_path(FROM) + + #### Reading input sample ids from file or command line #### + samples=[] + # from file + if args.cohort_ids_file: + with open(args.cohort_ids_file, "r") as infile: + for line in infile: + samples.append(line.strip("\n")) + # from string + if args.cohort_ids: + samples = [id.strip() for id in args.cohort_ids.split(",")] + + #### Validate the input cohort IDs #### + # Get the table/entity and field/column of the dataset from the descriptor + rec_descriptor = DXDataset(resp["dataset"], project=dataset_project).get_descriptor() + + try: + list_of_ids, lambda_for_list_conv = validate_cohort_ids(rec_descriptor, dataset_project, resp, samples) + except ValueError as err: + err_exit(str(err), expected_exceptions=(ValueError,)) + except VizserverError as err: + err_exit(str(err), expected_exceptions=(VizserverError,)) + except Exception as err: + err_exit(str(err)) + # Input cohort IDs have been succesfully validated + + # converting list of IDs to list of string IDs + + base_sql = resp.get("baseSql", resp.get("base_sql")) + try: + raw_cohort_query_payload = cohort_filter_payload( + list_of_ids, + rec_descriptor.model["global_primary_key"]["entity"], + rec_descriptor.model["global_primary_key"]["field"], + resp.get("filters", {}), + from_project, + lambda_for_list_conv, + base_sql, + ) + except Exception as e: + err_exit("{}: {}".format(entity_result["id"], e)) + sql = raw_cohort_query_api_call(resp, raw_cohort_query_payload) + cohort_payload = cohort_final_payload( + path_name, + path_folder, + path_project, + resp["databases"], + resp["dataset"], + resp["schema"], + raw_cohort_query_payload["filters"], + sql, + base_sql, + resp.get("combined"), + ) + + dx_record = dxpy.bindings.dxrecord.new_dxrecord(**cohort_payload) + # print record details to stdout + if args.brief: + print(dx_record.get_id()) + else: + try: + print_desc(dx_record.describe(incl_properties=True, incl_details=True), args.verbose) + except Exception as e: + err_exit(str(e)) + + +class DXDataset(DXRecord): + """ + A class to handle record objects of type Dataset. + Inherits from DXRecord, but automatically populates default fields, details and properties. + + Attributes: + All the same as DXRecord + name - from record details + description - from record details + schema - from record details + version - from record details + descriptor - DXDatasetDescriptor object + Functions + get_descriptor - calls DXDatasetDescriptor(descriptor_dxfile) if descriptor is None + get_dictionary - calls descriptor.get_dictionary + + """ + + _record_type = "Dataset" + + def __init__(self, dxid=None, project=None): + super(DXDataset, self).__init__(dxid, project) + self.describe(default_fields=True, fields={"properties", "details"}) + assert self._record_type in self.types + assert "descriptor" in self.details + if is_dxlink(self.details["descriptor"]): + self.descriptor_dxfile = DXFile(self.details["descriptor"], mode="rb", project=project) + else: + err_exit("%s : Invalid cohort or dataset" % self.details["descriptor"]) + self.descriptor = None + self.name = self.details.get("name") + self.description = self.details.get("description") + self.schema = self.details.get("schema") + self.version = self.details.get("version") + + def get_descriptor(self): + if self.descriptor is None: + self.descriptor = DXDatasetDescriptor( + self.descriptor_dxfile, schema=self.schema + ) + return self.descriptor + + def get_dictionary(self): + if self.descriptor is None: + self.get_descriptor() + return self.descriptor.get_dictionary() + + +class DXDatasetDescriptor: + """ + A class to represent a parsed descriptor of a Dataset record object. + + Attributes + Representation of JSON object stored in descriptor file + Functions + get_dictionary - calls DXDatasetDictionary(descriptor) + + """ + + def __init__(self, dxfile, **kwargs): + python3_5_x = sys.version_info.major == 3 and sys.version_info.minor == 5 + + with as_handle(dxfile, is_gzip=True, **kwargs) as f: + if python3_5_x: + jsonstr = f.read() + if type(jsonstr) != str: + jsonstr = jsonstr.decode("utf-8") + + obj = json.loads(jsonstr, object_pairs_hook=collections.OrderedDict) + else: + obj = json.load(f, object_pairs_hook=collections.OrderedDict) + + for key in obj: + setattr(self, key, obj[key]) + self.schema = kwargs.get("schema") + + def get_dictionary(self): + return DXDatasetDictionary(self) + + +class DXDatasetDictionary: + """ + A class to represent data, coding and entity dictionaries based on the descriptor. + All 3 dictionaries will have the same internal representation as dictionaries of string to pandas dataframe. + Attributes + data - dictionary of entity name to pandas dataframe representing entity with fields, relationships, etc. + entity - dictionary of entity name to pandas dataframe representing entity title, etc. + coding - dictionary of coding name to pandas dataframe representing codes, their hierarchy (if applicable) and their meanings + """ + + def __init__(self, descriptor): + self.data_dictionary = self.load_data_dictionary(descriptor) + self.coding_dictionary = self.load_coding_dictionary(descriptor) + self.entity_dictionary = self.load_entity_dictionary(descriptor) + + def load_data_dictionary(self, descriptor): + """ + Processes data dictionary from descriptor + """ + eblocks = collections.OrderedDict() + join_path_to_entity_field = collections.OrderedDict() + for entity_name in descriptor.model["entities"]: + eblocks[entity_name] = self.create_entity_dframe( + descriptor.model["entities"][entity_name], + is_primary_entity=( + entity_name == descriptor.model["global_primary_key"]["entity"] + ), + global_primary_key=(descriptor.model["global_primary_key"]), + ) + + join_path_to_entity_field.update( + self.get_join_path_to_entity_field_map( + descriptor.model["entities"][entity_name] + ) + ) + + edges = [] + for ji in descriptor.join_info: + skip_edge = False + + for path in [ji["joins"][0]["to"], ji["joins"][0]["from"]]: + if path not in join_path_to_entity_field: + skip_edge = True + break + + if not skip_edge: + edges.append(self.create_edge(ji, join_path_to_entity_field)) + + for edge in edges: + source_eblock = eblocks.get(edge["source_entity"]) + if not source_eblock.empty: + eb_row_idx = source_eblock["name"] == edge["source_field"] + if eb_row_idx.sum() != 1: + raise ValueError("Invalid edge: " + str(edge)) + + ref = source_eblock["referenced_entity_field"].values + rel = source_eblock["relationship"].values + ref[eb_row_idx] = "{}:{}".format( + edge["destination_entity"], edge["destination_field"] + ) + rel[eb_row_idx] = edge["relationship"] + + source_eblock = source_eblock.assign( + relationship=rel, referenced_entity_field=ref + ) + + return eblocks + + def create_entity_dframe(self, entity, is_primary_entity, global_primary_key): + """ + Returns DataDictionary pandas DataFrame for an entity. + """ + required_columns = ["entity", "name", "type", "primary_key_type"] + + extra_cols = [ + "coding_name", + "concept", + "description", + "folder_path", + "is_multi_select", + "is_sparse_coding", + "linkout", + "longitudinal_axis_type", + "referenced_entity_field", + "relationship", + "title", + "units", + ] + dataset_datatype_dict = { + "integer": "integer", + "double": "float", + "date": "date", + "datetime": "datetime", + "string": "string", + } + dcols = {col: [] for col in required_columns + extra_cols} + dcols["entity"] = [entity["name"]] * len(entity["fields"]) + dcols["referenced_entity_field"] = [""] * len(entity["fields"]) + dcols["relationship"] = [""] * len(entity["fields"]) + + for field in entity["fields"]: + # Field-level parameters + field_dict = entity["fields"][field] + dcols["name"].append(field_dict["name"]) + dcols["type"].append(dataset_datatype_dict[field_dict["type"]]) + dcols["primary_key_type"].append( + ("global" if is_primary_entity else "local") + if ( + entity["primary_key"] + and field_dict["name"] == entity["primary_key"] + ) + else "" + ) + # Optional cols to be filled in with blanks regardless + dcols["coding_name"].append( + field_dict["coding_name"] if field_dict["coding_name"] else "" + ) + dcols["concept"].append(field_dict["concept"]) + dcols["description"].append(field_dict["description"]) + dcols["folder_path"].append( + " > ".join(field_dict["folder_path"]) + if ("folder_path" in field_dict.keys() and field_dict["folder_path"]) + else "" + ) + dcols["is_multi_select"].append( + "yes" if field_dict["is_multi_select"] else "" + ) + dcols["is_sparse_coding"].append( + "yes" if field_dict["is_sparse_coding"] else "" + ) + dcols["linkout"].append(field_dict["linkout"]) + dcols["longitudinal_axis_type"].append( + field_dict["longitudinal_axis_type"] + if field_dict["longitudinal_axis_type"] + else "" + ) + dcols["title"].append(field_dict["title"]) + dcols["units"].append(field_dict["units"]) + + try: + dframe = pd.DataFrame(dcols) + except ValueError as exc: + raise exc + + return dframe + + def get_join_path_to_entity_field_map(self, entity): + """ + Returns map with "database$table$column", "unique_database$table$column", + as keys and values are (entity, field) + """ + join_path_to_entity_field = collections.OrderedDict() + for field in entity["fields"]: + field_value = entity["fields"][field]["mapping"] + db_tb_col_path = "{}${}${}".format( + field_value["database_name"], + field_value["table"], + field_value["column"], + ) + join_path_to_entity_field[db_tb_col_path] = (entity["name"], field) + + if field_value["database_unique_name"] and database_unique_name_regex.match( + field_value["database_unique_name"] + ): + unique_db_tb_col_path = "{}${}${}".format( + field_value["database_unique_name"], + field_value["table"], + field_value["column"], + ) + join_path_to_entity_field[unique_db_tb_col_path] = ( + entity["name"], + field, + ) + elif ( + field_value["database_name"] + and field_value["database_id"] + and database_id_regex.match(field_value["database_name"]) + ): + unique_db_name = "{}__{}".format( + field_value["database_id"].replace("-", "_").lower(), + field_value["database_name"], + ) + join_path_to_entity_field[unique_db_name] = (entity["name"], field) + return join_path_to_entity_field + + def create_edge(self, join_info_joins, join_path_to_entity_field): + """ + Convert an item join_info to an edge. Returns ordereddict. + """ + edge = collections.OrderedDict() + column_to = join_info_joins["joins"][0]["to"] + column_from = join_info_joins["joins"][0]["from"] + edge["source_entity"], edge["source_field"] = join_path_to_entity_field[ + column_to + ] + ( + edge["destination_entity"], + edge["destination_field"], + ) = join_path_to_entity_field[column_from] + edge["relationship"] = join_info_joins["relationship"] + return edge + + def load_coding_dictionary(self, descriptor): + """ + Processes coding dictionary from descriptor + """ + cblocks = collections.OrderedDict() + for entity in descriptor.model["entities"]: + for field in descriptor.model["entities"][entity]["fields"]: + coding_name_value = descriptor.model["entities"][entity]["fields"][ + field + ]["coding_name"] + if coding_name_value and coding_name_value not in cblocks: + cblocks[coding_name_value] = self.create_coding_name_dframe( + descriptor.model, entity, field, coding_name_value + ) + return cblocks + + def create_coding_name_dframe(self, model, entity, field, coding_name_value): + """ + Returns CodingDictionary pandas DataFrame for a coding_name. + """ + dcols = {} + if model["entities"][entity]["fields"][field]["is_hierarchical"]: + displ_ord = 0 + + def unpack_hierarchy(nodes, parent_code, displ_ord): + """Serialize the node hierarchy by depth-first traversal. + + Yields: tuples of (code, parent_code) + """ + for node in nodes: + if isinstance(node, dict): + next_parent_code, child_nodes = next(iter(node.items())) + # internal: unpack recursively + displ_ord += 1 + yield next_parent_code, parent_code, displ_ord + for deep_node, deep_parent, displ_ord in unpack_hierarchy( + child_nodes, next_parent_code, displ_ord + ): + yield (deep_node, deep_parent, displ_ord) + else: + # terminal: serialize + displ_ord += 1 + yield (node, parent_code, displ_ord) + + all_codes, parents, displ_ord = zip( + *unpack_hierarchy( + model["codings"][coding_name_value]["display"], "", displ_ord + ) + ) + dcols.update( + { + "code": all_codes, + "parent_code": parents, + "meaning": [ + model["codings"][coding_name_value]["codes_to_meanings"][c] + for c in all_codes + ], + "concept": [ + model["codings"][coding_name_value]["codes_to_concepts"][c] + if ( + model["codings"][coding_name_value]["codes_to_concepts"] + and c + in model["codings"][coding_name_value][ + "codes_to_concepts" + ].keys() + ) + else None + for c in all_codes + ], + "display_order": displ_ord, + } + ) + + else: + # No hierarchy; just unpack the codes dictionary + codes, meanings = zip( + *model["codings"][coding_name_value]["codes_to_meanings"].items() + ) + if model["codings"][coding_name_value]["codes_to_concepts"]: + codes, concepts = zip( + *model["codings"][coding_name_value]["codes_to_concepts"].items() + ) + else: + concepts = [None] * len(codes) + display_order = [ + int(model["codings"][coding_name_value]["display"].index(c) + 1) + for c in codes + ] + dcols.update( + { + "code": codes, + "meaning": meanings, + "concept": concepts, + "display_order": display_order, + } + ) + + dcols["coding_name"] = [coding_name_value] * len(dcols["code"]) + + try: + dframe = pd.DataFrame(dcols) + except ValueError as exc: + raise exc + + return dframe + + def load_entity_dictionary(self, descriptor): + """ + Processes entity dictionary from descriptor + """ + entity_dictionary = collections.OrderedDict() + for entity_name in descriptor.model["entities"]: + entity = descriptor.model["entities"][entity_name] + entity_dictionary[entity_name] = pd.DataFrame.from_dict( + [ + { + "entity": entity_name, + "entity_title": entity.get("entity_title"), + "entity_label_singular": entity.get("entity_label_singular"), + "entity_label_plural": entity.get("entity_label_plural"), + "entity_description": entity.get("entity_description"), + } + ] + ) + return entity_dictionary + + def write( + self, output_file_data="", output_file_entity="", output_file_coding="", sep="," + ): + """ + Create CSV files with the contents of the dictionaries. + """ + csv_opts = dict( + sep=sep, + header=True, + index=False, + na_rep="", + ) + + def sort_dataframe_columns(dframe, required_columns): + """Sort dataframe columns alphabetically but with `required_columns` first.""" + extra_cols = dframe.columns.difference(required_columns) + sorted_cols = list(required_columns) + extra_cols.sort_values().tolist() + return dframe.loc[:, sorted_cols] + + def as_dataframe(ord_dict_of_df, required_columns): + """Join all blocks into a pandas DataFrame.""" + df = pd.concat([b for b in ord_dict_of_df.values()], sort=False) + return sort_dataframe_columns(df, required_columns) + + if self.data_dictionary: + data_dframe = as_dataframe( + self.data_dictionary, + required_columns=["entity", "name", "type", "primary_key_type"], + ) + data_dframe.to_csv(output_file_data, **csv_opts) + + if self.coding_dictionary: + coding_dframe = as_dataframe( + self.coding_dictionary, + required_columns=["coding_name", "code", "meaning"], + ) + coding_dframe.to_csv(output_file_coding, **csv_opts) + + if self.entity_dictionary: + entity_dframe = as_dataframe( + self.entity_dictionary, required_columns=["entity", "entity_title"] + ) + entity_dframe.to_csv(output_file_entity, **csv_opts) diff --git a/src/python/dxpy/cli/download.py b/src/python/dxpy/cli/download.py index 3dcd8b0ec7..24302ce38c 100644 --- a/src/python/dxpy/cli/download.py +++ b/src/python/dxpy/cli/download.py @@ -23,8 +23,6 @@ import os import subprocess import sys -import tempfile -import warnings import logging import dxpy @@ -87,13 +85,16 @@ def download_one_file(project, file_desc, dest_filename, args): except AttributeError: show_progress = False + try: + symlink_max_tries = args.symlink_max_tries if vars(args).get('symlink_max_tries') is not None else 15 dxpy.download_dxfile( file_desc['id'], dest_filename, show_progress=show_progress, project=project, - describe_output=file_desc) + describe_output=file_desc, + symlink_max_tries=symlink_max_tries) return except: err_exit() diff --git a/src/python/dxpy/cli/exec_io.py b/src/python/dxpy/cli/exec_io.py index a4b47abc0d..e1d927f016 100644 --- a/src/python/dxpy/cli/exec_io.py +++ b/src/python/dxpy/cli/exec_io.py @@ -22,7 +22,7 @@ # TODO: refactor all dx run helper functions here -import os, sys, json, collections, pipes +import os, sys, json, collections, shlex from ..bindings.dxworkflow import DXWorkflow import dxpy @@ -35,8 +35,9 @@ from ..utils import OrderedDefaultdict from ..compat import input, str, shlex, basestring, USING_PYTHON2 try: + # Import gnureadline if installed for macOS import gnureadline as readline -except ImportError: +except ImportError as e: import readline #################### # -i Input Parsing # @@ -326,7 +327,7 @@ def format_data_object_reference(item): # TODO: in interactive prompts the quotes here may be a bit # misleading. Perhaps it should be a separate mode to print # "interactive-ready" suggestions. - return fill(header + ' ' + ', '.join([pipes.quote(str(item)) for item in items]), + return fill(header + ' ' + ', '.join([shlex.quote(str(item)) for item in items]), initial_indent=initial_indent, subsequent_indent=subsequent_indent) @@ -675,8 +676,7 @@ def add(self, input_name, input_value): def init_completer(self): try: - import rlcompleter - readline.parse_and_bind("tab: complete") + readline.parse_and_bind("bind ^I rl_complete" if "libedit" in (readline.__doc__ or "") else "tab: complete") readline.set_completer_delims("") diff --git a/src/python/dxpy/cli/help_messages.py b/src/python/dxpy/cli/help_messages.py new file mode 100644 index 0000000000..a2aabbaf62 --- /dev/null +++ b/src/python/dxpy/cli/help_messages.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- + +from ..utils.printing import fill + + +EXTRACT_ASSAY_EXPRESSION_JSON_TEMPLATE = """ +{ + "location": [ + { + "chromosome": "1", + "starting_position": "10000", + "ending_position": "20000" + }, + { + "chromosome": "X", + "starting_position": "500", + "ending_position": "1700" + } + ], + "expression": { + "min_value": 10.2, + "max_value": 10000 + }, + "annotation": { + "feature_name": ["BRCA2"], + "feature_id": ["ENSG0000001", "ENSG00000002"] + }, + "sample_id": ["Sample1", "Sample2"] +} +""" + +EXTRACT_ASSAY_EXPRESSION_JSON_HELP = ( + fill( + "# Additional descriptions of filtering keys and permissible values", + ) + + EXTRACT_ASSAY_EXPRESSION_JSON_TEMPLATE +) + +EXTRACT_ASSAY_EXPRESSION_ADDITIONAL_FIELDS_HELP = """ +The following fields will always be returned by default: + + NAME TITLE DESCRIPTION + sample_id Sample ID A unique identifier for the sample +feature_id Feature ID An unique identification of the feature + value Expression Value Expression value for the sample ID of the respective feature ID + + +The following fields may be added to the output by using option --additional-fields. +If multiple fields are specified, use a comma to separate each entry. For example, “chrom,feature_name” + + + NAME TITLE DESCRIPTION +feature_name Feature Name Name of the feature + chrom Chromosome Chromosome of the feature + start Start Position Genomic starting position of the feature + end End Position Genomic ending position of the feature + strand Strand Orientation of the feature with respect to forward or reverse strand + +""" \ No newline at end of file diff --git a/src/python/dxpy/cli/org.py b/src/python/dxpy/cli/org.py index 285b5a6428..32912470de 100644 --- a/src/python/dxpy/cli/org.py +++ b/src/python/dxpy/cli/org.py @@ -146,12 +146,12 @@ def _get_org_set_member_access_args(args, current_level): admin_to_member = args.level == "MEMBER" and current_level == "ADMIN" if args.allow_billable_activities is not None: - org_set_member_access_input[user_id]["allowBillableActivities"] = (True if args.allow_billable_activities == "true" else False) + org_set_member_access_input[user_id]["allowBillableActivities"] = args.allow_billable_activities == "true" elif admin_to_member: org_set_member_access_input[user_id]["allowBillableActivities"] = False if args.app_access is not None: - org_set_member_access_input[user_id]["appAccess"] = (True if args.app_access == "true" else False) + org_set_member_access_input[user_id]["appAccess"] = args.app_access == "true" elif admin_to_member: org_set_member_access_input[user_id]["appAccess"] = True @@ -242,7 +242,7 @@ def _get_org_update_args(args): if args.saml_idp is not None: org_update_inputs["samlIdP"] = args.saml_idp - if any(policy not in (None, False) for policy in (args.member_list_visibility, args.project_transfer_ability, args.enable_job_reuse, args.disable_job_reuse)): + if any(policy not in (None, False) for policy in (args.member_list_visibility, args.project_transfer_ability, args.enable_job_reuse, args.disable_job_reuse)) or args.detailed_job_metrics_collect_default is not None: org_update_inputs["policies"] = {} if args.member_list_visibility is not None: org_update_inputs["policies"]["memberListVisibility"] = args.member_list_visibility @@ -252,7 +252,13 @@ def _get_org_update_args(args): org_update_inputs["policies"]["jobReuse"] = True elif args.disable_job_reuse == True: org_update_inputs["policies"]["jobReuse"] = False - + if args.detailed_job_metrics_collect_default is not None: + org_update_inputs["policies"]["detailedJobMetricsCollectDefault"] = args.detailed_job_metrics_collect_default == 'true' + if args.job_logs_forwarding_json is not None: + try: + org_update_inputs["jobLogsForwarding"] = json.loads(args.job_logs_forwarding_json) + except: + err_exit("Invalid JSON input for --job-logs-forwarding-json") return org_update_inputs diff --git a/src/python/dxpy/cli/output_handling.py b/src/python/dxpy/cli/output_handling.py new file mode 100644 index 0000000000..8c06761170 --- /dev/null +++ b/src/python/dxpy/cli/output_handling.py @@ -0,0 +1,182 @@ +from __future__ import annotations +import sys +import csv +import os +import json +from ..exceptions import err_exit + + +def write_expression_output( + arg_output, + arg_delim, + arg_sql, + output_listdict_or_string, + save_uncommon_delim_to_txt=True, + output_file_name=None, + error_handler=err_exit, + colnames=None, +): + """ + arg_output: str + A string representing the output file path. + When it's "-", output is written to stdout. + This can be directly set as args.output from argparse when calling the function (e.g within dataset_utilities) + + arg_delim: str + A string representing the delimiter. Defaults to "," when not specified. + It also determines the file suffix when writing to file. + This can be set to args.delimiter from argparse when the method is called + + arg_sql: bool + A boolean representing whether the output_listdict_or_string is a SQL query (string) or not. + This can be args.sql from argparse + + output_listdict_or_string: 'list of dicts' or 'str' depending on whether arg_sql is False or True, respectively + This is expected to be the response from vizserver + if arg_sql is True, this is expected to be a string representing the SQL query + if arg_sql is False, this is expected to be a list of dicts representing the output of a SQL query + if output_listdict_or_string is a list of dicts, all dicts must have the same keys which will be used as column names + + save_uncommon_delim_to_txt: bool + Set this to False if you want to error out when any delimiter other than "," or "\t" is specified + + output_file_name: str + This is expected to be a record_name which will be used when arg_output is not specified + Do not append a suffix to this string + output_file_name is mandatory when arg_output is not specified + + By default delimiter is set "," and file suffix is csv (when writing to file) + + None values are written as empty strings by default (csv.DictWriter behavior) + + """ + + IS_OS_WINDOWS = os.name == "nt" + OS_SPECIFIC_LINE_SEPARATOR = os.linesep + IS_PYTHON_2 = sys.version_info.major == 2 + IS_PYTHON_3 = sys.version_info.major == 3 + + if arg_sql: + SUFFIX = ".sql" + if not isinstance(output_listdict_or_string, str): + error_handler("Expected SQL query to be a string") + elif arg_delim: + if arg_delim == ",": + SUFFIX = ".csv" + elif arg_delim == "\t": + SUFFIX = ".tsv" + else: + if save_uncommon_delim_to_txt: + SUFFIX = ".txt" + else: + error_handler("Unsupported delimiter: {}".format(arg_delim)) + else: + SUFFIX = ".csv" + + if arg_output: + if arg_output == "-": + WRITE_METHOD = "STDOUT" + else: + WRITE_METHOD = "FILE" + output_file_name = arg_output + + else: + OUTPUT_DIR = os.getcwd() + + if output_file_name is None: + error_handler( + "No output filename specified" + ) # Developer expected to provide record_name upstream when calling this function + + else: + WRITE_METHOD = "FILE" + output_file_name = os.path.join(OUTPUT_DIR, output_file_name + SUFFIX) + + if WRITE_METHOD == "FILE": + # error out if file already exists or output_file_name is a directory + if os.path.exists(output_file_name): + if os.path.isfile(output_file_name): + error_handler( + "{} already exists. Please specify a new file path".format( + output_file_name + ) + ) + if os.path.isdir(output_file_name): + error_handler( + "{} is a directory. Please specify a new file path".format( + output_file_name + ) + ) + + if arg_sql: + if WRITE_METHOD == "STDOUT": + print(output_listdict_or_string) + elif WRITE_METHOD == "FILE": + with open(output_file_name, "w") as f: + f.write(output_listdict_or_string) + else: + error_handler("Unexpected error occurred while writing SQL query output") + + else: + if colnames: + COLUMN_NAMES = colnames + else: + COLUMN_NAMES = output_listdict_or_string[0].keys() + + if not all( + set(i.keys()) == set(COLUMN_NAMES) for i in output_listdict_or_string + ): + error_handler("All rows must have the same column names") + + WRITE_MODE = "wb" if IS_PYTHON_2 or IS_OS_WINDOWS else "w" + NEWLINE = "" if IS_PYTHON_3 else None + DELIMITER = str(arg_delim) if arg_delim else "," + QUOTING = csv.QUOTE_MINIMAL + QUOTE_CHAR = '"' + + write_args = { + "mode": WRITE_MODE, + } + + if IS_PYTHON_3: + write_args["newline"] = NEWLINE + + dictwriter_params = { + "fieldnames": COLUMN_NAMES, + "delimiter": DELIMITER, + "lineterminator": OS_SPECIFIC_LINE_SEPARATOR, + "quoting": QUOTING, + "quotechar": QUOTE_CHAR, + } + + if WRITE_METHOD == "FILE": + with open(output_file_name, **write_args) as f: + w = csv.DictWriter(f, **dictwriter_params) + w.writeheader() + w.writerows(output_listdict_or_string) + + elif WRITE_METHOD == "STDOUT": + w = csv.DictWriter(sys.stdout, **dictwriter_params) + w.writeheader() + w.writerows(output_listdict_or_string) + + else: + error_handler("Unexpected error occurred while writing output") + + + +def pretty_print_json(json_dict: dict) -> str: + """Pretty-prints the provided JSON object. + + Args: + json_dict: A string containing valid JSON data. + + Returns: + Returns a string with formatted JSON or None if there's an error. + """ + if isinstance(json_dict, dict): + formatted_json = json.dumps(json_dict, sort_keys=True, indent=4) + return formatted_json + else: + print("WARNING: Invalid JSON provided.", file=sys.stderr) + return None diff --git a/src/python/dxpy/cli/parsers.py b/src/python/dxpy/cli/parsers.py index 71fb3b689d..59c218647e 100644 --- a/src/python/dxpy/cli/parsers.py +++ b/src/python/dxpy/cli/parsers.py @@ -56,6 +56,10 @@ def __str__(self): json_arg = argparse.ArgumentParser(add_help=False) json_arg.add_argument('--json', help='Display return value in JSON', action='store_true') +try_arg = argparse.ArgumentParser(add_help=False) +try_arg.add_argument('--try', metavar="T", dest="job_try", type=int, + help=fill('When modifying a job that was restarted, apply the change to try T of the restarted job. T=0 refers to the first try. Default is the last job try.', width_adjustment=-24)) + stdout_args = argparse.ArgumentParser(add_help=False) stdout_args_gp = stdout_args.add_mutually_exclusive_group() stdout_args_gp.add_argument('--brief', help=fill('Display a brief version of the return value; for most commands, prints a DNAnexus ID per line', width_adjustment=-24), action='store_true') @@ -94,12 +98,13 @@ def __str__(self): find_executions_args.add_argument('--state', help=fill('State of the job, e.g. \"done\", \"failed\"', width_adjustment=-24)) find_executions_args.add_argument('--origin', help=fill('Job ID of the top-level job', width_adjustment=-24)) # Redundant but might as well find_executions_args.add_argument('--parent', help=fill('Job ID of the parent job; implies --all-jobs', width_adjustment=-24)) -find_executions_args.add_argument('--created-after', help=fill('Date (e.g. 2012-01-01) or integer timestamp after which the job was last created (negative number means ms in the past, or use suffix s, m, h, d, w, M, y)', width_adjustment=-24)) -find_executions_args.add_argument('--created-before', help=fill('Date (e.g. 2012-01-01) or integer timestamp before which the job was last created (negative number means ms in the past, or use suffix s, m, h, d, w, M, y)', width_adjustment=-24)) +find_executions_args.add_argument('--created-after', help=fill('Date (e.g. --created-after="2021-12-01" or --created-after="2021-12-01 19:01:33") or integer Unix epoch timestamp in milliseconds (e.g. --created-after=1642196636000) after which the job was last created. You can also specify negative numbers to indicate a time period in the past suffixed by s, m, h, d, w, M or y to indicate seconds, minutes, hours, days, weeks, months or years (e.g. --created-after=-2d for executions created in the last 2 days)', width_adjustment=-24)) +find_executions_args.add_argument('--created-before', help=fill('Date (e.g. --created-before="2021-12-01" or --created-before="2021-12-01 19:01:33") or integer Unix epoch timestamp in milliseconds (e.g. --created-before=1642196636000) before which the job was last created. You can also specify negative numbers to indicate a time period in the past suffixed by s, m, h, d, w, M or y to indicate seconds, minutes, hours, days, weeks, months or years (e.g. --created-before=-2d for executions created earlier than 2 days ago)', width_adjustment=-24)) find_executions_args.add_argument('--no-subjobs', help=fill('Do not show any subjobs', width_adjustment=-24), action='store_true') find_executions_args.add_argument('--root-execution', '--root', help=fill('Execution ID of the top-level (user-initiated) job or analysis', width_adjustment=-24)) find_executions_args.add_argument('-n', '--num-results', metavar='N', type=int, help=fill('Max number of results (trees or jobs, as according to the search mode) to return (default 10)', width_adjustment=-24), default=10) find_executions_args.add_argument('-o', '--show-outputs', help=fill('Show job outputs in results', width_adjustment=-24), action='store_true') +find_executions_args.add_argument('--include-restarted', help=fill('if specified, results will include restarted jobs and job trees rooted in restarted jobs', width_adjustment=-24), action='store_true') def add_find_executions_search_gp(parser): find_executions_search_gp = parser.add_argument_group('Search mode') @@ -144,7 +149,7 @@ def process_dataobject_args(args): process_properties_args(args) # Visibility - args.hidden = (args.hidden == 'hidden') + args.hidden = (args.hidden == 'hidden' or args.hidden is True) # Details if args.details is not None: @@ -226,23 +231,85 @@ class PrintInstanceTypeHelp(argparse.Action): def __call__(self, parser, namespace, values, option_string=None): print("Help: Specifying instance types for " + parser.prog) print() + print(fill('Instance types can be requested with --instance-type-by-executable and ' + + '--instance-type arguments, with --instance-type-by-executable specification ' + + 'taking priority over --instance-type, workflow\'s stageSystemRequirements, ' + + 'and specifications provided during app and applet creation.')) + print() + print(fill('--instance-type specifications do not propagate to subjobs and sub-analyses ' + + 'launched from a job with a /executable-xxxx/run call, but --instance-type-by-executable do ' + + '(where executable refers to an app, applet or workflow).')) + print() + print(fill('When running an app or an applet, --instance-type lets you specify the ' + + 'instance type to be used by each entry point.')) print(fill('A single instance type can be requested to be used by all entry points by providing the instance type name. Different instance types can also be requested for different entry points of an app or applet by providing a JSON string mapping from function names to instance types, e.g.')) print() print(' {"main": "mem2_hdd2_v2_x2", "other_function": "mem1_ssd1_v2_x2"}') if parser.prog == 'dx run': print() - print(fill('If running a workflow, different stages can have different instance type ' + - 'requests by prepending the request with "=" (where a ' + - 'stage identifier is an ID, a numeric index, or a unique stage name) and ' + + print(fill('When running a workflow, --instance-type lets you specify instance types for ' + + 'each entry point of each workflow stage by prepending the request with "=" ' + + '(where a stage identifier is an ID, a numeric index, or a unique stage name) and ' + 'repeating the argument for as many stages as desired. If no stage ' + 'identifier is provided, the value is applied as a default for all stages.')) print() - print(fill('The following example runs all entry points of the first stage with ' + - 'mem2_hdd2_v2_x2, the stage named "BWA" with mem1_ssd1_v2_x2, and all other ' + + print('Examples') + print() + print(fill('1. Run the main entry point of applet-xxxx on mem1_ssd1_v2_x2, and ' + 'all other entry points on mem1_ssd1_v2_x4')) + print(' dx run applet-xxxx --instance-type \'{"main": "mem1_ssd1_v2_x2",\n' + + ' "*": "mem1_ssd1_v2_x4"}\'') + print() + print(fill('2. Runs all entry points of the first stage with ' + + 'mem2_hdd2_v2_x2, the main entry point of the second stage with mem1_ssd1_v2_x4, ' + + 'the stage named "BWA" with mem1_ssd1_v2_x2, and all other ' + 'stages with mem2_hdd2_v2_x4')) print() - print(' Example: dx run workflow --instance-type 0=mem2_hdd2_v2_x2 \\') - print(' --instance-type BWA=mem1_ssd1_v2_x2 --instance-type mem2_hdd2_v2_x4') + print(' dx run workflow-xxxx \\\n' + + ' --instance-type 0=mem2_hdd2_v2_x2 \\\n' + + ' --instance-type 1=\'{"main": "mem1_ssd1_v2_x4"}\' \\\n' + + ' --instance-type BWA=mem1_ssd1_v2_x2 \\\n' + + ' --instance-type mem2_hdd2_v2_x4') + print() + print(fill('--instance-type-by-executable argument is a JSON string with a double mapping that ' + + 'specifies instance types by app or applet id, then by entry point within the executable.' + + 'This specification applies across the entire nested execution tree and is propagated ' + + 'across /executable-xxxx/run calls issued with the execution tree.')) + print() + print('More examples') + print(fill('3. Force every job in the execution tree to use mem2_ssd1_v2_x2')) + print() + print( + ' dx run workflow-xxxx --instance-type-by-executable \'{"*": {"*": "mem2_ssd1_v2_x2"}}\'') + print() + print(fill( + '4. Force every job in the execution tree executing applet-xyz1 to use mem2_ssd1_v2_x2')) + print() + print( + ' dx run workflow-xxxx --instance-type-by-executable \'{"applet-xyz1":{"*": "mem2_ssd1_v2_x2"}}\'') + print() + print(fill('5. Force every job executing applet-xyz1 to use mem2_ssd1_v2_x4 ' + + 'for the main entry point and mem2_ssd1_v2_x2 for all other entry points.' + + 'Also force the collect entry point of all executables other than applet-xyz1 to use mem2_ssd1_v2_x8.' + + 'Other entry points of executable other than applet-xyz1 may be overridden by ' + + 'lower-priority mechanisms')) + print() + print(' dx run workflow-xxxx --instance-type-by-executable \\\n' + + ' \'{"applet-xyz1": {"main": "mem2_ssd1_v2_x4", "*": "mem2_ssd1_v2_x2"},\n' + + ' "*": {"collect": "mem2_ssd1_v2_x8"}}\'') + print() + print(fill('6. Force every job executing applet-xxxx to use mem2_ssd1_v2_x2 for all entry points ' + + 'in the entire execution tree. ' + + 'Also force stage 0 executable to run on mem2_ssd1_v2_x4, unless stage 0 invokes ' + + 'applet-xxxx, in which case applet-xxxx\'s jobs will use mem2_ssd1_v2_x2 as specified by ' + + '--instance-type-by-executable.')) + print() + print(' dx run workflow-xxxx \\\n' + + ' --instance-type-by-executable \'{"applet-xxxx": {"*": "mem2_ssd1_v2_x2"}}\' \\\n' + + ' --instance-type 0=mem2_ssd1_v2_x4') + print() + print(fill( + 'See "Requesting Instance Types" in DNAnexus documentation for more details.')) print() print('Available instance types:') print() @@ -253,8 +320,25 @@ def __call__(self, parser, namespace, values, option_string=None): instance_type_arg = argparse.ArgumentParser(add_help=False) instance_type_arg.add_argument('--instance-type', metavar='INSTANCE_TYPE_OR_MAPPING', - help=fill('Specify instance type(s) for jobs this executable will run; see --instance-type-help for more details', width_adjustment=-24), + help=fill('''When running an app or applet, the mapping lists executable's entry points or "*" as keys, and instance types to use for these entry points as values. +When running a workflow, the specified instance types can be prefixed by a stage name or stage index followed by "=" to apply to a specific stage, or apply to all workflow stages without such prefix. +The instance type corresponding to the "*" key is applied to all entry points not explicitly mentioned in the --instance-type mapping. Specifying a single instance type is equivalent to using it for all entry points, so "--instance-type mem1_ssd1_v2_x2" is same as "--instance-type '{"*":"mem1_ssd1_v2_x2"}'. +Note that "dx run" calls within the execution subtree may override the values specified at the root of the execution tree. +See dx run --instance-type-help for details. +''', width_adjustment=-24, replace_whitespace=False), action='append').completer = InstanceTypesCompleter() + +instance_type_arg.add_argument('--instance-type-by-executable', + metavar='DOUBLE_MAPPING', + help=fill( + '''Specifies instance types by app or applet id, then by entry point within the executable. +The order of priority for this specification is: + * --instance-type, systemRequirements and stageSystemRequirements specified at runtime + * stage's systemRequirements, systemRequirements supplied to /app/new and /applet/new at workflow/app/applet build time + * systemRequirementsByExecutable specified in downstream executions (if any) +See dx run --instance-type-help for details. +''', width_adjustment=-24, replace_whitespace=False)) + instance_type_arg.add_argument('--instance-type-help', nargs=0, help=fill('Print help for specifying instance types'), @@ -317,6 +401,17 @@ def process_instance_type_arg(args, for_workflow=False): # is a string args.instance_type = _parse_inst_type(args.instance_type) +def process_instance_type_by_executable_arg(args): + if args.instance_type_by_executable: + if args.instance_type_by_executable.strip().startswith('{'): + # expects a map, e.g of entry point to instance type or instance count + try: + args.instance_type_by_executable = json.loads(args.instance_type_by_executable) + except ValueError: + raise DXParserError('Error while parsing JSON value for --instance-type-by-executable') + else: + raise DXParserError('Value given for --instance-type-by-executable could not be parsed as JSON') + def process_instance_count_arg(args): if args.instance_count: # If --instance-count was used multiple times, the last one @@ -336,27 +431,36 @@ def get_update_project_args(args): if args.description is not None: input_params["description"] = args.description if args.protected is not None: - input_params["protected"] = True if args.protected == 'true' else False + input_params["protected"] = args.protected == 'true' if args.restricted is not None: - input_params["restricted"] = True if args.restricted == 'true' else False + input_params["restricted"] = args.restricted == 'true' if args.download_restricted is not None: - input_params["downloadRestricted"] = True if args.download_restricted == 'true' else False + input_params["downloadRestricted"] = args.download_restricted == 'true' if args.containsPHI is not None: - input_params["containsPHI"] = True if args.containsPHI == 'true' else False + input_params["containsPHI"] = args.containsPHI == 'true' if args.database_ui_view_only is not None: - input_params["databaseUIViewOnly"] = True if args.database_ui_view_only == 'true' else False + input_params["databaseUIViewOnly"] = args.database_ui_view_only == 'true' if args.bill_to is not None: input_params["billTo"] = args.bill_to if args.allowed_executables is not None: input_params['allowedExecutables'] = args.allowed_executables if args.unset_allowed_executables: input_params['allowedExecutables'] = None + if args.database_results_restricted is not None: + input_params['databaseResultsRestricted'] = args.database_results_restricted + if args.unset_database_results_restricted: + input_params['databaseResultsRestricted'] = None + if args.external_upload_restricted is not None: + input_params['externalUploadRestricted'] = args.external_upload_restricted == 'true' return input_params - def process_phi_param(args): if args.containsPHI is not None: if args.containsPHI == "true": args.containsPHI = True elif args.containsPHI == "false": args.containsPHI = False + +def process_external_upload_restricted_param(args): + if args.external_upload_restricted is not None: + args.external_upload_restricted = (args.external_upload_restricted == "true") diff --git a/src/python/dxpy/cli/workflow.py b/src/python/dxpy/cli/workflow.py index 8c0bf51b49..167f9eec3a 100644 --- a/src/python/dxpy/cli/workflow.py +++ b/src/python/dxpy/cli/workflow.py @@ -111,7 +111,7 @@ def add_stage(args): elif args.relative_output_folder is not None: folderpath = args.relative_output_folder - # process instance type + # process instance type only; instance type by executable is not applicable for add stage try_call(process_instance_type_arg, args) dxworkflow = dxpy.DXWorkflow(workflow_id, project=project) @@ -207,7 +207,7 @@ def update_stage(args): workflow_id, project = get_workflow_id_and_project(args.workflow) dxworkflow = dxpy.DXWorkflow(workflow_id, project=project) - # process instance type + # process instance type only; instance type by executable is not applicable for update stage try_call(process_instance_type_arg, args) initial_edit_version = dxworkflow.editVersion diff --git a/src/python/dxpy/compat.py b/src/python/dxpy/compat.py index 8f30973c3e..63c0f32a8a 100644 --- a/src/python/dxpy/compat.py +++ b/src/python/dxpy/compat.py @@ -16,7 +16,7 @@ from __future__ import print_function, unicode_literals, division, absolute_import -import os, sys, io, locale, threading +import os, sys, io, locale, threading, hashlib from io import TextIOWrapper from contextlib import contextmanager try: @@ -38,6 +38,7 @@ from cStringIO import StringIO from httplib import BadStatusLine from repr import Repr + from collections import Mapping BytesIO = StringIO builtin_str = str bytes = str @@ -90,6 +91,7 @@ def expanduser(path): from http.client import BadStatusLine from reprlib import Repr import shlex + from collections.abc import Mapping builtin_str = str str = str bytes = bytes @@ -220,3 +222,11 @@ def unwrap_stream(stream_name): finally: if wrapped_stream: setattr(sys, stream_name, wrapped_stream) + +# Support FIPS enabled Python +def md5_hasher(): + try: + md5_hasher = hashlib.new('md5', usedforsecurity=False) + except: + md5_hasher = hashlib.new('md5') + return md5_hasher diff --git a/src/python/dxpy/dx_extract_utils/Homo_sapiens_genes_manifest.json b/src/python/dxpy/dx_extract_utils/Homo_sapiens_genes_manifest.json new file mode 100644 index 0000000000..e303572065 --- /dev/null +++ b/src/python/dxpy/dx_extract_utils/Homo_sapiens_genes_manifest.json @@ -0,0 +1,20 @@ +{ + "GRCh37.87": + { + "aws:us-east-1" : "file-FpkYyk80vF51g4GY1g4K2xfb", + "aws:eu-west-2" : "file-Fx6pb30JjG8z3XzFFbkKBQX5", + "azure:westus" : "file-G14Z1x89zxzxPFfZ3f1QpfJz", + "aws:eu-west-2-g" : "file-G3zqf2XK3qJVjPg63XZx6Y0V", + "aws:eu-central-1" : "file-G3zqbJ84kv96763j8y36q9pq", + "azure:uksouth-ofh": "file-GK78pF127Pxxf5J5B95FKP67" + }, + "GRCh38.92": + { + "aws:us-east-1" : "file-FpkZ0b80vF5KZBb386Z4j3gq", + "aws:eu-west-2" : "file-Fx6pb3jJjG8Yg6VpPJFf32y9", + "azure:westus" : "file-G14Z1yQ9zxzbP5qx3bgfBJQb", + "aws:eu-west-2-g" : "file-G3zqf3XK3qJYYv3F1pv9bYX6", + "aws:eu-central-1" : "file-G3zqbK84kv9BF8zB4Y526yVf", + "azure:uksouth-ofh": "file-GK78pK927Pxvqyy656gqB1bp" + } +} \ No newline at end of file diff --git a/src/python/dxpy/dx_extract_utils/Homo_sapiens_genes_manifest_staging.json b/src/python/dxpy/dx_extract_utils/Homo_sapiens_genes_manifest_staging.json new file mode 100644 index 0000000000..6a71469e5e --- /dev/null +++ b/src/python/dxpy/dx_extract_utils/Homo_sapiens_genes_manifest_staging.json @@ -0,0 +1,20 @@ +{ + "GRCh37.87": + { + "aws:us-east-1" : "file-FpkX6f80bgYxy306BJQ6gPV0", + "aws:eu-west-2" : "file-FvpvGp0JqpJy91kp3j20Kjy7", + "azure:westus" : "file-FpPZP5Q91zJPY8zF79Yff2Kv", + "aws:eu-west-2-g" : "file-G3jqzPpK338fjjbB1jffy3Gg", + "aws:eu-central-1" : "file-FvX8Ppj4zx991B411457FzXg", + "azure:uksouth-ofh": "file-GJVzzBk2pp2jJkZ9Fj27BGZq" + }, + "GRCh38.92": + { + "aws:us-east-1" : "file-FpkX9zj0bgYgxJz70QQ5gJxB", + "aws:eu-west-2" : "file-FvpvGp0JqpJZVz1j3fXQfkBx", + "azure:westus" : "file-FpPZKkj91zJ7qqpx7B53116K", + "aws:eu-west-2-g" : "file-G3jv1KpK338fK7jq1kqVfQQ6", + "aws:eu-central-1" : "file-FvX8PG04zx9JkbvgPJQf0YQ4", + "azure:uksouth-ofh": "file-GJVzzG12pp2ZZqKJFkFB7xBy" + } +} \ No newline at end of file diff --git a/src/python/dxpy/dx_extract_utils/Homo_sapiens_genes_manifest_staging_vep.json b/src/python/dxpy/dx_extract_utils/Homo_sapiens_genes_manifest_staging_vep.json new file mode 100644 index 0000000000..5fb5ed5517 --- /dev/null +++ b/src/python/dxpy/dx_extract_utils/Homo_sapiens_genes_manifest_staging_vep.json @@ -0,0 +1,29 @@ +{ + "GRCh38.109_symbol": + { + "aws:us-east-1" : "file-GYZVXP00bgYf09kyQj4jVVjX", + "aws:eu-west-2" : "file-GYZX0Z0JqpJXJJ92pV9Y8gF3", + "azure:westus" : "file-GYZVjP091zJ8kBVyq63f8JKy", + "aws:eu-west-2-g" : "file-GYZVzGXK338zBVvKbpFGZpz7", + "aws:eu-central-1" : "file-GYZVqQ84zx91k7p8GZgYkZBB", + "azure:uksouth-ofh": "file-GYZVbgV2pp2jkBVyq63f8JKv" + }, + "GRCh38.109_gene": + { + "aws:us-east-1" : "file-GYZVXP00bgYjkBVyq63f8JKp", + "aws:eu-west-2" : "file-GYZX0Z0JqpJkV7yp0BVqKZ0k", + "azure:westus" : "file-GYZVjP091zJK5BZ7Pb9V1kFy", + "aws:eu-west-2-g" : "file-GYZVzGXK338g6P594bBjbzZf", + "aws:eu-central-1" : "file-GYZVqQ84zx9PBVvKbpFGZpz2", + "azure:uksouth-ofh": "file-GYZVbgV2pp2y5BZ7Pb9V1kFp" + }, + "GRCh38.109_feature": + { + "aws:us-east-1" : "file-GYZVXP00bgYy5BZ7Pb9V1kFf", + "aws:eu-west-2" : "file-GYZX0Z0JqpJQXX3F5qbGxVjP", + "azure:westus" : "file-GYZVjP091zJ8g3qf0132zYBj", + "aws:eu-west-2-g" : "file-GYZVzGXK338k3fkVP8xk78vx", + "aws:eu-central-1" : "file-GYZVqQ84zx976P594bBjbzZZ", + "azure:uksouth-ofh": "file-GYZVbgV2pp2jg3qf0132zYBf" + } +} \ No newline at end of file diff --git a/src/python/dxpy/dx_extract_utils/Homo_sapiens_genes_manifest_vep.json b/src/python/dxpy/dx_extract_utils/Homo_sapiens_genes_manifest_vep.json new file mode 100644 index 0000000000..0ab4d26560 --- /dev/null +++ b/src/python/dxpy/dx_extract_utils/Homo_sapiens_genes_manifest_vep.json @@ -0,0 +1,29 @@ +{ + "GRCh38.109_symbol": + { + "aws:us-east-1" : "file-GYZVZZQ0vF54Xj6K8gY2p1fp", + "aws:eu-west-2" : "file-GYZX2PQJjG8p0x9749bKg9fB", + "azure:westus" : "file-GYZVkQQ9zxzfXv1BFyqb2Xj5", + "aws:eu-west-2-g" : "file-GYZVy6XK3qJz5QP498bJV1Y8", + "aws:eu-central-1" : "file-GYZVvX04kv93yV5PG68vX7Bg", + "azure:uksouth-ofh": "file-GYZVfp127PxZXj6K8gY2p1kX" + }, + "GRCh38.109_gene": + { + "aws:us-east-1" : "file-GYZVZZQ0vF5Bbky21PF7j53B", + "aws:eu-west-2" : "file-GYZX2PQJjG8VXx57F1jQ8z8F", + "azure:westus" : "file-GYZVkQQ9zxzYyV5PG68vX75v", + "aws:eu-west-2-g" : "file-GYZVy6XK3qJVF9G6xYqbxq7j", + "aws:eu-central-1" : "file-GYZVvX04kv932101g4jf24K8", + "azure:uksouth-ofh": "file-GYZVfp127Pxy39GVXFZZ1Yx2" + }, + "GRCh38.109_feature": + { + "aws:us-east-1" : "file-GYZVZZQ0vF5K39GVXFZZ1YZ9", + "aws:eu-west-2" : "file-GYZX2PQJjG8z5QP498bJV1qj", + "azure:westus" : "file-GYZVkQQ9zxzY2101g4jf244Z", + "aws:eu-west-2-g" : "file-GYZVy6XK3qJZbJ45y0JFB4B9", + "aws:eu-central-1" : "file-GYZVvX04kv9Bbky21PF7j5f0", + "azure:uksouth-ofh": "file-GYZVfp127PxQQk86BV1y7QXP" + } +} \ No newline at end of file diff --git a/src/python/dxpy/dx_extract_utils/__init__.py b/src/python/dxpy/dx_extract_utils/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/python/dxpy/dx_extract_utils/cohort_filter_payload.py b/src/python/dxpy/dx_extract_utils/cohort_filter_payload.py new file mode 100644 index 0000000000..0bd07cda40 --- /dev/null +++ b/src/python/dxpy/dx_extract_utils/cohort_filter_payload.py @@ -0,0 +1,141 @@ +import copy +from functools import reduce + + +def generate_pheno_filter(values, entity, field, filters, lambda_for_list_conv): + + if "pheno_filters" not in filters: + # Create a pheno_filter if none exists. This will be a compound filter + # even though only be one filter defined for the entity field values. + filters["pheno_filters"] = {"compound": [], "logic": "and"} + + if "compound" not in filters["pheno_filters"]: + # Only compound pheno_filters are supported, and the existing + # pheno_filter is not a compound filter. Move the pheno_filter into a + # compound filter so a filter for the primary entity field values can + # be added. + filters["pheno_filters"] = {"compound": [filters["pheno_filters"]], "logic": "and"} + elif "logic" in filters["pheno_filters"] and filters["pheno_filters"]["logic"] != "and": + # pheno_filter is a compound filter, but the logic is not "and" so + # entity field values cannot be selected by adding a filter to the + # existing compound pheno_filter. + raise ValueError("Invalid input cohort. Cohorts must have 'and' logic on the primary entity and field.") + + entity_field = "$".join([entity, field]) + + # Search for an existing filter for the entity and field + for compound_filter in filters["pheno_filters"]["compound"]: + if "filters" not in compound_filter or entity_field not in compound_filter["filters"]: + continue + if "logic" in compound_filter and compound_filter["logic"] != "and": + raise ValueError("Invalid input cohort. Cohorts must have 'and' logic on the primary entity and field.") + primary_filters = [] + for primary_filter in compound_filter["filters"][entity_field]: + if primary_filter["condition"] == "exists": + pass + elif primary_filter["condition"] == "in": + values = sorted(set(values).intersection(set(reduce(lambda_for_list_conv, primary_filter["values"], [])))) + elif primary_filter["condition"] == "not-in": + values = sorted(set(values) - set(reduce(lambda_for_list_conv, primary_filter["values"], []))) + else: + raise ValueError("Invalid input cohort." + " Cohorts cannot have conditions other than \"in\", \"not-in\", or \"exists\" on the primary entity and field.") + primary_filters.append({"condition": "in", "values": values}) + compound_filter["filters"][entity_field] = primary_filters + return filters + + entity_field_filter = {"condition": "in", "values": values} + + # Search for an existing filter with the entity since no entity and field filter was found + for compound_filter in filters["pheno_filters"]["compound"]: + if "entity" not in compound_filter or "name" not in compound_filter["entity"]: + continue + if compound_filter["entity"]["name"] != entity: + continue + if "logic" in compound_filter and compound_filter["logic"] != "and": + raise ValueError("Invalid input cohort. Cohorts must have 'and' logic on the primary entity and field.") + # The entity filter is valid for addition of field filter + compound_filter["filters"][entity_field] = [entity_field_filter] + return filters + + # Search for an existing filter with the entity in a filter with a different field since no entity was found + for compound_filter in filters["pheno_filters"]["compound"]: + if "filters" not in compound_filter: + continue + for other_entity_field in compound_filter["filters"]: + if other_entity_field.split("$")[0] != entity: + continue + if "logic" in compound_filter and compound_filter["logic"] != "and": + continue + # Filter with the entity is valid for addition of field filter + compound_filter["filters"][entity_field] = [entity_field_filter] + return filters + + # No existing filters for the entity were found so create the entity filter + filters["pheno_filters"]["compound"].append({ + "name": "phenotype", + "logic": "and", + "filters": { + entity_field: [entity_field_filter], + }, + }) + + return filters + + +def cohort_filter_payload(values, entity, field, filters, project_context, lambda_for_list_conv, base_sql=None): + if "logic" in filters and filters["logic"] != "and": + raise ValueError("Invalid input cohort. Cohorts must have 'and' logic on the primary entity and field.") + filter_payload = {} + filter_payload["filters"] = generate_pheno_filter(values, entity, field, filters, lambda_for_list_conv) + if "logic" not in filter_payload["filters"]: + filter_payload["filters"]["logic"] = "and" + filter_payload["project_context"] = project_context + if base_sql is not None: + filter_payload["base_sql"] = base_sql + + return filter_payload + + +def cohort_combined_payload(combined): + combined = copy.copy(combined) + source = [] + for source_dict in combined["source"]: + source.append({ + "$dnanexus_link": { + "id": source_dict["id"], + "project": source_dict["project"], + } + }) + combined["source"] = source + + return combined + + +def cohort_final_payload(name, folder, project, databases, dataset, schema, filters, sql, base_sql=None, combined=None): + details = { + "databases": databases, + "dataset": {"$dnanexus_link": dataset}, + "description": "", + "filters": filters, + "schema": schema, + "sql": sql, + "version": "3.0", + } + if base_sql is not None: + details["baseSql"] = base_sql + if combined is not None: + details["combined"] = cohort_combined_payload(combined) + + final_payload = { + "name": name, + "folder": folder, + "project": project, + "types": ["DatabaseQuery", "CohortBrowser"], + "details": details, + "close": True, + } + if combined is not None: + final_payload["types"].append("CombinedDatabaseQuery") + + return final_payload diff --git a/src/python/dxpy/dx_extract_utils/column_conditions.json b/src/python/dxpy/dx_extract_utils/column_conditions.json new file mode 100644 index 0000000000..ed1e909d86 --- /dev/null +++ b/src/python/dxpy/dx_extract_utils/column_conditions.json @@ -0,0 +1,30 @@ +{ + "allele": { + "rsid": "any", + "type": "in", + "dataset_alt_af": "between", + "gnomad_alt_af": "between" + }, + "genotype": { + "genotype_type": "in", + "allele_id": "in", + "sample_id": "in" + }, + "genotype_only": { + "genotype_type": "in", + "sample_id": "in", + "ref_yn": "is", + "halfref_yn": "is", + "nocall_yn": "is" + }, + "annotation": { + "allele_id": "in", + "gene_name": "in", + "gene_id": "in", + "feature_id": "in", + "consequences": "any", + "putative_impact": "in", + "hgvs_c": "in", + "hgvs_p": "in" + } +} diff --git a/src/python/dxpy/dx_extract_utils/column_conversion.json b/src/python/dxpy/dx_extract_utils/column_conversion.json new file mode 100644 index 0000000000..aaa6c2a7d9 --- /dev/null +++ b/src/python/dxpy/dx_extract_utils/column_conversion.json @@ -0,0 +1,31 @@ +{ + "allele": { + "rsid": "allele$dbsnp151_rsid", + "type": "allele$allele_type", + "dataset_alt_af": "allele$alt_freq", + "gnomad_alt_af": "allele$gnomad201_alt_freq" + }, + "genotype": { + "genotype_type": "genotype$type", + "sample_id": "genotype$sample_id", + "allele_id": "allele$a_id" + }, + "genotype_only": { + "genotype_type": "genotype$type", + "sample_id": "genotype$sample_id", + "allele_id": "genotype$a_id", + "ref_yn": "genotype$ref_yn", + "halfref_yn": "genotype$halfref_yn", + "nocall_yn": "genotype$nocall_yn" + }, + "annotation": { + "allele_id": "allele$a_id", + "gene_name": "annotation$gene_name", + "gene_id": "annotation$gene_id", + "feature_id": "annotation$feature_id", + "consequences": "annotation$effect", + "putative_impact": "annotation$putative_impact", + "hgvs_c": "annotation$hgvs_c", + "hgvs_p": "annotation$hgvs_p" + } +} diff --git a/src/python/dxpy/dx_extract_utils/filter_to_payload.py b/src/python/dxpy/dx_extract_utils/filter_to_payload.py new file mode 100644 index 0000000000..e9f1fab167 --- /dev/null +++ b/src/python/dxpy/dx_extract_utils/filter_to_payload.py @@ -0,0 +1,307 @@ +import json + +from ..exceptions import err_exit +from .input_validation import validate_filter +import os +import dxpy +from .retrieve_bins import retrieve_bins + +extract_utils_basepath = os.path.join( + os.path.dirname(dxpy.__file__), "dx_extract_utils" +) + +# A dictionary relating the user-facing names of columns to their actual column +# names in the tables +with open( + os.path.join(extract_utils_basepath, "column_conversion.json"), "r" +) as infile: + column_conversion = json.load(infile) + +# A dictionary relating the user-facing names of columns to the condition that needs +# to be applied in the basic filter for the column +with open( + os.path.join(extract_utils_basepath, "column_conditions.json"), "r" +) as infile: + column_conditions = json.load(infile) + + +def retrieve_geno_bins(list_of_genes, project, genome_reference): + """ + A function for determining appropriate geno bins to attach to a given annotation$gene_name + or annotation$gene_id + """ + stage_file = "Homo_sapiens_genes_manifest_staging.json" + platform_file = "Homo_sapiens_genes_manifest.json" + error_message = "Following gene names or IDs are invalid" + geno_bins = retrieve_bins(list_of_genes, project, genome_reference, extract_utils_basepath, + stage_file,platform_file,error_message) + return geno_bins + + +def basic_filter( + filter_type, friendly_name, values=[], project_context=None, genome_reference=None +): + """ + A low-level filter consisting of a dictionary with one key defining the table and column + and whose value is dictionary defining the user-provided value to be compared to, and the logical operator + used to do the comparison + ex. + {"allele$allele_type": [ + { + "condition": "in", + "values": [ + "SNP" + ] + } + ]} + """ + filter_key = column_conversion[filter_type][friendly_name] + condition = column_conditions[filter_type][friendly_name] + + # Input validation. Check that the user hasn't provided an invalid min/max in any fields + if condition == "between": + min_val = float(values["min"]) + max_val = float(values["max"]) + if min_val > max_val: + err_exit( + "min value greater than max value for filter {}".format(friendly_name) + ) + + values = [min_val, max_val] + if condition == "less-than" or condition == "greater-than": + values = int(values) + + # Check for special cases where the user-input values need to be changed before creating payload + # Case 1: Some fields need to be changed to upper case + if friendly_name in ["gene_id", "feature_id", "putative_impact"]: + values = [x.upper() for x in values] + # Case 2: remove duplicate rsid values + if filter_type == "allele" and friendly_name == "rsid": + values = list(set(values)) + + # Check if we need to add geno bins as well + if friendly_name == "gene_id" or friendly_name == "gene_name": + listed_filter = { + filter_key: [ + { + "condition": condition, + "values": values, + "geno_bins": retrieve_geno_bins( + values, project_context, genome_reference + ), + } + ] + } + else: + listed_filter = {filter_key: [{"condition": condition, "values": values}]} + + return listed_filter + + +def location_filter(location_list, table): + """ + A location filter is actually an table$a_id filter with no filter values + The geno_bins perform the actual location filtering. Related to other geno_bins filters by "or" + """ + + location_aid_filter = { + "{}$a_id".format(table): [ + { + "condition": "in", + "values": [], + "geno_bins": [], + } + ] + } + + for location in location_list: + start = int(location["starting_position"]) + if "ending_position" in location: + end = int(location["ending_position"]) + # Ensure that the geno bins width isn't greater than 5 megabases + if end - start > 5000000: + err_exit('\n'.join([ + "Error in location {}".format(location), + "Location filters may not specify regions larger than 5 megabases", + ])) + else: + end = start + + # Fill out the contents of an object in the geno_bins array + location_aid_filter["{}$a_id".format(table)][0]["geno_bins"].append( + { + "chr": location["chromosome"], + "start": start, + "end": end, + } + ) + + return location_aid_filter + + +def generate_assay_filter( + full_input_dict, + name, + id, + project_context, + genome_reference, + filter_type, + exclude_nocall=None, + exclude_refdata=None, + exclude_halfref=None +): + """ + Generate the entire assay filters object by reading the filter JSON, making the relevant + Basic and Location filters, and creating the structure that relates them logically + + There are three possible types of input JSON: a genotype filter, an allele filter, + and an annotation filter + """ + + filters_dict = {} + table = filter_type if filter_type != "genotype_only" else "genotype" + + for key in full_input_dict.keys(): + if key == "location": + location_list = full_input_dict["location"] + location_aid_filter = location_filter(location_list, table) + filters_dict.update(location_aid_filter) + + else: + if not (full_input_dict[key] == "*" or full_input_dict[key] == None): + filters_dict.update( + basic_filter( + filter_type, + key, + full_input_dict[key], + project_context, + genome_reference, + ) + ) + final_filter_dict = {"assay_filters": {"name": name, "id": id}} + + # Additional structure of the payload + final_filter_dict["assay_filters"].update({"filters": filters_dict}) + # The general filters are related by "and" + final_filter_dict["assay_filters"]["logic"] = "and" + + if exclude_nocall is not None: + # no-call genotypes + final_filter_dict["assay_filters"]["nocall_yn"] = not exclude_nocall + if exclude_refdata is not None: + # reference genotypes + final_filter_dict["assay_filters"]["ref_yn"] = not exclude_refdata + if exclude_halfref is not None: + # half-reference genotypes + final_filter_dict["assay_filters"]["halfref_yn"] = not exclude_halfref + + return final_filter_dict + + +def final_payload( + full_input_dict, + name, + id, + project_context, + genome_reference, + filter_type, + order=True, + exclude_nocall=None, + exclude_refdata=None, + exclude_halfref=None +): + """ + Assemble the top level payload. Top level dict contains the project context, fields (return columns), + and raw filters objects. This payload is sent in its entirety to the vizserver via an + HTTPS POST request + """ + # Generate the assay filter component of the payload + assay_filter = generate_assay_filter( + full_input_dict, + name, + id, + project_context, + genome_reference, + filter_type, + exclude_nocall, + exclude_refdata, + exclude_halfref + ) + + final_payload = {} + # Set the project context + final_payload["project_context"] = project_context + + # Section for defining returned columns for each of the three filter types + if filter_type == "allele": + with open( + os.path.join(extract_utils_basepath, "return_columns_allele.json"), "r" + ) as infile: + fields = json.load(infile) + elif filter_type == "annotation": + with open( + os.path.join(extract_utils_basepath, "return_columns_annotation.json"), "r" + ) as infile: + fields = json.load(infile) + elif filter_type == "genotype": + with open( + os.path.join(extract_utils_basepath, "return_columns_genotype.json"), "r" + ) as infile: + fields = json.load(infile) + elif filter_type == "genotype_only": + with open( + os.path.join(extract_utils_basepath, "return_columns_genotype_only.json"), "r" + ) as infile: + fields = json.load(infile) + + if order: + order_by = [{"allele_id":"asc"}] + + if any("locus_id" in field for field in fields): + order_by.insert(0, {"locus_id":"asc"}) + + # In order for output to be deterministic, we need to do a secondary sort by sample_id + # if it is present in the fields + sample_id_present = False + for field in fields: + if "sample_id" in field: + sample_id_present = True + if sample_id_present: + order_by.append({"sample_id":"asc"}) + + final_payload["order_by"] = order_by + + final_payload["fields"] = fields + final_payload["adjust_geno_bins"] = False + final_payload["raw_filters"] = assay_filter + final_payload["is_cohort"] = True + final_payload["distinct"] = True + + field_names = [] + for f in fields: + field_names.append(list(f.keys())[0]) + + return final_payload, field_names + + +def validate_JSON(filter, type): + """ + Check user-provdied JSON filter for validity + Errors out if JSON is invalid, continues otherwise + """ + + schema_file = "retrieve_{}_schema.json".format(type) + + # Open the schema asset. + with open(os.path.join(extract_utils_basepath, schema_file), "r") as infile: + json_schema = json.load(infile) + + # Note: jsonschema disabled in this release + # The jsonschema validation function will error out if the schema is invalid. The error message will contain + # an explanation of which part of the schema failed + try: + # A function for doing basic input validation that does not rely on jsonschema + validate_filter(filter, type) + # validate(filter, json_schema) + except Exception as inst: + err_exit(inst) diff --git a/src/python/dxpy/dx_extract_utils/germline_utils.py b/src/python/dxpy/dx_extract_utils/germline_utils.py new file mode 100644 index 0000000000..b0e858e3f6 --- /dev/null +++ b/src/python/dxpy/dx_extract_utils/germline_utils.py @@ -0,0 +1,330 @@ +from __future__ import annotations +import json +import os +import re + +from .filter_to_payload import extract_utils_basepath +from .input_validation import GENOTYPE_TYPES + + +GENOTYPE_ONLY_TYPES = ( + "ref", + "no-call", +) + +SELECT_LIST_REGEX = r"SELECT\s+(DISTINCT\s+)?(.+?)\s+FROM" +NAMED_EXPRESSION_REGEX = r"(`?(\w+)?`?\.)?`?(\w+)`?( AS `?(\w+)`?)?" + + +def get_genotype_types(filter_dict): + """ + Returns a list of genotype types for the filter that are use in genotype and allele table queries. + """ + + genotype_types = filter_dict.get("genotype_type") or GENOTYPE_TYPES + return [genotype_type for genotype_type in genotype_types if genotype_type not in GENOTYPE_ONLY_TYPES] + + +def get_genotype_only_types(filter_dict, exclude_refdata, exclude_halfref, exclude_nocall): + """ + Returns a list of genotype types for the filter that are use in genotype table only queries. + """ + + genotype_type_exclude_flag_map = { + "ref": exclude_refdata, + "half": exclude_halfref, + "no-call": exclude_nocall, + } + genotype_types = filter_dict.get("genotype_type") or list(genotype_type_exclude_flag_map) + genotype_only_types = [] + for genotype_type, excluded in genotype_type_exclude_flag_map.items(): + if genotype_type in genotype_types and excluded is not None and not excluded: + genotype_only_types.append(genotype_type) + + return genotype_only_types + + +def get_types_to_filter_out_when_infering( + requested_types: list[str], +) -> list: + """ + Infer option require all genotypes types to be queried. + If users wishes to obtain only certain types of genotypes, + reminder of the types should be filtered out post querying + """ + return [ + type + for type in ["ref", "het-ref", "hom", "het-alt", "half", "no-call"] + if type not in requested_types + ] if requested_types else [] + + +def add_germline_base_sql(resp, payload): + if "CohortBrowser" in resp["recordTypes"]: + if resp.get("baseSql"): + payload["base_sql"] = resp.get("baseSql") + payload["filters"] = resp["filters"] + + +def sort_germline_variant(d): + if "allele_id" in d and d["allele_id"]: + chrom, pos, _, alt = d["allele_id"].split("_") + elif "locus_id" in d and d["locus_id"]: + chrom, pos = d["locus_id"].split("_")[:2] + alt = "" + sample_id = d.get("sample_id", "") + if chrom.isdigit(): + return int(chrom), "", int(pos), alt, sample_id + return float("inf"), chrom, int(pos), alt, sample_id + + +def _parse_sql_select_named_expression(sql): + """ + Parses the SELECT list of a SQL statement and returns a generator of named expressions table, column, and alias. + """ + select_list_match = re.match(SELECT_LIST_REGEX, sql).group(2) + for named_expression in [x.strip() for x in select_list_match.split(",")]: + named_expression_match = re.match(NAMED_EXPRESSION_REGEX, named_expression).groups() + yield named_expression_match[1], named_expression_match[2], named_expression_match[4] + + +def _harmonize_sql_select_named_expression(sql, return_columns, **kwargs): + """ + Harmonizes the SELECT list of a SQL statement to include all columns in return_columns. NULL values are used for + columns not in the SELECT list. + """ + select_info = {alias: (table, column) for table, column, alias in _parse_sql_select_named_expression(sql)} + + select_lists = [] + for return_column in return_columns: + return_column = tuple(return_column)[0] + if return_column in select_info: + table, column = select_info[return_column] + select_lists.append("`{table}`.`{column}` AS `{return_column}`".format(table=table, + column=column, + return_column=return_column)) + elif return_column in kwargs: + select_lists.append("`{table}`.`{column}` AS `ref`".format(table=kwargs[return_column][0], + column=kwargs[return_column][1])) + else: + select_lists.append("NULL AS `{return_column}`".format(return_column=return_column)) + select_list = ", ".join(select_lists) + + distinct = re.match(SELECT_LIST_REGEX, sql).group(1) + return re.sub( + SELECT_LIST_REGEX, + "SELECT {distinct}{select_list} FROM".format(distinct=distinct, select_list=select_list), + sql, + ) + + +def harmonize_germline_sql(sql): + """ + Harmonize the SQL statement for genotype table only queries to include columns to UNION with genotype and allele + table queries. JOIN genotype table to allele table on locus_id to include ref columns values. + """ + with open(os.path.join(extract_utils_basepath, "return_columns_genotype.json")) as infile: + genotype_return_columns = json.load(infile) + + # join genotype table to allele table on locus_id + join_condition_regex = r"ON\s+`(\w+)`\.`(a_id)`\s+=\s+`(\w+)`\.`(a_id)`\s+WHERE" + allele_table = re.search(join_condition_regex, sql).group(2) + sql = re.sub(join_condition_regex, "ON `\\1`.`locus_id` = `\\3`.`locus_id` WHERE", sql) + + # Add return columns missing in SQL statement to the SELECT list as NULL values + sql = _harmonize_sql_select_named_expression(sql, genotype_return_columns, ref=(allele_table, "a_id")) + + return sql + + +def harmonize_germline_results(results, fields_list): + """ + Harmonizes raw query results to include all columns in fields_list. Columns not in fields_list have value None. + """ + harmonized_results = [] + for result in results: + harmonized_result = {} + for field in fields_list: + if field in result: + harmonized_result[field] = result[field] + else: + harmonized_result[field] = None + harmonized_results.append(harmonized_result) + return harmonized_results + + +def get_germline_ref_payload(results, genotype_payload): + """ + Create a payload to query locus_id/ref pairs from the allele table for genotypes missing ref column values. + """ + locus_ids = set((r["locus_id"], r["chromosome"], r["starting_position"]) for r in results if r["ref"] is None) + allele_filters = [] + if not locus_ids: + return None + for locus_id, chr, pos in locus_ids: + allele_filters.append({ + "condition": "in", + "values": [locus_id], + "geno_bins": [{"chr": chr, "start": pos, "end": pos}], + }) + return { + "project_context": genotype_payload["project_context"], + "adjust_geno_bins": False, + "distinct": True, + "logic": "and", + "fields": [ + {"locus_id": "allele$locus_id"}, + {"ref": "allele$ref"}, + ], + "is_cohort": False, + "raw_filters": { + "assay_filters": { + "id": genotype_payload["raw_filters"]["assay_filters"]["id"], + "name": genotype_payload["raw_filters"]["assay_filters"]["name"], + "filters": { + "allele$a_id": allele_filters, + }, + }, + }, + } + + +def update_genotype_only_ref(results, locus_id_refs): + """ + Update genotype results with ref column values from locus_id/ref query result + """ + locus_id_ref_map = {result["locus_id"]: result["ref"] for result in locus_id_refs["results"]} + for result in results: + if result["ref"] is not None: + continue + result["ref"] = locus_id_ref_map[result["locus_id"]] + + +def get_germline_loci_payload(locations, genotype_payload): + """ + Create a payload to query locus ids from the allele table with a location filter + """ + return { + "project_context": genotype_payload["project_context"], + "adjust_geno_bins": False, + "distinct": True, + "logic": "and", + "fields": [ + {"locus_id": "allele$locus_id"}, + {"chromosome": "allele$chr"}, + {"starting_position": "allele$pos"}, + {"ref": "allele$ref"}, + ], + "is_cohort": False, + "raw_filters": { + "assay_filters": { + "id": genotype_payload["raw_filters"]["assay_filters"]["id"], + "name": genotype_payload["raw_filters"]["assay_filters"]["name"], + "filters": { + "allele$a_id": [{ + "condition": "in", + "values": [], + "geno_bins": [ + { + "chr": location["chromosome"], + "start": location["starting_position"], + "end": location["starting_position"] + } for location in locations + ], + }], + }, + }, + }, + } + + +def _produce_loci_dict(loci: list[dict], results_entries: list[dict]) -> dict: + """ + Produces a dictionary with locus_id as key and a set of samples and entry as value. + """ + loci_dict = {} + for locus in loci: + loci_dict[locus["locus_id"]] = { + "samples": set(), + "entry": { + "allele_id": None, + "locus_id": locus["locus_id"], + "chromosome": locus["chromosome"], + "starting_position": locus["starting_position"], + "ref": locus["ref"], + "alt": None, + }, + } + + for entry in results_entries: + loci_dict[entry["locus_id"]]["samples"].add(entry["sample_id"]) + + return loci_dict + + +def infer_genotype_type( + samples: list, loci: list[dict], result_entries: list[dict], type_to_infer: str +) -> list[dict]: + """ + If the result_entries does not contain entry with sample_id of specifific starting_position the the genotype type is either no-call or ref. + Args: + samples: list of all samples + loci: list of information on each loci within the filter e.g. + { + "locus_id": "1_1076145_A_T", + "chromosome": "1", + "starting_position": 1076145, + "ref": "A", + } + result_entries: list of results from extract_assay query. e.g. + { + "sample_id": "SAMPLE_2", + "allele_id": "1_1076145_A_AT", + "locus_id": "1_1076145_A_T", + "chromosome": "1", + "starting_position": 1076145, + "ref": "A", + "alt": "AT", + "genotype_type": "het-alt", + } + type_to_infer: type to infer either "ref" or "no-call" + Returns: list of infered entries with added inferred genotype type and other entries retrieved from result for loci of interest. + """ + loci_dict = _produce_loci_dict(loci, result_entries) + inferred_entries = [] + for locus in loci_dict: + for sample in samples: + if sample not in loci_dict[locus]["samples"]: + inferred_entries.append( + { + "sample_id": sample, + **loci_dict[locus]["entry"], + "genotype_type": type_to_infer, + } + ) + return result_entries + inferred_entries + + +def filter_results( + results: list[dict], key: str, restricted_values: list +) -> list[dict]: + """ + Filters results by key and restricted_values. + Args: + results: list of results from extract_assay query. e.g. + { + "sample_id": "SAMPLE_2", + "allele_id": "1_1076145_A_AT", + "locus_id": "1_1076145_A_T", + "chromosome": "1", + "starting_position": 1076145, + "ref": "A", + "alt": "AT", + "genotype_type": "het-alt", + } + key: key to filter by + restricted_values: list of values to filter by + Returns: list of filtered entries + """ + return [entry for entry in results if entry[key] not in restricted_values] + diff --git a/src/python/dxpy/dx_extract_utils/input_validation.py b/src/python/dxpy/dx_extract_utils/input_validation.py new file mode 100644 index 0000000000..9d4a123458 --- /dev/null +++ b/src/python/dxpy/dx_extract_utils/input_validation.py @@ -0,0 +1,334 @@ +from __future__ import annotations +import sys +from ..exceptions import err_exit + +# Generic error messages +malformed_filter = "Found following invalid filters: {}" +maxitem_message = "Too many items given in field {}, maximum is {}" +# An integer equel to 2 if script is run with python2, and 3 if run with python3 +python_version = sys.version_info.major + + +GENOTYPE_TYPES = ( + "ref", + "het-ref", + "hom", + "het-alt", + "half", + "no-call", +) + + +def warn(msg: str): + print(f"WARNING: {msg}", file=sys.stderr) + + +def is_list_of_strings(object): + if not isinstance(object, list): + return False + for item in object: + # Note that in python 2.7 these strings are read in as unicode + if python_version == 2: + if not (isinstance(item, str) or isinstance(item, unicode)): + return False + else: + if not isinstance(item, str): + return False + return True + + +def validate_filter(filter, filter_type): + keys = filter.keys() + if filter_type == "allele": + # Check for required fields + if ("location" in keys) and ("rsid" in keys): + err_exit( + "location and rsid fields cannot both be specified in the same filter" + ) + if not (("location" in keys) or ("rsid" in keys)): + err_exit("Either location or rsid must be specified in an allele filter") + # Check rsid field + if "rsid" in keys: + if not is_list_of_strings(filter["rsid"]): + err_exit(malformed_filter.format("rsid")) + # Check for max item number + if len(filter["rsid"]) > 100: + err_exit(maxitem_message.format("rsid", 100)) + # Check type field + if "type" in keys: + if not is_list_of_strings(filter["type"]): + err_exit(malformed_filter.format("type")) + # Check against allowed values + for item in filter["type"]: + if item not in ["SNP", "Ins", "Del", "Mixed"]: + err_exit(malformed_filter.format("type")) + # Check for max item number + if len(filter["type"]) > 100: + err_exit(maxitem_message.format("type", 100)) + + # Check dataset_alt_af + if "dataset_alt_af" in keys: + min_val = filter["dataset_alt_af"]["min"] + max_val = filter["dataset_alt_af"]["max"] + if min_val < 0: + err_exit(malformed_filter.format("dataset_alt_af")) + if max_val > 1: + err_exit(malformed_filter.format("dataset_alt_af")) + if min_val > max_val: + err_exit(malformed_filter.format("dataset_alt_af")) + # Check gnomad_alt_af + if "gnomad_alt_af" in keys: + min_val = filter["gnomad_alt_af"]["min"] + max_val = filter["gnomad_alt_af"]["max"] + if min_val < 0: + err_exit(malformed_filter.format("gnomad_alt_af")) + if max_val > 1: + err_exit(malformed_filter.format("gnomad_alt_af")) + if min_val > max_val: + err_exit(malformed_filter.format("gnomad_alt_af")) + # Check location field + if "location" in keys: + # Ensure there are not more than 100 locations + if len(filter["location"]) > 100: + err_exit(maxitem_message.format("location", 100)) + for indiv_location in filter["location"]: + indiv_loc_keys = indiv_location.keys() + # Ensure all keys are there + if not ( + ("chromosome" in indiv_loc_keys) + and ("starting_position" in indiv_loc_keys) + and ("ending_position" in indiv_loc_keys) + ): + err_exit(malformed_filter.format("location")) + # Check that each key is a string + if not is_list_of_strings(list(indiv_location.values())): + err_exit(malformed_filter.format("location")) + + if filter_type == "annotation": + keys = filter.keys() + if not ( + ("allele_id" in filter.keys()) + or ("gene_name" in filter.keys()) + or ("gene_id" in filter.keys()) + ): + err_exit( + "allele_id, gene_name, or gene_id is required in annotation_filters" + ) + + # Ensure only one of the required fields is given + if "allele_id" in keys: + if ("gene_name" in keys) or ("gene_id" in keys): + err_exit( + "Only one of allele_id, gene_name, and gene_id can be provided in an annotation filter" + ) + + elif "gene_id" in keys: + if ("gene_name" in keys) or ("allele_id" in keys): + err_exit( + "Only one of allele_id, gene_name, and gene_id can be provided in an annotation filter" + ) + + elif "gene_name" in keys: + if ("gene_id" in keys) or ("allele_id" in keys): + err_exit( + "Only one of allele_id, gene_name, and gene_id can be provided in an annotation filter" + ) + + # Consequences and putative impact cannot be provided without at least one of gene_id, gene_name, feature_id + if ("consequences" in keys) or ("putative_impact" in keys): + if ( + ("gene_id" not in keys) + and ("gene_name" not in keys) + and ("feature_id" not in keys) + ): + err_exit( + "consequences and putative impact fields may not be specified without " + + "at least one of gene_id, gene_name, or feature_id" + ) + + # All annotation fields are lists of strings + for key in keys: + if not is_list_of_strings(filter[key]): + err_exit(malformed_filter.format(key)) + + # Now ensure no fields have more than the maximum allowable number of items + if "allele_id" in keys: + if len(filter["allele_id"]) > 100: + err_exit(maxitem_message.format("allele_id", 100)) + + if "gene_name" in keys: + if len(filter["gene_name"]) > 100: + err_exit(maxitem_message.format("gene_name", 100)) + + if "gene_id" in keys: + if len(filter["gene_id"]) > 100: + err_exit(maxitem_message.format("gene_id", 100)) + + if "hgvs_c" in keys: + if len(filter["hgvs_c"]) > 100: + err_exit(maxitem_message.format("hgvs_c", 100)) + + if "hgvs_p" in keys: + if len(filter["hgvs_p"]) > 100: + err_exit(maxitem_message.format("hgvs_p", 100)) + + if filter_type == "genotype": + keys = filter.keys() + if not ("allele_id" in keys or "location" in keys): + err_exit("allele_id or location is required in genotype filters") + + if "allele_id" in keys and "location" in keys: + err_exit("allele_id and location fields cannot both be specified in the same genotype filter") + + # Check allele_id field + if "allele_id" in keys: + if not is_list_of_strings(filter["allele_id"]): + err_exit(malformed_filter.format("allele_id")) + + # Check for too many values given + if len(filter["allele_id"]) > 100: + err_exit(maxitem_message.format("allele_id", 100)) + + # Check location field + if "location" in keys: + # Ensure there are not more than 100 locations + if len(filter["location"]) > 100: + err_exit(maxitem_message.format("location", 100)) + for indiv_location in filter["location"]: + indiv_loc_keys = indiv_location.keys() + # Ensure all keys are there + if not ("chromosome" in indiv_loc_keys and "starting_position" in indiv_loc_keys): + err_exit(malformed_filter.format("location")) + if "ending_position" in indiv_loc_keys: + err_exit(malformed_filter.format("location")) + # Check that each key is a string + if not is_list_of_strings(list(indiv_location.values())): + err_exit(malformed_filter.format("location")) + + # Check sample_id field + if "sample_id" in keys: + if not is_list_of_strings(filter["sample_id"]): + err_exit(malformed_filter.format("sample_id")) + + # Check for too many values given + if len(filter["sample_id"]) > 1000: + err_exit(maxitem_message.format("sample_id", 1000)) + + # Check genotype field + if "genotype_type" in keys: + if not is_list_of_strings(filter["genotype_type"]): + err_exit(malformed_filter.format("genotype_type") + "\ngenotype type is not a list of strings") + + # Check against allowed values + for item in filter["genotype_type"]: + if item not in GENOTYPE_TYPES: + err_exit(malformed_filter.format("genotype_type") +"\nvalue {} is not a valid genotype_type".format(item)) + + # Check for too many values given + if len(filter["genotype_type"]) > 6: + err_exit(maxitem_message.format("genotype_type", 4)) + +def validate_infer_flags( + infer_nocall: bool, + infer_ref: bool, + exclude_nocall: bool, + exclude_refdata: bool, + exclude_halfref: bool, +): + # Validate that the genomic_variant assay ingestion exclusion marks and the infer flags are used properly + if (infer_ref or infer_nocall) and exclude_nocall is None: + err_exit( + "The --infer-ref or --infer-nocall flags can only be used when the undelying assay is of version generalized_assay_model_version 1.0.1/1.1.1 or higher." + ) + ingestion_parameters_str = f"Exclusion parameters set at the ingestion: exclude_nocall={str(exclude_nocall).lower()}, exclude_halfref={str(exclude_halfref).lower()}, exclude_refdata={str(exclude_refdata).lower()}" + if infer_ref: + if not ( + exclude_nocall is False + and exclude_halfref is False + and exclude_refdata + ): + err_exit( + f"The --infer-ref flag can only be used when exclusion parameters at ingestion were set to 'exclude_nocall=false', 'exclude_halfref=false', and 'exclude_refdata=true'.\n{ingestion_parameters_str}" + ) + if infer_nocall: + if not ( + exclude_nocall + and exclude_halfref is False + and exclude_refdata is False + ): + err_exit( + f"The --infer-nocall flag can only be used when exclusion parameters at ingestion were set to 'exclude_nocall=true', 'exclude_halfref=false', and 'exclude_refdata=false'.\n{ingestion_parameters_str}" + ) + + +def validate_filter_applicable_genotype_types( + infer_nocall: bool, + infer_ref: bool, + filter_dict: dict, + exclude_nocall: bool, + exclude_refdata: bool, + exclude_halfref: bool, +): + # Check filter provided genotype_types against exclusion options at ingestion. + # e.g. no-call is not applicable when exclude_genotype set and infer-nocall false + + if "genotype_type" in filter_dict: + if exclude_nocall and not infer_nocall: + if "no-call" in filter_dict["genotype_type"]: + warn( + "Filter requested genotype type 'no-call', genotype entries of this type were not ingested in the provided dataset and the --infer-nocall flag is not set!" + ) + if filter_dict["genotype_type"] == []: + warn( + "No genotype type requested in the filter. All genotype types will be returned. Genotype entries of type 'no-call' were not ingested in the provided dataset and the --infer-nocall flag is not set!" + ) + if exclude_refdata and not infer_ref: + if "ref" in filter_dict["genotype_type"]: + warn( + "Filter requested genotype type 'ref', genotype entries of this type were not ingested in the provided dataset and the --infer-ref flag is not set!" + ) + if filter_dict["genotype_type"] == []: + warn( + "No genotype type requested in the filter. All genotype types will be returned. Genotype entries of type 'ref' were not ingested in the provided dataset and the --infer-ref flag is not set!" + ) + if exclude_halfref: + if "half" in filter_dict["genotype_type"]: + warn( + "Filter requested genotype type 'half', 'half-ref genotype' entries (0/.) were not ingested in the provided dataset!" + ) + if filter_dict["genotype_type"] == []: + warn( + "No genotype type requested in the filter. All genotype types will be returned. 'half-ref' genotype entries (0/.) were not ingested in the provided dataset!" + ) + if ( + exclude_refdata is None + and "ref" in filter_dict["genotype_type"] + or exclude_nocall is None + and "no-call" in filter_dict["genotype_type"] + ): + err_exit( + '"ref" and "no-call" genotype types can only be filtered when the undelying assay is of version generalized_assay_model_version 1.0.1/1.1.1 or higher.' + ) + + +def inference_validation( + infer_nocall: bool, + infer_ref: bool, + filter_dict: dict, + exclude_nocall: bool, + exclude_refdata: bool, + exclude_halfref: bool, +): + validate_infer_flags( + infer_nocall, infer_ref, exclude_nocall, exclude_refdata, exclude_halfref + ) + validate_filter_applicable_genotype_types( + infer_nocall, + infer_ref, + filter_dict, + exclude_nocall, + exclude_refdata, + exclude_halfref, + ) + + diff --git a/src/python/dxpy/dx_extract_utils/input_validation_somatic.py b/src/python/dxpy/dx_extract_utils/input_validation_somatic.py new file mode 100644 index 0000000000..8aa02b44a9 --- /dev/null +++ b/src/python/dxpy/dx_extract_utils/input_validation_somatic.py @@ -0,0 +1,139 @@ +import sys +from ..exceptions import err_exit + +# Generic error messages +malformed_filter = 'Found following invalid filters: {}' +maxitem_message = 'Too many items given in field {}, maximum is {}' +# An integer equel to 2 if script is run with python2, and 3 if run with python3 +python_version = sys.version_info.major + +def is_list_of_strings(object): + if not isinstance(object, list): + return False + for item in object: + # Note that in python 2.7 these strings are read in as unicode + if python_version == 2: + if not (isinstance(item, str) or isinstance(item, unicode)): + return False + else: + if not isinstance(item, str): + return False + return True + +def validate_somatic_filter(filter, filter_type): + keys = filter.keys() + if filter_type == 'variant': + required_filter_count = 0 + expected_keys = {'annotation', 'allele', 'sample', 'location'} + if not set(keys).issubset(expected_keys): + err_exit(malformed_filter.format(str(set(keys).difference(list(expected_keys))))) + + if 'annotation' in keys: + annotation_filter = filter['annotation'] + sub_keys = annotation_filter.keys() + expected_sub_keys = {'symbol', 'gene', 'feature', 'hgvsc', 'hgvsp'} + if not set(sub_keys).issubset(expected_sub_keys): + err_exit(malformed_filter.format(str(set(sub_keys).difference(list(expected_sub_keys))))) + + if 'symbol' in sub_keys: + if not is_list_of_strings(annotation_filter['symbol']): + err_exit(malformed_filter.format('annotation["symbol"]')) + if len(annotation_filter['symbol']) > 100: + err_exit(maxitem_message.format('annotation["symbol"]',100)) + if annotation_filter['symbol']: + required_filter_count += 1 + + if 'gene' in sub_keys: + if not is_list_of_strings(annotation_filter['gene']): + err_exit(malformed_filter.format('annotation["gene"]')) + if len(annotation_filter['gene']) > 100: + err_exit(maxitem_message.format('annotation["gene"]',100)) + if annotation_filter['gene']: + required_filter_count += 1 + + if 'feature' in sub_keys: + if not is_list_of_strings(annotation_filter['feature']): + err_exit(malformed_filter.format('annotation["feature"]')) + if len(annotation_filter['feature']) > 100: + err_exit(maxitem_message.format('annotation["feature"]',100)) + if annotation_filter['feature']: + required_filter_count += 1 + + if 'hgvsc' in sub_keys: + if not is_list_of_strings(annotation_filter['hgvsc']): + err_exit(malformed_filter.format('annotation["hgvsc"]')) + if len(annotation_filter['hgvsc']) > 100: + err_exit(maxitem_message.format('annotation["hgvsc"]',100)) + + if 'hgvsp' in sub_keys: + if not is_list_of_strings(annotation_filter['hgvsp']): + err_exit(malformed_filter.format('annotation["hgvsp"]')) + if len(annotation_filter['hgvsp']) > 100: + err_exit(maxitem_message.format('annotation["hgvsp"]',100)) + + if 'allele' in keys: + allele_filter = filter['allele'] + sub_keys = allele_filter.keys() + expected_sub_keys = {'allele_id', 'variant_type'} + if not set(sub_keys).issubset(expected_sub_keys): + err_exit(malformed_filter.format(str(set(sub_keys).difference(list(expected_sub_keys))))) + + if 'allele_id' in sub_keys: + if not is_list_of_strings(allele_filter['allele_id']): + err_exit(malformed_filter.format('allele["allele_id"]')) + if len(allele_filter['allele_id']) > 100: + err_exit(maxitem_message.format('allele["allele_id"]',100)) + if allele_filter['allele_id']: + required_filter_count += 1 + + if 'variant_type' in sub_keys: + if not is_list_of_strings(allele_filter['variant_type']): + err_exit(malformed_filter.format('allele["variant_type"]')) + for item in allele_filter['variant_type']: + if item not in ['SNP', 'INS', 'DEL', 'DUP', 'INV', 'CNV', 'CNV:TR', 'BND', 'DUP:TANDEM', 'DEL:ME', 'INS:ME', 'MISSING', 'MISSING:DEL', 'UNSPECIFIED', 'REF', 'OTHER']: + err_exit(malformed_filter.format('allele["variant_type"]')) + if len(allele_filter['variant_type']) > 10: + err_exit(maxitem_message.format('allele["variant_type"]',10)) + + if 'sample' in keys: + sample_filter = filter['sample'] + sub_keys = sample_filter.keys() + expected_sub_keys = {'sample_id', 'assay_sample_id'} + if not set(sub_keys).issubset(expected_sub_keys): + err_exit(malformed_filter.format(str(set(sub_keys).difference(list(expected_sub_keys))))) + + if 'sample_id' in sub_keys: + if not is_list_of_strings(sample_filter['sample_id']): + err_exit(malformed_filter.format('sample["sample_id"]')) + if len(sample_filter['sample_id']) > 500: + err_exit(maxitem_message.format('sample["sample_id"]',500)) + + if 'assay_sample_id' in sub_keys: + if not is_list_of_strings(sample_filter['assay_sample_id']): + err_exit(malformed_filter.format('sample["assay_sample_id"]')) + if len(sample_filter['assay_sample_id']) > 1000: + err_exit(maxitem_message.format('sample["assay_sample_id"]',1000)) + + if 'location' in keys: + # Ensure there are not more than 100 locations + if len(filter['location']) > 100: + print(maxitem_message.format('location',100)) + for indiv_location in filter['location']: + indiv_loc_keys = indiv_location.keys() + # Ensure all keys are there + if not ( + ('chromosome' in indiv_loc_keys) + and ('starting_position' in indiv_loc_keys) + and ('ending_position' in indiv_loc_keys) + ): + print(malformed_filter.format('location')) + err_exit() + # Check that each key is a string + if not is_list_of_strings(list(indiv_location.values())): + print(malformed_filter.format('location')) + err_exit() + if filter['location']: + required_filter_count += 1 + + if required_filter_count != 1: + err_exit('Exactly one of "symbol", "gene", feature", "allele_id" or "location" must be provided in the json') diff --git a/src/python/dxpy/dx_extract_utils/retrieve_allele_schema.json b/src/python/dxpy/dx_extract_utils/retrieve_allele_schema.json new file mode 100644 index 0000000000..bf2b253269 --- /dev/null +++ b/src/python/dxpy/dx_extract_utils/retrieve_allele_schema.json @@ -0,0 +1,90 @@ +{ + "title": "Retrieve Allele Schema", + "$id": "/retrieve_allele_schema", + "description": "A description of the allele JSON file", + "type": "object", + "additionalProperties": false, + "oneOf": [ + { + "required": [ + "location" + ] + }, + { + "required": [ + "rsid" + ] + } + ], + "properties": { + "rsid": { + "type": "array", + "description": "list of rsIDs of alleles", + "items": { + "type": "string" + }, + "maxItems": 100 + }, + "type": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "SNP", + "Ins", + "Del", + "Mixed" + ] + }, + "maxItems": 4 + }, + "dataset_alt_af": { + "type": "object", + "properties": { + "min": { + "type": "number", + "min": 0, + "max": 1 + }, + "max": { + "type": "number", + "min": 0, + "max": 1 + } + } + }, + "gnomad_alt_af": { + "type": "object", + "properties": { + "min": { + "type": "number", + "min": 0, + "max": 1 + }, + "max": { + "type": "number", + "min": 0, + "max": 1 + } + } + }, + "location": { + "type": "array", + "items": { + "type": "object", + "properties": { + "chromosome": { + "type": "string" + }, + "starting_position": { + "type": "string" + }, + "ending_position": { + "type": "string" + } + }, + "maxItems": 100 + } + } + } +} \ No newline at end of file diff --git a/src/python/dxpy/dx_extract_utils/retrieve_annotation_schema.json b/src/python/dxpy/dx_extract_utils/retrieve_annotation_schema.json new file mode 100644 index 0000000000..6ab5871181 --- /dev/null +++ b/src/python/dxpy/dx_extract_utils/retrieve_annotation_schema.json @@ -0,0 +1,79 @@ +{ + "title": "Retrieve Annotation Schema", + "$id": "/retrieve_annotation_schema", + "description": "A description of the retrieve annotation JSON file", + "type": "object", + "additionalProperties": false, + "oneOf": [ + { + "required": [ + "allele_id" + ] + }, + { + "required": [ + "gene_name" + ] + }, + { + "required": [ + "gene_id" + ] + } + ], + "properties": { + "allele_id": { + "type": "array", + "items": { + "type": "string" + }, + "maxItems": 1000 + }, + "gene_name": { + "type": "array", + "items": { + "type": "string" + }, + "maxItems": 100 + }, + "gene_id": { + "type": "array", + "items": { + "type": "string" + }, + "maxItems": 100 + }, + "feature_id": { + "type": "array", + "items": { + "type": "string" + } + }, + "consequences": { + "type": "array", + "items": { + "type": "string" + } + }, + "putative_impact": { + "type": "array", + "items": { + "type": "string" + } + }, + "hgvs_c": { + "type": "array", + "items": { + "type": "string" + }, + "maxItems": 100 + }, + "hgvs_p": { + "type": "array", + "items": { + "type": "string" + }, + "maxItems": 100 + } + } +} \ No newline at end of file diff --git a/src/python/dxpy/dx_extract_utils/retrieve_bins.py b/src/python/dxpy/dx_extract_utils/retrieve_bins.py new file mode 100644 index 0000000000..4877d6a989 --- /dev/null +++ b/src/python/dxpy/dx_extract_utils/retrieve_bins.py @@ -0,0 +1,51 @@ +import json +from ..exceptions import err_exit, ResourceNotFound +import os +import dxpy +import subprocess + +def retrieve_bins(list_of_genes, project, genome_reference, extract_utils_basepath, + stage_file,platform_file,error_message): + """ + A function for determining appropriate geno bins to attach to a given filter + """ + project_desc = dxpy.describe(project) + geno_positions = [] + + try: + with open( + os.path.join( + extract_utils_basepath, platform_file + ), + "r", + ) as geno_bin_manifest: + r = json.load(geno_bin_manifest) + dxpy.describe(r[genome_reference][project_desc["region"]]) + except ResourceNotFound: + with open( + os.path.join( + extract_utils_basepath, stage_file + ), + "r", + ) as geno_bin_manifest: + r = json.load(geno_bin_manifest) + + geno_bins = subprocess.check_output( + ["dx", "cat", r[genome_reference][project_desc["region"]]] + ) + geno_bins_json = json.loads(geno_bins) + invalid_genes = [] + + for gene in list_of_genes: + bin = geno_bins_json.get(gene) + if bin is None: + invalid_genes.append(gene) + else: + bin.pop("strand") + geno_positions.append(bin) + + if invalid_genes: + error_message = error_message + ": " + str(invalid_genes) + err_exit(error_message) + + return geno_positions \ No newline at end of file diff --git a/src/python/dxpy/dx_extract_utils/retrieve_genotype_schema.json b/src/python/dxpy/dx_extract_utils/retrieve_genotype_schema.json new file mode 100644 index 0000000000..334eaf75b2 --- /dev/null +++ b/src/python/dxpy/dx_extract_utils/retrieve_genotype_schema.json @@ -0,0 +1,50 @@ +{ + "title": "Retrieve Genotype Schema", + "$id": "/retrieve_genotype_schema", + "description": "A description of the retrieve sample JSON file", + "type": "object", + "additionalProperties": false, + "oneOf": [ + { + "required": [ + "allele_id" + ] + }, + { + "required": [ + "location" + ] + } + ], + "properties": { + "allele_id": { + "type": "array", + "items": { + "type": "string" + }, + "maxItems": 1000 + }, + "sample_id": { + "type": "array", + "items": { + "type": "string" + }, + "maxItems": 1000 + }, + "genotype_type": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "hom", + "het-ref", + "het-alt", + "half", + "ref", + "no-call" + ] + }, + "maxItems": 6 + } + } +} diff --git a/src/python/dxpy/dx_extract_utils/return_columns_allele.json b/src/python/dxpy/dx_extract_utils/return_columns_allele.json new file mode 100644 index 0000000000..c0dce991f1 --- /dev/null +++ b/src/python/dxpy/dx_extract_utils/return_columns_allele.json @@ -0,0 +1,32 @@ +[ + { + "allele_id": "allele$a_id" + }, + { + "chromosome": "allele$chr" + }, + { + "starting_position": "allele$pos" + }, + { + "ref": "allele$ref" + }, + { + "alt": "allele$alt" + }, + { + "rsid": "allele$dbsnp151_rsid" + }, + { + "allele_type": "allele$allele_type" + }, + { + "dataset_alt_freq": "allele$alt_freq" + }, + { + "gnomad_alt_freq": "allele$gnomad201_alt_freq" + }, + { + "worst_effect": "allele$worst_effect" + } +] \ No newline at end of file diff --git a/src/python/dxpy/dx_extract_utils/return_columns_annotation.json b/src/python/dxpy/dx_extract_utils/return_columns_annotation.json new file mode 100644 index 0000000000..f2967eab4f --- /dev/null +++ b/src/python/dxpy/dx_extract_utils/return_columns_annotation.json @@ -0,0 +1,26 @@ +[ + { + "allele_id": "annotation$a_id" + }, + { + "gene_name": "annotation$gene_name" + }, + { + "gene_id": "annotation$gene_id" + }, + { + "feature_id": "annotation$feature_id" + }, + { + "consequences": "annotation$effect" + }, + { + "putative_impact": "annotation$putative_impact" + }, + { + "hgvs_c": "annotation$hgvs_c" + }, + { + "hgvs_p": "annotation$hgvs_p" + } +] \ No newline at end of file diff --git a/src/python/dxpy/dx_extract_utils/return_columns_genotype.json b/src/python/dxpy/dx_extract_utils/return_columns_genotype.json new file mode 100644 index 0000000000..6444ae697c --- /dev/null +++ b/src/python/dxpy/dx_extract_utils/return_columns_genotype.json @@ -0,0 +1,26 @@ +[ + { + "sample_id": "genotype$sample_id" + }, + { + "allele_id": "allele$a_id" + }, + { + "locus_id": "genotype$locus_id" + }, + { + "chromosome": "genotype$chr" + }, + { + "starting_position": "genotype$pos" + }, + { + "ref": "allele$ref" + }, + { + "alt": "allele$alt" + }, + { + "genotype_type": "genotype$type" + } +] \ No newline at end of file diff --git a/src/python/dxpy/dx_extract_utils/return_columns_genotype_only.json b/src/python/dxpy/dx_extract_utils/return_columns_genotype_only.json new file mode 100644 index 0000000000..a3500309e3 --- /dev/null +++ b/src/python/dxpy/dx_extract_utils/return_columns_genotype_only.json @@ -0,0 +1,20 @@ +[ + { + "sample_id": "genotype$sample_id" + }, + { + "allele_id": "genotype$a_id" + }, + { + "locus_id": "genotype$locus_id" + }, + { + "chromosome": "genotype$chr" + }, + { + "starting_position": "genotype$pos" + }, + { + "genotype_type": "genotype$type" + } +] diff --git a/src/python/dxpy/dx_extract_utils/somatic_filter_payload.py b/src/python/dxpy/dx_extract_utils/somatic_filter_payload.py new file mode 100644 index 0000000000..d3e201aaca --- /dev/null +++ b/src/python/dxpy/dx_extract_utils/somatic_filter_payload.py @@ -0,0 +1,290 @@ +import os + +from ..exceptions import err_exit +from .retrieve_bins import retrieve_bins +import dxpy + +# A dictionary relating the user-facing names of columns to their actual column + +column_conversion = { + "allele_id": "variant_read_optimized$allele_id", + "variant_type": "variant_read_optimized$variant_type", + "symbol": "variant_read_optimized$SYMBOL", + "gene": "variant_read_optimized$Gene", + "feature": "variant_read_optimized$Feature", + "hgvsc": "variant_read_optimized$HGVSc", + "hgvsp": "variant_read_optimized$HGVSp", + "assay_sample_id": "variant_read_optimized$assay_sample_id", + "sample_id": "variant_read_optimized$sample_id", + "tumor_normal": "variant_read_optimized$tumor_normal", +} + +column_conditions = { + "allele_id": "in", + "variant_type": "in", + "symbol": "any", + "gene": "any", + "feature": "any", + "hgvsc": "any", + "hgvsp": "any", + "assay_sample_id": "in", + "sample_id": "in", + "tumor_normal": "is", +} + +extract_utils_basepath = os.path.join( + os.path.dirname(dxpy.__file__), "dx_extract_utils" +) + +chrs = [] +for i in range(1, 23): + chrs.append(str(i)) +chrs.extend(["Y", "X"]) + +def retrieve_geno_bins(list_of_genes, project, genome_reference, friendly_name): + """ + A function for determining appropriate geno bins to attach to a given SYMBOL, Gene or Feature + """ + stage_file = "Homo_sapiens_genes_manifest_staging_vep.json" + platform_file = "Homo_sapiens_genes_manifest_vep.json" + error_message = "Following symbols, genes or features are invalid" + genome_reference = genome_reference + "_" + friendly_name + geno_bins = retrieve_bins(list_of_genes, project, genome_reference, extract_utils_basepath, + stage_file,platform_file,error_message) + updated_bins = [] + # If a gene, symbol or feature has non standard contig, + # The correct bin is 'Other' + for bin in geno_bins: + if bin['chr'].strip("chr").strip("Chr") not in chrs: + bin['chr'] = 'Other' + updated_bins.append(bin) + return updated_bins + +def basic_filter( + table, friendly_name, values=[], project_context=None, genome_reference=None +): + """ + A low-level filter consisting of a dictionary with one key defining the table and column + and whose value is dictionary defining the user-provided value to be compared to, and the logical operator + used to do the comparison + ex. + {"allele$allele_type": [ + { + "condition": "in", + "values": [ + "SNP" + ] + } + ]} + """ + table = "variant_read_optimized" + # Get the name of this field in the variant table + # If the column isn't in the regular fields list, use the friendly name itself as the column name + # This could be the case when "--additional-fields" flag is used + filter_key = column_conversion.get( + friendly_name, "variant_read_optimized${}".format(friendly_name) + ) + # Handle special cases where values need to be capitalized + if friendly_name == "variant_type" or friendly_name == "gene" or friendly_name == "feature": + values = [str(x).upper() for x in values] + # Get the condition ofr this field + condition = column_conditions.get(friendly_name, "in") + + # Check if we need to add geno bins as well + if friendly_name in ["symbol", "gene", "feature"]: + listed_filter = { + filter_key: [ + { + "condition": condition, + "values": values, + "geno_bins": retrieve_geno_bins( + values, project_context, genome_reference, friendly_name + ), + } + ] + } + else: + listed_filter = {filter_key: [{"condition": condition, "values": values}]} + + return listed_filter + +def location_filter(location_list): + """ + A location filter is actually an variant_read_optimized$allele_id filter with no filter values + The geno_bins perform the actual location filtering. Related to other geno_bins filters by "or" + """ + + location_aid_filter = { + "variant_read_optimized$allele_id": [ + { + "condition": "in", + "values": [], + "geno_bins": [], + } + ] + } + + chrom =[] + + for location in location_list: + # First, ensure that the geno bins width isn't greater than 250 megabases + start = int(location["starting_position"]) + end = int(location["ending_position"]) + if end - start > 250000000: + err_exit( + "Error in location {}\nLocation filters may not specify regions larger than 250 megabases".format( + location + ) + ) + + # Fill out the contents of an object in the geno_bins array + chrom.append(location["chromosome"]) + chr = location["chromosome"].strip("chr").strip("Chr") + + # If a non standard contig ID is passed in location filter's chr, + # Add the filter as a CHROM filter and pass 'Other' in geno_bins + # Standard contigs are also passed to CHROM filter + if chr not in chrs: + chr = "Other" + + location_aid_filter["variant_read_optimized$allele_id"][0]["geno_bins"].append( + { + "chr": chr, + "start": start, + "end": end, + } + ) + + return location_aid_filter, list(set(chrom)) + + +def generate_assay_filter( + full_input_dict, + name, + id, + project_context, + genome_reference=None, + include_normal=False, +): + """ + Generate asasy filter consisting of a compound that links the Location filters if present + to the regular filters + { + "assay_filters": { + "id": "", + "name":"", + "compound":[ + { + + } + ] + } + } + """ + + filters_dict = {} + + for filter_group in full_input_dict.keys(): + if filter_group == "location": + location_list = full_input_dict["location"] + if location_list: + location_aid_filter, chrom = location_filter(location_list) + filters_dict.update(location_aid_filter) + # Add a CHROM filter to handle non standard contigs + filters_dict.update(basic_filter( + "variant_read_optimized", + "CHROM", + chrom, + project_context, + genome_reference, + )) + + else: + for individual_filter_name in full_input_dict[filter_group].keys(): + individual_filter = full_input_dict[filter_group][individual_filter_name] + if individual_filter: + filters_dict.update(basic_filter( + "variant_read_optimized", + individual_filter_name, + individual_filter, + project_context, + genome_reference, + )) + + # If include_normal is False, then add a filter to select data where tumor_normal = tumor + if not include_normal: + filters_dict.update(basic_filter( + "variant_read_optimized", + "tumor_normal", + "tumor", + project_context, + genome_reference, + )) + + final_filter_dict = {"assay_filters": {"name": name, "id": id}} + + # Additional structure of the payload + final_filter_dict["assay_filters"].update({"filters": filters_dict}) + # The general filters are related by "and" + final_filter_dict["assay_filters"]["logic"] = "and" + + return final_filter_dict + + +def somatic_final_payload( + full_input_dict, + name, + id, + project_context, + genome_reference=None, + additional_fields=None, + include_normal=False, +): + """ + Assemble the top level payload. Top level dict contains the project context, fields (return columns), + and raw filters objects. This payload is sent in its entirety to the vizserver via an + HTTPS POST request + """ + # Generate the assay filter component of the payload + assay_filter = generate_assay_filter( + full_input_dict, name, id, project_context, genome_reference, include_normal + ) + + final_payload = {} + # Set the project context + final_payload["project_context"] = project_context + + fields = [ + {"assay_sample_id": "variant_read_optimized$assay_sample_id"}, + {"allele_id": "variant_read_optimized$allele_id"}, + {"CHROM": "variant_read_optimized$CHROM"}, + {"POS": "variant_read_optimized$POS"}, + {"REF": "variant_read_optimized$REF"}, + {"allele": "variant_read_optimized$allele"}, + ] + + order_by = [ + {"CHROM":"asc"}, + {"POS":"asc"}, + {"allele_id":"asc"}, + {"assay_sample_id":"asc"} + ] + + # If the user has specified additional return columns, add them to the payload here + if additional_fields: + for add_field in additional_fields: + fields.append( + {"{}".format(add_field): "variant_read_optimized${}".format(add_field)} + ) + + final_payload["fields"] = fields + final_payload["order_by"] = order_by + final_payload["raw_filters"] = assay_filter + final_payload["distinct"] = True + final_payload["adjust_geno_bins"] = False + + field_names = [] + for f in fields: + field_names.append(list(f.keys())[0]) + + return final_payload, field_names diff --git a/src/python/dxpy/dxlog.py b/src/python/dxpy/dxlog.py index e0de9465a8..00a6d09a7a 100644 --- a/src/python/dxpy/dxlog.py +++ b/src/python/dxpy/dxlog.py @@ -70,16 +70,18 @@ def encodePriority(self, record): return self.priority_names[self.priority_map.get(record.levelname, "warning")] def truncate_message(self, message): - if USING_PYTHON2: - if len(message) > 8015: - message = message[:8000] + "... [truncated]" - else: - # Trim bytes - encoded = message.encode('utf-8') - if len(encoded) > 8015: - # Ignore UnicodeDecodeError chars that could have been messed up by truncating - message = encoded[:8015].decode('utf-8', 'ignore') + "... [truncated]" - return message + msg_bytes = message if USING_PYTHON2 else message.encode('utf-8') + + if len(json.dumps(message)) <= 8015: + return message + + msg_bytes = msg_bytes[:8000] + while len(json.dumps(_bytes2utf8(msg_bytes))) > 8000: + msg_bytes = msg_bytes[:-1] + + message = _bytes2utf8(msg_bytes) + message = message.encode('utf-8') if USING_PYTHON2 else message + return message + "... [truncated]" def is_resource_log(self, message): if USING_PYTHON2: @@ -119,3 +121,9 @@ def emit(self, record): raise except: self.handleError(record) + +def _bytes2utf8(bytes): + """ + Convert bytes to a UTF-8 string and ignore UnicodeDecodeError for chars that could have been messed up by truncating. + """ + return bytes.decode('utf-8', 'ignore') diff --git a/src/python/dxpy/exceptions.py b/src/python/dxpy/exceptions.py index 62aedad249..3782508d0f 100644 --- a/src/python/dxpy/exceptions.py +++ b/src/python/dxpy/exceptions.py @@ -20,11 +20,17 @@ from __future__ import print_function, unicode_literals, division, absolute_import -import sys, json, traceback, errno, socket -import requests -from requests.exceptions import HTTPError +import sys +import json +import traceback +import errno +import socket +from urllib3.exceptions import HTTPError import dxpy +from .compat import USING_PYTHON2 +import urllib3 +import ssl EXPECTED_ERR_EXIT_STATUS = 3 @@ -155,6 +161,18 @@ class ContentLengthError(HTTPError): match the "Content-Length" header ''' +class HTTPErrorWithContent(HTTPError): + ''' + Specific variant of HTTPError with response content. + + This class was created to avoid appending content directly to error message + which makes difficult to format log strings. + ''' + + def __init__(self, value, content): + super(HTTPError, self).__init__(value) + self.content = content + class BadJSONInReply(ValueError): ''' Raised when the server returned invalid JSON in the response body. Possible reasons @@ -219,16 +237,26 @@ def exit_with_exc_info(code=1, message='', print_tb=False, exception=None): sys.stderr.write('\n') sys.exit(code) -network_exceptions = (requests.packages.urllib3.exceptions.ProtocolError, - requests.packages.urllib3.exceptions.NewConnectionError, - requests.packages.urllib3.exceptions.DecodeError, - requests.packages.urllib3.exceptions.ConnectTimeoutError, - requests.packages.urllib3.exceptions.ReadTimeoutError, - requests.packages.urllib3.connectionpool.HTTPException, +network_exceptions = (urllib3.exceptions.ProtocolError, + urllib3.exceptions.NewConnectionError, + urllib3.exceptions.DecodeError, + urllib3.exceptions.ConnectTimeoutError, + urllib3.exceptions.ReadTimeoutError, + urllib3.connectionpool.HTTPException, + urllib3.exceptions.SSLError, + ssl.SSLError, HTTPError, socket.error) +if not USING_PYTHON2: + network_exceptions += (ConnectionResetError,) + + +try: + json_exceptions = (json.decoder.JSONDecodeError,) +except: + json_exceptions = (ValueError,) -default_expected_exceptions = network_exceptions + (DXAPIError, +default_expected_exceptions = network_exceptions + json_exceptions + (DXAPIError, DXCLIError, KeyboardInterrupt) diff --git a/src/python/dxpy/executable_builder.py b/src/python/dxpy/executable_builder.py index affe92ffa7..bac7529e18 100644 --- a/src/python/dxpy/executable_builder.py +++ b/src/python/dxpy/executable_builder.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # # Copyright (C) 2013-2016 DNAnexus, Inc. # @@ -98,6 +98,34 @@ def delete_temporary_projects(projects): except Exception: pass +def get_valid_bill_to(bill_to, executable_builder_exception): + """ + Check if the requesting user can perform billable activities on behalf of the billTo + If not specified, default to the billTo of the requesting user + otherwise it must be either the ID of the requesting user, + or an org of which the requesting user is a member with 'allowBillableActivities' permission + """ + user_id = dxpy.whoami() + if not bill_to: + return dxpy.api.user_describe(user_id)['billTo'] + + exception_msg = None + if bill_to.startswith('user-') and bill_to != user_id: + exception_msg = 'Cannot request another user to be the "billTo"' + elif bill_to.startswith('org-'): + try: + member_access = dxpy.api.org_describe(bill_to) + if not member_access['allowBillableActivities']: + exception_msg='You are not a member in {} with allowBillableActivities permission. Please check the billing policy of the org.'.format(bill_to) + except: + exception_msg='Cannot retrieve billing information for {}. Please check your access level and the billing policy of the org.'.format(bill_to) + else: + exception_msg='The field "billTo" must be a valid ID of a user/org.' + + if exception_msg: + raise executable_builder_exception(exception_msg) + + return bill_to def verify_developer_rights(prefixed_name): """ @@ -118,7 +146,7 @@ def verify_developer_rights(prefixed_name): describe_method = dxpy.api.global_workflow_describe exception_msg = \ 'A global workflow with the given name already exists and you are not a developer of that workflow' - + name_already_exists = True is_developer = False version = None @@ -152,7 +180,7 @@ def verify_developer_rights(prefixed_name): return FoundExecutable(name=name_without_prefix, version=version, id=executable_id) -def assert_consistent_regions(from_spec, from_command_line, builder_exception): +def assert_consistent_regions(from_spec, from_command_line, executable_builder_exception): """ Verifies the regions passed with --region CLI argument and the ones specified in regionalOptions are the same (if both CLI and spec were used) @@ -160,26 +188,26 @@ def assert_consistent_regions(from_spec, from_command_line, builder_exception): if from_spec is None or from_command_line is None: return if set(from_spec) != set(from_command_line): - raise builder_exception("--region and the 'regionalOptions' key in the JSON file do not agree") + raise executable_builder_exception("--region and the 'regionalOptions' key in the JSON file or --extra-args do not agree") -def assert_consistent_reg_options(exec_type, json_spec, executable_builder_exeception): +def assert_consistent_reg_options(exec_type, json_spec, executable_builder_exception): """ Validates the "regionalOptions" field and verifies all the regions used in "regionalOptions" have the same options. """ reg_options_spec = json_spec.get('regionalOptions') - json_fn = 'dxapp.json' if exec_type == 'app' else 'dxworkflow.json' + json_fn = 'dxapp.json or --extra-args' if exec_type == 'app' else 'dxworkflow.json or --extra-args' if not isinstance(reg_options_spec, dict): - raise executable_builder_exeception("The field 'regionalOptions' in must be a mapping") + raise executable_builder_exception("The field 'regionalOptions' in must be a mapping") if not reg_options_spec: - raise executable_builder_exeception( + raise executable_builder_exception( "The field 'regionalOptions' in " + json_fn + " must be a non-empty mapping") regional_options_list = list(reg_options_spec.items()) for region, opts_for_region in regional_options_list: if not isinstance(opts_for_region, dict): - raise executable_builder_exeception("The field 'regionalOptions['" + region + + raise executable_builder_exception("The field 'regionalOptions['" + region + "']' in " + json_fn + " must be a mapping") if set(opts_for_region.keys()) != set(regional_options_list[0][1].keys()): if set(opts_for_region.keys()) - set(regional_options_list[0][1].keys()): @@ -188,18 +216,29 @@ def assert_consistent_reg_options(exec_type, json_spec, executable_builder_exece else: with_key, without_key = regional_options_list[0][0], region key_name = next(iter(set(regional_options_list[0][1].keys()) - set(opts_for_region.keys()))) - raise executable_builder_exeception( + raise executable_builder_exception( "All regions in regionalOptions must specify the same options; " + - "%s was given for %s but not for %s" % (key_name, with_key, without_key) + "{} was given for {} but not for {}" .format (key_name, with_key, without_key) ) if exec_type == 'app': for key in opts_for_region: if key in json_spec.get('runSpec', {}): - raise executable_builder_exeception( + raise executable_builder_exception( key + " cannot be given in both runSpec and in regional options for " + region) -def get_enabled_regions(exec_type, json_spec, from_command_line, executable_builder_exeception): +def get_permitted_regions(bill_to, executable_builder_exception): + """ + Validates requested bill_to and returns the set of its permitted regions. + """ + billable_regions = set() + try: + billable_regions= set(dxpy.DXHTTPRequest('/' + bill_to + '/describe', {}).get("permittedRegions")) + except: + raise executable_builder_exception("Failed to get permitted regions of {}".format(bill_to)) + return billable_regions + +def get_enabled_regions(exec_type, json_spec, from_command_line, executable_builder_exception): """ Return a list of regions in which the global executable (app or global workflow) will be enabled, based on the "regionalOption" in their JSON specification @@ -211,16 +250,16 @@ def get_enabled_regions(exec_type, json_spec, from_command_line, executable_buil :type json_spec: dict or None. :param from_command_line: The regional options specified on the command-line via --region. :type from_command_line: list or None. - :param builder_exception: Exception that will be thrown. - :type builder_exception: AppBuilderException or WorkflowBuilderException. + :param executable_builder_exception: Exception that will be thrown. + :type executable_builder_exception: AppBuilderException or WorkflowBuilderException. """ from_spec = json_spec.get('regionalOptions') if from_spec is not None: - assert_consistent_reg_options(exec_type, json_spec, executable_builder_exeception) + assert_consistent_reg_options(exec_type, json_spec, executable_builder_exception) - assert_consistent_regions(from_spec, from_command_line, executable_builder_exeception) + assert_consistent_regions(from_spec, from_command_line, executable_builder_exception) enabled_regions = None if from_spec is not None: diff --git a/src/python/dxpy/nextflow/ImageRef.py b/src/python/dxpy/nextflow/ImageRef.py new file mode 100644 index 0000000000..b0e04d1793 --- /dev/null +++ b/src/python/dxpy/nextflow/ImageRef.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python + +import os +import subprocess + +from dxpy import DXFile, config, upload_local_file +from dxpy.exceptions import err_exit + + +class ImageRef(object): + def __init__( + self, + process, + digest, + dx_file_id=None, + repository=None, + image_name=None, + tag=None + ): + """ + A class to handle an image reference from nextflow pipeline. + :param process: An NPA proces name (aka task name) which uses a given image + :type process: String + :param digest: An image digest + :type digest: String + :param dx_file_id: dx file id on the platform + :type dx_file_id: Optional[String] + :param repository: Image repository + :type repository: Optional[String] + :param image_name: Image name (usually a basename of the image referenced with repository) + :type image_name: Optional[String] + :param tag: A version tag + :type tag: Optional[String] + """ + self._caching_dir = os.path.join("/.cached_docker_images/", image_name or "") + self._dx_file_id = dx_file_id + self._bundled_depends = None + self._repository = repository + self._image_name = image_name + self._tag = tag + self._digest = digest + self._process = process + + @property + def bundled_depends(self): + if not self._bundled_depends: + self._bundled_depends = self._package_bundle() + return self._bundled_depends + + @property + def identifier(self): + return self._join_if_exists("_", [self._repository, self._image_name, self._tag, self._digest]) + + def _cache(self, file_name): + """ + Function to store an image on the platform as a dx file object. Should be implemented in subclasses. + :param file_name: A file name under which the image will be saved on the platform + :type file_name: String + :returns: Tuple[String, String] dx file id, file name (basename) + """ + raise NotImplementedError("Abstract class. Method not implemented. Use the concrete implementations.") + + def _reconstruct_image_ref(self): + raise NotImplementedError("Abstract class. Method not implemented. Use the concrete implementations.") + + def _construct_cache_file_name(self): + return self._join_if_exists("_", [self._image_name, self._tag]) + + @staticmethod + def _join_if_exists(delimiter, parts): + return delimiter.join([x for x in parts if x]) + + def _dx_file_get_name(self): + dx_file_handle = DXFile(self._dx_file_id, config["DX_PROJECT_CONTEXT_ID"]) + return dx_file_handle.describe().get("name") + + def _package_bundle(self): + """ + Function to include a container image stored on the platform into NPA + :returns: Dict in the format of {"name": "bundle.tar.gz", "id": {"$dnanexus_link": "file-xxxx"}} + """ + if not self._dx_file_id: + cache_file_name = self._construct_cache_file_name() + self._dx_file_id = self._cache(cache_file_name) + else: + cache_file_name = self._dx_file_get_name() + return { + "name": cache_file_name, + "id": {"$dnanexus_link": self._dx_file_id} + } + + +class DockerImageRef(ImageRef): + def __init__( + self, + process, + digest, + dx_file_id=None, + repository=None, + image_name=None, + tag=None + ): + super().__init__( + process, + digest, + dx_file_id, + repository, + image_name, + tag) + + def _cache(self, file_name): + full_image_ref = self._reconstruct_image_ref() + docker_pull_cmd = "sudo docker pull {}".format(full_image_ref) + docker_save_cmd = "sudo docker save {} | gzip > {}".format(full_image_ref, file_name) + for cmd in [docker_pull_cmd, docker_save_cmd]: + try: + _ = subprocess.check_output(cmd, shell=True) + except subprocess.CalledProcessError: + err_exit("Failed to run a subprocess command: {}".format(cmd)) + # may need wait_on_close = True?? + extracted_digest = self._digest + if not self._digest: + digest_cmd = "docker images --no-trunc --quiet {}".format(full_image_ref) + extracted_digest = subprocess.check_output(digest_cmd, shell=True).decode().strip() + uploaded_dx_file = upload_local_file( + filename=file_name, + project=config["DX_PROJECT_CONTEXT_ID"], + folder=self._caching_dir, + name=file_name, + parents=True, + properties={"image_digest": self._digest or extracted_digest} + ) + return uploaded_dx_file.get_id() + + def _reconstruct_image_ref(self): + """ + Docker image reference has the form of /: or + /@ + """ + repo_and_image_name = self._join_if_exists("", [self._repository, self._image_name]) + if self._digest: + full_ref = self._join_if_exists("@", [repo_and_image_name, self._digest]) + else: + full_ref = self._join_if_exists(":", [repo_and_image_name, self._tag]) + return full_ref diff --git a/src/python/dxpy/nextflow/ImageRefFactory.py b/src/python/dxpy/nextflow/ImageRefFactory.py new file mode 100644 index 0000000000..70d693d260 --- /dev/null +++ b/src/python/dxpy/nextflow/ImageRefFactory.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python + +from dxpy.nextflow.ImageRef import DockerImageRef + + +class ImageRefFactoryError(Exception): + """ + Class to handle errors with instantiation of ImageRef subclasses + """ + + +class ImageRefFactory(object): + def __init__( + self, + image_ref + ): + """ + A class to instantiate subclasses of ImageRef based on the container engine. Ususally instantiated after using the + nextaur:collect function to collect images for docker and other container engines. + :param image_ref: Image ref details + :type image_ref: Dict + """ + self._image_ref = image_ref + self._engine = image_ref.get("engine", None) + if not self._engine: + raise ImageRefFactoryError("Provide the container engine") + self._imageRef_switch = { + "docker": DockerImageRef + } + + def get_image(self): + image = self._imageRef_switch.get(self._engine, None) + if not image: + raise ImageRefFactoryError("Unsupported container engine: {}".format(self._engine)) + return image( + process=self._image_ref["process"], + digest=self._image_ref["digest"], + dx_file_id=self._image_ref.get("file_id", None), + repository=self._image_ref.get("repository", None), + image_name=self._image_ref.get("image_name", None), + tag=self._image_ref.get("tag", None) + ) + diff --git a/src/python/dxpy/nextflow/__init__.py b/src/python/dxpy/nextflow/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/python/dxpy/nextflow/awscli_assets.json b/src/python/dxpy/nextflow/awscli_assets.json new file mode 100644 index 0000000000..6b3b891efc --- /dev/null +++ b/src/python/dxpy/nextflow/awscli_assets.json @@ -0,0 +1,9 @@ +{ + "aws:ap-southeast-2": "record-Ggjx0YQ5qP61v7Q7Qq4QQ2f7", + "aws:eu-central-1": "record-Ggjx2Jj4jbKyKxGXYfZgpJYy", + "aws:eu-west-2-g": "record-Ggjx4qBKYFQ35Gp87ybKV28Z", + "aws:me-south-1": "record-GxFKpy135qFZjpBqPyPV0Z4F", + "aws:us-east-1": "record-GgjvvP00GFJ5JjG1gX3Jbx6j", + "azure:westeurope": "record-Ggjx8Z8BxzQqXY9Fv4vXBqpj", + "azure:westus": "record-GgjxGf89ZB67Bz7f3815qk11" +} diff --git a/src/python/dxpy/nextflow/awscli_assets.staging.json b/src/python/dxpy/nextflow/awscli_assets.staging.json new file mode 100644 index 0000000000..ec862e76d4 --- /dev/null +++ b/src/python/dxpy/nextflow/awscli_assets.staging.json @@ -0,0 +1,9 @@ +{ + "aws:ap-southeast-2": "record-Ggjp4Fj50y9Vp27Fx8gB65fY", + "aws:eu-central-1": "record-Ggjp62Q4PFG1K0g562PKZgbV", + "aws:eu-west-2-g": "record-Ggjp7K2K60b11G4p2X4X8B98", + "aws:me-south-1": "record-GxFJy0V3kVvxjKfZF7p3JbGV", + "aws:us-east-1": "record-Ggjp2p009010jB06F520vZQG", + "azure:westeurope": "record-GgjpB2QB7Vp4Kz8FV25Q68Jv", + "azure:westus": "record-GgjpJ509bvJ1p27Fx8gB65jg" +} diff --git a/src/python/dxpy/nextflow/collect_images.py b/src/python/dxpy/nextflow/collect_images.py new file mode 100644 index 0000000000..0505b83a15 --- /dev/null +++ b/src/python/dxpy/nextflow/collect_images.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright (C) 2013-2016 DNAnexus, Inc. +# +# This file is part of dx-toolkit (DNAnexus platform client libraries). +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy +# of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import json +import os.path +import subprocess +from dxpy.nextflow.ImageRefFactory import ImageRefFactory, ImageRefFactoryError + +CONTAINERS_JSON = "containers.json" + + +def bundle_docker_images(image_refs): + """ + :param image_refs: Image references extracted from run_nextaur_collect(). + :type image_refs: Dict + :returns: Array of dicts for bundledDepends attribute of the applet resources. Also saves images on the platform + if not done that before. + """ + image_factories = [ImageRefFactory(x) for x in image_refs] + images = [x.get_image() for x in image_factories] + seen_images = set() + bundled_depends = [] + for image in images: + if image.identifier in seen_images: + continue + else: + bundled_depends.append(image.bundled_depends.copy()) + seen_images.add(image.identifier) + return bundled_depends + + +def run_nextaur_collect(resources_dir, profile, nextflow_pipeline_params): + """ + :param resources_dir: URL to the local(ized) NF pipeline in the app(let) resources. + :type resources_dir: String + :param profile: Custom Nextflow profile. More profiles can be provided by using comma separated string (without whitespaces). + :type profile: str + :param nextflow_pipeline_params: Custom Nextflow pipeline parameters + :type nextflow_pipeline_params: string + :returns: Dict. Image references in the form of + "process": String. Name of the process/task + "repository": String. Repository (host) prefix + "image_name": String. Image base name + "tag": String. Version tag + "digest": String. Image digest + "file_id": String. File ID if found on the platform + "engine": String. Container engine. + Runs nextaur:collect + """ + base_cmd = "nextflow plugin nextaur:collect docker {}".format(resources_dir) + pipeline_params_arg = "pipelineParams={}".format(nextflow_pipeline_params) if nextflow_pipeline_params else "" + profile_arg = "profile={}".format(profile) if profile else "" + nextaur_cmd = " ".join([base_cmd, pipeline_params_arg, profile_arg]) + process = subprocess.run(nextaur_cmd, shell=True, capture_output=True, text=True) + if not os.path.exists(CONTAINERS_JSON): + raise ImageRefFactoryError(process.stdout) + with open(CONTAINERS_JSON, "r") as json_file: + image_refs = json.load(json_file).get("processes", None) + if not image_refs: + raise ImageRefFactoryError("Could not extract processes from nextaur:collect") + return image_refs diff --git a/src/python/dxpy/nextflow/nextaur_assets.json b/src/python/dxpy/nextflow/nextaur_assets.json new file mode 100755 index 0000000000..402b2eea2c --- /dev/null +++ b/src/python/dxpy/nextflow/nextaur_assets.json @@ -0,0 +1,9 @@ +{ + "aws:ap-southeast-2": "record-GpzQzk852Z6F46605Q45G2fk", + "aws:eu-central-1": "record-GpzV15j4Gp5b5GpyJ67GX24x", + "aws:eu-west-2-g": "record-GpzV2xXKqzvbxk4z6884j8jQ", + "aws:me-south-1": "record-GxFKf693XYGq0ZbVf1FjpjkP", + "aws:us-east-1": "record-GpzQxg00Zb6YVFZ1bBJ0QFQY", + "azure:westeurope": "record-GpzV6kQBGXPfFK2jBq1P5fqK", + "azure:westus": "record-GpzVFv894pgvz7qyjKY3GJbG" +} diff --git a/src/python/dxpy/nextflow/nextaur_assets.staging.json b/src/python/dxpy/nextflow/nextaur_assets.staging.json new file mode 100755 index 0000000000..9f7d9115d8 --- /dev/null +++ b/src/python/dxpy/nextflow/nextaur_assets.staging.json @@ -0,0 +1,9 @@ +{ + "aws:ap-southeast-2": "record-GpyvF1Q5Ky99FPzYYgZKK1VP", + "aws:eu-central-1": "record-GpyvGVj4gv75gPB1F7jP18kx", + "aws:eu-west-2-g": "record-GpyvJbBKVkbzYxqfQ409202F", + "aws:me-south-1": "record-GxFJbvk3jfbfyPj9VjK660qp", + "aws:us-east-1": "record-Gpyv8jj023vP2x068G1bZ7Zz", + "azure:westeurope": "record-GpyvQvQB69bFPJZFgY0J3807", + "azure:westus": "record-GpyvZ3Q9vf40pyGqgb5G7Kvp" +} diff --git a/src/python/dxpy/nextflow/nextflow_assets.json b/src/python/dxpy/nextflow/nextflow_assets.json new file mode 100755 index 0000000000..4a626e2434 --- /dev/null +++ b/src/python/dxpy/nextflow/nextflow_assets.json @@ -0,0 +1,9 @@ +{ + "aws:ap-southeast-2": "record-GbqXZx85YkJYkXbJGx2712B0", + "aws:eu-central-1": "record-GbqXbKQ4f06Fb6yFVJ2jf91z", + "aws:eu-west-2-g": "record-GbqXffpKbPkPGyqxBKj4bkGP", + "aws:me-south-1": "record-GxFKjXV3v1BFj0jJ8Bz53k5Q", + "aws:us-east-1": "record-GbqX3YQ0zF96QY6XkKgGj6zJ", + "azure:westeurope": "record-GbqXq50By9ZZF5JpzzvZVX3F", + "azure:westus": "record-GbqXy9Q91ggZQg52Z4Ffk84q" +} diff --git a/src/python/dxpy/nextflow/nextflow_assets.staging.json b/src/python/dxpy/nextflow/nextflow_assets.staging.json new file mode 100755 index 0000000000..01366e74f1 --- /dev/null +++ b/src/python/dxpy/nextflow/nextflow_assets.staging.json @@ -0,0 +1,9 @@ +{ + "aws:ap-southeast-2": "record-GbqVfV85P3j22G98Qk3Pbp54", + "aws:eu-central-1": "record-GbqVj684y262YXY4gkXP12YK", + "aws:eu-west-2-g": "record-GbqVkkpKqzF471k20pKZkpZJ", + "aws:me-south-1": "record-GxFJpgk3gX2Kf655z3KP1x80", + "aws:us-east-1": "record-GbqVBJ80g9g6vzxj51xYzYj6", + "azure:westeurope": "record-GbqVv98B1PXF5g7GGYVqfFjx", + "azure:westus": "record-GbqVz1Q9K7yp85ZVKQVbVgz7" +} diff --git a/src/python/dxpy/nextflow/nextflow_builder.py b/src/python/dxpy/nextflow/nextflow_builder.py new file mode 100644 index 0000000000..15a06f17bf --- /dev/null +++ b/src/python/dxpy/nextflow/nextflow_builder.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +import os +import dxpy +import json +import argparse +from glob import glob +import shutil +import tempfile + +from dxpy.nextflow.nextflow_templates import (get_nextflow_dxapp, get_nextflow_src) +from dxpy.nextflow.nextflow_utils import (get_template_dir, write_exec, write_dxapp, get_importer_name, create_readme) +from dxpy.cli.exec_io import parse_obj +from dxpy.cli import try_call +from dxpy.utils.resolver import resolve_existing_path + + +parser = argparse.ArgumentParser(description="Uploads a DNAnexus App.") + + +def build_pipeline_with_npi( + repository=None, + tag=None, + cache_docker=False, + docker_secrets=None, + nextflow_pipeline_params="", + profile="", + git_creds=None, + brief=False, + destination=None, + extra_args=None +): + """ + :param repository: URL to a Git repository + :type repository: string + :param tag: tag of a given Git repository. If it is not provided, the default branch is used. + :type tag: string + :param cache_docker: Pull a remote docker image and store it on the platform + :type cache_docker: bool + :param docker_secrets: Dx file id with the private docker registry credentials + :type docker_secrets: string + :param nextflow_pipeline_params: Custom Nextflow pipeline parameters + :type nextflow_pipeline_params: string + :param profile: Custom Nextflow profile, for more information visit https://www.nextflow.io/docs/latest/config.html#config-profiles + :type profile: string + :param brief: Level of verbosity + :type brief: boolean + :returns: ID of the created applet + + Runs the Nextflow Pipeline Importer app, which creates a Nextflow applet from a given Git repository. + """ + + def parse_extra_args(extra_args): + dx_input = {} + if extra_args.get("name") is not None: + dx_input["name"] = extra_args.get("name") + if extra_args.get("title") is not None: + dx_input["title"] = extra_args.get("title") + if extra_args.get("summary") is not None: + dx_input["summary"] = extra_args.get("summary") + if extra_args.get("runSpec", {}).get("timeoutPolicy") is not None: + dx_input["timeout_policy"] = extra_args.get("runSpec", {}).get("timeoutPolicy") + if extra_args.get("details", {}).get("whatsNew") is not None: + dx_input["whats_new"] = extra_args.get("details", {}).get("whatsNew") + return dx_input + + extra_args = extra_args or {} + build_project_id = dxpy.WORKSPACE_ID + build_folder = None + input_hash = parse_extra_args(extra_args) + input_hash["repository_url"] = repository + if tag: + input_hash["repository_tag"] = tag + if profile: + input_hash["config_profile"] = profile + if git_creds: + input_hash["github_credentials"] = parse_obj(git_creds, "file") + if destination: + build_project_id, build_folder, _ = try_call(resolve_existing_path, destination, expected='folder') + if docker_secrets: + input_hash["docker_secrets"] = parse_obj(docker_secrets, "file") + if cache_docker: + input_hash["cache_docker"] = cache_docker + if nextflow_pipeline_params: + input_hash["nextflow_pipeline_params"] = nextflow_pipeline_params + + if build_project_id is None: + parser.error( + "Can't create an applet without specifying a destination project; please use the -d/--destination flag to explicitly specify a project") + + nf_builder_job = dxpy.DXApp(name=get_importer_name()).run(app_input=input_hash, project=build_project_id, + folder=build_folder, + name="Nextflow build of %s" % (repository), detach=True) + + if not brief: + print("Started builder job %s" % (nf_builder_job.get_id(),)) + nf_builder_job.wait_on_done(interval=1) + applet_id, _ = dxpy.get_dxlink_ids(nf_builder_job.describe(fields={"output": True})['output']['output_applet']) + if not brief: + print("Created Nextflow pipeline %s" % (applet_id)) + else: + print(json.dumps(dict(id=applet_id))) + return applet_id + + +def prepare_nextflow( + resources_dir, + profile, + region, + cache_docker=False, + nextflow_pipeline_params="" +): + """ + :param resources_dir: Directory with all resources needed for the Nextflow pipeline. Usually directory with user's Nextflow files. + :type resources_dir: str or Path + :param profile: Custom Nextflow profile. More profiles can be provided by using comma separated string (without whitespaces). + :type profile: str + :param region: The region in which the applet will be built. + :type region: str + :param cache_docker: Perform pipeline analysis and cache the detected docker images on the platform + :type cache_docker: boolean + :param nextflow_pipeline_params: Custom Nextflow pipeline parameters + :type nextflow_pipeline_params: string + :returns: Path to the created dxapp_dir + :rtype: Path + + Creates files necessary for creating an applet on the Platform, such as dxapp.json and a source file. These files are created in '.dx.nextflow' directory. + """ + assert os.path.exists(resources_dir) + if not glob(os.path.join(resources_dir, "*.nf")): + raise dxpy.app_builder.AppBuilderException( + "Directory %s does not contain Nextflow file (*.nf): not a valid Nextflow directory" % resources_dir) + dxapp_dir = tempfile.mkdtemp(prefix=".dx.nextflow") + + custom_inputs = prepare_custom_inputs(schema_file=os.path.join(resources_dir, "nextflow_schema.json")) + dxapp_content = get_nextflow_dxapp( + custom_inputs=custom_inputs, + resources_dir=resources_dir, + region=region, + profile=profile, + cache_docker=cache_docker, + nextflow_pipeline_params=nextflow_pipeline_params + ) + exec_content = get_nextflow_src(custom_inputs=custom_inputs, profile=profile, resources_dir=resources_dir) + shutil.copytree(get_template_dir(), dxapp_dir, dirs_exist_ok=True) + write_dxapp(dxapp_dir, dxapp_content) + write_exec(dxapp_dir, exec_content) + create_readme(resources_dir, dxapp_dir) + return dxapp_dir + + +def prepare_custom_inputs(schema_file="./nextflow_schema.json"): + """ + :param schema_file: path to nextflow_schema.json file + :type schema_file: str or Path + :returns: list of custom inputs defined with DNAnexus datatype + :rtype: list + Creates custom input list from nextflow_schema.json that + will be added in dxapp.json inputSpec field + """ + + def get_dx_type(nf_type, nf_format=None): + types = { + "string": "string", + "integer": "int", + "number": "float", + "boolean": "boolean", + "object": "hash" + } + str_types = { + "file-path": "file", + "directory-path": "string", # So far we will stick with strings dx://... + "path": "string" + } + if nf_type == "string" and nf_format in str_types: + return str_types[nf_format] + elif nf_type in types: + return types[nf_type] + raise Exception("type {} is not supported by DNAnexus".format(nf_type)) + + inputs = [] + if not os.path.exists(schema_file): + return inputs + + with open(schema_file, "r") as fh: + schema = json.load(fh) + for d_key, d_schema in schema.get("definitions", {}).items(): + required_inputs = d_schema.get("required", []) + for property_key, property in d_schema.get("properties", {}).items(): + dx_input = {} + dx_input["name"] = property_key + dx_input["title"] = dx_input['name'] + if "default" in property: + dx_input["help"] = "Default value:{}\n".format(property.get("default", "")) + if "help_text" in property: + dx_input["help"] = dx_input.get("help", "") + property.get('help_text', "") + dx_input["hidden"] = property.get('hidden', False) + dx_input["class"] = get_dx_type(property.get("type"), property.get("format")) + dx_input["optional"] = True + if property_key not in required_inputs: + dx_input["help"] = "(Nextflow pipeline optional) {}".format(dx_input.get("help", "")) + inputs.append(dx_input) + else: + dx_input["help"] = "(Nextflow pipeline required) {}".format(dx_input.get("help", "")) + inputs.insert(0, dx_input) + + return inputs diff --git a/src/python/dxpy/nextflow/nextflow_templates.py b/src/python/dxpy/nextflow/nextflow_templates.py new file mode 100644 index 0000000000..8e4eb0d6fc --- /dev/null +++ b/src/python/dxpy/nextflow/nextflow_templates.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 + +from .nextflow_utils import (get_template_dir, get_source_file_name, get_resources_subpath, + is_importer_job, get_regional_options, get_resources_dir_name) +import json +import os +from dxpy import TOOLKIT_VERSION +from dxpy.compat import USING_PYTHON2, sys_encoding + + +def get_nextflow_dxapp( + custom_inputs=None, + resources_dir="", + region="aws:us-east-1", + profile="", + cache_docker=False, + nextflow_pipeline_params="" +): + """ + :param custom_inputs: Custom inputs that will be used in the created Nextflow pipeline. + :type custom_inputs: list + :param resources_dir: Directory with all resources needed for the Nextflow pipeline. Usually directory with user's Nextflow files. + :type resources_dir: str or Path + :param region: The name of the region in which the applet will be built. + :type region: str + :param profile: Custom Nextflow profile. More profiles can be provided by using comma separated string (without whitespaces). + :type profile: str + :param cache_docker: Perform pipeline analysis and cache the detected docker images on the platform + :type cache_docker: boolean + :param nextflow_pipeline_params: Custom Nextflow pipeline parameters + :type nextflow_pipeline_params: string + Creates Nextflow dxapp.json from the Nextflow dxapp.json template + """ + + if custom_inputs is None: + custom_inputs = [] + with open(os.path.join(str(get_template_dir()), 'dxapp.json'), 'r') as f: + dxapp = json.load(f) + dxapp["inputSpec"] = custom_inputs + dxapp["inputSpec"] + dxapp["runSpec"]["file"] = get_source_file_name() + + # By default, title and summary will be set to the pipeline name + name = get_resources_dir_name(resources_dir) + if name is None or name == "": + name = "Nextflow pipeline" + dxapp["name"] = name + dxapp["title"] = name + dxapp["summary"] = name + dxapp["regionalOptions"] = get_regional_options(region, resources_dir, profile, cache_docker, nextflow_pipeline_params) + + # Record dxpy version used for this Nextflow build + dxapp["details"]["dxpyBuildVersion"] = TOOLKIT_VERSION + if os.environ.get("DX_JOB_ID") is None or not is_importer_job(): + dxapp["details"]["repository"] = "local" + return dxapp + + +def get_nextflow_src(custom_inputs=None, profile=None, resources_dir=None): + """ + :param custom_inputs: Custom inputs (as configured in nextflow_schema.json) that will be used in created runtime configuration and runtime params argument + :type custom_inputs: list + :param profile: Custom Nextflow profile to be used when running a Nextflow pipeline, for more information visit https://www.nextflow.io/docs/latest/config.html#config-profiles + :type profile: string + :param resources_dir: Directory with all source files needed to build an applet. Can be an absolute or a relative path. + :type resources_dir: str or Path + :returns: String containing the whole source file of an applet. + :rtype: string + + Creates Nextflow source file from the Nextflow source file template + """ + if custom_inputs is None: + custom_inputs = [] + with open(os.path.join(str(get_template_dir()), get_source_file_name()), 'r') as f: + src = f.read() + + exclude_input_download = "" + applet_runtime_params = "" + for i in custom_inputs: + value = "${%s}" % (i['name']) + if i.get("class") == "file": + value = "dx://${DX_WORKSPACE_ID}:/$(echo ${%s} | jq .[$dnanexus_link] -r | xargs -I {} dx describe {} --json | jq -r .name)" % \ + i['name'] + exclude_input_download += "--except {} ".format(i['name']) + + # applet_runtime_inputs variable is initialized in the nextflow.sh script template + applet_runtime_params = applet_runtime_params + ''' + if [ -n "${}" ]; then + applet_runtime_inputs+=(--{} "{}") + fi + '''.format(i['name'], i['name'], value) + + profile_arg = "-profile {}".format(profile) if profile else "" + src = src.replace("@@APPLET_RUNTIME_PARAMS@@", applet_runtime_params) + src = src.replace("@@PROFILE_ARG@@", profile_arg) + src = src.replace("@@EXCLUDE_INPUT_DOWNLOAD@@", exclude_input_download) + src = src.replace("@@DXPY_BUILD_VERSION@@", TOOLKIT_VERSION) + if USING_PYTHON2: + src = src.replace("@@RESOURCES_SUBPATH@@", + get_resources_subpath(resources_dir).encode(sys_encoding)) + else: + src = src.replace("@@RESOURCES_SUBPATH@@", + get_resources_subpath(resources_dir)) + + return src diff --git a/src/python/dxpy/nextflow/nextflow_utils.py b/src/python/dxpy/nextflow/nextflow_utils.py new file mode 100644 index 0000000000..a9a577b764 --- /dev/null +++ b/src/python/dxpy/nextflow/nextflow_utils.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python3 +from os import path, makedirs, listdir +import re +import errno +import dxpy +import json +import shutil +from dxpy.exceptions import ResourceNotFound +from dxpy.nextflow.collect_images import run_nextaur_collect, bundle_docker_images + + +def get_source_file_name(): + return "src/nextflow.sh" + + +def get_resources_dir_name(resources_dir): + """ + :param resources_dir: Directory with all source files needed to build an applet. Can be an absolute or a relative path. + :type resources_dir: str or Path + :returns: The name of the folder + :rtype: str + """ + if resources_dir == None: + return '' + return path.basename(path.abspath(resources_dir)) + + +def get_resources_subpath(resources_dir): + return path.join("/home/dnanexus/", get_resources_dir_name(resources_dir)) + + +def get_importer_name(): + return "nextflow_pipeline_importer" + + +def get_template_dir(): + return path.join(path.dirname(dxpy.__file__), 'templating', 'templates', 'nextflow') + + +def is_importer_job(): + try: + with open("/home/dnanexus/dnanexus-job.json", "r") as f: + job_info = json.load(f) + return job_info.get("executableName") == get_importer_name() + except Exception: + return False + + +def write_exec(folder, content): + exec_file = "{}/{}".format(folder, get_source_file_name()) + try: + makedirs(path.dirname(path.abspath(exec_file))) + except OSError as e: + if e.errno != errno.EEXIST: + raise + pass + with open(exec_file, "w") as fh: + fh.write(content) + + +def find_readme(dir): + """ + Returns first readme (in alphabetical order) from a root of a given folder + :param dir: Directory in which we search for readme files + :type dir: str or Path + :returns: List[str] + """ + readme_pattern = re.compile(r"readme(\.(txt|md|rst|adoc|html|txt|asciidoc|org|text|textile|pod|wiki))?", re.IGNORECASE) + file_list = [f for f in listdir(dir) if path.isfile(path.join(dir, f))] + readme_files = [file for file in file_list if readme_pattern.match(file)] + readme_files.sort() + return readme_files[0] if readme_files else None + + +def create_readme(source_dir, destination_dir): + """ + :param destination_dir: Directory where readme is going to be created + :type destination_dir: str or Path + :param source_dir: Directory from which readme is going to be copied + :type source_dir: str or Path + :returns: None + """ + readme_file = find_readme(source_dir) + + if readme_file: + source_path = path.join(source_dir, readme_file) + destination_path = path.join(destination_dir, "Readme.md") + shutil.copy2(source_path, destination_path) + + +def write_dxapp(folder, content): + dxapp_file = "{}/dxapp.json".format(folder) + with open(dxapp_file, "w") as dxapp: + json.dump(content, dxapp) + + +def get_regional_options(region, resources_dir, profile, cache_docker, nextflow_pipeline_params): + nextaur_asset, nextflow_asset, awscli_asset = get_nextflow_assets(region) + regional_instance_type = get_instance_type(region) + if cache_docker: + image_refs = run_nextaur_collect(resources_dir, profile, nextflow_pipeline_params) + image_bundled = bundle_docker_images(image_refs) + else: + image_bundled = {} + regional_options = { + region: { + "systemRequirements": { + "*": { + "instanceType": regional_instance_type + } + }, + "assetDepends": [ + {"id": nextaur_asset}, + {"id": nextflow_asset}, + {"id": awscli_asset} + ], + "bundledDepends": image_bundled + } + } + return regional_options + + +def get_instance_type(region): + instance_type = { + "aws:ap-southeast-2": "mem2_ssd1_v2_x4", + "aws:eu-central-1": "mem2_ssd1_v2_x4", + "aws:us-east-1": "mem2_ssd1_v2_x4", + "aws:me-south-1": "mem2_ssd1_v2_x4", + "azure:westeurope": "azure:mem2_ssd1_x4", + "azure:westus": "azure:mem2_ssd1_x4", + "aws:eu-west-2-g": "mem2_ssd1_v2_x4" + }.get(region) + if not instance_type: + raise dxpy.exceptions.ResourceNotFound("Instance type is not specified for region {}.".format(region)) + return instance_type + + +def get_nextflow_assets(region): + nextflow_basepath = path.join(path.dirname(dxpy.__file__), 'nextflow') + # The order of assets in the tuple is: nextaur, nextflow + nextaur_assets = path.join(nextflow_basepath, "nextaur_assets.json") + nextflow_assets = path.join(nextflow_basepath, "nextflow_assets.json") + awscli_assets = path.join(nextflow_basepath, "awscli_assets.json") + try: + with open(nextaur_assets, 'r') as nextaur_f, open(nextflow_assets, 'r') as nextflow_f, open(awscli_assets, 'r') as awscli_f: + nextaur = json.load(nextaur_f)[region] + nextflow = json.load(nextflow_f)[region] + awscli = json.load(awscli_f)[region] + dxpy.describe(nextflow, fields={}) # existence check + return nextaur, nextflow, awscli + except ResourceNotFound: + nextaur_assets = path.join(nextflow_basepath, "nextaur_assets.staging.json") + nextflow_assets = path.join(nextflow_basepath, "nextflow_assets.staging.json") + awscli_assets = path.join(nextflow_basepath, "awscli_assets.staging.json") + + with open(nextaur_assets, 'r') as nextaur_f, open(nextflow_assets, 'r') as nextflow_f, open(awscli_assets, 'r') as awscli_f: + return json.load(nextaur_f)[region], json.load(nextflow_f)[region], json.load(awscli_f)[region] diff --git a/src/python/dxpy/packages/__init__.py b/src/python/dxpy/packages/__init__.py index 8c05f96a01..e69de29bb2 100644 --- a/src/python/dxpy/packages/__init__.py +++ b/src/python/dxpy/packages/__init__.py @@ -1,3 +0,0 @@ -import sys -import requests -sys.modules[__name__ + '.requests'] = sys.modules['requests'] diff --git a/src/python/dxpy/scripts/dx.py b/src/python/dxpy/scripts/dx.py index 5589e6cf14..423b225002 100644 --- a/src/python/dxpy/scripts/dx.py +++ b/src/python/dxpy/scripts/dx.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # coding: utf-8 # # Copyright (C) 2013-2016 DNAnexus, Inc. @@ -20,9 +20,7 @@ from __future__ import print_function, unicode_literals, division, absolute_import import os, sys, datetime, getpass, collections, re, json, argparse, copy, hashlib, io, time, subprocess, glob, logging, functools -import shlex # respects quoted substrings when splitting -import requests import csv logging.basicConfig(level=logging.INFO) @@ -36,25 +34,26 @@ import dxpy from dxpy.scripts import dx_build_app from dxpy import workflow_builder -from dxpy.exceptions import PermissionDenied +from dxpy.exceptions import PermissionDenied, InvalidState, ResourceNotFound from ..cli import try_call, prompt_for_yn, INTERACTIVE_CLI from ..cli import workflow as workflow_cli from ..cli.cp import cp +from ..cli.dataset_utilities import extract_dataset, extract_assay_germline, extract_assay_somatic, create_cohort, extract_assay_expression from ..cli.download import (download_one_file, download_one_database_file, download) -from ..cli.parsers import (no_color_arg, delim_arg, env_args, stdout_args, all_arg, json_arg, parser_dataobject_args, +from ..cli.parsers import (no_color_arg, delim_arg, env_args, stdout_args, all_arg, json_arg, try_arg, parser_dataobject_args, parser_single_dataobject_output_args, process_properties_args, find_by_properties_and_tags_args, process_find_by_property_args, process_dataobject_args, process_single_dataobject_output_args, find_executions_args, add_find_executions_search_gp, set_env_from_args, extra_args, process_extra_args, DXParserError, exec_input_args, - instance_type_arg, process_instance_type_arg, process_instance_count_arg, get_update_project_args, - property_args, tag_args, contains_phi, process_phi_param) + instance_type_arg, process_instance_type_arg, process_instance_type_by_executable_arg, process_instance_count_arg, get_update_project_args, + property_args, tag_args, contains_phi, process_phi_param, process_external_upload_restricted_param) from ..cli.exec_io import (ExecutableInputs, format_choices_or_suggestions) from ..cli.org import (get_org_invite_args, add_membership, remove_membership, update_membership, new_org, update_org, find_orgs, org_find_members, org_find_projects, org_find_apps) from ..exceptions import (err_exit, DXError, DXCLIError, DXAPIError, network_exceptions, default_expected_exceptions, format_exception) -from ..utils import warn, group_array_by_field, normalize_timedelta, normalize_time_input +from ..utils import warn, group_array_by_field, normalize_timedelta, normalize_time_input, merge from ..utils.batch_utils import (batch_run, batch_launch_args) from ..app_categories import APP_CATEGORIES @@ -62,7 +61,7 @@ DNANEXUS_X, set_colors, set_delimiter, get_delimiter, DELIMITER, fill, tty_rows, tty_cols, pager, format_find_results, nostderr) from ..utils.pretty_print import format_tree, format_table -from ..utils.resolver import (pick, paginate_and_pick, is_hashid, is_data_obj_id, is_container_id, is_job_id, +from ..utils.resolver import (clean_folder_path, pick, paginate_and_pick, is_hashid, is_data_obj_id, is_container_id, is_job_id, is_analysis_id, get_last_pos_of_char, resolve_container_id_or_name, resolve_path, resolve_existing_path, get_app_from_path, resolve_app, resolve_global_executable, get_exec_handler, split_unescaped, ResolutionError, resolve_to_objects_or_project, is_project_explicit, @@ -72,7 +71,11 @@ from ..utils.describe import (print_data_obj_desc, print_desc, print_ls_desc, get_ls_l_desc, print_ls_l_header, print_ls_l_desc, get_ls_l_desc_fields, get_io_desc, get_find_executions_string) from ..system_requirements import SystemRequirementsDict - +try: + from urllib.parse import urlparse +except: + # Python 2 + from urlparse import urlparse try: import colorama colorama.init() @@ -92,16 +95,14 @@ if 'TERM' in os.environ and os.environ['TERM'].startswith('xterm'): old_term_setting = os.environ['TERM'] os.environ['TERM'] = 'vt100' - # gnureadline required on macos try: + # Import gnureadline if installed for macOS import gnureadline as readline - except ImportError: + except ImportError as e: import readline if old_term_setting: os.environ['TERM'] = old_term_setting - if readline.__doc__ and 'libedit' in readline.__doc__: - print('Warning: incompatible readline module detected (libedit), tab completion disabled', file=sys.stderr) except ImportError: if os.name != 'nt': print('Warning: readline module is not available, tab completion disabled', file=sys.stderr) @@ -716,7 +717,7 @@ def cd(args): try: dxproj = dxpy.get_handler(dxpy.WORKSPACE_ID) - dxproj.list_folder(folder=folderpath) + dxproj.list_folder(folder=folderpath, only='folders') except: err_exit(fill(folderpath + ': No such file or directory found in project ' + dxpy.WORKSPACE_ID), 3) @@ -1036,7 +1037,8 @@ def mv(args): if src_results[0]['describe']['folder'] != dest_folder: dxpy.api.project_move(src_proj, {"objects": [result['id'] for result in src_results], - "destination": dest_folder}) + "destination": dest_folder, + "targetFileRelocation": args.target_file_relocation}) for result in src_results: dxpy.DXHTTPRequest('/' + result['id'] + '/rename', {"project": src_proj, @@ -1064,7 +1066,8 @@ def mv(args): dxpy.api.project_move(src_proj, {"objects": src_objects, "folders": src_folders, - "destination": dest_path}) + "destination": dest_path, + "targetFileRelocation": args.target_file_relocation}) except: err_exit() @@ -1137,9 +1140,10 @@ def describe_global_executable(json_output, args, exec_type): print(get_result_str()) print_desc(desc, args.verbose) found_match = True - except dxpy.DXAPIError as details: - if details.code != requests.codes.not_found: - raise + except dxpy.DXAPIError as e: + if not isinstance(e, ResourceNotFound): + raise e + return found_match def find_global_executable(json_output, args): @@ -1176,22 +1180,48 @@ def append_to_output_json_and_print(result): # Attempt to resolve name # First, if it looks like a hash id, do that. json_input = {} + extra_fields = [] json_input["properties"] = True if args.name and (args.verbose or args.details or args.json): raise DXCLIError('Cannot request --name in addition to one of --verbose, --details, or --json') # Always retrieve details too (just maybe don't render them) json_input["details"] = True if is_data_obj_id(args.path): - # Should prefer the current project's version if possible + # Prefer the current project's version if possible if dxpy.WORKSPACE_ID is not None: try: - # But only put it in the JSON if you still have - # access. - dxpy.api.project_list_folder(dxpy.WORKSPACE_ID) + # But only put it in the JSON if you still have access + dxpy.api.project_describe(dxpy.WORKSPACE_ID, input_params={"fields": {"id": True}}) json_input['project'] = dxpy.WORKSPACE_ID - except dxpy.DXAPIError as details: - if details.code != requests.codes.not_found: - raise + except dxpy.DXAPIError as e: + if not isinstance(e, ResourceNotFound): + raise e + + if is_job_id(args.path): + extra_fields.append('spotCostSavings') + if args.verbose: + extra_fields.append('internetUsageIPs') + extra_fields.append('runSystemRequirements') + extra_fields.append('runSystemRequirementsByExecutable') + extra_fields.append('mergedSystemRequirementsByExecutable') + extra_fields.append('jobLogsForwardingStatus') + + if is_analysis_id(args.path): + extra_fields.append('spotCostSavings') + if args.verbose: + extra_fields.append('runSystemRequirements') + extra_fields.append('runSystemRequirementsByExecutable') + extra_fields.append('mergedSystemRequirementsByExecutable') + extra_fields.append('runStageSystemRequirements') + + if len(extra_fields) > 0: + json_input['defaultFields'] = True + json_input['fields'] = {field: True for field in extra_fields} + + if args.job_try is not None: + if not is_job_id(args.path): + err_exit('Parameter --try T can be used only when describing jobs') + json_input['try'] = args.job_try # Otherwise, attempt to look for it as a data object or # execution @@ -1237,9 +1267,9 @@ def append_to_output_json_and_print(result): else: print(get_result_str()) print_desc(desc, args.verbose) - except dxpy.DXAPIError as details: - if details.code != requests.codes.not_found: - raise + except dxpy.DXAPIError as e: + if not isinstance(e, ResourceNotFound): + raise e elif is_container_id(args.path): try: desc = dxpy.api.project_describe(args.path, json_input) @@ -1251,15 +1281,18 @@ def append_to_output_json_and_print(result): else: print(get_result_str()) print_desc(desc, args.verbose) - except dxpy.DXAPIError as details: - if details.code != requests.codes.not_found: - raise + except dxpy.DXAPIError as e: + if not isinstance(e, ResourceNotFound): + raise e # Found data object or is an id if entity_results is not None: if len(entity_results) > 0: found_match = True for result in entity_results: + if is_analysis_id(result['id']) and args.verbose: + default_analysis_desc = dxpy.DXAnalysis(result['id']).describe() + result['describe'].update(default_analysis_desc) if args.json: json_output.append(result['describe']) elif args.name: @@ -1290,9 +1323,9 @@ def append_to_output_json_and_print(result): else: print(get_result_str()) print_desc(desc, args.verbose) - except dxpy.DXAPIError as details: - if details.code != requests.codes.not_found: - raise + except dxpy.DXAPIError as e: + if not isinstance(e, ResourceNotFound): + raise e elif args.path.startswith('org-') or args.path.startswith('team-'): # Org or team try: @@ -1305,9 +1338,9 @@ def append_to_output_json_and_print(result): else: print(get_result_str()) print_desc(desc, args.verbose) - except dxpy.DXAPIError as details: - if details.code != requests.codes.not_found: - raise + except dxpy.DXAPIError as e: + if not isinstance(e, ResourceNotFound): + raise e if args.json: if args.multi: @@ -1356,6 +1389,8 @@ def _get_user_new_args(args): user_new_args["occupation"] = args.occupation if args.set_bill_to is True: user_new_args["billTo"] = args.org + if args.on_behalf_of is not None: + user_new_args["provisioningOrg"] = args.on_behalf_of return user_new_args @@ -1399,7 +1434,18 @@ def new_project(args): inputs["containsPHI"] = True if args.database_ui_view_only: inputs["databaseUIViewOnly"] = True - + if args.database_results_restricted is not None: + inputs["databaseResultsRestricted"] = args.database_results_restricted + if args.monthly_compute_limit is not None: + inputs["monthlyComputeLimit"] = args.monthly_compute_limit + if args.monthly_egress_bytes_limit is not None: + inputs["monthlyEgressBytesLimit"] = args.monthly_egress_bytes_limit + if args.monthly_storage_limit is not None: + inputs["monthlyStorageLimit"] = args.monthly_storage_limit + if args.default_symlink is not None: + inputs["defaultSymlink"] = json.loads(args.default_symlink) + if args.drive is not None: + inputs["drive"] = args.drive try: resp = dxpy.api.project_new(inputs) if args.brief: @@ -1471,14 +1517,18 @@ def set_visibility(args): def get_details(args): # Attempt to resolve name - _project, _folderpath, entity_result = try_call(resolve_existing_path, + project, _folderpath, entity_result = try_call(resolve_existing_path, args.path, expected='entity') if entity_result is None: err_exit(fill('Could not resolve "' + args.path + '" to a name or ID'), 3) + + input_params = {} + if project is not None: + input_params['project'] = project try: - print(json.dumps(dxpy.DXHTTPRequest('/' + entity_result['id'] + '/getDetails', {}), indent=4)) + print(json.dumps(dxpy.DXHTTPRequest('/' + entity_result['id'] + '/getDetails', input_params), indent=4)) except: err_exit() @@ -1578,11 +1628,16 @@ def add_tags(args): args.all) if entity_results is not None: + payload = {"project": project, "tags": args.tags} + + if args.job_try is not None and any(map(lambda x: not is_job_id(x['id']), entity_results)): + err_exit('Parameter --try T can be used only with jobs') + for result in entity_results: + if args.job_try is not None: + payload['try'] = args.job_try try: - dxpy.DXHTTPRequest('/' + result['id'] + '/addTags', - {"project": project, - "tags": args.tags}) + dxpy.DXHTTPRequest('/' + result['id'] + '/addTags', payload) except (dxpy.DXAPIError,) + network_exceptions as details: print(format_exception(details), file=sys.stderr) had_error = True @@ -1591,6 +1646,9 @@ def add_tags(args): elif not project.startswith('project-'): err_exit('Cannot add tags to a non-project data container', 3) else: + if args.job_try is not None: + err_exit('Parameter --try T can be used only with jobs') + try: dxpy.DXHTTPRequest('/' + project + '/addTags', {"tags": args.tags}) @@ -1605,11 +1663,16 @@ def remove_tags(args): args.all) if entity_results is not None: + payload = {"project": project, "tags": args.tags} + + if args.job_try is not None and any(map(lambda x: not is_job_id(x['id']), entity_results)): + err_exit('Parameter --try T can be used only with jobs') + for result in entity_results: + if args.job_try is not None: + payload['try'] = args.job_try try: - dxpy.DXHTTPRequest('/' + result['id'] + '/removeTags', - {"project": project, - "tags": args.tags}) + dxpy.DXHTTPRequest('/' + result['id'] + '/removeTags', payload) except (dxpy.DXAPIError,) + network_exceptions as details: print(format_exception(details), file=sys.stderr) had_error = True @@ -1618,6 +1681,9 @@ def remove_tags(args): elif not project.startswith('project-'): err_exit('Cannot remove tags from a non-project data container', 3) else: + if args.job_try is not None: + err_exit('Parameter --try T can be used only with jobs') + try: dxpy.DXHTTPRequest('/' + project + '/removeTags', {"tags": args.tags}) @@ -1659,11 +1725,16 @@ def set_properties(args): try_call(process_properties_args, args) if entity_results is not None: + payload = {"project": project, "properties": args.properties} + + if args.job_try is not None and any(map(lambda x: not is_job_id(x['id']), entity_results)): + err_exit('Parameter --try T can be used only with jobs') + for result in entity_results: + if args.job_try is not None: + payload['try'] = args.job_try try: - dxpy.DXHTTPRequest('/' + result['id'] + '/setProperties', - {"project": project, - "properties": args.properties}) + dxpy.DXHTTPRequest('/' + result['id'] + '/setProperties', payload) except (dxpy.DXAPIError,) + network_exceptions as details: print(format_exception(details), file=sys.stderr) had_error = True @@ -1672,6 +1743,8 @@ def set_properties(args): elif not project.startswith('project-'): err_exit('Cannot set properties on a non-project data container', 3) else: + if args.job_try is not None: + err_exit('Parameter --try T can be used only with jobs') try: dxpy.api.project_set_properties(project, {"properties": args.properties}) except: @@ -1687,11 +1760,16 @@ def unset_properties(args): for prop in args.properties: properties[prop] = None if entity_results is not None: + payload = {"project": project, "properties": properties} + + if args.job_try is not None and any(map(lambda x: not is_job_id(x['id']), entity_results)): + err_exit('Parameter --try T can be used only with jobs') + for result in entity_results: + if args.job_try is not None: + payload['try'] = args.job_try try: - dxpy.DXHTTPRequest('/' + result['id'] + '/setProperties', - {"project": project, - "properties": properties}) + dxpy.DXHTTPRequest('/' + result['id'] + '/setProperties', payload) except (dxpy.DXAPIError,) + network_exceptions as details: print(format_exception(details), file=sys.stderr) had_error = True @@ -1700,6 +1778,8 @@ def unset_properties(args): elif not project.startswith('project-'): err_exit('Cannot unset properties on a non-project data container', 3) else: + if args.job_try is not None: + err_exit('Parameter --try T can be used only with jobs') try: dxpy.api.project_set_properties(project, {"properties": properties}) except: @@ -2093,8 +2173,26 @@ def find_executions(args): origin = None more_results = False include_io = (args.verbose and args.json) or args.show_outputs + include_internet_usage_ips = args.verbose and args.json + include_restarted = args.include_restarted + if args.classname == 'job': + describe_args = { + "defaultFields": True, + "fields": { + "runInput": include_io, + "originalInput": include_io, + "input": include_io, + "output": include_io, + "internetUsageIPs": include_internet_usage_ips + } + } + else: + describe_args = {"io": include_io} id_desc = None + # DEVEX-2277: Increase limit by max number of retries (10) for resorting + jobs_to_fetch = args.num_results if args.trees else args.num_results + 10 + # Now start parsing flags if args.id is not None: id_desc = try_call(dxpy.api.job_describe, args.id, {"io": False}) @@ -2123,7 +2221,7 @@ def find_executions(args): 'state': args.state, 'origin_job': origin, 'parent_job': "none" if args.origin_jobs else args.parent, - 'describe': {"io": include_io}, + 'describe': describe_args, 'created_after': args.created_after, 'created_before': args.created_before, 'name': args.name, @@ -2131,46 +2229,99 @@ def find_executions(args): 'tags': args.tag, 'properties': args.properties, 'include_subjobs': False if args.no_subjobs else True, - 'root_execution': args.root_execution} - if args.num_results < 1000 and not args.trees: - query['limit'] = args.num_results + 1 + 'root_execution': args.root_execution, + 'include_restarted': include_restarted} + if jobs_to_fetch < 1000 and not args.trees: + query['limit'] = jobs_to_fetch + 1 json_output = [] # for args.json - def build_tree(root, executions_by_parent, execution_descriptions, is_cached_result=False): + class ExecutionId: + + def __init__(self, id, try_num=None): + self.id = id + self.try_num = try_num if try_num is not None else -1 + + def __eq__(self, other): + if not isinstance(other, ExecutionId): + return NotImplemented + + return self.id == other.id and self.try_num == other.try_num + + def __hash__(self): + return hash((self.id, self.try_num)) + + def __str__(self): + return "%s_%d" % (self.id, self.try_num) + + def __repr__(self): + return "ExecutionId(execution_id='%s',retry_num=%d)" % (self.id, self.try_num) + + def print_brief(job_id, job_try, has_retries): + print(job_id + (" try %d" % job_try if has_retries and include_restarted and job_try is not None else "")) + + def build_tree(root, root_try, executions_by_parent, execution_descriptions, execution_retries): tree, root_string = {}, '' + # When try is not explicitly specified, use the most recent try + execution_id = ExecutionId(root, root_try if root_try is not None else execution_retries[root][0]) + root_has_retries = len(execution_retries[root]) > 1 + root_has_children = execution_id in executions_by_parent + root_has_reused_output = execution_descriptions[execution_id].get('outputReusedFrom') is not None + + if root_try is None: + if root_has_retries: + if not args.json and not args.brief: + root_string = get_find_executions_string(execution_descriptions[execution_id], + has_children=root_has_children, + show_outputs=args.show_outputs, + is_cached_result=root_has_reused_output, + show_try=include_restarted, + as_try_group_root=True) + tree[root_string] = collections.OrderedDict() + for rtry in execution_retries[root]: + subtree, _ = build_tree(root, rtry, executions_by_parent, execution_descriptions, execution_retries) + if tree: + tree[root_string].update(subtree) + return tree, root_string + else: + return build_tree(root, execution_retries[root][0], executions_by_parent, execution_descriptions, execution_retries) + if args.json: - json_output.append(execution_descriptions[root]) + json_output.append(execution_descriptions[execution_id]) elif args.brief: - print(root) + print_brief(root, root_try, root_has_retries) else: - root_string = get_find_executions_string(execution_descriptions[root], - has_children=root in executions_by_parent, + root_string = get_find_executions_string(execution_descriptions[execution_id], + has_children=root_has_children, show_outputs=args.show_outputs, - is_cached_result=is_cached_result) + is_cached_result=root_has_reused_output, + show_try=include_restarted and root_has_retries) tree[root_string] = collections.OrderedDict() - for child_execution in executions_by_parent.get(root, {}): - child_is_cached_result = is_cached_result or (execution_descriptions[child_execution].get('outputReusedFrom') is not None) + for child_execution in executions_by_parent.get(execution_id, {}): subtree, _subtree_root = build_tree(child_execution, + execution_retries[child_execution][0] if len(execution_retries[child_execution]) == 1 else None, executions_by_parent, execution_descriptions, - is_cached_result=child_is_cached_result) + execution_retries) if tree: tree[root_string].update(subtree) + return tree, root_string - def process_tree(result, executions_by_parent, execution_descriptions): - is_cached_result = False - if 'outputReusedFrom' in result and result['outputReusedFrom'] is not None: - is_cached_result = True - tree, root = build_tree(result['id'], executions_by_parent, execution_descriptions, is_cached_result) + def process_tree(root_id, executions_by_parent, execution_descriptions, executions_retries): + tree, root = build_tree(root_id, None, executions_by_parent, execution_descriptions, executions_retries) if tree: print(format_tree(tree[root], root)) try: num_processed_results = 0 roots = collections.OrderedDict() + execution_retries = collections.defaultdict(set) + executions_cache = [] + for execution_result in dxpy.find_executions(**query): + execution_id = ExecutionId(execution_result['id'], execution_result['describe'].get('try')) + if args.trees: if args.classname == 'job': root = execution_result['describe']['originJob'] @@ -2180,33 +2331,52 @@ def process_tree(result, executions_by_parent, execution_descriptions): num_processed_results += 1 else: num_processed_results += 1 + executions_cache.append(execution_result) - if (num_processed_results > args.num_results): + execution_retries[execution_id.id].add(execution_id.try_num) + + if num_processed_results > jobs_to_fetch: more_results = True break - if args.json: - json_output.append(execution_result['describe']) - elif args.trees: + if args.trees: roots[root] = root if args.classname == 'analysis' and root.startswith('job-'): # Analyses in trees with jobs at their root found in "dx find analyses" are displayed unrooted, # and only the last analysis found is displayed. roots[root] = execution_result['describe']['id'] - elif args.brief: - print(execution_result['id']) - elif not args.trees: - print(format_tree({}, get_find_executions_string(execution_result['describe'], - has_children=False, - single_result=True, - show_outputs=args.show_outputs))) - if args.trees: + + if not args.trees: + # Handle situations where the number of results is between args.num_results and args.num_results + 10 + if len(executions_cache) > args.num_results: + more_results = True + + executions = sorted(executions_cache, key=lambda x: (-x['describe']['created'], -x['describe'].get('try', 0)))[:args.num_results] + + for execution_result in executions: + execution_id = execution_result['id'] + execution_try = execution_result['describe'].get('try') + execution_max_try = max(execution_retries[execution_id]) + show_try = include_restarted and execution_max_try > 0 + + if args.json: + json_output.append(execution_result['describe']) + elif args.brief: + print_brief(execution_id, execution_try, show_try) + else: + print(format_tree({}, get_find_executions_string(execution_result['describe'], + has_children=False, + single_result=True, + show_outputs=args.show_outputs, + show_try=show_try))) + else: executions_by_parent, descriptions = collections.defaultdict(list), {} root_field = 'origin_job' if args.classname == 'job' else 'root_execution' parent_field = 'masterJob' if args.no_subjobs else 'parentJob' query = {'classname': args.classname, - 'describe': {"io": include_io}, + 'describe': describe_args, 'include_subjobs': False if args.no_subjobs else True, + 'include_restarted': include_restarted, root_field: list(roots.keys())} if not args.all_projects: # If the query doesn't specify a project, the server finds all projects to which the user has explicit @@ -2221,20 +2391,30 @@ def process_tree(result, executions_by_parent, execution_descriptions): def process_execution_result(execution_result): execution_desc = execution_result['describe'] - parent = execution_desc.get(parent_field) or execution_desc.get('parentAnalysis') - descriptions[execution_result['id']] = execution_desc - if parent: - executions_by_parent[parent].append(execution_result['id']) + execution_id = ExecutionId(execution_result['id'], execution_desc.get('try')) + + if execution_desc.get(parent_field) or execution_desc.get('parentAnalysis'): + if parent_field == 'parentJob' and execution_desc.get(parent_field): + parent = ExecutionId(execution_desc.get(parent_field), execution_desc.get('parentJobTry')) + else: + parent = ExecutionId(execution_desc.get(parent_field) or execution_desc.get('parentAnalysis')) + if execution_id.id not in executions_by_parent[parent]: + executions_by_parent[parent].append(execution_id.id) + + descriptions[execution_id] = execution_desc + execution_retries[execution_id.id].add(execution_id.try_num) # If an analysis with cached children, also insert those if execution_desc['class'] == 'analysis': for stage_desc in execution_desc['stages']: if 'parentAnalysis' in stage_desc['execution'] and stage_desc['execution']['parentAnalysis'] != execution_result['id'] and \ (args.classname != 'analysis' or stage_desc['execution']['class'] == 'analysis'): - # this is a cached stage (with a different parent) - executions_by_parent[execution_result['id']].append(stage_desc['execution']['id']) - if stage_desc['execution']['id'] not in descriptions: - descriptions[stage_desc['execution']['id']] = stage_desc['execution'] + stage_execution_id = stage_desc['execution']['id'] + if stage_execution_id not in executions_by_parent[execution_id.id]: + # this is a cached stage (with a different parent) + executions_by_parent[execution_id.id].append(stage_execution_id) + if stage_execution_id not in descriptions: + descriptions[stage_execution_id] = stage_desc['execution'] # Short-circuit the find_execution API call(s) if there are # no root executions (and therefore we would have gotten 0 @@ -2243,11 +2423,17 @@ def process_execution_result(execution_result): for execution_result in dxpy.find_executions(**query): process_execution_result(execution_result) + # ensure tries are sorted from newest to oldest + execution_retries = {k: sorted(v, reverse=True) for k, v in execution_retries.items()} + # ensure roots are sorted by their creation time - sorted_roots = sorted(roots, key=lambda root: -descriptions[roots[root]]['created']) + sorted_roots = sorted( + roots.keys(), + key=lambda root: -descriptions[ExecutionId(roots[root], execution_retries[roots[root]][-1])]['created'] + ) for root in sorted_roots: - process_tree(descriptions[roots[root]], executions_by_parent, descriptions) + process_tree(roots[root], executions_by_parent, descriptions, execution_retries) if args.json: print(json.dumps(json_output, indent=4)) @@ -2297,7 +2483,7 @@ def find_data(args): visibility=args.visibility, properties=args.properties, name=args.name, - name_mode='glob', + name_mode=args.name_mode, typename=args.type, tags=args.tag, link=args.link, project=args.project, @@ -2329,6 +2515,8 @@ def find_data(args): def find_projects(args): try_call(process_find_by_property_args, args) try_call(process_phi_param, args) + try_call(process_external_upload_restricted_param, args) + try: results = dxpy.find_projects(name=args.name, name_mode='glob', properties=args.properties, tags=args.tag, @@ -2339,7 +2527,8 @@ def find_projects(args): created_after=args.created_after, created_before=args.created_before, region=args.region, - containsPHI=args.containsPHI) + containsPHI=args.containsPHI, + externalUploadRestricted=args.external_upload_restricted) except: err_exit() format_find_results(args, results) @@ -2524,26 +2713,57 @@ def wait(args): def build(args): sys.argv = ['dx build'] + sys.argv[2:] + def get_source_exec_desc(source_exec_path): + """ + Return source executable description when --from option is used + + Accecptable format of source_exec_path: + - applet-ID/workflow-ID + - project-ID-or-name:applet-ID/workflow-ID + - project-ID-or-name:folder/path/to/exec-name + where exec-name must be the name of only one applet or workflow + + :param source_exec_path: applet/workflow path given using --from + :type source_exec_path: string + :return: applet/workflow description + :rtype: dict + """ + exec_describe_fields={'fields':{"properties":True, "details":True},'defaultFields':True} + _, _, exec_result = try_call(resolve_existing_path, + source_exec_path, + expected='entity', + ask_to_resolve=False, + expected_classes=["applet", "workflow"], + all_mult=False, + allow_mult=False, + describe=exec_describe_fields) + + if exec_result is None: + err_exit('Could not resolve {} to an existing applet or workflow.'.format(source_exec_path), 3) + elif len(exec_result)>1: + err_exit('More than one match found for {}. Please use an applet/workflow ID instead.'.format(source_exec_path), 3) + else: + if exec_result[0]["id"].startswith("applet") or exec_result[0]["id"].startswith("workflow"): + return exec_result[0]["describe"] + else: + err_exit('Could not resolve {} to a valid applet/workflow ID'.format(source_exec_path), 3) + def get_mode(args): """ Returns an applet or a workflow mode based on whether the source directory contains dxapp.json or dxworkflow.json. If --from option is used, it will set it to: - app if --from=applet-xxxx - globalworkflow if --from=workflow-xxxx + app if --from has been resolved to applet-xxxx + globalworkflow if --from has been resolved to workflow-xxxx Note: dictionaries of regional options that can replace optionally ID strings will be supported in the future """ if args._from is not None: - if not is_hashid(args._from): - build_parser.error('--from option only accepts a DNAnexus applet ID') - if args._from.startswith("applet"): + if args._from["id"].startswith("applet"): return "app" - elif args._from.startswith("workflow"): - build_parser.error('--from option with a workflow is not supported') - else: - build_parser.error('--from option only accepts a DNAnexus applet ID') + elif args._from["id"].startswith("workflow"): + return "globalworkflow" if not os.path.isdir(args.src_dir): parser.error("{} is not a directory".format(args.src_dir)) @@ -2573,6 +2793,9 @@ def handle_arg_conflicts(args): if args.mode == "app" and args.destination != '.': build_parser.error("--destination cannot be used when creating an app (only an applet)") + if args.mode == "globalworkflow" and args.destination != '.': + build_parser.error("--destination cannot be used when creating a global workflow (only a workflow)") + if args.mode == "applet" and args.region: build_parser.error("--region cannot be used when creating an applet (only an app)") @@ -2587,9 +2810,6 @@ def handle_arg_conflicts(args): # conflicts and incompatibilities with --from - if args._from is not None and args.region: - build_parser.error("Options --from and --region cannot be specified together. The app will be enabled only in the region of the project in which the applet is stored") - if args._from is not None and args.ensure_upload: build_parser.error("Options --from and --ensure-upload cannot be specified together") @@ -2599,23 +2819,20 @@ def handle_arg_conflicts(args): if args._from is not None and args.remote: build_parser.error("Options --from and --remote cannot be specified together") - if args._from is not None and not args.dx_toolkit_autodep: - build_parser.error("Options --from and --no-dx-toolkit-autodep cannot be specified together") - if args._from is not None and not args.parallel_build: build_parser.error("Options --from and --no-parallel-build cannot be specified together") - if args._from is not None and args.mode == "globalworkflow": - build_parser.error("building a global workflow using --from is not supported") + if args._from is not None and (args.mode != "app" and args.mode != "globalworkflow"): + build_parser.error("--from can only be used to build an app from an applet or a global workflow from a project-based workflow") - if args._from is not None and args.mode != "app": - build_parser.error("--from can only be used to build an app from an applet") + if args._from is not None and not args.version_override: + build_parser.error("--version must be specified when using the --from option") - if args.mode == "app" and args._from is not None and not args._from.startswith("applet"): + if args.mode == "app" and args._from is not None and not args._from["id"].startswith("applet"): build_parser.error("app can only be built from an applet (--from should be set to an applet ID)") - if args.mode == "app" and args._from is not None and not args.version_override: - build_parser.error("--version must be specified when using the --from option") + if args.mode == "globalworkflow" and args._from is not None and not args._from["id"].startswith("workflow"): + build_parser.error("globalworkflow can only be built from an workflow (--from should be set to a workflow ID)") if args._from and args.dry_run: build_parser.error("Options --dry-run and --from cannot be specified together") @@ -2623,6 +2840,34 @@ def handle_arg_conflicts(args): if args.mode in ("globalworkflow", "applet", "app") and args.keep_open: build_parser.error("Global workflows, applets and apps cannot be kept open") + if args.repository and not args.nextflow: + build_parser.error("Repository argument is available only when building a Nextflow pipeline. Did you mean 'dx build --nextflow'?") + + if args.repository and args.remote: + build_parser.error("Nextflow pipeline built from a remote Git repository is always built using the Nextflow Pipeline Importer app. This is not compatible with --remote.") + + if args.cache_docker and args.remote: + build_parser.error("Nextflow pipeline built with an option to cache the docker images is always built using the Nextflow Pipeline Importer app. This is not compatible with --remote.") + + if args.git_credentials and not args.repository: + build_parser.error("Git credentials can be supplied only when building Nextflow pipeline from a Git repository.") + + if args.nextflow and args.mode == "app": + build_parser.error("Building Nextflow apps is not supported. Build applet instead.") + + if args.cache_docker and not args.nextflow: + build_parser.error( + "Docker caching argument is available only when building a Nextflow pipeline. Did you mean 'dx build --nextflow'?") + + if args.cache_docker: + logging.warning( + "WARNING: Caching the docker images (--cache-docker) makes you responsible for honoring the " + "Intellectual Property agreements of the software within the Docker container. You are also " + "responsible for remediating the security vulnerabilities of the Docker images of the pipeline." + "Also, cached images will be accessible by the users with VIEW permissions to the projects where the " + "cached images will be stored." + ) + # options not supported by workflow building if args.mode == "workflow": @@ -2640,7 +2885,6 @@ def handle_arg_conflicts(args): # True by default and will be currently silently ignored #'--[no-]watch': args.watch, #'--parallel-build': args.parallel_build, - #'--dx-toolkit[-stable][-legacy-git]-autodep': args.dx_toolkit_autodep, #'--[no]version-autonumbering': args.version_autonumbering, #'--[no]update': args.update, '--region': args.region, @@ -2658,12 +2902,14 @@ def handle_arg_conflicts(args): try: args.src_dir = get_validated_source_dir(args) - # If mode is not specified, determine it by the json file + if args._from is not None: + args._from = get_source_exec_desc(args._from) + + # If mode is not specified, determine it by the json file or by --from if args.mode is None: args.mode = get_mode(args) handle_arg_conflicts(args) - if args.mode in ("app", "applet"): dx_build_app.build(args) elif args.mode in ("workflow", "globalworkflow"): @@ -2749,8 +2995,28 @@ def render_timestamp(epochSeconds): def list_database_files(args): try: + # check if database was given as an object hash id + if is_hashid(args.database): + desc = dxpy.api.database_describe(args.database) + entity_result = {"id": desc["id"], "describe": desc} + else: + # otherwise it was provided as a path, so try and resolve + project, _folderpath, entity_result = try_call(resolve_existing_path, + args.database, + expected='entity') + + # if we couldn't resolved the entity, fail + if entity_result is None: + err_exit('Could not resolve ' + args.database + ' to a data object', 3) + else: + # else check and verify that the found entity is a database object + entity_result_class = entity_result['describe']['class'] + if entity_result_class != 'database': + err_exit('Error: The given object is of class ' + entity_result_class + + ' but an object of class database was expected', 3) + results = dxpy.api.database_list_folder( - args.database, + entity_result['id'], input_params={"folder": args.folder, "recurse": args.recurse, "timeout": args.timeout}) for r in results["results"]: date_str = render_timestamp(r["modified"]) if r["modified"] != 0 else '' @@ -2814,7 +3080,7 @@ def _get_input_for_run(args, executable, preset_inputs=None, input_name_prefix=N if args.input_json is None and args.filename is None: # --input-json and --input-json-file completely override input # from the cloned job - exec_inputs.update(args.input_from_clone, strip_prefix=False) + exec_inputs.update(args.cloned_job_desc.get("runInput", {}), strip_prefix=False) # Update with inputs passed to the this function if preset_inputs is not None: @@ -2924,29 +3190,91 @@ def run_batch_all_steps(args, executable, dest_proj, dest_path, input_json, run_ def run_body(args, executable, dest_proj, dest_path, preset_inputs=None, input_name_prefix=None): input_json = _get_input_for_run(args, executable, preset_inputs) - if args.sys_reqs_from_clone and not isinstance(args.instance_type, str): - args.instance_type = dict({stage: reqs['instanceType'] for stage, reqs in list(args.sys_reqs_from_clone.items())}, - **(args.instance_type or {})) + requested_instance_type, requested_cluster_spec, requested_system_requirements_by_executable = {}, {}, {} + executable_describe = None - if args.sys_reqs_from_clone and not isinstance(args.instance_count, str): - # extract instance counts from cloned sys reqs and override them with args provided with "dx run" - args.instance_count = dict({fn: reqs['clusterSpec']['initialInstanceCount'] - for fn, reqs in list(args.sys_reqs_from_clone.items()) if 'clusterSpec' in reqs}, - **(args.instance_count or {})) + if args.cloned_job_desc: + # override systemRequirements and systemRequirementsByExecutable mapping with cloned job description + # Note: when cloning from a job, we have 1)runtime 2)cloned 3) default runSpec specifications, and we need to merge the first two to make the new request + # however when cloning from an analysis, the temporary workflow already has the cloned spec as its default, so no need to merge 1) and 2) here + cloned_system_requirements = copy.deepcopy(args.cloned_job_desc).get("systemRequirements", {}) + cloned_instance_type = SystemRequirementsDict.from_sys_requirements(cloned_system_requirements, _type='instanceType') + cloned_cluster_spec = SystemRequirementsDict.from_sys_requirements(cloned_system_requirements, _type='clusterSpec') + cloned_fpga_driver = SystemRequirementsDict.from_sys_requirements(cloned_system_requirements, _type='fpgaDriver') + cloned_nvidia_driver = SystemRequirementsDict.from_sys_requirements(cloned_system_requirements, _type='nvidiaDriver') + cloned_system_requirements_by_executable = args.cloned_job_desc.get("mergedSystemRequirementsByExecutable", {}) or {} + else: + cloned_system_requirements = {} + cloned_instance_type, cloned_cluster_spec, cloned_fpga_driver, cloned_nvidia_driver = ( + SystemRequirementsDict({}), SystemRequirementsDict({}), SystemRequirementsDict({}), SystemRequirementsDict({})) + cloned_system_requirements_by_executable = {} + + # convert runtime --instance-type into mapping {entrypoint:{'instanceType':xxx}} + # here the args.instance_type no longer contains specifications for stage sys reqs + if args.instance_type: + requested_instance_type = SystemRequirementsDict.from_instance_type(args.instance_type) + if not isinstance(args.instance_type, basestring): + requested_instance_type = SystemRequirementsDict(merge(cloned_instance_type.as_dict(), requested_instance_type.as_dict())) + else: + requested_instance_type = cloned_instance_type - executable_describe = None - srd_cluster_spec = SystemRequirementsDict(None) - if args.instance_count is not None: + # convert runtime --instance-count into mapping {entrypoint:{'clusterSpec':{'initialInstanceCount': N}}}) + if args.instance_count: + # retrieve the full cluster spec defined in executable's runSpec.systemRequirements + # and overwrite the field initialInstanceCount with the runtime mapping + requested_instance_count = SystemRequirementsDict.from_instance_count(args.instance_count) executable_describe = executable.describe() - srd_default = SystemRequirementsDict.from_sys_requirements( - executable_describe['runSpec'].get('systemRequirements', {}), _type='clusterSpec') - srd_requested = SystemRequirementsDict.from_instance_count(args.instance_count) - srd_cluster_spec = srd_default.override_cluster_spec(srd_requested) + cluster_spec_to_override = SystemRequirementsDict.from_sys_requirements(executable_describe.get('runSpec',{}).get('systemRequirements', {}),_type='clusterSpec') + + if not isinstance(args.instance_count, basestring): + if cloned_cluster_spec.as_dict(): + requested_instance_count = SystemRequirementsDict(merge(cloned_cluster_spec.as_dict(), requested_instance_count.as_dict())) + cluster_spec_to_override = SystemRequirementsDict(merge(cluster_spec_to_override.as_dict(), cloned_cluster_spec.as_dict())) + + requested_cluster_spec = cluster_spec_to_override.override_cluster_spec(requested_instance_count) + else: + requested_cluster_spec = cloned_cluster_spec + + # fpga/nvidia driver now does not have corresponding dx run option, + # so it can only be requested using the cloned value + requested_fpga_driver = cloned_fpga_driver + requested_nvidia_driver = cloned_nvidia_driver + + # combine the requested instance type, full cluster spec, fpga spec, nvidia spec + # into the runtime systemRequirements + requested_system_requirements = (requested_instance_type + requested_cluster_spec + requested_fpga_driver + + requested_nvidia_driver).as_dict() + + if (args.instance_type and cloned_system_requirements_by_executable): + warning = BOLD("WARNING") + ": --instance-type argument: {} may get overridden by".format(args.instance_type) + warning += " {} mergedSystemRequirementsByExecutable: {}".format(args.cloned_job_desc.get('id'), json.dumps(cloned_system_requirements_by_executable)) + if (args.instance_type_by_executable): + warning += " and runtime --instance-type-by-executable argument:{}\n".format(json.dumps(args.instance_type_by_executable)) + print(fill(warning)) + print() + + # store runtime --instance-type-by-executable {executable:{entrypoint:xxx}} as systemRequirementsByExecutable + # Note: currently we don't have -by-executable options for other fields, for example --instance-count-by-executable + # so this runtime systemRequirementsByExecutable double mapping only contains instanceType under each executable.entrypoint + if args.instance_type_by_executable: + requested_system_requirements_by_executable = {exec: SystemRequirementsDict.from_instance_type(sys_req_by_exec).as_dict( + ) for exec, sys_req_by_exec in args.instance_type_by_executable.items()} + requested_system_requirements_by_executable = SystemRequirementsDict(merge( + cloned_system_requirements_by_executable, requested_system_requirements_by_executable)).as_dict() + else: + requested_system_requirements_by_executable = cloned_system_requirements_by_executable + if args.debug_on: if 'All' in args.debug_on: args.debug_on = ['AppError', 'AppInternalError', 'ExecutionError'] + preserve_job_outputs = None + if args.preserve_job_outputs: + preserve_job_outputs = True + elif args.preserve_job_outputs_folder is not None: + preserve_job_outputs = {"folder": args.preserve_job_outputs_folder} + run_kwargs = { "project": dest_proj, "folder": dest_path, @@ -2960,17 +3288,39 @@ def run_body(args, executable, dest_proj, dest_path, preset_inputs=None, input_n "ignore_reuse_stages": args.ignore_reuse_stages or None, "debug": {"debugOn": args.debug_on} if args.debug_on else None, "delay_workspace_destruction": args.delay_workspace_destruction, - "priority": ("high" if args.watch or args.ssh or args.allow_ssh else args.priority), - "instance_type": args.instance_type, + "priority": args.priority, + "system_requirements": requested_system_requirements or None, + "system_requirements_by_executable": requested_system_requirements_by_executable or None, + "instance_type": None, + "cluster_spec": None, + "fpga_driver": None, + "nvidia_driver": None, "stage_instance_types": args.stage_instance_types, "stage_folders": args.stage_folders, "rerun_stages": args.rerun_stages, - "cluster_spec": srd_cluster_spec.as_dict(), "detach": args.detach, "cost_limit": args.cost_limit, + "rank": args.rank, + "detailed_job_metrics": args.detailed_job_metrics, + "max_tree_spot_wait_time": normalize_timedelta(args.max_tree_spot_wait_time)//1000 if args.max_tree_spot_wait_time else None, + "max_job_spot_wait_time": normalize_timedelta(args.max_job_spot_wait_time)//1000 if args.max_job_spot_wait_time else None, + "preserve_job_outputs": preserve_job_outputs, "extra_args": args.extra_args } + if isinstance(executable, dxpy.DXApplet) or isinstance(executable, dxpy.DXApp): + run_kwargs["head_job_on_demand"] = args.head_job_on_demand + + if any([args.watch or args.ssh or args.allow_ssh]): + if run_kwargs["priority"] in ["low", "normal"]: + if not args.brief: + print(fill(BOLD("WARNING") + ": You have requested that jobs be run under " + + BOLD(run_kwargs["priority"]) + + " priority, which may cause them to be restarted at any point, interrupting interactive work.")) + print() + else: # if run_kwargs["priority"] is None + run_kwargs["priority"] = "high" + if run_kwargs["priority"] in ["low", "normal"] and not args.brief: special_access = set() executable_desc = executable_describe or executable.describe() @@ -3221,15 +3571,24 @@ def print_run_input_help(): def run(args): if args.help: print_run_help(args.executable, args.alias) - + client_ip = None if args.allow_ssh is not None: - args.allow_ssh = [i for i in args.allow_ssh if i is not None] - if args.allow_ssh == [] or ((args.ssh or args.debug_on) and not args.allow_ssh): - args.allow_ssh = ['*'] + # --allow-ssh without IP retrieves client IP + if any(ip is None for ip in args.allow_ssh): + args.allow_ssh = list(filter(None, args.allow_ssh)) + client_ip = get_client_ip() + args.allow_ssh.append(client_ip) + if args.allow_ssh is None and ((args.ssh or args.debug_on) and not args.allow_ssh): + client_ip = get_client_ip() + args.allow_ssh = [client_ip] if args.ssh_proxy and not args.ssh: err_exit(exception=DXCLIError("Option --ssh-proxy cannot be specified without --ssh")) + if args.ssh_proxy: + args.allow_ssh.append(args.ssh_proxy.split(':'[0])) if args.ssh or args.allow_ssh or args.debug_on: verify_ssh_config() + if not args.brief and client_ip is not None: + print("Detected client IP as '{}'. Setting allowed IP ranges to '{}'. To change the permitted IP addresses use --allow-ssh.".format(client_ip, ', '.join(args.allow_ssh))) try_call(process_extra_args, args) try_call(process_properties_args, args) @@ -3238,8 +3597,6 @@ def run(args): err_exit(parser_map['run'].format_help() + fill("Error: Either the executable must be specified, or --clone must be used to indicate a job or analysis to clone"), 2) - args.input_from_clone, args.sys_reqs_from_clone = {}, {} - dest_proj, dest_path = None, None if args.project is not None: @@ -3272,10 +3629,15 @@ def run(args): args.stage_folders = stage_folders clone_desc = None + args.cloned_job_desc = {} if args.clone is not None: - # Resolve job ID or name + # Resolve job ID or name; both job-id and analysis-id can be described using job_describe() if is_job_id(args.clone) or is_analysis_id(args.clone): - clone_desc = dxpy.api.job_describe(args.clone) + clone_desc = dxpy.api.job_describe(args.clone, {"defaultFields": True, + "fields": {"runSystemRequirements": True, + "runSystemRequirementsByExecutable": True, + "mergedSystemRequirementsByExecutable": True}}) + else: iterators = [] if ":" in args.clone: @@ -3334,10 +3696,9 @@ def run(args): setattr(args, metadata, clone_desc.get(metadata)) if clone_desc['class'] == 'job': + args.cloned_job_desc = clone_desc if args.executable == "": args.executable = clone_desc.get("applet", clone_desc.get("app", "")) - args.input_from_clone = clone_desc["runInput"] - args.sys_reqs_from_clone = clone_desc["systemRequirements"] if args.details is None: args.details = { "clonedFrom": { @@ -3367,9 +3728,11 @@ def run(args): is_global_workflow = isinstance(handler, dxpy.DXGlobalWorkflow) if args.depends_on and (is_workflow or is_global_workflow): - err_exit(exception=DXParserError("-d/--depends-on cannot be supplied when running workflows."), expected_exceptions=(DXParserError,)) + if args.head_job_on_demand and (is_workflow or is_global_workflow): + err_exit(exception=DXParserError("--head-job-on-demand cannot be used when running workflows"), + expected_exceptions=(DXParserError,)) # if the destination project has still not been set, use the # current project @@ -3381,8 +3744,6 @@ def run(args): 'Please run "dx select" to set the working project, or use --folder=project:path' )) - is_workflow = isinstance(handler, dxpy.DXWorkflow) - is_global_workflow = isinstance(handler, dxpy.DXGlobalWorkflow) # Get region from the project context args.region = None @@ -3403,6 +3764,8 @@ def run(args): process_instance_type_arg(args, is_workflow or is_global_workflow) + try_call(process_instance_type_by_executable_arg, args) + # Validate and process instance_count argument if args.instance_count: if is_workflow or is_global_workflow: @@ -3421,10 +3784,44 @@ def terminate(args): err_exit() def watch(args): - level_colors = {level: RED() for level in ("EMERG", "ALERT", "CRITICAL", "ERROR")} - level_colors.update({level: YELLOW() for level in ("WARNING", "STDERR")}) - level_colors.update({level: GREEN() for level in ("NOTICE", "INFO", "DEBUG", "STDOUT")}) - + level_color_mapping = ( + (("EMERG", "ALERT", "CRITICAL", "ERROR"), RED(), 2), + (("WARNING", "STDERR"), YELLOW(), 3), + (("NOTICE", "INFO", "DEBUG", "STDOUT", "METRICS"), GREEN(), 4), + ) + level_colors = {lvl: item[1] for item in level_color_mapping for lvl in item[0]} + level_colors_curses = {lvl: item[2] for item in level_color_mapping for lvl in item[0]} + + def check_args_compatibility(incompatible_list): + for adest, aarg in map(lambda arg: arg if isinstance(arg, tuple) else (arg, arg), incompatible_list): + if getattr(args, adest) != parser_watch.get_default(adest): + return "--" + (aarg.replace("_", "-")) + + incompatible_args = None + if args.levels and "METRICS" in args.levels and args.metrics == "none": + incompatible_args = ("--levels METRICS", "--metrics none") + elif args.metrics == "top": + if args.levels and "METRICS" not in args.levels: + err_exit(exception=DXCLIError("'--metrics' is specified, but METRICS level is not included")) + + iarg = check_args_compatibility(["get_stdout", "get_stderr", "get_streams", ("tail", "no-wait"), "tree", "num_recent_messages"]) + if iarg: + incompatible_args = ("--metrics top", iarg) + elif args.metrics == "csv": + iarg = check_args_compatibility(["get_stdout", "get_stderr", "get_streams", "tree", "num_recent_messages", "levels", ("timestamps", "no_timestamps"), "job_ids", "format"]) + if iarg: + incompatible_args = ("--metrics csv", iarg) + + if incompatible_args: + err_exit(exception=DXCLIError("Can not specify both '%s' and '%s'" % incompatible_args)) + + def enrich_msg(log_client, message): + message['timestamp'] = str(datetime.datetime.fromtimestamp(message.get('timestamp', 0)//1000)) + message['level_color'] = level_colors.get(message.get('level', ''), '') + message['level_color_curses'] = level_colors_curses.get(message.get('level', ''), 0) + message['job_name'] = log_client.seen_jobs[message['job']]['name'] if message['job'] in log_client.seen_jobs else message['job'] + + is_try_provided = args.job_try is not None msg_callback, log_client = None, None if args.get_stdout: args.levels = ['STDOUT'] @@ -3438,50 +3835,80 @@ def watch(args): args.levels = ['STDOUT', 'STDERR'] args.format = "{msg}" args.job_info = False + elif args.metrics == "csv": + args.levels = ['METRICS'] + args.format = "{msg}" + args.job_info = False + args.quiet = True elif args.format is None: if args.job_ids: - args.format = BLUE("{job_name} ({job})") + " {level_color}{level}" + ENDC() + " {msg}" + format = BLUE("{job_name} ({job}" + (" try {jobTry}" if is_try_provided else "") + ")") + " {level_color}{level}" + ENDC() + " {msg}" else: - args.format = BLUE("{job_name}") + " {level_color}{level}" + ENDC() + " {msg}" + format = BLUE("{job_name}") + " {level_color}{level}" + ENDC() + " {msg}" if args.timestamps: - args.format = "{timestamp} " + args.format + format = "{timestamp} " + format def msg_callback(message): - message['timestamp'] = str(datetime.datetime.fromtimestamp(message.get('timestamp', 0)//1000)) - message['level_color'] = level_colors.get(message.get('level', ''), '') - message['job_name'] = log_client.seen_jobs[message['job']]['name'] if message['job'] in log_client.seen_jobs else message['job'] - print(args.format.format(**message)) + enrich_msg(log_client, message) + print(format.format(**message)) - from dxpy.utils.job_log_client import DXJobLogStreamClient + from dxpy.utils.job_log_client import DXJobLogStreamClient, metrics_top input_params = {"numRecentMessages": args.num_recent_messages, "recurseJobs": args.tree, "tail": args.tail} - if args.levels: - input_params['levels'] = args.levels + if is_try_provided: + input_params['try'] = args.job_try if not re.match("^job-[0-9a-zA-Z]{24}$", args.jobid): err_exit(args.jobid + " does not look like a DNAnexus job ID") job_describe = dxpy.describe(args.jobid) + + # For finished jobs and --metrics top, behave like --metrics none + if args.metrics == "top" and job_describe['state'] in ('terminated', 'failed', 'done'): + args.metrics = "none" + if args.levels and "METRICS" in args.levels: + args.levels.remove("METRICS") + if 'outputReusedFrom' in job_describe and job_describe['outputReusedFrom'] is not None: args.jobid = job_describe['outputReusedFrom'] if not args.quiet: - print("Output reused from %s" %(args.jobid)) + print("Output reused from %s" % args.jobid) + + if args.levels: + input_params['levels'] = args.levels - log_client = DXJobLogStreamClient(args.jobid, input_params=input_params, msg_callback=msg_callback, - msg_output_format=args.format, print_job_info=args.job_info) + if args.metrics == "none": + input_params['excludeMetrics'] = True + elif args.metrics == "csv": + input_params['metricsFormat'] = "csv" + else: + input_params['metricsFormat'] = "text" # Note: currently, the client is synchronous and blocks until the socket is closed. # If this changes, some refactoring may be needed below try: - if not args.quiet: - print("Watching job %s%s. Press Ctrl+C to stop." % (args.jobid, (" and sub-jobs" if args.tree else "")), file=sys.stderr) - log_client.connect() + if args.metrics == "top": + metrics_top(args, input_params, enrich_msg) + else: + log_client = DXJobLogStreamClient(args.jobid, job_try=args.job_try, input_params=input_params, msg_callback=msg_callback, + msg_output_format=args.format, print_job_info=args.job_info) + + if not args.quiet: + print("Watching job %s%s. Press Ctrl+C to stop watching." % ( + args.jobid + (" try %d" % args.job_try if is_try_provided else ""), + (" and sub-jobs" if args.tree else "") + ), file=sys.stderr) + + log_client.connect() except Exception as details: err_exit(fill(str(details)), 3) +def get_client_ip(): + return dxpy.api.system_whoami({"fields": {"clientIp": True}}).get('clientIp') + def ssh_config(args): user_id = try_call(dxpy.whoami) @@ -3569,35 +3996,67 @@ def verify_ssh_config(): def ssh(args, ssh_config_verified=False): if not re.match("^job-[0-9a-zA-Z]{24}$", args.job_id): err_exit(args.job_id + " does not look like a DNAnexus job ID") - job_desc = try_call(dxpy.describe, args.job_id) + ssh_desc_fields = {"state":True, "sshHostKey": True, "httpsApp": True, "sshPort": True, "host": True, "allowSSH": True} + job_desc = try_call(dxpy.describe, args.job_id, fields=ssh_desc_fields) if job_desc['state'] in ['done', 'failed', 'terminated']: - err_exit(args.job_id + " is in a terminal state, and you cannot connect to it") + err_exit(f"{args.job_id} is in terminal state {job_desc['state']}, and you cannot connect to it") if not ssh_config_verified: verify_ssh_config() + job_allow_ssh = job_desc.get('allowSSH', []) + + # Check requested IPs (--allow-ssh or client IP) against job's allowSSH field and update if necessary + if not args.no_firewall_update: + if args.allow_ssh is not None: + args.allow_ssh = [i for i in args.allow_ssh if i is not None] + else: + # Get client IP from API if --allow-ssh not provided + args.allow_ssh = [get_client_ip()] + if args.ssh_proxy: + args.allow_ssh.append(args.ssh_proxy.split(':')[0]) + # If client IP or args.allow_ssh already exist in job's allowSSH, skip firewall update + if not all(ip in job_allow_ssh for ip in args.allow_ssh): + # Append new IPs to existing job allowSSH + for ip in args.allow_ssh: + if ip not in job_allow_ssh: + job_allow_ssh.append(ip) + sys.stdout.write("Updating allowed IP ranges for SSH to '{}'\n".format(', '.join(job_allow_ssh))) + dxpy.api.job_update(object_id=args.job_id, input_params={"allowSSH": job_allow_ssh}) + sys.stdout.write("Waiting for {} to start...".format(args.job_id)) sys.stdout.flush() while job_desc['state'] not in ['running', 'debug_hold']: + if job_desc['state'] in ['done', 'failed', 'terminated']: + err_exit(f"\n{args.job_id} is in terminal state {job_desc['state']}, and you cannot connect to it") time.sleep(1) - job_desc = dxpy.describe(args.job_id) + job_desc = dxpy.describe(args.job_id, fields=ssh_desc_fields) sys.stdout.write(".") sys.stdout.flush() sys.stdout.write("\n") sys.stdout.write("Resolving job hostname and SSH host key...") sys.stdout.flush() + + known_host = "{job_id}.dnanex.us".format(job_id=args.job_id) host, host_key, ssh_port = None, None, None for i in range(90): host = job_desc.get('host') - host_key = job_desc.get('sshHostKey') or job_desc['properties'].get('ssh_host_rsa_key') + url = job_desc.get('httpsApp', {}).get('dns', {}).get('url') + if url is not None: + https_host = urlparse(url).hostname + # If the hostname is not parsed properly revert back to default behavior + if https_host is not None: + host = https_host + known_host = https_host + host_key = job_desc.get('sshHostKey') ssh_port = job_desc.get('sshPort') or 22 if host and host_key: break else: time.sleep(1) - job_desc = dxpy.describe(args.job_id) + job_desc = dxpy.describe(args.job_id, fields=ssh_desc_fields) sys.stdout.write(".") sys.stdout.flush() sys.stdout.write("\n") @@ -3608,7 +4067,7 @@ def ssh(args, ssh_config_verified=False): known_hosts_file = os.path.join(dxpy.config.get_user_conf_dir(), 'ssh_known_hosts') with open(known_hosts_file, 'a') as fh: - fh.write("{job_id}.dnanex.us {key}\n".format(job_id=args.job_id, key=host_key.rstrip())) + fh.write("{known_host} {key}\n".format(known_host=known_host, key=host_key.rstrip())) import socket connected = False @@ -3618,7 +4077,7 @@ def ssh(args, ssh_config_verified=False): sys.stdout.write(" through proxy {}".format(proxy_args[0])) sys.stdout.write("...") sys.stdout.flush() - for i in range(12): + for i in range(20): try: if args.ssh_proxy: # Test connecting to host through proxy @@ -3632,7 +4091,7 @@ def ssh(args, ssh_config_verified=False): connected = True break except Exception: - time.sleep(2) + time.sleep(3) sys.stdout.write(".") sys.stdout.flush() if args.ssh_proxy: @@ -3648,13 +4107,13 @@ def ssh(args, ssh_config_verified=False): if connected: sys.stdout.write(GREEN("OK") + "\n") else: - msg = "Failed to connect to {h}. Please check your connectivity and try {cmd} again." - err_exit(msg.format(h=host, cmd=BOLD("dx ssh {}".format(args.job_id))), + msg = "Failed to connect to {h}. Please check your connectivity, verify your ssh client IP is added to job's allowedSSH list by describing the job and if needed, retry the command with additional --allow-ssh ADDRESS argument." + err_exit(msg.format(h=host, job_id=args.job_id, cmd=BOLD("dx ssh {}".format(args.job_id))), exception=DXCLIError()) print("Connecting to {}:{}".format(host, ssh_port)) ssh_args = ['ssh', '-i', os.path.join(dxpy.config.get_user_conf_dir(), 'ssh_id'), - '-o', 'HostKeyAlias={}.dnanex.us'.format(args.job_id), + '-o', 'HostKeyAlias={}'.format(known_host), '-o', 'UserKnownHostsFile={}'.format(known_hosts_file), '-p', str(ssh_port), '-l', 'dnanexus', host] if args.ssh_proxy: @@ -3663,7 +4122,7 @@ def ssh(args, ssh_config_verified=False): ssh_args += args.ssh_args exit_code = subprocess.call(ssh_args) try: - job_desc = dxpy.describe(args.job_id) + job_desc = dxpy.describe(args.job_id, fields=ssh_desc_fields) if args.check_running and job_desc['state'] == 'running': msg = "Job {job_id} is still running. Terminate now?".format(job_id=args.job_id) if prompt_for_yn(msg, default=False): @@ -3675,27 +4134,6 @@ def ssh(args, ssh_config_verified=False): print(fill(tip.format(job_id=args.job_id))) exit(exit_code) -def upgrade(args): - if len(args.args) == 0: - try: - greeting = dxpy.api.system_greet({'client': 'dxclient', 'version': 'v'+dxpy.TOOLKIT_VERSION}, auth=None) - if greeting['update']['available']: - recommended_version = greeting['update']['version'] - else: - err_exit("Your SDK is up to date.", code=0) - except default_expected_exceptions as e: - print(e) - recommended_version = "current" - print("Upgrading to", recommended_version) - args.args = [recommended_version] - - try: - cmd = os.path.join(os.environ['DNANEXUS_HOME'], 'build', 'upgrade.sh') - args.args.insert(0, cmd) - os.execv(cmd, args.args) - except: - err_exit() - def generate_batch_inputs(args): # Internally restricted maximum batch size for a TSV @@ -3787,6 +4225,175 @@ def publish(args): except: err_exit() +def archive(args): + def send_archive_request(target_project, request_input, request_func): + api_errors = [InvalidState, ResourceNotFound, PermissionDenied] + try: + res = request_func(target_project, request_input) + except Exception as e: + eprint("Failed request: {}".format(request_input)) + if type(e) in api_errors: + eprint(" API error: {}. {}".format(e.name, e.msg)) + else: + eprint(" Unexpected error: {}".format(format_exception(e))) + + err_exit("Failed request: {}. {}".format(request_input, format_exception(e)), code=3) + return res + + def get_valid_archival_input(args, target_files, target_folder, target_project): + request_input = {} + if target_files: + target_files = list(target_files) + request_input = {"files": target_files} + elif target_folder: + request_input = {"folder": target_folder, "recurse":args.recurse} + else: + err_exit("No input file/folder is found in project {}".format(target_project), code=3) + + request_mode = args.request_mode + options = {} + if request_mode == "archival": + options = {"allCopies": args.all_copies} + request_func = dxpy.api.project_archive + elif request_mode == "unarchival": + options = {"rate": args.rate} + request_func = dxpy.api.project_unarchive + + request_input.update(options) + return request_mode, request_func, request_input + + def get_archival_paths(args): + target_project = None + target_folder = None + target_files = set() + + paths = [split_unescaped(':', path, include_empty_strings=True) for path in args.path] + possible_projects = set() + possible_folder = set() + possible_files = set() + + # Step 0: parse input paths into projects and objects + for p in paths: + if len(p)>2: + err_exit("Path '{}' is invalid. Please check the inputs or check --help for example inputs.".format(":".join(p)), code=3) + elif len(p) == 2: + possible_projects.add(p[0]) + elif len(p) == 1: + possible_projects.add('') + + obj = p[-1] + if obj[-1] == '/': + folder, entity_name = clean_folder_path(('' if obj.startswith('/') else '/') + obj) + if entity_name: + possible_files.add(obj) + else: + possible_folder.add(folder) + else: + possible_files.add(obj) + + # Step 1: find target project + for proj in possible_projects: + # is project ID + if is_container_id(proj) and proj.startswith('project-'): + pass + # is "": use current project + elif proj == '': + if not dxpy.PROJECT_CONTEXT_ID: + err_exit("Cannot find current project. Please check the environment.", code=3) + proj = dxpy.PROJECT_CONTEXT_ID + # name is given + else: + try: + project_results = list(dxpy.find_projects(name=proj, describe=True)) + except: + err_exit("Cannot find project with name {}".format(proj), code=3) + + if project_results: + choice = pick(["{} ({})".format(result['describe']['name'], result['id']) for result in project_results], allow_mult=False) + proj = project_results[choice]['id'] + else: + err_exit("Cannot find project with name {}".format(proj), code=3) + + if target_project and proj!= target_project: + err_exit("All paths must refer to files/folder in a single project, but two project ids: '{}' and '{}' are given. ".format( + target_project, proj), code=3) + elif not target_project: + target_project = proj + + # Step 2: check 1) target project + # 2) either one folder or a list of files + if not target_project: + err_exit('No target project has been set. Please check the input or check your permission to the given project.', code=3) + if len(possible_folder) >1: + err_exit("Only one folder is allowed for each request. Please check the inputs or check --help for example inputs.".format(p), code=3) + if possible_folder and possible_files: + err_exit('Expecting either a single folder or a list of files for each API request', code=3) + + # Step 3: assign target folder or target files + if possible_folder: + target_folder = possible_folder.pop() + else: + for fp in possible_files: + # find a filename + # is file ID + if is_data_obj_id(fp) and fp.startswith("file-"): + target_files.add(fp) + # is folderpath/filename + else: + folderpath, filename = clean_folder_path(('' if obj.startswith('/') else '/') + fp) + try: + file_results = list(dxpy.find_data_objects(classname="file", name=filename,project=target_project,folder=folderpath,describe=True,recurse=False)) + except: + err_exit("Input '{}' is not found as a file in project '{}'".format(fp, target_project), code=3) + + if not file_results: + err_exit("Input '{}' is not found as a file in project '{}'".format(fp, target_project), code=3) + # elif file_results + if not args.all: + choice = pick([ "{} ({})".format(result['describe']['name'], result['id']) for result in file_results],allow_mult=True) + if choice == "*" : + target_files.update([file['id'] for file in file_results]) + else: + target_files.add(file_results[choice]['id']) + else: + target_files.update([file['id'] for file in file_results]) + + return target_files, target_folder, target_project + + # resolve paths + target_files, target_folder, target_project = get_archival_paths(args) + + # set request command and add additional options + request_mode, request_func, request_input = get_valid_archival_input(args, target_files, target_folder, target_project) + + # ask for confirmation if needed + if args.confirm and INTERACTIVE_CLI: + if request_mode == "archival": + if target_files: + counts = len(target_files) + print('Will tag {} file(s) for archival in {}'.format(counts,target_project)) + else: + print('Will tag file(s) for archival in folder {}:{} {}recursively'.format(target_project, target_folder, 'non-' if not args.recurse else '')) + elif request_mode == "unarchival": + dryrun_request_input = copy.deepcopy(request_input) + dryrun_request_input.update(dryRun=True) + dryrun_res = send_archive_request(target_project, dryrun_request_input, request_func) + print('Will tag {} file(s) for unarchival in {}, totalling {} GB, costing ${}'.format(dryrun_res["files"], target_project, dryrun_res["size"],dryrun_res["cost"]/1000)) + + if not prompt_for_yn('Confirm all paths?', default=True): + parser.exit(0) + + # send request and display final results + res = send_archive_request(target_project, request_input, request_func) + + if not args.quiet: + print() + if request_mode == "archival": + print('Tagged {} file(s) for archival in {}'.format(res["count"],target_project)) + elif request_mode == "unarchival": + print('Tagged {} file(s) for unarchival, totalling {} GB, costing ${}'.format(res["files"], res["size"],res["cost"]/1000)) + print() + def print_help(args): if args.command_or_category is None: parser_help.print_help() @@ -3960,6 +4567,17 @@ def register_parser(parser, subparsers_action=None, categories=('other', ), add_ for category in categories: parser_categories[category]['cmds'].append((name, _help)) +def positive_integer(value): + ivalue = int(value) + if ivalue <= 0: + raise argparse.ArgumentTypeError("%s is an invalid positive int value" % value) + return ivalue + +def positive_number(value): + number_value = float(value) + if number_value <= 0: + raise argparse.ArgumentTypeError("%s is an invalid positive number value" % value) + return number_value parser = DXArgumentParser(description=DNANEXUS_LOGO() + ' Command-Line Client, API v%s, client v%s' % (dxpy.API_VERSION, dxpy.TOOLKIT_VERSION) + '\n\n' + fill('dx is a command-line client for interacting with the DNAnexus platform. You can log in, navigate, upload, organize and share your data, launch analyses, and more. For a quick tour of what the tool can do, see') + '\n\n https://documentation.dnanexus.com/getting-started/tutorials/cli-quickstart#quickstart-for-cli\n\n' + fill('For a breakdown of dx commands by category, run "dx help".') + '\n\n' + fill('dx exits with exit code 3 if invalid input is provided or an invalid operation is requested, and exit code 1 if an internal error is encountered. The latter usually indicate bugs in dx; please report them at') + "\n\n https://github.com/dnanexus/dx-toolkit/issues", formatter_class=argparse.RawTextHelpFormatter, @@ -4187,6 +4805,7 @@ def register_parser(parser, subparsers_action=None, categories=('other', ), add_ nargs='+') cp_sources_action.completer = DXPathCompleter() parser_cp.add_argument('destination', help=fill('Folder into which to copy the sources or new pathname (if only one source is provided). Must be in a different project/container than all source paths.', width_adjustment=-15)) +parser_cp.add_argument('--target-file-relocation', help='Allow symlink target file relocation in external storage while cloning a symlink.', action='store_true', default=False) parser_cp.set_defaults(func=cp) register_parser(parser_cp, categories='fs') @@ -4202,6 +4821,7 @@ def register_parser(parser, subparsers_action=None, categories=('other', ), add_ nargs='+') mv_sources_action.completer = DXPathCompleter() parser_mv.add_argument('destination', help=fill('Folder into which to move the sources or new pathname (if only one source is provided). Must be in the same project/container as all source paths.', width_adjustment=-15)) +parser_mv.add_argument('--target-file-relocation', help='Allow symlink target file relocation in external storage while moving a symlink.', action='store_true', default=False) parser_mv.set_defaults(func=mv) register_parser(parser_mv, categories='fs') @@ -4254,10 +4874,13 @@ def register_parser(parser, subparsers_action=None, categories=('other', ), add_ parents=[json_arg, no_color_arg, delim_arg, env_args], prog='dx describe') parser_describe.add_argument('--details', help='Include details of data objects', action='store_true') -parser_describe.add_argument('--verbose', help='Include all possible metadata', action='store_true') +parser_describe.add_argument('--verbose', help='Include additional metadata', action='store_true') parser_describe.add_argument('--name', help='Only print the matching names, one per line', action='store_true') parser_describe.add_argument('--multi', help=fill('If the flag --json is also provided, then returns a JSON array of describe hashes of all matching results', width_adjustment=-24), action='store_true') +parser_describe.add_argument('--try', metavar="T", dest="job_try", type=int, + help=fill('When describing a job that was restarted, describe job try T. T=0 refers to the first try. Default is the last job try.', width_adjustment=-24)) + describe_path_action = parser_describe.add_argument('path', help=fill('Object ID or path to an object (possibly in another project) to describe.', width_adjustment=-24)) describe_path_action.completer = DXPathCompleter() parser_describe.set_defaults(func=describe) @@ -4305,6 +4928,9 @@ def register_parser(parser, subparsers_action=None, categories=('other', ), add_ action='store_false', default=sys.stderr.isatty()) parser_download.add_argument('--lightweight', help='Skip some validation steps to make fewer API calls', action='store_true') +parser_download.add_argument('--symlink-max-tries', help='Set maximum number of tries for downloading symlinked files using aria2c', + type=positive_integer, + default=15) parser_download.add_argument('--unicode', help='Display the characters as text/unicode when writing to stdout', dest="unicode_text", action='store_true') parser_download.set_defaults(func=download_or_cat) @@ -4316,7 +4942,7 @@ def register_parser(parser, subparsers_action=None, categories=('other', ), add_ parser_make_download_url = subparsers.add_parser('make_download_url', help='Create a file download link for sharing', description='Creates a pre-authenticated link that can be used to download a file without logging in.', prog='dx make_download_url') -path_action = parser_make_download_url.add_argument('path', help='Data object ID or name to access') +path_action = parser_make_download_url.add_argument('path', help='Project-qualified data object ID or name, e.g. project-xxxx:file-yyyy, or project-xxxx:/path/to/file.txt') path_action.completer = DXPathCompleter(classes=['file']) parser_make_download_url.add_argument('--duration', help='Time for which the URL will remain valid (in seconds, or use suffix s, m, h, d, w, M, y). Default: 1 day') parser_make_download_url.add_argument('--filename', help='Name that the server will instruct the client to save the file as (default is the filename)') @@ -4358,8 +4984,9 @@ def register_parser(parser, subparsers_action=None, categories=('other', ), add_ prog='dx build', parents=[env_args, stdout_args]) -app_options = build_parser.add_argument_group('options for creating apps', '(Only valid when --app/--create-app is specified)') -applet_and_workflow_options = build_parser.add_argument_group('options for creating applets or workflows', '(Only valid when --app/--create-app is NOT specified)') +app_and_globalworkflow_options = build_parser.add_argument_group('Options for creating apps or globalworkflows', '(Only valid when --app/--create-app/--globalworkflow/--create-globalworkflow is specified)') +applet_and_workflow_options = build_parser.add_argument_group('Options for creating applets or workflows', '(Only valid when --app/--create-app/--globalworkflow/--create-globalworkflow is NOT specified)') +nextflow_options = build_parser.add_argument_group('Options for creating Nextflow applets', '(Only valid when --nextflow is specified)') # COMMON OPTIONS build_parser.add_argument("--ensure-upload", help="If specified, will bypass computing checksum of " + @@ -4376,7 +5003,7 @@ def register_parser(parser, subparsers_action=None, categories=('other', ), add_ "will cause an error).", action="store_true") -src_dir_action = build_parser.add_argument("src_dir", help="App, applet, or workflow source directory (default: current directory)", nargs='?') +src_dir_action = build_parser.add_argument("src_dir", help="Source directory that contains dxapp.json, dxworkflow.json or *.nf (for --nextflow option). (default: current directory)", nargs='?') src_dir_action.completer = LocalCompleter() build_parser.add_argument("--app", "--create-app", help="Create an app.", action="store_const", dest="mode", const="app") @@ -4402,48 +5029,47 @@ def register_parser(parser, subparsers_action=None, categories=('other', ), add_ build_parser.add_argument("--no-dry-run", help=argparse.SUPPRESS, action="store_false", dest="dry_run") # --[no-]publish -app_options.set_defaults(publish=False) -app_options.add_argument("--publish", help="Publish the resulting app and make it the default.", action="store_true", +app_and_globalworkflow_options.set_defaults(publish=False) +app_and_globalworkflow_options.add_argument("--publish", help="Publish the resulting app/globalworkflow and make it the default.", action="store_true", dest="publish") -app_options.add_argument("--no-publish", help=argparse.SUPPRESS, action="store_false", dest="publish") -app_options.add_argument("--from", help="ID of an applet to create an app from. Source directory cannot be given with this option", - dest="_from").completer = DXPathCompleter(classes=['applet']) +app_and_globalworkflow_options.add_argument("--no-publish", help=argparse.SUPPRESS, action="store_false", dest="publish") +app_and_globalworkflow_options.add_argument("--from", help="ID or path of the source applet/workflow to create an app/globalworkflow from. Source directory src_dir cannot be given when using this option", + dest="_from").completer = DXPathCompleter(classes=['applet','workflow']) # --[no-]remote build_parser.set_defaults(remote=False) build_parser.add_argument("--remote", help="Build the app remotely by uploading the source directory to the DNAnexus Platform and building it there. This option is useful if you would otherwise need to cross-compile the app(let) to target the Execution Environment.", action="store_true", dest="remote") -build_parser.add_argument("--no-watch", help="Don't watch the real-time logs of the remote builder. (This option only applicable if --remote was specified).", action="store_false", dest="watch") +build_parser.add_argument("--no-watch", help="Don't watch the real-time logs of the remote builder (this option is only applicable if --remote or --repository is specified).", action="store_false", dest="watch") build_parser.add_argument("--no-remote", help=argparse.SUPPRESS, action="store_false", dest="remote") applet_and_workflow_options.add_argument("-f", "--overwrite", help="Remove existing applet(s) of the same name in the destination folder. This option is not yet supported for workflows.", action="store_true", default=False) applet_and_workflow_options.add_argument("-a", "--archive", help="Archive existing applet(s) of the same name in the destination folder. This option is not yet supported for workflows.", action="store_true", default=False) -build_parser.add_argument("-v", "--version", help="Override the version number supplied in the manifest.", default=None, +build_parser.add_argument("-v", "--version", help="Override the version number supplied in the manifest. This option needs to be specified when using --from option.", default=None, dest="version_override", metavar='VERSION') -app_options.add_argument("-b", "--bill-to", help="Entity (of the form user-NAME or org-ORGNAME) to bill for the app.", +app_and_globalworkflow_options.add_argument("-b", "--bill-to", help="Entity (of the form user-NAME or org-ORGNAME) to bill for the app/globalworkflow.", default=None, dest="bill_to", metavar='USER_OR_ORG') # --[no-]check-syntax build_parser.set_defaults(check_syntax=True) build_parser.add_argument("--check-syntax", help=argparse.SUPPRESS, action="store_true", dest="check_syntax") -build_parser.add_argument("--no-check-syntax", help="Warn but do not fail when syntax problems are found (default is to fail on such errors)", action="store_false", dest="check_syntax") +build_parser.add_argument("--no-check-syntax", help="Warn but do not fail when syntax problems are found (default is to fail on such errors).", action="store_false", dest="check_syntax") # --[no-]version-autonumbering -app_options.set_defaults(version_autonumbering=True) -app_options.add_argument("--version-autonumbering", help=argparse.SUPPRESS, action="store_true", dest="version_autonumbering") -app_options.add_argument("--no-version-autonumbering", help="Only attempt to create the version number supplied in the manifest (that is, do not try to create an autonumbered version such as 1.2.3+git.ab1b1c1d if 1.2.3 already exists and is published).", action="store_false", dest="version_autonumbering") +app_and_globalworkflow_options.set_defaults(version_autonumbering=True) +app_and_globalworkflow_options.add_argument("--version-autonumbering", help=argparse.SUPPRESS, action="store_true", dest="version_autonumbering") +app_and_globalworkflow_options.add_argument("--no-version-autonumbering", help="Only attempt to create the version number supplied in the manifest (that is, do not try to create an autonumbered version such as 1.2.3+git.ab1b1c1d if 1.2.3 already exists and is published).", action="store_false", dest="version_autonumbering") # --[no-]update -app_options.set_defaults(update=True) -app_options.add_argument("--update", help=argparse.SUPPRESS, action="store_true", dest="update") -app_options.add_argument("--no-update", help="Never update an existing unpublished app in place.", action="store_false", dest="update") +app_and_globalworkflow_options.set_defaults(update=True) +app_and_globalworkflow_options.add_argument("--update", help=argparse.SUPPRESS, action="store_true", dest="update") +app_and_globalworkflow_options.add_argument("--no-update", help="Never update an existing unpublished app/globalworkflow in place.", action="store_false", dest="update") # --[no-]dx-toolkit-autodep -build_parser.set_defaults(dx_toolkit_autodep="stable") build_parser.add_argument("--dx-toolkit-legacy-git-autodep", help=argparse.SUPPRESS, action="store_const", dest="dx_toolkit_autodep", const="git") build_parser.add_argument("--dx-toolkit-stable-autodep", help=argparse.SUPPRESS, action="store_const", dest="dx_toolkit_autodep", const="stable") build_parser.add_argument("--dx-toolkit-autodep", help=argparse.SUPPRESS, action="store_const", dest="dx_toolkit_autodep", const="stable") -build_parser.add_argument("--no-dx-toolkit-autodep", help="Do not auto-insert the dx-toolkit dependency (default is to add it if it would otherwise be absent from the runSpec)", action="store_false", dest="dx_toolkit_autodep") +build_parser.add_argument("--no-dx-toolkit-autodep", help=argparse.SUPPRESS, action="store_false", dest="dx_toolkit_autodep") # --[no-]parallel-build build_parser.set_defaults(parallel_build=True) @@ -4451,12 +5077,12 @@ def register_parser(parser, subparsers_action=None, categories=('other', ), add_ build_parser.add_argument("--no-parallel-build", help="Build with " + BOLD("make") + " instead of " + BOLD("make -jN") + ".", action="store_false", dest="parallel_build") -app_options.set_defaults(use_temp_build_project=True) +app_and_globalworkflow_options.set_defaults(use_temp_build_project=True) # Original help: "When building an app, build its applet in the current project instead of a temporary project". -app_options.add_argument("--no-temp-build-project", help=argparse.SUPPRESS, action="store_false", dest="use_temp_build_project") +app_and_globalworkflow_options.add_argument("--no-temp-build-project", help="When building an app in a single region, build its applet in the current project instead of a temporary project.", action="store_false", dest="use_temp_build_project") # --yes -app_options.add_argument('-y', '--yes', dest='confirm', help='Do not ask for confirmation for potentially dangerous operations', action='store_false') +app_and_globalworkflow_options.add_argument('-y', '--yes', dest='confirm', help='Do not ask for confirmation for potentially dangerous operations', action='store_false') # --[no-]json (undocumented): dumps the JSON describe of the app or # applet that was created. Useful for tests. @@ -4464,15 +5090,50 @@ def register_parser(parser, subparsers_action=None, categories=('other', ), add_ build_parser.add_argument("--json", help=argparse.SUPPRESS, action="store_true", dest="json") build_parser.add_argument("--no-json", help=argparse.SUPPRESS, action="store_false", dest="json") build_parser.add_argument("--extra-args", help="Arguments (in JSON format) to pass to the /applet/new API method, overriding all other settings") -build_parser.add_argument("--run", help="Run the app or applet after building it (options following this are passed to "+BOLD("dx run")+"; run at high priority by default)", nargs=argparse.REMAINDER) +build_parser.add_argument("--run", help="Run the app or applet after building it (options following this are passed to "+BOLD("dx run")+"; run at high priority by default).", nargs=argparse.REMAINDER) # --region -app_options.add_argument("--region", action="append", help="Enable the app in this region. This flag can be specified multiple times to enable the app in multiple regions. If --region is not specified, then the enabled region(s) will be determined by 'regionalOptions' in dxapp.json, or the project context.") +app_and_globalworkflow_options.add_argument("--region", action="append", help="Enable the app/globalworkflow in this region. This flag can be specified multiple times to enable the app/globalworkflow in multiple regions. If --region is not specified, then the enabled region(s) will be determined by 'regionalOptions' in dxapp.json, or the project context.") # --keep-open build_parser.add_argument('--keep-open', help=fill("Do not close workflow after building it. Cannot be used when building apps, applets or global workflows.", width_adjustment=-24), action='store_true') +# --nextflow +build_parser.add_argument('--nextflow', help=fill("Build Nextflow applet.", + width_adjustment=-24), action='store_true') + +# --profile +nextflow_options.add_argument('--profile', help=fill("Default profile for the Nextflow pipeline.", + width_adjustment=-24), dest="profile") + +# --repository +nextflow_options.add_argument('--repository', help=fill("Specifies a Git repository of a Nextflow pipeline. Incompatible with --remote.", + width_adjustment=-24), dest="repository") + +# --repository-tag +nextflow_options.add_argument('--repository-tag', help=fill("Specifies tag for Git repository. Can be used only with --repository.", + width_adjustment=-24), dest="tag") + +# --git-credentials +nextflow_options.add_argument('--git-credentials', help=fill("Git credentials used to access Nextflow pipelines from private Git repositories. " + "Can be used only with --repository. More information about the file syntax can be found" + " at https://www.nextflow.io/blog/2021/configure-git-repositories-with-nextflow.html.", + width_adjustment=-24), dest="git_credentials").completer = DXPathCompleter(classes=['file']) +# --cache-docker +nextflow_options.add_argument('--cache-docker', help=fill("Stores a container image tarball in the currently selected project " + "in /.cached_dockerImages. Currently only docker engine is supported. Incompatible with --remote, --force, --archive, --dry-run, --json.", + width_adjustment=-24), action="store_true", dest="cache_docker") + +# --docker-secrets +nextflow_options.add_argument('--docker-secrets', help=fill("A dx file id with credentials for a private " + "docker repository.", + width_adjustment=-24), dest="docker_secrets") + +# --nextflow-pipeline-params +nextflow_options.add_argument('--nextflow-pipeline-params', help=fill("Custom pipeline parameters to be referenced when collecting the docker images.", + width_adjustment=-24), dest="nextflow_pipeline_params") + build_parser.set_defaults(func=build) register_parser(build_parser, categories='exec') @@ -4611,7 +5272,7 @@ def register_parser(parser, subparsers_action=None, categories=('other', ), add_ parents=[env_args], prog='dx list database files' ) -parser_list_database_files.add_argument('database', help='ID of the database.') +parser_list_database_files.add_argument('database', help='Data object ID or path of the database.') parser_list_database_files.add_argument('--folder', default='/', help='Name of folder (directory) in which to start searching for database files. This will typically match the name of the table whose files are of interest. The default value is "/" which will start the search at the root folder of the database.') parser_list_database_files.add_argument("--recurse", default=False, help='Look for files recursively down the directory structure. Otherwise, by default, only look on one level.', action='store_true') parser_list_database_files.add_argument("--csv", default=False, help='Write output as comma delimited fields, suitable as CSV format.', action='store_true') @@ -4690,9 +5351,11 @@ def register_parser(parser, subparsers_action=None, categories=('other', ), add_ parser_update_org.add_argument('--member-list-visibility', help='New org membership level that is required to be able to view the membership level and/or permissions of any other member in the specified org (corresponds to the memberListVisibility org policy)', choices=['ADMIN', 'MEMBER', 'PUBLIC']) parser_update_org.add_argument('--project-transfer-ability', help='New org membership level that is required to be able to change the billing account of a project that is billed to the specified org, to some other entity (corresponds to the restrictProjectTransfer org policy)', choices=['ADMIN', 'MEMBER']) parser_update_org.add_argument('--saml-idp', help='New SAML identity provider') +parser_update_org.add_argument('--detailed-job-metrics-collect-default', choices=['true', 'false'], help='If set to true, jobs launched in the projects billed to this org will collect detailed job metrics by default') update_job_reuse_args = parser_update_org.add_mutually_exclusive_group(required=False) update_job_reuse_args.add_argument('--enable-job-reuse', action='store_true', help='Enable job reuse for projects where the org is the billTo') update_job_reuse_args.add_argument('--disable-job-reuse', action='store_true', help='Disable job reuse for projects where the org is the billTo') +parser_update_org.add_argument('--job-logs-forwarding-json', metavar='JLF', help='JLF is a JSON string with url and token enabling forwarding of job logs to Splunk, e.g. \'{"url":"https://http-inputs-acme.splunkcloud.com/event/collector","token":"splunk-hec-token"}\'') parser_update_org.set_defaults(func=update_org) register_parser(parser_update_org, subparsers_action=subparsers_update, categories='org') @@ -4762,16 +5425,22 @@ def register_parser(parser, subparsers_action=None, categories=('other', ), add_ help="Whether the project should be DOWNLOAD RESTRICTED") parser_update_project.add_argument('--containsPHI', choices=["true"], help="Flag to tell if project contains PHI") +parser_update_project.add_argument('--external-upload-restricted', choices=["true", "false"], + help="Whether uploads of file and table data to the project should be restricted") parser_update_project.add_argument('--database-ui-view-only', choices=["true", "false"], help="Whether the viewers on the project can access the database data directly") parser_update_project.add_argument('--bill-to', help="Update the user or org ID of the billing account", type=str) allowed_executables_group = parser_update_project.add_mutually_exclusive_group() allowed_executables_group.add_argument('--allowed-executables', help='Executable ID(s) this project is allowed to run. This operation overrides any existing list of executables.', type=str, nargs="+") allowed_executables_group.add_argument('--unset-allowed-executables', help='Removes any restriction to run executables as set by --allowed-executables', action='store_true') +database_results_restricted_group = parser_update_project.add_mutually_exclusive_group() +database_results_restricted_group.add_argument('--database-results-restricted', help='Viewers on the project can access only more than specified size of visual data from databases', type=positive_integer) +database_results_restricted_group.add_argument('--unset-database-results-restricted', help='Removes any restriction to return data from databases as set by --database-results-restricted', action='store_true') parser_update_project.set_defaults(func=update_project) register_parser(parser_update_project, subparsers_action=subparsers_update, categories="metadata") + ##################################### # install ##################################### @@ -4821,7 +5490,7 @@ def register_parser(parser, subparsers_action=None, categories=('other', ), add_ parser_run.add_argument('--clone', help=fill('Job or analysis ID or name from which to use as default options (will use the exact same executable ID, destination project and folder, job input, instance type requests, and a similar name unless explicitly overridden by command-line arguments. When using an analysis with --clone a workflow executable cannot be overriden and should not be provided.)', width_adjustment=-24)) parser_run.add_argument('--alias', '--version', dest='alias', help=fill('Alias (tag) or version of the app to run (default: "default" if an app)', width_adjustment=-24)) -parser_run.add_argument('--destination', '--folder', metavar='PATH', dest='folder', help=fill('The full project:folder path in which to output the results. By default, the current working directory will be used.', width_adjustment=-24)) +parser_run.add_argument('--destination', '--folder', metavar='PATH', dest='folder', help=fill('The full project:folder path in which to output the results. By default, the current working directory will be used.', width_adjustment=-24)) parser_run.add_argument('--batch-folders', dest='batch_folders', help=fill('Output results to separate folders, one per batch, using batch ID as the name of the output folder. The batch output folder location will be relative to the path set in --destination', width_adjustment=-24), action='store_true') @@ -4849,19 +5518,24 @@ def register_parser(parser, subparsers_action=None, categories=('other', ), add_ action='store_true') parser_run.add_argument('--priority', choices=['low', 'normal', 'high'], - help='Request a scheduling priority for all resulting jobs. Will be overriden (set to high) ' + - 'when either --watch, --ssh, or --allow-ssh flags are used') + help=fill('Request a scheduling priority for all resulting jobs. ' + + 'Defaults to high when --watch, --ssh, or --allow-ssh flags are used.', + width_adjustment=-24)) +parser_run.add_argument('--head-job-on-demand', action='store_true', + help=fill('Requests that the head job of an app or applet be run in an on-demand instance. ' + + 'Note that --head-job-on-demand option will override the --priority setting for the head job', + width_adjustment=-24)) parser_run.add_argument('-y', '--yes', dest='confirm', help='Do not ask for confirmation', action='store_false') parser_run.add_argument('--wait', help='Wait until the job is done before returning', action='store_true') -parser_run.add_argument('--watch', help="Watch the job after launching it; sets --priority high", action='store_true') +parser_run.add_argument('--watch', help="Watch the job after launching it. Defaults --priority to high.", action='store_true') parser_run.add_argument('--allow-ssh', action='append', nargs='?', metavar='ADDRESS', - help=fill('Configure the job to allow SSH access; sets --priority high. If an argument is ' + - 'supplied, it is interpreted as an IP or hostname mask to allow connections from, ' + - 'e.g. "--allow-ssh 1.2.3.4 --allow-ssh berkeley.edu"', + help=fill('Configure the job to allow SSH access. Defaults --priority to high. If an argument is ' + + 'supplied, it is interpreted as an IP range, e.g. "--allow-ssh 1.2.3.4". ' + + 'If no argument is supplied then the client IP visible to the DNAnexus API server will be used by default', width_adjustment=-24)) parser_run.add_argument('--ssh', - help=fill("Configure the job to allow SSH access and connect to it after launching; " + - "sets --priority high", + help=fill("Configure the job to allow SSH access and connect to it after launching. " + + "Defaults --priority to high.", width_adjustment=-24), action='store_true') parser_run.add_argument('--ssh-proxy', metavar=('
:'), @@ -4910,23 +5584,46 @@ def register_parser(parser, subparsers_action=None, categories=('other', ), add_ parser_run.add_argument('--cost-limit', help=fill("Maximum cost of the job before termination. In case of workflows it is cost of the " "entire analysis job. For batch run, this limit is applied per job.", width_adjustment=-24), metavar='cost_limit', type=float) +parser_run.add_argument('-r', '--rank', type=int, default=None, help=fill('Set the rank of the root execution, integer between -1024 and 1023. Requires executionRankEnabled license feature for the billTo. Default is 0.', width_adjustment=-24)) +parser_run.add_argument('--max-tree-spot-wait-time', help=fill('The amount of time allocated to each path in the root execution\'s tree to wait for Spot (in seconds, or use suffix s, m, h, d, w, M, y)', width_adjustment=-24)) +parser_run.add_argument('--max-job-spot-wait-time', help=fill('The amount of time allocated to each job in the root execution\'s tree to wait for Spot (in seconds, or use suffix s, m, h, d, w, M, y)', width_adjustment=-24)) +parser_run.add_argument('--detailed-job-metrics', action='store_true', default=None, help=fill('Collect CPU, memory, network and disk metrics every 60 seconds', width_adjustment=-24)) + +preserve_outputs = parser_run.add_mutually_exclusive_group() +preserve_outputs.add_argument('--preserve-job-outputs', action='store_true', + help=fill("Copy cloneable outputs of every non-reused job entering \"done\" state in this " + "root execution R into the \"intermediateJobOutputs\" subfolder under R's output " + "folder. As R's root job or root analysis' stages complete, R's regular outputs " + "will be moved to R's regular output folder.", + width_adjustment=-24)) +preserve_outputs.add_argument('--preserve-job-outputs-folder', metavar="JOB_OUTPUTS_FOLDER", + help=fill("Similar to --preserve-job-outputs, copy cloneable outputs of every non-reused " + "job entering \"done\" state in this root execution to the specified folder in " + "the project. JOB_OUTPUTS_FOLDER starting with '/' refers to an absolute path " + "within the project, otherwise, it refers to a subfolder under root execution's " + "output folder.", + width_adjustment=-24)) + parser_run.set_defaults(func=run, verbose=False, help=False, details=None, - stage_instance_types=None, stage_folders=None) + stage_instance_types=None, stage_folders=None, head_job_on_demand=None) register_parser(parser_run, categories='exec') ##################################### # watch ##################################### parser_watch = subparsers.add_parser('watch', help='Watch logs of a job and its subjobs', prog='dx watch', - description='Monitors logging output from a running job', + description='Monitors logging output from a running or finished job', parents=[env_args, no_color_arg]) parser_watch.add_argument('jobid', help='ID of the job to watch') # .completer = TODO parser_watch.add_argument('-n', '--num-recent-messages', help='Number of recent messages to get', type=int, default=1024*256) -parser_watch.add_argument('--tree', help='Include the entire job tree', action='store_true') +parser_watch_trygroup = parser_watch.add_mutually_exclusive_group() +parser_watch_trygroup.add_argument('--tree', help='Include the entire job tree', action='store_true') +parser_watch_trygroup.add_argument('--try', metavar="T", dest="job_try", type=int, + help=fill('Allows to watch older tries of a restarted job. T=0 refers to the first try. Default is the last job try.', width_adjustment=-24)) parser_watch.add_argument('-l', '--levels', action='append', choices=["EMERG", "ALERT", "CRITICAL", "ERROR", "WARNING", - "NOTICE", "INFO", "DEBUG", "STDERR", "STDOUT"]) + "NOTICE", "INFO", "DEBUG", "STDERR", "STDOUT", "METRICS"]) parser_watch.add_argument('--get-stdout', help='Extract stdout only from this job', action='store_true') parser_watch.add_argument('--get-stderr', help='Extract stderr only from this job', action='store_true') parser_watch.add_argument('--get-streams', help='Extract only stdout and stderr from this job', action='store_true') @@ -4936,9 +5633,66 @@ def register_parser(parser, subparsers_action=None, categories=('other', ), add_ parser_watch.add_argument('--no-job-info', help='Omit job info and status updates', action='store_false', dest='job_info') parser_watch.add_argument('-q', '--quiet', help='Do not print extra info messages', action='store_true') -parser_watch.add_argument('-f', '--format', help='Message format. Available fields: job, level, msg, date') +parser_watch.add_argument('-f', '--format', help='Message format. Available fields: job, try, level, msg, date') parser_watch.add_argument('--no-wait', '--no-follow', action='store_false', dest='tail', help='Exit after the first new message is received, instead of waiting for all logs') +parser_watch.add_argument('--metrics', help=fill('Select display mode for detailed job metrics if they were collected and are available based on retention policy; see --metrics-help for details', width_adjustment=-24), + choices=["interspersed", "none", "top", "csv"], default="interspersed") + +class MetricsHelpAction(argparse.Action): + + def __call__(self, parser, namespace, values, option_string=None): + print( +"""Help: Displaying detailed job metrics +Detailed job metrics describe job's consumption of CPU, memory, disk, network, etc at 60 second intervals. +If collection of job metrics was enabled for a job (e.g with dx run --detailed-job-metrics), the metrics can be displayed by "dx watch" for 15 days from the time the job started running. + +Note that all reported data-related values are in base 2 units - i.e. 1 MB = 1024 * 1024 bytes. + +The "interspersed" default mode shows METRICS job log messages interspersed with other jog log messages. + +The "none" mode omits all METRICS messages from "dx watch" output. + +The "top" mode interactively shows the latest METRICS message at the top of the screen and updates it for running jobs instead of showing every METRICS message interspersed with the currently-displayed job log messages. For completed jobs, this mode does not show any metrics. Built-in help describing key bindings is available by pressing "?". + +The "csv" mode outputs the following columns with headers in csv format to stdout: +- timestamp: An integer number representing the number of milliseconds since the Unix epoch. +- cpuCount: A number of CPUs available on the instance that ran the job. +- cpuUsageUser: The percentage of cpu time spent in user mode on the instance during the metric collection period. +- cpuUsageSystem: The percentage of cpu time spent in system mode on the instance during the metric collection period. +- cpuUsageIowait: The percentage of cpu time spent in waiting for I/O operations to complete on the instance during the metric collection period. +- cpuUsageIdle: The percentage of cpu time spent in waiting for I/O operations to complete on the instance during the metric collection period. +- memoryUsedBytes: Bytes of memory used (calculated as total - free - buffers - cache - slab_reclaimable + shared_memory). +- memoryTotalBytes: Total memory available on the instance that ran the job. +- diskUsedBytes: Bytes of storage allocated to the AEE that are used by the filesystem. +- diskTotalBytes: Total bytes of disk space available to the job within the AEE. +- networkOutBytes: Total network bytes transferred out from AEE since the job started. Includes "dx upload" bytes. +- networkInBytes: Total network bytes transferred into AEE since the job started. Includes "dx download" bytes. +- diskReadBytes: Total bytes read from the AEE-accessible disks since the job started. +- diskWriteBytes: Total bytes written to the AEE-accessible disks since the job started. +- diskReadOpsCount: Total disk read operation count against AEE-accessible disk since the job started. +- diskWriteOpsCount: Total disk write operation count against AEE-accessible disk since the job started. + +Note 1: cpuUsageUser, cpuUsageSystem, cpuUsageIowait, cpuUsageIdle and memoryUsedBytes metrics reflect usage by processes inside and outside of the AEE which include DNAnexus services responsible for proxying DNAnexus data. +Note 2: cpuUsageUser + cpuUsageSystem + cpuUsageIowait + cpuUsageIdle + cpuUsageSteal = 100. cpuUsageSteal is unreported, but can be derived from the other 4 quantities given that they add up to 100. +Note 3: cpuUsage numbers are rounded to 2 decimal places. +Note 4: networkOutBytes may be larger than job's egressReport which does not include "dx upload" bytes. + +The format of METRICS job log lines is defined as follows using the example below: + +2023-03-15 12:23:44 some-job-name METRICS ** CPU usr/sys/idl/wai: 24/11/1/64% (4 cores) * Memory: 1566/31649MB * Storage: 19/142GB * Net: 10↓/0↑MBps * Disk: r/w 20/174 MBps iops r/w 8/1300 + +"2023-03-15 12:23:44" is the metrics collection time. +"METRICS" is a type of job log line containing detailed job metrics. +"CPU usr/sys/idl/wai: 24/11/1/64%" maps to cpuUsageUser, cpuUsageSystem, cpuUsageIdle, cpuUsageIowait values. +"(4 cores)" maps to cpuCount. +"Memory: 1566/31649MB" maps to memoryUsedBytes and memoryTotalBytes. +"Storage: 19/142GB" maps to diskUsedBytes and diskTotalBytes. +"Net: 10↓/0↑MBps" is derived from networkOutBytes and networkInBytes cumulative totals by subtracting previous measurement from the measurement at the metric collection time, and dividing the difference by the time span between the two measurements. +"Disk: r/w 20/174 MBps iops r/w 8/1300" is derived similar to "Net:" from diskReadBytes, diskWriteBytes, diskReadOpsCount, and diskWriteOpsCount.""") + parser.exit(0) + +parser_watch.add_argument('--metrics-help', action=MetricsHelpAction, nargs=0, help='Print help for displaying detailed job metrics') parser_watch.set_defaults(func=watch) register_parser(parser_watch, categories='exec') @@ -4969,6 +5723,12 @@ def register_parser(parser, subparsers_action=None, categories=('other', ), add_ parser_ssh.add_argument('ssh_args', help='Command-line arguments to pass to the SSH client', nargs=argparse.REMAINDER) parser_ssh.add_argument('--ssh-proxy', metavar=('
:'), help='SSH connect via proxy, argument supplied is used as the proxy address and port') +parser_ssh_firewall = parser_ssh.add_mutually_exclusive_group() +parser_ssh_firewall.add_argument('--no-firewall-update', help='Do not update the allowSSH allowed IP ranges before connecting with ssh', action='store_true', default=False) +parser_ssh_firewall.add_argument('--allow-ssh', action='append', nargs='?', metavar='ADDRESS', + help=fill('Configure the job to allow SSH access from an IP range, e.g. "--allow-ssh 1.2.3.4". ' + + 'If no argument is supplied then the client IP visible to the DNAnexus API server will be used by default', + width_adjustment=-24)) # If ssh is run with the supress-running-check flag, then dx won't prompt # the user whether they would like to terminate the currently running job # after they exit ssh. Among other things, this will allow users to setup @@ -5025,6 +5785,7 @@ def register_parser(parser, subparsers_action=None, categories=('other', ), add_ parser_new_user_user_opts.add_argument("--last", help="Last name") parser_new_user_user_opts.add_argument("--token-duration", help='Time duration for which the newly generated auth token for the new user will be valid (default 30 days; max 30 days). An integer will be interpreted as seconds; you can append a suffix (s, m, h, d) to indicate different units (e.g. "--token-duration 10m" to indicate 10 minutes).') parser_new_user_user_opts.add_argument("--occupation", help="Occupation") +parser_new_user_user_opts.add_argument("--on-behalf-of", help="On behalf of which org is the account provisioned") parser_new_user_org_opts = parser_new_user.add_argument_group("Org options", "Optionally invite the new user to an org with the specified parameters") parser_new_user_org_opts.add_argument("--org", help="ID of the org") parser_new_user_org_opts.add_argument("--level", choices=["ADMIN", "MEMBER"], default="MEMBER", action=DXNewUserOrgArgsAction, help="Org membership level that will be granted to the new user; default MEMBER") @@ -5059,8 +5820,14 @@ def register_parser(parser, subparsers_action=None, categories=('other', ), add_ parser_new_project.add_argument('--bill-to', help='ID of the user or org to which the project will be billed. The default value is the billTo of the requesting user.') parser_new_project.add_argument('--phi', help='Add PHI protection to project', default=False, action='store_true') -parser_new_project.add_argument('--database-ui-view-only', help='If set to true, viewers of the project will not be able to access database data directly', default=False, +parser_new_project.add_argument('--database-ui-view-only', help='Viewers on the project cannot access database data directly', default=False, action='store_true') +parser_new_project.add_argument('--database-results-restricted', help='Viewers on the project can access only more than specified size of visual data from databases', type=positive_integer) +parser_new_project.add_argument('--monthly-compute-limit', type=positive_integer, help='Monthly project spending limit for compute') +parser_new_project.add_argument('--monthly-egress-bytes-limit', type=positive_integer, help='Monthly project spending limit for egress (in Bytes)') +parser_new_project.add_argument('--monthly-storage-limit', type=positive_number, help='Monthly project spending limit for storage') +parser_new_project.add_argument('--default-symlink', help='Default symlink for external storage account') +parser_new_project.add_argument('--drive', help='Drive for external storage account') parser_new_project.set_defaults(func=new_project) register_parser(parser_new_project, subparsers_action=subparsers_new, categories='fs') @@ -5154,7 +5921,7 @@ def register_parser(parser, subparsers_action=None, categories=('other', ), add_ ##################################### parser_tag = subparsers.add_parser('tag', help='Tag a project, data object, or execution', prog='dx tag', description='Tag a project, data object, or execution. Note that a project context must be either set or specified for data object IDs or paths.', - parents=[env_args, all_arg]) + parents=[env_args, all_arg, try_arg]) parser_tag.add_argument('path', help='ID or path to project, data object, or execution to modify').completer = DXPathCompleter() parser_tag.add_argument('tags', nargs='+', metavar='tag', help='Tags to add') parser_tag.set_defaults(func=add_tags) @@ -5165,7 +5932,7 @@ def register_parser(parser, subparsers_action=None, categories=('other', ), add_ ##################################### parser_untag = subparsers.add_parser('untag', help='Untag a project, data object, or execution', prog='dx untag', description='Untag a project, data object, or execution. Note that a project context must be either set or specified for data object IDs or paths.', - parents=[env_args, all_arg]) + parents=[env_args, all_arg, try_arg]) parser_untag.add_argument('path', help='ID or path to project, data object, or execution to modify').completer = DXPathCompleter() parser_untag.add_argument('tags', nargs='+', metavar='tag', help='Tags to remove') parser_untag.set_defaults(func=remove_tags) @@ -5190,7 +5957,7 @@ def register_parser(parser, subparsers_action=None, categories=('other', ), add_ ##################################### parser_set_properties = subparsers.add_parser('set_properties', help='Set properties of a project, data object, or execution', description='Set properties of a project, data object, or execution. Note that a project context must be either set or specified for data object IDs or paths.', prog='dx set_properties', - parents=[env_args, all_arg]) + parents=[env_args, all_arg, try_arg]) parser_set_properties.add_argument('path', help='ID or path to project, data object, or execution to modify').completer = DXPathCompleter() parser_set_properties.add_argument('properties', nargs='+', metavar='propertyname=value', help='Key-value pairs of property names and their new values') @@ -5203,7 +5970,7 @@ def register_parser(parser, subparsers_action=None, categories=('other', ), add_ parser_unset_properties = subparsers.add_parser('unset_properties', help='Unset properties of a project, data object, or execution', description='Unset properties of a project, data object, or execution. Note that a project context must be either set or specified for data object IDs or paths.', prog='dx unset_properties', - parents=[env_args, all_arg]) + parents=[env_args, all_arg, try_arg]) path_action = parser_unset_properties.add_argument('path', help='ID or path to project, data object, or execution to modify') path_action.completer = DXPathCompleter() parser_unset_properties.add_argument('properties', nargs='+', metavar='propertyname', help='Property names to unset') @@ -5284,14 +6051,10 @@ def register_parser(parser, subparsers_action=None, categories=('other', ), add_ parser_find_apps.add_argument('--billed-to', help='User or organization responsible for the app') parser_find_apps.add_argument('--creator', help='Creator of the app version') parser_find_apps.add_argument('--developer', help='Developer of the app') -parser_find_apps.add_argument('--created-after', help='''Date (e.g. 2012-01-01) or integer timestamp after which the app version was created (negative number means ms in the past, or use suffix s, m, h, d, w, M, y) - Negative input example "--created-after=-2d"''') -parser_find_apps.add_argument('--created-before', help='''Date (e.g. 2012-01-01) or integer timestamp before which the app version was created (negative number means ms in the past, or use suffix s, m, h, d, w, M, y) - Negative input example "--created-before=-2d"''') -parser_find_apps.add_argument('--mod-after',help='''Date (e.g. 2012-01-01) or integer timestamp after which the app was last modified (negative number means ms in the past, or use suffix s, m, h, d, w, M, y) - Negative input example "--mod-after=-2d"''') -parser_find_apps.add_argument('--mod-before', help='''Date (e.g. 2012-01-01) or integer timestamp before which the app was last modified (negative number means ms in the past, or use suffix s, m, h, d, w, M, y) - Negative input example "--mod-before=-2d"''') +parser_find_apps.add_argument('--created-after', help='''Date (e.g. --created-after="2021-12-01" or --created-after="2021-12-01 19:01:33") or integer Unix epoch timestamp in milliseconds (e.g. --created-after=1642196636000) after which the app created. You can also specify negative numbers to indicate a time period in the past suffixed by s, m, h, d, w, M or y to indicate seconds, minutes, hours, days, weeks, months or years (e.g. --created-after=-2d for apps created in the last 2 days)''') +parser_find_apps.add_argument('--created-before', help='''Date (e.g. --created-before="2021-12-01" or --created-before="2021-12-01 19:01:33") or integer Unix epoch timestamp in milliseconds (e.g. --created-before=1642196636000) before which the app was created. You can also specify negative numbers to indicate a time period in the past suffixed by s, m, h, d, w, M or y to indicate seconds, minutes, hours, days, weeks, months or years (e.g. --created-before=-2d for apps created earlier than 2 days ago)''') +parser_find_apps.add_argument('--mod-after',help='''Date (e.g. --mod-after="2021-12-01" or --mod-after="2021-12-01 19:01:33") or integer Unix epoch timestamp in milliseconds (e.g. --mod-after=1642196636000) after which the app modified. You can also specify negative numbers to indicate a time period in the past suffixed by s, m, h, d, w, M or y to indicate seconds, minutes, hours, days, weeks, months or years (e.g. --mod-after=-2d for apps modified in the last 2 days)''') +parser_find_apps.add_argument('--mod-before', help='''Date (e.g. --mod-before="2021-12-01" or --mod-before="2021-12-01 19:01:33") or integer Unix epoch timestamp in milliseconds (e.g. --mod-before=1642196636000) after which the app modified. You can also specify negative numbers to indicate a time period in the past suffixed by s, m, h, d, w, M or y to indicate seconds, minutes, hours, days, weeks, months or years (e.g. --mod-before=-2d for apps modified earlier than 2 days ago)''') parser_find_apps.set_defaults(func=find_apps) register_parser(parser_find_apps, subparsers_action=subparsers_find, categories='exec') @@ -5314,14 +6077,10 @@ def register_parser(parser, subparsers_action=None, categories=('other', ), add_ parser_find_globalworkflows.add_argument('--billed-to', help='User or organization responsible for the workflow') parser_find_globalworkflows.add_argument('--creator', help='Creator of the workflow version') parser_find_globalworkflows.add_argument('--developer', help='Developer of the workflow') -parser_find_globalworkflows.add_argument('--created-after', help='''Date (e.g. 2012-01-01) or integer timestamp after which the workflow version was created (negative number means ms in the past, or use suffix s, m, h, d, w, M, y) - Negative input example "--created-after=-2d"''') -parser_find_globalworkflows.add_argument('--created-before', help='''Date (e.g. 2012-01-01) or integer timestamp before which the workflow version was created (negative number means ms in the past, or use suffix s, m, h, d, w, M, y) - Negative input example "--created-before=-2d"''') -parser_find_globalworkflows.add_argument('--mod-after',help='''Date (e.g. 2012-01-01) or integer timestamp after which the workflow was last modified (negative number means ms in the past, or use suffix s, m, h, d, w, M, y) - Negative input example "--mod-after=-2d"''') -parser_find_globalworkflows.add_argument('--mod-before', help='''Date (e.g. 2012-01-01) or integer timestamp before which the workflow was last modified (negative number means ms in the past, or use suffix s, m, h, d, w, M, y) - Negative input example "--mod-before=-2d"''') +parser_find_globalworkflows.add_argument('--created-after', help='''Date (e.g. --created-after="2021-12-01" or --created-after="2021-12-01 19:01:33") or integer Unix epoch timestamp in milliseconds (e.g. --created-after=1642196636000) after which the workflow was created. You can also specify negative numbers to indicate a time period in the past suffixed by s, m, h, d, w, M or y to indicate seconds, minutes, hours, days, weeks, months or years (e.g. --created-after=-2d for workflows created in the last 2 days).''') +parser_find_globalworkflows.add_argument('--created-before', help='''Date (e.g. --created-before="2021-12-01" or --created-before="2021-12-01 19:01:33") or integer Unix epoch timestamp in milliseconds (e.g. --created-before=1642196636000) before which the workflow was created. You can also specify negative numbers to indicate a time period in the past suffixed by s, m, h, d, w, M or y to indicate seconds, minutes, hours, days, weeks, months or years (e.g. --created-before=-2d for workflows created earlier than 2 days ago)''') +parser_find_globalworkflows.add_argument('--mod-after',help='''Date (e.g. --mod-after="2021-12-01" or --mod-after="2021-12-01 19:01:33") or integer Unix epoch timestamp in milliseconds (e.g. --mod-after=1642196636000) after which the workflow was created. You can also specify negative numbers to indicate a time period in the past suffixed by s, m, h, d, w, M or y to indicate seconds, minutes, hours, days, weeks, months or years (e.g. --mod-after=-2d for workflows modified in the last 2 days)''') +parser_find_globalworkflows.add_argument('--mod-before', help='''Date (e.g. --mod-before="2021-12-01" or --mod-before="2021-12-01 19:01:33") or integer Unix epoch timestamp in milliseconds (e.g. --mod-before=1642196636000) before which the workflow was created. You can also specify negative numbers to indicate a time period in the past suffixed by s, m, h, d, w, M or y to indicate seconds, minutes, hours, days, weeks, months or years (e.g. --mod-before=-2d for workflows modified earlier than 2 days ago)''') parser_find_globalworkflows.set_defaults(func=find_global_workflows) register_parser(parser_find_globalworkflows, subparsers_action=subparsers_find, categories='exec') @@ -5388,7 +6147,8 @@ def register_parser(parser, subparsers_action=None, categories=('other', ), add_ ) parser_find_data.add_argument('--state', choices=['open', 'closing', 'closed', 'any'], help='State of the object') parser_find_data.add_argument('--visibility', choices=['hidden', 'visible', 'either'], default='visible', help='Whether the object is hidden or not') -parser_find_data.add_argument('--name', help='Name of the object') +parser_find_data.add_argument('--name', help='Search criteria for the object name, interpreted according to the --name-mode') +parser_find_data.add_argument('--name-mode', default='glob', help='Name mode to use for searching', choices=['glob', 'exact', 'regexp']) parser_find_data.add_argument('--type', help='Type of the data object') parser_find_data.add_argument('--link', help='Object ID that the data object links to') parser_find_data.add_argument('--all-projects', '--allprojects', help='Extend search to all projects (excluding public projects)', action='store_true') @@ -5397,14 +6157,10 @@ def register_parser(parser, subparsers_action=None, categories=('other', ), add_ parser_find_data.add_argument('--path', help='Project and/or folder in which to restrict the results', metavar='PROJECT:FOLDER').completer = DXPathCompleter(expected='folder') parser_find_data.add_argument('--norecurse', dest='recurse', help='Do not recurse into subfolders', action='store_false') -parser_find_data.add_argument('--created-after', help='''Date (e.g. 2012-01-01) or integer timestamp after which the object was created (negative number means ms in the past, or use suffix s, m, h, d, w, M, y) - Negative input example "--created-after=-2d"''') -parser_find_data.add_argument('--created-before', help='''Date (e.g. 2012-01-01) or integer timestamp before which the object was created (negative number means ms in the past, or use suffix s, m, h, d, w, M, y) - Negative input example "--created-before=-2d"''') -parser_find_data.add_argument('--mod-after',help='''Date (e.g. 2012-01-01) or integer timestamp after which the object was last modified (negative number means ms in the past, or use suffix s, m, h, d, w, M, y) - Negative input example "--mod-after=-2d"''') -parser_find_data.add_argument('--mod-before', help='''Date (e.g. 2012-01-01) or integer timestamp before which the object was last modified (negative number means ms in the past, or use suffix s, m, h, d, w, M, y) - Negative input example "--mod-before=-2d"''') +parser_find_data.add_argument('--created-after', help='''Date (e.g. --created-after="2021-12-01" or --created-after="2021-12-01 19:01:33") or integer Unix epoch timestamp in milliseconds (e.g. --created-after=1642196636000) after which the object was created. You can also specify negative numbers to indicate a time period in the past suffixed by s, m, h, d, w, M or y to indicate seconds, minutes, hours, days, weeks, months or years (e.g. --created-after=-2d for objects created in the last 2 days).''') +parser_find_data.add_argument('--created-before', help='''Date (e.g. --created-before="2021-12-01" or --created-before="2021-12-01 19:01:33") or integer Unix epoch timestamp in milliseconds (e.g. --created-before=1642196636000) before which the object was created. You can also specify negative numbers to indicate a time period in the past suffixed by s, m, h, d, w, M or y to indicate seconds, minutes, hours, days, weeks, months or years (e.g. --created-before=-2d for objects created earlier than 2 days ago)''') +parser_find_data.add_argument('--mod-after',help='''Date (e.g. --mod-after="2021-12-01" or --mod-after="2021-12-01 19:01:33") or integer Unix epoch timestamp in milliseconds (e.g. --mod-after=1642196636000) after which the object was modified. You can also specify negative numbers to indicate a time period in the past suffixed by s, m, h, d, w, M or y to indicate seconds, minutes, hours, days, weeks, months or years (e.g. --mod-after=-2d for objects modified in the last 2 days)''') +parser_find_data.add_argument('--mod-before', help='''Date (e.g. --mod-before="2021-12-01" or --mod-before="2021-12-01 19:01:33") or integer Unix epoch timestamp in milliseconds (e.g. --mod-before=1642196636000) before which the object was modified. You can also specify negative numbers to indicate a time period in the past suffixed by s, m, h, d, w, M or y to indicate seconds, minutes, hours, days, weeks, months or years (e.g. --mod-before=-2d for objects modified earlier than 2 days ago)''') parser_find_data.add_argument('--region', help='Restrict the search to the provided region') parser_find_data.set_defaults(func=find_data) @@ -5425,14 +6181,12 @@ def register_parser(parser, subparsers_action=None, categories=('other', ), add_ help='Include ONLY public projects (will automatically set --level to VIEW)', action='store_true') parser_find_projects.add_argument('--created-after', - help='''Date (e.g. 2012-01-01) or integer timestamp after which the project was - created (negative number means ms in the past, or use suffix s, m, h, d, w, M, y) - Negative input example "--created-after=-2d"''') + help='''Date (e.g. --created-after="2021-12-01" or --created-after="2021-12-01 19:01:33") or integer Unix epoch timestamp in milliseconds (e.g. --created-after=1642196636000) after which the project was created. You can also specify negative numbers to indicate a time period in the past suffixed by s, m, h, d, w, M or y to indicate seconds, minutes, hours, days, weeks, months or years (e.g. --created-after=-2d for projects created in the last 2 days).''') parser_find_projects.add_argument('--created-before', - help='''Date (e.g. 2012-01-01) or integer timestamp after which the project was - created (negative number means ms in the past, or use suffix s, m, h, d, w, M, y) - Negative input example "--created-before=-2d"''') + help='''Date (e.g. --created-before="2021-12-01" or --created-before="2021-12-01 19:01:33") or integer Unix epoch timestamp in milliseconds (e.g. --created-before=1642196636000) before which the project was created. You can also specify negative numbers to indicate a time period in the past suffixed by s, m, h, d, w, M or y to indicate seconds, minutes, hours, days, weeks, months or years (e.g. --created-before=-2d for projects created earlier than 2 days ago)''') parser_find_projects.add_argument('--region', help='Restrict the search to the provided region') +parser_find_projects.add_argument('--external-upload-restricted', choices=["true", "false"], + help="If set to true, only externalUploadRestricted projects will be retrieved. If set to false, only projects that are not externalUploadRestricted will be retrieved.") parser_find_projects.set_defaults(func=find_projects) register_parser(parser_find_projects, subparsers_action=subparsers_find, categories='data') @@ -5478,10 +6232,8 @@ def register_parser(parser, subparsers_action=None, categories=('other', ), add_ find_org_projects_public = parser_find_org_projects.add_mutually_exclusive_group() find_org_projects_public.add_argument('--public-only', dest='public', help='Include only public projects', action='store_true', default=None) find_org_projects_public.add_argument('--private-only', dest='public', help='Include only private projects', action='store_false', default=None) -parser_find_org_projects.add_argument('--created-after', help='''Date (e.g. 2012-01-31) or integer timestamp after which the project was created (negative number means ms in the past, or use suffix s, m, h, d, w, M, y). Integer timestamps will be parsed as milliseconds since epoch. - Negative input example "--created-after=-2d"''') -parser_find_org_projects.add_argument('--created-before', help='''Date (e.g. 2012-01-31) or integer timestamp before which the project was created (negative number means ms in the past, or use suffix s, m, h, d, w, M, y). Integer timestamps will be parsed as milliseconds since epoch. - Negative input example "--created-before=-2d"''') +parser_find_org_projects.add_argument('--created-after', help='''Date (e.g. --created-after="2021-12-01" or --created-after="2021-12-01 19:01:33") or integer Unix epoch timestamp in milliseconds (e.g. --created-after=1642196636000) after which the project was created. You can also specify negative numbers to indicate a time period in the past suffixed by s, m, h, d, w, M or y to indicate seconds, minutes, hours, days, weeks, months or years (e.g. --created-after=-2d for projects created in the last 2 days).''') +parser_find_org_projects.add_argument('--created-before', help='''Date (e.g. --created-before="2021-12-01" or --created-before="2021-12-01 19:01:33") or integer Unix epoch timestamp in milliseconds (e.g. --created-before=1642196636000) before which the project was created. You can also specify negative numbers to indicate a time period in the past suffixed by s, m, h, d, w, M or y to indicate seconds, minutes, hours, days, weeks, months or years (e.g. --created-before=-2d for projects created earlier than 2 days ago)''') parser_find_org_projects.add_argument('--region', help='Restrict the search to the provided region') parser_find_org_projects.set_defaults(func=org_find_projects) register_parser(parser_find_org_projects, subparsers_action=subparsers_find_org, categories=('data', 'org')) @@ -5508,13 +6260,11 @@ def register_parser(parser, subparsers_action=None, categories=('other', ), add_ parser_find_org_apps.add_argument('--installed', help='Return only installed apps', action='store_true') parser_find_org_apps.add_argument('--creator', help='Creator of the app version') parser_find_org_apps.add_argument('--developer', help='Developer of the app') -parser_find_org_apps.add_argument('--created-after', help='''Date (e.g. 2012-01-31) or integer timestamp after which the app version was created (negative number means ms in the past, or use suffix s, m, h, d, w, M, y). Integer timestamps will be parsed as milliseconds since epoch. - Negative input example "--created-after=-2d"''') -parser_find_org_apps.add_argument('--created-before', help='''Date (e.g. 2012-01-31) or integer timestamp before which the app version was created (negative number means ms in the past, or use suffix s, m, h, d, w, M, y). Integer timestamps will be parsed as milliseconds since epoch. - Negative input example "--created-before=-2d"''') -parser_find_org_apps.add_argument('--mod-after', help='''Date (e.g. 2012-01-01) or integer timestamp after which the app was last modified (negative number means ms in the past, or use suffix s, m, h, d, w, M, y) +parser_find_org_apps.add_argument('--created-after', help='''Date (e.g. --created-after="2021-12-01" or --created-after="2021-12-01 19:01:33") or integer Unix epoch timestamp in milliseconds (e.g. --created-after=1642196636000) after which the app was created. You can also specify negative numbers to indicate a time period in the past suffixed by s, m, h, d, w, M or y to indicate seconds, minutes, hours, days, weeks, months or years (e.g. --created-after=-2d for apps created in the last 2 days).''') +parser_find_org_apps.add_argument('--created-before', help='''Date (e.g. --created-before="2021-12-01" or --created-before="2021-12-01 19:01:33") or integer Unix epoch timestamp in milliseconds (e.g. --created-before=1642196636000) before which the app was created. You can also specify negative numbers to indicate a time period in the past suffixed by s, m, h, d, w, M or y to indicate seconds, minutes, hours, days, weeks, months or years (e.g. --created-before=-2d for apps created earlier than 2 days ago)''') +parser_find_org_apps.add_argument('--mod-after', help='''Date (e.g. 2012-01-01) or integer timestamp after which the app was last modified (negative number means seconds in the past, or use suffix s, m, h, d, w, M, y) Negative input example "--mod-after=-2d"''') -parser_find_org_apps.add_argument('--mod-before', help='''Date (e.g. 2012-01-01) or integer timestamp before which the app was last modified (negative number means ms in the past, or use suffix s, m, h, d, w, M, y) +parser_find_org_apps.add_argument('--mod-before', help='''Date (e.g. 2012-01-01) or integer timestamp before which the app was last modified (negative number means seconds in the past, or use suffix s, m, h, d, w, M, y) Negative input example "--mod-before=-2d"''') parser_find_org_apps.set_defaults(func=org_find_apps) register_parser(parser_find_org_apps, subparsers_action=subparsers_find_org, categories=('exec', 'org')) @@ -5598,16 +6348,6 @@ def register_parser(parser, subparsers_action=None, categories=('other', ), add_ # parser_api.completer = TODO register_parser(parser_api) -##################################### -# upgrade -##################################### -parser_upgrade = subparsers.add_parser('upgrade', help='Upgrade dx-toolkit (the DNAnexus SDK and this program)', - description='Upgrades dx-toolkit (the DNAnexus SDK and this program) to the latest recommended version, or to a specified version and platform.', - prog='dx upgrade') -parser_upgrade.add_argument('args', nargs='*') -parser_upgrade.set_defaults(func=upgrade) -register_parser(parser_upgrade) - ##################################### # generate_batch_inputs ##################################### @@ -5636,6 +6376,431 @@ def register_parser(parser, subparsers_action=None, categories=('other', ), add_ parser_publish.set_defaults(func=publish) register_parser(parser_publish) +##################################### +# archive +##################################### + +parser_archive = subparsers.add_parser( + 'archive', + help='Requests for the specified set files or for the files in a single specified folder in one project to be archived on the platform', + description= +''' +Requests for {} or for the files in {} in {} to be archived on the platform. +For each file, if this is the last copy of a file to have archival requested, it will trigger the full archival of the object. +Otherwise, the file will be marked in an archival state denoting that archival has been requested. +'''.format(BOLD('the specified set files'), BOLD('a single specified folder'), BOLD('ONE project')) + +''' +The input paths should be either 1 folder path or up to 1000 files, and all path(s) need to be in the same project. +To specify which project to use, prepend the path or ID of the file/folder with the project ID or name and a colon. + +EXAMPLES: + + # archive 3 files in project "FirstProj" with project ID project-B0VK6F6gpqG6z7JGkbqQ000Q + $ dx archive FirstProj:file-B0XBQFygpqGK8ZPjbk0Q000Q FirstProj:/path/to/file1 project-B0VK6F6gpqG6z7JGkbqQ000Q:/file2 + + # archive 2 files in current project. Specifying file ids saves time by avoiding file name resolution. + $ dx select FirstProj + $ dx archive file-A00000ygpqGK8ZPjbk0Q000Q file-B00000ygpqGK8ZPjbk0Q000Q + + # archive all files recursively in project-B0VK6F6gpqG6z7JGkbqQ000Q + $ dx archive project-B0VK6F6gpqG6z7JGkbqQ000Q:/ + ''', + formatter_class=argparse.RawTextHelpFormatter, + parents=[all_arg], + prog='dx archive') + +parser_archive.add_argument('-q', '--quiet', help='Do not print extra info messages', + action='store_true') +parser_archive.add_argument( + '--all-copies', + dest = "all_copies", + help=fill('If true, archive all the copies of files in projects with the same billTo org.' ,width_adjustment=-24)+ '\n'+ fill('See https://documentation.dnanexus.com/developer/api/data-containers/projects#api-method-project-xxxx-archive for details.',width_adjustment=-24), + default=False, action='store_true') +parser_archive.add_argument( + '-y','--yes', dest='confirm', + help=fill('Do not ask for confirmation.' , width_adjustment=-24), + default=True, action='store_false') +parser_archive.add_argument('--no-recurse', dest='recurse',help=fill('When `path` refers to a single folder, this flag causes only files in the specified folder and not its subfolders to be archived. This flag has no impact when `path` input refers to a collection of files.', width_adjustment=-24), action='store_false') + +parser_archive.add_argument( + 'path', + help=fill('May refer to a single folder or specify up to 1000 files inside a project.',width_adjustment=-24), + default=[], nargs='+').completer = DXPathCompleter() + +parser_archive_output = parser_archive.add_argument_group(title='Output', description='If -q option is not specified, prints "Tagged file(s) for archival"') + +parser_archive.set_defaults(func=archive, request_mode = "archival") +register_parser(parser_archive, categories='fs') + +##################################### +# unarchive +##################################### + +parser_unarchive = subparsers.add_parser( + 'unarchive', + help='Requests for the specified set files or for the files in a single specified folder in one project to be unarchived on the platform.', + description= +''' +Requests for {} or for the files in {} in {} to be unarchived on the platform. +The requested copy will eventually be transitioned over to the live state while all other copies will move over to the archival state. +'''.format(BOLD('a specified set files'), BOLD('a single specified folder'), BOLD('ONE project')) + +''' +The input paths should be either 1 folder path or up to 1000 files, and all path(s) need to be in the same project. +To specify which project to use, prepend the path or ID of the file/folder with the project ID or name and a colon. + +EXAMPLES: + + # unarchive 3 files in project "FirstProj" with project ID project-B0VK6F6gpqG6z7JGkbqQ000Q + $ dx unarchive FirstProj:file-B0XBQFygpqGK8ZPjbk0Q000Q FirstProj:/path/to/file1 project-B0VK6F6gpqG6z7JGkbqQ000Q:/file2 + + # unarchive 2 files in current project. Specifying file ids saves time by avoiding file name resolution. + $ dx select FirstProj + $ dx unarchive file-A00000ygpqGK8ZPjbk0Q000Q file-B00000ygpqGK8ZPjbk0Q000Q + + # unarchive all files recursively in project-B0VK6F6gpqG6z7JGkbqQ000Q + $ dx unarchive project-B0VK6F6gpqG6z7JGkbqQ000Q:/ + ''', + formatter_class=argparse.RawTextHelpFormatter, + parents=[all_arg], + prog='dx unarchive') + +parser_unarchive.add_argument('--rate', help=fill('The speed at which all files in this request are unarchived.', width_adjustment=-24) + '\n'+ fill('- Azure regions: {Expedited, Standard}', width_adjustment=-24,initial_indent=' ') + '\n'+ +fill('- AWS regions: {Expedited, Standard, Bulk}', width_adjustment=-24,initial_indent=' '), choices=["Expedited", "Standard", "Bulk"], default="Standard") + +parser_unarchive.add_argument('-q', '--quiet', help='Do not print extra info messages', action='store_true') +parser_unarchive.add_argument( + '-y','--yes', dest='confirm', + help=fill('Do not ask for confirmation.' , width_adjustment=-24), + default=True, action='store_false') +parser_unarchive.add_argument('--no-recurse', dest='recurse',help=fill('When `path` refers to a single folder, this flag causes only files in the specified folder and not its subfolders to be unarchived. This flag has no impact when `path` input refers to a collection of files.', width_adjustment=-24), action='store_false') + +parser_unarchive.add_argument( + 'path', + help=fill('May refer to a single folder or specify up to 1000 files inside a project.', width_adjustment=-24), + default=[], nargs='+').completer = DXPathCompleter() + +parser_unarchive.add_argument_group(title='Output', description='If -q option is not specified, prints "Tagged <> file(s) for unarchival, totalling <> GB, costing <> "') +parser_unarchive.set_defaults(func=archive, request_mode="unarchival") +register_parser(parser_unarchive, categories='fs') + +##################################### +# extract_dataset +##################################### +parser_extract_dataset = subparsers.add_parser('extract_dataset', help="Retrieves the data or generates SQL to retrieve the data from a dataset or cohort for a set of entity.fields. Additionally, the dataset's dictionary can be extracted independently or in conjunction with data. Listing options enable enumeration of the entities and their respective fields in the dataset.", + description="Retrieves the data or generates SQL to retrieve the data from a dataset or cohort for a set of entity.fields. Additionally, the dataset's dictionary can be extracted independently or in conjunction with data. Provides listing options for entities and fields.", + prog='dx extract_dataset') +parser_extract_dataset.add_argument('path', help='v3.0 Dataset or Cohort object ID (project-id:record-id where "record-id" indicates the record ID in the currently selected project) or name') +parser_extract_dataset.add_argument('-ddd', '--dump-dataset-dictionary', action="store_true", default=False, help='If provided, the three dictionary files, .data_dictionary.csv, .entity_dictionary.csv, and .codings.csv will be generated. Files will be comma delimited and written to the local working directory, unless otherwise specified using --delimiter and --output arguments. If stdout is specified with the output argument, the data dictionary, entity dictionary, and coding are output in succession, without separators. If any of the three dictionary files does not contain data (i.e. the dictionary is empty), then that particular file will not be created (or be output if the output is stdout).') +parser_extract_dataset.add_argument('--fields', type=str, help='A comma-separated string where each value is the phenotypic entity name and field name, separated by a dot. For example: ".,.". Internal spaces are permitted. If multiple entities are provided, field values will be automatically inner joined. If only the --fields argument is provided, data will be retrieved and returned. If both --fields and --sql arguments are provided, a SQL statement to retrieve the specified field data will be automatically generated and returned. Alternatively, use --fields-file option when the number of fields to be retrieved is large.') +parser_extract_dataset.add_argument('--fields-file', type=str, help='A file with no header and one entry per line where every entry is the phenotypic entity name and field name, separated by a dot. For example: .. If multiple entities are provided, field values will be automatically inner joined. If only the --fields-file argument is provided, data will be retrieved and returned. If both --fields-file and --sql arguments are provided, a SQL statement to retrieve the specified field data will be automatically generated and returned. May not be used in conjunction with the argument --fields.') +parser_extract_dataset.add_argument('--sql', action="store_true", default=False, help='If provided, a SQL statement (string) will be returned to query the set of entity.fields, instead of returning stored values from the set of entity.fields') +parser_extract_dataset.add_argument('--delim', '--delimiter', nargs='?', const=',', default=',', help='Always use exactly one of DELIMITER to separate fields to be printed; if no delimiter is provided with this flag, COMMA will be used') +parser_extract_dataset.add_argument('-o', '--output', help='Local filename or directory to be used ("-" indicates stdout output). If not supplied, output will create a file with a default name in the current folder') +parser_extract_dataset.add_argument( "--list-fields", action="store_true", default=False, help='List the names and titles of all fields available in the dataset specified. When not specified together with "–-entities", it will return all the fields from the main entity. Output will be a two column table, field names and field titles, separated by a tab, where field names will be of the format, "." and field titles will be of the format, "".') +parser_extract_dataset.add_argument( "--list-entities", action="store_true", default=False, help='List the names and titles of all the entities available in the dataset specified. Output will be a two column table, entity names and entity titles, separated by a tab.') +parser_extract_dataset.add_argument("--entities", help='Similar output to "--list-fields", however using "--entities" will allow for specific entities to be specified. When multiple entities are specified, use comma as the delimiter. For example: "--list-fields --entities entityA,entityB,entityC"') +parser_extract_dataset.set_defaults(func=extract_dataset) +register_parser(parser_extract_dataset) + +##################################### +# extract_assay +##################################### +parser_extract_assay = subparsers.add_parser( + "extract_assay", + help="Retrieve the selected data or generate SQL to retrieve the data from a genetic variant or somatic assay in a dataset or cohort based on provided rules.", + description="Retrieve the selected data or generate SQL to retrieve the data from a genetic variant or somatic assay in a dataset or cohort based on provided rules.", + prog="dx extract_assay", +) +subparsers_extract_assay = parser_extract_assay.add_subparsers( + parser_class=DXArgumentParser +) +parser_extract_assay.metavar = "class" +register_parser(parser_extract_assay) + +##################################### +# germline +##################################### +parser_extract_assay_germline = subparsers_extract_assay.add_parser( + "germline", + help="Query a Dataset or Cohort for an instance of a germline variant assay and retrieve data, or generate SQL to retrieve data, as defined by user-provided filters.", + description="Query a Dataset or Cohort for an instance of a germline variant assay and retrieve data, or generate SQL to retrieve data, as defined by user-provided filters.", + +) + +parser_extract_assay_germline.add_argument( + "path", + type=str, + help='v3.0 Dataset or Cohort object ID (project-id:record-id, where ":record-id" indicates the record-id in the currently selected project) or name.' +) + + +parser_extract_assay_germline.add_argument( + "--assay-name", + default=None, + help='Specify the genetic variant assay to query. If the argument is not specified, the default assay used is the first assay listed when using the argument, "--list-assays."' +) + +parser_e_a_g_mutex_group = parser_extract_assay_germline.add_mutually_exclusive_group(required=True) +parser_e_a_g_mutex_group.add_argument( + "--list-assays", + action="store_true", + help='List genetic variant assays available for query in the specified Dataset or Cohort object.' +) + +parser_e_a_g_mutex_group.add_argument( + "--retrieve-allele", + type=str, + const='{}', + default=None, + nargs='?', + help='A JSON object, either in a file (.json extension) or as a string (‘’), specifying criteria of alleles to retrieve. Returns a list of allele IDs with additional information. Use --json-help with this option to get detailed information on the JSON format and filters' +) +parser_e_a_g_mutex_group.add_argument( + "--retrieve-annotation", + type=str, + const='{}', + default=None, + nargs='?', + help='A JSON object, either in a file (.json extension) or as a string (‘’), specifying criteria to retrieve corresponding alleles and their annotation. Use --json-help with this option to get detailed information on the JSON format and filters.' +) +parser_e_a_g_mutex_group.add_argument( + "--retrieve-genotype", + type=str, + const='{}', + default=None, + nargs='?', + help='A JSON object, either in a file (.json extension) or as a string (‘’), specifying criteria of samples to retrieve. Returns a list of genotypes and associated sample IDs and allele IDs. Genotype types "ref" and "no-call" have no allele ID, and "half" types where the genotype is half reference and half no-call also have no allele ID. All other genotype types have an allele ID, including "half" types where the genotype is half alternate allele and half no-call. Use --json-help with this option to get detailed information on the JSON format and filters.' +) + +parser_e_a_g_infer_new_mutex_group = parser_extract_assay_germline.add_mutually_exclusive_group(required=False) +parser_e_a_g_infer_new_mutex_group.add_argument( + "--infer-nocall", + action="store_true", + help='When using the "--retrieve-genotype" option, infer genotypes with type "no-call" if they were excluded when the dataset was created. This option is only valid if the exclusion parameters at ingestion were set to "exclude_nocall=true", "exclude_halfref=false", and "exclude_refdata=false".') +parser_e_a_g_infer_new_mutex_group.add_argument( + "--infer-ref", + action="store_true", + help='When using the "--retrieve-genotype" option, infer genotypes with type "ref" if they were excluded when the dataset was created. This option is only valid if the exclusion parameters at ingestion were set to "exclude_nocall=false", "exclude_halfref=false", and "exclude_refdata=true".' +) + +parser_extract_assay_germline.add_argument( + '--json-help', + help=argparse.SUPPRESS, + action="store_true", +) +parser_extract_assay_germline.add_argument( + "--sql", + action="store_true", + help='If the flag is provided, a SQL statement, returned as a string, will be provided to query the specified data instead of returning data.' +) +parser_extract_assay_germline.add_argument( + "-o", "--output", + type=str, + default=None, + help = 'A local filename or directory to be used, where "-" indicates printing to STDOUT. If -o/--output is not supplied, default behavior is to create a file with a constructed name in the current folder.' +) +parser_extract_assay_germline.set_defaults(func=extract_assay_germline) +register_parser(parser_extract_assay_germline) + +##################################### +# somatic +##################################### +parser_extract_assay_somatic = subparsers_extract_assay.add_parser( + "somatic", + help='Query a Dataset or Cohort for an instance of a somatic variant assay and retrieve data, or generate SQL to retrieve data, as defined by user-provided filters.', + description='Query a Dataset or Cohort for an instance of a somatic variant assay and retrieve data, or generate SQL to retrieve data, as defined by user-provided filters.' +) + +parser_extract_assay_somatic.add_argument( + "path", + type=str, + help='v3.0 Dataset or Cohort object ID (project-id:record-id, where ":record-id" indicates the record-id in the currently selected project) or name.' +) + +parser_e_a_s_mutex_group = parser_extract_assay_somatic.add_mutually_exclusive_group(required=True) +parser_e_a_s_mutex_group.add_argument( + "--list-assays", + action="store_true", + help='List somatic variant assays available for query in the specified Dataset or Cohort object.' +) + +parser_e_a_s_mutex_group.add_argument( + "--retrieve-meta-info", + action="store_true", + help='List meta information, as it exists in the original VCF headers for both INFO and FORMAT fields.' +) + +parser_e_a_s_mutex_group.add_argument( + "--retrieve-variant", + type=str, + const='{}', + default=None, + nargs='?', + help='A JSON object, either in a file (.json extension) or as a string (‘’), specifying criteria of somatic variants to retrieve. Retrieves rows from the variant table, optionally extended with sample and annotation information (the extension is inline without affecting row count). By default returns the following set of fields; "assay_sample_id", "allele_id", "CHROM", "POS", "REF", and "allele". Additional fields may be returned using --additional-fields. Use --json-help with this option to get detailed information on the JSON format and filters. When filtering, the user must supply one, and only one of "location", "annotation.symbol", "annotation.gene", "annotation.feature", "allele.allele_id".' +) + +parser_e_a_s_mutex_group.add_argument( + "--additional-fields-help", + action="store_true", + help="List all fields available for output.", +) + +parser_extract_assay_somatic.add_argument( + "--include-normal-sample", + action="store_true", + help='Include variants associated with normal samples in the assay. If no flag is supplied, variants from normal samples will not be supplied.' +) + +parser_extract_assay_somatic.add_argument( + "--additional-fields", + type=str, + default=None, + help='A set of fields to return, in addition to the default set; "assay_sample_id", "allele_id", "CHROM", "POS", "REF", "allele". Fields must be represented as field names and supplied as a single string, where each field name is separated by a single comma. For example, "fieldA,fieldB,fieldC." Internal spaces are permitted. Use --additional-fields-help with this option to get detailed information and the full list of output fields available.' +) + +parser_extract_assay_somatic.add_argument( + "--assay-name", + default=None, + help='Specify the somatic variant assay to query. If the argument is not specified, the default assay used is the first assay listed when using the argument, "--list-assays."' +) + +parser_extract_assay_somatic.add_argument( + '--json-help', + help=argparse.SUPPRESS, + action="store_true", +) + +parser_extract_assay_somatic.add_argument( + "--sql", + action="store_true", + help='If the flag is provided, a SQL statement, returned as a string, will be provided to query the specified data instead of returning data.' +) +parser_extract_assay_somatic.add_argument( + "-o", "--output", + type=str, + default=None, + help='A local filename or directory to be used, where "-" indicates printing to STDOUT. If -o/--output is not supplied, default behavior is to create a file with a constructed name in the current folder.' +) + +parser_extract_assay_somatic.set_defaults(func=extract_assay_somatic) +register_parser(parser_extract_assay_somatic) + +##################################### +# expression +##################################### +parser_extract_assay_expression = subparsers_extract_assay.add_parser( + "expression", + help="Retrieve the selected data or generate SQL to retrieve the data from a molecular expression assay in a dataset or cohort based on provided rules.", + description="Retrieve the selected data or generate SQL to retrieve the data from a molecular expression assay in a dataset or cohort based on provided rules.", +) + +parser_extract_assay_expression.add_argument( + "path", + nargs='?', + type=str, + help='v3.0 Dataset or Cohort object ID, project-id:record-id, where ":record-id" indicates the record-id in current selected project, or name', +) + +parser_extract_assay_expression.add_argument( + "--list-assays", + action="store_true", + help="List molecular expression assays available for query in the specified Dataset or Cohort object", +) + +parser_extract_assay_expression.add_argument( + "--retrieve-expression", + action="store_true", + help='A flag to support, specifying criteria of molecular expression to retrieve. Retrieves rows from the expression table, optionally extended with sample and annotation information where the extension is inline without affecting row count. By default returns the following set of fields; "sample_id", "feature_id", and "value". Additional fields may be returned using "--additional-fields". Must be used with either "--filter-json" or "--filter-json-file". Specify "--json-help" following this option to get detailed information on the json format and filters. When filtering, one, and only one of "location", "annotation.feature_id", or "annotation.feature_name" may be supplied. If a Cohort object is supplied, returned samples will be initially filtered to match the cohort-defined set of samples, and any additional filters will only further refine the cohort-defined set.', +) + +parser_extract_assay_expression.add_argument( + "--additional-fields-help", + action="store_true", + help="List all fields available for output.", +) + +parser_extract_assay_expression.add_argument( + "--assay-name", + type=str, + help='Specify a specific molecular expression assay to query. If the argument is not specified, the default assay used is the first assay listed when using the argument, "--list-assays"', +) + +parser_extract_assay_expression.add_argument( + "--filter-json", + "-j", + type=str, + help='The full input JSON object as a string and corresponding to "--retrieve-expression". Must be used with "--retrieve-expression" flag. Either "--filter-json" or "--filter-json-file" may be supplied, not both.', +) + +parser_extract_assay_expression.add_argument( + "--filter-json-file", + "-f", + type=str, + help='The full input JSON object as a file and corresponding to "--retrieve-expression". Must be used with "--retrieve-expression" flag. Either "--filter-json" or "--filter-json-file" may be supplied, not both.', +) + +parser_extract_assay_expression.add_argument( + "--json-help", + help='When set, return a json template of "--retrieve-expression" and a list of filters with definitions.', + action="store_true", +) + +parser_extract_assay_expression.add_argument( + "--sql", + action="store_true", + help='If the flag is provided, a SQL statement (as a string) will be returned for the user to further query the specified data, instead of returning actual data values. Use of "--sql" is not supported when also using the flag, --expression-matrix/-em', +) + +parser_extract_assay_expression.add_argument( + "--additional-fields", + nargs="+", + default=None, + help='A set of fields to return, in addition to the default set; "sample_id", "feature_id", and "value". Fields must be represented as field names and supplied as a single string, where each field name is separated by a single comma. For example, fieldA,fieldB,fieldC. Use "--additional-fields-help" to get the full list of output fields available.', +) + +parser_extract_assay_expression.add_argument( + "--expression-matrix", + "-em", + action="store_true", + help='If the flag is provided with "--retrieve-expression", the returned data will be a matrix of sample IDs (rows) by feature IDs (columns), where each cell is the respective pairwise value. The flag is not compatible with "--additional-fields". Additionally, the flag is not compatible with an "expression" filter. If the underlying expression value is missing, the value will be empty in returned data. Use of --expression-matrix/-em is not supported when also using the flag, "--sql".', +) + +parser_extract_assay_expression.add_argument( + "--delim", + "--delimiter", + type=lambda d: '\t' if d == '\\t' else str(d), + help='Always use exactly one of DELIMITER to separate fields to be printed; if no delimiter is provided with this flag, COMMA will be used. If a file is specified and no --delim argument is passed or is COMMA, the file suffix will be ".csv". If a file is specified and the --delim argument is TAB, the file suffix will be ".tsv". Otherwise, if a file is specified and "--delim" is neither COMMA or TAB file suffix will be ".txt".', +) + +parser_extract_assay_expression.add_argument( + "--output", + "-o", + type=str, + help='A local filename to be used, where "-" indicates printing to STDOUT. If -o/--output is not supplied, default behavior is to create a file with a constructed name in the current folder.', +) + +parser_extract_assay_expression.set_defaults(func=extract_assay_expression) +register_parser(parser_extract_assay_expression) + +##################################### +# create_cohort +##################################### +parser_create_cohort = subparsers.add_parser('create_cohort', help='Generates a new Cohort object on the platform from an existing Dataset or Cohort object and using list of IDs.', + description='Generates a new Cohort object on the platform from an existing Dataset or Cohort object and using list of IDs.', + prog="dx create_cohort", + parents=[stdout_args], + add_help=False) + +parser_create_cohort.add_argument('PATH', nargs='?', type=str, help='DNAnexus path for the new data object. If not provided, default behavior uses current project and folder, and will name the object identical to the assigned record-id.') +parser_create_cohort.add_argument('--from', required=True, type=str, help='v3.0 Dataset or Cohort object ID, project-id:record-id, where ":record-id" indicates the record-id in current selected project, or name') +parser_create_c_mutex_group = parser_create_cohort.add_mutually_exclusive_group(required=True) +parser_create_c_mutex_group.add_argument('--cohort-ids', type=str, help='A set of IDs used to subset the Dataset or Cohort object as a comma-separated string. IDs must match identically in the supplied Dataset. If a Cohort is supplied instead of a Dataset, the intersection of supplied and existing cohort IDs will be used to create the new cohort.') +parser_create_c_mutex_group.add_argument('--cohort-ids-file', type=str, help='A set of IDs used to subset the Dataset or Cohort object in a file with one ID per line and no header. IDs must match identically in the supplied Dataset. If a Cohort is supplied instead of a Dataset, the intersection of supplied and existing cohort IDs will be used to create the new cohort.') +parser_create_cohort.add_argument('-h','--help', help='Return the docstring and exit', action='help') + +parser_create_cohort.set_defaults(func=create_cohort) +register_parser(parser_create_cohort) + ##################################### # help diff --git a/src/python/dxpy/scripts/dx_app_wizard.py b/src/python/dxpy/scripts/dx_app_wizard.py index 882d085f7e..03e7500dbd 100755 --- a/src/python/dxpy/scripts/dx_app_wizard.py +++ b/src/python/dxpy/scripts/dx_app_wizard.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # # Copyright (C) 2013-2016 DNAnexus, Inc. # @@ -272,7 +272,7 @@ def main(**kwargs): if 'inputSpec' in app_json: for param in app_json['inputSpec']: - may_be_missing = param['optional'] and "default" not in param + may_be_missing = param.get('optional') and "default" not in param if param['class'] == 'file': param_list = optional_file_input_names if may_be_missing else required_file_input_names elif param['class'] == 'array:file': diff --git a/src/python/dxpy/scripts/dx_build_app.py b/src/python/dxpy/scripts/dx_build_app.py index ad8fc5434d..61048b8e37 100755 --- a/src/python/dxpy/scripts/dx_build_app.py +++ b/src/python/dxpy/scripts/dx_build_app.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # # Copyright (C) 2013-2016 DNAnexus, Inc. # @@ -21,7 +21,7 @@ import logging logging.basicConfig(level=logging.WARNING) -logging.getLogger('requests.packages.urllib3.connectionpool').setLevel(logging.ERROR) +logging.getLogger('urllib3.connectionpool').setLevel(logging.ERROR) import os, sys, json, subprocess, argparse import platform @@ -37,6 +37,9 @@ import dxpy.executable_builder from .. import logger +from dxpy.nextflow.nextflow_builder import build_pipeline_with_npi, prepare_nextflow +from dxpy.nextflow.nextflow_utils import get_resources_subpath, is_importer_job + from ..utils import json_load_raise_on_duplicates from ..utils.resolver import resolve_path, check_folder_exists, ResolutionError, is_container_id from ..utils.completer import LocalCompleter @@ -44,6 +47,7 @@ from ..exceptions import err_exit from ..utils.printing import BOLD from ..compat import open, USING_PYTHON2, decode_command_line_args, basestring +from ..cli.parsers import process_extra_args decode_command_line_args() @@ -194,7 +198,7 @@ def _find_readme(dirname): if 'name' in app_spec: if app_spec['name'] != app_spec['name'].lower(): logger.warn('name "%s" should be all lowercase' % (app_spec['name'],)) - if dirname != app_spec['name']: + if dirname != app_spec['name'] and not os.path.abspath(dxapp_json_filename).startswith("/tmp") and not dirname.startswith("."): logger.warn('app name "%s" does not match containing directory "%s"' % (app_spec['name'], dirname)) else: logger.warn('app is missing a name, please add one in the "name" field of dxapp.json') @@ -284,13 +288,26 @@ def check_bash(filename): 'Skipping bash syntax check due to unavailability of bash on Windows.') else: subprocess.check_output(["/bin/bash", "-n", filename], stderr=subprocess.STDOUT) - - if override_lang == 'python2.7': + python_unsure = False + if override_lang == 'python2.7' and USING_PYTHON2: + checker_fn = check_python + elif override_lang == 'python3' and not USING_PYTHON2: checker_fn = check_python elif override_lang == 'bash': checker_fn = check_bash elif filename.endswith('.py'): checker_fn = check_python + # don't enforce and ignore if the shebang is ambiguous and we're not sure + # that the file version is the same as the one we're running + read_mode = "r" + if USING_PYTHON2: + read_mode = "rb" + with open(filename, read_mode) as f: + first_line = f.readline() + if not (('python3' in first_line and not USING_PYTHON2) or + ('python2' in first_line and USING_PYTHON2)): + enforce = False + python_unsure = True elif filename.endswith('.sh'): checker_fn = check_bash else: @@ -314,6 +331,8 @@ def check_bash(filename): if enforce: raise DXSyntaxError(filename + " has a syntax error") except py_compile.PyCompileError as e: + if python_unsure: + print("Unsure if " + filename + " is using Python 2 or Python 3, the following error might not be relevant", file=sys.stderr) print(filename + " has a syntax error! Interpreter output:", file=sys.stderr) if USING_PYTHON2: errmsg = e.msg @@ -338,13 +357,13 @@ def _verify_app_source_dir_impl(src_dir, temp_dir, mode, enforce=True): if "interpreter" not in manifest['runSpec']: raise dxpy.app_builder.AppBuilderException('runSpec.interpreter field was not present') - if "release" not in manifest['runSpec'] or "distribution" not in manifest['runSpec']: - warn_message = 'runSpec.distribution or runSpec.release was not present. These fields ' - warn_message += 'will be required in a future version of the API. Recommended value ' - warn_message += 'for distribution is \"Ubuntu\" and release - \"14.04\".' - logger.warn(warn_message) + if "distribution" not in manifest['runSpec']: + raise dxpy.app_builder.AppBuilderException('Required field runSpec.distribution is not present') + + if "release" not in manifest['runSpec']: + raise dxpy.app_builder.AppBuilderException('Required field runSpec.release is not present') - if manifest['runSpec']['interpreter'] in ["python2.7", "bash"]: + if manifest['runSpec']['interpreter'] in ["python2.7", "bash", "python3"]: if "file" in manifest['runSpec']: entry_point_file = os.path.abspath(os.path.join(src_dir, manifest['runSpec']['file'])) try: @@ -487,7 +506,7 @@ def _parse_app_spec(src_dir): raise dxpy.app_builder.AppBuilderException("Could not parse dxapp.json file as JSON: " + str(e.args)) def _build_app_remote(mode, src_dir, publish=False, destination_override=None, - version_override=None, bill_to_override=None, dx_toolkit_autodep="stable", + version_override=None, bill_to_override=None, do_version_autonumbering=True, do_try_update=True, do_parallel_build=True, do_check_syntax=True, region=None, watch=True): if mode == 'app': @@ -497,7 +516,7 @@ def _build_app_remote(mode, src_dir, publish=False, destination_override=None, app_spec = _parse_app_spec(src_dir) builder_versions = {"12.04": "", "14.04": "_trusty", "16.04": "_xenial", - "20.04": "_focal"} + "20.04": "_focal", "24.04": "_noble"} release = app_spec['runSpec'].get('release') # Remote builder app/applet for 16.04 version 1 if release == "16.04" and app_spec['runSpec'].get('version', '0') == '1': @@ -508,7 +527,7 @@ def _build_app_remote(mode, src_dir, publish=False, destination_override=None, temp_dir = tempfile.mkdtemp() - build_options = {'dx_toolkit_autodep': dx_toolkit_autodep} + build_options = {} if version_override: build_options['version_override'] = version_override @@ -681,10 +700,10 @@ def _build_app_remote(mode, src_dir, publish=False, destination_override=None, shutil.rmtree(temp_dir) -def build_app_from(applet_id, version, publish=False, do_try_update=True, bill_to_override=None, +def build_app_from(applet_desc, version, publish=False, do_try_update=True, bill_to_override=None, return_object_dump=False, confirm=True, brief=False, **kwargs): - applet_desc = dxpy.api.applet_describe(applet_id) + applet_id = applet_desc["id"] app_name = applet_desc["name"] dxpy.executable_builder.verify_developer_rights('app-' + app_name) if not brief: @@ -708,7 +727,7 @@ def build_app_from(applet_id, version, publish=False, do_try_update=True, bill_t "details", "access", "ignoreReuse" ) inherited_metadata = {} - for field in fields_to_inherit: + for field in fields_to_inherit: if field in applet_desc: inherited_metadata[field] = applet_desc[field] @@ -716,7 +735,7 @@ def build_app_from(applet_id, version, publish=False, do_try_update=True, bill_t for field in required_non_empty: if field not in inherited_metadata or not inherited_metadata[field]: inherited_metadata[field] = applet_desc["name"] - + app_id = dxpy.app_builder.create_app_multi_region(regional_options, app_name, None, @@ -743,9 +762,9 @@ def build_app_from(applet_id, version, publish=False, do_try_update=True, bill_t def build_and_upload_locally(src_dir, mode, overwrite=False, archive=False, publish=False, destination_override=None, version_override=None, bill_to_override=None, use_temp_build_project=True, do_parallel_build=True, do_version_autonumbering=True, do_try_update=True, - dx_toolkit_autodep="stable", do_check_syntax=True, dry_run=False, + do_check_syntax=True, dry_run=False, return_object_dump=False, confirm=True, ensure_upload=False, force_symlinks=False, - region=None, brief=False, **kwargs): + region=None, brief=False, resources_dir=None, worker_resources_subpath="", **kwargs): dxpy.app_builder.build(src_dir, parallel_build=do_parallel_build) app_json = _parse_app_spec(src_dir) @@ -761,7 +780,7 @@ def build_and_upload_locally(src_dir, mode, overwrite=False, archive=False, publ enabled_regions = dxpy.app_builder.get_enabled_regions(app_json, region) - # Cannot build multi-region app if `use_temp_build_project` is falsy. + # Cannot build multi-region app if `use_temp_build_project` is false. if enabled_regions is not None and len(enabled_regions) > 1 and not use_temp_build_project: raise dxpy.app_builder.AppBuilderException("Cannot specify --no-temp-build-project when building multi-region apps") @@ -769,6 +788,12 @@ def build_and_upload_locally(src_dir, mode, overwrite=False, archive=False, publ projects_by_region = None if mode == "applet" and destination_override: working_project, override_folder, override_applet_name = parse_destination(destination_override) + if kwargs.get("name"): + if override_applet_name: + logger.warning("Name of the applet is set in both destination and extra args! " + "\"{}\" will be used!".format(kwargs.get("name"))) + override_applet_name = kwargs.get("name") + region = dxpy.api.project_describe(working_project, input_params={"fields": {"region": True}})["region"] projects_by_region = {region: working_project} @@ -832,7 +857,7 @@ def build_and_upload_locally(src_dir, mode, overwrite=False, archive=False, publ projects_by_region = {region: dest_project} if not overwrite and not archive: - # If we cannot overwite or archive an existing applet and an + # If we cannot overwrite or archive an existing applet and an # applet in the destination exists with the same name as this # one, then we should err out *before* uploading resources. try: @@ -850,10 +875,6 @@ def build_and_upload_locally(src_dir, mode, overwrite=False, archive=False, publ msg += " -f/--overwrite nor -a/--archive were given." raise dxpy.app_builder.AppBuilderException(msg) - if "buildOptions" in app_json: - if app_json["buildOptions"].get("dx_toolkit_autodep") is False: - dx_toolkit_autodep = False - if dry_run: # Set a dummy "projects_by_region" so that we can exercise the dry # run flows for uploading resources bundles and applets below. @@ -878,7 +899,9 @@ def build_and_upload_locally(src_dir, mode, overwrite=False, archive=False, publ folder=override_folder, ensure_upload=ensure_upload, force_symlinks=force_symlinks, - brief=brief) if not dry_run else [] + brief=brief, + resources_dir=resources_dir, + worker_resources_subpath=worker_resources_subpath) if not dry_run else [] # TODO: Clean up these applets if the app build fails. applet_ids_by_region = {} @@ -893,7 +916,6 @@ def build_and_upload_locally(src_dir, mode, overwrite=False, archive=False, publ project=project, override_folder=override_folder, override_name=override_applet_name, - dx_toolkit_autodep=dx_toolkit_autodep, dry_run=dry_run, brief=brief, **kwargs) @@ -979,6 +1001,42 @@ def build_and_upload_locally(src_dir, mode, overwrite=False, archive=False, publ if using_temp_project: dxpy.executable_builder.delete_temporary_projects(list(projects_by_region.values())) +def get_destination_region(destination): + """ + :param destination: The destination path for building the applet, as given by the --destination option to "dx build". Will be in the form [PROJECT_NAME_OR_ID:][/[FOLDER/][NAME]]. + :type destination: str + + :returns: The name of the region in which the applet will be built, e.g. 'aws:us-east-1'. It doesn't take into account the destination project specified in dxapp.json. + :rtype: str + """ + if destination: + dest_project_id, _, _ = parse_destination(destination) + else: + dest_project_id = dxpy.WORKSPACE_ID + return dxpy.api.project_describe(dest_project_id, input_params={"fields": {"region": True}})["region"] + + +def get_project_to_check(destination, extra_args): + # extra args overrides the destination argument + # so we're checking it first + if "project" in extra_args: + return extra_args["project"] + if destination: + dest_project_id, _, _ = parse_destination(destination) + # checkFeatureAccess is not implemented on the container + if dest_project_id.startswith("container-"): + dest_project_id = dxpy.PROJECT_CONTEXT_ID + return dest_project_id + else: + return dxpy.PROJECT_CONTEXT_ID + + +def verify_nf_license(destination, extra_args): + dest_project_to_check = get_project_to_check(destination, extra_args) + features = dxpy.DXHTTPRequest("/" + dest_project_to_check + "/checkFeatureAccess", {"features": ["dxNextflow"]}, always_retry=True).get("features", {}) + dx_nextflow_lic = features.get("dxNextflow", False) + if not dx_nextflow_lic: + raise dxpy.app_builder.AppBuilderException("PermissionDenied: billTo of the applet's destination project must have the dxNextflow feature enabled. For inquiries, please contact support@dnanexus.com") def _build_app(args, extra_args): """Builds an app or applet and returns the resulting executable ID @@ -987,74 +1045,76 @@ def _build_app(args, extra_args): TODO: remote app builds still return None, but we should fix this. """ - + resources_dir = None + source_dir = args.src_dir + worker_resources_subpath = "" # no subpath, files will be saved to root directory by default. + + if args.nextflow: + verify_nf_license(args.destination, extra_args) + + # determine if a nextflow applet ought to be built with Nextflow Pipeline Importer (NPI) app + build_nf_with_npi = any([x for x in [args.repository, args.cache_docker]]) + # this is to ensure to not call any more NPI executions if already inside an NPI job. + build_nf_with_npi = False if is_importer_job() else build_nf_with_npi + + if args.nextflow and (not build_nf_with_npi): + source_dir = prepare_nextflow( + resources_dir=args.src_dir, + profile=args.profile, + region=get_destination_region(args.destination), + cache_docker=args.cache_docker, + nextflow_pipeline_params=args.nextflow_pipeline_params + ) + resources_dir = args.src_dir + worker_resources_subpath = get_resources_subpath(resources_dir) if args._from: # BUILD FROM EXISTING APPLET - try: - output = build_app_from( - args._from, - [args.version_override], - publish=args.publish, - do_try_update=args.update, - bill_to_override=args.bill_to, - confirm=args.confirm, - return_object_dump=args.json, - brief=args.brief, - **extra_args - ) - if output is not None and args.run is None: - print(json.dumps(output)) - - except dxpy.app_builder.AppBuilderException as e: - # AppBuilderException represents errors during app building - # that could reasonably have been anticipated by the user. - print("Error: %s" % (e.args,), file=sys.stderr) - sys.exit(3) - except dxpy.exceptions.DXAPIError as e: - print("Error: %s" % (e,), file=sys.stderr) - sys.exit(3) + output = build_app_from( + args._from, + [args.version_override], + publish=args.publish, + do_try_update=args.update, + bill_to_override=args.bill_to, + confirm=args.confirm, + return_object_dump=args.json, + brief=args.brief, + **extra_args + ) + if output is not None and args.run is None: + print(json.dumps(output)) return output['id'] - if not args.remote: + if not args.remote and not build_nf_with_npi: # building with NF repository or cache_docker is done remotely by npi # LOCAL BUILD + output = build_and_upload_locally( + source_dir, + args.mode, + overwrite=args.overwrite, + archive=args.archive, + publish=args.publish, + destination_override=args.destination, + version_override=args.version_override, + bill_to_override=args.bill_to, + use_temp_build_project=args.use_temp_build_project, + do_parallel_build=args.parallel_build, + do_version_autonumbering=args.version_autonumbering, + do_try_update=args.update, + do_check_syntax=args.check_syntax, + ensure_upload=args.ensure_upload, + force_symlinks=args.force_symlinks, + dry_run=args.dry_run, + confirm=args.confirm, + return_object_dump=args.json, + region=args.region, + brief=args.brief, + resources_dir=resources_dir, + worker_resources_subpath=worker_resources_subpath, + **extra_args + ) - try: - output = build_and_upload_locally( - args.src_dir, - args.mode, - overwrite=args.overwrite, - archive=args.archive, - publish=args.publish, - destination_override=args.destination, - version_override=args.version_override, - bill_to_override=args.bill_to, - use_temp_build_project=args.use_temp_build_project, - do_parallel_build=args.parallel_build, - do_version_autonumbering=args.version_autonumbering, - do_try_update=args.update, - dx_toolkit_autodep=args.dx_toolkit_autodep, - do_check_syntax=args.check_syntax, - ensure_upload=args.ensure_upload, - force_symlinks=args.force_symlinks, - dry_run=args.dry_run, - confirm=args.confirm, - return_object_dump=args.json, - region=args.region, - brief=args.brief, - **extra_args - ) - - if output is not None and args.run is None: - print(json.dumps(output)) - except dxpy.app_builder.AppBuilderException as e: - # AppBuilderException represents errors during app or applet building - # that could reasonably have been anticipated by the user. - print("Error: %s" % (e.args,), file=sys.stderr) - sys.exit(3) - except dxpy.exceptions.DXAPIError as e: - print("Error: %s" % (e,), file=sys.stderr) - sys.exit(3) + if output is not None and args.run is None: + print(json.dumps(output)) if args.dry_run: return None @@ -1063,33 +1123,23 @@ def _build_app(args, extra_args): else: # REMOTE BUILD - - try: - app_json = _parse_app_spec(args.src_dir) - _check_suggestions(app_json, publish=args.publish) - _verify_app_source_dir(args.src_dir, args.mode) - if args.mode == "app" and not args.dry_run: - dxpy.executable_builder.verify_developer_rights('app-' + app_json['name']) - except dxpy.app_builder.AppBuilderException as e: - print("Error: %s" % (e.args,), file=sys.stderr) - sys.exit(3) - # The following flags might be useful in conjunction with # --remote. To enable these, we need to learn how to pass these # options through to the interior call of dx_build_app(let). + incompatible_options = "--cache-docker" if args.cache_docker else "--remote and --repository" if args.dry_run: - parser.error('--remote cannot be combined with --dry-run') + parser.error('{} cannot be combined with --dry-run'.format(incompatible_options)) if args.overwrite: - parser.error('--remote cannot be combined with --overwrite/-f') + parser.error('{} cannot be combined with --overwrite/-f'.format(incompatible_options)) if args.archive: - parser.error('--remote cannot be combined with --archive/-a') + parser.error('{} cannot be combined with --archive/-a'.format(incompatible_options)) # The following flags are probably not useful in conjunction # with --remote. if args.json: - parser.error('--remote cannot be combined with --json') + parser.error('{} cannot be combined with --json'.format(incompatible_options)) if not args.use_temp_build_project: - parser.error('--remote cannot be combined with --no-temp-build-project') + parser.error('{} cannot be combined with --no-temp-build-project'.format(incompatible_options)) if isinstance(args.region, list) and len(args.region) > 1: parser.error('--region can only be specified once for remote builds') @@ -1109,14 +1159,80 @@ def _build_app(args, extra_args): if not args.check_syntax: more_kwargs['do_check_syntax'] = False - return _build_app_remote(args.mode, args.src_dir, destination_override=args.destination, - publish=args.publish, dx_toolkit_autodep=args.dx_toolkit_autodep, + + if args.nextflow and build_nf_with_npi: + nf_scr = args.repository + if (not args.repository) and args.src_dir: + logger.info( + "Building nextflow pipeline with the Nextflow Pipeline Importer app. " + "Uploading the local nextflow source to the platform" + ) + dest_project = get_project_to_check(args.destination, extra_args) + _, dest_folder, _ = parse_destination(args.destination) + dest_folder = dest_folder or "" + upload_destination_dir = os.path.join( + dest_folder, ".nf_source" + ).strip("/") + qualified_upload_dest = ":".join([dest_project, "/" + upload_destination_dir + "/"]) + dest_folder_exists = False + + try: + dest_folder_exists = check_folder_exists( + project=dest_project, + path="/" + os.path.join(dest_folder, ".nf_source").strip("/"), + folder_name=os.path.basename(args.src_dir) + ) + except ResolutionError: + logger.info( + "Destination folder {} does not exist. Creating and uploading the pipeline source.".format( + qualified_upload_dest + ) + ) + if dest_folder_exists: + raise dxpy.app_builder.AppBuilderException( + "Folder {} exists in the project {}. Remove the directory to avoid file duplication and retry".format( + os.path.join(upload_destination_dir, os.path.basename(args.src_dir)), dest_project + ) + ) + else: + upload_cmd = ["dx", "upload", args.src_dir, "-r", "-o", qualified_upload_dest, "-p"] + _ = subprocess.check_output(upload_cmd) + nf_scr = os.path.join(qualified_upload_dest, os.path.basename(args.src_dir)) + return build_pipeline_with_npi( + repository=nf_scr, + tag=args.tag, + cache_docker=args.cache_docker, + docker_secrets=args.docker_secrets, + nextflow_pipeline_params=args.nextflow_pipeline_params, + profile=args.profile, + git_creds=args.git_credentials, + brief=args.brief, + destination=args.destination, + extra_args=extra_args + ) + app_json = _parse_app_spec(source_dir) + _check_suggestions(app_json, publish=args.publish) + _verify_app_source_dir(source_dir, args.mode) + if args.mode == "app" and not args.dry_run: + dxpy.executable_builder.verify_developer_rights('app-' + app_json['name']) + + return _build_app_remote(args.mode, source_dir, destination_override=args.destination, + publish=args.publish, region=region, watch=args.watch, **more_kwargs) def build(args): - executable_id = _build_app(args, - json.loads(args.extra_args) if args.extra_args else {}) + try: + process_extra_args(args) + executable_id = _build_app(args, args.extra_args or {}) + except dxpy.app_builder.AppBuilderException as e: + # AppBuilderException represents errors during app building + # that could reasonably have been anticipated by the user. + print("Error: %s" % (e.args,), file=sys.stderr) + sys.exit(3) + except dxpy.exceptions.DXAPIError as e: + print("Error: %s" % (e,), file=sys.stderr) + sys.exit(3) if args.run is not None: if executable_id is None: raise AssertionError('Expected executable_id to be set here') diff --git a/src/python/dxpy/scripts/dx_build_applet.py b/src/python/dxpy/scripts/dx_build_applet.py index 008423aa11..d274d60484 100755 --- a/src/python/dxpy/scripts/dx_build_applet.py +++ b/src/python/dxpy/scripts/dx_build_applet.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # # Copyright (C) 2013-2016 DNAnexus, Inc. # diff --git a/src/python/dxpy/system_requirements.py b/src/python/dxpy/system_requirements.py index 321169f5e3..d0eb768ac2 100644 --- a/src/python/dxpy/system_requirements.py +++ b/src/python/dxpy/system_requirements.py @@ -17,7 +17,8 @@ class SystemRequirementsDict(object): """ A class representing system requirements that can be passed as - "systemRequirements" to the class-xxxx/run API call (after converting + "systemRequirements" or an entry of the "systemRequirementsByExecutable" + to the class-xxxx/run or job/new API call (after converting it to a dictionary with as_dict()). """ @@ -40,7 +41,8 @@ def __init__(self, entrypoints): def from_instance_count(cls, instance_count_arg, entrypoint="*"): """ Returns a SystemRequirementsDict that can be passed as a - "systemRequirements" input to job/new or run/ API calls. + "systemRequirements" input or an entry of the "systemRequirementsByExecutable" mapping + to job/new or run/ API calls. The instance_count_arg should be either a: * string or int eg. "6" or 8 * dictionary, eg. {"main": 4, "other_function": 2} @@ -60,7 +62,8 @@ def from_instance_count(cls, instance_count_arg, entrypoint="*"): def from_instance_type(cls, instance_type_arg, entrypoint="*"): """ Returns SystemRequirementsDict that can be passed as a - "systemRequirements" input to job/new or run/ API calls. + "systemRequirements" input or an entry of the "systemRequirementsByExecutable" mapping + to job/new or run/ API calls. The instance_type_arg should be either a: * string, eg. mem1_ssd1_x2 * dictionary, eg. {"main": "mem2_hdd2_x2", "other_function": "mem2_hdd2_x1"} @@ -82,8 +85,9 @@ def from_sys_requirements(cls, system_requirements, _type='all'): It can extract only entrypoints with specific fields ('clusterSpec', 'instanceType', etc), depending on the value of _type. """ - if _type not in ('all', 'clusterSpec', 'instanceType'): - raise DXError("Expected '_type' to be either 'all', 'clusterSpec', or 'instanceType'") + allowed_types = ['all', 'clusterSpec', 'instanceType', 'fpgaDriver', 'nvidiaDriver'] + if _type not in (allowed_types): + raise DXError("Expected '_type' to be one of the following: {}".format(allowed_types)) if _type == 'all': return cls(system_requirements) @@ -97,7 +101,8 @@ def from_sys_requirements(cls, system_requirements, _type='all'): def override_cluster_spec(self, srd): """ Returns SystemRequirementsDict can be passed in a "systemRequirements" - input to app-xxx/run, e.g. {'fn': {'clusterSpec': {initialInstanceCount: 3, version: "2.4.0", ..}}} + input or as an entry of the "systemRequirementsByExecutable" mapping + to app-xxx/run, e.g. {'fn': {'clusterSpec': {initialInstanceCount: 3, version: "2.4.0", ..}}} Since full clusterSpec must be passed to the API server, we need to retrieve the cluster spec defined in app doc's systemRequirements and overwrite the field initialInstanceCount with the value the user passed to dx run for each entrypoint. @@ -162,6 +167,16 @@ def override_cluster_spec(self, srd): return SystemRequirementsDict(merged_cluster_spec) + def override_spec(self, requested_spec): + if "*" in requested_spec.as_dict(): + return requested_spec + + entrypoints = self.entrypoints.keys() | requested_spec.entrypoints.keys() + merged_spec = dict.fromkeys(entrypoints) + for e in entrypoints: + merged_spec[e] = requested_spec.entrypoints.get(e) or requested_spec.entrypoints.get("*") or self.entrypoints.get(e) or self.entrypoints.get("*") + return SystemRequirementsDict(merged_spec) + def _add_dict_values(self, d1, d2): """ Merges the values of two dictionaries, which are expected to be dictionaries, e.g diff --git a/src/python/dxpy/templating/templates/nextflow/dxapp.json b/src/python/dxpy/templating/templates/nextflow/dxapp.json new file mode 100644 index 0000000000..fb67420031 --- /dev/null +++ b/src/python/dxpy/templating/templates/nextflow/dxapp.json @@ -0,0 +1,109 @@ +{ + "title": "Nextflow Pipeline", + "summary": "Nextflow Pipeline", + "dxapi": "1.0.0", + "version": "1.0.0", + "types": ["nextflow"], + "inputSpec": [ + { + "name": "nextflow_run_opts", + "label": "Nextflow Run Options", + "help": "Additional run arguments for Nextflow (e.g. -profile docker).", + "class": "string", + "group": "Nextflow options", + "optional": true + }, + { + "name": "nextflow_top_level_opts", + "label": "Nextflow Top-level Options", + "help": "Additional top-level options for Nextflow (e.g. -quiet).", + "class": "string", + "group": "Nextflow options", + "optional": true + }, + { + "name": "nextflow_pipeline_params", + "label": "Nextflow Pipeline Parameters", + "help": "Additional pipeline parameters for Nextflow. Must be preceded with double dash characters (e.g. --foo, which can be accessed in the pipeline script using the params.foo identifier).", + "class": "string", + "group": "Additional pipeline parameters", + "optional": true + }, + { + "name": "docker_creds", + "label": "Docker Credentials", + "help": "Docker credentials used to obtain private docker images.", + "class": "file", + "optional": true + }, + { + "name": "debug", + "label": "Debug Mode", + "help": "Shows additional information in the job log. If true, the execution log messages from Nextflow will also be included.", + "class": "boolean", + "group": "Advanced Executable Development Options", + "default": false + }, + { + "name": "resume", + "label": "Resume", + "help": "Unique ID of the previous session to be resumed. If 'true' or 'last' is provided instead of the sessionID, will resume the latest resumable session run by an applet with the same name in the current project in the last 6 months.", + "class": "string", + "group": "Advanced Executable Development Options", + "optional": true + }, + { + "name": "preserve_cache", + "label": "Preserve Cache", + "help": "Enable storing pipeline cache and local working files to the current project. If true, local working files and cache files will be uploaded to the platform, so the current session could be resumed in the future", + "class": "boolean", + "group": "Advanced Executable Development Options", + "default": false + }, + { + "name": "nextflow_soft_confs", + "label": "Soft Configuration File", + "help": "(Optional) One or more nextflow configuration files to be appended to the Nextflow pipeline configuration set", + "class": "array:file", + "patterns": ["*.config"], + "optional": true, + "group": "Nextflow options" + }, + { + "name": "nextflow_params_file", + "label": "Script Parameters File", + "help": "(Optional) A file, in YAML or JSON format, for specifying input parameter values", + "class": "file", + "patterns": ["*.yml", "*.yaml", "*.json"], + "optional": true, + "group": "Nextflow options" + } + ], + "outputSpec": [ + { + "name": "published_files", + "label": "Published files of Nextflow pipeline", + "help": "Output files published by current Nextflow pipeline and uploaded to the job output destination.", + "class": "array:file", + "optional": true + } + ], + "runSpec": { + "headJobOnDemand": true, + "restartableEntryPoints": "all", + "interpreter": "bash", + "execDepends": [], + "distribution": "Ubuntu", + "release": "20.04", + "version": "0" + }, + "details": { + "whatsNew": "1.0.0: Initial version" + }, + "categories": [], + "access": { + "network": ["*"], + "project": "UPLOAD" + }, + "ignoreReuse": true +} diff --git a/src/python/dxpy/templating/templates/nextflow/src/nextflow.sh b/src/python/dxpy/templating/templates/nextflow/src/nextflow.sh new file mode 100644 index 0000000000..08aa4812ab --- /dev/null +++ b/src/python/dxpy/templating/templates/nextflow/src/nextflow.sh @@ -0,0 +1,656 @@ +#!/usr/bin/env bash + +# ========================================================= +# Script containing +# 1. Nextflow head (orchestrator) job entry, exit functions +# 2. Nextflow task subjob entry, exit functions +# 3. Helper functions +# They are currently in one file because supporting use +# of the resources folder for Nextflow Pipeline Applets +# would require more changes; currently the pipeline +# source folder completely replaces the resources folder. +# ========================================================= + +set -f + +AWS_ENV="$HOME/.dx-aws.env" +USING_S3_WORKDIR=false +LOGS_DIR="$HOME/.log/" + +# How long to let a subjob with error keep running for Nextflow to handle it +# before we end the DX job, in seconds +MAX_WAIT_AFTER_JOB_ERROR=240 +# How often to check when waiting for a subjob with error, in seconds +WAIT_INTERVAL=15 + +# ========================================================= +# 1. Nextflow head (orchestrator) job +# ========================================================= + +main() { + if [[ $debug == true ]]; then + export NXF_DEBUG=2 + TRACE_CMD="-trace nextflow.plugin" + env | grep -v DX_SECURITY_CONTEXT | sort + set -x + fi + + detect_nextaur_plugin_version + + # unset properties + cloned_job_properties=$(dx describe "$DX_JOB_ID" --json | jq -r '.properties | to_entries[] | select(.key | startswith("nextflow")) | .key') + [[ -z $cloned_job_properties ]] || dx unset_properties "$DX_JOB_ID" $cloned_job_properties + + # check if all run opts provided by user are supported + validate_run_opts + + # set default NXF env constants + export NXF_HOME=/opt/nextflow + export NXF_ANSI_LOG=false + export NXF_PLUGINS_DEFAULT=nextaur@$NXF_PLUGINS_VERSION + export NXF_EXECUTOR='dnanexus' + + # use /home/dnanexus/nextflow_execution as the temporary nextflow execution folder + mkdir -p /home/dnanexus/nextflow_execution + cd /home/dnanexus/nextflow_execution + + # make runtime parameter arguments from applet inputs + set +x + applet_runtime_inputs=() + @@APPLET_RUNTIME_PARAMS@@ + if [[ $debug == true ]]; then + if [[ "${#applet_runtime_inputs}" -gt 0 ]]; then + echo "Will specify the following runtime parameters:" + printf "[%s] " "${applet_runtime_inputs[@]}" + echo + else + echo "No runtime parameter is specified. Will use the default values." + fi + set -x + fi + + # get job output destination + DX_JOB_OUTDIR=$(jq -r '[.project, .folder] | join(":")' /home/dnanexus/dnanexus-job.json) + # initiate log file + LOG_NAME="nextflow-$DX_JOB_ID.log" + + # get current executable name + EXECUTABLE_NAME=$(jq -r .executableName /home/dnanexus/dnanexus-job.json) + + # download default applet file type inputs + dx-download-all-inputs --parallel @@EXCLUDE_INPUT_DOWNLOAD@@ 2>/dev/null 1>&2 + RUNTIME_CONFIG_CMD='' + RUNTIME_PARAMS_FILE='' + [[ -d "$HOME/in/nextflow_soft_confs/" ]] && RUNTIME_CONFIG_CMD=$(find "$HOME"/in/nextflow_soft_confs -name "*.config" -type f -printf "-c %p ") + [[ -d "$HOME/in/nextflow_params_file/" ]] && RUNTIME_PARAMS_FILE=$(find "$HOME"/in/nextflow_params_file -type f -printf "-params-file %p ") + if [[ -d "$HOME/in/docker_creds" ]]; then + CREDENTIALS=$(find "$HOME/in/docker_creds" -type f | head -1) + [[ -s $CREDENTIALS ]] && docker_registry_login || echo "no docker credential available" + dx upload "$CREDENTIALS" --path "$DX_WORKSPACE_ID:/dx_docker_creds" --brief --wait --no-progress || true + fi + + # First Nextflow run, only to parse & save config required for AWS login + local env_job_suffix='-GET-ENV' + declare -a NEXTFLOW_CMD_ENV="(nextflow \ + ${TRACE_CMD} \ + $nextflow_top_level_opts \ + ${RUNTIME_CONFIG_CMD} \ + -log ${LOGS_DIR}${LOG_NAME}${env_job_suffix} \ + run @@RESOURCES_SUBPATH@@ \ + $profile_arg \ + -name ${DX_JOB_ID}${env_job_suffix} \ + $nextflow_run_opts \ + $RUNTIME_PARAMS_FILE \ + $nextflow_pipeline_params)" + + NEXTFLOW_CMD_ENV+=("${applet_runtime_inputs[@]}") + + AWS_ENV="$HOME/.dx-aws.env" + + get_nextflow_environment "${NEXTFLOW_CMD_ENV[@]}" + dx download "$DX_WORKSPACE_ID:/.dx-aws.env" -o $AWS_ENV -f --no-progress 2>/dev/null || true + + # Login to AWS, if configured + aws_login + aws_relogin_loop & AWS_RELOGIN_PID=$! + + set_vars_session_and_cache + + if [[ $preserve_cache == true ]]; then + set_job_properties_cache + check_cache_db_storage_limit + if [[ -n $resume ]]; then + check_no_concurrent_job_same_cache + fi + fi + + RESUME_CMD="" + if [[ -n $resume ]]; then + restore_cache_and_set_resume_cmd + fi + + # Set Nextflow workdir based on S3 workdir / preserve_cache options + setup_workdir + + # set beginning timestamp + BEGIN_TIME="$(date +"%Y-%m-%d %H:%M:%S")" + + # Start Nextflow run + declare -a NEXTFLOW_CMD="(nextflow \ + ${TRACE_CMD} \ + $nextflow_top_level_opts \ + ${RUNTIME_CONFIG_CMD} \ + -log ${LOGS_DIR}${LOG_NAME} \ + run @@RESOURCES_SUBPATH@@ \ + $profile_arg \ + -name ${DX_JOB_ID} \ + $RESUME_CMD \ + $nextflow_run_opts \ + $RUNTIME_PARAMS_FILE \ + $nextflow_pipeline_params)" + + NEXTFLOW_CMD+=("${applet_runtime_inputs[@]}") + + trap on_exit EXIT + log_context_info + + "${NEXTFLOW_CMD[@]}" & NXF_EXEC_PID=$! + set +x + if [[ $debug == true ]] ; then + # Forward Nextflow log to job log + touch "${LOGS_DIR}${LOG_NAME}" + tail --follow -n 0 "${LOGS_DIR}${LOG_NAME}" -s 60 >&2 & LOG_MONITOR_PID=$! + disown $LOG_MONITOR_PID + set -x + fi + + # After Nextflow run + wait $NXF_EXEC_PID + ret=$? + + kill "$AWS_RELOGIN_PID" + exit $ret +} + +on_exit() { + ret=$? + + properties=$(dx describe ${DX_JOB_ID} --json 2>/dev/null | jq -r ".properties") + if [[ $properties != "null" ]]; then + if [[ $(jq .nextflow_errorStrategy <<<${properties} -r) == "ignore" ]]; then + ignored_subjobs=$(jq .nextflow_errored_subjobs <<<${properties} -r) + if [[ ${ignored_subjobs} != "null" ]]; then + echo "Subjob(s) ${ignored_subjobs} ran into Nextflow process errors. \"ignore\" errorStrategy was applied." + fi + fi + fi + set +x + if [[ $debug == true ]]; then + # DEVEX-1943 Wait up to 30 seconds for log forwarders to terminate + set +e + i=0 + while [[ $i -lt 30 ]]; + do + if kill -0 "$LOG_MONITOR_PID" 2>/dev/null; then + sleep 1 + else + break + fi + ((i++)) + done + kill $LOG_MONITOR_PID 2>/dev/null || true + set -xe + fi + + if [[ $preserve_cache == true ]]; then + echo "=== Execution completed — caching current session to $DX_CACHEDIR/$NXF_UUID" + upload_session_cache_file + else + echo "=== Execution completed — cache and working files will not be resumable" + fi + + # remove .nextflow from the current folder /home/dnanexus/nextflow_execution + rm -rf .nextflow + + if [[ -s "${LOGS_DIR}${LOG_NAME}" ]]; then + echo "=== Execution completed — upload nextflow log to job output destination ${DX_JOB_OUTDIR%/}/" + NEXFLOW_LOG_ID=$(dx upload "${LOGS_DIR}${LOG_NAME}" --path "${DX_JOB_OUTDIR%/}/${LOG_NAME}" --wait --brief --no-progress --parents) && + echo "Upload nextflow log as file: $NEXFLOW_LOG_ID" || + echo "Failed to upload log file of current session $NXF_UUID" + else + echo "=== Execution completed — no nextflow log file available." + fi + + if [[ $ret -ne 0 ]]; then + echo "=== Execution failed — skip uploading published files to job output destination ${DX_JOB_OUTDIR%/}/" + + else + echo "=== Execution succeeded — upload published files to job output destination ${DX_JOB_OUTDIR%/}/" + mkdir -p /home/dnanexus/out/published_files + find . -type f -newermt "$BEGIN_TIME" -exec cp --parents {} /home/dnanexus/out/published_files/ \; -delete + dx-upload-all-outputs --parallel --wait-on-close || echo "No published files has been generated." + fi + exit $ret +} + +# ========================================================= +# 2. Nextflow task subjobs +# ========================================================= + +nf_task_entry() { + CREDENTIALS="$HOME/docker_creds" + dx download "$DX_WORKSPACE_ID:/dx_docker_creds" -o $CREDENTIALS --recursive --no-progress -f 2>/dev/null || true + [[ -f $CREDENTIALS ]] && docker_registry_login || echo "no docker credential available" + dx download "$DX_WORKSPACE_ID:/.dx-aws.env" -o $AWS_ENV -f --no-progress 2>/dev/null || true + aws_login + aws_relogin_loop & AWS_RELOGIN_PID=$! + # capture the exit code + trap nf_task_exit EXIT + + download_cmd_launcher_file + + # enable debugging mode + [[ $NXF_DEBUG ]] && set -x + + # run the task + set +e + bash .command.run > >(tee .command.log) 2>&1 + export exit_code=$? + kill "$AWS_RELOGIN_PID" + dx set_properties ${DX_JOB_ID} nextflow_exit_code=$exit_code + set -e +} + +nf_task_exit() { + if [ -f .command.log ]; then + if [[ $USING_S3_WORKDIR == true ]]; then + aws s3 cp .command.log "${cmd_log_file}" + else + dx upload .command.log --path "${cmd_log_file}" --brief --wait --no-progress || true + fi + else + >&2 echo "Missing Nextflow .command.log file" + fi + + # exit_code should already be set in nf_task_entry(); default just in case + # This is just for including as DX output; Nextflow internally uses .exitcode file + if [ -z ${exit_code} ]; then export exit_code=0; fi + + if [ "$exit_code" -ne 0 ]; then wait_for_terminate_or_retry; fi + + # There are cases where the Nextflow task had an error but we don't want to fail the whole + # DX job exec tree, e.g. because the error strategy should continue, + # so we let the DX job succeed but this output records Nextflow's exit code + dx-jobutil-add-output exit_code $exit_code --class=int +} + +# ========================================================= +# Helpers: Docker login +# ========================================================= + +# Logs the user to the docker registry. +# Uses docker credentials that have to be in $CREDENTIALS location. +# Format of the file: +# { +# docker_registry: { +# "registry": "", +# "username": "", +# "organization": "<(optional, default value equals username) organization as defined by DockerHub or Quay.io>", +# "token": "" +# } +# } +docker_registry_login() { + export REGISTRY=$(jq '.docker_registry.registry' "$CREDENTIALS" | tr -d '"') + export REGISTRY_USERNAME=$(jq '.docker_registry.username' "$CREDENTIALS" | tr -d '"') + export REGISTRY_ORGANIZATION=$(jq '.docker_registry.organization' "$CREDENTIALS" | tr -d '"') + if [[ -z $REGISTRY_ORGANIZATION || $REGISTRY_ORGANIZATION == "null" ]]; then + export REGISTRY_ORGANIZATION=$REGISTRY_USERNAME + fi + + if [[ -z $REGISTRY || $REGISTRY == "null" \ + || -z $REGISTRY_USERNAME || $REGISTRY_USERNAME == "null" ]]; then + echo "Error parsing the credentials file. The expected format to specify a Docker registry is: " + echo "{" + echo " \"docker_registry\": {" + echo " \"registry\": \"\"", + echo " \"username\": \"\"", + echo " \"organization\": \"<(optional, default value equals username) organization as defined by DockerHub or Quay.io>\"", + echo " \"token\": \"\"" + echo " }" + echo "}" + exit 1 + fi + + jq '.docker_registry.token' "$CREDENTIALS" -r | docker login $REGISTRY --username $REGISTRY_USERNAME --password-stdin 2> >(grep -v -E "WARNING! Your password will be stored unencrypted in |Configure a credential helper to remove this warning. See|https://docs.docker.com/engine/reference/commandline/login/#credentials-store") + if [ ! $? -eq 0 ]; then + echo "Docker authentication failed, please check if the docker credentials file is correct." 1>&2 + exit 2 + fi +} + +# ========================================================= +# Helpers: AWS login, job id tokens +# ========================================================= + +aws_login() { + if [ -f "$AWS_ENV" ]; then + source $AWS_ENV + detect_if_using_s3_workdir + + # aws env file example values: + # "iamRoleArnToAssume", "jobTokenAudience", "jobTokenSubjectClaims", "awsRegion" + roleSessionName="dnanexus_${DX_JOB_ID}" + job_id_token=$(dx-jobutil-get-identity-token --aud ${jobTokenAudience} --subject_claims ${jobTokenSubjectClaims}) + output=$(aws sts assume-role-with-web-identity --role-arn $iamRoleArnToAssume --role-session-name $roleSessionName --web-identity-token $job_id_token --duration-seconds 3600) + mkdir -p /home/dnanexus/.aws/ + + cat < /home/dnanexus/.aws/credentials +[default] +aws_access_key_id = $(echo "$output" | jq -r '.Credentials.AccessKeyId') +aws_secret_access_key = $(echo "$output" | jq -r '.Credentials.SecretAccessKey') +aws_session_token = $(echo "$output" | jq -r '.Credentials.SessionToken') +EOF + cat < /home/dnanexus/.aws/config +[default] +region = $awsRegion +EOF + echo "Successfully authenticated to AWS - $(aws sts get-caller-identity)" + fi +} + +aws_relogin_loop() { + while true; do + sleep 3300 # relogin every 55 minutes, first login is done separately, so we wait before the login + if [ -f "$AWS_ENV" ]; then + aws_login + fi + done +} + +# ========================================================= +# Helpers: workdir configuration +# ========================================================= + +detect_if_using_s3_workdir() { + if [[ -f "$AWS_ENV" ]]; then + source $AWS_ENV + fi + + if [[ -n $workdir && $workdir != "null" ]]; then + USING_S3_WORKDIR=true + fi +} + +setup_workdir() { + if [[ -f "$AWS_ENV" ]]; then + source $AWS_ENV + fi + + if [[ -n $workdir && $workdir != "null" ]]; then + # S3 work dir was specified, use that + NXF_WORK="${workdir}/${NXF_UUID}/work" + USING_S3_WORKDIR=true + elif [[ $preserve_cache == true ]]; then + # Work dir on platform and using cache, use project + [[ -n $resume ]] || dx mkdir -p $DX_CACHEDIR/$NXF_UUID/work/ + NXF_WORK="dx://$DX_CACHEDIR/$NXF_UUID/work/" + else + # Work dir on platform and not using cache, use workspace + NXF_WORK="dx://$DX_WORKSPACE_ID:/work/" + fi + + export NXF_WORK +} + +# ========================================================= +# Helpers: basic run +# ========================================================= + +validate_run_opts() { + profile_arg="@@PROFILE_ARG@@" + + IFS=" " read -r -a opts <<<"$nextflow_run_opts" + for opt in "${opts[@]}"; do + case $opt in + -w=* | -work-dir=* | -w | -work-dir) + dx-jobutil-report-error "Please remove workDir specification (-w|-work-dir path) in nextflow_run_opts. For Nextflow runs on DNAnexus, the workdir will be located at 1) In the workspace container-xxx 2) In project-yyy:/.nextflow_cache_db if preserve_cache=true, or 3) on S3, if specified." + ;; + -profile | -profile=*) + if [ -n "$profile_arg" ]; then + echo "Profile was given in run options... overriding the default profile ($profile_arg)" + profile_arg="" + fi + ;; + *) ;; + esac + done +} + +detect_nextaur_plugin_version() { + executable=$(cat dnanexus-executable.json | jq -r .id ) + bundled_dependency=$(dx describe ${executable} --json | jq -r '.runSpec.bundledDepends[] | select(.name=="nextaur.tar.gz") | .id."$dnanexus_link"') + asset_dependency=$(dx describe ${bundled_dependency} --json | jq -r .properties.AssetBundle) + export NXF_PLUGINS_VERSION=$(dx describe ${asset_dependency} --json | jq -r .properties.version) +} + +get_nextflow_environment() { + NEXTFLOW_CMD_ENV=("$@") + set +e + ENV_OUTPUT=$("${NEXTFLOW_CMD_ENV[@]}" 2>&1) + ENV_EXIT=$? + set -e + + if [ $ENV_EXIT -ne 0 ]; then + echo "$ENV_OUTPUT" + return $ENV_EXIT + else + echo "$ENV_OUTPUT" > "/home/dnanexus/.dx_get_env.log" + fi +} + +log_context_info() { + echo "=============================================================" + echo "=== NF projectDir : @@RESOURCES_SUBPATH@@" + echo "=== NF session ID : ${NXF_UUID}" + echo "=== NF log file : dx://${DX_JOB_OUTDIR%/}/${LOG_NAME}" + echo "=== NF workdir : ${NXF_WORK}" + if [[ $preserve_cache == true ]]; then + echo "=== NF cache folder : dx://${DX_CACHEDIR}/${NXF_UUID}/" + fi + echo "=== NF command :" "${NEXTFLOW_CMD[@]}" + echo "=== Built with dxpy : @@DXPY_BUILD_VERSION@@" + echo "=============================================================" +} + +wait_for_terminate_or_retry() { + terminate_record=$(dx find data --name $DX_JOB_ID --path $DX_WORKSPACE_ID:/.TERMINATE --brief | head -n 1) + if [ -n "${terminate_record}" ]; then + echo "Subjob exited with non-zero exit_code and the errorStrategy is terminate." + echo "Waiting for the head job to kill the job tree..." + sleep $MAX_WAIT_AFTER_JOB_ERROR + echo "This subjob was not killed in time, exiting to prevent excessive waiting." + exit + fi + + retry_record=$(dx find data --name $DX_JOB_ID --path $DX_WORKSPACE_ID:/.RETRY --brief | head -n 1) + if [ -n "${retry_record}" ]; then + wait_period=0 + echo "Subjob exited with non-zero exit_code and the errorStrategy is retry." + echo "Waiting for the head job to kill the job tree or for instruction to continue..." + while true + do + errorStrategy_set=$(dx describe $DX_JOB_ID --json | jq .properties.nextflow_errorStrategy -r) + if [ "$errorStrategy_set" = "retry" ]; then + break + fi + wait_period=$(($wait_period+$WAIT_INTERVAL)) + if [ $wait_period -ge $MAX_WAIT_AFTER_JOB_ERROR ];then + echo "This subjob was not killed in time, exiting to prevent excessive waiting." + break + else + echo "No instruction to continue was given. Waiting for ${WAIT_INTERVAL} seconds" + sleep $WAIT_INTERVAL + fi + done + fi +} + +download_cmd_launcher_file() { + if [[ $USING_S3_WORKDIR == true ]]; then + aws s3 cp "${cmd_launcher_file}" .command.run.tmp + else + dx download "${cmd_launcher_file}" --output .command.run.tmp + fi + + # remove the line in .command.run to disable printing env vars if debugging is on + cat .command.run.tmp | sed 's/\[\[ $NXF_DEBUG > 0 ]] && nxf_env//' > .command.run +} + +# ========================================================= +# Helpers: run with preserve cache, resume +# ========================================================= + +set_vars_session_and_cache() { + # Path in project to store cached sessions + export DX_CACHEDIR="${DX_PROJECT_CONTEXT_ID}:/.nextflow_cache_db" + + # Using the lenient mode to caching makes it possible to reuse working files for resume on the platform + export NXF_CACHE_MODE=LENIENT + + # If resuming session, use resume id; otherwise create id for this session + if [[ -n $resume ]]; then + get_resume_session_id + else + NXF_UUID=$(uuidgen) + fi + export NXF_UUID +} + +get_resume_session_id() { + if [[ $resume == 'true' || $resume == 'last' ]]; then + # find the latest job run by applet with the same name + echo "Will try to find the session ID of the latest session run by $EXECUTABLE_NAME." + PREV_JOB_SESSION_ID=$( + dx api system findExecutions \ + '{"state":["done","failed"], + "created": {"after":'$((($(date +%s) - 6 * 60 * 60 * 24 * 30) * 1000))'}, + "project":"'$DX_PROJECT_CONTEXT_ID'", + "limit":1, + "includeSubjobs":false, + "describe":{"fields":{"properties":true}}, + "properties":{ + "nextflow_session_id":true, + "nextflow_preserve_cache":"true", + "nextflow_executable":"'$EXECUTABLE_NAME'"}}' 2>/dev/null | + jq -r '.results[].describe.properties.nextflow_session_id' + ) + + [[ -n $PREV_JOB_SESSION_ID ]] || + dx-jobutil-report-error "Cannot find any jobs within the last 6 months to resume from. Please provide the exact sessionID for \"resume\" value or run without resume." + else + PREV_JOB_SESSION_ID=$resume + fi + + valid_id_pattern='^\{?[A-Z0-9a-z]{8}-[A-Z0-9a-z]{4}-[A-Z0-9a-z]{4}-[A-Z0-9a-z]{4}-[A-Z0-9a-z]{12}\}?$' + [[ "$PREV_JOB_SESSION_ID" =~ $valid_id_pattern ]] || + dx-jobutil-report-error "Invalid resume value. Please provide either \"true\", \"last\", or \"sessionID\". + If a sessionID was provided, Nextflow cached content could not be found under $DX_CACHEDIR/$PREV_JOB_SESSION_ID/. + Please provide the exact sessionID for \"resume\" or run without resume." + + NXF_UUID=$PREV_JOB_SESSION_ID +} + +set_job_properties_cache() { + # Set properties on job, which can be used to look up cached job later + dx set_properties "$DX_JOB_ID" \ + nextflow_executable="$EXECUTABLE_NAME" \ + nextflow_session_id="$NXF_UUID" \ + nextflow_preserve_cache="$preserve_cache" +} + +check_cache_db_storage_limit() { + # Enforce a limit on cached session workdirs stored in the DNAnexus project + # Removal must be manual, because applet can only upload, not delete project files + # Limit does not apply when the workdir is external (e.g. S3) + + MAX_CACHE_STORAGE=20 + existing_cache=$(dx ls $DX_CACHEDIR --folders 2>/dev/null | wc -l) + [[ $existing_cache -lt $MAX_CACHE_STORAGE || $USING_S3_WORKDIR == true ]] || + dx-jobutil-report-error "The limit for preserved sessions in the project is $MAX_CACHE_STORAGE. Please remove folders from $DX_CACHEDIR to be under the limit, run without preserve_cache=true, or use S3 as workdir." +} + +check_no_concurrent_job_same_cache() { + # Do not allow more than 1 concurrent run with the same session id, + # to prevent conflicting workdir contents + + FIRST_RESUMED_JOB=$( + dx api system findExecutions \ + '{"state":["idle", "waiting_on_input", "runnable", "running", "debug_hold", "waiting_on_output", "restartable", "terminating"], + "project":"'$DX_PROJECT_CONTEXT_ID'", + "includeSubjobs":false, + "properties":{ + "nextflow_session_id":"'$NXF_UUID'", + "nextflow_preserve_cache":"true", + "nextflow_executable":"'$EXECUTABLE_NAME'"}}' 2>/dev/null | + jq -r '.results[-1].id // empty' + ) + + [[ -n $FIRST_RESUMED_JOB && $DX_JOB_ID == $FIRST_RESUMED_JOB ]] || + dx-jobutil-report-error "There is at least one other non-terminal state job with the same sessionID $NXF_UUID. + Please wait until all other jobs sharing the same sessionID to enter their terminal state and rerun, + or run without preserve_cache set to true." +} + +restore_cache_and_set_resume_cmd() { + # download latest cache.tar from $DX_CACHEDIR/$PREV_JOB_SESSION_ID/ + PREV_JOB_CACHE_FILE=$( + dx api system findDataObjects \ + '{"visibility": "either", + "name":"cache.tar", + "scope": { + "project": "'$DX_PROJECT_CONTEXT_ID'", + "folder": "/.nextflow_cache_db/'$NXF_UUID'", + "recurse": false}, + "describe": true}' 2>/dev/null | + jq -r '.results | sort_by(.describe.created)[-1] | .id // empty' + ) + + [[ -n $PREV_JOB_CACHE_FILE ]] || + dx-jobutil-report-error "Cannot find any $DX_CACHEDIR/$PREV_JOB_SESSION_ID/cache.tar. Please provide a valid sessionID." + + local ret + ret=$(dx download $PREV_JOB_CACHE_FILE --no-progress -f -o cache.tar 2>&1) || + { + if [[ $ret == *"FileNotFoundError"* || $ret == *"ResolutionError"* ]]; then + dx-jobutil-report-error "Nextflow cached content cannot be downloaded from $DX_CACHEDIR/$PREV_JOB_SESSION_ID/cache.tar. Please provide the exact sessionID for \"resume\" value or run without resume." + else + dx-jobutil-report-error "$ret" + fi + } + + # untar cache.tar, which needs to contain + # 1. cache folder .nextflow/cache/$PREV_JOB_SESSION_ID + tar -xf cache.tar + [[ -n "$(ls -A .nextflow/cache/$PREV_JOB_SESSION_ID)" ]] || + dx-jobutil-report-error "Previous execution cache of session $PREV_JOB_SESSION_ID is empty." + rm cache.tar + + # resume succeeded, set session id and add it to job properties + echo "Will resume from previous session: $PREV_JOB_SESSION_ID" + RESUME_CMD="-resume $NXF_UUID" + dx tag "$DX_JOB_ID" "resumed" +} + +upload_session_cache_file() { + # wrap cache folder and upload cache.tar + if [[ -n "$(ls -A .nextflow)" ]]; then + tar -cf cache.tar .nextflow + + CACHE_ID=$(dx upload "cache.tar" --path "$DX_CACHEDIR/$NXF_UUID/cache.tar" --no-progress --brief --wait -p -r) && + echo "Upload cache of current session as file: $CACHE_ID" && + rm -f cache.tar || + echo "Failed to upload cache of current session $NXF_UUID" + else + echo "No cache is generated from this execution. Skip uploading cache." + fi +} diff --git a/src/python/dxpy/templating/templates/python/basic/src/code.py b/src/python/dxpy/templating/templates/python/basic/src/code.py index 14486b3811..6d09ad0550 100644 --- a/src/python/dxpy/templating/templates/python/basic/src/code.py +++ b/src/python/dxpy/templating/templates/python/basic/src/code.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # DX_APP_WIZARD_NAME DX_APP_WIZARD_VERSION # Generated by dx-app-wizard. # diff --git a/src/python/dxpy/templating/templates/python/basic/test/test.py b/src/python/dxpy/templating/templates/python/basic/test/test.py index d2b78ba5de..4322cc6da1 100755 --- a/src/python/dxpy/templating/templates/python/basic/test/test.py +++ b/src/python/dxpy/templating/templates/python/basic/test/test.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # DX_APP_WIZARD_NAME DX_APP_WIZARD_VERSION test suite # Generated by dx-app-wizard. diff --git a/src/python/dxpy/templating/templates/python/parallelized/src/code.py b/src/python/dxpy/templating/templates/python/parallelized/src/code.py index 8c483e5275..47dd31f966 100644 --- a/src/python/dxpy/templating/templates/python/parallelized/src/code.py +++ b/src/python/dxpy/templating/templates/python/parallelized/src/code.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # DX_APP_WIZARD_NAME DX_APP_WIZARD_VERSION # Generated by dx-app-wizard. # diff --git a/src/python/dxpy/templating/templates/python/parallelized/test/test.py b/src/python/dxpy/templating/templates/python/parallelized/test/test.py index b4210253f1..ce3da6758c 100644 --- a/src/python/dxpy/templating/templates/python/parallelized/test/test.py +++ b/src/python/dxpy/templating/templates/python/parallelized/test/test.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # DX_APP_WIZARD_NAME DX_APP_WIZARD_VERSION test suite # Generated by dx-app-wizard. diff --git a/src/python/dxpy/templating/templates/python/scatter-process-gather/src/code.py b/src/python/dxpy/templating/templates/python/scatter-process-gather/src/code.py index c19cc2c9af..9ba325b899 100644 --- a/src/python/dxpy/templating/templates/python/scatter-process-gather/src/code.py +++ b/src/python/dxpy/templating/templates/python/scatter-process-gather/src/code.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # coding: utf-8 # DX_APP_WIZARD_NAME DX_APP_WIZARD_VERSION # Generated by dx-app-wizard. diff --git a/src/python/dxpy/templating/templates/python/scatter-process-gather/test/test.py b/src/python/dxpy/templating/templates/python/scatter-process-gather/test/test.py index 021b8e4ef3..33928b2f3a 100644 --- a/src/python/dxpy/templating/templates/python/scatter-process-gather/test/test.py +++ b/src/python/dxpy/templating/templates/python/scatter-process-gather/test/test.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # DX_APP_WIZARD_NAME DX_APP_WIZARD_VERSION test suite # Generated by dx-app-wizard. diff --git a/src/python/dxpy/templating/utils.py b/src/python/dxpy/templating/utils.py index b50066dbd7..78f0d22979 100644 --- a/src/python/dxpy/templating/utils.py +++ b/src/python/dxpy/templating/utils.py @@ -20,12 +20,17 @@ from __future__ import print_function, unicode_literals, division, absolute_import -import os, shutil, subprocess, re, json +import os, sys, shutil, subprocess, re, json import stat from ..utils.printing import (BOLD, DNANEXUS_LOGO, ENDC, fill) from ..cli import prompt_for_yn from ..compat import input, open +try: + # Import gnureadline if installed for macOS + import gnureadline as readline +except ImportError as e: + import readline from . import python from . import bash @@ -40,12 +45,7 @@ } try: - try: - import gnureadline as readline - except ImportError: - import readline - import rlcompleter - readline.parse_and_bind("tab: complete") + readline.parse_and_bind("bind ^I rl_complete" if "libedit" in (readline.__doc__ or "") else "tab: complete") readline.set_completer_delims("") completer_state['available'] = True except ImportError: @@ -58,8 +58,8 @@ def __init__(self, choices): def complete(self, text, state): if state == 0: - self.matches = filter(lambda choice: choice.startswith(text), - self.choices) + self.matches = list(filter(lambda choice: choice.startswith(text), + self.choices)) if self.matches is not None and state < len(self.matches): return self.matches[state] @@ -291,7 +291,6 @@ def use_template_file(path): if any(template_filename.endswith(ext) for ext in ('~', '.pyc', '.pyo', '__pycache__')): continue use_template_file(os.path.join('test', template_filename)) - for template_filename in os.listdir(os.path.join(template_dir, 'src')): if template_filename.endswith('~'): continue diff --git a/src/python/dxpy/toolkit_version.py b/src/python/dxpy/toolkit_version.py index f31da3bd27..bae6df1d59 100644 --- a/src/python/dxpy/toolkit_version.py +++ b/src/python/dxpy/toolkit_version.py @@ -1 +1 @@ -version = '0.312.0' +version = '0.388.0' diff --git a/src/python/dxpy/utils/__init__.py b/src/python/dxpy/utils/__init__.py index 60236845ab..734417af59 100644 --- a/src/python/dxpy/utils/__init__.py +++ b/src/python/dxpy/utils/__init__.py @@ -20,11 +20,11 @@ from __future__ import print_function, unicode_literals, division, absolute_import -import os, json, collections, concurrent.futures, traceback, sys, time, gc +import os, json, collections, concurrent.futures, traceback, sys, time, gc, platform from multiprocessing import cpu_count import dateutil.parser from .. import logger -from ..compat import basestring, THREAD_TIMEOUT_MAX +from ..compat import basestring, THREAD_TIMEOUT_MAX, Mapping from ..exceptions import DXError import numbers import binascii @@ -248,7 +248,7 @@ def merge(d, u): Example: merge({"a": {"b": 1, "c": 2}}, {"a": {"b": 3}}) = {"a": {"b": 3, "c": 2}} """ for k, v in u.items(): - if isinstance(v, collections.Mapping): + if isinstance(v, Mapping): r = merge(d.get(k, {}), v) d[k] = r else: @@ -292,10 +292,10 @@ class Nonce: ''' def __init__(self): try: - self.nonce = "%s%f" % (str(binascii.hexlify(os.urandom(32))), time.time()) + self.nonce = "%s%f" % (binascii.hexlify(os.urandom(32)).decode('utf-8'), time.time()) except: random.seed(time.time()) - self.nonce = "%s%f" % (str(random.getrandbits(8*26)), time.time()) + self.nonce = "%s%f" % (random.getrandbits(8*26), time.time()) def __str__(self): return self.nonce diff --git a/src/python/dxpy/utils/completer.py b/src/python/dxpy/utils/completer.py index 6354a79c58..d0435c366c 100644 --- a/src/python/dxpy/utils/completer.py +++ b/src/python/dxpy/utils/completer.py @@ -250,10 +250,9 @@ def path_completer(text, expected=None, classes=None, perm_level=None, class DXPathCompleter(): ''' - This class can be used as a tab-completer with the modules - readline and rlcompleter. Note that to tab-complete data object - names with spaces, the delimiters set for the completer must not - include spaces. + This class can be used as a tab-completer with the readline module + Note that to tab-complete data object names with spaces, the delimiters + set for the completer must not include spaces. ''' def __init__(self, expected=None, classes=None, typespec=None, include_current_proj=False, visibility=None): diff --git a/src/python/dxpy/utils/config.py b/src/python/dxpy/utils/config.py index 5dfed6a08b..643f98cde8 100644 --- a/src/python/dxpy/utils/config.py +++ b/src/python/dxpy/utils/config.py @@ -25,7 +25,12 @@ import os, sys, json, time import platform -from collections import MutableMapping +try: + # Python 3 + from collections.abc import MutableMapping +except ImportError: + # Python 2.7 + from collections import MutableMapping from shutil import rmtree import dxpy @@ -196,6 +201,8 @@ def get_session_conf_dir(self, cleanup=False): rmtree(os.path.join(sessions_dir, session_dir), ignore_errors=True) parent_process = Process(os.getpid()).parent() + if parent_process is None: + parent_process = Process(os.getpid()) default_session_dir = os.path.join(sessions_dir, str(parent_process.pid)) while parent_process is not None and parent_process.pid != 0: session_dir = os.path.join(sessions_dir, str(parent_process.pid)) diff --git a/src/python/dxpy/utils/describe.py b/src/python/dxpy/utils/describe.py index ec0801cc45..6be9179bb0 100644 --- a/src/python/dxpy/utils/describe.py +++ b/src/python/dxpy/utils/describe.py @@ -25,12 +25,14 @@ from __future__ import print_function, unicode_literals, division, absolute_import import datetime, time, json, math, sys, copy +import locale import subprocess from collections import defaultdict import dxpy from .printing import (RED, GREEN, BLUE, YELLOW, WHITE, BOLD, UNDERLINE, ENDC, DELIMITER, get_delimiter, fill) -from ..compat import basestring +from .pretty_print import format_timedelta +from ..compat import basestring, USING_PYTHON2 def JOB_STATES(state): if state == 'failed': @@ -317,20 +319,19 @@ def render_bundleddepends(thing): from ..exceptions import DXError bundles = [] for item in thing: - bundle_asset_record = dxpy.DXFile(item["id"]["$dnanexus_link"]).get_properties().get("AssetBundle") + bundle_dxlink = item["id"]["$dnanexus_link"] asset = None - - if bundle_asset_record: - asset = dxpy.DXRecord(bundle_asset_record) - - if asset: + if bundle_dxlink.startswith("file-"): try: - bundles.append(asset.describe().get("name") + " (" + asset.get_id() + ")") + bundle_asset_record = dxpy.DXFile(bundle_dxlink).get_properties().get("AssetBundle") + if bundle_asset_record: + asset = dxpy.DXRecord(bundle_asset_record) + bundles.append(asset.describe().get("name") + " (" + asset.get_id() + ")") except DXError: asset = None if not asset: - bundles.append(item["name"] + " (" + item["id"]["$dnanexus_link"] + ")") + bundles.append(item["name"] + " (" + bundle_dxlink + ")") return bundles @@ -380,7 +381,7 @@ def render_timestamp(timestamp): return datetime.datetime.fromtimestamp(timestamp//1000).ctime() -FIELD_NAME_WIDTH = 22 +FIELD_NAME_WIDTH = 34 def print_field(label, value): @@ -389,8 +390,9 @@ def print_field(label, value): else: sys.stdout.write( label + " " * (FIELD_NAME_WIDTH-len(label)) + fill(value, - subsequent_indent=' '*FIELD_NAME_WIDTH, - width_adjustment=-FIELD_NAME_WIDTH) + + width=100, # revert back to width_adjustment=-FIELD_NAME_WIDTH when printing.std_width is increased + initial_indent=' '*FIELD_NAME_WIDTH, + subsequent_indent=' '*FIELD_NAME_WIDTH).lstrip() + '\n') @@ -410,7 +412,10 @@ def print_project_desc(desc, verbose=False): 'id', 'class', 'name', 'summary', 'description', 'protected', 'restricted', 'created', 'modified', 'dataUsage', 'sponsoredDataUsage', 'tags', 'level', 'folders', 'objects', 'permissions', 'properties', 'appCaches', 'billTo', 'version', 'createdBy', 'totalSponsoredEgressBytes', 'consumedSponsoredEgressBytes', - 'containsPHI', 'databaseUIViewOnly', 'region', 'storageCost', 'pendingTransfer','atSpendingLimit', + 'containsPHI', 'databaseUIViewOnly', 'externalUploadRestricted', 'region', 'storageCost', 'pendingTransfer', + 'atSpendingLimit', 'currentMonthComputeAvailableBudget', 'currentMonthEgressBytesAvailableBudget', + 'currentMonthStorageAvailableBudget', 'currentMonthComputeUsage', 'currentMonthEgressBytesUsage', + 'currentMonthExpectedStorageUsage', 'defaultSymlink', 'databaseResultsRestricted', # Following are app container-specific 'destroyAt', 'project', 'type', 'app', 'appName' ] @@ -446,6 +451,12 @@ def print_project_desc(desc, verbose=False): print_json_field('Contains PHI', desc['containsPHI']) if 'databaseUIViewOnly' in desc and desc['databaseUIViewOnly']: print_json_field('Database UI View Only', desc['databaseUIViewOnly']) + if 'externalUploadRestricted' in desc and desc['externalUploadRestricted']: + print_json_field('External Upload Restricted', desc['externalUploadRestricted']) + if 'defaultSymlink' in desc and verbose: + print_json_field('Default Symlink', desc['defaultSymlink']) + if 'databaseResultsRestricted' in desc and desc['databaseResultsRestricted']: + print_json_field('Database Results Restricted', desc['databaseResultsRestricted']) # Usage print_field("Created", render_timestamp(desc['created'])) @@ -464,6 +475,33 @@ def print_project_desc(desc, verbose=False): if 'consumedSponsoredEgressBytes' in desc else '??' print_field('Sponsored egress', ('%s used of %s total' % (consumed_egress_str, total_egress_str))) + if 'currentMonthComputeUsage' in desc: + current_usage = format_currency(desc['currentMonthComputeUsage'] if desc['currentMonthComputeUsage'] is not None else 0, meta=desc['currency']) + if desc.get('currentMonthComputeUsage') is None and desc.get('currentMonthComputeAvailableBudget') is None: + msg = '-' + elif desc.get('currentMonthComputeAvailableBudget') is not None: + msg = '%s of %s total' % (current_usage, format_currency(desc['currentMonthComputeAvailableBudget'], meta=desc['currency'])) + else: + msg = '%s of unlimited' % current_usage + print_field('Compute usage for current month', msg) + if 'currentMonthEgressBytesUsage' in desc: + current_usage = desc['currentMonthEgressBytesUsage'] if desc['currentMonthEgressBytesUsage'] is not None else 0 + if desc.get('currentMonthEgressBytesUsage') is None and desc.get('') is None: + msg = '-' + elif desc.get('currentMonthEgressBytesAvailableBudget') is not None: + msg = '%s Bytes of %s Bytes total' % (current_usage, desc['currentMonthEgressBytesAvailableBudget']) + else: + msg = '%s Bytes of unlimited' % current_usage + print_field('Egress usage for current month', msg) + if 'currentMonthExpectedStorageUsage' in desc: + current_usage = format_currency(desc['currentMonthExpectedStorageUsage'] if desc['currentMonthExpectedStorageUsage'] is not None else 0, meta=desc['currency']) + if desc.get('currentMonthExpectedStorageUsage') is None and desc.get('currentMonthStorageAvailableBudget') is None: + msg = '-' + elif desc.get('currentMonthStorageAvailableBudget') is not None: + msg = '%s of %s total' % (current_usage, format_currency(desc['currentMonthStorageAvailableBudget'], meta=desc['currency'])) + else: + msg = '%s of unlimited' % current_usage + print_field('Expected storage usage for current month', msg) if 'atSpendingLimit' in desc: print_json_field("At spending limit?", desc['atSpendingLimit']) @@ -509,7 +547,7 @@ def get_advanced_inputs(desc, verbose): def print_app_desc(desc, verbose=False): recognized_fields = ['id', 'class', 'name', 'version', 'aliases', 'createdBy', 'created', 'modified', 'deleted', 'published', 'title', 'subtitle', 'description', 'categories', 'access', 'dxapi', 'inputSpec', 'outputSpec', 'runSpec', 'resources', 'billTo', 'installed', 'openSource', 'summary', 'applet', 'installs', 'billing', 'details', 'developerNotes', - 'authorizedUsers'] + 'authorizedUsers', 'treeTurnaroundTimeThreshold'] print_field("ID", desc["id"]) print_field("Class", desc["class"]) if 'billTo' in desc: @@ -521,6 +559,8 @@ def print_app_desc(desc, verbose=False): print_field("Created", render_timestamp(desc['created'])) print_field("Last modified", render_timestamp(desc['modified'])) print_field("Created from", desc["applet"]) + if 'treeTurnaroundTimeThreshold' in desc: + print_field("Tree TAT threshold", str(desc["treeTurnaroundTimeThreshold"]) if desc["treeTurnaroundTimeThreshold"] is not None else "-") print_json_field('Installed', desc['installed']) print_json_field('Open source', desc['openSource']) print_json_field('Deleted', desc['deleted']) @@ -574,7 +614,7 @@ def print_globalworkflow_desc(desc, verbose=False): recognized_fields = ['id', 'class', 'name', 'version', 'aliases', 'createdBy', 'created', 'modified', 'deleted', 'published', 'title', 'description', 'categories', 'dxapi', 'billTo', 'summary', 'billing', 'developerNotes', - 'authorizedUsers', 'regionalOptions'] + 'authorizedUsers', 'regionalOptions', 'treeTurnaroundTimeThreshold'] is_locked_workflow = False print_field("ID", desc["id"]) print_field("Class", desc["class"]) @@ -586,6 +626,8 @@ def print_globalworkflow_desc(desc, verbose=False): print_field("Created by", desc["createdBy"][5 if desc['createdBy'].startswith('user-') else 0:]) print_field("Created", render_timestamp(desc['created'])) print_field("Last modified", render_timestamp(desc['modified'])) + if 'treeTurnaroundTimeThreshold' in desc: + print_field("Tree TAT threshold", str(desc["treeTurnaroundTimeThreshold"]) if desc["treeTurnaroundTimeThreshold"] is not None else "-") # print_json_field('Open source', desc['openSource']) print_json_field('Deleted', desc.get('deleted', False)) if not desc.get('deleted', False): @@ -641,7 +683,8 @@ def get_col_str(col_desc): def print_data_obj_desc(desc, verbose=False): recognized_fields = ['id', 'class', 'project', 'folder', 'name', 'properties', 'tags', 'types', 'hidden', 'details', 'links', 'created', 'modified', 'state', 'title', 'subtitle', 'description', 'inputSpec', 'outputSpec', 'runSpec', 'summary', 'dxapi', 'access', 'createdBy', 'summary', 'sponsored', 'developerNotes', - 'stages', 'inputs', 'outputs', 'latestAnalysis', 'editVersion', 'outputFolder', 'initializedFrom', 'temporary'] + 'stages', 'inputs', 'outputs', 'latestAnalysis', 'editVersion', 'outputFolder', 'initializedFrom', 'temporary', + 'treeTurnaroundTimeThreshold'] is_locked_workflow = False print_field("ID", desc["id"]) @@ -685,6 +728,8 @@ def print_data_obj_desc(desc, verbose=False): print_field("Description", desc["description"]) if 'outputFolder' in desc: print_field("Output Folder", desc["outputFolder"] if desc["outputFolder"] is not None else "-") + if 'treeTurnaroundTimeThreshold' in desc: + print_field("Tree TAT threshold", str(desc["treeTurnaroundTimeThreshold"]) if desc["treeTurnaroundTimeThreshold"] is not None else "-") if 'access' in desc: print_json_field("Access", desc["access"]) if 'dxapi' in desc: @@ -755,28 +800,39 @@ def print_data_obj_desc(desc, verbose=False): def printable_ssh_host_key(ssh_host_key): try: keygen = subprocess.Popen(["ssh-keygen", "-lf", "/dev/stdin"], stdin=subprocess.PIPE, stdout=subprocess.PIPE) - (stdout, stderr) = keygen.communicate(ssh_host_key) + if USING_PYTHON2: + (stdout, stderr) = keygen.communicate(ssh_host_key) + else: + (stdout, stderr) = keygen.communicate(ssh_host_key.encode()) except: return ssh_host_key.strip() else: + if not USING_PYTHON2: + stdout = stdout.decode() return stdout.replace(" no comment", "").strip() -def print_execution_desc(desc): - recognized_fields = ['id', 'class', 'project', 'workspace', 'region', +def print_execution_desc(desc, verbose=False): + recognized_fields = ['id', 'try', 'class', 'project', 'workspace', 'region', 'app', 'applet', 'executable', 'workflow', 'state', - 'rootExecution', 'parentAnalysis', 'parentJob', 'originJob', 'analysis', 'stage', + 'rootExecution', 'parentAnalysis', 'parentJob', 'parentJobTry', 'originJob', 'analysis', 'stage', 'function', 'runInput', 'originalInput', 'input', 'output', 'folder', 'launchedBy', 'created', 'modified', 'failureReason', 'failureMessage', 'stdout', 'stderr', 'waitingOnChildren', 'dependsOn', 'resources', 'projectCache', 'details', 'tags', 'properties', 'name', 'instanceType', 'systemRequirements', 'executableName', 'failureFrom', 'billTo', - 'startedRunning', 'stoppedRunning', 'stateTransitions', + 'tryCreated', 'startedRunning', 'stoppedRunning', 'stateTransitions', 'delayWorkspaceDestruction', 'stages', 'totalPrice', 'isFree', 'invoiceMetadata', - 'priority', 'sshHostKey'] + 'priority', 'sshHostKey', 'internetUsageIPs', 'spotWaitTime', 'maxTreeSpotWaitTime', + 'maxJobSpotWaitTime', 'spotCostSavings', 'preserveJobOutputs', 'treeTurnaroundTime', + 'selectedTreeTurnaroundTimeThreshold', 'selectedTreeTurnaroundTimeThresholdFrom', + 'runSystemRequirements', 'runSystemRequirementsByExecutable', 'mergedSystemRequirementsByExecutable', 'runStageSystemRequirements'] print_field("ID", desc["id"]) + if desc.get('try') is not None: + print_field("Try", str(desc['try'])) print_field("Class", desc["class"]) + if "name" in desc and desc['name'] is not None: print_field("Job name", desc['name']) if "executableName" in desc and desc['executableName'] is not None: @@ -815,6 +871,8 @@ def print_execution_desc(desc): print_field("Parent job", "-") else: print_field("Parent job", desc["parentJob"]) + if desc.get("parentJobTry") is not None: + print_field("Parent job try", str(desc["parentJobTry"])) if "parentAnalysis" in desc: if desc["parentAnalysis"] is not None: print_field("Parent analysis", desc["parentAnalysis"]) @@ -842,8 +900,11 @@ def print_execution_desc(desc): print_nofill_field("Output", get_io_field(desc["output"])) if 'folder' in desc: print_field('Output folder', desc['folder']) + print_field('Preserve Job Outputs Folder', desc['preserveJobOutputs']['folder'] if desc.get('preserveJobOutputs') and 'folder' in desc['preserveJobOutputs'] else '-') print_field("Launched by", desc["launchedBy"][5:]) print_field("Created", render_timestamp(desc['created'])) + if desc.get('tryCreated') is not None: + print_field("Try created", render_timestamp(desc['tryCreated'])) if 'startedRunning' in desc: if 'stoppedRunning' in desc: print_field("Started running", render_timestamp(desc['startedRunning'])) @@ -870,9 +931,10 @@ def print_execution_desc(desc): if "failureMessage" in desc: print_field("Failure message", desc["failureMessage"]) if "failureFrom" in desc and desc['failureFrom'] is not None and desc['failureFrom']['id'] != desc['id']: - print_field("Failure is from", desc['failureFrom']['id']) - if 'systemRequirements' in desc: - print_json_field("Sys Requirements", desc['systemRequirements']) + failure_from = desc['failureFrom']['id'] + if desc['failureFrom'].get('try') is not None: + failure_from += " try %d" % desc['failureFrom']['try'] + print_field("Failure is from", failure_from) if "tags" in desc: print_list_field("Tags", desc["tags"]) if "properties" in desc: @@ -908,19 +970,105 @@ def print_execution_desc(desc): else: print_nofill_field(" sys reqs", YELLOW() + json.dumps(cloned_sys_reqs) + ENDC()) if not desc.get('isFree') and desc.get('totalPrice') is not None: - print_field('Total Price', "$%.2f" % desc['totalPrice']) + print_field('Total Price', format_currency(desc['totalPrice'], meta=desc['currency'])) + if desc.get('spotCostSavings') is not None: + print_field('Spot Cost Savings', format_currency(desc['spotCostSavings'], meta=desc['currency'])) + if desc.get('spotWaitTime') is not None: + print_field('Spot Wait Time', format_timedelta(desc.get('spotWaitTime'), in_seconds=True)) + if desc.get('maxTreeSpotWaitTime') is not None: + print_field('Max Tree Spot Wait Time', format_timedelta(desc.get('maxTreeSpotWaitTime'), in_seconds=True)) + if desc.get('maxJobSpotWaitTime') is not None: + print_field('Max Job Spot Wait Time', format_timedelta(desc.get('maxJobSpotWaitTime'), in_seconds=True)) if desc.get('invoiceMetadata'): print_json_field("Invoice Metadata", desc['invoiceMetadata']) if desc.get('sshHostKey'): print_nofill_field("SSH Host Key", printable_ssh_host_key(desc['sshHostKey'])) + if 'internetUsageIPs' in desc: + print_json_field("Internet Usage IPs", desc['internetUsageIPs']) + if 'treeTurnaroundTime' in desc: + print_field("Tree TAT", str(desc['treeTurnaroundTime'])) + if 'selectedTreeTurnaroundTimeThreshold' in desc: + print_field("Selected tree TAT threshold", str(desc['selectedTreeTurnaroundTimeThreshold'])) + if 'selectedTreeTurnaroundTimeThresholdFrom' in desc: + print_field("Selected tree TAT from", desc['selectedTreeTurnaroundTimeThresholdFrom']) + + if 'systemRequirements' in desc: + print_json_field("Sys Requirements", desc['systemRequirements']) + if 'runSystemRequirements' in desc: + print_json_field("Run Sys Reqs", desc['runSystemRequirements']) + if 'runSystemRequirementsByExecutable' in desc: + print_json_field("Run Sys Reqs by Exec", desc['runSystemRequirementsByExecutable']) + if 'mergedSystemRequirementsByExecutable' in desc: + print_json_field("Merged Sys Reqs By Exec", desc['mergedSystemRequirementsByExecutable']) + if 'runStageSystemRequirements' in desc: + print_json_field("Run Stage Sys Reqs", desc['runStageSystemRequirements']) for field in desc: if field not in recognized_fields: print_json_field(field, desc[field]) + +def locale_from_currency_code(dx_code): + """ + This is a (temporary) hardcoded mapping between currency_list.json in nucleus and standard + locale string useful for further formatting + + :param dx_code: An id of nucleus/commons/pricing_models/currency_list.json collection + :return: standardised locale, eg 'en_US'; None when no mapping found + """ + currency_locale_map = {0: 'en_US', 1: 'en_GB'} + return currency_locale_map[dx_code] if dx_code in currency_locale_map else None + + +def format_currency_from_meta(value, meta): + """ + Formats currency value into properly decorated currency string based on provided currency metadata. + Please note that this is very basic solution missing some of the localisation features (such as + negative symbol position and type. + + Better option is to use 'locale' module to reflect currency string decorations more accurately. + + See 'format_currency' + + :param value: + :param meta: + :return: + """ + prefix = '-' if value < 0 else '' # .. TODO: some locales position neg symbol elsewhere, missing meta + prefix += meta['symbol'] if meta['symbolPosition'] == 'left' else '' + suffix = ' %s' % meta["symbol"] if meta['symbolPosition'] == 'right' else '' + # .. TODO: take the group and decimal separators from meta into account (US & UK are the same, so far we're safe) + formatted_value = '{:,.2f}'.format(abs(value)) + return prefix + formatted_value + suffix + + +def format_currency(value, meta, currency_locale=None): + """ + Formats currency value into properly decorated currency string based on either locale (preferred) + or if that is not available then currency metadata. Until locale is provided from the server + a crude mapping between `currency.dxCode` and a locale string is used instead (eg 0: 'en_US') + + :param value: amount + :param meta: server metadata (`currency`) + :return: formatted currency string + """ + try: + if currency_locale is None: + currency_locale = locale_from_currency_code(meta['dxCode']) + if currency_locale is None: + return format_currency_from_meta(value, meta) + else: + locale.setlocale(locale.LC_ALL, currency_locale) + return locale.currency(value, grouping=True) + except locale.Error: + # .. locale is probably not available -> fallback to format manually + return format_currency_from_meta(value, meta) + + def print_user_desc(desc): print_field("ID", desc["id"]) - print_field("Name", desc["first"] + " " + ((desc["middle"] + " ") if desc["middle"] != '' else '') + desc["last"]) + if "first" in desc and "middle" in desc and "last" in desc: + print_field("Name", desc["first"] + " " + ((desc["middle"] + " ") if desc["middle"] != '' else '') + desc["last"]) if "email" in desc: print_field("Email", desc["email"]) @@ -943,6 +1091,11 @@ def print_desc(desc, verbose=False): Depending on the class of the entity, this method will print a formatted and human-readable string containing the data in *desc*. ''' + if isinstance(desc, dict) and not desc.get('class'): + from ..utils.resolver import is_hashid + if is_hashid(desc.get('id')): + desc['class'] = desc['id'].split("-")[0] + if desc['class'] in ['project', 'workspace', 'container']: print_project_desc(desc, verbose=verbose) elif desc['class'] == 'app': @@ -950,7 +1103,7 @@ def print_desc(desc, verbose=False): elif desc['class'] == 'globalworkflow': print_globalworkflow_desc(desc, verbose=verbose) elif desc['class'] in ['job', 'analysis']: - print_execution_desc(desc) + print_execution_desc(desc, verbose=verbose) elif desc['class'] == 'user': print_user_desc(desc) elif desc['class'] in ['org', 'team']: @@ -1043,12 +1196,14 @@ def print_ls_l_desc(desc, **kwargs): def get_find_executions_string(desc, has_children, single_result=False, show_outputs=True, - is_cached_result=False): + is_cached_result=False, show_try=False, as_try_group_root=False): ''' :param desc: hash of execution's describe output :param has_children: whether the execution has children to be printed :param single_result: whether the execution is displayed as a single result or as part of an execution tree :param is_cached_result: whether the execution should be formatted as a cached result + :param show_try: whether to include also try no + :param as_try_group_root: whether the execution should be formatted as an artifical root for multiple tries ''' is_not_subjob = desc['parentJob'] is None or desc['class'] == 'analysis' or single_result result = ("* " if is_not_subjob and get_delimiter() is None else "") @@ -1074,6 +1229,12 @@ def get_find_executions_string(desc, has_children, single_result=False, show_out # Format state result += DELIMITER(' (') + JOB_STATES(desc['state']) + DELIMITER(') ') + desc['id'] + if as_try_group_root: + return result + ' tries' + + if show_try and desc.get('try') is not None: + result += ' try %d' % desc.get('try') + # Add unicode pipe to child if necessary result += DELIMITER('\n' + (u'│ ' if is_not_subjob and has_children else (" " if is_not_subjob else ""))) result += desc['launchedBy'][5:] + DELIMITER(' ') @@ -1087,7 +1248,7 @@ def get_find_executions_string(desc, has_children, single_result=False, show_out if desc['class'] == 'job': # Only print runtime if it ever started running if desc.get('startedRunning'): - if desc['state'] in ['done', 'failed', 'terminated', 'waiting_on_output']: + if desc['state'] in ['done', 'failed', 'terminated', 'waiting_on_output'] and desc.get('stoppedRunning'): runtime = datetime.timedelta(seconds=int(desc['stoppedRunning']-desc['startedRunning'])//1000) cached_and_runtime_strs.append("runtime " + str(runtime)) elif desc['state'] == 'running': diff --git a/src/python/dxpy/utils/exec_utils.py b/src/python/dxpy/utils/exec_utils.py index 84aa444aec..8d02293b5f 100644 --- a/src/python/dxpy/utils/exec_utils.py +++ b/src/python/dxpy/utils/exec_utils.py @@ -23,10 +23,10 @@ import os, sys, json, re, collections, logging, argparse, string, itertools, subprocess, tempfile from functools import wraps from collections import namedtuple -import pipes +import shlex import dxpy -from ..compat import USING_PYTHON2, open +from ..compat import USING_PYTHON2, open, Mapping from ..exceptions import AppInternalError ENTRY_POINT_TABLE = {} @@ -191,7 +191,7 @@ def save_error(e, working_dir, error_type="AppInternalError"): def convert_handlers_to_dxlinks(x): if isinstance(x, dxpy.DXObject): x = dxpy.dxlink(x) - elif isinstance(x, collections.Mapping): + elif isinstance(x, Mapping): for key, value in x.items(): x[key] = convert_handlers_to_dxlinks(value) elif isinstance(x, list): @@ -349,11 +349,9 @@ def log(self, message): def generate_shellcode(self, dep_group): base_apt_shellcode = "export DEBIAN_FRONTEND=noninteractive && apt-get install --yes --no-install-recommends {p}" dx_apt_update_shellcode = "apt-get update -o Dir::Etc::sourcelist=sources.list.d/nucleus.list -o Dir::Etc::sourceparts=- -o APT::Get::List-Cleanup=0" - change_apt_archive = r"sed -i -e s?http://.*.ec2.archive.ubuntu.com?http://us.archive.ubuntu.com? /etc/apt/sources.list" - apt_err_msg = "APT failed, retrying with full update against ubuntu.com" - apt_shellcode_template = "({dx_upd} && {inst}) || (echo {e}; {change_apt_archive} && apt-get update && {inst})" + apt_err_msg = "APT failed, retrying with full update against official package repository" + apt_shellcode_template = "({dx_upd} && {inst}) || (echo {e}; apt-get update && {inst})" apt_shellcode = apt_shellcode_template.format(dx_upd=dx_apt_update_shellcode, - change_apt_archive=change_apt_archive, inst=base_apt_shellcode, e=apt_err_msg) def make_pm_atoms(packages, version_separator="="): @@ -437,7 +435,7 @@ def _install_dep_bundle(self, bundle): dxpy.download_dxfile(bundle["id"], bundle["name"], project=dxpy.WORKSPACE_ID) except dxpy.exceptions.ResourceNotFound: dxpy.download_dxfile(bundle["id"], bundle["name"]) - self.run("dx-unpack {}".format(pipes.quote(bundle["name"]))) + self.run("dx-unpack {}".format(shlex.quote(bundle["name"]))) else: self.log('Skipping bundled dependency "{name}" because it does not refer to a file'.format(**bundle)) diff --git a/src/python/dxpy/utils/executable_unbuilder.py b/src/python/dxpy/utils/executable_unbuilder.py index 8ab4ae147b..4c00758262 100644 --- a/src/python/dxpy/utils/executable_unbuilder.py +++ b/src/python/dxpy/utils/executable_unbuilder.py @@ -30,6 +30,7 @@ import os import sys import tarfile +import shutil import dxpy from .. import get_handler, download_dxfile @@ -63,7 +64,7 @@ def _write_simple_file(filename, content): def _dump_workflow(workflow_obj, describe_output={}): dxworkflow_json_keys = ['name', 'title', 'summary', 'dxapi', 'version', - 'outputFolder'] + 'outputFolder', 'treeTurnaroundTimeThreshold'] dxworkflow_json_stage_keys = ['id', 'name', 'executable', 'folder', 'input', 'executionPolicy', 'systemRequirements'] @@ -97,15 +98,15 @@ def _dump_app_or_applet(executable, omit_resources=False, describe_output={}): if info["runSpec"]["interpreter"] == "bash": suffix = "sh" - elif info["runSpec"]["interpreter"] in ["python2.7", "python3", "python3.5"]: + elif info["runSpec"]["interpreter"].startswith("python"): suffix = "py" else: - print('Sorry, I don\'t know how to get executables with interpreter ' + - info["runSpec"]["interpreter"] + '\n', file=sys.stderr) + print("Sorry, I don\'t know how to get executables with interpreter {}.\n".format( + info["runSpec"]["interpreter"]), file=sys.stderr) sys.exit(1) # Entry point script - script = "src/code.%s" % (suffix,) + script = "src/code.{}".format(suffix) os.mkdir("src") with open(script, "w") as f: f.write(info["runSpec"]["code"]) @@ -115,85 +116,117 @@ def make_cluster_bootstrap_script_file(region, entry_point, code, suffix): Writes the string `code` into a file at the relative path "src/__clusterBootstrap." """ - script_name = "src/%s_%s_clusterBootstrap.%s" % (region, entry_point, suffix) + script_name = "src/{}_{}_clusterBootstrap.{}".format(region, entry_point, suffix) with open(script_name, "w") as f: f.write(code) return script_name - - # Get all the asset bundles - asset_depends = [] - deps_to_remove = [] - - # When an applet is built bundledDepends are added in the following order: - # 1. bundledDepends explicitly specified in the dxapp.json - # 2. resources (contents of resources directory added as bundledDepends) - # 3. assetDepends (translated into bundledDepends) - # - # Therefore while translating bundledDepends to assetDepends, we are traversing the - # list in reverse order and exiting when we can't find the "AssetBundle" property - # with the tarball file. - # - # NOTE: If last item (and contiguous earlier items) of bundledDepends (#1 above) refers to an - # AssetBundle tarball, those items will be converted to assetDepends. - # - # TODO: The bundledDepends should be annotated with another field called {"asset": true} - # to distinguish it from non assets. It will be needed to annotate the bundleDepends, - # when the wrapper record object is no more accessible. - - for dep in reversed(info["runSpec"]["bundledDepends"]): - file_handle = get_handler(dep["id"]) - if isinstance(file_handle, dxpy.DXFile): - asset_record_id = file_handle.get_properties().get("AssetBundle") - asset_record = None - if asset_record_id: - asset_record = dxpy.DXRecord(asset_record_id) - if asset_record: - try: - asset_json = {"name": asset_record.describe().get("name"), - "project": asset_record.get_proj_id(), - "folder": asset_record.describe().get("folder"), - "version": asset_record.describe(fields={"properties": True} - )["properties"]["version"] - } - if dep.get("stages"): - asset_json["stages"] = dep["stages"] - asset_depends.append(asset_json) - deps_to_remove.append(dep) - except DXError: - print("Describe failed on the assetDepends record object with ID - " + - asset_record_id + "\n", file=sys.stderr) - pass - else: - break - # Reversing the order of the asset_depends[] so that original order is maintained - asset_depends.reverse() - # resources/ directory - created_resources_directory = False + + # Get regions where the user's billTo are permitted + try: + bill_to = dxpy.api.user_describe(dxpy.whoami())['billTo'] + permitted_regions = set(dxpy.DXHTTPRequest('/' + bill_to + '/describe', {}).get("permittedRegions")) + except DXError: + print("Failed to get permitted regions where {} can perform billable activities.\n".format(bill_to), file=sys.stderr) + sys.exit(1) + + # when applet/app is built, the runSpec is initialized with fields "interpreter" and "bundledDependsByRegion" + # even when we don't have any bundledDepends in dxapp.json + enabled_regions = set(info["runSpec"]["bundledDependsByRegion"].keys()) + if not enabled_regions.issubset(permitted_regions): + print("Region(s) {} are not among the permitted regions of {}. Resources from these regions will not be available.".format( + ", ".join(enabled_regions.difference(permitted_regions)), bill_to), file=sys.stderr ) + # Update enabled regions + enabled_regions.intersection_update(permitted_regions) + + # Start downloading when not omitting resources + deps_downloaded = set() if not omit_resources: - for dep in info["runSpec"]["bundledDepends"]: - if dep in deps_to_remove: - continue - handler = get_handler(dep["id"]) - if isinstance(handler, dxpy.DXFile): + download_completed = False + + # Check if at least one region is enabled + if not enabled_regions: + raise DXError( + "Cannot download resources of the requested executable {} since it is not available in any of the billable regions. " + "You can use the --omit-resources flag to skip downloading the resources. ".format(info["name"])) + + # Pick a source region. The current selected region is preferred + try: + current_region = dxpy.api.project_describe(dxpy.WORKSPACE_ID, input_params={"fields": {"region": True}})["region"] + except: + current_region = None + + if current_region in enabled_regions: + source_region = current_region + print("Trying to download resources from the current region {}...".format(source_region), file=sys.stderr) + else: + source_region = list(enabled_regions)[0] + print("Trying to download resources from one of the enabled region {}...".format(source_region), file=sys.stderr) + + # When an app(let) is built the following dependencies are added as bundledDepends: + # 1. bundledDepends explicitly specified in the dxapp.json + # 2. resources (contents of resources directory added as bundledDepends) + # 3. assetDepends in the dxapp.json (with their record IDs translated into file IDs) + # + # To get the resources, we will tranverse the bundleDepends list in the source region and do the following: + # - If an file ID refers to an AssetBundle tarball (a file with the "AssetBundle" property), + # skip downloading the file, and keep this file ID as a bundledDepends in the final dxapp.json + # - Otherwise, download the file and remove this ID from the bundledDepends list in the final dxapp.json + + def untar_strip_leading_slash(tarfname, path): + with tarfile.open(tarfname) as t: + for m in t.getmembers(): + if m.name.startswith("/"): + m.name = m.name[1:] + t.extract(m, path) + t.close() + + created_resources_directory = False + # Download resources from the source region + for dep in info["runSpec"]["bundledDependsByRegion"][source_region]: + try: + file_handle = get_handler(dep["id"]) + handler_id = file_handle.get_id() + # if dep is not a file (record etc.), check the next dep + if not isinstance(file_handle, dxpy.DXFile): + continue + + # check if the file is an asset dependency + # if so, skip downloading + if file_handle.get_properties().get("AssetBundle"): + continue + + # if the file is a bundled dependency, try downloading it if not created_resources_directory: os.mkdir("resources") created_resources_directory = True - handler_id = handler.get_id() - fname = "resources/%s.tar.gz" % (handler_id) + + fname = "resources/{}.tar.gz" .format(handler_id) download_dxfile(handler_id, fname) - print("Unpacking resources", file=sys.stderr) - - def untar_strip_leading_slash(tarfname, path): - t = tarfile.open(tarfname) - for m in t.getmembers(): - if m.name.startswith("/"): - m.name = m.name[1:] - t.extract(m, path) - t.close() + print("Unpacking resource {}".format(dep.get("name")), file=sys.stderr) untar_strip_leading_slash(fname, "resources") os.unlink(fname) - deps_to_remove.append(dep) + # add dep name to deps_downloaded set + deps_downloaded.add(dep.get("name")) + + except DXError: + print("Failed to download {} from region {}.".format(handler_id, source_region), + file=sys.stderr) + # clean up deps already downloaded and quit downloading + deps_downloaded.clear() + shutil.rmtree("resources") + break + # if all deps have been checked without an error, mark downloading as completed + else: # for loop finished with no break + download_completed = True + + # Check if downloading is completed in one of the enabled regions + # if so, files in deps_downloaded will not shown in dxapp.json + # if not, deps_downloaded is an empty set. So ID of all deps will be in dxapp.json + if not download_completed: + print("Downloading resources from region {} failed. " + "Please try downloading with their IDs in dxapp.json, " + "or skip downloading resources entirely by using the --omit-resources flag.".format(source_region), file=sys.stderr) # TODO: if output directory is not the same as executable name we # should print a warning and/or offer to rewrite the "name" @@ -215,14 +248,6 @@ def untar_strip_leading_slash(tarfname, path): del dxapp_json["runSpec"]["code"] dxapp_json["runSpec"]["file"] = script - # Remove resources from bundledDepends - for dep in deps_to_remove: - dxapp_json["runSpec"]["bundledDepends"].remove(dep) - - # Add assetDepends to dxapp.json - if len(asset_depends) > 0: - dxapp_json["runSpec"]["assetDepends"] = asset_depends - # Ordering input/output spec keys ordered_spec_keys = ("name", "label", "help", "class", "type", "patterns", "optional", "default", "choices", "suggestions", "group") @@ -246,20 +271,15 @@ def untar_strip_leading_slash(tarfname, path): if dx_toolkit in dxapp_json["runSpec"].get("execDepends", ()): dxapp_json["runSpec"]["execDepends"].remove(dx_toolkit) - # Remove "bundledDependsByRegion" field from "runSpec". This utility - # will reconstruct the resources directory based on the - # "bundledDepends" field, which should be equivalent to - # "bundledDependsByRegion". - dxapp_json["runSpec"].pop("bundledDependsByRegion", None) - # "dx build" parses the "regionalOptions" key from dxapp.json into the # "runSpec.systemRequirements" field of applet/new. # "dx get" should parse the "systemRequirementsByRegion" field from # the response of /app-x/get or /applet-x/get into the "regionalOptions" # key in dxapp.json. - if "systemRequirementsByRegion" in dxapp_json['runSpec']: - dxapp_json["regionalOptions"] = {} - for region in dxapp_json['runSpec']["systemRequirementsByRegion"]: + dxapp_json["regionalOptions"] = {} + for region in enabled_regions: + dxapp_json["regionalOptions"][region] = {} + if "systemRequirementsByRegion" in dxapp_json['runSpec']: region_sys_reqs = dxapp_json['runSpec']['systemRequirementsByRegion'][region] # handle cluster bootstrap scripts if any are present @@ -275,15 +295,21 @@ def untar_strip_leading_slash(tarfname, path): # either no "clusterSpec" or no "bootstrapScript" within "clusterSpec" continue - dxapp_json["regionalOptions"][region] = \ - dict(systemRequirements=region_sys_reqs) + dxapp_json["regionalOptions"][region]["systemRequirements"]=region_sys_reqs + region_depends = dxapp_json["runSpec"]["bundledDependsByRegion"][region] + region_bundle_depends = [d for d in region_depends if d["name"] not in deps_downloaded] + if region_bundle_depends: + dxapp_json["regionalOptions"][region]["bundledDepends"]=region_bundle_depends + + # Remove "bundledDependsByRegion" and "bundledDepends" field from "runSpec". + # assetDepends and bundledDepends data are stored in regionalOptions instead. + dxapp_json["runSpec"].pop("bundledDependsByRegion", None) + dxapp_json["runSpec"].pop("bundledDepends", None) # systemRequirementsByRegion data is stored in regionalOptions, # systemRequirements is ignored - if 'systemRequirementsByRegion' in dxapp_json["runSpec"]: - del dxapp_json["runSpec"]["systemRequirementsByRegion"] - if 'systemRequirements' in dxapp_json["runSpec"]: - del dxapp_json["runSpec"]["systemRequirements"] + dxapp_json["runSpec"].pop("systemRequirementsByRegion",None) + dxapp_json["runSpec"].pop("systemRequirements",None) # Cleanup of empty elements. Be careful not to let this step # introduce any semantic changes to the app specification. For diff --git a/src/python/dxpy/utils/file_handle.py b/src/python/dxpy/utils/file_handle.py new file mode 100644 index 0000000000..dbc926d01d --- /dev/null +++ b/src/python/dxpy/utils/file_handle.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (C) 2022 DNAnexus, Inc. +# +# This file is part of dx-toolkit (DNAnexus platform client libraries). +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy +# of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from __future__ import absolute_import, division, print_function +import contextlib +import codecs +import gzip + + +@contextlib.contextmanager +def as_handle(path_or_handle, mode=None, is_gzip=False, **kwargs): + """Open a file path, or pass through an already open file handle. + + Args: + path_or_handle (str or file-like object): + The file path to open, or an open file-like object with a 'read' + method. + mode (str): File open mode, e.g. 'r' or 'w' + is_gzip (bool): Whether the file is (or should be) gzip-compressed. + **kwargs (dict): Passed through to `open` + + Returns: file-like object + """ + if mode is None: + mode = 'rb' if is_gzip else 'r' + if hasattr(path_or_handle, 'read'): + # File handle is already open + if is_gzip: + yield gzip.GzipFile(fileobj=path_or_handle, mode=mode) + else: + yield path_or_handle + else: + # File path needs to be opened + if 'encoding' in kwargs: + opener = codecs.open + elif is_gzip: + opener = gzip.open + # Need to add this for python 3.5 + if "r" in mode: mode = "rt" + else: + opener = open + with opener(path_or_handle, mode=mode, **kwargs) as fp: + yield fp \ No newline at end of file diff --git a/src/python/dxpy/utils/file_load_utils.py b/src/python/dxpy/utils/file_load_utils.py index 89aed97cfb..6f1566401b 100644 --- a/src/python/dxpy/utils/file_load_utils.py +++ b/src/python/dxpy/utils/file_load_utils.py @@ -83,7 +83,7 @@ from __future__ import print_function, unicode_literals, division, absolute_import import json -import pipes +import shlex import os import fnmatch import sys @@ -401,10 +401,6 @@ def factory(): return file_key_descs, rest_hash -# -# Note: pipes.quote() to be replaced with shlex.quote() in Python 3 -# (see http://docs.python.org/2/library/pipes.html#pipes.quote) -# def gen_bash_vars(job_input_file, job_homedir=None, check_name_collision=True): """ :param job_input_file: path to a JSON file describing the job inputs @@ -427,7 +423,7 @@ def string_of_elem(elem): result = json.dumps(dxpy.dxlink(elem)) else: result = json.dumps(elem) - return pipes.quote(result) + return shlex.quote(result) def string_of_value(val): if isinstance(val, list): diff --git a/src/python/dxpy/utils/job_log_client.py b/src/python/dxpy/utils/job_log_client.py index c4a1987d54..16bbae0852 100644 --- a/src/python/dxpy/utils/job_log_client.py +++ b/src/python/dxpy/utils/job_log_client.py @@ -23,9 +23,14 @@ import json import logging +import os +import signal import ssl +import sys +import textwrap import time +from threading import Thread from websocket import WebSocketApp import dxpy @@ -42,13 +47,15 @@ class DXJobLogStreamingException(Exception): class DXJobLogStreamClient: def __init__( - self, job_id, input_params=None, msg_output_format="{job} {level} {msg}", + self, job_id, job_try=None, input_params=None, msg_output_format="{job} {level} {msg}", msg_callback=None, print_job_info=True, exit_on_failed=True ): """Initialize job log client. :param job_id: dxid for a job (hash ID 'job-xxxx') :type job_id: str + :param job_try: try for given job. If None, it will use the latest try. + :type job_id: int or None :param input_params: blob with connection parameters, should have keys ``numRecentMessages`` (int) (wich may not be more than 1024 * 256, otherwise no logs will be returned), ``recurseJobs`` (bool) - if True, attempts to traverse subtree @@ -73,6 +80,8 @@ def __init__( # TODO: add unit tests; note it is a public class self.job_id = job_id + self.job_try = job_try + self.job_has_try = job_try is not None self.input_params = input_params self.msg_output_format = msg_output_format self.msg_callback = msg_callback @@ -101,10 +110,10 @@ def connect(self): try: self._app = WebSocketApp( self.url, - on_open=self.opened, - on_close=self.closed, - on_error=self.errored, - on_message=self.received_message + on_open=lambda app: self.opened(), + on_close=lambda app, close_status_code, close_msg: self.closed(close_status_code, close_msg), + on_error=lambda app, exception: self.errored(exception), + on_message=lambda app, message: self.received_message(message) ) self._app.run_forever(sslopt={"cert_reqs": ssl.CERT_NONE}) except: @@ -118,7 +127,7 @@ def connect(self): # API call that will do the same and block while it retries. logger.warn("Server restart, reconnecting...") time.sleep(1) - dxpy.describe(self.job_id) + self._describe_job(self.job_id) else: break @@ -173,16 +182,17 @@ def closed(self, code=None, reason=None): if self.job_id not in self.seen_jobs: self.seen_jobs[self.job_id] = {} for job_id in self.seen_jobs.keys(): - self.seen_jobs[job_id] = dxpy.describe(job_id) + self.seen_jobs[job_id] = self._describe_job(job_id) print( get_find_executions_string( self.seen_jobs[job_id], has_children=False, - show_outputs=True + show_outputs=True, + show_try=self.job_has_try ) ) else: - self.seen_jobs[self.job_id] = dxpy.describe(self.job_id) + self.seen_jobs[self.job_id] = self._describe_job(self.job_id) if (self.exit_on_failed and self.seen_jobs[self.job_id].get('state') in {'failed', 'terminated'}): @@ -196,12 +206,13 @@ def received_message(self, message): 'job' in message_dict and message_dict['job'] not in self.seen_jobs ): - self.seen_jobs[message_dict['job']] = dxpy.describe(message_dict['job']) + self.seen_jobs[message_dict['job']] = self._describe_job(message_dict['job']) print( get_find_executions_string( self.seen_jobs[message_dict['job']], has_children=False, - show_outputs=False + show_outputs=False, + show_try=self.job_has_try ) ) @@ -214,3 +225,183 @@ def received_message(self, message): self.msg_callback(message_dict) else: print(self.msg_output_format.format(**message_dict)) + + def _describe_job(self, job_id): + return dxpy.api.job_describe(job_id, {'try': self.job_try} if self.job_has_try else {}) + + +class CursesDXJobLogStreamClient(DXJobLogStreamClient): + + def closed(self, *args, **kwargs): + super(CursesDXJobLogStreamClient, self).closed(args, kwargs) + # Overcome inability to stop Python process from a thread by sending SIGINT + os.kill(os.getpid(), signal.SIGINT) + + +def metrics_top(args, input_params, enrich_msg): + try: + import curses + except: + err_exit("--metrics top is not supported on your platform due to missing curses library") + + class ScreenManager: + + def __init__(self, args): + self.stdscr = None + self.args = args + self.log_client = CursesDXJobLogStreamClient(args.jobid, input_params=input_params, msg_callback=self.msg_callback, + msg_output_format=None, print_job_info=False, exit_on_failed=False) + self.curr_screen = 'logs' + self.log = [] + self.metrics = ['Waiting for job logs...'] + self.scr_dim_y = 0 + self.scr_y_offset = 0 + self.scr_y_max_offset = 0 + self.scr_dim_x = 0 + self.scr_x_offset = 0 + self.scr_x_max_offset = 0 + self.curr_row = 0 + self.curr_row_total_chars = 0 + self.curr_col = 0 + + def main(self, stdscr): + self.stdscr = stdscr + + curses.use_default_colors() + curses.init_pair(1, curses.COLOR_BLUE, -1) + curses.init_pair(2, curses.COLOR_RED, -1) + curses.init_pair(3, curses.COLOR_YELLOW, -1) + curses.init_pair(4, curses.COLOR_GREEN, -1) + + t = Thread(target=self.log_client.connect) + t.daemon = True + t.start() + + self.refresh() + try: + while True: + ch = stdscr.getch() + if ch == curses.KEY_RESIZE: self.refresh() + elif self.curr_screen == 'logs': + if ch == curses.KEY_RIGHT: self.refresh(scr_x_offset_diff=1) + elif ch == curses.KEY_LEFT: self.refresh(scr_x_offset_diff=-1) + elif ch == curses.KEY_SRIGHT: self.refresh(scr_x_offset_diff=20) + elif ch == curses.KEY_SLEFT: self.refresh(scr_x_offset_diff=-20) + elif ch == curses.KEY_HOME: self.refresh(scr_x_offset_diff=-self.scr_x_offset) + elif ch == curses.KEY_END: self.refresh(scr_x_offset_diff=self.scr_x_max_offset) + elif ch == curses.KEY_UP: self.refresh(scr_y_offset_diff=1) + elif ch == curses.KEY_DOWN: self.refresh(scr_y_offset_diff=-1) + elif ch == curses.KEY_PPAGE: self.refresh(scr_y_offset_diff=10) + elif ch == curses.KEY_NPAGE: self.refresh(scr_y_offset_diff=-10) + elif ch == ord('?') or ch == ord('?'): self.refresh(target_screen='help') + elif ch == ord('q') or ch == ord('Q'): sys.exit(0) + elif self.curr_screen == 'help': + if ch >= 0: self.refresh(target_screen='logs') + # Capture SIGINT and exit normally + except KeyboardInterrupt: + sys.exit(0) + + def msg_callback(self, message): + if len(self.log) == 0: + self.metrics[0] = '' + + enrich_msg(self.log_client, message) + if message['level'] == 'METRICS': + self.metrics[0] = '[%s] %s' % (message['timestamp'], message['msg']) + else: + self.log.append(message) + if self.scr_y_offset > 0: + self.scr_y_offset += 1 + + self.refresh() + + def refresh(self, target_screen=None, scr_y_offset_diff=None, scr_x_offset_diff=None): + self.stdscr.erase() + self.scr_dim_y, self.scr_dim_x = self.stdscr.getmaxyx() + + self.scr_y_max_offset = max(len(self.log) - self.scr_dim_y + 3, 0) + self.update_screen_offsets(scr_y_offset_diff, scr_x_offset_diff) + + self.curr_row = 0 + + if target_screen is not None: + self.curr_screen = target_screen + + if self.curr_screen == 'help': + self.draw_help() + else: + self.draw_logs() + + self.stdscr.refresh() + + def draw_logs(self): + nlines = min(self.scr_dim_y - 3, len(self.log)) + self.stdscr.addnstr(self.curr_row, 0, self.metrics[-1], self.scr_dim_x) + self.curr_row += 2 + + for i in range(nlines): + message = self.log[len(self.log) - nlines + i - self.scr_y_offset] + self.curr_col = 0 + self.curr_row_total_chars = 0 + + if args.format: + self.print_field(args.format.format(**message), 0) + else: + if self.args.timestamps: + self.print_field(message['timestamp'], 0) + self.print_field(message['job_name'], 1) + if self.args.job_ids: + self.print_field('(%s)' % message['job'], 1) + self.print_field(message.get('level', ''), message['level_color_curses']) + self.print_field(message['msg'], 0) + + self.scr_x_max_offset = max(self.scr_x_max_offset, self.curr_row_total_chars - 1) + self.curr_row += 1 + + def draw_help(self): + text = '''Metrics top mode help +_ +This mode shows the latest METRICS message at the top of the screen and updates it for running jobs instead of showing every METRICS message interspersed with the currently-displayed job log messages. For completed jobs, this mode does not show any metrics. +_ +Controls: + Up/Down scroll up/down by one line + PgUp/PgDn scroll up/down by 10 lines + Left/Right scroll left/right by one character + Shift + Left/Right scroll left/right by 20 characters + Home/End scroll to the beginning/end of the line + ? display this help + q quit +_ +Press any key to return. +''' + lines = [] + for line in text.splitlines(): + if line == '_': + lines.append('') + continue + lines += textwrap.wrap(line, self.scr_dim_x - 1) + + for row in range(min(len(lines), self.scr_dim_y)): + self.stdscr.addnstr(row, 0, lines[row], self.scr_dim_x) + + def print_field(self, text, color): + if self.curr_col < self.scr_dim_x: + if self.curr_row_total_chars >= self.scr_x_offset: + self.stdscr.addnstr(self.curr_row, self.curr_col, text, self.scr_dim_x - self.curr_col, curses.color_pair(color)) + self.curr_col += len(text) + 1 + elif self.curr_row_total_chars + len(text) + 1 > self.scr_x_offset: + self.stdscr.addnstr(self.curr_row, self.curr_col, text[self.scr_x_offset - self.curr_row_total_chars:], self.scr_dim_x - self.curr_col, curses.color_pair(color)) + self.curr_col += len(text[self.scr_x_offset - self.curr_row_total_chars:]) + 1 + + self.curr_row_total_chars += len(text) + 1 + + def update_screen_offsets(self, diff_y, diff_x): + if not diff_y: + diff_y = 0 + if not diff_x: + diff_x = 0 + self.scr_y_offset = min(self.scr_y_offset + diff_y, self.scr_y_max_offset) if diff_y > 0 else max(self.scr_y_offset + diff_y, 0) + self.scr_x_offset = min(self.scr_x_offset + diff_x, self.scr_x_max_offset) if diff_x > 0 else max(self.scr_x_offset + diff_x, 0) + + manager = ScreenManager(args) + curses.wrapper(manager.main) diff --git a/src/python/dxpy/utils/local_exec_utils.py b/src/python/dxpy/utils/local_exec_utils.py index 72d7981362..6d1e6b0d9d 100755 --- a/src/python/dxpy/utils/local_exec_utils.py +++ b/src/python/dxpy/utils/local_exec_utils.py @@ -16,7 +16,7 @@ from __future__ import print_function, unicode_literals, division, absolute_import -import os, sys, json, subprocess, pipes +import os, sys, json, subprocess, shlex import collections, datetime import dxpy @@ -351,9 +351,9 @@ def run_one_entry_point(job_id, function, input_hash, run_spec, depends_on, name if [[ $(type -t {function}) == "function" ]]; then {function}; else echo "$0: Global scope execution complete. Not invoking entry point function {function} because it was not found" 1>&2; - fi'''.format(homedir=pipes.quote(job_homedir), - env_path=pipes.quote(os.path.join(job_env['HOME'], 'environment')), - code_path=pipes.quote(environ['DX_TEST_CODE_PATH']), + fi'''.format(homedir=shlex.quote(job_homedir), + env_path=shlex.quote(os.path.join(job_env['HOME'], 'environment')), + code_path=shlex.quote(environ['DX_TEST_CODE_PATH']), function=function) invocation_args = ['bash', '-c', '-e'] + (['-x'] if environ.get('DX_TEST_X_FLAG') else []) + [script] elif run_spec['interpreter'] == 'python2.7': diff --git a/src/python/dxpy/utils/pathmatch.py b/src/python/dxpy/utils/pathmatch.py index 31792b9660..92dcd4bf9b 100644 --- a/src/python/dxpy/utils/pathmatch.py +++ b/src/python/dxpy/utils/pathmatch.py @@ -77,4 +77,4 @@ def translate(pat): res = '%s[%s]' % (res, stuff) else: res = res + re.escape(c) - return res + '\Z(?ms)' + return '(?ms)' + res + '\Z' diff --git a/src/python/dxpy/utils/pretty_print.py b/src/python/dxpy/utils/pretty_print.py index 922ac52d6a..52472a27f8 100755 --- a/src/python/dxpy/utils/pretty_print.py +++ b/src/python/dxpy/utils/pretty_print.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright (C) 2013-2016 DNAnexus, Inc. # @@ -21,7 +21,16 @@ import re, collections from .printing import (GREEN, BLUE, YELLOW, WHITE, BOLD, ENDC) -from ..compat import str +from ..compat import str, Mapping + +TIME_UNITS = [ + ('miliseconds', 1000), + ('seconds', 60), + ('minutes', 60), + ('hours', 24), + ('days', 365), + ('years', None) +] REPLACEMENT_TABLE = ( '\\x00', # 0x00 -> NULL @@ -101,7 +110,7 @@ def _format(tree, prefix=' '): formatted_tree.append(my_multiline_prefix + line) n += 1 - if isinstance(tree[node], collections.Mapping): + if isinstance(tree[node], Mapping): subprefix = prefix if i < len(nodes)-1 and len(prefix) > 1 and prefix[-4:] == ' ': subprefix = prefix[:-4] + '│ ' @@ -197,3 +206,47 @@ def flatten_json_array(json_string, array_name): result = flatten_regexp.sub('"{}": [\\1 '.format(array_name), result) result = re.sub('"{}": \\[(.*)\r?\n\\s*\\]'.format(array_name), '"{}": [\\1]'.format(array_name), result, flags=re.MULTILINE) return result + +def format_timedelta(timedelta, in_seconds=False, largest_units=None, auto_singulars=False): + """ + Formats timedelta (duration) to a human readable form + + :param timedelta: Duration in miliseconds or seconds (see in_seconds) + :type timedelta: int + :param in_seconds: Whether the given duration is in seconds + :type in_seconds: bool + :param largest_units: Largest units to be displayed. Allowed values are miliseconds, seconds, minutes, hours, days and years + :type largest_units: str + :param auto_singulars: Automatically use singular when value of given units is 1 + :type auto_singulars: bool + """ + + units = TIME_UNITS[1:] if in_seconds else TIME_UNITS + + if largest_units is None: + largest_units = units[-1][0] + elif largest_units not in map(lambda x: x[0], units): + raise ValueError('Invalid largest units specified') + + if timedelta == 0: + return '0 ' + units[0][0] + + out_str = '' + + for name, diviser in units: + if timedelta == 0: + break + + if largest_units == name: + diviser = None + + val = timedelta % diviser if diviser else timedelta + if val != 0: + out_str = str(val) + ' ' + (name[:-1] if auto_singulars and val == 1 else name) + ', ' + out_str + + if diviser is None: + break + + timedelta //= diviser + + return out_str.strip(', ') diff --git a/src/python/dxpy/utils/resolver.py b/src/python/dxpy/utils/resolver.py index 0e2eda8a16..d9bcbd36f3 100644 --- a/src/python/dxpy/utils/resolver.py +++ b/src/python/dxpy/utils/resolver.py @@ -29,9 +29,9 @@ import dxpy from .describe import get_ls_l_desc -from ..exceptions import DXError from ..compat import str, input, basestring from ..cli import try_call, INTERACTIVE_CLI +from ..exceptions import DXCLIError, DXError def pick(choices, default=None, str_choices=None, prompt=None, allow_mult=False, more_choices=False): ''' @@ -53,6 +53,10 @@ def pick(choices, default=None, str_choices=None, prompt=None, allow_mult=False, At most one of allow_mult and more_choices should be set to True. ''' + if len(choices) == 1: + choice = 0 + return choice + for i in range(len(choices)): prefix = str(i) + ') ' lines = choices[i].split("\n") @@ -111,7 +115,7 @@ def paginate_and_pick(generator, render_fn=str, filter_fn=None, page_len=10, **p else: if filter_fn(possible_next): results.append(possible_next) - any_results = True + any_results = True if not any_results: return "none found" elif len(results) == 0: @@ -155,6 +159,9 @@ def is_data_obj_id(string): def is_container_id(string): return is_hashid(string) and (string.startswith('project-') or string.startswith('container-')) +def is_project_id(string): + return is_hashid(string) and string.startswith('project-') + def is_analysis_id(string): return is_hashid(string) and string.startswith('analysis-') @@ -208,7 +215,7 @@ def object_exists_in_project(obj_id, proj_id): raise ValueError("Expected proj_id to be a string") if not is_container_id(proj_id): raise ValueError('Expected %r to be a container ID' % (proj_id,)) - return try_call(dxpy.DXHTTPRequest, '/' + obj_id + '/describe', {'project': proj_id})['project'] == proj_id + return try_call(dxpy.DXHTTPRequest, '/' + obj_id + '/describe', {'project': proj_id}, always_retry=True)['project'] == proj_id # Special characters in bash to be escaped: #?*: ;&`"'/!$({[<>|~ @@ -675,13 +682,11 @@ def _check_resolution_needed(path, project, folderpath, entity_name, expected_cl # TODO: find a good way to check if folder exists and expected=folder return False, project, folderpath, None elif is_hashid(entity_name): - + entity_class = entity_name.split("-")[0] found_valid_class = True - if expected_classes is not None: + if expected_classes is not None and entity_class not in expected_classes: found_valid_class = False - for klass in expected_classes: - if entity_name.startswith(klass): - found_valid_class = True + if not found_valid_class: return False, None, None, None @@ -1171,7 +1176,6 @@ def get_global_exec_from_path(path): return get_app_from_path(path) elif path.startswith('globalworkflow-'): return get_global_workflow_from_path(path) - # If the path doesn't include a prefix, we must try describing # as an app and, if that fails, as a global workflow desc = get_app_from_path(path) @@ -1266,8 +1270,14 @@ def get_handler_from_desc(desc): else: raise DXError('The executable class {} is not supported'.format(desc['class'])) + def path_starts_with_non_global_prefix(path): + return path.startswith('workflow-') or path.startswith('applet-') + # First attempt to resolve a global executable: app or global workflow - global_exec_desc = get_global_exec_from_path(path) + # skip if it has non-global prefix + global_exec_desc = None + if not path_starts_with_non_global_prefix(path): + global_exec_desc = get_global_exec_from_path(path) if alias is None: try: diff --git a/src/python/dxpy/utils/version.py b/src/python/dxpy/utils/version.py new file mode 100644 index 0000000000..3230ae48ca --- /dev/null +++ b/src/python/dxpy/utils/version.py @@ -0,0 +1,28 @@ +class Version: + + def __init__(self, version): + parts = version.split(".") + + if len(parts) > 3: + raise ValueError("Unsupported version format") + + self.version = ( + int(parts[0]), + int(parts[1]) if len(parts) > 1 else 0, + int(parts[2]) if len(parts) > 2 else 0, + ) + + def __eq__(self, other): + return self.version == other.version + + def __lt__(self, other): + return self.version < other.version + + def __le__(self, other): + return self.version <= other.version + + def __gt__(self, other): + return self.version > other.version + + def __ge__(self, other): + return self.version >= other.version diff --git a/src/python/dxpy/workflow_builder.py b/src/python/dxpy/workflow_builder.py index 8ea8cb7666..b12a64423e 100644 --- a/src/python/dxpy/workflow_builder.py +++ b/src/python/dxpy/workflow_builder.py @@ -1,4 +1,5 @@ # Copyright (C) 2013-2016 DNAnexus, Inc. +# -*- coding: utf-8 -*- # # This file is part of dx-toolkit (DNAnexus platform client libraries). # @@ -29,6 +30,7 @@ import dxpy from .cli import INTERACTIVE_CLI +from .cli.parsers import process_extra_args from .utils.printing import fill from .compat import input from .utils import json_load_raise_on_duplicates @@ -37,9 +39,9 @@ UPDATABLE_GLOBALWF_FIELDS = {'title', 'summary', 'description', 'developerNotes', 'details'} GLOBALWF_SUPPORTED_KEYS = UPDATABLE_GLOBALWF_FIELDS.union({"name", "version", "regionalOptions", - "categories", "billTo", "dxapi"}) + "categories", "billTo", "dxapi", "tags"}) SUPPORTED_KEYS = GLOBALWF_SUPPORTED_KEYS.union({"project", "folder", "outputFolder", "stages", - "inputs", "outputs", "ignoreReuse"}) + "inputs", "outputs", "ignoreReuse", "properties"}) class WorkflowBuilderException(Exception): """ @@ -49,7 +51,7 @@ class WorkflowBuilderException(Exception): pass -def _parse_executable_spec(src_dir, json_file_name, parser): +def _fetch_spec_from_dxworkflowjson(src_dir, json_file_name, parser): """ Returns the parsed contents of a json specification. Raises WorkflowBuilderException (exit code 3) if this cannot be done. @@ -68,6 +70,40 @@ def _parse_executable_spec(src_dir, json_file_name, parser): except Exception as e: raise WorkflowBuilderException("Could not parse {} file as JSON: {}".format(json_file_name, e.args)) +def _cleanup_empty_keys(json_spec): + import re + clean_json = re.sub('\"\w*\": (null|\{\}|\"\"|\[\])(\,|)\s*','',json.dumps(json_spec)).replace(", }","}") + return json.loads(clean_json) + +def _check_dxcompiler_version(json_spec): + SUPPORTED_DXCOMPILER_VERSION = "2.8.0" + try: + from distutils.version import StrictVersion as Version + except ImportError: + from .utils.version import Version + if json_spec.get("details") and json_spec["details"].get("version"): + compiler_version_used = str(json_spec["details"].get("version")) + compiler_version_no_snapshot = compiler_version_used.split("-")[0] + if Version(compiler_version_no_snapshot) < Version(SUPPORTED_DXCOMPILER_VERSION): + raise WorkflowBuilderException("Source workflow {} is not compiled using dxCompiler (version>={}) that supports creating global workflows.".format(json_spec["name"], SUPPORTED_DXCOMPILER_VERSION)) + else: + raise WorkflowBuilderException("Cannot find the dxCompiler version from the dxworkflow.json/source workflow spec. Please specify it by updating the details field of the dxworkflow.json/source workflow spec using the 'version' key.") + +def _notify_instance_type_selection(json_spec): + is_static = json_spec["details"].get("staticInstanceTypeSelection", False) + if is_static: + print("Note: {workflow} was compiled with -instanceTypeSelection=static " + "and will produce a global workflow that relies on instance types that may not be available to all users." + .format(workflow=json_spec["name"])) + +def _notify_dependencies(json_spec): + # APPS-622: A textual representation of all the dependencies is appended to + # the "description" field of every workflow generated by the dxCompiler in markdown format. + dependency_list = json_spec.get("description") + if dependency_list: + print("Note: {workflow} was compiled with unbundled dependencies. " + "Please check the workflow description and make sure access to these dependencies is provided to all authorized users." + .format(workflow=json_spec["name"])) def _get_destination_project(json_spec, args, build_project_id=None): """ @@ -145,8 +181,11 @@ def _version_exists(json_spec, name=None, version=None): made a "describe" API call on the global workflow and so know the requested name and version already exists. """ - requested_name = json_spec['name'] - requested_version = json_spec['version'] + try: + requested_name = json_spec['name'] + requested_version = json_spec['version'] + except: + raise WorkflowBuilderException("Both 'name' and 'version' fields must be given in the dxworkflow.json/source workflow spec to build/update a global workflow.") if requested_name == name and requested_version == version: return True @@ -190,7 +229,7 @@ def validate_ignore_reuse(stages, ignore_reuse_stages): raise WorkflowBuilderException('"IgnoreReuse must be a list of strings - stage IDs or "*"') ignore_reuse_set = set(ignore_reuse_stages) - if '*' in ignore_reuse_set and ignore_reuse_set == 1: + if '*' in ignore_reuse_set and len(ignore_reuse_set) == 1: return stage_ids = set([stage.get('id') for stage in stages]) @@ -236,13 +275,9 @@ def _validate_json_for_global_workflow(json_spec, args): Since building a global workflow is done after all the underlying workflows are built, which may be time-consuming, we validate as much as possible here. """ - # TODO: verify the billTo can build the workflow - # TODO: if the global workflow build fails add an option to interactively change billto - # TODO: (or other simple fields) instead of failing altogether - # TODO: get a confirmation before building a workflow that may be costly if 'name' not in json_spec: raise WorkflowBuilderException( - "dxworkflow.json contains no 'name' field, but it is required to build a global workflow") + "dxworkflow.json/source workflow spec contains no 'name' field, but it is required to build a global workflow") if not dxpy.executable_builder.GLOBAL_EXEC_NAME_RE.match(json_spec['name']): raise WorkflowBuilderException( "The name of your workflow must match /^[a-zA-Z0-9._-]+$/") @@ -251,7 +286,7 @@ def _validate_json_for_global_workflow(json_spec, args): if 'version' not in json_spec: raise WorkflowBuilderException( - "dxworkflow.json contains no 'version' field, but it is required to build a global workflow") + "dxworkflow.json/source workflow spec contains no 'version' field, but it is required to build a global workflow") if not dxpy.executable_builder.GLOBAL_EXEC_VERSION_RE.match(json_spec['version']): logger.warn('"version" {} should be semver compliant (e.g. of the form X.Y.Z)'.format(json_spec['version'])) @@ -267,13 +302,11 @@ def _validate_json_for_global_workflow(json_spec, args): raise WorkflowBuilderException( 'The field "regionalOptions" must be a non-empty dictionary whose values are dictionaries') - if args.bill_to: - json_spec["billTo"] = args.bill_to def _get_validated_json(json_spec, args): """ - Validates dxworkflow.json and returns the json that can be sent with the + Validates workflow spec and returns the json that can be sent with the /workflow/new API or /globalworkflow/new request. """ if not json_spec: @@ -294,10 +327,11 @@ def _get_validated_json(json_spec, args): if 'stages' in validated_spec: validated_spec['stages'] = _get_validated_stages(validated_spec['stages']) - if 'name' in validated_spec: - if args.src_dir != validated_spec['name']: + if 'name' in validated_spec and not args._from: + dir_name = os.path.basename(os.path.normpath(args.src_dir)) + if dir_name != validated_spec['name']: logger.warn( - 'workflow name "%s" does not match containing directory "%s"' % (validated_spec['name'], args.src_dir)) + 'workflow name "{}" does not match containing directory "{}"'.format(validated_spec['name'], dir_name)) if 'ignoreReuse' in validated_spec: validate_ignore_reuse(validated_spec['stages'], validated_spec['ignoreReuse']) @@ -325,13 +359,14 @@ def _get_validated_json_for_build_or_update(json_spec, args): """ validated = copy.deepcopy(json_spec) - dxpy.executable_builder.inline_documentation_files(validated, args.src_dir) + if not args._from: + dxpy.executable_builder.inline_documentation_files(validated, args.src_dir) if 'title' not in json_spec: - logger.warn("dxworkflow.json is missing a title, please add one in the 'title' field") + logger.warn("dxworkflow.json/source workflow spec is missing a title, please add one in the 'title' field") if 'summary' not in json_spec: - logger.warn("dxworkflow.json is missing a summary, please add one in the 'summary' field") + logger.warn("dxworkflow.json/source workflow spec is missing a summary, please add one in the 'summary' field") else: if json_spec['summary'].endswith('.'): logger.warn("summary {} should be a short phrase not ending in a period".format(json_spec['summary'],)) @@ -341,37 +376,47 @@ def _get_validated_json_for_build_or_update(json_spec, args): def _assert_executable_regions_match(workflow_enabled_regions, workflow_spec): """ - Check if the global workflow regions and the regions of stages (apps) match. - If the workflow contains any applets, the workflow can be currently enabled + Check if the dependent apps/applets/subworkflows in the workflow are enabled in requested regions + Returns the subset of requested regions where all the dependent executables are enabled + If the workflow contains any applets, then the workflow can be currently enabled in only one region - the region in which the applets are stored. """ - executables = [i.get("executable") for i in workflow_spec.get("stages")] + if not workflow_enabled_regions: # empty set + return workflow_enabled_regions + + # get executable from all stages and sort them in the order app/applet/globalworkflow/workflow + executables = sorted([i.get("executable") for i in workflow_spec.get("stages")]) for exect in executables: - - if exect.startswith("applet-") and len(workflow_enabled_regions) > 1: - raise WorkflowBuilderException("Building a global workflow with applets in more than one region is not yet supported.") + if exect.startswith("applet-"): + applet_project = dxpy.DXApplet(exect).project + applet_region = dxpy.DXProject(applet_project).region + if {applet_region} != workflow_enabled_regions: + raise WorkflowBuilderException("The applet {} is not available in all requested region(s) {}" + .format(exect, ','.join(workflow_enabled_regions))) elif exect.startswith("app-"): app_regional_options = dxpy.api.app_describe(exect, input_params={"fields": {"regionalOptions": True}}) app_regions = set(app_regional_options['regionalOptions'].keys()) if not workflow_enabled_regions.issubset(app_regions): - additional_workflow_regions = workflow_enabled_regions - app_regions + additional_workflow_regions = workflow_enabled_regions.difference(app_regions) mesg = "The app {} is enabled in regions {} while the global workflow in {}.".format( exect, ", ".join(app_regions), ", ".join(workflow_enabled_regions)) mesg += " The workflow will not be able to run in {}.".format(", ".join(additional_workflow_regions)) mesg += " If you are a developer of the app, you can enable the app in {} to run the workflow in that region(s).".format( ", ".join(additional_workflow_regions)) logger.warn(mesg) + workflow_enabled_regions.intersection_update(app_regions) elif exect.startswith("workflow-"): # We recurse to check the regions of the executables of the inner workflow inner_workflow_spec = dxpy.api.workflow_describe(exect) - _assert_executable_regions_match(workflow_enabled_regions, inner_workflow_spec) + workflow_enabled_regions = _assert_executable_regions_match(workflow_enabled_regions, inner_workflow_spec) elif exect.startswith("globalworkflow-"): raise WorkflowBuilderException("Building a global workflow with nested global workflows is not yet supported") + return workflow_enabled_regions def _build_regular_workflow(json_spec, keep_open=False): """ @@ -383,54 +428,68 @@ def _build_regular_workflow(json_spec, keep_open=False): return workflow_id -def _get_validated_enabled_regions(json_spec, from_command_line): +def _get_validated_enabled_regions(json_spec, args): """ Returns a set of regions (region names) in which the global workflow - should be enabled. Also validates and synchronizes the regions - passed via CLI argument and in the regionalOptions field. + should be enabled. + + 1. validates and synchronizes the regions passed via CLI argument and in the regionalOptions field. + 2. checks if these regions are included in the permitted regions of the bill_to + 3. checks if the dependencies in all the stages are enabled in these regions """ + # Determine in which regions the global workflow are requested to be available enabled_regions = dxpy.executable_builder.get_enabled_regions('globalworkflow', json_spec, - from_command_line, + args.region, WorkflowBuilderException) if not enabled_regions: enabled_regions = [] - if not dxpy.WORKSPACE_ID: + if not dxpy.PROJECT_CONTEXT_ID: msg = "A context project must be selected to enable a workflow in the project's region." msg += " You can use 'dx select' to select a project. Otherwise you can use --region option" msg += " to select a region in which the workflow should be enabled" raise(WorkflowBuilderException(msg)) - region = dxpy.api.project_describe(dxpy.WORKSPACE_ID, + current_selected_region = dxpy.api.project_describe(dxpy.PROJECT_CONTEXT_ID, input_params={"fields": {"region": True}})["region"] - enabled_regions.append(region) + enabled_regions.append(current_selected_region) + + # Get billable regions + enabled_regions = set(enabled_regions) + billable_regions = dxpy.executable_builder.get_permitted_regions( + json_spec["billTo"], WorkflowBuilderException) + if not enabled_regions.issubset(billable_regions): + raise WorkflowBuilderException("The global workflow cannot be enabled in regions {}, which are not among the permittedRegions of the billTo." + .format(",".join(enabled_regions.difference(billable_regions)))) + # Verify dependencies in all the stages are also enabled in these regions + enabled_regions = _assert_executable_regions_match(enabled_regions, json_spec) + if not enabled_regions: - raise AssertionError("This workflow should be enabled in at least one region") + raise AssertionError("This workflow should be enabled in at least one region.") - return set(enabled_regions) + return enabled_regions -def _create_temporary_projects(enabled_regions, args): +def _create_temporary_projects(enabled_regions, bill_to): """ Creates a temporary project needed to build an underlying workflow for a global workflow. Returns a dictionary with region names as keys and project IDs as values The regions in which projects will be created can be: - i. regions specified in dxworkflow.json "regionalOptions" - ii. regions specified as an argument to "dx build" - iii. current context project, if None of the above are set - If both args and dxworkflow.json specify regions, they must match. + i. regions specified in the dxworkflow.json/source workflow spec "regionalOptions" + ii. regions specified as the argument "--region" when calling "dx build" + iii. current context project, if none of the above are set + iv. the regions where dependent applets/apps/workflows are enabled """ # Create one temp project in each region projects_by_region = {} # Project IDs by region for region in enabled_regions: try: project_input = {"name": "Temporary build project for dx build global workflow", - "region": region} - if args.bill_to: - project_input["billTo"] = args.bill_to + "region": region, + "billTo": bill_to} temp_project = dxpy.api.project_new(project_input)["id"] projects_by_region[region] = temp_project logger.debug("Created temporary project {} to build in".format(temp_project)) @@ -448,12 +507,15 @@ def _build_underlying_workflows(enabled_regions, json_spec, args): Returns a tuple of dictionaries: workflow IDs by region and project IDs by region. The caller is responsible for destroying the projects if this method returns properly. """ - projects_by_region = _create_temporary_projects(enabled_regions, args) + projects_by_region = _create_temporary_projects(enabled_regions, json_spec["billTo"]) workflows_by_region = {} try: for region, project in projects_by_region.items(): + # Override workflow project ID and folder in workflow spec + # when building underlying workflow in temporary project json_spec['project'] = project + json_spec['folder'] = '/' workflow_id = _build_regular_workflow(json_spec) logger.debug("Created workflow " + workflow_id + " successfully") workflows_by_region[region] = workflow_id @@ -466,27 +528,33 @@ def _build_underlying_workflows(enabled_regions, json_spec, args): return workflows_by_region, projects_by_region -def _build_global_workflow(json_spec, args): +def _build_global_workflow(json_spec, enabled_regions, args): """ Creates a workflow in a temporary project for each enabled region and builds a global workflow on the platform based on these workflows. """ - # First determine in which regions the global workflow needs to be available - enabled_regions = _get_validated_enabled_regions(json_spec, args.region) - - # Verify all the stages are also enabled in these regions - # TODO: Add support for dx building multi-region global workflows with applets - _assert_executable_regions_match(enabled_regions, json_spec) - workflows_by_region, projects_by_region = {}, {} # IDs by region try: # prepare "regionalOptions" field for the globalworkflow/new input + existing_regional_options = json_spec.get('regionalOptions', {}) + + if existing_regional_options: + if set(existing_regional_options.keys()) != set(enabled_regions): + raise WorkflowBuilderException("These enabled regions do have regional options specified \ + in the JSON spec or from --extra-args: {}", + ",".join(set(enabled_regions).difference(set(existing_regional_options.keys())))) + + updated_regional_options= copy.deepcopy(existing_regional_options) + else: + updated_regional_options = dict.fromkeys(enabled_regions, {}) + workflows_by_region, projects_by_region = \ _build_underlying_workflows(enabled_regions, json_spec, args) - regional_options = {} for region, workflow_id in workflows_by_region.items(): - regional_options[region] = {'workflow': workflow_id} - json_spec.update({'regionalOptions': regional_options}) + updated_regional_options[region]["workflow"] = workflow_id + + # update existing regionalOptions with underlying workflow ids and ignore regions that are not enabled + json_spec.update({'regionalOptions': updated_regional_options}) # leave only fields that are actually used to build the workflow gwf_provided_keys = GLOBALWF_SUPPORTED_KEYS.intersection(set(json_spec.keys())) @@ -560,7 +628,7 @@ def skip_update(): validated_spec = _get_validated_json_for_build_or_update(update_spec, args) non_empty_fields = dict((k, v) for k, v in validated_spec.items() if v) - if not skip_update(): + if args.update and not skip_update(): global_workflow_id = dxpy.api.global_workflow_update('globalworkflow-' + json_spec['name'], alias=json_spec['version'], input_params=non_empty_fields)['id'] @@ -569,16 +637,40 @@ def skip_update(): return global_workflow_id -def _build_or_update_workflow(json_spec, args): +def _build_or_update_workflow(args, parser): """ Creates or updates a workflow on the platform. Returns the workflow ID, or None if the workflow cannot be created. """ try: if args.mode == 'workflow': + json_spec = _fetch_spec_from_dxworkflowjson(args.src_dir, "dxworkflow.json", parser) + json_spec.update(args.extra_args or {}) json_spec = _get_validated_json(json_spec, args) workflow_id = _build_regular_workflow(json_spec, args.keep_open) elif args.mode == 'globalworkflow': + if args._from: + json_spec = args._from + else: + json_spec = _fetch_spec_from_dxworkflowjson(args.src_dir, "dxworkflow.json", parser) + + # Override version number in json_spec if --version is specified + # version is required when building global workflow but is optional for workflow + # so `dx build` requires --version to be specified when using the --from + if args.version_override: + json_spec["version"] = args.version_override + + json_spec.update(args.extra_args or {}) + json_spec = _get_validated_json(json_spec, args) + + # Check if the local or source workflow is compiled by dxCompiler that supported dependency annotation + if json_spec.get("tags") and "dxCompiler" in json_spec["tags"]: + _check_dxcompiler_version(json_spec) + if not args.brief: + _notify_instance_type_selection(json_spec) + _notify_dependencies(json_spec) + + json_spec = _cleanup_empty_keys(json_spec) # Verify if the global workflow already exists and if the user has developer rights to it # If the global workflow name doesn't exist, the user is free to build it # If the name does exist two things can be done: @@ -590,8 +682,9 @@ def _build_or_update_workflow(json_spec, args): existing_workflow.version): workflow_id = _update_global_workflow(json_spec, args, existing_workflow.id) else: - json_spec = _get_validated_json(json_spec, args) - workflow_id = _build_global_workflow(json_spec, args) + json_spec["billTo"] = dxpy.executable_builder.get_valid_bill_to(args.bill_to, WorkflowBuilderException) + enabled_regions = _get_validated_enabled_regions(json_spec, args) + workflow_id = _build_global_workflow(json_spec, enabled_regions, args) else: raise WorkflowBuilderException("Unrecognized workflow type: {}".format(args.mode)) except dxpy.exceptions.DXAPIError as e: @@ -619,9 +712,9 @@ def build(args, parser): raise Exception("Arguments not provided") try: - json_spec = _parse_executable_spec(args.src_dir, "dxworkflow.json", parser) - workflow_id = _build_or_update_workflow(json_spec, args) + process_extra_args(args) + workflow_id = _build_or_update_workflow(args, parser) _print_output(workflow_id, args) except WorkflowBuilderException as e: - print("Error: %s" % (e.args,), file=sys.stderr) + print("Error: {}".format(e), file=sys.stderr) sys.exit(3) diff --git a/src/python/pytest.ini b/src/python/pytest.ini new file mode 100644 index 0000000000..1d3725efc9 --- /dev/null +++ b/src/python/pytest.ini @@ -0,0 +1,4 @@ +[pytest] +markers = + TRACEABILITY_MATRIX + TRACEABILITY_ISOLATED_ENV \ No newline at end of file diff --git a/src/python/requirements.txt b/src/python/requirements.txt index b6c4c1c0c7..9f882f8688 100644 --- a/src/python/requirements.txt +++ b/src/python/requirements.txt @@ -1,9 +1,9 @@ -argcomplete>=1.9.4 -websocket-client==0.53.0 +argcomplete>=2.0.0; python_version >= "3.10" +argcomplete>=1.9.4,<2.0.0; python_version < "3.10" +websocket-client>=1.6.0,<1.8.0 python-dateutil>=2.5 -psutil>=3.3.0 -requests>=2.8.0,<2.24.0 -cryptography>=2.3,<3.0 -gnureadline==8.0.0; sys_platform == "darwin" and python_version < "3.9" -pyreadline==2.1; sys_platform == "win32" -colorama==0.2.4; sys_platform == "win32" \ No newline at end of file +psutil>=5.9.3 +certifi +urllib3>=1.25,<2.2 +pyreadline3==3.4.1; sys_platform == "win32" +colorama>=0.4.4,<=0.4.6; sys_platform == "win32" diff --git a/src/python/requirements_backports.txt b/src/python/requirements_backports.txt deleted file mode 100644 index b643b9078b..0000000000 --- a/src/python/requirements_backports.txt +++ /dev/null @@ -1,2 +0,0 @@ -futures>=3.2; python_version == "2.7" -backports.ssl_match_hostname==3.5.0.1 diff --git a/src/python/requirements_setuptools.txt b/src/python/requirements_setuptools.txt index 80b2016c3e..684462faa2 100644 --- a/src/python/requirements_setuptools.txt +++ b/src/python/requirements_setuptools.txt @@ -1,3 +1,3 @@ -pip==19.0.1 -setuptools==41.4.0 -wheel==0.33.4 +pip==23.3.1 +setuptools==69.0.2 +wheel==0.42.0 diff --git a/src/python/requirements_test.txt b/src/python/requirements_test.txt index a42b777748..f5787b2cde 100644 --- a/src/python/requirements_test.txt +++ b/src/python/requirements_test.txt @@ -1,6 +1,10 @@ pexpect==4.6 -pyopenssl==17.5.0 mock==2.0.0 -pytest==4.6.9 -pytest-xdist==1.31.0 -pytest-timeout==1.3.4 +pytest==7.4.2 +pytest-xdist==3.3.1 +pytest-timeout==2.1.0 +parameterized==0.8.1 +pandas==1.3.5; python_version>='3.7' +pandas>=0.23.3,<=0.25.3; python_version>='3.5.3' and python_version<'3.7' +numpy<2.0.0 +requests diff --git a/src/python/requirements_windows.txt b/src/python/requirements_windows.txt deleted file mode 100644 index 791ba233f4..0000000000 --- a/src/python/requirements_windows.txt +++ /dev/null @@ -1,2 +0,0 @@ -colorama==0.2.4 -six==1.10.0 diff --git a/src/python/scripts/dx-clone-asset b/src/python/scripts/dx-clone-asset index 925eee9aed..26d926d05e 100755 --- a/src/python/scripts/dx-clone-asset +++ b/src/python/scripts/dx-clone-asset @@ -1,4 +1,4 @@ -#!/usr/bin/env python2.7 +#!/usr/bin/env python3 from __future__ import print_function import argparse diff --git a/src/python/scripts/dx-download-all-inputs b/src/python/scripts/dx-download-all-inputs index ee821d6a5c..32409c2095 100755 --- a/src/python/scripts/dx-download-all-inputs +++ b/src/python/scripts/dx-download-all-inputs @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # # Copyright (C) 2014-2016 DNAnexus, Inc. # diff --git a/src/python/scripts/dx-fetch-bundled-depends b/src/python/scripts/dx-fetch-bundled-depends index 0f7cce447f..77b9136239 100755 --- a/src/python/scripts/dx-fetch-bundled-depends +++ b/src/python/scripts/dx-fetch-bundled-depends @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # # Copyright (C) 2013-2016 DNAnexus, Inc. # diff --git a/src/python/scripts/dx-generate-dxapp b/src/python/scripts/dx-generate-dxapp index f0e21ae079..22696e01bf 100755 --- a/src/python/scripts/dx-generate-dxapp +++ b/src/python/scripts/dx-generate-dxapp @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 """This script attempts to generate valid dxapp.json by introspecting arguments configured in an argparse.ArgumentParser. It monkeypatches ArgumentParser with a subclass that overrides parse_args to do the introspection, generate the JSON file, diff --git a/src/python/scripts/dx-jobutil-add-output b/src/python/scripts/dx-jobutil-add-output index 18eb603f2f..d5dcf6f99c 100755 --- a/src/python/scripts/dx-jobutil-add-output +++ b/src/python/scripts/dx-jobutil-add-output @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # # Copyright (C) 2013-2016 DNAnexus, Inc. # diff --git a/src/python/scripts/dx-jobutil-dxlink b/src/python/scripts/dx-jobutil-dxlink index 846317f703..2f1d7fe759 100755 --- a/src/python/scripts/dx-jobutil-dxlink +++ b/src/python/scripts/dx-jobutil-dxlink @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # # Copyright (C) 2013-2016 DNAnexus, Inc. # diff --git a/src/python/scripts/dx-jobutil-get-identity-token b/src/python/scripts/dx-jobutil-get-identity-token new file mode 100644 index 0000000000..198200e5bb --- /dev/null +++ b/src/python/scripts/dx-jobutil-get-identity-token @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2013-2024 DNAnexus, Inc. +# +# This file is part of dx-toolkit (DNAnexus platform client libraries). +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy +# of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import dxpy +import os, argparse, sys + + +def _parse_args(): + """ + Parse the input arguments. + """ + parser = argparse.ArgumentParser( + description="calls job-xxxx/getIdentityToken and retrieves a JWT token based on aud and subject claims input" + ) + parser.add_argument( + "--aud", help="Audience URI the JWT is intended for", required=True + ) + parser.add_argument( + "--subject_claims", + action="append", + metavar="", + help="Defines the subject claims to be validated by the cloud provider", + ) + + return parser.parse_args() + + +def main(aud, subject_claims): + job_get_identity_token_input = {} + + # Parse out audience and subject_claims from args + job_get_identity_token_input["audience"] = aud + + if subject_claims is not None: + # Iterate over subject_claims and flatten them into a single list + subject_claims_input = [] + for subject_claim in subject_claims: + subject_claims_input.extend(subject_claim.split(",")) + job_get_identity_token_input["subject_claims"] = subject_claims_input + + # Call job-xxxx/getIdentityToken + if "DX_JOB_ID" in os.environ: + response = dxpy.api.job_get_identity_token( + dxpy.JOB_ID, job_get_identity_token_input + ) + sys.stdout.write(response["Token"]) + else: + print("This script should be run from within a job environment.") + sys.exit(1) + + +if __name__ == "__main__": + args = _parse_args() + main(args.aud, args.subject_claims) diff --git a/src/python/scripts/dx-jobutil-new-job b/src/python/scripts/dx-jobutil-new-job index d7bec9c34d..1a31d384c1 100755 --- a/src/python/scripts/dx-jobutil-new-job +++ b/src/python/scripts/dx-jobutil-new-job @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # # Copyright (C) 2013-2016 DNAnexus, Inc. # @@ -25,13 +25,14 @@ from dxpy.utils import merge from dxpy.system_requirements import SystemRequirementsDict from dxpy.cli.exec_io import * from dxpy.cli import try_call -from dxpy.cli.parsers import (exec_input_args, process_instance_type_arg, instance_type_arg, process_properties_args, property_args, tag_args, extra_args, process_extra_args) +from dxpy.cli.parsers import (exec_input_args, process_instance_type_arg, process_instance_type_by_executable_arg, instance_type_arg, process_properties_args, property_args, tag_args, extra_args, process_extra_args) parser = argparse.ArgumentParser(description='Creates a new job to run the named function with the specified input. If successful, prints the ID of the new job.', parents=[exec_input_args, instance_type_arg, extra_args, property_args, tag_args]) parser.add_argument('function', help='Name of the function to run') parser.add_argument('--name', help='Name for the new job (default is the current job name, plus ":")') parser.add_argument('--depends-on', metavar='JOB_OR_OBJECT_ID', nargs='*', help='Job and/or data object IDs that must finish or close before the new job should be run. WARNING: For proper parsing, do not use this flag directly before the *function* parameter.') +parser.add_argument('--head-job-on-demand', help='Whether the head job should be run on an on-demand instance', action='store_true', default=None) # --test: Specify to print the JSON mapping that would have been supplied to # the /job/new API call, and additionally short-circuit before issuing the API # request. @@ -54,6 +55,16 @@ def get_job_new_input(args): try_call(process_instance_type_arg, args, False) job_new_input["systemRequirements"] = SystemRequirementsDict.from_instance_type(args.instance_type, args.function).as_dict() + if args.instance_type_by_executable is not None: + try_call(process_instance_type_by_executable_arg, args) + job_new_input["systemRequirementsByExecutable"] = { + exec: SystemRequirementsDict.from_instance_type(sys_req_by_exec).as_dict() + for exec, sys_req_by_exec in args.instance_type_by_executable.items() + } + + if args.head_job_on_demand is not None: + job_new_input['headJobOnDemand'] = args.head_job_on_demand + if args.properties is not None: try_call(process_properties_args, args) job_new_input["properties"] = args.properties diff --git a/src/python/scripts/dx-jobutil-parse-link b/src/python/scripts/dx-jobutil-parse-link index 46df14e4cd..44f6448f05 100755 --- a/src/python/scripts/dx-jobutil-parse-link +++ b/src/python/scripts/dx-jobutil-parse-link @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # # Copyright (C) 2013-2016 DNAnexus, Inc. # diff --git a/src/python/scripts/dx-jobutil-report-error b/src/python/scripts/dx-jobutil-report-error index 4e0561dc7b..c644afd334 100755 --- a/src/python/scripts/dx-jobutil-report-error +++ b/src/python/scripts/dx-jobutil-report-error @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # # Copyright (C) 2013-2016 DNAnexus, Inc. # diff --git a/src/python/scripts/dx-log-stream b/src/python/scripts/dx-log-stream index b3cd0d123d..3a976c9b40 100755 --- a/src/python/scripts/dx-log-stream +++ b/src/python/scripts/dx-log-stream @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # -*- coding: utf-8 -*- # # Copyright (C) 2013-2016 DNAnexus, Inc. diff --git a/src/python/scripts/dx-mount-all-inputs b/src/python/scripts/dx-mount-all-inputs index 927171a268..1334f68157 100755 --- a/src/python/scripts/dx-mount-all-inputs +++ b/src/python/scripts/dx-mount-all-inputs @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # # Copyright (C) 2014-2016 DNAnexus, Inc. # diff --git a/src/python/scripts/dx-print-bash-vars b/src/python/scripts/dx-print-bash-vars index 811f82a2ac..f40ce7f2d2 100755 --- a/src/python/scripts/dx-print-bash-vars +++ b/src/python/scripts/dx-print-bash-vars @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # # Copyright (C) 2014-2016 DNAnexus, Inc. # diff --git a/src/python/scripts/dx-upload-all-outputs b/src/python/scripts/dx-upload-all-outputs index 124979ec09..c791f278ee 100755 --- a/src/python/scripts/dx-upload-all-outputs +++ b/src/python/scripts/dx-upload-all-outputs @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # # Copyright (C) 2014-2016 DNAnexus, Inc. # diff --git a/src/python/setup.py b/src/python/setup.py index 6ce5eb8ba3..92779ae5e7 100755 --- a/src/python/setup.py +++ b/src/python/setup.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # # Copyright (C) 2013-2016 DNAnexus, Inc. # @@ -23,9 +23,6 @@ from setuptools import setup, find_packages import sys -if sys.version_info < (2, 7): - raise Exception("dxpy requires Python >= 2.7") - # Pypi is the repository for python packages. # It requires that version numbers look like this: X.Y.Z, # where X, Y, and Z are numbers. It is more complicated than that, but that's @@ -66,15 +63,6 @@ def make_valid_pypi_version(raw): dependencies = [line.rstrip() for line in open(os.path.join(os.path.dirname(__file__), "requirements.txt"))] test_dependencies = [line.rstrip() for line in open(os.path.join(os.path.dirname(__file__), "requirements_test.txt"))] -backports_dependencies = [line.rstrip() for line in open(os.path.join(os.path.dirname(__file__), "requirements_backports.txt"))] - -# If on Windows, also depend on colorama, which translates ANSI terminal color control sequences into whatever cmd.exe uses. -if platform.system() == 'Windows': - dependencies = [d for d in dependencies if not (d.startswith('distribute'))] - dependencies.append("colorama==0.2.4") - -if sys.version_info[0] < 3: - dependencies.extend(backports_dependencies) if 'DNANEXUS_INSTALL_PYTHON_TEST_DEPS' in os.environ: dependencies.extend(test_dependencies) @@ -85,26 +73,35 @@ def make_valid_pypi_version(raw): directory = directory[len("dxpy/templating/"):] template_files.extend([os.path.join(directory, _file) for _file in files]) +nextflow_files = os.listdir(os.path.join(os.path.dirname(__file__), 'dxpy', 'nextflow')) +nextflow_records = list(filter(lambda file: file[-5:] == ".json", nextflow_files)) + +dx_extract_files = os.listdir(os.path.join(os.path.dirname(__file__), 'dxpy', 'dx_extract_utils')) +dx_extract_records = list(filter(lambda file: file[-5:] == ".json", dx_extract_files)) + + setup( name='dxpy', version=version, description='DNAnexus Platform API bindings for Python', long_description=readme_content, long_description_content_type="text/markdown", - author='Aleksandra Zalcman, Andrey Kislyuk, Anurag Biyani, Geet Duggal, Katherine Lai, Kurt Jensen, Ohad Rodeh, Phil Sung', + author='Aleksandra Zalcman, Andrey Kislyuk, Anurag Biyani, Geet Duggal, Katherine Lai, Kurt Jensen, Marek Hrvol, Ohad Rodeh, Phil Sung', author_email='support@dnanexus.com', url='https://github.com/dnanexus/dx-toolkit', zip_safe=False, license='Apache Software License', packages = find_packages(exclude=['test']), - package_data={'dxpy.templating': template_files}, + package_data={'dxpy.templating': template_files, 'dxpy.nextflow': nextflow_records, 'dxpy.dx_extract_utils': dx_extract_records}, scripts = glob.glob(os.path.join(os.path.dirname(__file__), 'scripts', 'dx*')), entry_points = { "console_scripts": scripts, }, + python_requires = '>=3.8', install_requires = dependencies, extras_require={ - 'xattr': ["xattr==0.9.6; sys_platform == 'linux2' or sys_platform == 'linux'"] + 'pandas': ["pandas==1.3.5", "numpy<2.0.0"], + 'xattr': ["xattr==0.10.1; sys_platform == 'linux2' or sys_platform == 'linux'"] }, tests_require = test_dependencies, test_suite = "test", diff --git a/src/python/test/Readme.md b/src/python/test/Readme.md index 8535602f54..33cc869648 100644 --- a/src/python/test/Readme.md +++ b/src/python/test/Readme.md @@ -22,3 +22,5 @@ $ ./test_dxclient.py TestDXClient.test_dx_project_tagging Note however that in this case, the test runs your currently-installed version of `dx`, so if you have made local changes, you should rebuild it before running the test. + +Using print() for debugging inside dx-toolkit can break tests (causing premature tearDown). \ No newline at end of file diff --git a/src/python/test/clisam_test_filters/input/clisam_filter.json b/src/python/test/clisam_test_filters/input/clisam_filter.json new file mode 100644 index 0000000000..5dd4a0cbba --- /dev/null +++ b/src/python/test/clisam_test_filters/input/clisam_filter.json @@ -0,0 +1,29 @@ +{ + "location": [ + { + "chromosome": "1", + "starting_position": "10000", + "ending_position": "20000" + }, + { + "chromosome": "4", + "starting_position": "10", + "ending_position": "50" + }, + { + "chromosome": "X", + "starting_position": "500", + "ending_position": "1700" + } + ], + "allele": { + "allele_id": [ + "allele_1", + "allele_2" + ], + "variant_type": [ + "SNP", + "INS" + ] + } +} \ No newline at end of file diff --git a/src/python/test/clisam_test_filters/input/malformed_json/bad_location.json b/src/python/test/clisam_test_filters/input/malformed_json/bad_location.json new file mode 100644 index 0000000000..95078efde1 --- /dev/null +++ b/src/python/test/clisam_test_filters/input/malformed_json/bad_location.json @@ -0,0 +1,13 @@ +{ + "location": [ + { + "chromosome": "chr21", + "starting_position": "10", + "ending_position": "30000" + }, + { + "starting_position": "10", + "ending_position": "30000" + } + ] +} \ No newline at end of file diff --git a/src/python/test/clisam_test_filters/input/malformed_json/incorrect_type.json b/src/python/test/clisam_test_filters/input/malformed_json/incorrect_type.json new file mode 100644 index 0000000000..df4ef4c736 --- /dev/null +++ b/src/python/test/clisam_test_filters/input/malformed_json/incorrect_type.json @@ -0,0 +1,15 @@ +{ + "location": [ + { + "chromosome": "chr21", + "starting_position": "100", + "ending_position": "50000000" + } + ], + "allele": { + "variant_type": [ + "SNP", + "badtype" + ] + } +} \ No newline at end of file diff --git a/src/python/test/clisam_test_filters/input/malformed_json/location_max_range.json b/src/python/test/clisam_test_filters/input/malformed_json/location_max_range.json new file mode 100644 index 0000000000..fe0ffc3332 --- /dev/null +++ b/src/python/test/clisam_test_filters/input/malformed_json/location_max_range.json @@ -0,0 +1,8 @@ +{ + "location": [ + { "chromosome":"chr21", + "starting_position": "10", + "ending_position": "300000000" + } + ] +} \ No newline at end of file diff --git a/src/python/test/clisam_test_filters/input/malformed_json/max_items.json b/src/python/test/clisam_test_filters/input/malformed_json/max_items.json new file mode 100644 index 0000000000..2cfb9f9d2f --- /dev/null +++ b/src/python/test/clisam_test_filters/input/malformed_json/max_items.json @@ -0,0 +1,5 @@ +{ + "annotation":{ + "symbol":["BRCA0", "BRCA1", "BRCA2", "BRCA3", "BRCA4", "BRCA5", "BRCA6", "BRCA7", "BRCA8", "BRCA9", "BRCA10", "BRCA11", "BRCA12", "BRCA13", "BRCA14", "BRCA15", "BRCA16", "BRCA17", "BRCA18", "BRCA19", "BRCA20", "BRCA21", "BRCA22", "BRCA23", "BRCA24", "BRCA25", "BRCA26", "BRCA27", "BRCA28", "BRCA29", "BRCA30", "BRCA31", "BRCA32", "BRCA33", "BRCA34", "BRCA35", "BRCA36", "BRCA37", "BRCA38", "BRCA39", "BRCA40", "BRCA41", "BRCA42", "BRCA43", "BRCA44", "BRCA45", "BRCA46", "BRCA47", "BRCA48", "BRCA49", "BRCA50", "BRCA51", "BRCA52", "BRCA53", "BRCA54", "BRCA55", "BRCA56", "BRCA57", "BRCA58", "BRCA59", "BRCA60", "BRCA61", "BRCA62", "BRCA63", "BRCA64", "BRCA65", "BRCA66", "BRCA67", "BRCA68", "BRCA69", "BRCA70", "BRCA71", "BRCA72", "BRCA73", "BRCA74", "BRCA75", "BRCA76", "BRCA77", "BRCA78", "BRCA79", "BRCA80", "BRCA81", "BRCA82", "BRCA83", "BRCA84", "BRCA85", "BRCA86", "BRCA87", "BRCA88", "BRCA89", "BRCA90", "BRCA91", "BRCA92", "BRCA93", "BRCA94", "BRCA95", "BRCA96", "BRCA97", "BRCA98", "BRCA99", "BRCA100", "BRCA101", "BRCA102", "BRCA103", "BRCA104", "BRCA105", "BRCA106", "BRCA107", "BRCA108", "BRCA109"] + } +} \ No newline at end of file diff --git a/src/python/test/clisam_test_filters/input/malformed_json/missing_required.json b/src/python/test/clisam_test_filters/input/malformed_json/missing_required.json new file mode 100644 index 0000000000..0abdebed2f --- /dev/null +++ b/src/python/test/clisam_test_filters/input/malformed_json/missing_required.json @@ -0,0 +1,5 @@ +{ + "allele":{ + "variant_type":["SNP"] + } +} \ No newline at end of file diff --git a/src/python/test/clisam_test_filters/input/malformed_json/too_many_required.json b/src/python/test/clisam_test_filters/input/malformed_json/too_many_required.json new file mode 100644 index 0000000000..ae43213aa3 --- /dev/null +++ b/src/python/test/clisam_test_filters/input/malformed_json/too_many_required.json @@ -0,0 +1,8 @@ +{ + "annotation":{ + "symbol": [ + "LINC01968" + ], + "gene": ["ENSG00000289446"] + } +} \ No newline at end of file diff --git a/src/python/test/clisam_test_filters/input/malformed_json/unrecognized_key.json b/src/python/test/clisam_test_filters/input/malformed_json/unrecognized_key.json new file mode 100644 index 0000000000..74152fcca9 --- /dev/null +++ b/src/python/test/clisam_test_filters/input/malformed_json/unrecognized_key.json @@ -0,0 +1,5 @@ +{ + "allele":{ + "badkey":["badvalue"] + } +} \ No newline at end of file diff --git a/src/python/test/clisam_test_filters/input/multi_assay_sciprod_1347_v2/e2e/allele_id.json b/src/python/test/clisam_test_filters/input/multi_assay_sciprod_1347_v2/e2e/allele_id.json new file mode 100644 index 0000000000..a6a6a10607 --- /dev/null +++ b/src/python/test/clisam_test_filters/input/multi_assay_sciprod_1347_v2/e2e/allele_id.json @@ -0,0 +1,7 @@ +{ + "allele": { + "allele_id": [ + "chr3_193134700_N_N" + ] + } +} \ No newline at end of file diff --git a/src/python/test/clisam_test_filters/input/multi_assay_sciprod_1347_v2/e2e/feature.json b/src/python/test/clisam_test_filters/input/multi_assay_sciprod_1347_v2/e2e/feature.json new file mode 100644 index 0000000000..3094a2d98b --- /dev/null +++ b/src/python/test/clisam_test_filters/input/multi_assay_sciprod_1347_v2/e2e/feature.json @@ -0,0 +1,5 @@ +{ + "annotation":{ + "feature":["ENSM00524802067"] + } +} \ No newline at end of file diff --git a/src/python/test/clisam_test_filters/input/multi_assay_sciprod_1347_v2/e2e/gene.json b/src/python/test/clisam_test_filters/input/multi_assay_sciprod_1347_v2/e2e/gene.json new file mode 100644 index 0000000000..d737419d32 --- /dev/null +++ b/src/python/test/clisam_test_filters/input/multi_assay_sciprod_1347_v2/e2e/gene.json @@ -0,0 +1,5 @@ +{ + "annotation": { + "gene": ["ENSG00000289446"] + } +} \ No newline at end of file diff --git a/src/python/test/clisam_test_filters/input/multi_assay_sciprod_1347_v2/e2e/loc_and_basic.json b/src/python/test/clisam_test_filters/input/multi_assay_sciprod_1347_v2/e2e/loc_and_basic.json new file mode 100644 index 0000000000..84881884c3 --- /dev/null +++ b/src/python/test/clisam_test_filters/input/multi_assay_sciprod_1347_v2/e2e/loc_and_basic.json @@ -0,0 +1,25 @@ +{ + "location": [ + { + "chromosome": "chr1", + "starting_position": "1000", + "ending_position": "90000000" + }, + { + "chromosome": "chr4", + "starting_position": "10", + "ending_position": "100000000" + }, + { + "chromosome": "chrX", + "starting_position": "500", + "ending_position": "100000000" + } + ], + "allele": { + "variant_type": [ + "SNP", + "INS" + ] + } +} \ No newline at end of file diff --git a/src/python/test/clisam_test_filters/input/multi_assay_sciprod_1347_v2/e2e/multi_location.json b/src/python/test/clisam_test_filters/input/multi_assay_sciprod_1347_v2/e2e/multi_location.json new file mode 100644 index 0000000000..06b97b1797 --- /dev/null +++ b/src/python/test/clisam_test_filters/input/multi_assay_sciprod_1347_v2/e2e/multi_location.json @@ -0,0 +1,14 @@ +{ + "location": [ + { + "chromosome": "chr1", + "starting_position": "10", + "ending_position": "90000000" + }, + { + "chromosome": "chr2", + "starting_position": "160000000", + "ending_position": "180000000" + } + ] +} \ No newline at end of file diff --git a/src/python/test/clisam_test_filters/input/multi_assay_sciprod_1347_v2/e2e/single_location.json b/src/python/test/clisam_test_filters/input/multi_assay_sciprod_1347_v2/e2e/single_location.json new file mode 100644 index 0000000000..57bc7a47af --- /dev/null +++ b/src/python/test/clisam_test_filters/input/multi_assay_sciprod_1347_v2/e2e/single_location.json @@ -0,0 +1,9 @@ +{ + "location": [ + { + "chromosome": "chr3", + "starting_position": "183134700", + "ending_position": "203134700" + } + ] +} \ No newline at end of file diff --git a/src/python/test/clisam_test_filters/input/multi_assay_sciprod_1347_v2/e2e/symbol.json b/src/python/test/clisam_test_filters/input/multi_assay_sciprod_1347_v2/e2e/symbol.json new file mode 100644 index 0000000000..fc8841b93d --- /dev/null +++ b/src/python/test/clisam_test_filters/input/multi_assay_sciprod_1347_v2/e2e/symbol.json @@ -0,0 +1,7 @@ +{ + "annotation": { + "symbol": [ + "LINC01968" + ] + } +} \ No newline at end of file diff --git a/src/python/test/clisam_test_filters/input/multi_assay_sciprod_1347_v2/e2e/variant_type.json b/src/python/test/clisam_test_filters/input/multi_assay_sciprod_1347_v2/e2e/variant_type.json new file mode 100644 index 0000000000..5d6fb19167 --- /dev/null +++ b/src/python/test/clisam_test_filters/input/multi_assay_sciprod_1347_v2/e2e/variant_type.json @@ -0,0 +1,15 @@ +{ + "location": [ + { + "chromosome": "chr1", + "starting_position": "10", + "ending_position": "90000000" + } + ], + "allele": { + "variant_type": [ + "SNP", + "INS" + ] + } +} \ No newline at end of file diff --git a/src/python/test/clisam_test_filters/input/single_assay/e2e/allele_id.json b/src/python/test/clisam_test_filters/input/single_assay/e2e/allele_id.json new file mode 100644 index 0000000000..6d3f6929ff --- /dev/null +++ b/src/python/test/clisam_test_filters/input/single_assay/e2e/allele_id.json @@ -0,0 +1,7 @@ +{ + "allele": { + "allele_id": [ + "chr21_40590995_C_C" + ] + } +} \ No newline at end of file diff --git a/src/python/test/clisam_test_filters/input/single_assay/e2e/assay_sample_id.json b/src/python/test/clisam_test_filters/input/single_assay/e2e/assay_sample_id.json new file mode 100644 index 0000000000..591c85ac38 --- /dev/null +++ b/src/python/test/clisam_test_filters/input/single_assay/e2e/assay_sample_id.json @@ -0,0 +1,8 @@ +{ + "allele":{ + "allele_id":["chr21_40821995_T_A"] + }, + "sample":{ + "assay_sample_id":["SYN-1-T"] + } +} \ No newline at end of file diff --git a/src/python/test/clisam_test_filters/input/single_assay/e2e/feature.json b/src/python/test/clisam_test_filters/input/single_assay/e2e/feature.json new file mode 100644 index 0000000000..c1bc7d1849 --- /dev/null +++ b/src/python/test/clisam_test_filters/input/single_assay/e2e/feature.json @@ -0,0 +1,5 @@ +{ + "annotation":{ + "feature":["ENST00000400454"] + } +} \ No newline at end of file diff --git a/src/python/test/clisam_test_filters/input/single_assay/e2e/gene.json b/src/python/test/clisam_test_filters/input/single_assay/e2e/gene.json new file mode 100644 index 0000000000..e0f623e2e4 --- /dev/null +++ b/src/python/test/clisam_test_filters/input/single_assay/e2e/gene.json @@ -0,0 +1,5 @@ +{ + "annotation": { + "gene": ["ENSG00000171587"] + } +} \ No newline at end of file diff --git a/src/python/test/clisam_test_filters/input/single_assay/e2e/hgvsc.json b/src/python/test/clisam_test_filters/input/single_assay/e2e/hgvsc.json new file mode 100644 index 0000000000..dadcbef59f --- /dev/null +++ b/src/python/test/clisam_test_filters/input/single_assay/e2e/hgvsc.json @@ -0,0 +1,12 @@ +{ + "allele": { + "allele_id": [ + "chr21_40821995_T_A" + ] + }, + "annotation": { + "hgvsc": [ + "ENST00000400454.6:c.43+24624A>T" + ] + } +} \ No newline at end of file diff --git a/src/python/test/clisam_test_filters/input/single_assay/e2e/hgvsp.json b/src/python/test/clisam_test_filters/input/single_assay/e2e/hgvsp.json new file mode 100644 index 0000000000..eee91a1c74 --- /dev/null +++ b/src/python/test/clisam_test_filters/input/single_assay/e2e/hgvsp.json @@ -0,0 +1,12 @@ +{ + "allele": { + "allele_id": [ + "chr21_40821995_T_A" + ] + }, + "annotation": { + "hgvsp": [ + "ENST00000400454.6:c.43+24624A>T" + ] + } +} \ No newline at end of file diff --git a/src/python/test/clisam_test_filters/input/single_assay/e2e/loc_and_basic.json b/src/python/test/clisam_test_filters/input/single_assay/e2e/loc_and_basic.json new file mode 100644 index 0000000000..7f3a5d5365 --- /dev/null +++ b/src/python/test/clisam_test_filters/input/single_assay/e2e/loc_and_basic.json @@ -0,0 +1,25 @@ +{ + "location": [ + { + "chromosome": "chr21", + "starting_position": "40000000", + "ending_position": "50000000" + }, + { + "chromosome": "chr4", + "starting_position": "10", + "ending_position": "100000000" + }, + { + "chromosome": "chrX", + "starting_position": "500", + "ending_position": "100000000" + } + ], + "allele": { + "variant_type": [ + "SNP", + "INS" + ] + } +} \ No newline at end of file diff --git a/src/python/test/clisam_test_filters/input/single_assay/e2e/multi_location.json b/src/python/test/clisam_test_filters/input/single_assay/e2e/multi_location.json new file mode 100644 index 0000000000..d65d61ea16 --- /dev/null +++ b/src/python/test/clisam_test_filters/input/single_assay/e2e/multi_location.json @@ -0,0 +1,14 @@ +{ + "location": [ + { + "chromosome": "chr21", + "starting_position": "40000000", + "ending_position": "50000000" + }, + { + "chromosome": "chr21", + "starting_position": "13000000", + "ending_position": "14000000" + } + ] +} \ No newline at end of file diff --git a/src/python/test/clisam_test_filters/input/single_assay/e2e/sample_id.json b/src/python/test/clisam_test_filters/input/single_assay/e2e/sample_id.json new file mode 100644 index 0000000000..1d6f27bf86 --- /dev/null +++ b/src/python/test/clisam_test_filters/input/single_assay/e2e/sample_id.json @@ -0,0 +1,8 @@ +{ + "allele":{ + "allele_id":["chr21_40821995_T_A"] + }, + "sample":{ + "sample_id":["SAMPLE_1"] + } +} \ No newline at end of file diff --git a/src/python/test/clisam_test_filters/input/single_assay/e2e/single_location.json b/src/python/test/clisam_test_filters/input/single_assay/e2e/single_location.json new file mode 100644 index 0000000000..0863110503 --- /dev/null +++ b/src/python/test/clisam_test_filters/input/single_assay/e2e/single_location.json @@ -0,0 +1,9 @@ +{ + "location": [ + { + "chromosome": "chr21", + "starting_position": "100", + "ending_position": "50000000" + } + ] +} \ No newline at end of file diff --git a/src/python/test/clisam_test_filters/input/single_assay/e2e/symbol.json b/src/python/test/clisam_test_filters/input/single_assay/e2e/symbol.json new file mode 100644 index 0000000000..4c485650f9 --- /dev/null +++ b/src/python/test/clisam_test_filters/input/single_assay/e2e/symbol.json @@ -0,0 +1,7 @@ +{ + "annotation": { + "symbol": [ + "DSCAM" + ] + } +} \ No newline at end of file diff --git a/src/python/test/clisam_test_filters/input/single_assay/e2e/variant_type.json b/src/python/test/clisam_test_filters/input/single_assay/e2e/variant_type.json new file mode 100644 index 0000000000..d2f52b86f9 --- /dev/null +++ b/src/python/test/clisam_test_filters/input/single_assay/e2e/variant_type.json @@ -0,0 +1,15 @@ +{ + "location": [ + { + "chromosome": "chr21", + "starting_position": "40000000", + "ending_position": "41000000" + } + ], + "allele": { + "variant_type": [ + "SNP", + "INS" + ] + } +} \ No newline at end of file diff --git a/src/python/test/clisam_test_filters/input/small_original/e2e/allele_id.json b/src/python/test/clisam_test_filters/input/small_original/e2e/allele_id.json new file mode 100644 index 0000000000..009570434e --- /dev/null +++ b/src/python/test/clisam_test_filters/input/small_original/e2e/allele_id.json @@ -0,0 +1,7 @@ +{ + "allele": { + "allele_id": [ + "chr3_193134700_N__5070765" + ] + } +} \ No newline at end of file diff --git a/src/python/test/clisam_test_filters/input/small_original/e2e/feature.json b/src/python/test/clisam_test_filters/input/small_original/e2e/feature.json new file mode 100644 index 0000000000..3094a2d98b --- /dev/null +++ b/src/python/test/clisam_test_filters/input/small_original/e2e/feature.json @@ -0,0 +1,5 @@ +{ + "annotation":{ + "feature":["ENSM00524802067"] + } +} \ No newline at end of file diff --git a/src/python/test/clisam_test_filters/input/small_original/e2e/gene.json b/src/python/test/clisam_test_filters/input/small_original/e2e/gene.json new file mode 100644 index 0000000000..d737419d32 --- /dev/null +++ b/src/python/test/clisam_test_filters/input/small_original/e2e/gene.json @@ -0,0 +1,5 @@ +{ + "annotation": { + "gene": ["ENSG00000289446"] + } +} \ No newline at end of file diff --git a/src/python/test/clisam_test_filters/input/small_original/e2e/loc_and_basic.json b/src/python/test/clisam_test_filters/input/small_original/e2e/loc_and_basic.json new file mode 100644 index 0000000000..84881884c3 --- /dev/null +++ b/src/python/test/clisam_test_filters/input/small_original/e2e/loc_and_basic.json @@ -0,0 +1,25 @@ +{ + "location": [ + { + "chromosome": "chr1", + "starting_position": "1000", + "ending_position": "90000000" + }, + { + "chromosome": "chr4", + "starting_position": "10", + "ending_position": "100000000" + }, + { + "chromosome": "chrX", + "starting_position": "500", + "ending_position": "100000000" + } + ], + "allele": { + "variant_type": [ + "SNP", + "INS" + ] + } +} \ No newline at end of file diff --git a/src/python/test/clisam_test_filters/input/small_original/e2e/multi_location.json b/src/python/test/clisam_test_filters/input/small_original/e2e/multi_location.json new file mode 100644 index 0000000000..06b97b1797 --- /dev/null +++ b/src/python/test/clisam_test_filters/input/small_original/e2e/multi_location.json @@ -0,0 +1,14 @@ +{ + "location": [ + { + "chromosome": "chr1", + "starting_position": "10", + "ending_position": "90000000" + }, + { + "chromosome": "chr2", + "starting_position": "160000000", + "ending_position": "180000000" + } + ] +} \ No newline at end of file diff --git a/src/python/test/clisam_test_filters/input/small_original/e2e/single_location.json b/src/python/test/clisam_test_filters/input/small_original/e2e/single_location.json new file mode 100644 index 0000000000..0863110503 --- /dev/null +++ b/src/python/test/clisam_test_filters/input/small_original/e2e/single_location.json @@ -0,0 +1,9 @@ +{ + "location": [ + { + "chromosome": "chr21", + "starting_position": "100", + "ending_position": "50000000" + } + ] +} \ No newline at end of file diff --git a/src/python/test/clisam_test_filters/input/small_original/e2e/symbol.json b/src/python/test/clisam_test_filters/input/small_original/e2e/symbol.json new file mode 100644 index 0000000000..fc8841b93d --- /dev/null +++ b/src/python/test/clisam_test_filters/input/small_original/e2e/symbol.json @@ -0,0 +1,7 @@ +{ + "annotation": { + "symbol": [ + "LINC01968" + ] + } +} \ No newline at end of file diff --git a/src/python/test/clisam_test_filters/input/small_original/e2e/variant_type.json b/src/python/test/clisam_test_filters/input/small_original/e2e/variant_type.json new file mode 100644 index 0000000000..5d6fb19167 --- /dev/null +++ b/src/python/test/clisam_test_filters/input/small_original/e2e/variant_type.json @@ -0,0 +1,15 @@ +{ + "location": [ + { + "chromosome": "chr1", + "starting_position": "10", + "ending_position": "90000000" + } + ], + "allele": { + "variant_type": [ + "SNP", + "INS" + ] + } +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/input/sample_ids_10.txt b/src/python/test/create_cohort_test_files/input/sample_ids_10.txt new file mode 100644 index 0000000000..5f4dc1a912 --- /dev/null +++ b/src/python/test/create_cohort_test_files/input/sample_ids_10.txt @@ -0,0 +1,10 @@ +sample00759 +sample00713 +sample00870 +sample00659 +sample00682 +sample00997 +sample00605 +sample00509 +sample00038 +sample00102 diff --git a/src/python/test/create_cohort_test_files/input/sample_ids_100.txt b/src/python/test/create_cohort_test_files/input/sample_ids_100.txt new file mode 100644 index 0000000000..764183df2d --- /dev/null +++ b/src/python/test/create_cohort_test_files/input/sample_ids_100.txt @@ -0,0 +1,100 @@ +sample00759 +sample00713 +sample00870 +sample00659 +sample00682 +sample00997 +sample00605 +sample00509 +sample00038 +sample00102 +sample00681 +sample00718 +sample00653 +sample00552 +sample00800 +sample00099 +sample00181 +sample00878 +sample00900 +sample00755 +sample00793 +sample00791 +sample00042 +sample00155 +sample00311 +sample00190 +sample00373 +sample00159 +sample00696 +sample00859 +sample00312 +sample00203 +sample00639 +sample00881 +sample00882 +sample00931 +sample00879 +sample00309 +sample00128 +sample00407 +sample00200 +sample00098 +sample00080 +sample00108 +sample00630 +sample00726 +sample00376 +sample00588 +sample00142 +sample00040 +sample00629 +sample00645 +sample00706 +sample00841 +sample00547 +sample00667 +sample00757 +sample00146 +sample00837 +sample00304 +sample00869 +sample00219 +sample00177 +sample00610 +sample00171 +sample00480 +sample00369 +sample00585 +sample00445 +sample00413 +sample00216 +sample00132 +sample00546 +sample00864 +sample00332 +sample00013 +sample00499 +sample00339 +sample00739 +sample00138 +sample00196 +sample00301 +sample00487 +sample00650 +sample00974 +sample00992 +sample00295 +sample00125 +sample00274 +sample00435 +sample00778 +sample00028 +sample00258 +sample00556 +sample00425 +sample00945 +sample00999 +sample00980 +sample00194 +sample00803 diff --git a/src/python/test/create_cohort_test_files/input/sample_ids_valid_pheno.txt b/src/python/test/create_cohort_test_files/input/sample_ids_valid_pheno.txt new file mode 100644 index 0000000000..4a257ab366 --- /dev/null +++ b/src/python/test/create_cohort_test_files/input/sample_ids_valid_pheno.txt @@ -0,0 +1,4 @@ +patient_1 +patient_2 +patient_3 +patient_4 \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/input/sample_ids_wrong.txt b/src/python/test/create_cohort_test_files/input/sample_ids_wrong.txt new file mode 100644 index 0000000000..c0277316cf --- /dev/null +++ b/src/python/test/create_cohort_test_files/input/sample_ids_wrong.txt @@ -0,0 +1,4 @@ +patient_2 +patient_3 +patient_4 +wrong_sample_id \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/dx_new_input/combined_cohort_no_filter_as_input.json b/src/python/test/create_cohort_test_files/payloads/dx_new_input/combined_cohort_no_filter_as_input.json new file mode 100644 index 0000000000..666c34c2f8 --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/dx_new_input/combined_cohort_no_filter_as_input.json @@ -0,0 +1,75 @@ +{ + "name": "combined_cohort_no_filter_as_input", + "folder": "/Create_Cohort/manually_created_output_cohorts", + "project": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "types": [ + "DatabaseQuery", + "CohortBrowser", + "CombinedDatabaseQuery" + ], + "details": { + "databases": [ + "database-GYK2yg00vGPpzj7YGY3VJxb9" + ], + "dataset": { + "$dnanexus_link": "record-GYK2zyQ0g1bx86fBp2X8KpjY" + }, + "description": "", + "filters": { + "pheno_filters": { + "compound": [ + { + "name": "phenotype", + "logic": "and", + "filters": { + "patient$patient_id": [ + { + "condition": "in", + "values": [ + "patient_1", + "patient_2", + "patient_3" + ] + } + ] + }, + "entity": { + "logic": "and", + "name": "patient", + "operator": "exists", + "children": [] + } + } + ], + "logic": "and" + }, + "assay_filters": { + "compound": [], + "logic": "and" + }, + "logic": "and" + }, + "schema": "create_cohort_schema", + "sql": "SELECT `patient_1`.`patient_id` AS `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`patient_id` IN ('patient_1', 'patient_2', 'patient_3') AND `patient_1`.`patient_id` IN (SELECT `patient_id` FROM (SELECT `cohort_subquery`.`patient_id` AS `patient_id` FROM (SELECT `patient_1`.`patient_id` AS `patient_id`, `patient_1`.`hid` AS `hid` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE EXISTS (SELECT `hospital_1`.`hospital_id` AS `hospital_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`hospital` AS `hospital_1` WHERE `hospital_1`.`min_score_all` IN (500, 400) AND `hospital_1`.`hospital_id` = `patient_1`.`hid`)) AS `cohort_subquery` INTERSECT SELECT `patient_1`.`patient_id` AS `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`name` IN ('Sally', 'Diane', 'Cassy', 'John', 'Rosaleen')))", + "version": "3.0", + "baseSql": "SELECT `patient_id` FROM (SELECT `cohort_subquery`.`patient_id` AS `patient_id` FROM (SELECT `patient_1`.`patient_id` AS `patient_id`, `patient_1`.`hid` AS `hid` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE EXISTS (SELECT `hospital_1`.`hospital_id` AS `hospital_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`hospital` AS `hospital_1` WHERE `hospital_1`.`min_score_all` IN (500, 400) AND `hospital_1`.`hospital_id` = `patient_1`.`hid`)) AS `cohort_subquery` INTERSECT SELECT `patient_1`.`patient_id` AS `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`name` IN ('Sally', 'Diane', 'Cassy', 'John', 'Rosaleen'))", + "combined": { + "logic": "INTERSECT", + "source": [ + { + "$dnanexus_link": { + "id": "record-GYXGv700vGPpz41VkQXZpX47", + "project": "project-G9j1pX00vGPzF2XQ7843k2Jq" + } + }, + { + "$dnanexus_link": { + "id": "record-GYXGPGQ0vGPZbJF6yZ17kxpx", + "project": "project-G9j1pX00vGPzF2XQ7843k2Jq" + } + } + ] + } + }, + "close": true +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/dx_new_input/combined_cohort_non_primary_key_filter_input.json b/src/python/test/create_cohort_test_files/payloads/dx_new_input/combined_cohort_non_primary_key_filter_input.json new file mode 100644 index 0000000000..1a9ab0b994 --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/dx_new_input/combined_cohort_non_primary_key_filter_input.json @@ -0,0 +1,114 @@ +{ + "name": "combined_cohort_non_primary_key_filter_input", + "folder": "/Create_Cohort/manually_created_output_cohorts", + "project": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "types": [ + "DatabaseQuery", + "CohortBrowser", + "CombinedDatabaseQuery" + ], + "details": { + "databases": [ + "database-GYK2yg00vGPpzj7YGY3VJxb9" + ], + "dataset": { + "$dnanexus_link": "record-GYK2zyQ0g1bx86fBp2X8KpjY" + }, + "description": "", + "filters": { + "pheno_filters": { + "compound": [ + { + "name": "phenotype", + "logic": "and", + "filters": { + "patient$age": [ + { + "condition": "between", + "values": [ + 20, + 60 + ] + } + ], + "patient$patient_id": [ + { + "condition": "in", + "values": [ + "patient_1", + "patient_2" + ] + } + ] + }, + "entity": { + "logic": "and", + "name": "patient", + "operator": "exists", + "children": [ + { + "name": "hospital@e98fe382-3cb5-4c25-a771-09a24ae0f91e", + "logic": "or", + "filters": { + "hospital$min_score_all": [ + { + "condition": "in", + "values": [ + 400, + 500 + ] + } + ], + "hospital$location": [ + { + "condition": "any", + "values": [ + "CA", + "BC" + ] + } + ] + }, + "entity": { + "logic": "and", + "name": "hospital", + "operator": "exists", + "children": [] + } + } + ] + } + } + ], + "logic": "and" + }, + "assay_filters": { + "compound": [], + "logic": "and" + }, + "logic": "and" + }, + "schema": "create_cohort_schema", + "sql": "SELECT `cohort_subquery`.`patient_id` AS `patient_id` FROM (SELECT `patient_1`.`patient_id` AS `patient_id`, `patient_1`.`hid` AS `hid` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`age` BETWEEN 20 AND 60 AND `patient_1`.`patient_id` IN ('patient_1', 'patient_2') AND (EXISTS (SELECT `hospital_1`.`hospital_id` AS `hospital_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`hospital` AS `hospital_1` WHERE (`hospital_1`.`min_score_all` IN (400, 500) OR ARRAY_CONTAINS(`hospital_1`.`location_hierarchy`, 'CA') OR ARRAY_CONTAINS(`hospital_1`.`location_hierarchy`, 'BC')) AND `hospital_1`.`hospital_id` = `patient_1`.`hid`)) AND `patient_1`.`patient_id` IN (SELECT `patient_id` FROM (SELECT `cohort_subquery`.`patient_id` AS `patient_id` FROM (SELECT `patient_1`.`patient_id` AS `patient_id`, `patient_1`.`hid` AS `hid` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE EXISTS (SELECT `hospital_1`.`hospital_id` AS `hospital_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`hospital` AS `hospital_1` WHERE `hospital_1`.`min_score_all` IN (500, 400) AND `hospital_1`.`hospital_id` = `patient_1`.`hid`)) AS `cohort_subquery` INTERSECT SELECT `patient_1`.`patient_id` AS `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`name` IN ('Sally', 'Diane', 'Cassy', 'John', 'Rosaleen')))) AS `cohort_subquery`", + "version": "3.0", + "baseSql": "SELECT `patient_id` FROM (SELECT `cohort_subquery`.`patient_id` AS `patient_id` FROM (SELECT `patient_1`.`patient_id` AS `patient_id`, `patient_1`.`hid` AS `hid` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE EXISTS (SELECT `hospital_1`.`hospital_id` AS `hospital_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`hospital` AS `hospital_1` WHERE `hospital_1`.`min_score_all` IN (500, 400) AND `hospital_1`.`hospital_id` = `patient_1`.`hid`)) AS `cohort_subquery` INTERSECT SELECT `patient_1`.`patient_id` AS `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`name` IN ('Sally', 'Diane', 'Cassy', 'John', 'Rosaleen'))", + "combined": { + "logic": "INTERSECT", + "source": [ + { + "$dnanexus_link": { + "id": "record-GYXGv700vGPpz41VkQXZpX47", + "project": "project-G9j1pX00vGPzF2XQ7843k2Jq" + } + }, + { + "$dnanexus_link": { + "id": "record-GYXGPGQ0vGPZbJF6yZ17kxpx", + "project": "project-G9j1pX00vGPzF2XQ7843k2Jq" + } + } + ] + } + }, + "close": true +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/dx_new_input/combined_cohort_primary_field_filter_as_input.json b/src/python/test/create_cohort_test_files/payloads/dx_new_input/combined_cohort_primary_field_filter_as_input.json new file mode 100644 index 0000000000..c3e5eb47ed --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/dx_new_input/combined_cohort_primary_field_filter_as_input.json @@ -0,0 +1,75 @@ +{ + "name": "combined_cohort_primary_field_filter_as_input", + "folder": "/Create_Cohort/manually_created_output_cohorts", + "project": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "types": [ + "DatabaseQuery", + "CohortBrowser", + "CombinedDatabaseQuery" + ], + "details": { + "databases": [ + "database-GYK2yg00vGPpzj7YGY3VJxb9" + ], + "dataset": { + "$dnanexus_link": "record-GYK2zyQ0g1bx86fBp2X8KpjY" + }, + "description": "", + "filters": { + "pheno_filters": { + "compound": [ + { + "name": "phenotype", + "logic": "and", + "filters": { + "patient$patient_id": [ + { + "condition": "in", + "values": [ + "patient_1", + "patient_2", + "patient_3" + ] + } + ] + }, + "entity": { + "logic": "and", + "name": "patient", + "operator": "exists", + "children": [] + } + } + ], + "logic": "and" + }, + "assay_filters": { + "compound": [], + "logic": "and" + }, + "logic": "and" + }, + "schema": "create_cohort_schema", + "sql": "SELECT `patient_1`.`patient_id` AS `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`patient_id` IN ('patient_1', 'patient_2', 'patient_3') AND `patient_1`.`patient_id` IN (SELECT `patient_id` FROM (SELECT `cohort_subquery`.`patient_id` AS `patient_id` FROM (SELECT `patient_1`.`patient_id` AS `patient_id`, `patient_1`.`hid` AS `hid` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE EXISTS (SELECT `hospital_1`.`hospital_id` AS `hospital_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`hospital` AS `hospital_1` WHERE `hospital_1`.`min_score_all` IN (500, 400) AND `hospital_1`.`hospital_id` = `patient_1`.`hid`)) AS `cohort_subquery` INTERSECT SELECT `patient_1`.`patient_id` AS `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`name` IN ('Sally', 'Diane', 'Cassy', 'John', 'Rosaleen')))", + "baseSql": "SELECT `patient_id` FROM (SELECT `cohort_subquery`.`patient_id` AS `patient_id` FROM (SELECT `patient_1`.`patient_id` AS `patient_id`, `patient_1`.`hid` AS `hid` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE EXISTS (SELECT `hospital_1`.`hospital_id` AS `hospital_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`hospital` AS `hospital_1` WHERE `hospital_1`.`min_score_all` IN (500, 400) AND `hospital_1`.`hospital_id` = `patient_1`.`hid`)) AS `cohort_subquery` INTERSECT SELECT `patient_1`.`patient_id` AS `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`name` IN ('Sally', 'Diane', 'Cassy', 'John', 'Rosaleen'))", + "combined": { + "logic": "INTERSECT", + "source": [ + { + "$dnanexus_link": { + "id": "record-GYXGv700vGPpz41VkQXZpX47", + "project": "project-G9j1pX00vGPzF2XQ7843k2Jq" + } + }, + { + "$dnanexus_link": { + "id": "record-GYXGPGQ0vGPZbJF6yZ17kxpx", + "project": "project-G9j1pX00vGPzF2XQ7843k2Jq" + } + } + ] + }, + "version": "3.0" + }, + "close": true +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/dx_new_input/complex_cohort_as_input.json b/src/python/test/create_cohort_test_files/payloads/dx_new_input/complex_cohort_as_input.json new file mode 100644 index 0000000000..d4561cb3c4 --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/dx_new_input/complex_cohort_as_input.json @@ -0,0 +1,46 @@ +{ + "name": "complex_cohort_as_input", + "folder": "/Create_Cohort/manually_created_output_cohorts", + "project": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "types": [ + "DatabaseQuery", + "CohortBrowser" + ], + "details": { + "databases": [ + "database-GYK2yg00vGPpzj7YGY3VJxb9" + ], + "dataset": { + "$dnanexus_link": "record-GYK2zyQ0g1bx86fBp2X8KpjY" + }, + "description": "", + "schema": "create_cohort_schema", + "filters": { + "pheno_filters": { + "compound": [ + { + "name": "phenotype", + "logic": "and", + "filters": { + "patient$patient_id": [ + { + "condition": "in", + "values": [ + "patient_1", + "patient_2" + ] + } + ] + } + } + ], + "logic": "and" + }, + "logic": "and" + }, + "baseSql": "SELECT `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` WHERE `patient_id` in ('patient_1', 'patient_2', 'patient_3')", + "sql": "SELECT `patient_1`.`patient_id` AS `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`patient_id` IN ('patient_1', 'patient_2') AND `patient_1`.`patient_id` IN (SELECT `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` WHERE `patient_id` in ('patient_1', 'patient_2', 'patient_3'))", + "version": "3.0" + }, + "close": true +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/dx_new_input/dataset_as_input.json b/src/python/test/create_cohort_test_files/payloads/dx_new_input/dataset_as_input.json new file mode 100644 index 0000000000..e15fbad415 --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/dx_new_input/dataset_as_input.json @@ -0,0 +1,46 @@ +{ + "name": "dataset_as_input", + "folder": "/Create_Cohort/manually_created_output_cohorts", + "project": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "types": [ + "DatabaseQuery", + "CohortBrowser" + ], + "details": { + "databases": [ + "database-GYK2yg00vGPpzj7YGY3VJxb9" + ], + "dataset": { + "$dnanexus_link": "record-GYK2zyQ0g1bx86fBp2X8KpjY" + }, + "description": "", + "filters": { + "pheno_filters": { + "compound": [ + { + "name": "phenotype", + "logic": "and", + "filters": { + "patient$patient_id": [ + { + "condition": "in", + "values": [ + "patient_1", + "patient_2", + "patient_3" + ] + } + ] + } + } + ], + "logic": "and" + }, + "logic": "and" + }, + "schema": "create_cohort_schema", + "sql": "SELECT `patient_1`.`patient_id` AS `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`patient_id` IN ('patient_1', 'patient_2', 'patient_3')", + "version": "3.0" + }, + "close": true +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/dx_new_input/geno_cohort_with_geno_filters_filtered_with_primary_field.json b/src/python/test/create_cohort_test_files/payloads/dx_new_input/geno_cohort_with_geno_filters_filtered_with_primary_field.json new file mode 100644 index 0000000000..8a82e8b780 --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/dx_new_input/geno_cohort_with_geno_filters_filtered_with_primary_field.json @@ -0,0 +1,85 @@ +{ + "name": "geno_cohort_with_geno_filters_filtered_with_primary_field", + "folder": "/Create_Cohort/manually_created_output_cohorts", + "project": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "types": [ + "DatabaseQuery", + "CohortBrowser" + ], + "details": { + "databases": [ + "database-GYK3JJj0vGPQ2Y1y98pj8pGg", + "database-FkypXj80bgYvQqyqBx1FJG6y" + ], + "dataset": { + "$dnanexus_link": "record-GYK40bj06Yy4KPpB4zqX8kGb" + }, + "description": "", + "filters": { + "pheno_filters": { + "compound": [ + { + "name": "phenotype", + "logic": "and", + "filters": { + "sample$sample_id": [ + { + "condition": "in", + "values": [ + "sample_1_1", + "sample_1_2", + "sample_1_5" + ] + } + ] + }, + "entity": { + "logic": "and", + "name": "sample", + "operator": "exists", + "children": [] + } + } + ], + "logic": "and" + }, + "assay_filters": { + "compound": [ + { + "name": "genotype@b07df2d4-a05c-4c6a-9a20-7602695d9e47", + "logic": "and", + "filters": { + "annotation$gene_name": [ + { + "condition": "in", + "values": [], + "geno_bins": [ + { + "chr": "18", + "start": 47368, + "end": 47368 + } + ] + } + ], + "genotype$type": [ + { + "condition": "in", + "values": [ + "hom" + ] + } + ] + } + } + ], + "logic": "and" + }, + "logic": "and" + }, + "schema": "create_cohort_schema", + "sql": "SELECT `sample_1`.`sample_id` AS `sample_id` FROM `database_gyk3jjj0vgpq2y1y98pj8pgg__create_cohort_geno_database`.`sample` AS `sample_1` WHERE `sample_1`.`sample_id` IN ('sample_1_1', 'sample_1_2', 'sample_1_5') AND `sample_1`.`sample_id` IN (SELECT `assay_cohort_query`.`sample_id` FROM (SELECT `genotype_alt_read_optimized_1`.`sample_id` AS `sample_id` FROM `database_gyk3jjj0vgpq2y1y98pj8pgg__create_cohort_geno_database`.`genotype_alt_read_optimized` AS `genotype_alt_read_optimized_1` WHERE `genotype_alt_read_optimized_1`.`ref_yn` = false AND `genotype_alt_read_optimized_1`.`chr` = '18' AND `genotype_alt_read_optimized_1`.`pos` BETWEEN 46368 AND 48368 AND `genotype_alt_read_optimized_1`.`bin` IN (0) AND `genotype_alt_read_optimized_1`.`type` IN ('hom')) AS `assay_cohort_query`)", + "version": "3.0" + }, + "close": true +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/dx_new_input/geno_dataset_filtered_with_primary_field.json b/src/python/test/create_cohort_test_files/payloads/dx_new_input/geno_dataset_filtered_with_primary_field.json new file mode 100644 index 0000000000..6843666637 --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/dx_new_input/geno_dataset_filtered_with_primary_field.json @@ -0,0 +1,49 @@ +{ + "name": "geno_dataset_filtered_with_primary_field", + "folder": "/Create_Cohort/manually_created_output_cohorts", + "project": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "types": [ + "DatabaseQuery", + "CohortBrowser" + ], + "details": { + "databases": [ + "database-GYK3JJj0vGPQ2Y1y98pj8pGg", + "database-FkypXj80bgYvQqyqBx1FJG6y" + ], + "dataset": { + "$dnanexus_link": "record-GYK40bj06Yy4KPpB4zqX8kGb" + }, + "description": "", + "filters": { + "pheno_filters": { + "compound": [ + { + "name": "phenotype", + "logic": "and", + "filters": { + "sample$sample_id": [ + { + "condition": "in", + "values": [ + "sample_1_1", + "sample_1_2", + "sample_1_3", + "sample_1_4", + "sample_1_5" + ] + } + ] + } + } + ], + "logic": "and" + }, + "logic": "and" + }, + "schema": "create_cohort_schema", + "sql": "SELECT `sample_1`.`sample_id` AS `sample_id` FROM `database_gyk3jjj0vgpq2y1y98pj8pgg__create_cohort_geno_database`.`sample` AS `sample_1` WHERE `sample_1`.`sample_id` IN ('sample_1_1', 'sample_1_2', 'sample_1_3', 'sample_1_4', 'sample_1_5')", + "version": "3.0" + }, + "close": true +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/dx_new_input/not_null_as_input.json b/src/python/test/create_cohort_test_files/payloads/dx_new_input/not_null_as_input.json new file mode 100644 index 0000000000..8ef92ca0d5 --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/dx_new_input/not_null_as_input.json @@ -0,0 +1,56 @@ +{ + "name": "not_null_as_input", + "folder": "/Create_Cohort/manually_created_output_cohorts", + "project": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "types": [ + "DatabaseQuery", + "CohortBrowser" + ], + "details": { + "databases": [ + "database-GYK2yg00vGPpzj7YGY3VJxb9" + ], + "dataset": { + "$dnanexus_link": "record-GYK2zyQ0g1bx86fBp2X8KpjY" + }, + "description": "", + "filters": { + "pheno_filters": { + "compound": [ + { + "name": "phenotype", + "logic": "and", + "filters": { + "patient$patient_id": [ + { + "condition": "in", + "values": [ + "patient_3", + "patient_4", + "patient_5" + ] + } + ] + }, + "entity": { + "logic": "and", + "name": "patient", + "operator": "exists", + "children": [] + } + } + ], + "logic": "and" + }, + "assay_filters": { + "compound": [], + "logic": "and" + }, + "logic": "and" + }, + "schema": "create_cohort_schema", + "sql": "SELECT `patient_1`.`patient_id` AS `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`patient_id` IN ('patient_3', 'patient_4', 'patient_5')", + "version": "3.0" + }, + "close": true +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/dx_new_input/pheno_cohort_primary_entity_but_no_primary_key_filter.json b/src/python/test/create_cohort_test_files/payloads/dx_new_input/pheno_cohort_primary_entity_but_no_primary_key_filter.json new file mode 100644 index 0000000000..45c924536f --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/dx_new_input/pheno_cohort_primary_entity_but_no_primary_key_filter.json @@ -0,0 +1,68 @@ +{ + "name": "pheno_cohort_primary_entity_but_no_primary_key_filter", + "folder": "/Create_Cohort/manually_created_output_cohorts", + "project": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "types": [ + "DatabaseQuery", + "CohortBrowser" + ], + "details": { + "databases": [ + "database-GYK2yg00vGPpzj7YGY3VJxb9" + ], + "dataset": { + "$dnanexus_link": "record-GYK2zyQ0g1bx86fBp2X8KpjY" + }, + "description": "", + "filters": { + "pheno_filters": { + "compound": [ + { + "name": "phenotype", + "logic": "and", + "filters": { + "patient$name": [ + { + "condition": "in", + "values": [ + "Sally", + "Diane", + "Cassy", + "John", + "Rosaleen" + ] + } + ], + "patient$patient_id": [ + { + "condition": "in", + "values": [ + "patient_1", + "patient_2", + "patient_3" + ] + } + ] + }, + "entity": { + "logic": "and", + "name": "patient", + "operator": "exists", + "children": [] + } + } + ], + "logic": "and" + }, + "assay_filters": { + "compound": [], + "logic": "and" + }, + "logic": "and" + }, + "schema": "create_cohort_schema", + "sql": "SELECT `patient_1`.`patient_id` AS `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`name` IN ('Sally', 'Diane', 'Cassy', 'John', 'Rosaleen') AND `patient_1`.`patient_id` IN ('patient_1', 'patient_2', 'patient_3')", + "version": "3.0" + }, + "close": true +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/dx_new_input/pheno_cohort_primary_key_in_logic_and_as_input.json b/src/python/test/create_cohort_test_files/payloads/dx_new_input/pheno_cohort_primary_key_in_logic_and_as_input.json new file mode 100644 index 0000000000..aa47c0d627 --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/dx_new_input/pheno_cohort_primary_key_in_logic_and_as_input.json @@ -0,0 +1,56 @@ +{ + "name": "pheno_cohort_primary_key_in_logic_and_as_input", + "folder": "/Create_Cohort/manually_created_output_cohorts", + "project": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "types": [ + "DatabaseQuery", + "CohortBrowser" + ], + "details": { + "databases": [ + "database-GYK2yg00vGPpzj7YGY3VJxb9" + ], + "dataset": { + "$dnanexus_link": "record-GYK2zyQ0g1bx86fBp2X8KpjY" + }, + "description": "", + "filters": { + "pheno_filters": { + "compound": [ + { + "name": "phenotype", + "logic": "and", + "filters": { + "patient$patient_id": [ + { + "condition": "in", + "values": [ + "patient_1", + "patient_2", + "patient_3" + ] + } + ] + }, + "entity": { + "logic": "and", + "name": "patient", + "operator": "exists", + "children": [] + } + } + ], + "logic": "and" + }, + "assay_filters": { + "compound": [], + "logic": "and" + }, + "logic": "and" + }, + "schema": "create_cohort_schema", + "sql": "SELECT `patient_1`.`patient_id` AS `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`patient_id` IN ('patient_1', 'patient_2', 'patient_3')", + "version": "3.0" + }, + "close": true +} diff --git a/src/python/test/create_cohort_test_files/payloads/dx_new_input/pheno_cohort_primary_key_in_logic_and_as_input_2.json b/src/python/test/create_cohort_test_files/payloads/dx_new_input/pheno_cohort_primary_key_in_logic_and_as_input_2.json new file mode 100644 index 0000000000..61234a9050 --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/dx_new_input/pheno_cohort_primary_key_in_logic_and_as_input_2.json @@ -0,0 +1,54 @@ +{ + "name": "pheno_cohort_primary_key_in_logic_and_as_input_2", + "folder": "/Create_Cohort/manually_created_output_cohorts", + "project": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "types": [ + "DatabaseQuery", + "CohortBrowser" + ], + "details": { + "databases": [ + "database-GYK2yg00vGPpzj7YGY3VJxb9" + ], + "dataset": { + "$dnanexus_link": "record-GYK2zyQ0g1bx86fBp2X8KpjY" + }, + "description": "", + "filters": { + "pheno_filters": { + "compound": [ + { + "name": "phenotype", + "logic": "and", + "filters": { + "patient$patient_id": [ + { + "condition": "in", + "values": [ + "patient_3" + ] + } + ] + }, + "entity": { + "logic": "and", + "name": "patient", + "operator": "exists", + "children": [] + } + } + ], + "logic": "and" + }, + "assay_filters": { + "compound": [], + "logic": "and" + }, + "logic": "and" + }, + "schema": "create_cohort_schema", + "sql": "SELECT `patient_1`.`patient_id` AS `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`patient_id` IN ('patient_3')", + "version": "3.0" + }, + "close": true +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/dx_new_input/pheno_cohort_primary_key_not_in_logic_and_as_input.json b/src/python/test/create_cohort_test_files/payloads/dx_new_input/pheno_cohort_primary_key_not_in_logic_and_as_input.json new file mode 100644 index 0000000000..af6d298c6b --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/dx_new_input/pheno_cohort_primary_key_not_in_logic_and_as_input.json @@ -0,0 +1,56 @@ +{ + "name": "pheno_cohort_primary_key_not_in_logic_and_as_input", + "folder": "/Create_Cohort/manually_created_output_cohorts", + "project": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "types": [ + "DatabaseQuery", + "CohortBrowser" + ], + "details": { + "databases": [ + "database-GYK2yg00vGPpzj7YGY3VJxb9" + ], + "dataset": { + "$dnanexus_link": "record-GYK2zyQ0g1bx86fBp2X8KpjY" + }, + "description": "", + "filters": { + "pheno_filters": { + "compound": [ + { + "name": "phenotype", + "logic": "and", + "filters": { + "patient$patient_id": [ + { + "condition": "in", + "values": [ + "patient_1", + "patient_2", + "patient_3" + ] + } + ] + }, + "entity": { + "logic": "and", + "name": "patient", + "operator": "exists", + "children": [] + } + } + ], + "logic": "and" + }, + "assay_filters": { + "compound": [], + "logic": "and" + }, + "logic": "and" + }, + "schema": "create_cohort_schema", + "sql": "SELECT `patient_1`.`patient_id` AS `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`patient_id` IN ('patient_1', 'patient_2', 'patient_3')", + "version": "3.0" + }, + "close": true +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/dx_new_input/pheno_cohort_primary_key_not_in_logic_and_as_input_2.json b/src/python/test/create_cohort_test_files/payloads/dx_new_input/pheno_cohort_primary_key_not_in_logic_and_as_input_2.json new file mode 100644 index 0000000000..e6dc6f93a9 --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/dx_new_input/pheno_cohort_primary_key_not_in_logic_and_as_input_2.json @@ -0,0 +1,55 @@ +{ + "name": "pheno_cohort_primary_key_not_in_logic_and_as_input_2", + "folder": "/Create_Cohort/manually_created_output_cohorts", + "project": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "types": [ + "DatabaseQuery", + "CohortBrowser" + ], + "details": { + "databases": [ + "database-GYK2yg00vGPpzj7YGY3VJxb9" + ], + "dataset": { + "$dnanexus_link": "record-GYK2zyQ0g1bx86fBp2X8KpjY" + }, + "description": "", + "filters": { + "pheno_filters": { + "compound": [ + { + "name": "phenotype", + "logic": "and", + "filters": { + "patient$patient_id": [ + { + "condition": "in", + "values": [ + "patient_3", + "patient_4" + ] + } + ] + }, + "entity": { + "logic": "and", + "name": "patient", + "operator": "exists", + "children": [] + } + } + ], + "logic": "and" + }, + "assay_filters": { + "compound": [], + "logic": "and" + }, + "logic": "and" + }, + "schema": "create_cohort_schema", + "sql": "SELECT `patient_1`.`patient_id` AS `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`patient_id` IN ('patient_3', 'patient_4')", + "version": "3.0" + }, + "close": true +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/dx_new_input/pheno_cohort_with_no_primary_entity_filter.json b/src/python/test/create_cohort_test_files/payloads/dx_new_input/pheno_cohort_with_no_primary_entity_filter.json new file mode 100644 index 0000000000..73ce9dc267 --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/dx_new_input/pheno_cohort_with_no_primary_entity_filter.json @@ -0,0 +1,78 @@ +{ + "name": "pheno_cohort_with_no_primary_entity_filter", + "folder": "/Create_Cohort/manually_created_output_cohorts", + "project": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "types": [ + "DatabaseQuery", + "CohortBrowser" + ], + "details": { + "databases": [ + "database-GYK2yg00vGPpzj7YGY3VJxb9" + ], + "dataset": { + "$dnanexus_link": "record-GYK2zyQ0g1bx86fBp2X8KpjY" + }, + "description": "", + "filters": { + "pheno_filters": { + "compound": [ + { + "name": "phenotype", + "logic": "and", + "filters": { + "patient$patient_id": [ + { + "condition": "in", + "values": [ + "patient_1", + "patient_2", + "patient_3" + ] + } + ] + }, + "entity": { + "logic": "and", + "name": "patient", + "operator": "exists", + "children": [ + { + "name": "hospital@18e6dfe5-3369-44ff-8305-4feca890af15", + "logic": "and", + "filters": { + "hospital$min_score_all": [ + { + "condition": "in", + "values": [ + 500, + 400 + ] + } + ] + }, + "entity": { + "logic": "and", + "name": "hospital", + "operator": "exists", + "children": [] + } + } + ] + } + } + ], + "logic": "and" + }, + "assay_filters": { + "compound": [], + "logic": "and" + }, + "logic": "and" + }, + "schema": "create_cohort_schema", + "sql": "SELECT `cohort_subquery`.`patient_id` AS `patient_id` FROM (SELECT `patient_1`.`patient_id` AS `patient_id`, `patient_1`.`hid` AS `hid` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`patient_id` IN ('patient_1', 'patient_2', 'patient_3') AND (EXISTS (SELECT `hospital_1`.`hospital_id` AS `hospital_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`hospital` AS `hospital_1` WHERE `hospital_1`.`min_score_all` IN (500, 400) AND `hospital_1`.`hospital_id` = `patient_1`.`hid`))) AS `cohort_subquery`", + "version": "3.0" + }, + "close": true +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/dx_new_input/pheno_geno_merged_dataset_as_input.json b/src/python/test/create_cohort_test_files/payloads/dx_new_input/pheno_geno_merged_dataset_as_input.json new file mode 100644 index 0000000000..02894f44cb --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/dx_new_input/pheno_geno_merged_dataset_as_input.json @@ -0,0 +1,49 @@ +{ + "name": "pheno_geno_merged_dataset_as_input", + "folder": "/Create_Cohort/manually_created_output_cohorts", + "project": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "types": [ + "DatabaseQuery", + "CohortBrowser" + ], + "details": { + "databases": [ + "database-GYK2yg00vGPpzj7YGY3VJxb9", + "database-FkypXj80bgYvQqyqBx1FJG6y", + "database-GYK3JJj0vGPQ2Y1y98pj8pGg", + "database-GYgzGf80vGPvvQG47p5bpX7x" + ], + "dataset": { + "$dnanexus_link": "record-GYgzjV80yB6QPJJ6kk40bBFy" + }, + "description": "", + "filters": { + "pheno_filters": { + "compound": [ + { + "name": "phenotype", + "logic": "and", + "filters": { + "patient$patient_id": [ + { + "condition": "in", + "values": [ + "patient_1", + "patient_2", + "patient_3" + ] + } + ] + } + } + ], + "logic": "and" + }, + "logic": "and" + }, + "schema": "create_cohort_schema", + "sql": "SELECT `patient_1`.`patient_id` AS `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`patient_id` IN ('patient_1', 'patient_2', 'patient_3')", + "version": "3.0" + }, + "close": true +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/dx_new_input/unfiltered_cohort_as_input.json b/src/python/test/create_cohort_test_files/payloads/dx_new_input/unfiltered_cohort_as_input.json new file mode 100644 index 0000000000..fb0fbc3c12 --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/dx_new_input/unfiltered_cohort_as_input.json @@ -0,0 +1,56 @@ +{ + "name": "unfiltered_cohort_as_input", + "folder": "/Create_Cohort/manually_created_output_cohorts", + "project": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "types": [ + "DatabaseQuery", + "CohortBrowser" + ], + "details": { + "databases": [ + "database-GYK2yg00vGPpzj7YGY3VJxb9" + ], + "dataset": { + "$dnanexus_link": "record-GYK2zyQ0g1bx86fBp2X8KpjY" + }, + "description": "", + "filters": { + "pheno_filters": { + "compound": [ + { + "name": "phenotype", + "logic": "and", + "filters": { + "patient$patient_id": [ + { + "condition": "in", + "values": [ + "patient_1", + "patient_2", + "patient_3" + ] + } + ] + }, + "entity": { + "logic": "and", + "name": "patient", + "operator": "exists", + "children": [] + } + } + ], + "logic": "and" + }, + "assay_filters": { + "compound": [], + "logic": "and" + }, + "logic": "and" + }, + "schema": "create_cohort_schema", + "sql": "SELECT `patient_1`.`patient_id` AS `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`patient_id` IN ('patient_1', 'patient_2', 'patient_3')", + "version": "3.0" + }, + "close": true +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/input_parameters/combined_cohort_no_filter_as_input.json b/src/python/test/create_cohort_test_files/payloads/input_parameters/combined_cohort_no_filter_as_input.json new file mode 100644 index 0000000000..265ca22353 --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/input_parameters/combined_cohort_no_filter_as_input.json @@ -0,0 +1,11 @@ +{ + "values": [ + "patient_1", + "patient_2", + "patient_3" + ], + "entity": "patient", + "field": "patient_id", + "project": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "folder": "/Create_Cohort/manually_created_output_cohorts" +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/input_parameters/combined_cohort_non_primary_key_filter_input.json b/src/python/test/create_cohort_test_files/payloads/input_parameters/combined_cohort_non_primary_key_filter_input.json new file mode 100644 index 0000000000..257f59c5da --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/input_parameters/combined_cohort_non_primary_key_filter_input.json @@ -0,0 +1,10 @@ +{ + "values": [ + "patient_1", + "patient_2" + ], + "entity": "patient", + "field": "patient_id", + "project": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "folder": "/Create_Cohort/manually_created_output_cohorts" +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/input_parameters/combined_cohort_primary_field_filter_as_input.json b/src/python/test/create_cohort_test_files/payloads/input_parameters/combined_cohort_primary_field_filter_as_input.json new file mode 100644 index 0000000000..265ca22353 --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/input_parameters/combined_cohort_primary_field_filter_as_input.json @@ -0,0 +1,11 @@ +{ + "values": [ + "patient_1", + "patient_2", + "patient_3" + ], + "entity": "patient", + "field": "patient_id", + "project": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "folder": "/Create_Cohort/manually_created_output_cohorts" +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/input_parameters/complex_cohort_as_input.json b/src/python/test/create_cohort_test_files/payloads/input_parameters/complex_cohort_as_input.json new file mode 100644 index 0000000000..257f59c5da --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/input_parameters/complex_cohort_as_input.json @@ -0,0 +1,10 @@ +{ + "values": [ + "patient_1", + "patient_2" + ], + "entity": "patient", + "field": "patient_id", + "project": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "folder": "/Create_Cohort/manually_created_output_cohorts" +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/input_parameters/dataset_as_input.json b/src/python/test/create_cohort_test_files/payloads/input_parameters/dataset_as_input.json new file mode 100644 index 0000000000..265ca22353 --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/input_parameters/dataset_as_input.json @@ -0,0 +1,11 @@ +{ + "values": [ + "patient_1", + "patient_2", + "patient_3" + ], + "entity": "patient", + "field": "patient_id", + "project": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "folder": "/Create_Cohort/manually_created_output_cohorts" +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/input_parameters/geno_cohort_with_geno_filters_filtered_with_primary_field.json b/src/python/test/create_cohort_test_files/payloads/input_parameters/geno_cohort_with_geno_filters_filtered_with_primary_field.json new file mode 100644 index 0000000000..cf75a41a23 --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/input_parameters/geno_cohort_with_geno_filters_filtered_with_primary_field.json @@ -0,0 +1,11 @@ +{ + "values": [ + "sample_1_1", + "sample_1_2", + "sample_1_5" + ], + "entity": "sample", + "field": "sample_id", + "project": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "folder": "/Create_Cohort/manually_created_output_cohorts" +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/input_parameters/geno_dataset_filtered_with_primary_field.json b/src/python/test/create_cohort_test_files/payloads/input_parameters/geno_dataset_filtered_with_primary_field.json new file mode 100644 index 0000000000..402fe156ba --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/input_parameters/geno_dataset_filtered_with_primary_field.json @@ -0,0 +1,13 @@ +{ + "values": [ + "sample_1_1", + "sample_1_2", + "sample_1_3", + "sample_1_4", + "sample_1_5" + ], + "entity": "sample", + "field": "sample_id", + "project": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "folder": "/Create_Cohort/manually_created_output_cohorts" +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/input_parameters/not_null_as_input.json b/src/python/test/create_cohort_test_files/payloads/input_parameters/not_null_as_input.json new file mode 100644 index 0000000000..a9ac3620af --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/input_parameters/not_null_as_input.json @@ -0,0 +1,11 @@ +{ + "values": [ + "patient_3", + "patient_4", + "patient_5" + ], + "entity": "patient", + "field": "patient_id", + "project": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "folder": "/Create_Cohort/manually_created_output_cohorts" +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/input_parameters/pheno_cohort_primary_entity_but_no_primary_key_filter.json b/src/python/test/create_cohort_test_files/payloads/input_parameters/pheno_cohort_primary_entity_but_no_primary_key_filter.json new file mode 100644 index 0000000000..265ca22353 --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/input_parameters/pheno_cohort_primary_entity_but_no_primary_key_filter.json @@ -0,0 +1,11 @@ +{ + "values": [ + "patient_1", + "patient_2", + "patient_3" + ], + "entity": "patient", + "field": "patient_id", + "project": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "folder": "/Create_Cohort/manually_created_output_cohorts" +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/input_parameters/pheno_cohort_primary_key_in_logic_and_as_input.json b/src/python/test/create_cohort_test_files/payloads/input_parameters/pheno_cohort_primary_key_in_logic_and_as_input.json new file mode 100644 index 0000000000..265ca22353 --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/input_parameters/pheno_cohort_primary_key_in_logic_and_as_input.json @@ -0,0 +1,11 @@ +{ + "values": [ + "patient_1", + "patient_2", + "patient_3" + ], + "entity": "patient", + "field": "patient_id", + "project": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "folder": "/Create_Cohort/manually_created_output_cohorts" +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/input_parameters/pheno_cohort_primary_key_in_logic_and_as_input_2.json b/src/python/test/create_cohort_test_files/payloads/input_parameters/pheno_cohort_primary_key_in_logic_and_as_input_2.json new file mode 100644 index 0000000000..a9ac3620af --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/input_parameters/pheno_cohort_primary_key_in_logic_and_as_input_2.json @@ -0,0 +1,11 @@ +{ + "values": [ + "patient_3", + "patient_4", + "patient_5" + ], + "entity": "patient", + "field": "patient_id", + "project": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "folder": "/Create_Cohort/manually_created_output_cohorts" +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/input_parameters/pheno_cohort_primary_key_not_in_logic_and_as_input.json b/src/python/test/create_cohort_test_files/payloads/input_parameters/pheno_cohort_primary_key_not_in_logic_and_as_input.json new file mode 100644 index 0000000000..265ca22353 --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/input_parameters/pheno_cohort_primary_key_not_in_logic_and_as_input.json @@ -0,0 +1,11 @@ +{ + "values": [ + "patient_1", + "patient_2", + "patient_3" + ], + "entity": "patient", + "field": "patient_id", + "project": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "folder": "/Create_Cohort/manually_created_output_cohorts" +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/input_parameters/pheno_cohort_primary_key_not_in_logic_and_as_input_2.json b/src/python/test/create_cohort_test_files/payloads/input_parameters/pheno_cohort_primary_key_not_in_logic_and_as_input_2.json new file mode 100644 index 0000000000..106df5c2c5 --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/input_parameters/pheno_cohort_primary_key_not_in_logic_and_as_input_2.json @@ -0,0 +1,11 @@ +{ + "values": [ + "patient_3", + "patient_4", + "patient_8" + ], + "entity": "patient", + "field": "patient_id", + "project": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "folder": "/Create_Cohort/manually_created_output_cohorts" +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/input_parameters/pheno_cohort_with_no_primary_entity_filter.json b/src/python/test/create_cohort_test_files/payloads/input_parameters/pheno_cohort_with_no_primary_entity_filter.json new file mode 100644 index 0000000000..265ca22353 --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/input_parameters/pheno_cohort_with_no_primary_entity_filter.json @@ -0,0 +1,11 @@ +{ + "values": [ + "patient_1", + "patient_2", + "patient_3" + ], + "entity": "patient", + "field": "patient_id", + "project": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "folder": "/Create_Cohort/manually_created_output_cohorts" +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/input_parameters/pheno_geno_merged_dataset_as_input.json b/src/python/test/create_cohort_test_files/payloads/input_parameters/pheno_geno_merged_dataset_as_input.json new file mode 100644 index 0000000000..265ca22353 --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/input_parameters/pheno_geno_merged_dataset_as_input.json @@ -0,0 +1,11 @@ +{ + "values": [ + "patient_1", + "patient_2", + "patient_3" + ], + "entity": "patient", + "field": "patient_id", + "project": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "folder": "/Create_Cohort/manually_created_output_cohorts" +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/input_parameters/unfiltered_cohort_as_input.json b/src/python/test/create_cohort_test_files/payloads/input_parameters/unfiltered_cohort_as_input.json new file mode 100644 index 0000000000..265ca22353 --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/input_parameters/unfiltered_cohort_as_input.json @@ -0,0 +1,11 @@ +{ + "values": [ + "patient_1", + "patient_2", + "patient_3" + ], + "entity": "patient", + "field": "patient_id", + "project": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "folder": "/Create_Cohort/manually_created_output_cohorts" +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_input/combined_cohort_no_filter_as_input.json b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_input/combined_cohort_no_filter_as_input.json new file mode 100644 index 0000000000..ce338707eb --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_input/combined_cohort_no_filter_as_input.json @@ -0,0 +1,38 @@ +{ + "filters": { + "pheno_filters": { + "compound": [ + { + "name": "phenotype", + "logic": "and", + "filters": { + "patient$patient_id": [ + { + "condition": "in", + "values": [ + "patient_1", + "patient_2", + "patient_3" + ] + } + ] + }, + "entity": { + "logic": "and", + "name": "patient", + "operator": "exists", + "children": [] + } + } + ], + "logic": "and" + }, + "assay_filters": { + "compound": [], + "logic": "and" + }, + "logic": "and" + }, + "project_context": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "base_sql": "SELECT `patient_id` FROM (SELECT `cohort_subquery`.`patient_id` AS `patient_id` FROM (SELECT `patient_1`.`patient_id` AS `patient_id`, `patient_1`.`hid` AS `hid` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE EXISTS (SELECT `hospital_1`.`hospital_id` AS `hospital_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`hospital` AS `hospital_1` WHERE `hospital_1`.`min_score_all` IN (500, 400) AND `hospital_1`.`hospital_id` = `patient_1`.`hid`)) AS `cohort_subquery` INTERSECT SELECT `patient_1`.`patient_id` AS `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`name` IN ('Sally', 'Diane', 'Cassy', 'John', 'Rosaleen'))" +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_input/combined_cohort_non_primary_key_filter_input.json b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_input/combined_cohort_non_primary_key_filter_input.json new file mode 100644 index 0000000000..4650bc4ba5 --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_input/combined_cohort_non_primary_key_filter_input.json @@ -0,0 +1,77 @@ +{ + "filters": { + "pheno_filters": { + "compound": [ + { + "name": "phenotype", + "logic": "and", + "filters": { + "patient$age": [ + { + "condition": "between", + "values": [ + 20, + 60 + ] + } + ], + "patient$patient_id": [ + { + "condition": "in", + "values": [ + "patient_1", + "patient_2" + ] + } + ] + }, + "entity": { + "logic": "and", + "name": "patient", + "operator": "exists", + "children": [ + { + "name": "hospital@e98fe382-3cb5-4c25-a771-09a24ae0f91e", + "logic": "or", + "filters": { + "hospital$min_score_all": [ + { + "condition": "in", + "values": [ + 400, + 500 + ] + } + ], + "hospital$location": [ + { + "condition": "any", + "values": [ + "CA", + "BC" + ] + } + ] + }, + "entity": { + "logic": "and", + "name": "hospital", + "operator": "exists", + "children": [] + } + } + ] + } + } + ], + "logic": "and" + }, + "assay_filters": { + "compound": [], + "logic": "and" + }, + "logic": "and" + }, + "base_sql": "SELECT `patient_id` FROM (SELECT `cohort_subquery`.`patient_id` AS `patient_id` FROM (SELECT `patient_1`.`patient_id` AS `patient_id`, `patient_1`.`hid` AS `hid` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE EXISTS (SELECT `hospital_1`.`hospital_id` AS `hospital_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`hospital` AS `hospital_1` WHERE `hospital_1`.`min_score_all` IN (500, 400) AND `hospital_1`.`hospital_id` = `patient_1`.`hid`)) AS `cohort_subquery` INTERSECT SELECT `patient_1`.`patient_id` AS `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`name` IN ('Sally', 'Diane', 'Cassy', 'John', 'Rosaleen'))", + "project_context": "project-G9j1pX00vGPzF2XQ7843k2Jq" +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_input/combined_cohort_primary_field_filter_as_input.json b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_input/combined_cohort_primary_field_filter_as_input.json new file mode 100644 index 0000000000..ce338707eb --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_input/combined_cohort_primary_field_filter_as_input.json @@ -0,0 +1,38 @@ +{ + "filters": { + "pheno_filters": { + "compound": [ + { + "name": "phenotype", + "logic": "and", + "filters": { + "patient$patient_id": [ + { + "condition": "in", + "values": [ + "patient_1", + "patient_2", + "patient_3" + ] + } + ] + }, + "entity": { + "logic": "and", + "name": "patient", + "operator": "exists", + "children": [] + } + } + ], + "logic": "and" + }, + "assay_filters": { + "compound": [], + "logic": "and" + }, + "logic": "and" + }, + "project_context": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "base_sql": "SELECT `patient_id` FROM (SELECT `cohort_subquery`.`patient_id` AS `patient_id` FROM (SELECT `patient_1`.`patient_id` AS `patient_id`, `patient_1`.`hid` AS `hid` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE EXISTS (SELECT `hospital_1`.`hospital_id` AS `hospital_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`hospital` AS `hospital_1` WHERE `hospital_1`.`min_score_all` IN (500, 400) AND `hospital_1`.`hospital_id` = `patient_1`.`hid`)) AS `cohort_subquery` INTERSECT SELECT `patient_1`.`patient_id` AS `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`name` IN ('Sally', 'Diane', 'Cassy', 'John', 'Rosaleen'))" +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_input/complex_cohort_as_input.json b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_input/complex_cohort_as_input.json new file mode 100644 index 0000000000..c50dc3c90d --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_input/complex_cohort_as_input.json @@ -0,0 +1,27 @@ +{ + "filters": { + "pheno_filters": { + "compound": [ + { + "name": "phenotype", + "logic": "and", + "filters": { + "patient$patient_id": [ + { + "condition": "in", + "values": [ + "patient_1", + "patient_2" + ] + } + ] + } + } + ], + "logic": "and" + }, + "logic": "and" + }, + "base_sql": "SELECT `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` WHERE `patient_id` in ('patient_1', 'patient_2', 'patient_3')", + "project_context": "project-G9j1pX00vGPzF2XQ7843k2Jq" +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_input/dataset_as_input.json b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_input/dataset_as_input.json new file mode 100644 index 0000000000..dd8e337805 --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_input/dataset_as_input.json @@ -0,0 +1,27 @@ +{ + "filters": { + "pheno_filters": { + "compound": [ + { + "name": "phenotype", + "logic": "and", + "filters": { + "patient$patient_id": [ + { + "condition": "in", + "values": [ + "patient_1", + "patient_2", + "patient_3" + ] + } + ] + } + } + ], + "logic": "and" + }, + "logic": "and" + }, + "project_context": "project-G9j1pX00vGPzF2XQ7843k2Jq" +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_input/geno_cohort_with_geno_filters_filtered_with_primary_field.json b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_input/geno_cohort_with_geno_filters_filtered_with_primary_field.json new file mode 100644 index 0000000000..6138c4c65a --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_input/geno_cohort_with_geno_filters_filtered_with_primary_field.json @@ -0,0 +1,65 @@ +{ + "filters": { + "pheno_filters": { + "compound": [ + { + "name": "phenotype", + "logic": "and", + "filters": { + "sample$sample_id": [ + { + "condition": "in", + "values": [ + "sample_1_1", + "sample_1_2", + "sample_1_5" + ] + } + ] + }, + "entity": { + "logic": "and", + "name": "sample", + "operator": "exists", + "children": [] + } + } + ], + "logic": "and" + }, + "assay_filters": { + "compound": [ + { + "name": "genotype@b07df2d4-a05c-4c6a-9a20-7602695d9e47", + "logic": "and", + "filters": { + "annotation$gene_name": [ + { + "condition": "in", + "values": [], + "geno_bins": [ + { + "chr": "18", + "start": 47368, + "end": 47368 + } + ] + } + ], + "genotype$type": [ + { + "condition": "in", + "values": [ + "hom" + ] + } + ] + } + } + ], + "logic": "and" + }, + "logic": "and" + }, + "project_context": "project-G9j1pX00vGPzF2XQ7843k2Jq" +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_input/geno_dataset_filtered_with_primary_field.json b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_input/geno_dataset_filtered_with_primary_field.json new file mode 100644 index 0000000000..5fc2bb742c --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_input/geno_dataset_filtered_with_primary_field.json @@ -0,0 +1,29 @@ +{ + "filters": { + "pheno_filters": { + "compound": [ + { + "name": "phenotype", + "logic": "and", + "filters": { + "sample$sample_id": [ + { + "condition": "in", + "values": [ + "sample_1_1", + "sample_1_2", + "sample_1_3", + "sample_1_4", + "sample_1_5" + ] + } + ] + } + } + ], + "logic": "and" + }, + "logic": "and" + }, + "project_context": "project-G9j1pX00vGPzF2XQ7843k2Jq" +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_input/not_null_as_input.json b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_input/not_null_as_input.json new file mode 100644 index 0000000000..912fea1659 --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_input/not_null_as_input.json @@ -0,0 +1,37 @@ +{ + "filters": { + "pheno_filters": { + "compound": [ + { + "name": "phenotype", + "logic": "and", + "filters": { + "patient$patient_id": [ + { + "condition": "in", + "values": [ + "patient_3", + "patient_4", + "patient_5" + ] + } + ] + }, + "entity": { + "logic": "and", + "name": "patient", + "operator": "exists", + "children": [] + } + } + ], + "logic": "and" + }, + "assay_filters": { + "compound": [], + "logic": "and" + }, + "logic": "and" + }, + "project_context": "project-G9j1pX00vGPzF2XQ7843k2Jq" +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_input/pheno_cohort_primary_entity_but_no_primary_key_filter.json b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_input/pheno_cohort_primary_entity_but_no_primary_key_filter.json new file mode 100644 index 0000000000..5c9883994c --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_input/pheno_cohort_primary_entity_but_no_primary_key_filter.json @@ -0,0 +1,49 @@ +{ + "filters": { + "pheno_filters": { + "compound": [ + { + "name": "phenotype", + "logic": "and", + "filters": { + "patient$name": [ + { + "condition": "in", + "values": [ + "Sally", + "Diane", + "Cassy", + "John", + "Rosaleen" + ] + } + ], + "patient$patient_id": [ + { + "condition": "in", + "values": [ + "patient_1", + "patient_2", + "patient_3" + ] + } + ] + }, + "entity": { + "logic": "and", + "name": "patient", + "operator": "exists", + "children": [] + } + } + ], + "logic": "and" + }, + "assay_filters": { + "compound": [], + "logic": "and" + }, + "logic": "and" + }, + "project_context": "project-G9j1pX00vGPzF2XQ7843k2Jq" +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_input/pheno_cohort_primary_key_in_logic_and_as_input.json b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_input/pheno_cohort_primary_key_in_logic_and_as_input.json new file mode 100644 index 0000000000..0ac5396c90 --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_input/pheno_cohort_primary_key_in_logic_and_as_input.json @@ -0,0 +1,37 @@ +{ + "filters": { + "pheno_filters": { + "compound": [ + { + "name": "phenotype", + "logic": "and", + "filters": { + "patient$patient_id": [ + { + "condition": "in", + "values": [ + "patient_1", + "patient_2", + "patient_3" + ] + } + ] + }, + "entity": { + "logic": "and", + "name": "patient", + "operator": "exists", + "children": [] + } + } + ], + "logic": "and" + }, + "assay_filters": { + "compound": [], + "logic": "and" + }, + "logic": "and" + }, + "project_context": "project-G9j1pX00vGPzF2XQ7843k2Jq" +} diff --git a/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_input/pheno_cohort_primary_key_in_logic_and_as_input_2.json b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_input/pheno_cohort_primary_key_in_logic_and_as_input_2.json new file mode 100644 index 0000000000..8977e778a9 --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_input/pheno_cohort_primary_key_in_logic_and_as_input_2.json @@ -0,0 +1,35 @@ +{ + "filters": { + "pheno_filters": { + "compound": [ + { + "name": "phenotype", + "logic": "and", + "filters": { + "patient$patient_id": [ + { + "condition": "in", + "values": [ + "patient_3" + ] + } + ] + }, + "entity": { + "logic": "and", + "name": "patient", + "operator": "exists", + "children": [] + } + } + ], + "logic": "and" + }, + "assay_filters": { + "compound": [], + "logic": "and" + }, + "logic": "and" + }, + "project_context": "project-G9j1pX00vGPzF2XQ7843k2Jq" +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_input/pheno_cohort_primary_key_not_in_logic_and_as_input.json b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_input/pheno_cohort_primary_key_not_in_logic_and_as_input.json new file mode 100644 index 0000000000..ed8f70a499 --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_input/pheno_cohort_primary_key_not_in_logic_and_as_input.json @@ -0,0 +1,37 @@ +{ + "filters": { + "pheno_filters": { + "compound": [ + { + "name": "phenotype", + "logic": "and", + "filters": { + "patient$patient_id": [ + { + "condition": "in", + "values": [ + "patient_1", + "patient_2", + "patient_3" + ] + } + ] + }, + "entity": { + "logic": "and", + "name": "patient", + "operator": "exists", + "children": [] + } + } + ], + "logic": "and" + }, + "assay_filters": { + "compound": [], + "logic": "and" + }, + "logic": "and" + }, + "project_context": "project-G9j1pX00vGPzF2XQ7843k2Jq" +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_input/pheno_cohort_primary_key_not_in_logic_and_as_input_2.json b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_input/pheno_cohort_primary_key_not_in_logic_and_as_input_2.json new file mode 100644 index 0000000000..a06d2d698c --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_input/pheno_cohort_primary_key_not_in_logic_and_as_input_2.json @@ -0,0 +1,36 @@ +{ + "filters": { + "pheno_filters": { + "compound": [ + { + "name": "phenotype", + "logic": "and", + "filters": { + "patient$patient_id": [ + { + "condition": "in", + "values": [ + "patient_3", + "patient_4" + ] + } + ] + }, + "entity": { + "logic": "and", + "name": "patient", + "operator": "exists", + "children": [] + } + } + ], + "logic": "and" + }, + "assay_filters": { + "compound": [], + "logic": "and" + }, + "logic": "and" + }, + "project_context": "project-G9j1pX00vGPzF2XQ7843k2Jq" +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_input/pheno_cohort_with_no_primary_entity_filter.json b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_input/pheno_cohort_with_no_primary_entity_filter.json new file mode 100644 index 0000000000..a8c8c5c03b --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_input/pheno_cohort_with_no_primary_entity_filter.json @@ -0,0 +1,59 @@ +{ + "filters": { + "pheno_filters": { + "compound": [ + { + "name": "phenotype", + "logic": "and", + "filters": { + "patient$patient_id": [ + { + "condition": "in", + "values": [ + "patient_1", + "patient_2", + "patient_3" + ] + } + ] + }, + "entity": { + "logic": "and", + "name": "patient", + "operator": "exists", + "children": [ + { + "name": "hospital@18e6dfe5-3369-44ff-8305-4feca890af15", + "logic": "and", + "filters": { + "hospital$min_score_all": [ + { + "condition": "in", + "values": [ + 500, + 400 + ] + } + ] + }, + "entity": { + "logic": "and", + "name": "hospital", + "operator": "exists", + "children": [] + } + } + ] + } + } + ], + "logic": "and" + }, + "assay_filters": { + "compound": [], + "logic": "and" + }, + "logic": "and" + }, + "project_context": "project-G9j1pX00vGPzF2XQ7843k2Jq" +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_input/pheno_geno_merged_dataset_as_input.json b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_input/pheno_geno_merged_dataset_as_input.json new file mode 100644 index 0000000000..dd8e337805 --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_input/pheno_geno_merged_dataset_as_input.json @@ -0,0 +1,27 @@ +{ + "filters": { + "pheno_filters": { + "compound": [ + { + "name": "phenotype", + "logic": "and", + "filters": { + "patient$patient_id": [ + { + "condition": "in", + "values": [ + "patient_1", + "patient_2", + "patient_3" + ] + } + ] + } + } + ], + "logic": "and" + }, + "logic": "and" + }, + "project_context": "project-G9j1pX00vGPzF2XQ7843k2Jq" +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_input/unfiltered_cohort_as_input.json b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_input/unfiltered_cohort_as_input.json new file mode 100644 index 0000000000..ed8f70a499 --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_input/unfiltered_cohort_as_input.json @@ -0,0 +1,37 @@ +{ + "filters": { + "pheno_filters": { + "compound": [ + { + "name": "phenotype", + "logic": "and", + "filters": { + "patient$patient_id": [ + { + "condition": "in", + "values": [ + "patient_1", + "patient_2", + "patient_3" + ] + } + ] + }, + "entity": { + "logic": "and", + "name": "patient", + "operator": "exists", + "children": [] + } + } + ], + "logic": "and" + }, + "assay_filters": { + "compound": [], + "logic": "and" + }, + "logic": "and" + }, + "project_context": "project-G9j1pX00vGPzF2XQ7843k2Jq" +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_output/combined_cohort_no_filter_as_input.sql b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_output/combined_cohort_no_filter_as_input.sql new file mode 100644 index 0000000000..38095f7f67 --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_output/combined_cohort_no_filter_as_input.sql @@ -0,0 +1 @@ +SELECT `patient_1`.`patient_id` AS `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`patient_id` IN ('patient_1', 'patient_2', 'patient_3') AND `patient_1`.`patient_id` IN (SELECT `patient_id` FROM (SELECT `cohort_subquery`.`patient_id` AS `patient_id` FROM (SELECT `patient_1`.`patient_id` AS `patient_id`, `patient_1`.`hid` AS `hid` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE EXISTS (SELECT `hospital_1`.`hospital_id` AS `hospital_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`hospital` AS `hospital_1` WHERE `hospital_1`.`min_score_all` IN (500, 400) AND `hospital_1`.`hospital_id` = `patient_1`.`hid`)) AS `cohort_subquery` INTERSECT SELECT `patient_1`.`patient_id` AS `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`name` IN ('Sally', 'Diane', 'Cassy', 'John', 'Rosaleen'))) \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_output/combined_cohort_non_primary_key_filter_input.sql b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_output/combined_cohort_non_primary_key_filter_input.sql new file mode 100644 index 0000000000..66e5456760 --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_output/combined_cohort_non_primary_key_filter_input.sql @@ -0,0 +1 @@ +SELECT `cohort_subquery`.`patient_id` AS `patient_id` FROM (SELECT `patient_1`.`patient_id` AS `patient_id`, `patient_1`.`hid` AS `hid` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`age` BETWEEN 20 AND 60 AND `patient_1`.`patient_id` IN ('patient_1', 'patient_2') AND (EXISTS (SELECT `hospital_1`.`hospital_id` AS `hospital_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`hospital` AS `hospital_1` WHERE (`hospital_1`.`min_score_all` IN (400, 500) OR ARRAY_CONTAINS(`hospital_1`.`location_hierarchy`, 'CA') OR ARRAY_CONTAINS(`hospital_1`.`location_hierarchy`, 'BC')) AND `hospital_1`.`hospital_id` = `patient_1`.`hid`)) AND `patient_1`.`patient_id` IN (SELECT `patient_id` FROM (SELECT `cohort_subquery`.`patient_id` AS `patient_id` FROM (SELECT `patient_1`.`patient_id` AS `patient_id`, `patient_1`.`hid` AS `hid` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE EXISTS (SELECT `hospital_1`.`hospital_id` AS `hospital_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`hospital` AS `hospital_1` WHERE `hospital_1`.`min_score_all` IN (500, 400) AND `hospital_1`.`hospital_id` = `patient_1`.`hid`)) AS `cohort_subquery` INTERSECT SELECT `patient_1`.`patient_id` AS `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`name` IN ('Sally', 'Diane', 'Cassy', 'John', 'Rosaleen')))) AS `cohort_subquery` \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_output/combined_cohort_primary_field_filter_as_input.sql b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_output/combined_cohort_primary_field_filter_as_input.sql new file mode 100644 index 0000000000..38095f7f67 --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_output/combined_cohort_primary_field_filter_as_input.sql @@ -0,0 +1 @@ +SELECT `patient_1`.`patient_id` AS `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`patient_id` IN ('patient_1', 'patient_2', 'patient_3') AND `patient_1`.`patient_id` IN (SELECT `patient_id` FROM (SELECT `cohort_subquery`.`patient_id` AS `patient_id` FROM (SELECT `patient_1`.`patient_id` AS `patient_id`, `patient_1`.`hid` AS `hid` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE EXISTS (SELECT `hospital_1`.`hospital_id` AS `hospital_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`hospital` AS `hospital_1` WHERE `hospital_1`.`min_score_all` IN (500, 400) AND `hospital_1`.`hospital_id` = `patient_1`.`hid`)) AS `cohort_subquery` INTERSECT SELECT `patient_1`.`patient_id` AS `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`name` IN ('Sally', 'Diane', 'Cassy', 'John', 'Rosaleen'))) \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_output/complex_cohort_as_input.sql b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_output/complex_cohort_as_input.sql new file mode 100644 index 0000000000..e033382ea5 --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_output/complex_cohort_as_input.sql @@ -0,0 +1 @@ +SELECT `patient_1`.`patient_id` AS `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`patient_id` IN ('patient_1', 'patient_2') AND `patient_1`.`patient_id` IN (SELECT `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` WHERE `patient_id` in ('patient_1', 'patient_2', 'patient_3')) \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_output/dataset_as_input.sql b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_output/dataset_as_input.sql new file mode 100644 index 0000000000..0b0daa9ab6 --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_output/dataset_as_input.sql @@ -0,0 +1 @@ +SELECT `patient_1`.`patient_id` AS `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`patient_id` IN ('patient_1', 'patient_2', 'patient_3') \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_output/geno_cohort_with_geno_filters_filtered_with_primary_field.sql b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_output/geno_cohort_with_geno_filters_filtered_with_primary_field.sql new file mode 100644 index 0000000000..f0e88d550c --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_output/geno_cohort_with_geno_filters_filtered_with_primary_field.sql @@ -0,0 +1 @@ +SELECT `sample_1`.`sample_id` AS `sample_id` FROM `database_gyk3jjj0vgpq2y1y98pj8pgg__create_cohort_geno_database`.`sample` AS `sample_1` WHERE `sample_1`.`sample_id` IN ('sample_1_1', 'sample_1_2', 'sample_1_5') AND `sample_1`.`sample_id` IN (SELECT `assay_cohort_query`.`sample_id` FROM (SELECT `genotype_alt_read_optimized_1`.`sample_id` AS `sample_id` FROM `database_gyk3jjj0vgpq2y1y98pj8pgg__create_cohort_geno_database`.`genotype_alt_read_optimized` AS `genotype_alt_read_optimized_1` WHERE `genotype_alt_read_optimized_1`.`ref_yn` = false AND `genotype_alt_read_optimized_1`.`chr` = '18' AND `genotype_alt_read_optimized_1`.`pos` BETWEEN 46368 AND 48368 AND `genotype_alt_read_optimized_1`.`bin` IN (0) AND `genotype_alt_read_optimized_1`.`type` IN ('hom')) AS `assay_cohort_query`) \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_output/geno_dataset_filtered_with_primary_field.sql b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_output/geno_dataset_filtered_with_primary_field.sql new file mode 100644 index 0000000000..c2e2e86c21 --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_output/geno_dataset_filtered_with_primary_field.sql @@ -0,0 +1 @@ +SELECT `sample_1`.`sample_id` AS `sample_id` FROM `database_gyk3jjj0vgpq2y1y98pj8pgg__create_cohort_geno_database`.`sample` AS `sample_1` WHERE `sample_1`.`sample_id` IN ('sample_1_1', 'sample_1_2', 'sample_1_3', 'sample_1_4', 'sample_1_5') \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_output/not_null_as_input.sql b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_output/not_null_as_input.sql new file mode 100644 index 0000000000..19e0f7e585 --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_output/not_null_as_input.sql @@ -0,0 +1 @@ +SELECT `patient_1`.`patient_id` AS `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`patient_id` IN ('patient_3', 'patient_4', 'patient_5') \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_output/pheno_cohort_primary_entity_but_no_primary_key_filter.sql b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_output/pheno_cohort_primary_entity_but_no_primary_key_filter.sql new file mode 100644 index 0000000000..870226ad74 --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_output/pheno_cohort_primary_entity_but_no_primary_key_filter.sql @@ -0,0 +1 @@ +SELECT `patient_1`.`patient_id` AS `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`name` IN ('Sally', 'Diane', 'Cassy', 'John', 'Rosaleen') AND `patient_1`.`patient_id` IN ('patient_1', 'patient_2', 'patient_3') \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_output/pheno_cohort_primary_key_in_logic_and_as_input.sql b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_output/pheno_cohort_primary_key_in_logic_and_as_input.sql new file mode 100644 index 0000000000..0b0daa9ab6 --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_output/pheno_cohort_primary_key_in_logic_and_as_input.sql @@ -0,0 +1 @@ +SELECT `patient_1`.`patient_id` AS `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`patient_id` IN ('patient_1', 'patient_2', 'patient_3') \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_output/pheno_cohort_primary_key_in_logic_and_as_input_2.sql b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_output/pheno_cohort_primary_key_in_logic_and_as_input_2.sql new file mode 100644 index 0000000000..0421a82e56 --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_output/pheno_cohort_primary_key_in_logic_and_as_input_2.sql @@ -0,0 +1 @@ +SELECT `patient_1`.`patient_id` AS `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`patient_id` IN ('patient_3') \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_output/pheno_cohort_primary_key_not_in_logic_and_as_input.sql b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_output/pheno_cohort_primary_key_not_in_logic_and_as_input.sql new file mode 100644 index 0000000000..0b0daa9ab6 --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_output/pheno_cohort_primary_key_not_in_logic_and_as_input.sql @@ -0,0 +1 @@ +SELECT `patient_1`.`patient_id` AS `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`patient_id` IN ('patient_1', 'patient_2', 'patient_3') \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_output/pheno_cohort_primary_key_not_in_logic_and_as_input_2.sql b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_output/pheno_cohort_primary_key_not_in_logic_and_as_input_2.sql new file mode 100644 index 0000000000..d7c3d2838e --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_output/pheno_cohort_primary_key_not_in_logic_and_as_input_2.sql @@ -0,0 +1 @@ +SELECT `patient_1`.`patient_id` AS `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`patient_id` IN ('patient_3', 'patient_4') \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_output/pheno_cohort_with_no_primary_entity_filter.sql b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_output/pheno_cohort_with_no_primary_entity_filter.sql new file mode 100644 index 0000000000..8d4ff34aa3 --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_output/pheno_cohort_with_no_primary_entity_filter.sql @@ -0,0 +1 @@ +SELECT `cohort_subquery`.`patient_id` AS `patient_id` FROM (SELECT `patient_1`.`patient_id` AS `patient_id`, `patient_1`.`hid` AS `hid` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`patient_id` IN ('patient_1', 'patient_2', 'patient_3') AND (EXISTS (SELECT `hospital_1`.`hospital_id` AS `hospital_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`hospital` AS `hospital_1` WHERE `hospital_1`.`min_score_all` IN (500, 400) AND `hospital_1`.`hospital_id` = `patient_1`.`hid`))) AS `cohort_subquery` \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_output/pheno_geno_merged_dataset_as_input.sql b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_output/pheno_geno_merged_dataset_as_input.sql new file mode 100644 index 0000000000..0b0daa9ab6 --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_output/pheno_geno_merged_dataset_as_input.sql @@ -0,0 +1 @@ +SELECT `patient_1`.`patient_id` AS `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`patient_id` IN ('patient_1', 'patient_2', 'patient_3') \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_output/unfiltered_cohort_as_input.sql b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_output/unfiltered_cohort_as_input.sql new file mode 100644 index 0000000000..0b0daa9ab6 --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/raw-cohort-query_output/unfiltered_cohort_as_input.sql @@ -0,0 +1 @@ +SELECT `patient_1`.`patient_id` AS `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`patient_id` IN ('patient_1', 'patient_2', 'patient_3') \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/visualize_response/combined_cohort_no_filter_as_input.json b/src/python/test/create_cohort_test_files/payloads/visualize_response/combined_cohort_no_filter_as_input.json new file mode 100644 index 0000000000..6fb87ca030 --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/visualize_response/combined_cohort_no_filter_as_input.json @@ -0,0 +1,127 @@ +{ + "dataset": "record-GYK2zyQ0g1bx86fBp2X8KpjY", + "schema": "create_cohort_schema", + "version": "3.0", + "datasetVersion": "3.0", + "url": "https://vizserver.us-east-1-stg.apollo.dnanexus.com", + "datasetLoaded": false, + "datasetUpdated": true, + "name": "combined_cohort_without_filters", + "description": "", + "recordName": "combined_cohort_without_filters", + "recordState": "closed", + "recordTypes": [ + "DatabaseQuery", + "CohortBrowser", + "CombinedDatabaseQuery" + ], + "recordProject": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "datasetName": "create_cohort_pheno_dataset", + "datasetDescription": "Dataset: create_cohort_pheno_dataset", + "datasetRecordName": "create_cohort_pheno_dataset", + "datasetRecordState": "closed", + "datasetRecordProject": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "databases": [ + "database-GYK2yg00vGPpzj7YGY3VJxb9" + ], + "dashboardViews": [], + "dashboardConfig": { + "cohort_browser": { + "config": { + "showHeader": true, + "showFooter": true, + "showFilter": true + }, + "id": "cohort_browser", + "type": "cohort_browser", + "containers": [ + { + "id": "dashboard_tiles", + "title": "Overview", + "tiles": [], + "type": "FieldsAsTiles", + "options": {} + }, + { + "id": "ted_container", + "title": "Data Preview", + "tiles": [ + { + "id": "ted@patient", + "title": "Patients", + "type": "Table", + "dataQuery": { + "fields": { + "patient$patient_id": "patient$patient_id" + }, + "entity": "patient" + }, + "options": { + "columns": [ + { + "id": "patient$patient_id", + "title": "Patient ID", + "isPinned": true, + "isPrimary": true + } + ] + }, + "layout": { + "height": 1, + "width": 1 + } + } + ], + "type": "TedContainer", + "options": {} + } + ] + } + }, + "filters": { + "pheno_filters": { + "compound": [ + { + "name": "phenotype", + "logic": "and", + "filters": {}, + "entity": { + "logic": "and", + "name": "patient", + "operator": "exists", + "children": [] + } + } + ], + "logic": "and" + }, + "assay_filters": { + "compound": [], + "logic": "and" + }, + "logic": "and" + }, + "sql": "SELECT `patient_1`.`patient_id` AS `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`patient_id` IN (SELECT `patient_id` FROM (SELECT `cohort_subquery`.`patient_id` AS `patient_id` FROM (SELECT `patient_1`.`patient_id` AS `patient_id`, `patient_1`.`hid` AS `hid` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE EXISTS (SELECT `hospital_1`.`hospital_id` AS `hospital_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`hospital` AS `hospital_1` WHERE `hospital_1`.`min_score_all` IN (500, 400) AND `hospital_1`.`hospital_id` = `patient_1`.`hid`)) AS `cohort_subquery` INTERSECT SELECT `patient_1`.`patient_id` AS `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`name` IN ('Sally', 'Diane', 'Cassy', 'John', 'Rosaleen')))", + "baseSql": "SELECT `patient_id` FROM (SELECT `cohort_subquery`.`patient_id` AS `patient_id` FROM (SELECT `patient_1`.`patient_id` AS `patient_id`, `patient_1`.`hid` AS `hid` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE EXISTS (SELECT `hospital_1`.`hospital_id` AS `hospital_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`hospital` AS `hospital_1` WHERE `hospital_1`.`min_score_all` IN (500, 400) AND `hospital_1`.`hospital_id` = `patient_1`.`hid`)) AS `cohort_subquery` INTERSECT SELECT `patient_1`.`patient_id` AS `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`name` IN ('Sally', 'Diane', 'Cassy', 'John', 'Rosaleen'))", + "combined": { + "logic": "INTERSECT", + "source": [ + { + "id": "record-GYXGv700vGPpz41VkQXZpX47", + "project": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "recordName": "pheno_cohort_with_no_main_entity_filter", + "name": "pheno_cohort_with_no_main_entity_filter" + }, + { + "id": "record-GYXGPGQ0vGPZbJF6yZ17kxpx", + "project": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "recordName": "pheno_cohort_with_filter_on_non_primary_key", + "name": "pheno_cohort_with_filter_on_non_primary_key" + } + ] + }, + "downloadRestricted": false, + "containsPHI": false, + "restrictedProjects": [], + "clipboardRestricted": false +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/visualize_response/combined_cohort_non_primary_key_filter_input.json b/src/python/test/create_cohort_test_files/payloads/visualize_response/combined_cohort_non_primary_key_filter_input.json new file mode 100644 index 0000000000..eca9bc3334 --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/visualize_response/combined_cohort_non_primary_key_filter_input.json @@ -0,0 +1,168 @@ +{ + "dataset": "record-GYK2zyQ0g1bx86fBp2X8KpjY", + "schema": "create_cohort_schema", + "version": "3.0", + "datasetVersion": "3.0", + "url": "https://vizserver.us-east-1-stg.apollo.dnanexus.com", + "datasetLoaded": false, + "datasetUpdated": true, + "name": "combined_cohort_with_pheno_filters_non_primary_field", + "description": "", + "recordName": "combined_cohort_with_pheno_filters_non_primary_field", + "recordState": "closed", + "recordTypes": [ + "DatabaseQuery", + "CohortBrowser", + "CombinedDatabaseQuery" + ], + "recordProject": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "datasetName": "create_cohort_pheno_dataset", + "datasetDescription": "Dataset: create_cohort_pheno_dataset", + "datasetRecordName": "create_cohort_pheno_dataset", + "datasetRecordState": "closed", + "datasetRecordProject": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "databases": [ + "database-GYK2yg00vGPpzj7YGY3VJxb9" + ], + "dashboardViews": [], + "dashboardConfig": { + "cohort_browser": { + "config": { + "showHeader": true, + "showFooter": true, + "showFilter": true + }, + "id": "cohort_browser", + "type": "cohort_browser", + "containers": [ + { + "id": "dashboard_tiles", + "title": "Overview", + "tiles": [], + "type": "FieldsAsTiles", + "options": {} + }, + { + "id": "ted_container", + "title": "Data Preview", + "tiles": [ + { + "id": "ted@patient", + "title": "Patients", + "type": "Table", + "dataQuery": { + "fields": { + "patient$patient_id": "patient$patient_id" + }, + "entity": "patient" + }, + "options": { + "columns": [ + { + "id": "patient$patient_id", + "title": "Patient ID", + "isPinned": true, + "isPrimary": true + } + ] + }, + "layout": { + "height": 1, + "width": 1 + } + } + ], + "type": "TedContainer", + "options": {} + } + ] + } + }, + "filters": { + "pheno_filters": { + "compound": [ + { + "name": "phenotype", + "logic": "and", + "filters": { + "patient$age": [ + { + "condition": "between", + "values": [ + 20, + 60 + ] + } + ] + }, + "entity": { + "logic": "and", + "name": "patient", + "operator": "exists", + "children": [ + { + "name": "hospital@e98fe382-3cb5-4c25-a771-09a24ae0f91e", + "logic": "or", + "filters": { + "hospital$min_score_all": [ + { + "condition": "in", + "values": [ + 400, + 500 + ] + } + ], + "hospital$location": [ + { + "condition": "any", + "values": [ + "CA", + "BC" + ] + } + ] + }, + "entity": { + "logic": "and", + "name": "hospital", + "operator": "exists", + "children": [] + } + } + ] + } + } + ], + "logic": "and" + }, + "assay_filters": { + "compound": [], + "logic": "and" + }, + "logic": "and" + }, + "sql": "SELECT `cohort_subquery`.`patient_id` AS `patient_id` FROM (SELECT `patient_1`.`patient_id` AS `patient_id`, `patient_1`.`hid` AS `hid` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`age` BETWEEN 20 AND 60 AND (EXISTS (SELECT `hospital_1`.`hospital_id` AS `hospital_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`hospital` AS `hospital_1` WHERE (`hospital_1`.`min_score_all` IN (400, 500) OR ARRAY_CONTAINS(`hospital_1`.`location_hierarchy`, 'CA') OR ARRAY_CONTAINS(`hospital_1`.`location_hierarchy`, 'BC')) AND `hospital_1`.`hospital_id` = `patient_1`.`hid`)) AND `patient_1`.`patient_id` IN (SELECT `patient_id` FROM (SELECT `cohort_subquery`.`patient_id` AS `patient_id` FROM (SELECT `patient_1`.`patient_id` AS `patient_id`, `patient_1`.`hid` AS `hid` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE EXISTS (SELECT `hospital_1`.`hospital_id` AS `hospital_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`hospital` AS `hospital_1` WHERE `hospital_1`.`min_score_all` IN (500, 400) AND `hospital_1`.`hospital_id` = `patient_1`.`hid`)) AS `cohort_subquery` INTERSECT SELECT `patient_1`.`patient_id` AS `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`name` IN ('Sally', 'Diane', 'Cassy', 'John', 'Rosaleen')))) AS `cohort_subquery`", + "baseSql": "SELECT `patient_id` FROM (SELECT `cohort_subquery`.`patient_id` AS `patient_id` FROM (SELECT `patient_1`.`patient_id` AS `patient_id`, `patient_1`.`hid` AS `hid` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE EXISTS (SELECT `hospital_1`.`hospital_id` AS `hospital_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`hospital` AS `hospital_1` WHERE `hospital_1`.`min_score_all` IN (500, 400) AND `hospital_1`.`hospital_id` = `patient_1`.`hid`)) AS `cohort_subquery` INTERSECT SELECT `patient_1`.`patient_id` AS `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`name` IN ('Sally', 'Diane', 'Cassy', 'John', 'Rosaleen'))", + "combined": { + "logic": "INTERSECT", + "source": [ + { + "id": "record-GYXGv700vGPpz41VkQXZpX47", + "project": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "recordName": "pheno_cohort_with_no_main_entity_filter", + "name": "pheno_cohort_with_no_main_entity_filter" + }, + { + "id": "record-GYXGPGQ0vGPZbJF6yZ17kxpx", + "project": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "recordName": "pheno_cohort_with_filter_on_non_primary_key", + "name": "pheno_cohort_with_filter_on_non_primary_key" + } + ] + }, + "downloadRestricted": false, + "containsPHI": false, + "restrictedProjects": [], + "clipboardRestricted": false +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/visualize_response/combined_cohort_primary_field_filter_as_input.json b/src/python/test/create_cohort_test_files/payloads/visualize_response/combined_cohort_primary_field_filter_as_input.json new file mode 100644 index 0000000000..a9597fd572 --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/visualize_response/combined_cohort_primary_field_filter_as_input.json @@ -0,0 +1,139 @@ +{ + "dataset": "record-GYK2zyQ0g1bx86fBp2X8KpjY", + "schema": "create_cohort_schema", + "version": "3.0", + "datasetVersion": "3.0", + "url": "https://vizserver.us-east-1-stg.apollo.dnanexus.com", + "datasetLoaded": false, + "datasetUpdated": true, + "name": "combined_cohort_with_pheno_filters", + "description": "", + "recordName": "combined_cohort_with_pheno_filters", + "recordState": "closed", + "recordTypes": [ + "DatabaseQuery", + "CohortBrowser", + "CombinedDatabaseQuery" + ], + "recordProject": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "datasetName": "create_cohort_pheno_dataset", + "datasetDescription": "Dataset: create_cohort_pheno_dataset", + "datasetRecordName": "create_cohort_pheno_dataset", + "datasetRecordState": "closed", + "datasetRecordProject": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "databases": [ + "database-GYK2yg00vGPpzj7YGY3VJxb9" + ], + "dashboardViews": [], + "dashboardConfig": { + "cohort_browser": { + "config": { + "showHeader": true, + "showFooter": true, + "showFilter": true + }, + "id": "cohort_browser", + "type": "cohort_browser", + "containers": [ + { + "id": "dashboard_tiles", + "title": "Overview", + "tiles": [], + "type": "FieldsAsTiles", + "options": {} + }, + { + "id": "ted_container", + "title": "Data Preview", + "tiles": [ + { + "id": "ted@patient", + "title": "Patients", + "type": "Table", + "dataQuery": { + "fields": { + "patient$patient_id": "patient$patient_id" + }, + "entity": "patient" + }, + "options": { + "columns": [ + { + "id": "patient$patient_id", + "title": "Patient ID", + "isPinned": true, + "isPrimary": true + } + ] + }, + "layout": { + "height": 1, + "width": 1 + } + } + ], + "type": "TedContainer", + "options": {} + } + ] + } + }, + "filters": { + "pheno_filters": { + "compound": [ + { + "name": "phenotype", + "logic": "and", + "filters": { + "patient$patient_id": [ + { + "condition": "in", + "values": [ + "patient_1", + "patient_2", + "patient_3", + "patient_8" + ] + } + ] + }, + "entity": { + "logic": "and", + "name": "patient", + "operator": "exists", + "children": [] + } + } + ], + "logic": "and" + }, + "assay_filters": { + "compound": [], + "logic": "and" + }, + "logic": "and" + }, + "sql": "SELECT `patient_1`.`patient_id` AS `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`patient_id` IN ('patient_1', 'patient_2', 'patient_3', 'patient_8') AND `patient_1`.`patient_id` IN (SELECT `patient_id` FROM (SELECT `cohort_subquery`.`patient_id` AS `patient_id` FROM (SELECT `patient_1`.`patient_id` AS `patient_id`, `patient_1`.`hid` AS `hid` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE EXISTS (SELECT `hospital_1`.`hospital_id` AS `hospital_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`hospital` AS `hospital_1` WHERE `hospital_1`.`min_score_all` IN (500, 400) AND `hospital_1`.`hospital_id` = `patient_1`.`hid`)) AS `cohort_subquery` INTERSECT SELECT `patient_1`.`patient_id` AS `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`name` IN ('Sally', 'Diane', 'Cassy', 'John', 'Rosaleen')))", + "baseSql": "SELECT `patient_id` FROM (SELECT `cohort_subquery`.`patient_id` AS `patient_id` FROM (SELECT `patient_1`.`patient_id` AS `patient_id`, `patient_1`.`hid` AS `hid` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE EXISTS (SELECT `hospital_1`.`hospital_id` AS `hospital_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`hospital` AS `hospital_1` WHERE `hospital_1`.`min_score_all` IN (500, 400) AND `hospital_1`.`hospital_id` = `patient_1`.`hid`)) AS `cohort_subquery` INTERSECT SELECT `patient_1`.`patient_id` AS `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`name` IN ('Sally', 'Diane', 'Cassy', 'John', 'Rosaleen'))", + "combined": { + "logic": "INTERSECT", + "source": [ + { + "id": "record-GYXGv700vGPpz41VkQXZpX47", + "project": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "recordName": "pheno_cohort_with_no_main_entity_filter", + "name": "pheno_cohort_with_no_main_entity_filter" + }, + { + "id": "record-GYXGPGQ0vGPZbJF6yZ17kxpx", + "project": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "recordName": "pheno_cohort_with_filter_on_non_primary_key", + "name": "pheno_cohort_with_filter_on_non_primary_key" + } + ] + }, + "downloadRestricted": false, + "containsPHI": false, + "restrictedProjects": [], + "clipboardRestricted": false +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/visualize_response/complex_cohort_as_input.json b/src/python/test/create_cohort_test_files/payloads/visualize_response/complex_cohort_as_input.json new file mode 100644 index 0000000000..265c4aca7a --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/visualize_response/complex_cohort_as_input.json @@ -0,0 +1,32 @@ +{ + "dataset": "record-GYK2zyQ0g1bx86fBp2X8KpjY", + "schema": "create_cohort_schema", + "version": "3.0", + "datasetVersion": "3.0", + "url": "https://vizserver.us-east-1-stg.apollo.dnanexus.com", + "datasetLoaded": false, + "datasetUpdated": true, + "description": "", + "recordName": "complex_cohort", + "recordState": "closed", + "recordTypes": [ + "DatabaseQuery", + "CohortBrowser" + ], + "recordProject": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "datasetName": "create_cohort_pheno_dataset", + "datasetDescription": "Dataset: create_cohort_pheno_dataset", + "datasetRecordName": "create_cohort_pheno_dataset", + "datasetRecordState": "closed", + "datasetRecordProject": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "databases": [ + "database-GYK2yg00vGPpzj7YGY3VJxb9" + ], + "dashboardViews": [], + "sql": "SELECT `patient_1`.`patient_id` AS `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`patient_id` IN (SELECT `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` WHERE `patient_id` in ('patient_1', 'patient_2', 'patient_3'))", + "baseSql": "SELECT `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` WHERE `patient_id` in ('patient_1', 'patient_2', 'patient_3')", + "downloadRestricted": false, + "containsPHI": false, + "restrictedProjects": [], + "clipboardRestricted": false +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/visualize_response/dataset_as_input.json b/src/python/test/create_cohort_test_files/payloads/visualize_response/dataset_as_input.json new file mode 100644 index 0000000000..7b9b09c5e2 --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/visualize_response/dataset_as_input.json @@ -0,0 +1,30 @@ +{ + "dataset": "record-GYK2zyQ0g1bx86fBp2X8KpjY", + "schema": "create_cohort_schema", + "version": "3.0", + "datasetVersion": "3.0", + "url": "https://vizserver.us-east-1-stg.apollo.dnanexus.com", + "datasetLoaded": false, + "datasetUpdated": true, + "name": "create_cohort_pheno_dataset", + "description": "Dataset: create_cohort_pheno_dataset", + "recordName": "create_cohort_pheno_dataset", + "recordState": "closed", + "recordTypes": [ + "Dataset" + ], + "recordProject": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "datasetName": "create_cohort_pheno_dataset", + "datasetDescription": "Dataset: create_cohort_pheno_dataset", + "datasetRecordName": "create_cohort_pheno_dataset", + "datasetRecordState": "closed", + "datasetRecordProject": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "databases": [ + "database-GYK2yg00vGPpzj7YGY3VJxb9" + ], + "dashboardViews": [], + "downloadRestricted": false, + "containsPHI": false, + "restrictedProjects": [], + "clipboardRestricted": false +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/visualize_response/geno_cohort_with_geno_filters_filtered_with_primary_field.json b/src/python/test/create_cohort_test_files/payloads/visualize_response/geno_cohort_with_geno_filters_filtered_with_primary_field.json new file mode 100644 index 0000000000..28f89099d1 --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/visualize_response/geno_cohort_with_geno_filters_filtered_with_primary_field.json @@ -0,0 +1,268 @@ +{ + "dataset": "record-GYK40bj06Yy4KPpB4zqX8kGb", + "schema": "create_cohort_schema", + "version": "3.0", + "datasetVersion": "3.0", + "url": "https://vizserver.us-east-1-stg.apollo.dnanexus.com", + "datasetLoaded": false, + "datasetUpdated": true, + "name": "geno_cohort_with_geno_filters", + "description": "", + "recordName": "geno_cohort_with_geno_filters", + "recordState": "closed", + "recordTypes": [ + "DatabaseQuery", + "CohortBrowser" + ], + "recordProject": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "datasetName": "create_cohort_geno_dataset", + "datasetDescription": "Dataset: create_cohort_geno_dataset", + "datasetRecordName": "create_cohort_geno_dataset", + "datasetRecordState": "closed", + "datasetRecordProject": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "databases": [ + "database-GYK3JJj0vGPQ2Y1y98pj8pGg", + "database-FkypXj80bgYvQqyqBx1FJG6y" + ], + "dashboardViews": [], + "dashboardConfig": { + "cohort_browser": { + "config": { + "showHeader": true, + "showFooter": true, + "showFilter": true + }, + "id": "cohort_browser", + "type": "cohort_browser", + "containers": [ + { + "id": "dashboard_tiles", + "title": "Overview", + "tiles": [], + "type": "FieldsAsTiles", + "options": {} + }, + { + "id": "ted_container", + "title": "Data Preview", + "tiles": [ + { + "id": "ted@sample", + "title": "sample", + "type": "Table", + "dataQuery": { + "fields": { + "sample$sample_id": "sample$sample_id" + }, + "entity": "sample" + }, + "options": { + "columns": [ + { + "id": "sample$sample_id", + "title": "Sample ID", + "isPinned": true, + "isPrimary": true + } + ] + }, + "layout": { + "height": 1, + "width": 1 + } + } + ], + "type": "TedContainer", + "options": {} + }, + { + "id": "variant_browser_container", + "title": "Genomics", + "tiles": [ + { + "id": "variant_browser_plots_lollipop", + "title": "Variant Browser Lollipop Plot", + "type": "Lollipop", + "dataQuery": { + "computeAlleleFrequency": "genotype$type", + "fields": { + "allele_frequency": "allele$gnomad201_alt_freq", + "allele_type": "allele$allele_type", + "alt": "allele$alt", + "alt_count": "allele$alt_count", + "chr": "allele$chr", + "consequence": "allele$worst_effect", + "id": "allele$a_id", + "locus": "allele$locus_id", + "pos": "allele$pos", + "ref": "allele$ref", + "rsid": "allele$dbsnp151_rsid", + "y": "allele$alt_freq" + }, + "isCohort": true, + "limit": 10000 + }, + "options": {}, + "layout": { + "height": 1, + "width": 1 + } + }, + { + "id": "variant_browser_plots_transcript", + "title": "Variant Browser Transcript Plot", + "type": "Transcript", + "dataQuery": { + "fields": { + "id": "transcript$transcript_name", + "gene": "transcript$gene_name", + "chr": "transcript$chr", + "pos": "transcript$pos", + "pos_end": "transcript$end_pos", + "exons": "transcript$exon", + "strand": "transcript$strand" + }, + "isCohort": false, + "limit": 10000 + }, + "options": {}, + "layout": { + "height": 1, + "width": 1 + } + }, + { + "id": "variant_browser_table", + "dataID": "variant_browser_plots_lollipop", + "title": "Variant Browser Table", + "type": "Table", + "dataQuery": { + "fields": {} + }, + "options": { + "columns": [ + { + "id": "id", + "title": "Location", + "type": "genotype_location", + "isStatic": true + }, + { + "id": "rsid", + "title": "RSID" + }, + { + "id": "ref", + "title": "Reference" + }, + { + "id": "alt", + "title": "Alternate" + }, + { + "id": "alleleType", + "title": "Type" + }, + { + "id": "consequence", + "title": "Consequence (Most severe by gene)", + "type": "consequence" + }, + { + "id": "cohortAlleleFrequency", + "title": "Cohort AF", + "type": "integer" + }, + { + "id": "y", + "title": "Population AF", + "type": "integer" + }, + { + "id": "alleleFrequency", + "title": "GnomAD AF", + "type": "integer" + } + ] + }, + "layout": { + "height": 1, + "width": 1 + } + } + ], + "type": "GenomeBrowser", + "options": { + "genomicRangeSearch": { + "chr": "1", + "end": 55065852, + "start": 55038548 + } + } + } + ] + }, + "locus_detail": { + "config": null, + "containers": [], + "type": "locus_detail", + "id": "locus_detail" + } + }, + "filters": { + "pheno_filters": { + "compound": [ + { + "name": "phenotype", + "logic": "and", + "filters": {}, + "entity": { + "logic": "and", + "name": "sample", + "operator": "exists", + "children": [] + } + } + ], + "logic": "and" + }, + "assay_filters": { + "compound": [ + { + "name": "genotype@b07df2d4-a05c-4c6a-9a20-7602695d9e47", + "logic": "and", + "filters": { + "annotation$gene_name": [ + { + "condition": "in", + "values": [], + "geno_bins": [ + { + "chr": "18", + "start": 47368, + "end": 47368 + } + ] + } + ], + "genotype$type": [ + { + "condition": "in", + "values": [ + "hom" + ] + } + ] + } + } + ], + "logic": "and" + }, + "logic": "and" + }, + "sql": "SELECT `sample_1`.`sample_id` AS `sample_id` FROM `database_gyk3jjj0vgpq2y1y98pj8pgg__create_cohort_geno_database`.`sample` AS `sample_1` WHERE `sample_1`.`sample_id` IN (SELECT `assay_cohort_query`.`sample_id` FROM (SELECT `genotype_alt_read_optimized_1`.`sample_id` AS `sample_id` FROM `database_gyk3jjj0vgpq2y1y98pj8pgg__create_cohort_geno_database`.`genotype_alt_read_optimized` AS `genotype_alt_read_optimized_1` WHERE `genotype_alt_read_optimized_1`.`ref_yn` = false AND `genotype_alt_read_optimized_1`.`chr` = '18' AND `genotype_alt_read_optimized_1`.`pos` BETWEEN 46368 AND 48368 AND `genotype_alt_read_optimized_1`.`bin` IN (0) AND `genotype_alt_read_optimized_1`.`type` IN ('hom')) AS `assay_cohort_query`)", + "downloadRestricted": false, + "containsPHI": false, + "restrictedProjects": [], + "clipboardRestricted": false +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/visualize_response/geno_dataset_filtered_with_primary_field.json b/src/python/test/create_cohort_test_files/payloads/visualize_response/geno_dataset_filtered_with_primary_field.json new file mode 100644 index 0000000000..43b52c8104 --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/visualize_response/geno_dataset_filtered_with_primary_field.json @@ -0,0 +1,31 @@ +{ + "dataset": "record-GYK40bj06Yy4KPpB4zqX8kGb", + "schema": "create_cohort_schema", + "version": "3.0", + "datasetVersion": "3.0", + "url": "https://vizserver.us-east-1-stg.apollo.dnanexus.com", + "datasetLoaded": false, + "datasetUpdated": true, + "name": "create_cohort_geno_dataset", + "description": "Dataset: create_cohort_geno_dataset", + "recordName": "create_cohort_geno_dataset", + "recordState": "closed", + "recordTypes": [ + "Dataset" + ], + "recordProject": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "datasetName": "create_cohort_geno_dataset", + "datasetDescription": "Dataset: create_cohort_geno_dataset", + "datasetRecordName": "create_cohort_geno_dataset", + "datasetRecordState": "closed", + "datasetRecordProject": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "databases": [ + "database-GYK3JJj0vGPQ2Y1y98pj8pGg", + "database-FkypXj80bgYvQqyqBx1FJG6y" + ], + "dashboardViews": [], + "downloadRestricted": false, + "containsPHI": false, + "restrictedProjects": [], + "clipboardRestricted": false +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/visualize_response/not_null_as_input.json b/src/python/test/create_cohort_test_files/payloads/visualize_response/not_null_as_input.json new file mode 100644 index 0000000000..cd5fb0c1bc --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/visualize_response/not_null_as_input.json @@ -0,0 +1,114 @@ +{ + "dataset": "record-GYK2zyQ0g1bx86fBp2X8KpjY", + "schema": "create_cohort_schema", + "version": "3.0", + "datasetVersion": "3.0", + "url": "https://vizserver.us-east-1-stg.apollo.dnanexus.com", + "datasetLoaded": false, + "datasetUpdated": true, + "name": "not_null_primary_field", + "description": "", + "recordName": "not_null_primary_field", + "recordState": "closed", + "recordTypes": [ + "DatabaseQuery", + "CohortBrowser" + ], + "recordProject": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "datasetName": "create_cohort_pheno_dataset", + "datasetDescription": "Dataset: create_cohort_pheno_dataset", + "datasetRecordName": "create_cohort_pheno_dataset", + "datasetRecordState": "closed", + "datasetRecordProject": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "databases": [ + "database-GYK2yg00vGPpzj7YGY3VJxb9" + ], + "dashboardViews": [], + "dashboardConfig": { + "cohort_browser": { + "config": { + "showHeader": true, + "showFooter": true, + "showFilter": true + }, + "id": "cohort_browser", + "type": "cohort_browser", + "containers": [ + { + "id": "dashboard_tiles", + "title": "Overview", + "tiles": [], + "type": "FieldsAsTiles", + "options": {} + }, + { + "id": "ted_container", + "title": "Data Preview", + "tiles": [ + { + "id": "ted@patient", + "title": "Patients", + "type": "Table", + "dataQuery": { + "fields": { + "patient$patient_id": "patient$patient_id" + }, + "entity": "patient" + }, + "options": { + "columns": [ + { + "id": "patient$patient_id", + "title": "Patient ID", + "isPinned": true, + "isPrimary": true + } + ] + }, + "layout": { + "height": 1, + "width": 1 + } + } + ], + "type": "TedContainer", + "options": {} + } + ] + } + }, + "filters": { + "pheno_filters": { + "compound": [ + { + "name": "phenotype", + "logic": "and", + "filters": { + "patient$patient_id": [ + { + "condition": "exists" + } + ] + }, + "entity": { + "logic": "and", + "name": "patient", + "operator": "exists", + "children": [] + } + } + ], + "logic": "and" + }, + "assay_filters": { + "compound": [], + "logic": "and" + }, + "logic": "and" + }, + "sql": "SELECT `patient_1`.`patient_id` AS `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`patient_id` IS NOT NULL", + "downloadRestricted": false, + "containsPHI": false, + "restrictedProjects": [], + "clipboardRestricted": false +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/visualize_response/pheno_cohort_primary_entity_but_no_primary_key_filter.json b/src/python/test/create_cohort_test_files/payloads/visualize_response/pheno_cohort_primary_entity_but_no_primary_key_filter.json new file mode 100644 index 0000000000..03a391330f --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/visualize_response/pheno_cohort_primary_entity_but_no_primary_key_filter.json @@ -0,0 +1,121 @@ +{ + "dataset": "record-GYK2zyQ0g1bx86fBp2X8KpjY", + "schema": "create_cohort_schema", + "version": "3.0", + "datasetVersion": "3.0", + "url": "https://vizserver.us-east-1-stg.apollo.dnanexus.com", + "datasetLoaded": false, + "datasetUpdated": true, + "name": "pheno_cohort_with_filter_on_non_primary_key", + "description": "", + "recordName": "pheno_cohort_with_filter_on_non_primary_key", + "recordState": "closed", + "recordTypes": [ + "DatabaseQuery", + "CohortBrowser" + ], + "recordProject": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "datasetName": "create_cohort_pheno_dataset", + "datasetDescription": "Dataset: create_cohort_pheno_dataset", + "datasetRecordName": "create_cohort_pheno_dataset", + "datasetRecordState": "closed", + "datasetRecordProject": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "databases": [ + "database-GYK2yg00vGPpzj7YGY3VJxb9" + ], + "dashboardViews": [], + "dashboardConfig": { + "cohort_browser": { + "config": { + "showHeader": true, + "showFooter": true, + "showFilter": true + }, + "id": "cohort_browser", + "type": "cohort_browser", + "containers": [ + { + "id": "dashboard_tiles", + "title": "Overview", + "tiles": [], + "type": "FieldsAsTiles", + "options": {} + }, + { + "id": "ted_container", + "title": "Data Preview", + "tiles": [ + { + "id": "ted@patient", + "title": "Patients", + "type": "Table", + "dataQuery": { + "fields": { + "patient$patient_id": "patient$patient_id" + }, + "entity": "patient" + }, + "options": { + "columns": [ + { + "id": "patient$patient_id", + "title": "Patient ID", + "isPinned": true, + "isPrimary": true + } + ] + }, + "layout": { + "height": 1, + "width": 1 + } + } + ], + "type": "TedContainer", + "options": {} + } + ] + } + }, + "filters": { + "pheno_filters": { + "compound": [ + { + "name": "phenotype", + "logic": "and", + "filters": { + "patient$name": [ + { + "condition": "in", + "values": [ + "Sally", + "Diane", + "Cassy", + "John", + "Rosaleen" + ] + } + ] + }, + "entity": { + "logic": "and", + "name": "patient", + "operator": "exists", + "children": [] + } + } + ], + "logic": "and" + }, + "assay_filters": { + "compound": [], + "logic": "and" + }, + "logic": "and" + }, + "sql": "SELECT `patient_1`.`patient_id` AS `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`name` IN ('Sally', 'Diane', 'Cassy', 'John', 'Rosaleen')", + "downloadRestricted": false, + "containsPHI": false, + "restrictedProjects": [], + "clipboardRestricted": false +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/visualize_response/pheno_cohort_primary_key_in_logic_and_as_input.json b/src/python/test/create_cohort_test_files/payloads/visualize_response/pheno_cohort_primary_key_in_logic_and_as_input.json new file mode 100644 index 0000000000..a549c7200a --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/visualize_response/pheno_cohort_primary_key_in_logic_and_as_input.json @@ -0,0 +1,121 @@ +{ + "dataset": "record-GYK2zyQ0g1bx86fBp2X8KpjY", + "schema": "create_cohort_schema", + "version": "3.0", + "datasetVersion": "3.0", + "url": "https://vizserver.us-east-1-stg.apollo.dnanexus.com", + "datasetLoaded": false, + "datasetUpdated": true, + "name": "pheno_cohort_with_in_condition_on_primary_key", + "description": "", + "recordName": "pheno_cohort_with_in_condition_on_primary_key", + "recordState": "closed", + "recordTypes": [ + "DatabaseQuery", + "CohortBrowser" + ], + "recordProject": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "datasetName": "create_cohort_pheno_dataset", + "datasetDescription": "Dataset: create_cohort_pheno_dataset", + "datasetRecordName": "create_cohort_pheno_dataset", + "datasetRecordState": "closed", + "datasetRecordProject": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "databases": [ + "database-GYK2yg00vGPpzj7YGY3VJxb9" + ], + "dashboardViews": [], + "dashboardConfig": { + "cohort_browser": { + "config": { + "showHeader": true, + "showFooter": true, + "showFilter": true + }, + "id": "cohort_browser", + "type": "cohort_browser", + "containers": [ + { + "id": "dashboard_tiles", + "title": "Overview", + "tiles": [], + "type": "FieldsAsTiles", + "options": {} + }, + { + "id": "ted_container", + "title": "Data Preview", + "tiles": [ + { + "id": "ted@patient", + "title": "Patients", + "type": "Table", + "dataQuery": { + "fields": { + "patient$patient_id": "patient$patient_id" + }, + "entity": "patient" + }, + "options": { + "columns": [ + { + "id": "patient$patient_id", + "title": "Patient ID", + "isPinned": true, + "isPrimary": true + } + ] + }, + "layout": { + "height": 1, + "width": 1 + } + } + ], + "type": "TedContainer", + "options": {} + } + ] + } + }, + "filters": { + "pheno_filters": { + "compound": [ + { + "name": "phenotype", + "logic": "and", + "filters": { + "patient$patient_id": [ + { + "condition": "in", + "values": [ + "patient_1", + "patient_2", + "patient_3", + "patient_8", + "patient_9" + ] + } + ] + }, + "entity": { + "logic": "and", + "name": "patient", + "operator": "exists", + "children": [] + } + } + ], + "logic": "and" + }, + "assay_filters": { + "compound": [], + "logic": "and" + }, + "logic": "and" + }, + "sql": "SELECT `patient_1`.`patient_id` AS `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`patient_id` IN ('patient_1', 'patient_2', 'patient_3', 'patient_8', 'patient_9')", + "downloadRestricted": false, + "containsPHI": false, + "restrictedProjects": [], + "clipboardRestricted": false +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/visualize_response/pheno_cohort_primary_key_in_logic_and_as_input_2.json b/src/python/test/create_cohort_test_files/payloads/visualize_response/pheno_cohort_primary_key_in_logic_and_as_input_2.json new file mode 100644 index 0000000000..a549c7200a --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/visualize_response/pheno_cohort_primary_key_in_logic_and_as_input_2.json @@ -0,0 +1,121 @@ +{ + "dataset": "record-GYK2zyQ0g1bx86fBp2X8KpjY", + "schema": "create_cohort_schema", + "version": "3.0", + "datasetVersion": "3.0", + "url": "https://vizserver.us-east-1-stg.apollo.dnanexus.com", + "datasetLoaded": false, + "datasetUpdated": true, + "name": "pheno_cohort_with_in_condition_on_primary_key", + "description": "", + "recordName": "pheno_cohort_with_in_condition_on_primary_key", + "recordState": "closed", + "recordTypes": [ + "DatabaseQuery", + "CohortBrowser" + ], + "recordProject": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "datasetName": "create_cohort_pheno_dataset", + "datasetDescription": "Dataset: create_cohort_pheno_dataset", + "datasetRecordName": "create_cohort_pheno_dataset", + "datasetRecordState": "closed", + "datasetRecordProject": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "databases": [ + "database-GYK2yg00vGPpzj7YGY3VJxb9" + ], + "dashboardViews": [], + "dashboardConfig": { + "cohort_browser": { + "config": { + "showHeader": true, + "showFooter": true, + "showFilter": true + }, + "id": "cohort_browser", + "type": "cohort_browser", + "containers": [ + { + "id": "dashboard_tiles", + "title": "Overview", + "tiles": [], + "type": "FieldsAsTiles", + "options": {} + }, + { + "id": "ted_container", + "title": "Data Preview", + "tiles": [ + { + "id": "ted@patient", + "title": "Patients", + "type": "Table", + "dataQuery": { + "fields": { + "patient$patient_id": "patient$patient_id" + }, + "entity": "patient" + }, + "options": { + "columns": [ + { + "id": "patient$patient_id", + "title": "Patient ID", + "isPinned": true, + "isPrimary": true + } + ] + }, + "layout": { + "height": 1, + "width": 1 + } + } + ], + "type": "TedContainer", + "options": {} + } + ] + } + }, + "filters": { + "pheno_filters": { + "compound": [ + { + "name": "phenotype", + "logic": "and", + "filters": { + "patient$patient_id": [ + { + "condition": "in", + "values": [ + "patient_1", + "patient_2", + "patient_3", + "patient_8", + "patient_9" + ] + } + ] + }, + "entity": { + "logic": "and", + "name": "patient", + "operator": "exists", + "children": [] + } + } + ], + "logic": "and" + }, + "assay_filters": { + "compound": [], + "logic": "and" + }, + "logic": "and" + }, + "sql": "SELECT `patient_1`.`patient_id` AS `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`patient_id` IN ('patient_1', 'patient_2', 'patient_3', 'patient_8', 'patient_9')", + "downloadRestricted": false, + "containsPHI": false, + "restrictedProjects": [], + "clipboardRestricted": false +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/visualize_response/pheno_cohort_primary_key_not_in_logic_and_as_input.json b/src/python/test/create_cohort_test_files/payloads/visualize_response/pheno_cohort_primary_key_not_in_logic_and_as_input.json new file mode 100644 index 0000000000..a1d8081f62 --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/visualize_response/pheno_cohort_primary_key_not_in_logic_and_as_input.json @@ -0,0 +1,118 @@ +{ + "dataset": "record-GYK2zyQ0g1bx86fBp2X8KpjY", + "schema": "create_cohort_schema", + "version": "3.0", + "datasetVersion": "3.0", + "url": "https://vizserver.us-east-1-stg.apollo.dnanexus.com", + "datasetLoaded": false, + "datasetUpdated": true, + "name": "pheno_cohort_primary_key_not_in_logic_and_as_input", + "description": "", + "recordName": "pheno_cohort_primary_key_not_in_logic_and_as_input", + "recordState": "closed", + "recordTypes": [ + "DatabaseQuery", + "CohortBrowser" + ], + "recordProject": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "datasetName": "create_cohort_pheno_dataset", + "datasetDescription": "Dataset: create_cohort_pheno_dataset", + "datasetRecordName": "create_cohort_pheno_dataset", + "datasetRecordState": "closed", + "datasetRecordProject": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "databases": [ + "database-GYK2yg00vGPpzj7YGY3VJxb9" + ], + "dashboardViews": [], + "dashboardConfig": { + "cohort_browser": { + "config": { + "showHeader": true, + "showFooter": true, + "showFilter": true + }, + "id": "cohort_browser", + "type": "cohort_browser", + "containers": [ + { + "id": "dashboard_tiles", + "title": "Overview", + "tiles": [], + "type": "FieldsAsTiles", + "options": {} + }, + { + "id": "ted_container", + "title": "Data Preview", + "tiles": [ + { + "id": "ted@patient", + "title": "Patients", + "type": "Table", + "dataQuery": { + "fields": { + "patient$patient_id": "patient$patient_id" + }, + "entity": "patient" + }, + "options": { + "columns": [ + { + "id": "patient$patient_id", + "title": "Patient ID", + "isPinned": true, + "isPrimary": true + } + ] + }, + "layout": { + "height": 1, + "width": 1 + } + } + ], + "type": "TedContainer", + "options": {} + } + ] + } + }, + "filters": { + "pheno_filters": { + "compound": [ + { + "name": "phenotype", + "logic": "and", + "filters": { + "patient$patient_id": [ + { + "condition": "not-in", + "values": [ + "patient_8", + "patient_9" + ] + } + ] + }, + "entity": { + "logic": "and", + "name": "patient", + "operator": "exists", + "children": [] + } + } + ], + "logic": "and" + }, + "assay_filters": { + "compound": [], + "logic": "and" + }, + "logic": "and" + }, + "sql": "SELECT `patient_1`.`patient_id` AS `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`patient_id` NOT IN ('patient_8', 'patient_9') OR `patient_1`.`patient_id` IS NULL", + "downloadRestricted": false, + "containsPHI": false, + "restrictedProjects": [], + "clipboardRestricted": false +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/visualize_response/pheno_cohort_primary_key_not_in_logic_and_as_input_2.json b/src/python/test/create_cohort_test_files/payloads/visualize_response/pheno_cohort_primary_key_not_in_logic_and_as_input_2.json new file mode 100644 index 0000000000..a1d8081f62 --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/visualize_response/pheno_cohort_primary_key_not_in_logic_and_as_input_2.json @@ -0,0 +1,118 @@ +{ + "dataset": "record-GYK2zyQ0g1bx86fBp2X8KpjY", + "schema": "create_cohort_schema", + "version": "3.0", + "datasetVersion": "3.0", + "url": "https://vizserver.us-east-1-stg.apollo.dnanexus.com", + "datasetLoaded": false, + "datasetUpdated": true, + "name": "pheno_cohort_primary_key_not_in_logic_and_as_input", + "description": "", + "recordName": "pheno_cohort_primary_key_not_in_logic_and_as_input", + "recordState": "closed", + "recordTypes": [ + "DatabaseQuery", + "CohortBrowser" + ], + "recordProject": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "datasetName": "create_cohort_pheno_dataset", + "datasetDescription": "Dataset: create_cohort_pheno_dataset", + "datasetRecordName": "create_cohort_pheno_dataset", + "datasetRecordState": "closed", + "datasetRecordProject": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "databases": [ + "database-GYK2yg00vGPpzj7YGY3VJxb9" + ], + "dashboardViews": [], + "dashboardConfig": { + "cohort_browser": { + "config": { + "showHeader": true, + "showFooter": true, + "showFilter": true + }, + "id": "cohort_browser", + "type": "cohort_browser", + "containers": [ + { + "id": "dashboard_tiles", + "title": "Overview", + "tiles": [], + "type": "FieldsAsTiles", + "options": {} + }, + { + "id": "ted_container", + "title": "Data Preview", + "tiles": [ + { + "id": "ted@patient", + "title": "Patients", + "type": "Table", + "dataQuery": { + "fields": { + "patient$patient_id": "patient$patient_id" + }, + "entity": "patient" + }, + "options": { + "columns": [ + { + "id": "patient$patient_id", + "title": "Patient ID", + "isPinned": true, + "isPrimary": true + } + ] + }, + "layout": { + "height": 1, + "width": 1 + } + } + ], + "type": "TedContainer", + "options": {} + } + ] + } + }, + "filters": { + "pheno_filters": { + "compound": [ + { + "name": "phenotype", + "logic": "and", + "filters": { + "patient$patient_id": [ + { + "condition": "not-in", + "values": [ + "patient_8", + "patient_9" + ] + } + ] + }, + "entity": { + "logic": "and", + "name": "patient", + "operator": "exists", + "children": [] + } + } + ], + "logic": "and" + }, + "assay_filters": { + "compound": [], + "logic": "and" + }, + "logic": "and" + }, + "sql": "SELECT `patient_1`.`patient_id` AS `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`patient_id` NOT IN ('patient_8', 'patient_9') OR `patient_1`.`patient_id` IS NULL", + "downloadRestricted": false, + "containsPHI": false, + "restrictedProjects": [], + "clipboardRestricted": false +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/visualize_response/pheno_cohort_with_no_primary_entity_filter.json b/src/python/test/create_cohort_test_files/payloads/visualize_response/pheno_cohort_with_no_primary_entity_filter.json new file mode 100644 index 0000000000..7f0cd83d0d --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/visualize_response/pheno_cohort_with_no_primary_entity_filter.json @@ -0,0 +1,130 @@ +{ + "dataset": "record-GYK2zyQ0g1bx86fBp2X8KpjY", + "schema": "create_cohort_schema", + "version": "3.0", + "datasetVersion": "3.0", + "url": "https://vizserver.us-east-1-stg.apollo.dnanexus.com", + "datasetLoaded": false, + "datasetUpdated": true, + "name": "pheno_cohort_with_no_main_entity_filter", + "description": "", + "recordName": "pheno_cohort_with_no_main_entity_filter", + "recordState": "closed", + "recordTypes": [ + "DatabaseQuery", + "CohortBrowser" + ], + "recordProject": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "datasetName": "create_cohort_pheno_dataset", + "datasetDescription": "Dataset: create_cohort_pheno_dataset", + "datasetRecordName": "create_cohort_pheno_dataset", + "datasetRecordState": "closed", + "datasetRecordProject": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "databases": [ + "database-GYK2yg00vGPpzj7YGY3VJxb9" + ], + "dashboardViews": [], + "dashboardConfig": { + "cohort_browser": { + "config": { + "showHeader": true, + "showFooter": true, + "showFilter": true + }, + "id": "cohort_browser", + "type": "cohort_browser", + "containers": [ + { + "id": "dashboard_tiles", + "title": "Overview", + "tiles": [], + "type": "FieldsAsTiles", + "options": {} + }, + { + "id": "ted_container", + "title": "Data Preview", + "tiles": [ + { + "id": "ted@patient", + "title": "Patients", + "type": "Table", + "dataQuery": { + "fields": { + "patient$patient_id": "patient$patient_id" + }, + "entity": "patient" + }, + "options": { + "columns": [ + { + "id": "patient$patient_id", + "title": "Patient ID", + "isPinned": true, + "isPrimary": true + } + ] + }, + "layout": { + "height": 1, + "width": 1 + } + } + ], + "type": "TedContainer", + "options": {} + } + ] + } + }, + "filters": { + "pheno_filters": { + "compound": [ + { + "name": "phenotype", + "logic": "and", + "filters": {}, + "entity": { + "logic": "and", + "name": "patient", + "operator": "exists", + "children": [ + { + "name": "hospital@18e6dfe5-3369-44ff-8305-4feca890af15", + "logic": "and", + "filters": { + "hospital$min_score_all": [ + { + "condition": "in", + "values": [ + 500, + 400 + ] + } + ] + }, + "entity": { + "logic": "and", + "name": "hospital", + "operator": "exists", + "children": [] + } + } + ] + } + } + ], + "logic": "and" + }, + "assay_filters": { + "compound": [], + "logic": "and" + }, + "logic": "and" + }, + "sql": "SELECT `cohort_subquery`.`patient_id` AS `patient_id` FROM (SELECT `patient_1`.`patient_id` AS `patient_id`, `patient_1`.`hid` AS `hid` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE EXISTS (SELECT `hospital_1`.`hospital_id` AS `hospital_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`hospital` AS `hospital_1` WHERE `hospital_1`.`min_score_all` IN (500, 400) AND `hospital_1`.`hospital_id` = `patient_1`.`hid`)) AS `cohort_subquery`", + "downloadRestricted": false, + "containsPHI": false, + "restrictedProjects": [], + "clipboardRestricted": false +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/visualize_response/pheno_geno_merged_dataset_as_input.json b/src/python/test/create_cohort_test_files/payloads/visualize_response/pheno_geno_merged_dataset_as_input.json new file mode 100644 index 0000000000..2079922d4e --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/visualize_response/pheno_geno_merged_dataset_as_input.json @@ -0,0 +1,222 @@ +{ + "dataset": "record-GYgzjV80yB6QPJJ6kk40bBFy", + "schema": "create_cohort_schema", + "version": "3.0", + "datasetVersion": "3.0", + "url": "https://vizserver.us-east-1-stg.apollo.dnanexus.com", + "datasetLoaded": false, + "datasetUpdated": true, + "name": "pheno_geno_merged_dataset", + "description": "Dataset: pheno_geno_merged_dataset", + "recordName": "pheno_geno_merged_dataset", + "recordState": "closed", + "recordTypes": [ + "Dataset" + ], + "recordProject": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "datasetName": "pheno_geno_merged_dataset", + "datasetDescription": "Dataset: pheno_geno_merged_dataset", + "datasetRecordName": "pheno_geno_merged_dataset", + "datasetRecordState": "closed", + "datasetRecordProject": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "databases": [ + "database-GYK2yg00vGPpzj7YGY3VJxb9", + "database-FkypXj80bgYvQqyqBx1FJG6y", + "database-GYK3JJj0vGPQ2Y1y98pj8pGg", + "database-GYgzGf80vGPvvQG47p5bpX7x" + ], + "dashboardViews": [ + { + "name": "pheno_geno_merged_dataset", + "record": "record-GYgzjV80yB6zV109Xb0ZJZzx", + "project": "project-G9j1pX00vGPzF2XQ7843k2Jq" + } + ], + "dashboardConfig": { + "cohort_browser": { + "type": "CohortBrowser", + "containers": [ + { + "type": "FieldSelector", + "id": "field_selector", + "title": "Field Selector", + "options": { + "search_string": "", + "selected": [] + } + }, + { + "type": "FieldsAsTiles", + "id": "dashboard_tiles", + "title": "Overview", + "options": {} + }, + { + "type": "Accordion", + "id": "cohort_table_container", + "tiles": [ + { + "type": "Table", + "id": "cohort_table", + "dataQuery": { + "fields": { + "cohort_id": "patient$patient_id" + }, + "isCohort": true, + "limit": 30000 + }, + "options": { + "columns": [ + { + "id": "cohort_id", + "title": "Cohort ID", + "isStatic": true + } + ] + }, + "layout": { + "height": 1, + "width": 1 + }, + "title": "Cohort Table" + } + ], + "title": "Data Preview", + "options": { + "showDownloadButton": true, + "showCopyButton": true + } + }, + { + "type": "GenomeBrowser", + "id": "variant_browser_container", + "tiles": [ + { + "type": "Lollipop", + "id": "variant_browser_plots_lollipop", + "title": "Variant Browser Lollipop Plot", + "dataQuery": { + "computeAlleleFrequency": "genotype$type", + "fields": { + "allele_frequency": "allele$gnomad201_alt_freq", + "allele_type": "allele$allele_type", + "alt": "allele$alt", + "alt_count": "allele$alt_count", + "chr": "allele$chr", + "consequence": "allele$worst_effect", + "id": "allele$a_id", + "locus": "allele$locus_id", + "pos": "allele$pos", + "ref": "allele$ref", + "rsid": "allele$dbsnp151_rsid", + "y": "allele$alt_freq" + }, + "isCohort": true, + "limit": 10000 + } + }, + { + "type": "Transcript", + "id": "variant_browser_plots_transcript", + "title": "Variant Browser Transcript Plot", + "dataQuery": { + "fields": { + "id": "transcript$transcript_name", + "gene": "transcript$gene_name", + "chr": "transcript$chr", + "pos": "transcript$pos", + "pos_end": "transcript$end_pos", + "exons": "transcript$exon", + "strand": "transcript$strand" + }, + "isCohort": false, + "limit": 10000 + } + }, + { + "type": "Table", + "id": "variant_browser_table", + "dataQuery": { + "fields": {} + }, + "options": { + "columns": [ + { + "id": "id", + "title": "Location", + "isStatic": true + }, + { + "id": "rsid", + "title": "RSID" + }, + { + "id": "ref", + "title": "Reference" + }, + { + "id": "alt", + "title": "Alternate" + }, + { + "id": "alleleType", + "title": "Type" + }, + { + "id": "consequence", + "title": "Consequence (Most severe by gene)" + }, + { + "id": "cohortAlleleFrequency", + "title": "Cohort AF" + }, + { + "id": "y", + "title": "Population AF" + }, + { + "id": "alleleFrequency", + "title": "GnomAD AF" + } + ] + }, + "layout": { + "height": 1, + "width": 1 + }, + "dataID": "variant_browser_plots_lollipop", + "title": "Variant Browser Table" + } + ], + "title": "Genomics", + "options": { + "genomicRangeSearch": { + "chr": "1", + "end": 55065852, + "start": 55038548 + } + } + } + ], + "id": "cohort_browser", + "title": "Cohort Browser", + "config": { + "showHeader": true, + "showFooter": true, + "showFilter": true + } + }, + "locus_detail": { + "type": "LocusDetails", + "containers": [], + "id": "locus_details", + "title": "Locus Detail", + "config": null + } + }, + "dashboardConfigRecord": "record-GYgzjV80yB6zV109Xb0ZJZzx", + "downloadRestricted": false, + "containsPHI": false, + "restrictedProjects": [], + "clipboardRestricted": false +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/payloads/visualize_response/unfiltered_cohort_as_input.json b/src/python/test/create_cohort_test_files/payloads/visualize_response/unfiltered_cohort_as_input.json new file mode 100644 index 0000000000..4ddf05fecf --- /dev/null +++ b/src/python/test/create_cohort_test_files/payloads/visualize_response/unfiltered_cohort_as_input.json @@ -0,0 +1,108 @@ +{ + "dataset": "record-GYK2zyQ0g1bx86fBp2X8KpjY", + "schema": "create_cohort_schema", + "version": "3.0", + "datasetVersion": "3.0", + "url": "https://vizserver.us-east-1-stg.apollo.dnanexus.com", + "datasetLoaded": false, + "datasetUpdated": true, + "name": "unfiltered_cohort", + "description": "", + "recordName": "unfiltered_cohort", + "recordState": "closed", + "recordTypes": [ + "DatabaseQuery", + "CohortBrowser" + ], + "recordProject": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "datasetName": "create_cohort_pheno_dataset", + "datasetDescription": "Dataset: create_cohort_pheno_dataset", + "datasetRecordName": "create_cohort_pheno_dataset", + "datasetRecordState": "closed", + "datasetRecordProject": "project-G9j1pX00vGPzF2XQ7843k2Jq", + "databases": [ + "database-GYK2yg00vGPpzj7YGY3VJxb9" + ], + "dashboardViews": [], + "dashboardConfig": { + "cohort_browser": { + "config": { + "showHeader": true, + "showFooter": true, + "showFilter": true + }, + "id": "cohort_browser", + "type": "cohort_browser", + "containers": [ + { + "id": "dashboard_tiles", + "title": "Overview", + "tiles": [], + "type": "FieldsAsTiles", + "options": {} + }, + { + "id": "ted_container", + "title": "Data Preview", + "tiles": [ + { + "id": "ted@patient", + "title": "Patients", + "type": "Table", + "dataQuery": { + "fields": { + "patient$patient_id": "patient$patient_id" + }, + "entity": "patient" + }, + "options": { + "columns": [ + { + "id": "patient$patient_id", + "title": "Patient ID", + "isPinned": true, + "isPrimary": true + } + ] + }, + "layout": { + "height": 1, + "width": 1 + } + } + ], + "type": "TedContainer", + "options": {} + } + ] + } + }, + "filters": { + "pheno_filters": { + "compound": [ + { + "name": "phenotype", + "logic": "and", + "filters": {}, + "entity": { + "logic": "and", + "name": "patient", + "operator": "exists", + "children": [] + } + } + ], + "logic": "and" + }, + "assay_filters": { + "compound": [], + "logic": "and" + }, + "logic": "and" + }, + "sql": "SELECT `patient_1`.`patient_id` AS `patient_id` FROM `database_gyk2yg00vgppzj7ygy3vjxb9__create_cohort_pheno_database`.`patient` AS `patient_1`", + "downloadRestricted": false, + "containsPHI": false, + "restrictedProjects": [], + "clipboardRestricted": false +} \ No newline at end of file diff --git a/src/python/test/create_cohort_test_files/usage_message.txt b/src/python/test/create_cohort_test_files/usage_message.txt new file mode 100644 index 0000000000..c1da82daca --- /dev/null +++ b/src/python/test/create_cohort_test_files/usage_message.txt @@ -0,0 +1,36 @@ +usage: dx create_cohort [--brief | --verbose] --from FROM + (--cohort-ids COHORT_IDS | --cohort-ids-file COHORT_IDS_FILE) + [-h] + [PATH] + +Generates a new Cohort object on the platform from an existing Dataset or +Cohort object and using list of IDs. + +positional arguments: + PATH DNAnexus path for the new data object. If not + provided, default behavior uses current project and + folder, and will name the object identical to the + assigned record-id. + +optional arguments: + --brief Display a brief version of the return value; for most + commands, prints a DNAnexus ID per line + --verbose If available, displays extra verbose output + --from FROM v3.0 Dataset or Cohort object ID, project-id:record- + id, where ":record-id" indicates the record-id in + current selected project, or name + --cohort-ids COHORT_IDS + A set of IDs used to subset the Dataset or Cohort + object as a comma-separated string. IDs must match + identically in the supplied Dataset. If a Cohort is + supplied instead of a Dataset, the intersection of + supplied and existing cohort IDs will be used to + create the new cohort. + --cohort-ids-file COHORT_IDS_FILE + A set of IDs used to subset the Dataset or Cohort + object in a file with one ID per line and no header. + IDs must match identically in the supplied Dataset. If + a Cohort is supplied instead of a Dataset, the + intersection of supplied and existing cohort IDs will + be used to create the new cohort. + -h, --help Return the docstring and exit diff --git a/src/python/test/dxpy_testutil.py b/src/python/test/dxpy_testutil.py index de9b877574..ae0a5c7ec0 100644 --- a/src/python/test/dxpy_testutil.py +++ b/src/python/test/dxpy_testutil.py @@ -29,7 +29,9 @@ import dxpy from dxpy.compat import str, basestring, USING_PYTHON2 +from pathlib import Path +THIS_DIR = Path(__file__).parent _run_all_tests = 'DXTEST_FULL' in os.environ TEST_AZURE = ((os.environ.get('DXTEST_AZURE', '').startswith('azure:') and os.environ['DXTEST_AZURE']) or (os.environ.get('DXTEST_AZURE') and 'azure:westus')) @@ -47,6 +49,7 @@ TEST_TCSH = _run_all_tests or 'DXTEST_TCSH' in os.environ TEST_WITH_AUTHSERVER = _run_all_tests or 'DXTEST_WITH_AUTHSERVER' in os.environ TEST_WITH_SMOKETEST_APP = _run_all_tests or 'DXTEST_WITH_SMOKETEST_APP' in os.environ +TEST_NF_DOCKER = _run_all_tests or 'DXTEST_NF_DOCKER' in os.environ def _transform_words_to_regexp(s): @@ -573,7 +576,7 @@ def create_dxworkflow_spec(self): "executable": self.test_applet_id, "input": {"number": 777}, "folder": "/stage_0_output", - "executionPolicy": {"restartOn": {}, "onNonRestartableFailure": "failStage"}, + "executionPolicy": {"onNonRestartableFailure": "failStage"}, "systemRequirements": {"main": {"instanceType": "mem1_ssd1_x2"}}}, {"id": "stage_1", "executable": self.test_applet_id, @@ -594,10 +597,10 @@ class DXTestCaseBuildApps(DXTestCase): "dxapi": "1.0.0", "runSpec": { "file": "code.py", - "interpreter": "python2.7", + "interpreter": "python3", "distribution": "Ubuntu", - "release": "14.04", - "version": '0' + "release": "20.04", + "version": "0" }, "inputSpec": [], "outputSpec": [], @@ -650,6 +653,53 @@ def write_app_directory(self, app_name, dxapp_str, code_filename=None, code_cont return p +class DXTestCaseBuildNextflowApps(DXTestCase): + """ + This class adds methods to ``DXTestCase`` related to app creation, + app destruction, and extraction of app data as local files. + """ + + base_nextflow_nf = THIS_DIR / "nextflow/hello/main.nf" + base_nextflow_docker = THIS_DIR / "nextflow/profile_with_docker" + + def setUp(self): + super(DXTestCaseBuildNextflowApps, self).setUp() + self.temp_file_path = tempfile.mkdtemp() + + def tearDown(self): + shutil.rmtree(self.temp_file_path) + super(DXTestCaseBuildNextflowApps, self).tearDown() + + def write_nextflow_applet_directory_from_folder(self, applet_name, existing_nf_directory_path): + p = os.path.join(self.temp_file_path, applet_name) + try: + shutil.copytree(existing_nf_directory_path, p) + except OSError as e: + if e.errno != 17: # directory already exists + raise e + return p + + + def write_nextflow_applet_directory(self, applet_name, existing_nf_file_path=None, nf_file_name="main.nf", nf_file_content="\n"): + # Note: if called twice with the same app_name, will overwrite + # the dxapp.json and the nf file (if specified) but will not + # remove any other files that happened to be present; + # applet_name will be the name of the folder storing the pipeline + p = os.path.join(self.temp_file_path, applet_name) + pb = p.encode("utf-8") + try: + os.mkdir(pb) + except OSError as e: + if e.errno != 17: # directory already exists + raise e + if existing_nf_file_path: + # copy the nf file to the temporary pipeline directory + shutil.copyfile(existing_nf_file_path, p + "/" + os.path.basename(existing_nf_file_path)) + else: + with open(os.path.join(pb, nf_file_name.encode("utf-8")), 'w') as nf_file: + nf_file.write(nf_file_content) + return p + class TemporaryFile: ''' A wrapper class around a NamedTemporaryFile. Intended for use inside a 'with' statement. It returns a file-like object that can be opened by another process for writing, in particular diff --git a/src/python/test/ea_malformed_json/allele/allele_missing_rsid.json b/src/python/test/ea_malformed_json/allele/allele_missing_rsid.json new file mode 100644 index 0000000000..75c3d4df6f --- /dev/null +++ b/src/python/test/ea_malformed_json/allele/allele_missing_rsid.json @@ -0,0 +1,5 @@ +{ + "type": [ + "SNP" + ] +} \ No newline at end of file diff --git a/src/python/test/ea_malformed_json/allele/allele_rsid_location.json b/src/python/test/ea_malformed_json/allele/allele_rsid_location.json new file mode 100644 index 0000000000..9784dd710c --- /dev/null +++ b/src/python/test/ea_malformed_json/allele/allele_rsid_location.json @@ -0,0 +1,12 @@ +{ + "rsid": [ + "rs1342568097" + ], + "location": [ + { + "chromosome": "18", + "starting_position": "47361", + "ending_position": "47364" + } + ] +} \ No newline at end of file diff --git a/src/python/test/ea_malformed_json/allele/alt_freq_minmax.json b/src/python/test/ea_malformed_json/allele/alt_freq_minmax.json new file mode 100644 index 0000000000..3e03ab9c59 --- /dev/null +++ b/src/python/test/ea_malformed_json/allele/alt_freq_minmax.json @@ -0,0 +1,9 @@ +{ + "rsid": [ + "rs749233405" + ], + "dataset_alt_af": { + "min": 0.3, + "max": 0.2 + } +} \ No newline at end of file diff --git a/src/python/test/ea_malformed_json/allele/bad_location.json b/src/python/test/ea_malformed_json/allele/bad_location.json new file mode 100644 index 0000000000..b0773af67a --- /dev/null +++ b/src/python/test/ea_malformed_json/allele/bad_location.json @@ -0,0 +1,8 @@ +{ + "location": [ + { + "starting_position": "47361", + "ending_position": "47364" + } + ] +} \ No newline at end of file diff --git a/src/python/test/ea_malformed_json/allele/bad_rsid.json b/src/python/test/ea_malformed_json/allele/bad_rsid.json new file mode 100644 index 0000000000..8ec27daf4f --- /dev/null +++ b/src/python/test/ea_malformed_json/allele/bad_rsid.json @@ -0,0 +1,3 @@ +{ + "rsid": 7 +} \ No newline at end of file diff --git a/src/python/test/ea_malformed_json/allele/bad_type.json b/src/python/test/ea_malformed_json/allele/bad_type.json new file mode 100644 index 0000000000..e0c707f370 --- /dev/null +++ b/src/python/test/ea_malformed_json/allele/bad_type.json @@ -0,0 +1,9 @@ +{ + "rsid": [ + "rs1342568097" + ], + "type": [ + "SNP", + "badentry" + ] +} \ No newline at end of file diff --git a/src/python/test/ea_malformed_json/allele/gnomad_minmax.json b/src/python/test/ea_malformed_json/allele/gnomad_minmax.json new file mode 100644 index 0000000000..21e9cc6137 --- /dev/null +++ b/src/python/test/ea_malformed_json/allele/gnomad_minmax.json @@ -0,0 +1,9 @@ +{ + "rsid": [ + "rs749233405" + ], + "gnomad_alt_af": { + "min": 0.3, + "max": 0.2 + } +} \ No newline at end of file diff --git a/src/python/test/ea_malformed_json/annotation/annotation_aid_gid.json b/src/python/test/ea_malformed_json/annotation/annotation_aid_gid.json new file mode 100644 index 0000000000..f47a68e345 --- /dev/null +++ b/src/python/test/ea_malformed_json/annotation/annotation_aid_gid.json @@ -0,0 +1,8 @@ +{ + "allele_id": [ + "18_47408_G_A" + ], + "gene_id": [ + "ENSG00000173213" + ] +} \ No newline at end of file diff --git a/src/python/test/ea_malformed_json/annotation/annotation_aid_gname.json b/src/python/test/ea_malformed_json/annotation/annotation_aid_gname.json new file mode 100644 index 0000000000..1457e16ee4 --- /dev/null +++ b/src/python/test/ea_malformed_json/annotation/annotation_aid_gname.json @@ -0,0 +1,8 @@ +{ + "allele_id": [ + "18_47408_G_A" + ], + "gene_name": [ + "TUBB8P12" + ] +} \ No newline at end of file diff --git a/src/python/test/ea_malformed_json/annotation/annotation_gid_gname.json b/src/python/test/ea_malformed_json/annotation/annotation_gid_gname.json new file mode 100644 index 0000000000..6f7e61863a --- /dev/null +++ b/src/python/test/ea_malformed_json/annotation/annotation_gid_gname.json @@ -0,0 +1,8 @@ +{ + "gene_id": [ + "ENSG00000173213" + ], + "gene_name": [ + "TUBB8P12" + ] +} \ No newline at end of file diff --git a/src/python/test/ea_malformed_json/annotation/annotation_no_required.json b/src/python/test/ea_malformed_json/annotation/annotation_no_required.json new file mode 100644 index 0000000000..8cd84b14ae --- /dev/null +++ b/src/python/test/ea_malformed_json/annotation/annotation_no_required.json @@ -0,0 +1,5 @@ +{ + "consequences": [ + "missense_variant" + ] +} \ No newline at end of file diff --git a/src/python/test/ea_malformed_json/annotation/consequences_no_required.json b/src/python/test/ea_malformed_json/annotation/consequences_no_required.json new file mode 100644 index 0000000000..e0effb1e2d --- /dev/null +++ b/src/python/test/ea_malformed_json/annotation/consequences_no_required.json @@ -0,0 +1,14 @@ +{ + "allele_id": [ + "18_47408_G_A" + ], + "consequences": [ + "synonymous_variant" + ], + "hgvs_c": [ + "c.1317C>T" + ], + "hgvs_p": [ + "p.Ala439Ala" + ] +} \ No newline at end of file diff --git a/src/python/test/ea_malformed_json/annotation/putative_no_required.json b/src/python/test/ea_malformed_json/annotation/putative_no_required.json new file mode 100644 index 0000000000..2746957e19 --- /dev/null +++ b/src/python/test/ea_malformed_json/annotation/putative_no_required.json @@ -0,0 +1,14 @@ +{ + "allele_id": [ + "18_47408_G_A" + ], + "putative_impact": [ + "LOW" + ], + "hgvs_c": [ + "c.1317C>T" + ], + "hgvs_p": [ + "p.Ala439Ala" + ] +} \ No newline at end of file diff --git a/src/python/test/ea_malformed_json/genotype/genotype_no_required.json b/src/python/test/ea_malformed_json/genotype/genotype_no_required.json new file mode 100644 index 0000000000..8c774cdfe4 --- /dev/null +++ b/src/python/test/ea_malformed_json/genotype/genotype_no_required.json @@ -0,0 +1,5 @@ +{ + "genotype_type": [ + "het-alt" + ] +} \ No newline at end of file diff --git a/src/python/test/expression_test_assets/expression_test_expected_output_dict.py b/src/python/test/expression_test_assets/expression_test_expected_output_dict.py new file mode 100644 index 0000000000..1db57c8bc7 --- /dev/null +++ b/src/python/test/expression_test_assets/expression_test_expected_output_dict.py @@ -0,0 +1,205 @@ +VIZPAYLOADERBUILDER_EXPECTED_OUTPUT = { + "test_vizpayloadbuilder_location_cohort": { + "expected_data_output": [ + { + "feature_id": "ENST00000456328", + "sample_id": "sample_1", + "expression": 23, + }, + { + "feature_id": "ENST00000456328", + "sample_id": "sample_2", + "expression": 90, + }, + { + "feature_id": "ENST00000456328", + "sample_id": "sample_3", + "expression": 58, + }, + ], + "expected_sql_output": [ + "SELECT `expression_1`.`feature_id` AS `feature_id`, `expression_1`.`sample_id` AS `sample_id`, `expression_1`.`value` AS `expression` FROM `database_gzky7400vgpyzy621q43gkkf__molecular_expression1_db`.`expression` AS `expression_1` LEFT OUTER JOIN `database_gzky7400vgpyzy621q43gkkf__molecular_expression1_db`.`expr_annotation` AS `expr_annotation_1` ON `expression_1`.`feature_id` = `expr_annotation_1`.`feature_id` LEFT OUTER JOIN `database_gzky7400vgpyzy621q43gkkf__molecular_expression1_db`.`sample` AS `sample_1` ON `expression_1`.`sample_id` = `sample_1`.`sample_id` WHERE `expr_annotation_1`.`chr` = '1' AND (`expr_annotation_1`.`start` BETWEEN 10000 AND 12000 OR `expr_annotation_1`.`end` BETWEEN 10000 AND 12000 OR `expr_annotation_1`.`start` <= 10000 AND `expr_annotation_1`.`end` >= 12000) AND `sample_1`.`sample_id` IN (SELECT `cohort_query`.`sample_id` FROM (SELECT `sample_1`.`sample_id` AS `sample_id` FROM `database_gzky7400vgpyzy621q43gkkf__molecular_expression1_db`.`sample` AS `sample_1` WHERE `sample_1`.`sample_id` IN (SELECT `sample_id` FROM (SELECT `sample_1`.`sample_id` AS `sample_id` FROM `database_gzky7400vgpyzy621q43gkkf__molecular_expression1_db`.`sample` AS `sample_1` UNION SELECT `sample_1`.`sample_id` AS `sample_id` FROM `database_gzky7400vgpyzy621q43gkkf__molecular_expression1_db`.`sample` AS `sample_1`))) AS `cohort_query`) ORDER BY `feature_id` ASC, `sample_id` ASC", + "SELECT `expression_1`.`feature_id` AS `feature_id`, `expression_1`.`sample_id` AS `sample_id`, `expression_1`.`value` AS `expression` FROM `database_gzky7400vgpyzy621q43gkkf__molecular_expression1_db`.`expression` AS `expression_1` LEFT OUTER JOIN `database_gzky7400vgpyzy621q43gkkf__molecular_expression1_db`.`expr_annotation` AS `expr_annotation_1` ON `expression_1`.`feature_id` = `expr_annotation_1`.`feature_id` LEFT OUTER JOIN `database_gzky7400vgpyzy621q43gkkf__molecular_expression1_db`.`sample` AS `sample_1` ON `expression_1`.`sample_id` = `sample_1`.`sample_id` WHERE `expr_annotation_1`.`chr` = '1' AND (`expr_annotation_1`.`end` BETWEEN 10000 AND 12000 OR `expr_annotation_1`.`start` BETWEEN 10000 AND 12000 OR `expr_annotation_1`.`end` >= 12000 AND `expr_annotation_1`.`start` <= 10000) AND `sample_1`.`sample_id` IN (SELECT `cohort_query`.`sample_id` FROM (SELECT `sample_1`.`sample_id` AS `sample_id` FROM `database_gzky7400vgpyzy621q43gkkf__molecular_expression1_db`.`sample` AS `sample_1` WHERE `sample_1`.`sample_id` IN (SELECT `sample_id` FROM (SELECT `sample_1`.`sample_id` AS `sample_id` FROM `database_gzky7400vgpyzy621q43gkkf__molecular_expression1_db`.`sample` AS `sample_1` UNION SELECT `sample_1`.`sample_id` AS `sample_id` FROM `database_gzky7400vgpyzy621q43gkkf__molecular_expression1_db`.`sample` AS `sample_1`))) AS `cohort_query`) ORDER BY `feature_id` ASC, `sample_id` ASC", + ], + }, + "test_vizpayloadbuilder_location_multiple": { + "expected_data_output": [ + { + "feature_id": "ENST00000327669", + "sample_id": "sample_1", + "expression": 11, + }, + { + "feature_id": "ENST00000327669", + "sample_id": "sample_2", + "expression": 78, + }, + { + "feature_id": "ENST00000327669", + "sample_id": "sample_3", + "expression": 23, + }, + { + "feature_id": "ENST00000456328", + "sample_id": "sample_3", + "expression": 58, + }, + { + "feature_id": "ENST00000456328", + "sample_id": "sample_2", + "expression": 90, + }, + { + "feature_id": "ENST00000456328", + "sample_id": "sample_1", + "expression": 23, + }, + ], + "expected_sql_output": [ + "SELECT `expression_1`.`feature_id` AS `feature_id`, `expression_1`.`sample_id` AS `sample_id`, `expression_1`.`value` AS `expression` FROM `database_gzky7400vgpyzy621q43gkkf__molecular_expression1_db`.`expression` AS `expression_1` LEFT OUTER JOIN `database_gzky7400vgpyzy621q43gkkf__molecular_expression1_db`.`expr_annotation` AS `expr_annotation_1` ON `expression_1`.`feature_id` = `expr_annotation_1`.`feature_id` WHERE `expr_annotation_1`.`chr` = '1' AND (`expr_annotation_1`.`start` BETWEEN 10000 AND 12000 OR `expr_annotation_1`.`end` BETWEEN 10000 AND 12000 OR `expr_annotation_1`.`start` <= 10000 AND `expr_annotation_1`.`end` >= 12000) OR `expr_annotation_1`.`chr` = '2' AND (`expr_annotation_1`.`start` BETWEEN 30000 AND 40000 OR `expr_annotation_1`.`end` BETWEEN 30000 AND 40000 OR `expr_annotation_1`.`start` <= 30000 AND `expr_annotation_1`.`end` >= 40000) ORDER BY `feature_id` ASC, `sample_id` ASC", + "SELECT `expression_1`.`feature_id` AS `feature_id`, `expression_1`.`sample_id` AS `sample_id`, `expression_1`.`value` AS `expression` FROM `database_gzky7400vgpyzy621q43gkkf__molecular_expression1_db`.`expression` AS `expression_1` LEFT OUTER JOIN `database_gzky7400vgpyzy621q43gkkf__molecular_expression1_db`.`expr_annotation` AS `expr_annotation_1` ON `expression_1`.`feature_id` = `expr_annotation_1`.`feature_id` WHERE `expr_annotation_1`.`chr` = '1' AND (`expr_annotation_1`.`end` BETWEEN 10000 AND 12000 OR `expr_annotation_1`.`start` BETWEEN 10000 AND 12000 OR `expr_annotation_1`.`end` >= 12000 AND `expr_annotation_1`.`start` <= 10000) OR `expr_annotation_1`.`chr` = '2' AND (`expr_annotation_1`.`end` BETWEEN 30000 AND 40000 OR `expr_annotation_1`.`start` BETWEEN 30000 AND 40000 OR `expr_annotation_1`.`end` >= 40000 AND `expr_annotation_1`.`start` <= 30000) ORDER BY `feature_id` ASC, `sample_id` ASC", + ], + }, + "test_vizpayloadbuilder_annotation_feature_name": { + "expected_data_output": [ + { + "feature_id": "ENST00000318560", + "sample_id": "sample_3", + "expression": 56, + }, + { + "feature_id": "ENST00000318560", + "sample_id": "sample_2", + "expression": 24, + }, + { + "feature_id": "ENST00000318560", + "sample_id": "sample_1", + "expression": 39, + }, + { + "feature_id": "ENST00000372348", + "sample_id": "sample_3", + "expression": 90, + }, + { + "feature_id": "ENST00000372348", + "sample_id": "sample_2", + "expression": 40, + }, + { + "feature_id": "ENST00000372348", + "sample_id": "sample_1", + "expression": 87, + }, + { + "feature_id": "ENST00000393293", + "sample_id": "sample_2", + "expression": 11, + }, + { + "feature_id": "ENST00000393293", + "sample_id": "sample_3", + "expression": 40, + }, + { + "feature_id": "ENST00000393293", + "sample_id": "sample_1", + "expression": 17, + }, + ], + "expected_sql_output": "SELECT `expression_1`.`feature_id` AS `feature_id`, `expression_1`.`sample_id` AS `sample_id`, `expression_1`.`value` AS `expression` FROM `database_gzky7400vgpyzy621q43gkkf__molecular_expression1_db`.`expression` AS `expression_1` LEFT OUTER JOIN `database_gzky7400vgpyzy621q43gkkf__molecular_expression1_db`.`expr_annotation` AS `expr_annotation_1` ON `expression_1`.`feature_id` = `expr_annotation_1`.`feature_id` WHERE `expr_annotation_1`.`gene_name` IN ('ABL1') ORDER BY `feature_id` ASC, `sample_id` ASC", + }, + "test_vizpayloadbuilder_annotation_feature_id": { + "expected_data_output": [ + { + "feature_id": "ENST00000327669", + "sample_id": "sample_1", + "expression": 11, + }, + { + "feature_id": "ENST00000327669", + "sample_id": "sample_2", + "expression": 78, + }, + { + "feature_id": "ENST00000327669", + "sample_id": "sample_3", + "expression": 23, + }, + { + "feature_id": "ENST00000456328", + "sample_id": "sample_3", + "expression": 58, + }, + { + "feature_id": "ENST00000456328", + "sample_id": "sample_2", + "expression": 90, + }, + { + "feature_id": "ENST00000456328", + "sample_id": "sample_1", + "expression": 23, + }, + ], + "expected_sql_output": "SELECT `expression_1`.`feature_id` AS `feature_id`, `expression_1`.`sample_id` AS `sample_id`, `expression_1`.`value` AS `expression` FROM `database_gzky7400vgpyzy621q43gkkf__molecular_expression1_db`.`expression` AS `expression_1` LEFT OUTER JOIN `database_gzky7400vgpyzy621q43gkkf__molecular_expression1_db`.`expr_annotation` AS `expr_annotation_1` ON `expression_1`.`feature_id` = `expr_annotation_1`.`feature_id` WHERE `expr_annotation_1`.`feature_id` IN ('ENST00000327669', 'ENST00000456328') ORDER BY `feature_id` ASC, `sample_id` ASC", + }, + "test_vizpayloadbuilder_expression_min": { + "expected_data_output": [ + { + "feature_id": "ENST00000327669", + "sample_id": "sample_2", + "expression": 78, + }, + { + "feature_id": "ENST00000456328", + "sample_id": "sample_2", + "expression": 90, + }, + ], + "expected_sql_output": "SELECT `expression_1`.`feature_id` AS `feature_id`, `expression_1`.`sample_id` AS `sample_id`, `expression_1`.`value` AS `expression` FROM `database_gzky7400vgpyzy621q43gkkf__molecular_expression1_db`.`expression` AS `expression_1` LEFT OUTER JOIN `database_gzky7400vgpyzy621q43gkkf__molecular_expression1_db`.`expr_annotation` AS `expr_annotation_1` ON `expression_1`.`feature_id` = `expr_annotation_1`.`feature_id` WHERE `expression_1`.`value` >= 70 AND `expr_annotation_1`.`feature_id` IN ('ENST00000327669', 'ENST00000456328') ORDER BY `feature_id` ASC, `sample_id` ASC", + }, + "test_vizpayloadbuilder_expression_max": { + "expected_data_output": [ + {"feature_id": "ENST00000666593", "sample_id": "sample_2", "expression": 3} + ], + "expected_sql_output": "SELECT `expression_1`.`feature_id` AS `feature_id`, `expression_1`.`sample_id` AS `sample_id`, `expression_1`.`value` AS `expression` FROM `database_gzky7400vgpyzy621q43gkkf__molecular_expression1_db`.`expression` AS `expression_1` LEFT OUTER JOIN `database_gzky7400vgpyzy621q43gkkf__molecular_expression1_db`.`expr_annotation` AS `expr_annotation_1` ON `expression_1`.`feature_id` = `expr_annotation_1`.`feature_id` WHERE `expression_1`.`value` <= 10 AND `expr_annotation_1`.`gene_name` IN ('BRCA2') ORDER BY `feature_id` ASC, `sample_id` ASC", + }, + "test_vizpayloadbuilder_expression_mixed": { + "expected_data_output": [ + {"feature_id": "ENST00000456328", "sample_id": "sample_3", "expression": 58} + ], + "expected_sql_output": [ + "SELECT `expression_1`.`feature_id` AS `feature_id`, `expression_1`.`sample_id` AS `sample_id`, `expression_1`.`value` AS `expression` FROM `database_gzky7400vgpyzy621q43gkkf__molecular_expression1_db`.`expression` AS `expression_1` LEFT OUTER JOIN `database_gzky7400vgpyzy621q43gkkf__molecular_expression1_db`.`expr_annotation` AS `expr_annotation_1` ON `expression_1`.`feature_id` = `expr_annotation_1`.`feature_id` WHERE `expression_1`.`value` BETWEEN 30 AND 60 AND `expr_annotation_1`.`chr` = '1' AND (`expr_annotation_1`.`start` BETWEEN 10000 AND 12000 OR `expr_annotation_1`.`end` BETWEEN 10000 AND 12000 OR `expr_annotation_1`.`start` <= 10000 AND `expr_annotation_1`.`end` >= 12000) ORDER BY `feature_id` ASC, `sample_id` ASC", + "SELECT `expression_1`.`feature_id` AS `feature_id`, `expression_1`.`sample_id` AS `sample_id`, `expression_1`.`value` AS `expression` FROM `database_gzky7400vgpyzy621q43gkkf__molecular_expression1_db`.`expression` AS `expression_1` LEFT OUTER JOIN `database_gzky7400vgpyzy621q43gkkf__molecular_expression1_db`.`expr_annotation` AS `expr_annotation_1` ON `expression_1`.`feature_id` = `expr_annotation_1`.`feature_id` WHERE `expression_1`.`value` BETWEEN 30 AND 60 AND `expr_annotation_1`.`chr` = '1' AND (`expr_annotation_1`.`end` BETWEEN 10000 AND 12000 OR `expr_annotation_1`.`start` BETWEEN 10000 AND 12000 OR `expr_annotation_1`.`end` >= 12000 AND `expr_annotation_1`.`start` <= 10000) ORDER BY `feature_id` ASC, `sample_id` ASC", + ], + }, + "test_vizpayloadbuilder_sample": { + "expected_sql_output": "SELECT `expression_1`.`feature_id` AS `feature_id`, `expression_1`.`sample_id` AS `sample_id`, `expression_1`.`value` AS `expression` FROM `database_gzky7400vgpyzy621q43gkkf__molecular_expression1_db`.`expression` AS `expression_1` WHERE `expression_1`.`sample_id` IN ('sample_1') ORDER BY `feature_id` ASC, `sample_id` ASC" + }, + "test_vizpayloadbuilder_location_sample_expression": { + "expected_data_output": [ + { + "feature_id": "ENST00000450305", + "sample_id": "sample_1", + "expression": 76, + }, + { + "feature_id": "ENST00000619216", + "sample_id": "sample_1", + "expression": 59, + }, + ], + "expected_sql_output": [ + "SELECT `expression_1`.`feature_id` AS `feature_id`, `expression_1`.`sample_id` AS `sample_id`, `expression_1`.`value` AS `expression` FROM `database_gzky7400vgpyzy621q43gkkf__molecular_expression1_db`.`expression` AS `expression_1` LEFT OUTER JOIN `database_gzky7400vgpyzy621q43gkkf__molecular_expression1_db`.`expr_annotation` AS `expr_annotation_1` ON `expression_1`.`feature_id` = `expr_annotation_1`.`feature_id` WHERE `expr_annotation_1`.`chr` = '1' AND (`expr_annotation_1`.`start` BETWEEN 10000 AND 20000 OR `expr_annotation_1`.`end` BETWEEN 10000 AND 20000 OR `expr_annotation_1`.`start` <= 10000 AND `expr_annotation_1`.`end` >= 20000) AND `expression_1`.`sample_id` IN ('sample_1') AND `expression_1`.`value` BETWEEN 25 AND 80 ORDER BY `feature_id` ASC, `sample_id` ASC", + "SELECT `expression_1`.`feature_id` AS `feature_id`, `expression_1`.`sample_id` AS `sample_id`, `expression_1`.`value` AS `expression` FROM `database_gzky7400vgpyzy621q43gkkf__molecular_expression1_db`.`expression` AS `expression_1` LEFT OUTER JOIN `database_gzky7400vgpyzy621q43gkkf__molecular_expression1_db`.`expr_annotation` AS `expr_annotation_1` ON `expression_1`.`feature_id` = `expr_annotation_1`.`feature_id` WHERE `expr_annotation_1`.`chr` = '1' AND (`expr_annotation_1`.`end` BETWEEN 10000 AND 20000 OR `expr_annotation_1`.`start` BETWEEN 10000 AND 20000 OR `expr_annotation_1`.`end` >= 20000 AND `expr_annotation_1`.`start` <= 10000) AND `expression_1`.`sample_id` IN ('sample_1') AND `expression_1`.`value` BETWEEN 25 AND 80 ORDER BY `feature_id` ASC, `sample_id` ASC", + ], + }, + "test_vizpayloadbuilder_annotation_sample_expression": { + "expected_data_output": [ + {"feature_id": "ENST00000327669", "sample_id": "sample_1", "expression": 11} + ], + "expected_sql_output": "SELECT `expression_1`.`feature_id` AS `feature_id`, `expression_1`.`sample_id` AS `sample_id`, `expression_1`.`value` AS `expression` FROM `database_gzky7400vgpyzy621q43gkkf__molecular_expression1_db`.`expression` AS `expression_1` LEFT OUTER JOIN `database_gzky7400vgpyzy621q43gkkf__molecular_expression1_db`.`expr_annotation` AS `expr_annotation_1` ON `expression_1`.`feature_id` = `expr_annotation_1`.`feature_id` WHERE `expr_annotation_1`.`feature_id` IN ('ENST00000327669', 'ENST00000456328') AND `expression_1`.`value` <= 20 AND `expression_1`.`sample_id` IN ('sample_1') ORDER BY `feature_id` ASC, `sample_id` ASC", + }, +} diff --git a/src/python/test/expression_test_assets/expression_test_input_dict.py b/src/python/test/expression_test_assets/expression_test_input_dict.py new file mode 100644 index 0000000000..57d44c37ef --- /dev/null +++ b/src/python/test/expression_test_assets/expression_test_input_dict.py @@ -0,0 +1,269 @@ +CLIEXPRESS_TEST_INPUT = { + "malformed": { + "location_end_before_start": { + "location": [ + {"chromosome": "1", "starting_position": "200", "ending_position": "1"} + ] + }, + "expression_type": { + "expression": ["shouldbedict"], + "annotation": {"feature_id": ["ENSG0000001", "ENSG00000002"]}, + }, + "location_max_width": { + "location": [ + { + "chromosome": "1", + "starting_position": "1", + "ending_position": "260000000", + }, + { + "chromosome": "X", + "starting_position": "500", + "ending_position": "1700", + }, + ], + "expression": {"min_value": 10.2, "max_value": 10000}, + }, + "bad_toplevel_key": {"not_real_key": "1", "sample_id": ["sample1"]}, + "location_missing_end": { + "location": [{"chromosome": "1", "starting_position": "1"}] + }, + "annotation_name_maxitem": {"annotation": {"feature_name": ["item"] * 101}}, + "conflicting_toplevel": { + "location": [ + { + "chromosome": "1", + "starting_position": "10000", + "ending_position": "20000", + }, + { + "chromosome": "X", + "starting_position": "500", + "ending_position": "1700", + }, + ], + "expression": {"min_value": 10.2, "max_value": 10000}, + "annotation": { + "feature_name": ["BRCA2"], + }, + }, + "location_chrom_type": { + "location": [ + {"chromosome": 1, "starting_position": "1", "ending_position": "200"} + ] + }, + "expression_max_type": { + "expression": {"max_value": "200"}, + "annotation": {"feature_id": ["ENSG0000001", "ENSG00000002"]}, + }, + "expression_empty_dict": { + "expression": {}, + "annotation": {"feature_id": ["ENSG0000001", "ENSG00000002"]}, + }, + "annotation_name_type": { + "annotation": {"feature_name": {"shouldnot": "bedict"}} + }, + "annotation_type": {"annotation": ["list instead of dict"]}, + "location_end_type": { + "location": [ + {"chromosome": "1", "starting_position": "1", "ending_position": 200} + ] + }, + "location_missing_start": { + "location": [{"chromosome": "1", "ending_position": "200"}] + }, + "annotation_conflicting_keys": { + "annotation": { + "feature_name": ["BRCA2"], + "feature_id": ["ENSG0000001", "ENSG00000002"], + } + }, + "sample_id_maxitem": {"sample_id": ["item"] * 101}, + "expression_min_type": { + "expression": {"min_value": "1"}, + "annotation": {"feature_id": ["ENSG0000001", "ENSG00000002"]}, + }, + "location_type": {"location": {"shouldbe": "alist"}}, + "annotation_id_maxitem": {"annotation": {"feature_id": ["item"] * 101}}, + "empty_dict": {}, + "location_missing_chr": { + "location": [{"starting_position": "1", "ending_position": "200"}] + }, + "bad_dependent_conditional": { + "expression": {"min_value": 10.2, "max_value": 10000} + }, + "sample_id_type": {"sample_id": {"shouldbe": "alist"}}, + "annotation_id_type": {"annotation": {"feature_id": {"shouldnot": "bedict"}}}, + "location_start_type": { + "location": [ + {"chromosome": "1", "starting_position": 1, "ending_position": "200"} + ] + }, + "location_item_type": {"location": [["shouldbedict"]]}, + }, + "valid": { + "multi_location": { + "location": [ + {"chromosome": "1", "starting_position": "1", "ending_position": "200"}, + {"chromosome": "2", "starting_position": "1", "ending_position": "500"}, + { + "chromosome": "10", + "starting_position": "1000", + "ending_position": "20000000", + }, + ] + }, + "annotation_feature_id": { + "annotation": {"feature_id": ["ENSG0000001", "ENSG00000002"]} + }, + "expression_min_only": { + "expression": {"min_value": 1}, + "annotation": {"feature_id": ["ENSG0000001", "ENSG00000002"]}, + }, + "expression_min_and_max": { + "expression": {"min_value": 1, "max_value": 200}, + "annotation": {"feature_id": ["ENSG0000001", "ENSG00000002"]}, + }, + "single_location": { + "location": [ + {"chromosome": "1", "starting_position": "1", "ending_position": "200"} + ] + }, + "annotation_feature_name": {"annotation": {"feature_name": ["BRCA2"]}}, + "dependent_conditional_annotation": { + "expression": {"min_value": 10.2, "max_value": 10000}, + "annotation": {"feature_name": ["BRCA2"]}, + }, + "dependent_conditional_location": { + "location": [ + { + "chromosome": "1", + "starting_position": "10000", + "ending_position": "20000", + }, + { + "chromosome": "X", + "starting_position": "500", + "ending_position": "1700", + }, + ], + "expression": {"min_value": 10.2, "max_value": 10000}, + }, + "expression_max_only": { + "expression": {"max_value": 200}, + "annotation": {"feature_id": ["ENSG0000001", "ENSG00000002"]}, + }, + }, +} + +VIZPAYLOADERBUILDER_TEST_INPUT = { + "test_vizpayloadbuilder_location_cohort": { + "location": [ + { + "chromosome": "1", + "starting_position": "10000", + "ending_position": "12000", + }, + ], + }, + "test_vizpayloadbuilder_location_multiple": { + "location": [ + { + "chromosome": "1", + "starting_position": "10000", + "ending_position": "12000", + }, + { + "chromosome": "2", + "starting_position": "30000", + "ending_position": "40000", + }, + ], + }, + "test_vizpayloadbuilder_annotation_feature_name": { + "annotation": {"feature_name": ["ABL1"]} + }, + "test_vizpayloadbuilder_annotation_feature_id": { + "annotation": {"feature_id": ["ENST00000327669", "ENST00000456328"]} + }, + "test_vizpayloadbuilder_expression_min": { + "expression": {"min_value": 70}, + "annotation": {"feature_id": ["ENST00000327669", "ENST00000456328"]}, + }, + "test_vizpayloadbuilder_expression_max": { + "expression": {"max_value": 10}, + "annotation": {"feature_name": ["BRCA2"]}, + }, + "test_vizpayloadbuilder_expression_mixed": { + "expression": {"min_value": 30, "max_value": 60}, + "location": [ + { + "chromosome": "1", + "starting_position": "10000", + "ending_position": "12000", + }, + ], + }, + "test_vizpayloadbuilder_sample": { + "sample_id": ["sample_1"], + }, + "test_vizpayloadbuilder_location_sample_expression": { + "location": [ + { + "chromosome": "1", + "starting_position": "10000", + "ending_position": "20000", + }, + ], + "sample_id": ["sample_1"], + "expression": {"min_value": 25, "max_value": 80}, + }, + "test_vizpayloadbuilder_annotation_sample_expression": { + "annotation": {"feature_id": ["ENST00000327669", "ENST00000456328"]}, + "expression": {"max_value": 20}, + "sample_id": ["sample_1"], + }, +} + +EXPRESSION_CLI_JSON_FILTERS = { + "positive_test": { + "location_expression_sample": { + "location": [ + { + "chromosome": "11", + "starting_position": "8693350", + "ending_position": "67440200", + }, + { + "chromosome": "X", + "starting_position": "148500700", + "ending_position": "148994424", + }, + { + "chromosome": "17", + "starting_position": "75228160", + "ending_position": "75235759", + }, + ], + "expression": {"min_value": 25.63}, + "sample_id": ["sample_1", "sample_2"], + }, + "sample_id_with_additional_fields": {"sample_id": ["sample_1"]}, + }, + "negative_test": { + "empty_json": {}, + "large_location_range": { + "location": [ + { + "chromosome": "1", + "starting_position": "1", + "ending_position": "950000000", + } + ], + "expression": {"min_value": 9.1, "max_value": 50}, + }, + "sample_id_maxitem_limit": { + "sample_id": ["sample_" + str(i) for i in range(1, 200)] + }, + }, +} diff --git a/src/python/test/extract_dataset_test_files/fields_file.txt b/src/python/test/extract_dataset_test_files/fields_file.txt new file mode 100644 index 0000000000..bd5823f5f7 --- /dev/null +++ b/src/python/test/extract_dataset_test_files/fields_file.txt @@ -0,0 +1,10 @@ +patient.patient_id +patient.name +patient.weight +patient.date_of_birth +patient.verified_dtm +test.test_id +trial_visit.visit_id +baseline.baseline_id +hospital.hospital_id +doctor.doctor_id \ No newline at end of file diff --git a/src/python/test/file_load/basic/dxapp.json b/src/python/test/file_load/basic/dxapp.json index e207ffda6f..7562130fdb 100644 --- a/src/python/test/file_load/basic/dxapp.json +++ b/src/python/test/file_load/basic/dxapp.json @@ -29,28 +29,21 @@ "aws:us-east-1": { "systemRequirements": { "*": { - "instanceType": "mem2_ssd1_x2" + "instanceType": "mem2_ssd1_v2_x2" } } }, "aws:ap-southeast-2": { "systemRequirements": { "*": { - "instanceType": "mem2_ssd1_x2" - } - } - }, - "aws:cn-north-1": { - "systemRequirements": { - "*": { - "instanceType": "mem2_ssd1_x2" + "instanceType": "mem2_ssd1_v2_x2" } } }, "aws:eu-central-1": { "systemRequirements": { "*": { - "instanceType": "mem2_ssd1_x2" + "instanceType": "mem2_ssd1_v2_x2" } } }, diff --git a/src/python/test/file_load/basic_except/dxapp.json b/src/python/test/file_load/basic_except/dxapp.json index 5944ffc373..985594bc0e 100644 --- a/src/python/test/file_load/basic_except/dxapp.json +++ b/src/python/test/file_load/basic_except/dxapp.json @@ -26,28 +26,21 @@ "aws:us-east-1": { "systemRequirements": { "*": { - "instanceType": "mem2_ssd1_x2" + "instanceType": "mem2_ssd1_v2_x2" } } }, "aws:ap-southeast-2": { "systemRequirements": { "*": { - "instanceType": "mem2_ssd1_x2" - } - } - }, - "aws:cn-north-1": { - "systemRequirements": { - "*": { - "instanceType": "mem2_ssd1_x2" + "instanceType": "mem2_ssd1_v2_x2" } } }, "aws:eu-central-1": { "systemRequirements": { "*": { - "instanceType": "mem2_ssd1_x2" + "instanceType": "mem2_ssd1_v2_x2" } } }, diff --git a/src/python/test/file_load/benchmark/dxapp.json b/src/python/test/file_load/benchmark/dxapp.json index 516cb40a4d..0b57c71784 100644 --- a/src/python/test/file_load/benchmark/dxapp.json +++ b/src/python/test/file_load/benchmark/dxapp.json @@ -22,28 +22,21 @@ "aws:us-east-1": { "systemRequirements": { "*": { - "instanceType": "mem2_ssd1_x2" + "instanceType": "mem2_ssd1_v2_x2" } } }, "aws:ap-southeast-2": { "systemRequirements": { "*": { - "instanceType": "mem2_ssd1_x2" - } - } - }, - "aws:cn-north-1": { - "systemRequirements": { - "*": { - "instanceType": "mem2_ssd1_x2" + "instanceType": "mem2_ssd1_v2_x2" } } }, "aws:eu-central-1": { "systemRequirements": { "*": { - "instanceType": "mem2_ssd1_x2" + "instanceType": "mem2_ssd1_v2_x2" } } }, diff --git a/src/python/test/file_load/deepdirs/dxapp.json b/src/python/test/file_load/deepdirs/dxapp.json index 798cdd9e09..9becd8e436 100644 --- a/src/python/test/file_load/deepdirs/dxapp.json +++ b/src/python/test/file_load/deepdirs/dxapp.json @@ -24,28 +24,21 @@ "aws:us-east-1": { "systemRequirements": { "*": { - "instanceType": "mem2_ssd1_x2" + "instanceType": "mem2_ssd1_v2_x2" } } }, "aws:ap-southeast-2": { "systemRequirements": { "*": { - "instanceType": "mem2_ssd1_x2" - } - } - }, - "aws:cn-north-1": { - "systemRequirements": { - "*": { - "instanceType": "mem2_ssd1_x2" + "instanceType": "mem2_ssd1_v2_x2" } } }, "aws:eu-central-1": { "systemRequirements": { "*": { - "instanceType": "mem2_ssd1_x2" + "instanceType": "mem2_ssd1_v2_x2" } } }, diff --git a/src/python/test/file_load/file_optional/dxapp.json b/src/python/test/file_load/file_optional/dxapp.json index f996050063..e8c635dd32 100644 --- a/src/python/test/file_load/file_optional/dxapp.json +++ b/src/python/test/file_load/file_optional/dxapp.json @@ -22,28 +22,21 @@ "aws:us-east-1": { "systemRequirements": { "*": { - "instanceType": "mem2_ssd1_x2" + "instanceType": "mem2_ssd1_v2_x2" } } }, "aws:ap-southeast-2": { "systemRequirements": { "*": { - "instanceType": "mem2_ssd1_x2" - } - } - }, - "aws:cn-north-1": { - "systemRequirements": { - "*": { - "instanceType": "mem2_ssd1_x2" + "instanceType": "mem2_ssd1_v2_x2" } } }, "aws:eu-central-1": { "systemRequirements": { "*": { - "instanceType": "mem2_ssd1_x2" + "instanceType": "mem2_ssd1_v2_x2" } } }, diff --git a/src/python/test/file_load/parseq/dxapp.json b/src/python/test/file_load/parseq/dxapp.json index 7d55ed6101..b218674201 100644 --- a/src/python/test/file_load/parseq/dxapp.json +++ b/src/python/test/file_load/parseq/dxapp.json @@ -26,28 +26,21 @@ "aws:us-east-1": { "systemRequirements": { "*": { - "instanceType": "mem2_ssd1_x2" + "instanceType": "mem2_ssd1_v2_x2" } } }, "aws:ap-southeast-2": { "systemRequirements": { "*": { - "instanceType": "mem2_ssd1_x2" - } - } - }, - "aws:cn-north-1": { - "systemRequirements": { - "*": { - "instanceType": "mem2_ssd1_x2" + "instanceType": "mem2_ssd1_v2_x2" } } }, "aws:eu-central-1": { "systemRequirements": { "*": { - "instanceType": "mem2_ssd1_x2" + "instanceType": "mem2_ssd1_v2_x2" } } }, diff --git a/src/python/test/file_load/prefix_patterns/dxapp.json b/src/python/test/file_load/prefix_patterns/dxapp.json index df4392c36f..63524d6705 100644 --- a/src/python/test/file_load/prefix_patterns/dxapp.json +++ b/src/python/test/file_load/prefix_patterns/dxapp.json @@ -57,28 +57,21 @@ "aws:us-east-1": { "systemRequirements": { "*": { - "instanceType": "mem2_ssd1_x2" + "instanceType": "mem2_ssd1_v2_x2" } } }, "aws:ap-southeast-2": { "systemRequirements": { "*": { - "instanceType": "mem2_ssd1_x2" - } - } - }, - "aws:cn-north-1": { - "systemRequirements": { - "*": { - "instanceType": "mem2_ssd1_x2" + "instanceType": "mem2_ssd1_v2_x2" } } }, "aws:eu-central-1": { "systemRequirements": { "*": { - "instanceType": "mem2_ssd1_x2" + "instanceType": "mem2_ssd1_v2_x2" } } }, diff --git a/src/python/test/file_load/vars/dxapp.json b/src/python/test/file_load/vars/dxapp.json index ef2a3ed057..2190fac78a 100644 --- a/src/python/test/file_load/vars/dxapp.json +++ b/src/python/test/file_load/vars/dxapp.json @@ -32,28 +32,21 @@ "aws:us-east-1": { "systemRequirements": { "*": { - "instanceType": "mem2_ssd1_x2" + "instanceType": "mem2_ssd1_v2_x2" } } }, "aws:ap-southeast-2": { "systemRequirements": { "*": { - "instanceType": "mem2_ssd1_x2" - } - } - }, - "aws:cn-north-1": { - "systemRequirements": { - "*": { - "instanceType": "mem2_ssd1_x2" + "instanceType": "mem2_ssd1_v2_x2" } } }, "aws:eu-central-1": { "systemRequirements": { "*": { - "instanceType": "mem2_ssd1_x2" + "instanceType": "mem2_ssd1_v2_x2" } } }, diff --git a/src/python/test/file_load/with-subjobs/dxapp.json b/src/python/test/file_load/with-subjobs/dxapp.json index 1ab1b9daf1..a3b6e9b9d0 100644 --- a/src/python/test/file_load/with-subjobs/dxapp.json +++ b/src/python/test/file_load/with-subjobs/dxapp.json @@ -39,28 +39,21 @@ "aws:us-east-1": { "systemRequirements": { "*": { - "instanceType": "mem2_ssd1_x2" + "instanceType": "mem2_ssd1_v2_x2" } } }, "aws:ap-southeast-2": { "systemRequirements": { "*": { - "instanceType": "mem2_ssd1_x2" - } - } - }, - "aws:cn-north-1": { - "systemRequirements": { - "*": { - "instanceType": "mem2_ssd1_x2" + "instanceType": "mem2_ssd1_v2_x2" } } }, "aws:eu-central-1": { "systemRequirements": { "*": { - "instanceType": "mem2_ssd1_x2" + "instanceType": "mem2_ssd1_v2_x2" } } }, diff --git a/src/python/test/file_load/xattr_properties/dxapp.json b/src/python/test/file_load/xattr_properties/dxapp.json index dadebc9ad5..358236d63a 100644 --- a/src/python/test/file_load/xattr_properties/dxapp.json +++ b/src/python/test/file_load/xattr_properties/dxapp.json @@ -27,28 +27,21 @@ "aws:us-east-1": { "systemRequirements": { "*": { - "instanceType": "mem2_ssd1_x2" + "instanceType": "mem2_ssd1_v2_x2" } } }, "aws:ap-southeast-2": { "systemRequirements": { "*": { - "instanceType": "mem2_ssd1_x2" - } - } - }, - "aws:cn-north-1": { - "systemRequirements": { - "*": { - "instanceType": "mem2_ssd1_x2" + "instanceType": "mem2_ssd1_v2_x2" } } }, "aws:eu-central-1": { "systemRequirements": { "*": { - "instanceType": "mem2_ssd1_x2" + "instanceType": "mem2_ssd1_v2_x2" } } }, diff --git a/src/python/test/file_mount/basic/dxapp.json b/src/python/test/file_mount/basic/dxapp.json index fac492ec6c..f94ea09dee 100644 --- a/src/python/test/file_mount/basic/dxapp.json +++ b/src/python/test/file_mount/basic/dxapp.json @@ -5,8 +5,8 @@ "file": "run.sh", "interpreter": "bash", "distribution": "Ubuntu", - "release": "16.04", - "version": "1" + "release": "20.04", + "version": "0" }, "inputSpec": [ {"name": "seq1", "class": "file"}, @@ -17,5 +17,35 @@ ], "access": { "network": ["*"] + }, + "regionalOptions": { + "aws:us-east-1": { + "systemRequirements": { + "*": { + "instanceType": "mem2_ssd1_v2_x2" + } + } + }, + "aws:ap-southeast-2": { + "systemRequirements": { + "*": { + "instanceType": "mem2_ssd1_v2_x2" + } + } + }, + "aws:eu-central-1": { + "systemRequirements": { + "*": { + "instanceType": "mem2_ssd1_v2_x2" + } + } + }, + "azure:westus": { + "systemRequirements": { + "*": { + "instanceType": "azure:mem2_ssd1_x2" + } + } + } } } diff --git a/src/python/test/help_messages/extract_expression_help_message.txt b/src/python/test/help_messages/extract_expression_help_message.txt new file mode 100644 index 0000000000..cade358d20 --- /dev/null +++ b/src/python/test/help_messages/extract_expression_help_message.txt @@ -0,0 +1,102 @@ +usage: dx extract_assay expression [-h] [--list-assays] + [--retrieve-expression] + [--additional-fields-help] + [--assay-name ASSAY_NAME] + [--filter-json FILTER_JSON] + [--filter-json-file FILTER_JSON_FILE] + [--json-help] [--sql] + [--additional-fields ADDITIONAL_FIELDS [ADDITIONAL_FIELDS ...]] + [--expression-matrix] [--delim DELIM] + [--output OUTPUT] + [path] + +Retrieve the selected data or generate SQL to retrieve the data from a +molecular expression assay in a dataset or cohort based on provided rules. + +positional arguments: + path v3.0 Dataset or Cohort object ID, project-id:record- + id, where ":record-id" indicates the record-id in + current selected project, or name + +optional arguments: + -h, --help show this help message and exit + --list-assays List molecular expression assays available for query + in the specified Dataset or Cohort object + --retrieve-expression + A flag to support, specifying criteria of molecular + expression to retrieve. Retrieves rows from the + expression table, optionally extended with sample and + annotation information where the extension is inline + without affecting row count. By default returns the + following set of fields; "sample_id", "feature_id", + and "value". Additional fields may be returned using " + --additional-fields". Must be used with either "-- + filter-json" or "--filter-json-file". Specify "--json- + help" following this option to get detailed + information on the json format and filters. When + filtering, one, and only one of "location", + "annotation.feature_id", or "annotation.feature_name" + may be supplied. If a Cohort object is supplied, + returned samples will be initially filtered to match + the cohort-defined set of samples, and any additional + filters will only further refine the cohort-defined + set. + --additional-fields-help + List all fields available for output. + --assay-name ASSAY_NAME + Specify a specific molecular expression assay to + query. If the argument is not specified, the default + assay used is the first assay listed when using the + argument, "--list-assays" + --filter-json FILTER_JSON, -j FILTER_JSON + The full input JSON object as a string and + corresponding to "--retrieve-expression". Must be used + with "--retrieve-expression" flag. Either "--filter- + json" or "--filter-json-file" may be supplied, not + both. + --filter-json-file FILTER_JSON_FILE, -f FILTER_JSON_FILE + The full input JSON object as a file and corresponding + to "--retrieve-expression". Must be used with "-- + retrieve-expression" flag. Either "--filter-json" or " + --filter-json-file" may be supplied, not both. + --json-help When set, return a json template of "--retrieve- + expression" and a list of filters with definitions. + --sql If the flag is provided, a SQL statement (as a string) + will be returned for the user to further query the + specified data, instead of returning actual data + values. Use of "--sql" is not supported when also + using the flag, --expression-matrix/-em + --additional-fields ADDITIONAL_FIELDS [ADDITIONAL_FIELDS ...] + A set of fields to return, in addition to the default + set; "sample_id", "feature_id", and "value". Fields + must be represented as field names and supplied as a + single string, where each field name is separated by a + single comma. For example, fieldA,fieldB,fieldC. Use " + --additional-fields-help" to get the full list of + output fields available. + --expression-matrix, -em + If the flag is provided with "--retrieve-expression", + the returned data will be a matrix of sample IDs + (rows) by feature IDs (columns), where each cell is + the respective pairwise value. The flag is not + compatible with "--additional-fields". Additionally, + the flag is not compatible with an "expression" + filter. If the underlying expression value is missing, + the value will be empty in returned data. Use of + --expression-matrix/-em is not supported when also + using the flag, "--sql". + --delim DELIM, --delimiter DELIM + Always use exactly one of DELIMITER to separate fields + to be printed; if no delimiter is provided with this + flag, COMMA will be used. If a file is specified and + no --delim argument is passed or is COMMA, the file + suffix will be ".csv". If a file is specified and the + --delim argument is TAB, the file suffix will be + ".tsv". Otherwise, if a file is specified and "-- + delim" is neither COMMA or TAB file suffix will be + ".txt". + --output OUTPUT, -o OUTPUT + A local filename to be used, where "-" indicates + printing to STDOUT. If -o/--output is not supplied, + default behavior is to create a file with a + constructed name in the current folder. diff --git a/src/python/test/jwt/README.md b/src/python/test/jwt/README.md new file mode 100644 index 0000000000..6d6eb02bdf --- /dev/null +++ b/src/python/test/jwt/README.md @@ -0,0 +1,2 @@ +This is a testing app for the dx-jobutil-get-identity-token script. + diff --git a/src/python/test/jwt/dxapp.json b/src/python/test/jwt/dxapp.json new file mode 100644 index 0000000000..764176acf7 --- /dev/null +++ b/src/python/test/jwt/dxapp.json @@ -0,0 +1,51 @@ +{ "name": "basic jwt retrieval", + "title": "basic jwt retrieval", + "summary" : "basic jwt retrieval", + "runSpec": { + "file": "run.sh", + "interpreter": "bash", + "distribution": "Ubuntu", + "release": "20.04", + "version": "0" + }, + "inputSpec": [ + {"name": "audience", "class": "string"}, + {"name": "subject_claims", "class": "array:string", "optional" : true} + ], + "outputSpec": [ + {"name": "token","class": "string"} + ], + "access": { + "network": ["*"] + }, + "regionalOptions": { + "aws:us-east-1": { + "systemRequirements": { + "*": { + "instanceType": "mem2_ssd1_v2_x2" + } + } + }, + "aws:ap-southeast-2": { + "systemRequirements": { + "*": { + "instanceType": "mem2_ssd1_v2_x2" + } + } + }, + "aws:eu-central-1": { + "systemRequirements": { + "*": { + "instanceType": "mem2_ssd1_v2_x2" + } + } + }, + "azure:westus": { + "systemRequirements": { + "*": { + "instanceType": "azure:mem2_ssd1_x2" + } + } + } + } +} diff --git a/src/python/test/jwt/run.sh b/src/python/test/jwt/run.sh new file mode 100644 index 0000000000..d831fb7dfa --- /dev/null +++ b/src/python/test/jwt/run.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +# FOR BRANCH TEST, UNDO LATER +install_dxpy_from_branch() { + dxpy_git_ref='job_id_tokens_debug_subject_claims' + pip3 install --upgrade "git+https://github.com/dnanexus/dx-toolkit.git@${dxpy_git_ref}#egg=dxpy&subdirectory=src/python" + dx --version +} + +main() { + dx-download-all-inputs + + # check if subject_claims is an empty string + if [ -z "$subject_claims" ]; then + token=$(dx-jobutil-get-identity-token --aud "$audience") + else + token=$(dx-jobutil-get-identity-token --aud "$audience" --subject_claims "$subject_claims") + fi + + dx-jobutil-add-output token "$token" +} diff --git a/src/python/test/mock_api/api.py b/src/python/test/mock_api/api.py index e77411b47b..bd88ae7d94 100755 --- a/src/python/test/mock_api/api.py +++ b/src/python/test/mock_api/api.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # coding: utf-8 from __future__ import print_function, unicode_literals diff --git a/src/python/test/nextflow/RetryMaxRetries/main.nf b/src/python/test/nextflow/RetryMaxRetries/main.nf new file mode 100644 index 0000000000..9a848ca887 --- /dev/null +++ b/src/python/test/nextflow/RetryMaxRetries/main.nf @@ -0,0 +1,19 @@ +#!/usr/bin/env nextflow +nextflow.enable.dsl=2 +process sayHello { + errorStrategy 'retry' + maxRetries 2 + maxErrors 20 + input: + val x + output: + stdout + script: + """ + ecsho '$x world!' + """ +} + +workflow { + Channel.of('Bonjour', 'Ciao', 'Hello', 'Hola') | sayHello | view +} diff --git a/src/python/test/nextflow/RetryMaxRetries/nextflow.config b/src/python/test/nextflow/RetryMaxRetries/nextflow.config new file mode 100644 index 0000000000..b90e8610ae --- /dev/null +++ b/src/python/test/nextflow/RetryMaxRetries/nextflow.config @@ -0,0 +1 @@ +process.container = 'quay.io/nextflow/bash' diff --git a/src/python/test/nextflow/__init__.py b/src/python/test/nextflow/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/python/test/nextflow/hello/main.nf b/src/python/test/nextflow/hello/main.nf new file mode 100644 index 0000000000..7dd30888f5 --- /dev/null +++ b/src/python/test/nextflow/hello/main.nf @@ -0,0 +1,17 @@ +#!/usr/bin/env nextflow +nextflow.enable.dsl=2 +params.input = "default_input" +process sayHello { + input: + val x + output: + stdout + script: + """ + echo '$x world!' + """ +} + +workflow { + Channel.of('Bonjour', 'Ciao', 'Hello', 'Hola', params.input) | sayHello | view +} diff --git a/src/python/test/nextflow/hello/nextflow.config b/src/python/test/nextflow/hello/nextflow.config new file mode 100644 index 0000000000..b90e8610ae --- /dev/null +++ b/src/python/test/nextflow/hello/nextflow.config @@ -0,0 +1 @@ +process.container = 'quay.io/nextflow/bash' diff --git a/src/python/test/nextflow/print_env_nextflow_soft_confs/first.config b/src/python/test/nextflow/print_env_nextflow_soft_confs/first.config new file mode 100644 index 0000000000..daa82089eb --- /dev/null +++ b/src/python/test/nextflow/print_env_nextflow_soft_confs/first.config @@ -0,0 +1,2 @@ +env.ALPHA = 'runtime alpha 1' +env.BETA = 'runtime beta 1' \ No newline at end of file diff --git a/src/python/test/nextflow/print_env_nextflow_soft_confs/main.nf b/src/python/test/nextflow/print_env_nextflow_soft_confs/main.nf new file mode 100644 index 0000000000..478bb52cb8 --- /dev/null +++ b/src/python/test/nextflow/print_env_nextflow_soft_confs/main.nf @@ -0,0 +1,15 @@ +#!/usr/bin/env nextflow +nextflow.enable.dsl=2 +process printEnv { + output: + stdout + script: + """ + echo The env var ALPHA is: $ALPHA + echo The env var BETA is: $BETA + """ +} + +workflow { + printEnv | view +} diff --git a/src/python/test/nextflow/print_env_nextflow_soft_confs/nextflow.config b/src/python/test/nextflow/print_env_nextflow_soft_confs/nextflow.config new file mode 100644 index 0000000000..0371d0d1a3 --- /dev/null +++ b/src/python/test/nextflow/print_env_nextflow_soft_confs/nextflow.config @@ -0,0 +1,4 @@ +env { + ALPHA = 'default alpha' + BETA = 'default beta' +} \ No newline at end of file diff --git a/src/python/test/nextflow/print_env_nextflow_soft_confs/second.config b/src/python/test/nextflow/print_env_nextflow_soft_confs/second.config new file mode 100644 index 0000000000..47919a6ddd --- /dev/null +++ b/src/python/test/nextflow/print_env_nextflow_soft_confs/second.config @@ -0,0 +1,2 @@ +env.ALPHA = 'runtime alpha 2' + diff --git a/src/python/test/nextflow/print_param_nextflow_params_file/main.nf b/src/python/test/nextflow/print_param_nextflow_params_file/main.nf new file mode 100644 index 0000000000..ba76728ba8 --- /dev/null +++ b/src/python/test/nextflow/print_param_nextflow_params_file/main.nf @@ -0,0 +1,20 @@ +#!/usr/bin/env nextflow +nextflow.enable.dsl=2 +params.ALPHA = 'default alpha' +params.BETA = 'default beta' +process printParams { + input: + val alpha + val beta + output: + stdout + script: + """ + echo The parameter ALPHA is: $alpha + echo The parameter BETA is: $beta + """ +} + +workflow { + printParams(params.ALPHA, params.BETA) | view +} diff --git a/src/python/test/nextflow/print_param_nextflow_params_file/params_file.yml b/src/python/test/nextflow/print_param_nextflow_params_file/params_file.yml new file mode 100644 index 0000000000..25cffcff52 --- /dev/null +++ b/src/python/test/nextflow/print_param_nextflow_params_file/params_file.yml @@ -0,0 +1,2 @@ +ALPHA: 'param file alpha' +BETA: 'param file beta' \ No newline at end of file diff --git a/src/python/test/nextflow/profile/conf/second.config b/src/python/test/nextflow/profile/conf/second.config new file mode 100644 index 0000000000..f7eff1c1e7 --- /dev/null +++ b/src/python/test/nextflow/profile/conf/second.config @@ -0,0 +1,6 @@ + + +params { + // Input data + input = 'second_config' +} diff --git a/src/python/test/nextflow/profile/conf/test.config b/src/python/test/nextflow/profile/conf/test.config new file mode 100644 index 0000000000..9cdeaa0b72 --- /dev/null +++ b/src/python/test/nextflow/profile/conf/test.config @@ -0,0 +1,7 @@ + + +params { + // Input data + input = 'test_config' + +} diff --git a/src/python/test/nextflow/profile/main.nf b/src/python/test/nextflow/profile/main.nf new file mode 100644 index 0000000000..a473ffc0ee --- /dev/null +++ b/src/python/test/nextflow/profile/main.nf @@ -0,0 +1,17 @@ +#!/usr/bin/env nextflow +nextflow.enable.dsl=2 + +process sayHello { + input: + val x + output: + stdout + script: + """ + echo '$x world!' + """ +} + +workflow { + Channel.of(params.input, 'Ciao', 'Hello', 'Hola') | sayHello | view +} \ No newline at end of file diff --git a/src/python/test/nextflow/profile/nextflow.config b/src/python/test/nextflow/profile/nextflow.config new file mode 100644 index 0000000000..f3e5076649 --- /dev/null +++ b/src/python/test/nextflow/profile/nextflow.config @@ -0,0 +1,7 @@ +process.container = 'quay.io/nextflow/bash' + + +profiles { + test { includeConfig 'conf/test.config' } + second { includeConfig 'conf/second.config' } +} \ No newline at end of file diff --git a/src/python/test/nextflow/profile_with_docker/conf/second.config b/src/python/test/nextflow/profile_with_docker/conf/second.config new file mode 100644 index 0000000000..f7eff1c1e7 --- /dev/null +++ b/src/python/test/nextflow/profile_with_docker/conf/second.config @@ -0,0 +1,6 @@ + + +params { + // Input data + input = 'second_config' +} diff --git a/src/python/test/nextflow/profile_with_docker/conf/test.config b/src/python/test/nextflow/profile_with_docker/conf/test.config new file mode 100644 index 0000000000..9cdeaa0b72 --- /dev/null +++ b/src/python/test/nextflow/profile_with_docker/conf/test.config @@ -0,0 +1,7 @@ + + +params { + // Input data + input = 'test_config' + +} diff --git a/src/python/test/nextflow/profile_with_docker/main.nf b/src/python/test/nextflow/profile_with_docker/main.nf new file mode 100644 index 0000000000..a473ffc0ee --- /dev/null +++ b/src/python/test/nextflow/profile_with_docker/main.nf @@ -0,0 +1,17 @@ +#!/usr/bin/env nextflow +nextflow.enable.dsl=2 + +process sayHello { + input: + val x + output: + stdout + script: + """ + echo '$x world!' + """ +} + +workflow { + Channel.of(params.input, 'Ciao', 'Hello', 'Hola') | sayHello | view +} \ No newline at end of file diff --git a/src/python/test/nextflow/profile_with_docker/nextflow.config b/src/python/test/nextflow/profile_with_docker/nextflow.config new file mode 100644 index 0000000000..012088c201 --- /dev/null +++ b/src/python/test/nextflow/profile_with_docker/nextflow.config @@ -0,0 +1,8 @@ +process.container = 'quay.io/nextflow/bash' +docker.enabled = true + + +profiles { + test { includeConfig 'conf/test.config' } + second { includeConfig 'conf/second.config' } +} \ No newline at end of file diff --git a/src/python/test/nextflow/publishDir/main.nf b/src/python/test/nextflow/publishDir/main.nf new file mode 100644 index 0000000000..689f00fd5e --- /dev/null +++ b/src/python/test/nextflow/publishDir/main.nf @@ -0,0 +1,38 @@ +#!/usr/bin/env nextflow +nextflow.enable.dsl=2 + +process catFile { + debug true + errorStrategy 'ignore' + publishDir "$params.outdir/cat_output", mode: 'copy' + input: + path 'in_file' + + output: + path 'cat_file.txt' + script: + """ + cat in_file > cat_file.txt + """ +} + + +process listFolder { + debug true + errorStrategy 'ignore' + publishDir "$params.outdir/ls_output", mode: 'copy' + input: + path 'in_folder' + + output: + path 'ls_folder.txt' + script: + """ + ls in_folder > ls_folder.txt + """ +} + +workflow { + catFile(params.inFile)| view + listFolder(params.inFolder) | view +} \ No newline at end of file diff --git a/src/python/test/nextflow/schema1.json b/src/python/test/nextflow/schema1.json new file mode 100644 index 0000000000..6b42ba68a9 --- /dev/null +++ b/src/python/test/nextflow/schema1.json @@ -0,0 +1,709 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "https://raw.githubusercontent.com/nf-core/rnaseq/master/nextflow_schema.json", + "title": "nf-core/rnaseq pipeline parameters", + "description": "RNA sequencing analysis pipeline for gene/isoform quantification and extensive quality control.", + "type": "object", + "definitions": { + "input_output_options": { + "title": "Input/output options", + "type": "object", + "fa_icon": "fas fa-terminal", + "description": "Define where the pipeline should find input data and save output data.", + "required": ["outdir"], + "properties": { + "input": { + "type": "string", + "format": "file-path", + "mimetype": "text/csv", + "pattern": "^\\S+\\.csv$", + "schema": "assets/schema_input.json", + "description": "Path to comma-separated file containing information about the samples in the experiment.", + "help_text": "You will need to create a design file with information about the samples in your experiment before running the pipeline. Use this parameter to specify its location. It has to be a comma-separated file with 4 columns, and a header row. See [usage docs](https://nf-co.re/rnaseq/usage#samplesheet-input).", + "fa_icon": "fas fa-file-csv" + }, + "outdir": { + "type": "string", + "format": "directory-path", + "description": "The output directory where the results will be saved. You have to use absolute paths to storage on Cloud infrastructure.", + "fa_icon": "fas fa-folder-open" + }, + "email": { + "type": "string", + "description": "Email address for completion summary.", + "fa_icon": "fas fa-envelope", + "help_text": "Set this parameter to your e-mail address to get a summary e-mail with details of the run sent to you when the workflow exits. If set in your user config file (`~/.nextflow/config`) then you don't need to specify this on the command line for every run.", + "pattern": "^([a-zA-Z0-9_\\-\\.]+)@([a-zA-Z0-9_\\-\\.]+)\\.([a-zA-Z]{2,5})$" + }, + "multiqc_title": { + "type": "string", + "description": "MultiQC report title. Printed as page header, used for filename if not otherwise specified.", + "fa_icon": "fas fa-file-signature" + }, + "save_merged_fastq": { + "type": "boolean", + "fa_icon": "fas fa-save", + "description": "Save FastQ files after merging re-sequenced libraries in the results directory." + } + } + }, + "umi_options": { + "title": "UMI options", + "type": "object", + "description": "Options for processing reads with unique molecular identifiers", + "default": "", + "properties": { + "with_umi": { + "type": "boolean", + "fa_icon": "fas fa-barcode", + "description": "Enable UMI-based read deduplication." + }, + "umitools_extract_method": { + "type": "string", + "default": "string", + "fa_icon": "fas fa-barcode", + "description": "UMI pattern to use. Can be either 'string' (default) or 'regex'.", + "help_text": "More details can be found in the [UMI-tools documentation](https://umi-tools.readthedocs.io/en/latest/reference/extract.html#extract-method).\n" + }, + "skip_umi_extract": { + "type": "boolean", + "fa_icon": "fas fa-compress-alt", + "description": "Skip the UMI extraction from the read in case the UMIs have been moved to the headers in advance of the pipeline run." + }, + "umitools_bc_pattern": { + "type": "string", + "fa_icon": "fas fa-barcode", + "help_text": "More details can be found in the [UMI-tools documentation](https://umi-tools.readthedocs.io/en/latest/reference/extract.html#extract-method).", + "description": "The UMI barcode pattern to use e.g. 'NNNNNN' indicates that the first 6 nucleotides of the read are from the UMI." + }, + "umitools_dedup_stats": { + "type": "boolean", + "fa_icon": "fas fa-barcode", + "help_text": "It can be quite time consuming generating these output stats - see [#827](https://github.com/nf-core/rnaseq/issues/827).", + "description": "Generate output stats when running \"umi_tools dedup\"." + }, + "umi_discard_read": { + "type": "integer", + "fa_icon": "fas fa-barcode", + "description": "After UMI barcode extraction discard either R1 or R2 by setting this parameter to 1 or 2, respectively." + }, + "save_umi_intermeds": { + "type": "boolean", + "fa_icon": "fas fa-save", + "description": "If this option is specified, intermediate FastQ and BAM files produced by UMI-tools are also saved in the results directory." + } + }, + "fa_icon": "fas fa-barcode" + }, + "read_filtering_options": { + "title": "Read filtering options", + "type": "object", + "description": "Options for filtering reads prior to alignment", + "default": "", + "properties": { + "bbsplit_fasta_list": { + "type": "string", + "fa_icon": "fas fa-list-alt", + "description": "Path to comma-separated file containing a list of reference genomes to filter reads against with BBSplit. You have to also explicitly set `--skip_bbsplit false` if you want to use BBSplit.", + "help_text": "The file should contain 2 columns: short name and full path to reference genome(s) e.g. \n```\nmm10,/path/to/mm10.fa\necoli,/path/to/ecoli.fa\n```" + }, + "bbsplit_index": { + "type": "string", + "fa_icon": "fas fa-bezier-curve", + "description": "Path to directory or tar.gz archive for pre-built BBSplit index.", + "help_text": "The BBSplit index will have to be built at least once with this pipeline (see `--save_reference` to save index). It can then be provided via `--bbsplit_index` for future runs." + }, + "save_bbsplit_reads": { + "type": "boolean", + "fa_icon": "fas fa-save", + "description": "If this option is specified, FastQ files split by reference will be saved in the results directory." + }, + "skip_bbsplit": { + "type": "boolean", + "default": true, + "fa_icon": "fas fa-fast-forward", + "description": "Skip BBSplit for removal of non-reference genome reads." + }, + "remove_ribo_rna": { + "type": "boolean", + "fa_icon": "fas fa-trash-alt", + "description": "Enable the removal of reads derived from ribosomal RNA using SortMeRNA.", + "help_text": "Any patterns found in the sequences defined by the '--ribo_database_manifest' parameter will be used." + }, + "ribo_database_manifest": { + "type": "string", + "default": "${projectDir}/assets/rrna-db-defaults.txt", + "fa_icon": "fas fa-database", + "description": "Text file containing paths to fasta files (one per line) that will be used to create the database for SortMeRNA.", + "help_text": "By default, [rRNA databases](https://github.com/biocore/sortmerna/tree/master/data/rRNA_databases) defined in the SortMeRNA GitHub repo are used. You can see an example in the pipeline Github repository in `assets/rrna-default-dbs.txt`.\nPlease note that commercial/non-academic entities require [`licensing for SILVA`](https://www.arb-silva.de/silva-license-information) for these default databases." + }, + "save_non_ribo_reads": { + "type": "boolean", + "fa_icon": "fas fa-save", + "description": "If this option is specified, intermediate FastQ files containing non-rRNA reads will be saved in the results directory." + } + }, + "fa_icon": "fas fa-trash-alt" + }, + "reference_genome_options": { + "title": "Reference genome options", + "type": "object", + "fa_icon": "fas fa-dna", + "description": "Reference genome related files and options required for the workflow.", + "properties": { + "genome": { + "type": "string", + "description": "Name of iGenomes reference.", + "fa_icon": "fas fa-book", + "help_text": "If using a reference genome configured in the pipeline using iGenomes, use this parameter to give the ID for the reference. This is then used to build the full paths for all required reference genome files e.g. `--genome GRCh38`. \n\nSee the [nf-core website docs](https://nf-co.re/usage/reference_genomes) for more details." + }, + "fasta": { + "type": "string", + "format": "file-path", + "mimetype": "text/plain", + "pattern": "^\\S+\\.fn?a(sta)?(\\.gz)?$", + "description": "Path to FASTA genome file.", + "help_text": "This parameter is *mandatory* if `--genome` is not specified. If you don't have the appropriate alignment index available this will be generated for you automatically. Combine with `--save_reference` to save alignment index for future runs.", + "fa_icon": "far fa-file-code" + }, + "gtf": { + "type": "string", + "format": "file-path", + "mimetype": "text/plain", + "pattern": "^\\S+\\.gtf(\\.gz)?$", + "description": "Path to GTF annotation file.", + "fa_icon": "fas fa-code-branch", + "help_text": "This parameter is *mandatory* if `--genome` is not specified." + }, + "gff": { + "type": "string", + "format": "file-path", + "mimetype": "text/plain", + "pattern": "^\\S+\\.gff(\\.gz)?$", + "fa_icon": "fas fa-code-branch", + "description": "Path to GFF3 annotation file.", + "help_text": "This parameter must be specified if `--genome` or `--gtf` are not specified." + }, + "gene_bed": { + "type": "string", + "format": "file-path", + "mimetype": "text/plain", + "pattern": "^\\S+\\.bed(\\.gz)?$", + "fa_icon": "fas fa-procedures", + "description": "Path to BED file containing gene intervals. This will be created from the GTF file if not specified." + }, + "transcript_fasta": { + "type": "string", + "format": "file-path", + "mimetype": "text/plain", + "pattern": "^\\S+\\.fn?a(sta)?(\\.gz)?$", + "fa_icon": "far fa-file-code", + "description": "Path to FASTA transcriptome file." + }, + "additional_fasta": { + "type": "string", + "format": "file-path", + "mimetype": "text/plain", + "pattern": "^\\S+\\.fn?a(sta)?(\\.gz)?$", + "fa_icon": "far fa-file-code", + "description": "FASTA file to concatenate to genome FASTA file e.g. containing spike-in sequences.", + "help_text": "If provided, the sequences in this file will get concatenated to the existing genome FASTA file, a GTF file will be automatically created using the entire sequence as the gene, transcript, and exon features, and any alignment index will get created from the combined FASTA and GTF. It is recommended to save the reference with `--save_reference` to re-use the index for future runs so you do not need to create it again." + }, + "splicesites": { + "type": "string", + "format": "file-path", + "mimetype": "text/plain", + "fa_icon": "fas fa-hand-scissors", + "description": "Splice sites file required for HISAT2." + }, + "star_index": { + "type": "string", + "format": "path", + "fa_icon": "fas fa-bezier-curve", + "description": "Path to directory or tar.gz archive for pre-built STAR index." + }, + "hisat2_index": { + "type": "string", + "format": "path", + "fa_icon": "fas fa-bezier-curve", + "description": "Path to directory or tar.gz archive for pre-built HISAT2 index." + }, + "rsem_index": { + "type": "string", + "format": "path", + "fa_icon": "fas fa-bezier-curve", + "description": "Path to directory or tar.gz archive for pre-built RSEM index." + }, + "salmon_index": { + "type": "string", + "format": "path", + "fa_icon": "fas fa-bezier-curve", + "description": "Path to directory or tar.gz archive for pre-built Salmon index." + }, + "hisat2_build_memory": { + "type": "string", + "default": "200.GB", + "fa_icon": "fas fa-memory", + "pattern": "^\\d+(\\.\\d+)?\\.?\\s*(K|M|G|T)?B$", + "description": "Minimum memory required to use splice sites and exons in the HiSAT2 index build process.", + "help_text": "HiSAT2 requires a huge amount of RAM to build a genome index for larger genomes, if including splice sites and exons e.g. the human genome might typically require 200GB. If you specify less than this threshold for the `HISAT2_BUILD` process then the splice sites and exons will be ignored, meaning that the process will require a lot less memory. If you are working with a small genome, set this parameter to a lower value to reduce the threshold for skipping this check. If using a larger genome, consider supplying more memory to the `HISAT2_BUILD` process." + }, + "gencode": { + "type": "boolean", + "fa_icon": "fas fa-code-branch", + "description": "Specify if your GTF annotation is in GENCODE format.", + "help_text": "If your GTF file is in GENCODE format and you would like to run Salmon i.e. `--pseudo_aligner salmon`, you will need to provide this parameter in order to build the Salmon index appropriately." + }, + "gtf_extra_attributes": { + "type": "string", + "default": "gene_name", + "fa_icon": "fas fa-plus-square", + "description": "By default, the pipeline uses the `gene_name` field to obtain additional gene identifiers from the input GTF file when running Salmon.", + "help_text": "This behaviour can be modified by specifying `--gtf_extra_attributes` when running the pipeline. Note that you can also specify more than one desired value, separated by a comma e.g. `--gtf_extra_attributes gene_id,...`.\n" + }, + "gtf_group_features": { + "type": "string", + "default": "gene_id", + "description": "Define the attribute type used to group features in the GTF file when running Salmon.", + "fa_icon": "fas fa-layer-group" + }, + "featurecounts_group_type": { + "type": "string", + "default": "gene_biotype", + "fa_icon": "fas fa-layer-group", + "description": "The attribute type used to group feature types in the GTF file when generating the biotype plot with featureCounts." + }, + "featurecounts_feature_type": { + "type": "string", + "default": "exon", + "description": "By default, the pipeline assigns reads based on the 'exon' attribute within the GTF file.", + "fa_icon": "fas fa-indent", + "help_text": "The feature type used from the GTF file when generating the biotype plot with featureCounts." + }, + "save_reference": { + "type": "boolean", + "description": "If generated by the pipeline save the STAR index in the results directory.", + "help_text": "If an alignment index is generated by the pipeline use this parameter to save it to your results folder. These can then be used for future pipeline runs, reducing processing times.", + "fa_icon": "fas fa-save" + }, + "igenomes_base": { + "type": "string", + "format": "directory-path", + "description": "Directory / URL base for iGenomes references.", + "default": "s3://ngi-igenomes/igenomes", + "fa_icon": "fas fa-cloud-download-alt", + "hidden": true + }, + "igenomes_ignore": { + "type": "boolean", + "description": "Do not load the iGenomes reference config.", + "fa_icon": "fas fa-ban", + "hidden": true, + "help_text": "Do not load `igenomes.config` when running the pipeline. You may choose this option if you observe clashes between custom parameters and those supplied in `igenomes.config`." + } + } + }, + "read_trimming_options": { + "title": "Read trimming options", + "type": "object", + "fa_icon": "fas fa-cut", + "description": "Options to adjust read trimming criteria.", + "properties": { + "clip_r1": { + "type": "integer", + "description": "Instructs Trim Galore to remove bp from the 5' end of read 1 (or single-end reads).", + "fa_icon": "fas fa-cut" + }, + "clip_r2": { + "type": "integer", + "description": "Instructs Trim Galore to remove bp from the 5' end of read 2 (paired-end reads only).", + "fa_icon": "fas fa-cut" + }, + "three_prime_clip_r1": { + "type": "integer", + "description": "Instructs Trim Galore to remove bp from the 3' end of read 1 AFTER adapter/quality trimming has been performed.", + "fa_icon": "fas fa-cut" + }, + "three_prime_clip_r2": { + "type": "integer", + "description": "Instructs Trim Galore to remove bp from the 3' end of read 2 AFTER adapter/quality trimming has been performed.", + "fa_icon": "fas fa-cut" + }, + "trim_nextseq": { + "type": "integer", + "description": "Instructs Trim Galore to apply the --nextseq=X option, to trim based on quality after removing poly-G tails.", + "help_text": "This enables the option Cutadapt `--nextseq-trim=3'CUTOFF` option via Trim Galore, which will set a quality cutoff (that is normally given with -q instead), but qualities of G bases are ignored. This trimming is in common for the NextSeq- and NovaSeq-platforms, where basecalls without any signal are called as high-quality G bases.", + "fa_icon": "fas fa-cut" + }, + "min_trimmed_reads": { + "type": "integer", + "default": 10000, + "fa_icon": "fas fa-hand-paper", + "description": "Minimum number of trimmed reads below which samples are removed from further processing. Some downstream steps in the pipeline will fail if this threshold is too low." + }, + "skip_trimming": { + "type": "boolean", + "description": "Skip the adapter trimming step.", + "help_text": "Use this if your input FastQ files have already been trimmed outside of the workflow or if you're very confident that there is no adapter contamination in your data.", + "fa_icon": "fas fa-fast-forward" + }, + "save_trimmed": { + "type": "boolean", + "description": "Save the trimmed FastQ files in the results directory.", + "help_text": "By default, trimmed FastQ files will not be saved to the results directory. Specify this flag (or set to true in your config file) to copy these files to the results directory when complete.", + "fa_icon": "fas fa-save" + } + } + }, + "alignment_options": { + "title": "Alignment options", + "type": "object", + "fa_icon": "fas fa-map-signs", + "description": "Options to adjust parameters and filtering criteria for read alignments.", + "properties": { + "aligner": { + "type": "string", + "default": "star_salmon", + "description": "Specifies the alignment algorithm to use - available options are 'star_salmon', 'star_rsem' and 'hisat2'.", + "fa_icon": "fas fa-map-signs", + "enum": ["star_salmon", "star_rsem", "hisat2"] + }, + "pseudo_aligner": { + "type": "string", + "description": "Specifies the pseudo aligner to use - available options are 'salmon'. Runs in addition to '--aligner'.", + "fa_icon": "fas fa-hamburger", + "enum": ["salmon"] + }, + "bam_csi_index": { + "type": "boolean", + "description": "Create a CSI index for BAM files instead of the traditional BAI index. This will be required for genomes with larger chromosome sizes.", + "fa_icon": "fas fa-sort-alpha-down" + }, + "star_ignore_sjdbgtf": { + "type": "boolean", + "fa_icon": "fas fa-ban", + "description": "When using pre-built STAR indices do not re-extract and use splice junctions from the GTF file." + }, + "salmon_quant_libtype": { + "type": "string", + "fa_icon": "fas fa-fast-forward", + "description": " Override Salmon library type inferred based on strandedness defined in meta object.", + "help_text": "See [Salmon docs](https://salmon.readthedocs.io/en/latest/library_type.html)." + }, + "min_mapped_reads": { + "type": "number", + "default": 5, + "fa_icon": "fas fa-percentage", + "description": "Minimum percentage of uniquely mapped reads below which samples are removed from further processing.", + "help_text": "Some downstream steps in the pipeline will fail if this threshold is too low." + }, + "seq_center": { + "type": "string", + "description": "Sequencing center information to be added to read group of BAM files.", + "fa_icon": "fas fa-synagogue" + }, + "stringtie_ignore_gtf": { + "type": "boolean", + "description": "Perform reference-guided de novo assembly of transcripts using StringTie i.e. dont restrict to those in GTF file.", + "fa_icon": "fas fa-ban" + }, + "save_unaligned": { + "type": "boolean", + "fa_icon": "fas fa-save", + "description": "Where possible, save unaligned reads from either STAR, HISAT2 or Salmon to the results directory.", + "help_text": "This may either be in the form of FastQ or BAM files depending on the options available for that particular tool." + }, + "save_align_intermeds": { + "type": "boolean", + "description": "Save the intermediate BAM files from the alignment step.", + "help_text": "By default, intermediate BAM files will not be saved. The final BAM files created after the appropriate filtering step are always saved to limit storage usage. Set this parameter to also save other intermediate BAM files.", + "fa_icon": "fas fa-save" + }, + "skip_markduplicates": { + "type": "boolean", + "fa_icon": "fas fa-fast-forward", + "description": "Skip picard MarkDuplicates step." + }, + "skip_alignment": { + "type": "boolean", + "fa_icon": "fas fa-fast-forward", + "description": "Skip all of the alignment-based processes within the pipeline." + } + } + }, + "process_skipping_options": { + "title": "Process skipping options", + "type": "object", + "fa_icon": "fas fa-fast-forward", + "description": "Options to skip various steps within the workflow.", + "properties": { + "rseqc_modules": { + "type": "string", + "default": "bam_stat,inner_distance,infer_experiment,junction_annotation,junction_saturation,read_distribution,read_duplication", + "fa_icon": "fas fa-chart-pie", + "description": "Specify the RSeQC modules to run." + }, + "deseq2_vst": { + "type": "boolean", + "description": "Use vst transformation instead of rlog with DESeq2.", + "help_text": "See [DESeq2 docs](http://bioconductor.org/packages/devel/bioc/vignettes/DESeq2/inst/doc/DESeq2.html#data-transformations-and-visualization).", + "fa_icon": "fas fa-dolly" + }, + "skip_bigwig": { + "type": "boolean", + "fa_icon": "fas fa-fast-forward", + "description": "Skip bigWig file creation." + }, + "skip_stringtie": { + "type": "boolean", + "fa_icon": "fas fa-fast-forward", + "description": "Skip StringTie." + }, + "skip_fastqc": { + "type": "boolean", + "description": "Skip FastQC.", + "fa_icon": "fas fa-fast-forward" + }, + "skip_preseq": { + "type": "boolean", + "description": "Skip Preseq.", + "fa_icon": "fas fa-fast-forward" + }, + "skip_dupradar": { + "type": "boolean", + "fa_icon": "fas fa-fast-forward", + "description": "Skip dupRadar." + }, + "skip_qualimap": { + "type": "boolean", + "fa_icon": "fas fa-fast-forward", + "description": "Skip Qualimap." + }, + "skip_rseqc": { + "type": "boolean", + "fa_icon": "fas fa-fast-forward", + "description": "Skip RSeQC." + }, + "skip_biotype_qc": { + "type": "boolean", + "fa_icon": "fas fa-fast-forward", + "description": "Skip additional featureCounts process for biotype QC." + }, + "skip_deseq2_qc": { + "type": "boolean", + "fa_icon": "fas fa-fast-forward", + "description": "Skip DESeq2 PCA and heatmap plotting." + }, + "skip_multiqc": { + "type": "boolean", + "description": "Skip MultiQC.", + "fa_icon": "fas fa-fast-forward" + }, + "skip_qc": { + "type": "boolean", + "fa_icon": "fas fa-fast-forward", + "description": "Skip all QC steps except for MultiQC." + } + } + }, + "institutional_config_options": { + "title": "Institutional config options", + "type": "object", + "fa_icon": "fas fa-university", + "description": "Parameters used to describe centralised config profiles. These should not be edited.", + "help_text": "The centralised nf-core configuration profiles use a handful of pipeline parameters to describe themselves. This information is then printed to the Nextflow log when you run a pipeline. You should not need to change these values when you run a pipeline.", + "properties": { + "custom_config_version": { + "type": "string", + "description": "Git commit id for Institutional configs.", + "default": "master", + "hidden": true, + "fa_icon": "fas fa-users-cog" + }, + "custom_config_base": { + "type": "string", + "description": "Base directory for Institutional configs.", + "default": "https://raw.githubusercontent.com/nf-core/configs/master", + "hidden": true, + "help_text": "If you're running offline, Nextflow will not be able to fetch the institutional config files from the internet. If you don't need them, then this is not a problem. If you do need them, you should download the files from the repo and tell Nextflow where to find them with this parameter.", + "fa_icon": "fas fa-users-cog" + }, + "config_profile_name": { + "type": "string", + "description": "Institutional config name.", + "hidden": true, + "fa_icon": "fas fa-users-cog" + }, + "config_profile_description": { + "type": "string", + "description": "Institutional config description.", + "hidden": true, + "fa_icon": "fas fa-users-cog" + }, + "config_profile_contact": { + "type": "string", + "description": "Institutional config contact information.", + "hidden": true, + "fa_icon": "fas fa-users-cog" + }, + "config_profile_url": { + "type": "string", + "description": "Institutional config URL link.", + "hidden": true, + "fa_icon": "fas fa-users-cog" + } + } + }, + "max_job_request_options": { + "title": "Max job request options", + "type": "object", + "fa_icon": "fab fa-acquisitions-incorporated", + "description": "Set the top limit for requested resources for any single job.", + "help_text": "If you are running on a smaller system, a pipeline step requesting more resources than are available may cause the Nextflow to stop the run with an error. These options allow you to cap the maximum resources requested by any single job so that the pipeline will run on your system.\n\nNote that you can not _increase_ the resources requested by any job using these options. For that you will need your own configuration file. See [the nf-core website](https://nf-co.re/usage/configuration) for details.", + "properties": { + "max_cpus": { + "type": "integer", + "description": "Maximum number of CPUs that can be requested for any single job.", + "default": 16, + "fa_icon": "fas fa-microchip", + "hidden": true, + "help_text": "Use to set an upper-limit for the CPU requirement for each process. Should be an integer e.g. `--max_cpus 1`" + }, + "max_memory": { + "type": "string", + "description": "Maximum amount of memory that can be requested for any single job.", + "default": "128.GB", + "fa_icon": "fas fa-memory", + "pattern": "^\\d+(\\.\\d+)?\\.?\\s*(K|M|G|T)?B$", + "hidden": true, + "help_text": "Use to set an upper-limit for the memory requirement for each process. Should be a string in the format integer-unit e.g. `--max_memory '8.GB'`" + }, + "max_time": { + "type": "string", + "description": "Maximum amount of time that can be requested for any single job.", + "default": "240.h", + "fa_icon": "far fa-clock", + "pattern": "^(\\d+\\.?\\s*(s|m|h|day)\\s*)+$", + "hidden": true, + "help_text": "Use to set an upper-limit for the time requirement for each process. Should be a string in the format integer-unit e.g. `--max_time '2.h'`" + } + } + }, + "generic_options": { + "title": "Generic options", + "type": "object", + "fa_icon": "fas fa-file-import", + "description": "Less common options for the pipeline, typically set in a config file.", + "help_text": "These options are common to all nf-core pipelines and allow you to customise some of the core preferences for how the pipeline runs.\n\nTypically these options would be set in a Nextflow config file loaded for all pipeline runs, such as `~/.nextflow/config`.", + "properties": { + "help": { + "type": "boolean", + "description": "Display help text.", + "fa_icon": "fas fa-question-circle", + "hidden": true + }, + "publish_dir_mode": { + "type": "string", + "default": "copy", + "description": "Method used to save pipeline results to output directory.", + "help_text": "The Nextflow `publishDir` option specifies which intermediate files should be saved to the output directory. This option tells the pipeline what method should be used to move these files. See [Nextflow docs](https://www.nextflow.io/docs/latest/process.html#publishdir) for details.", + "fa_icon": "fas fa-copy", + "enum": ["symlink", "rellink", "link", "copy", "copyNoFollow", "move"], + "hidden": true + }, + "email_on_fail": { + "type": "string", + "description": "Email address for completion summary, only when pipeline fails.", + "fa_icon": "fas fa-exclamation-triangle", + "pattern": "^([a-zA-Z0-9_\\-\\.]+)@([a-zA-Z0-9_\\-\\.]+)\\.([a-zA-Z]{2,5})$", + "help_text": "An email address to send a summary email to when the pipeline is completed - ONLY sent if the pipeline does not exit successfully.", + "hidden": true + }, + "plaintext_email": { + "type": "boolean", + "description": "Send plain-text email instead of HTML.", + "fa_icon": "fas fa-remove-format", + "hidden": true + }, + "max_multiqc_email_size": { + "type": "string", + "description": "File size limit when attaching MultiQC reports to summary emails.", + "default": "25.MB", + "fa_icon": "fas fa-file-upload", + "hidden": true + }, + "monochrome_logs": { + "type": "boolean", + "description": "Do not use coloured log outputs.", + "fa_icon": "fas fa-palette", + "hidden": true + }, + "multiqc_config": { + "type": "string", + "description": "Custom config file to supply to MultiQC.", + "fa_icon": "fas fa-cog", + "hidden": true + }, + "tracedir": { + "type": "string", + "description": "Directory to keep pipeline Nextflow logs and reports.", + "default": "${params.outdir}/pipeline_info", + "fa_icon": "fas fa-cogs", + "hidden": true + }, + "validate_params": { + "type": "boolean", + "description": "Boolean whether to validate parameters against the schema at runtime", + "default": true, + "fa_icon": "fas fa-check-square", + "hidden": true + }, + "show_hidden_params": { + "type": "boolean", + "fa_icon": "far fa-eye-slash", + "description": "Show all params when using `--help`", + "hidden": true, + "help_text": "By default, parameters set as _hidden_ in the schema are not shown on the command line when a user runs with `--help`. Specifying this option will tell the pipeline to show all parameters." + }, + "enable_conda": { + "type": "boolean", + "description": "Run this workflow with Conda. You can also use '-profile conda' instead of providing this parameter.", + "hidden": true, + "fa_icon": "fas fa-bacon" + } + } + } + }, + "allOf": [ + { + "$ref": "#/definitions/input_output_options" + }, + { + "$ref": "#/definitions/umi_options" + }, + { + "$ref": "#/definitions/read_filtering_options" + }, + { + "$ref": "#/definitions/reference_genome_options" + }, + { + "$ref": "#/definitions/read_trimming_options" + }, + { + "$ref": "#/definitions/alignment_options" + }, + { + "$ref": "#/definitions/process_skipping_options" + }, + { + "$ref": "#/definitions/institutional_config_options" + }, + { + "$ref": "#/definitions/max_job_request_options" + }, + { + "$ref": "#/definitions/generic_options" + } + ] +} \ No newline at end of file diff --git a/src/python/test/nextflow/schema2.json b/src/python/test/nextflow/schema2.json new file mode 100644 index 0000000000..ec7d6feafa --- /dev/null +++ b/src/python/test/nextflow/schema2.json @@ -0,0 +1,44 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "https://raw.githubusercontent.com/nf-core/rnaseq/master/nextflow_schema.json", + "title": "nf-core/rnaseq pipeline parameters", + "description": "RNA sequencing analysis pipeline for gene/isoform quantification and extensive quality control.", + "type": "object", + "definitions": { + "input_output_options": { + "title": "Input/output options", + "type": "object", + "fa_icon": "fas fa-terminal", + "description": "Define where the pipeline should find input data and save output data.", + "required": ["outdir"], + "properties": { + "input": { + "type": "string", + "format": "file-path", + "mimetype": "text/csv", + "pattern": "^\\S+\\.csv$", + "schema": "assets/schema_input.json", + "description": "Path to comma-separated file containing information about the samples in the experiment.", + "help_text": "You will need to create a design file with information about the samples in your experiment before running the pipeline. Use this parameter to specify its location. It has to be a comma-separated file with 4 columns, and a header row. See [usage docs](https://nf-co.re/rnaseq/usage#samplesheet-input).", + "fa_icon": "fas fa-file-csv" + }, + "outdir": { + "type": "string", + "format": "directory-path", + "description": "The output directory where the results will be saved. You have to use absolute paths to storage on Cloud infrastructure.", + "fa_icon": "fas fa-folder-open" + }, + "save_merged_fastq": { + "type": "boolean", + "fa_icon": "fas fa-save", + "description": "Save FastQ files after merging re-sequenced libraries in the results directory." + } + } + } + }, + "allOf": [ + { + "$ref": "#/definitions/input_output_options" + } + ] +} \ No newline at end of file diff --git a/src/python/test/nextflow/schema3.json b/src/python/test/nextflow/schema3.json new file mode 100644 index 0000000000..d67c8ad201 --- /dev/null +++ b/src/python/test/nextflow/schema3.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "https://raw.githubusercontent.com/nf-core/rnaseq/master/nextflow_schema.json", + "title": "nf-core/rnaseq pipeline parameters", + "description": "RNA sequencing analysis pipeline for gene/isoform quantification and extensive quality control.", + "type": "object", + "definitions": { + "input_output_options": { + "title": "Input/output options", + "type": "object", + "fa_icon": "fas fa-terminal", + "description": "Define where the pipeline should find input data and save output data.", + "required": ["outdir"], + "properties": { + "outdir": { + "type": "string", + "format": "directory-path", + "description": "The output directory where the results will be saved. You have to use absolute paths to storage on Cloud infrastructure.", + "fa_icon": "fas fa-folder-open", + "help_text": "out_directory help text" + } + } + } + }, + "allOf": [ + { + "$ref": "#/definitions/input_output_options" + } + ] +} \ No newline at end of file diff --git a/src/python/test/pytest.ini b/src/python/test/pytest.ini new file mode 100644 index 0000000000..ef9259e559 --- /dev/null +++ b/src/python/test/pytest.ini @@ -0,0 +1,5 @@ +[pytest] +markers = + TRACEABILITY_MATRIX + TRACEABILITY_ISOLATED_ENV +pythonpath = . \ No newline at end of file diff --git a/src/python/test/test_batch.py b/src/python/test/test_batch.py index eae7639c94..886b74feb6 100755 --- a/src/python/test/test_batch.py +++ b/src/python/test/test_batch.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # -*- coding: utf-8 -*- # # Copyright (C) 2013-2016 DNAnexus, Inc. @@ -75,10 +75,11 @@ def test_basic(self): "outputSpec": [ { "name": "thresholds", "class": "array:int" }, { "name": "pie", "class": "float" }, { "name": "misc", "class": "hash" } ], - "runSpec": { "interpreter": "python2.7", + "runSpec": { "interpreter": "python3", "code": code, "distribution": "Ubuntu", - "release": "14.04" } + "release": "20.04", + "version": "0" } }) # run in batch mode @@ -135,10 +136,11 @@ def test_files(self): "dxapi": "1.0.0", "inputSpec": [ { "name": "plant", "class": "file" } ], "outputSpec": [ { "name": "plant", "class": "file" } ], - "runSpec": { "interpreter": "python2.7", + "runSpec": { "interpreter": "python3", "code": code, "distribution": "Ubuntu", - "release": "14.04" } + "release": "20.04", + "version": "0" } }) job_id = run("dx run {} --batch-tsv={} --yes --brief" .format(applet["id"], arg_table)).strip() @@ -182,10 +184,11 @@ def test_file_arrays(self): "dxapi": "1.0.0", "inputSpec": [ { "name": "plant", "class": "array:file" } ], "outputSpec": [ { "name": "plant", "class": "array:file" } ], - "runSpec": { "interpreter": "python2.7", + "runSpec": { "interpreter": "python3", "code": code, "distribution": "Ubuntu", - "release": "14.04" } + "release": "20.04", + "version": "0" } }) job_id = run("dx run {} --batch-tsv={} --yes --brief" .format(applet["id"], arg_table)).strip() diff --git a/src/python/test/test_create_cohort.py b/src/python/test/test_create_cohort.py new file mode 100644 index 0000000000..d907316e14 --- /dev/null +++ b/src/python/test/test_create_cohort.py @@ -0,0 +1,678 @@ +#!/usr/bin/env python3 + +from __future__ import print_function, unicode_literals, division, absolute_import + +# -*- coding: utf-8 -*- +# +# Copyright (C) 2023 DNAnexus, Inc. +# +# This file is part of dx-toolkit (DNAnexus platform client libraries). +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy +# of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +# Run manually with python2 and python3 src/python/test/test_create_cohort.py + + +import json +import unittest +import os +import re +import subprocess +import dxpy +import sys +import hashlib +import uuid +from parameterized import parameterized +from dxpy.bindings import DXRecord, DXProject + +from dxpy.cli.dataset_utilities import ( + resolve_validate_dx_path, + validate_project_access, + resolve_validate_record_path, + raw_cohort_query_api_call +) +from dxpy.dx_extract_utils.cohort_filter_payload import ( + generate_pheno_filter, + cohort_filter_payload, + cohort_final_payload, +) + +dirname = os.path.dirname(__file__) +payloads_dir = os.path.join(dirname, "create_cohort_test_files/payloads/") + +python_version = sys.version_info.major + +class DescribeDetails: + """ + Strictly parses describe output into objects attributes. + ID record-GYvjYf00F69fGVYkgXqfzfQ2 + Class record + ... + Size 620 + """ + def __init__(self, describe): + self.parse_atributes(describe) + + def parse_atributes(self, describe): + for line in describe.split("\n"): + if line != "": + p_line = line.split(" ") + setattr(self, p_line[0].replace(" ", "_"), p_line[-1].strip(" ")) + + +class TestCreateCohort(unittest.TestCase): + @classmethod + def setUpClass(cls): + proj_name = "dx-toolkit_test_data" + proj_id = list( + dxpy.find_projects(describe=False, level="VIEW", name=proj_name) + )[0]["id"] + cls.general_input_dir = os.path.join(dirname, "create_cohort_test_files/input/") + # cls.general_output_dir = os.path.join(dirname, "create_cohort_test_files/output/") + cls.payloads_dir = payloads_dir + + # TODO: setup project folders + cls.proj_id = proj_id + cls.temp_proj = DXProject() + cls.temp_proj.new(name="temp_test_create_cohort_{}".format(uuid.uuid4())) + cls.temp_proj_id = cls.temp_proj._dxid + dxpy.config["DX_PROJECT_CONTEXT_ID"] = cls.temp_proj_id + cls.test_record_geno = "{}:/Create_Cohort/create_cohort_geno_dataset".format(proj_name) + cls.test_record_pheno = "{}:/Create_Cohort/create_cohort_pheno_dataset".format(proj_name) + cls.test_invalid_rec_type = "{}:/Create_Cohort/non_dataset_record".format(proj_name) + with open( + os.path.join(dirname, "create_cohort_test_files", "usage_message.txt"), "r" + ) as infile: + cls.usage_message = infile.read() + + cls.maxDiff = None + + @classmethod + def tearDownClass(cls): + print("Remmoving temporary testing project {}".format(cls.temp_proj_id)) + cls.temp_proj.destroy() + del cls.temp_proj + + def find_record_id(self, text): + match = re.search(r"\b(record-[A-Za-z0-9]{24})\b", text) + if match: + return match.group(0) + + def is_record_id(self, text): + return bool(re.match(r"^(record-[A-Za-z0-9]{24})",text)) + + def build_command(self, path=None, input_record=None, cohort_ids=None, cohort_ids_file=None): + command = [ + "dx", + "create_cohort", + "--from", + input_record + ] + if path: + command.append(path) + + if cohort_ids: + command.extend(["--cohort-ids", cohort_ids]) + + if cohort_ids_file: + command.extend(["--cohort-ids-file", cohort_ids_file]) + + return command + + # Test the message printed on stdout when the --help flag is provided + # This message is also printed on every error caught by argparse, before the specific message + def test_help_text(self): + expected_result = self.usage_message + command = "dx create_cohort --help" + + process = subprocess.check_output(command, shell=True) + + self.assertEqual(expected_result, process.decode()) + + # testing that command accepts file with sample ids + def test_accept_file_ids(self): + command = self.build_command( + path = "{}:/".format(self.temp_proj_id), + input_record = self.test_record_pheno, + cohort_ids_file = "{}sample_ids_valid_pheno.txt".format(self.general_input_dir) + ) + + process = subprocess.Popen( + command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + stdout, stderr = process.communicate() + + # testing if record object was created, retrieve record_id from stdout + record_id = self.find_record_id(stdout) + self.assertTrue(bool(record_id), "Record object was not created") + + + # EM-1 + # testing resolution of invalid sample_id provided via file + def test_accept_file_ids_negative(self): + command = self.build_command( + path = "{}:/".format(self.temp_proj_id), + input_record = self.test_record_pheno, + cohort_ids_file = "{}sample_ids_wrong.txt".format(self.general_input_dir) + ) + process = subprocess.Popen( + command, stderr=subprocess.PIPE, universal_newlines=True + ) + stderr = process.communicate()[1] + expected_error = ( + "The following supplied IDs do not match IDs in the main entity of dataset" + ) + self.assertTrue(expected_error in stderr, msg = stderr) + + def test_accept_cli_ids(self): + command = self.build_command( + path = "{}:/".format(self.temp_proj_id), + input_record = self.test_record_geno, + cohort_ids = " sample_1_1 , sample_1_10 " + ) + process = subprocess.Popen( + command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + stdout, stderr = process.communicate() + + # testing if record object was created, retrieve record_id from stdout + record_id = self.find_record_id(stdout) + self.assertTrue(bool(record_id), "Record object was not created") + + + # EM-1 + # Supplied IDs do not match IDs of main entity in Dataset/Cohort + def test_accept_cli_ids_negative(self): + command = self.build_command( + path = "{}:/".format(self.temp_proj_id), + input_record = self.test_record_geno, + cohort_ids = "wrong,sample,id" + ) + process = subprocess.Popen( + command, stderr=subprocess.PIPE, universal_newlines=True + ) + stderr = process.communicate()[1] + expected_error = ( + "The following supplied IDs do not match IDs in the main entity of dataset" + ) + self.assertTrue(expected_error in stderr, msg = stderr) + + + # EM-2 + # The structure of '--from' is invalid. This should be able to be reused from other dx functions + def test_errmsg_invalid_path(self): + bad_record = "record-badrecord" + expected_error_message = ( + 'Unable to resolve "{}" to a data object or folder name in'.format(bad_record) + ) + command = self.build_command( + input_record = "{}:{}".format(self.proj_id, bad_record), + cohort_ids = "id1,id2" + ) + + process = subprocess.Popen( + command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True + ) + + err_msg = process.communicate()[1] + + # stdout should be the first element in this list and stderr the second + self.assertIn(expected_error_message, err_msg.strip("\n")) + + # EM-3 + # The user does not have access to the object + def test_errmsg_no_data_access(self): + pass + + # EM-4 + # The record id or path is not a cohort or dataset + # This should fail before the id validity check + def test_errmsg_not_cohort_dataset(self): + + expected_error_message = "{}: Invalid path. The path must point to a record type of cohort or dataset".format( + self.test_invalid_rec_type + ) + command = self.build_command( + input_record = self.test_invalid_rec_type, + cohort_ids = "fakeid" + ) + process = subprocess.Popen( + command, stdout=subprocess.PIPE ,stderr=subprocess.PIPE, universal_newlines=True + ) + + # stdout should be the first element in this list and stderr the second + self.assertEqual(expected_error_message, process.communicate()[1].strip()) + + # EM-5 + # The record id or path is a cohort or dataset but is invalid (maybe corrupted, descriptor not accessible...etc) + def test_errmsg_invalid_record(self): + pass + + # EM-6 + # The record id or path is a cohort or dataset but the version is less than 3.0. + def test_errmsg_dataset_version(self): + pass + + # EM-7 + # If PATH is of the format `project-xxxx:folder/` and the project does not exist + def test_errmsg_project_not_exist(self): + bad_project = "project-notarealproject7843k2Jq" + expected_error_message = 'ResolutionError: Could not find a project named "{}"'.format( + bad_project + ) + command = self.build_command( + path = "{}:/".format(self.temp_proj_id), + input_record = "{}:/".format(bad_project), + cohort_ids = "id_1,id_2" + ) + process = subprocess.Popen( + command, stderr=subprocess.PIPE, universal_newlines=True + ) + err_msg = process.communicate()[1] + self.assertIn(expected_error_message, err_msg.strip("\n")) + + # EM-8 + # If PATH is of the format `project-xxxx:folder/` and the user does not have CONTRIBUTE or ADMINISTER access + # Note that this is the PATH that the output cohort is being created in, not the input dataset or cohort + def test_errmsg_no_path_access(self): + pass + + # EM-9 + # If PATH is of the format `folder/subfolder/` and the path does not exist + def test_errmsg_subfolder_not_exist(self): + bad_path = "{}:Create_Cohort/missing_folder/file_name".format(self.proj_id) + expected_error_message = "The folder: {} could not be found in the project: {}".format( + "/Create_Cohort/missing_folder", self.proj_id + ) + command = self.build_command( + path = bad_path, + input_record = self.test_record_pheno, + cohort_ids = "patient_1,patient_2" + ) + process = subprocess.Popen( + command, stderr=subprocess.PIPE, universal_newlines=True + ) + # split("\n")[-1] is added to ignore the warning message that gets added + # since DX_PROJECT_CONTEXT_ID environment variable is manually updated in setup class + err_msg = process.communicate()[1].strip("\n").split("\n")[-1] + self.assertEqual(expected_error_message, err_msg) + + # EM-10 + # If both --cohort-ids and --cohort-ids-file are supplied in the same call + # The file needs to exist for this check to be performed + def test_errmsg_incompat_args(self): + expected_error_message = "dx create_cohort: error: argument --cohort-ids-file: not allowed with argument --cohort-ids" + command = self.build_command( + input_record = self.test_record_pheno, + cohort_ids = "id1,id2", + cohort_ids_file = os.path.join(self.general_input_dir, "sample_ids_10.txt") + ) + process = subprocess.Popen( + command, stderr=subprocess.PIPE, universal_newlines=True + ) + err_msg = process.communicate()[1] + self.assertIn(expected_error_message, err_msg) + + # EM-11 The vizserver returns an error when attempting to validate cohort IDs + def test_vizserver_error(self): + pass + + def test_raw_cohort_query_api(self): + test_payload = { + "filters": { + "pheno_filters": { + "compound": [ + { + "name": "phenotype", + "logic": "and", + "filters": { + "patient$patient_id": [ + { + "condition": "in", + "values": [ + "patient_1", + "patient_2", + "patient_3" + ] + } + ] + } + } + ], + "logic": "and" + }, + "logic": "and" + }, + "project_context": self.proj_id + } + + expected_results = "SELECT `patient_1`.`patient_id` AS `patient_id` FROM `database_yyyyyyyyyyyyyyyyyyyyyyyy__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`patient_id` IN ('patient_1', 'patient_2', 'patient_3');" + + from_project, entity_result, resp, dataset_project = resolve_validate_record_path(self.test_record_pheno) + sql = raw_cohort_query_api_call(resp, test_payload) + self.assertEqual(expected_results, re.sub(r"\bdatabase_\w{24}__\w+", "database_yyyyyyyyyyyyyyyyyyyyyyyy__create_cohort_pheno_database", sql)) + + def test_create_pheno_filter(self): + """Verifying the correctness of created filters by examining this flow: + 1. creating the filter with: dxpy.dx_extract_utils.cohort_filter_payload.generate_pheno_filter + 2. obtaining sql with: dxpy.cli.dataset_utilities.raw_cohort_query_api_call + 3. creating record with obtained sql and the filter by: dxpy.bindings.dxrecord.new_dxrecord + """ + + # test creating pheno filter + values = ["patient_1", "patient_2", "patient_3"] + entity = "patient" + field = "patient_id" + filters = { + "pheno_filters": { + "compound": [ + { + "name": "phenotype", + "logic": "and", + "filters": { + "patient$patient_id": [ + { + "condition": "in", + "values": ["patient_1", "patient_2", "patient_6"], + } + ] + }, + } + ], + "logic": "and", + }, + "logic": "and", + } + expected_filter = { + "pheno_filters": { + "compound": [ + { + "name": "phenotype", + "logic": "and", + "filters": { + "patient$patient_id": [ + { + "condition": "in", + "values": ["patient_1", "patient_2"] + } + ] + }, + } + ], + "logic": "and", + }, + "logic": "and", + } + expected_sql = "SELECT `patient_1`.`patient_id` AS `patient_id` FROM `database_yyyyyyyyyyyyyyyyyyyyyyyy__create_cohort_pheno_database`.`patient` AS `patient_1` WHERE `patient_1`.`patient_id` IN ('patient_1', 'patient_2');" + lambda_for_list_conv = lambda a, b: a+[str(b)] + + generated_filter = generate_pheno_filter(values, entity, field, filters, lambda_for_list_conv) + self.assertEqual(expected_filter, generated_filter) + + # Testing raw cohort query api + resp = resolve_validate_record_path(self.test_record_pheno)[2] + payload = {"filters": generated_filter, "project_context": self.proj_id} + + sql = raw_cohort_query_api_call(resp, payload) + self.assertEqual(expected_sql, re.sub(r"\bdatabase_\w{24}__\w+", "database_yyyyyyyyyyyyyyyyyyyyyyyy__create_cohort_pheno_database", sql)) + + # Testing new record with generated filter and sql + details = { + "databases": [resp["databases"]], + "dataset": {"$dnanexus_link": resp["dataset"]}, + "description": "", + "filters": generated_filter, + "schema": "create_cohort_schema", + "sql": sql, + "version": "3.0", + } + + + new_record = dxpy.bindings.dxrecord.new_dxrecord( + details=details, + project=self.temp_proj_id, + name=None, + types=["DatabaseQuery", "CohortBrowser"], + folder="/", + close=True, + ) + new_record_details = new_record.get_details() + new_record.remove() + e = None + self.assertTrue(isinstance(new_record, DXRecord)) + self.assertEqual(new_record_details, details, "Details of created record does not match expected details.") + + @parameterized.expand( + os.path.splitext(file_name)[0] for file_name in sorted(os.listdir(os.path.join(payloads_dir, "raw-cohort-query_input"))) + ) + def test_cohort_filter_payload(self, payload_name): + with open(os.path.join(self.payloads_dir, "input_parameters", "{}.json".format(payload_name))) as f: + input_parameters = json.load(f) + values = input_parameters["values"] + entity = input_parameters["entity"] + field = input_parameters["field"] + project_context = input_parameters["project"] + + with open(os.path.join(self.payloads_dir, "visualize_response", "{}.json".format(payload_name))) as f: + visualize_response = json.load(f) + filters = visualize_response.get("filters", {}) + base_sql = visualize_response.get("baseSql", visualize_response.get("base_sql")) + lambda_for_list_conv = lambda a, b: a+[str(b)] + + test_payload = cohort_filter_payload(values, entity, field, filters, project_context, lambda_for_list_conv, base_sql) + + with open(os.path.join(self.payloads_dir, "raw-cohort-query_input", "{}.json".format(payload_name))) as f: + valid_payload = json.load(f) + + self.assertDictEqual(test_payload, valid_payload) + + @parameterized.expand( + os.path.splitext(file_name)[0] for file_name in sorted(os.listdir(os.path.join(payloads_dir, "dx_new_input"))) + ) + def test_cohort_final_payload(self, payload_name): + name = None + + with open(os.path.join(self.payloads_dir, "input_parameters", "{}.json".format(payload_name))) as f: + input_parameters = json.load(f) + folder = input_parameters["folder"] + project = input_parameters["project"] + + with open(os.path.join(self.payloads_dir, "visualize_response", "{}.json".format(payload_name))) as f: + visualize = json.load(f) + dataset = visualize["dataset"] + databases = visualize["databases"] + schema = visualize["schema"] + base_sql = visualize.get("baseSql", visualize.get("base_sql")) + combined = visualize.get("combined") + + with open(os.path.join(self.payloads_dir, "raw-cohort-query_input", "{}.json".format(payload_name))) as f: + filters = json.load(f)["filters"] + + with open(os.path.join(self.payloads_dir, "raw-cohort-query_output", "{}.sql".format(payload_name))) as f: + sql = f.read() + + test_output = cohort_final_payload(name, folder, project, databases, dataset, schema, filters, sql, base_sql, combined) + + with open(os.path.join(self.payloads_dir, "dx_new_input", "{}.json".format(payload_name))) as f: + valid_output = json.load(f) + + valid_output["name"] = None + + self.assertDictEqual(test_output, valid_output) + + def test_brief_verbose(self): + command = self.build_command( + path = "{}:/".format(self.temp_proj_id), + input_record = self.test_record_geno, + cohort_ids = "sample_1_1,sample_1_10" + ) + + for stdout_mode in ["--verbose", "--brief", ""]: + cmd = command + [stdout_mode] if stdout_mode != "" else command + process = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + stdout, stderr = process.communicate() + if stdout_mode == "--brief": + record_id = stdout.strip("\n").strip(" ") + self.assertTrue(self.is_record_id(record_id), + "Brief stdout has to be a record-id" + ) + elif stdout_mode == "--verbose": + self.assertIn( + "Details", stdout, "Verbose stdout has to contain 'Details' string" + ) + else: + self.assertIn( + "Types", stdout, "Default stdout has to contain 'Types' string" + ) + + + def test_path_upload_access(self): + # Having at least UPLOAD access to a project + err_msg = validate_project_access(self.temp_proj_id) + self.assertIsNone(err_msg) + + def test_path_upload_access_negative(self): + #TODO: delegate this to QE + pass + + def test_path_options(self): + """ + Testing different path formats. + Various path options and expected results are parametrized. + The dictionary `expected_in_out_pairs` expects form: {"": ()} + """ + self.temp_proj.new_folder("/folder/subfolder", parents=True) + + expected_in_out_pairs = { + "{}:/".format(self.proj_id): (self.proj_id, "/", None, None), + "{}:/folder/subfolder/record_name1".format(self.temp_proj_id): (self.temp_proj_id, "/folder/subfolder", "record_name1", None ), + "record_name": (self.temp_proj_id, "/", "record_name", None), + "/folder/record_name": (self.temp_proj_id, "/folder", "record_name", None), + "/folder/subfolder/record_name": ( + self.temp_proj_id, + "/folder/subfolder", + "record_name", + None, + ), + "/folder/subfolder/no_exist/record_name": ( + self.temp_proj_id, + "/folder/subfolder/no_exist", + "record_name", + "The folder: /folder/subfolder/no_exist could not be found in the project: {}".format( + self.temp_proj_id + ), + ), + "/folder/": (self.temp_proj_id, "/folder", None, None), + } + + for path, expected_result in expected_in_out_pairs.items(): + result = resolve_validate_dx_path(path) + self.assertEqual(result, expected_result) + + def test_path_options_negative(self): + expected_result = ( + self.temp_proj_id, + "/folder/subfolder/no_exist", + "record_name", + "The folder: /folder/subfolder/no_exist could not be found in the project: {}".format( + self.temp_proj_id + ) + ) + result = resolve_validate_dx_path("/folder/subfolder/no_exist/record_name") + self.assertEqual(result, expected_result) + + def test_path_options_cli(self): + """ + Testing different path formats. + Focusing on default values with record name or folder not specified. + """ + # create subfolder structure + self.temp_proj.new_folder("/folder/subfolder", parents=True) + # set cwd + dxpy.config['DX_CLI_WD'] = "/folder" + + command = self.build_command( + input_record = self.test_record_geno, + cohort_ids = "sample_1_1,sample_1_10" + ) + + path_options = [ + "record_name2", #Should create record in CWD + "/folder/subfolder/", #Name of record should be record-id + "", #Combination of above + ] + for path_format in path_options: + cmd = command[:] + if path_format != "": + cmd.insert(2, path_format) + + process = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + universal_newlines=True, + ) + stdout = process.communicate()[0] + desc = DescribeDetails(stdout) + + if path_format == "record_name2": + self.assertEqual(desc.Folder, "/folder") + self.assertEqual(desc.Project, self.temp_proj_id) + self.assertEqual(desc.Name, "record_name2") + elif path_format =="/folder/subfolder/": + self.assertEqual(desc.Folder, "/folder/subfolder") + self.assertEqual(desc.Project, self.temp_proj_id) + self.assertTrue(self.is_record_id(desc.Name), + "Record name should be a record-id" + ) + elif path_format =="": + self.assertEqual(desc.Folder, "/folder") + self.assertEqual(desc.Project, self.temp_proj_id) + self.assertTrue(self.is_record_id(desc.Name), + "Record name should be a record-id" + ) + + + def test_path_options_cli_negative(self): + command = self.build_command( + path = "/folder/subfolder/no_exist/record_name", + input_record = self.test_record_geno, + cohort_ids = "sample_1_1,sample_1_10" + ) + process = subprocess.Popen( + command, + stderr=subprocess.PIPE, + stdout=subprocess.PIPE, + universal_newlines=True, + ) + stderr = process.communicate()[1] + self.assertEqual( + "The folder: /folder/subfolder/no_exist could not be found in the project: {}".format( + self.temp_proj_id + ), + stderr.strip("\n")) + + +if __name__ == "__main__": + unittest.main() + diff --git a/src/python/test/test_describe.py b/src/python/test/test_describe.py index 307f2d9182..7eab6e8cf8 100755 --- a/src/python/test/test_describe.py +++ b/src/python/test/test_describe.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # -*- coding: utf-8 -*- # # Copyright (C) 2013-2016 DNAnexus, Inc. diff --git a/src/python/test/test_dx-docker.py b/src/python/test/test_dx-docker.py index eea9c4bf52..a7da6d8828 100755 --- a/src/python/test/test_dx-docker.py +++ b/src/python/test/test_dx-docker.py @@ -119,7 +119,7 @@ def test_dx_docker_home_dir(self): run("dx-docker run julia:0.5.0 julia -E 'println(\"hello world\")'") def test_dx_docker_run_rm(self): - run("dx-docker run --rm ubuntu ls") + run("dx-docker run --rm busybox ls") def test_dx_docker_set_env(self): dx_docker_out = run("dx-docker run --env HOME=/somethingelse busybox env") diff --git a/src/python/test/test_dx_app_wizard.py b/src/python/test/test_dx_app_wizard.py index d861f08ade..313378b6ed 100755 --- a/src/python/test/test_dx_app_wizard.py +++ b/src/python/test/test_dx_app_wizard.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # -*- coding: utf-8 -*- # # Copyright (C) 2013-2016 DNAnexus, Inc. @@ -19,7 +19,6 @@ import os, sys, unittest, json, tempfile, subprocess import pexpect -import pipes from dxpy_testutil import DXTestCase, check_output import dxpy_testutil as testutil diff --git a/src/python/test/test_dx_bash_helpers.py b/src/python/test/test_dx_bash_helpers.py index 85dd3f9746..5b7557982e 100755 --- a/src/python/test/test_dx_bash_helpers.py +++ b/src/python/test/test_dx_bash_helpers.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # -*- coding: utf-8 -*- # # Copyright (C) 2014-2016 DNAnexus, Inc. @@ -23,89 +23,99 @@ import dxpy_testutil as testutil import json import os -import pipes +import shlex import pytest import shutil import tempfile import unittest from dxpy.utils.completer import InstanceTypesCompleter -from dxpy_testutil import DXTestCase, check_output, temporary_project, override_environment +from dxpy_testutil import ( + DXTestCase, + check_output, + temporary_project, + override_environment, +) from dxpy.exceptions import DXJobFailureError from dxpy.bindings.download_all_inputs import _get_num_parallel_threads + def run(command, **kwargs): try: if isinstance(command, list) or isinstance(command, tuple): - print("$ %s" % ' '.join(pipes.quote(f) for f in command)) + print("$ %s" % " ".join(shlex.quote(f) for f in command)) output = check_output(command, **kwargs) else: print("$ %s" % (command,)) output = check_output(command, shell=True, **kwargs) except testutil.DXCalledProcessError as e: - print('== stdout ==') + print("== stdout ==") print(e.output) - print('== stderr ==') + print("== stderr ==") print(e.stderr) raise print(output) return output -TEST_APPS = os.path.join(os.path.dirname(__file__), 'file_load') -TEST_MOUNT_APPS = os.path.join(os.path.dirname(__file__), 'file_mount') -LOCAL_SCRIPTS = os.path.join(os.path.dirname(__file__), '..', 'scripts') -LOCAL_UTILS = os.path.join(os.path.dirname(__file__), '..', 'dxpy', 'utils') -DUMMY_HASH = "123456789012345678901234" +TEST_APPS = os.path.join(os.path.dirname(__file__), "file_load") +TEST_JWT = os.path.join(os.path.dirname(__file__), "jwt") +TEST_MOUNT_APPS = os.path.join(os.path.dirname(__file__), "file_mount") +LOCAL_SCRIPTS = os.path.join(os.path.dirname(__file__), "..", "scripts") +LOCAL_UTILS = os.path.join(os.path.dirname(__file__), "..", "dxpy", "utils") +DUMMY_HASH = "123456789012345678901234" -def ignore_folders(directory, contents): - accepted_bin = ['dx-unpack', 'dx-unpack-file', 'dxfs', 'register-python-argcomplete', - 'python-argcomplete-check-easy-install-script'] - # Omit Python test dir since it's pretty large - if "src/python/test" in directory: - return contents - if "../bin" in directory: - return [f for f in contents if f not in accepted_bin] - return [] def build_app_with_bash_helpers(app_dir, project_id): tempdir = tempfile.mkdtemp() try: updated_app_dir = os.path.join(tempdir, os.path.basename(app_dir)) - #updated_app_dir = os.path.abspath(os.path.join(tempdir, os.path.basename(app_dir))) + # updated_app_dir = os.path.abspath(os.path.join(tempdir, os.path.basename(app_dir))) shutil.copytree(app_dir, updated_app_dir) # Copy the current verion of dx-toolkit. We will build it on the worker # and source this version which will overload the stock version of dx-toolkit. # This way we can test all bash helpers as they would appear locally with all # necessary dependencies - #dxtoolkit_dir = os.path.abspath(os.path.join(updated_app_dir, 'resources', 'dxtoolkit')) - #local_dxtoolkit = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..')) - dxtoolkit_dir = os.path.join(updated_app_dir, 'resources', 'dxtoolkit') - local_dxtoolkit = os.path.join(os.path.dirname(__file__), '..', '..', '..') + # dxtoolkit_dir = os.path.abspath(os.path.join(updated_app_dir, 'resources', 'dxtoolkit')) + # local_dxtoolkit = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..')) + dxtoolkit_dir = os.path.join(updated_app_dir, "resources", "dxtoolkit") + local_dxtoolkit = os.path.join(os.path.dirname(__file__), "..", "..", "..") shutil.copytree(local_dxtoolkit, dxtoolkit_dir) # Add lines to the beginning of the job to make and use our new dx-toolkit preamble = [] - #preamble.append("cd {appdir}/resources && git clone https://github.com/dnanexus/dx-toolkit.git".format(appdir=updated_app_dir)) - preamble.append('python3 /dxtoolkit/src/python/setup.py sdist\n') - preamble.append('DIST=$(ls /dxtoolkit/src/python/dist)\n') - preamble.append('python3 -m pip install -U /dxtoolkit/src/python/dist/$DIST\n') + # preamble.append("cd {appdir}/resources && git clone https://github.com/dnanexus/dx-toolkit.git".format(appdir=updated_app_dir)) + preamble.append("python3 -m pip install /dxtoolkit/src/python/\n") # Now find the applet entry point file and prepend the # operations above, overwriting it in place. - with open(os.path.join(app_dir, 'dxapp.json')) as f: + with open(os.path.join(app_dir, "dxapp.json")) as f: dxapp_json = json.load(f) - if dxapp_json['runSpec']['interpreter'] != 'bash': - raise Exception('Sorry, I only know how to patch bash apps for remote testing') - entry_point_filename = os.path.join(app_dir, dxapp_json['runSpec']['file']) + if dxapp_json["runSpec"]["interpreter"] != "bash": + raise Exception( + "Sorry, I only know how to patch bash apps for remote testing" + ) + entry_point_filename = os.path.join(app_dir, dxapp_json["runSpec"]["file"]) with open(entry_point_filename) as fh: - entry_point_data = ''.join(preamble) + fh.read() - with open(os.path.join(updated_app_dir, dxapp_json['runSpec']['file']), 'w') as fh: + entry_point_data = "".join(preamble) + fh.read() + with open( + os.path.join(updated_app_dir, dxapp_json["runSpec"]["file"]), "w" + ) as fh: fh.write(entry_point_data) - build_output = run(['dx', 'build', '--json', '--destination', project_id + ':', updated_app_dir]) - return json.loads(build_output)['id'] + build_output = run( + [ + "dx", + "build", + "--json", + "--destination", + project_id + ":", + updated_app_dir, + ] + ) + return json.loads(build_output)["id"] finally: shutil.rmtree(tempdir) + def update_environ(**kwargs): """ Returns a copy of os.environ with the specified updates (VAR=value for each kwarg) @@ -119,13 +129,13 @@ def update_environ(**kwargs): return output -@unittest.skipUnless(testutil.TEST_RUN_JOBS, 'skipping tests that would run jobs') +@unittest.skipUnless(testutil.TEST_RUN_JOBS, "skipping tests that would run jobs") class TestDXBashHelpers(DXTestCase): @pytest.mark.TRACEABILITY_MATRIX @testutil.update_traceability_matrix(["DNA_CLI_HELP_PROVIDE_BASH_HELPER_COMMANDS"]) def test_vars(self): - ''' Quick test for the bash variables ''' - with temporary_project('TestDXBashHelpers.test_app1 temporary project') as p: + """Quick test for the bash variables""" + with temporary_project("TestDXBashHelpers.test_app1 temporary project") as p: env = update_environ(DX_PROJECT_CONTEXT_ID=p.get_id()) # Upload some files for use by the applet @@ -133,22 +143,42 @@ def test_vars(self): # Build the applet, patching in the bash helpers from the # local checkout - applet_id = build_app_with_bash_helpers(os.path.join(TEST_APPS, 'vars'), p.get_id()) + applet_id = build_app_with_bash_helpers( + os.path.join(TEST_APPS, "vars"), p.get_id() + ) # Run the applet - applet_args = ['-iseq1=A.txt', '-iseq2=A.txt', '-igenes=A.txt', '-igenes=A.txt', - '-ii=5', '-ix=4.2', '-ib=true', '-is=hello', - '-iil=6', '-iil=7', '-iil=8', - '-ixl=3.3', '-ixl=4.4', '-ixl=5.0', - '-ibl=true', '-ibl=false', '-ibl=true', - '-isl=hello', '-isl=world', '-isl=next', - '-imisc={"hello": "world", "foo": true}'] - cmd_args = ['dx', 'run', '--yes', '--watch', applet_id] + applet_args = [ + "-iseq1=A.txt", + "-iseq2=A.txt", + "-igenes=A.txt", + "-igenes=A.txt", + "-ii=5", + "-ix=4.2", + "-ib=true", + "-is=hello", + "-iil=6", + "-iil=7", + "-iil=8", + "-ixl=3.3", + "-ixl=4.4", + "-ixl=5.0", + "-ibl=true", + "-ibl=false", + "-ibl=true", + "-isl=hello", + "-isl=world", + "-isl=next", + '-imisc={"hello": "world", "foo": true}', + ] + cmd_args = ["dx", "run", "--yes", "--watch", applet_id] cmd_args.extend(applet_args) run(cmd_args, env=env) def test_basic(self): - with temporary_project('TestDXBashHelpers.test_app1 temporary project') as dxproj: + with temporary_project( + "TestDXBashHelpers.test_app1 temporary project" + ) as dxproj: env = update_environ(DX_PROJECT_CONTEXT_ID=dxproj.get_id()) # Upload some files for use by the applet @@ -157,16 +187,27 @@ def test_basic(self): # Build the applet, patching in the bash helpers from the # local checkout - applet_id = build_app_with_bash_helpers(os.path.join(TEST_APPS, 'basic'), dxproj.get_id()) + applet_id = build_app_with_bash_helpers( + os.path.join(TEST_APPS, "basic"), dxproj.get_id() + ) # Run the applet - applet_args = ['-iseq1=A.txt', '-iseq2=B.txt', '-iref=A.txt', '-iref=B.txt', "-ivalue=5", "-iages=4"] - cmd_args = ['dx', 'run', '--yes', '--watch', applet_id] + applet_args = [ + "-iseq1=A.txt", + "-iseq2=B.txt", + "-iref=A.txt", + "-iref=B.txt", + "-ivalue=5", + "-iages=4", + ] + cmd_args = ["dx", "run", "--yes", "--watch", applet_id] cmd_args.extend(applet_args) run(cmd_args, env=env) def test_mount_basic(self): - with temporary_project('TestDXBashHelpers.test_app1 temporary project') as dxproj: + with temporary_project( + "TestDXBashHelpers.test_app1 temporary project" + ) as dxproj: env = update_environ(DX_PROJECT_CONTEXT_ID=dxproj.get_id()) # Upload some files for use by the applet @@ -175,31 +216,37 @@ def test_mount_basic(self): # Build the applet, patching in the bash helpers from the # local checkout - applet_id = build_app_with_bash_helpers(os.path.join(TEST_MOUNT_APPS, 'basic'), dxproj.get_id()) + applet_id = build_app_with_bash_helpers( + os.path.join(TEST_MOUNT_APPS, "basic"), dxproj.get_id() + ) # Run the applet - applet_args = ['-iseq1=A.txt', '-iseq2=B.txt', '-iref=A.txt', '-iref=B.txt'] - cmd_args = ['dx', 'run', '--yes', '--watch', applet_id] + applet_args = ["-iseq1=A.txt", "-iseq2=B.txt", "-iref=A.txt", "-iref=B.txt"] + cmd_args = ["dx", "run", "--yes", "--watch", applet_id] cmd_args.extend(applet_args) run(cmd_args, env=env) def test_sub_jobs(self): - ''' Tests a bash script that generates sub-jobs ''' - with temporary_project('TestDXBashHelpers.test_app1 temporary project') as dxproj: + """Tests a bash script that generates sub-jobs""" + with temporary_project( + "TestDXBashHelpers.test_app1 temporary project" + ) as dxproj: env = update_environ(DX_PROJECT_CONTEXT_ID=dxproj.get_id()) - # Upload some files for use by the applet + # Upload some files for use by the applet dxpy.upload_string("1234\n", project=dxproj.get_id(), name="A.txt") dxpy.upload_string("ABCD\n", project=dxproj.get_id(), name="B.txt") # Build the applet, patching in the bash helpers from the # local checkout - applet_id = build_app_with_bash_helpers(os.path.join(TEST_APPS, 'with-subjobs'), dxproj.get_id()) - # Run the applet. + applet_id = build_app_with_bash_helpers( + os.path.join(TEST_APPS, "with-subjobs"), dxproj.get_id() + ) + # Run the applet. # Since the job creates two sub-jobs, we need to be a bit more sophisticated # in order to wait for completion. applet_args = ["-ifiles=A.txt", "-ifiles=B.txt"] - cmd_args = ['dx', 'run', '--yes', '--brief', applet_id] + cmd_args = ["dx", "run", "--yes", "--brief", applet_id] cmd_args.extend(applet_args) job_id = run(cmd_args, env=env).strip() @@ -217,7 +264,7 @@ def test_sub_jobs(self): job_output = job_handler.output def strip_white_space(_str): - return ''.join(_str.split()) + return "".join(_str.split()) def silent_file_remove(filename): try: @@ -227,12 +274,16 @@ def silent_file_remove(filename): # The output should include two files, this section verifies that they have # the correct data. - def check_file_content(out_param_name, out_filename, tmp_fname, str_content): + def check_file_content( + out_param_name, out_filename, tmp_fname, str_content + ): """ Download a file, read it from local disk, and verify that it has the correct contents """ if not out_param_name in job_output: - raise "Error: key {} does not appear in the job output".format(out_param_name) + raise "Error: key {} does not appear in the job output".format( + out_param_name + ) dxlink = job_output[out_param_name] # check that the filename gets preserved @@ -246,15 +297,21 @@ def check_file_content(out_param_name, out_filename, tmp_fname, str_content): data = fh.read() print(data) if not (strip_white_space(data) == strip_white_space(str_content)): - raise Exception("contents of file {} do not match".format(out_param_name)) + raise Exception( + "contents of file {} do not match".format(out_param_name) + ) silent_file_remove(tmp_fname) - check_file_content('first_file', 'first_file.txt', "f1.txt", "contents of first_file") - check_file_content('final_file', 'final_file.txt', "f2.txt", "1234ABCD") + check_file_content( + "first_file", "first_file.txt", "f1.txt", "contents of first_file" + ) + check_file_content("final_file", "final_file.txt", "f2.txt", "1234ABCD") def test_parseq(self): - ''' Tests the parallel/sequential variations ''' - with temporary_project('TestDXBashHelpers.test_app1 temporary project') as dxproj: + """Tests the parallel/sequential variations""" + with temporary_project( + "TestDXBashHelpers.test_app1 temporary project" + ) as dxproj: env = update_environ(DX_PROJECT_CONTEXT_ID=dxproj.get_id()) # Upload some files for use by the applet @@ -263,17 +320,21 @@ def test_parseq(self): # Build the applet, patching in the bash helpers from the # local checkout - applet_id = build_app_with_bash_helpers(os.path.join(TEST_APPS, 'parseq'), dxproj.get_id()) + applet_id = build_app_with_bash_helpers( + os.path.join(TEST_APPS, "parseq"), dxproj.get_id() + ) # Run the applet applet_args = ["-iseq1=A.txt", "-iseq2=B.txt", "-iref=A.txt", "-iref=B.txt"] - cmd_args = ['dx', 'run', '--yes', '--watch', applet_id] + cmd_args = ["dx", "run", "--yes", "--watch", applet_id] cmd_args.extend(applet_args) run(cmd_args, env=env) def test_xattr_parameters(self): - ''' Tests dx-upload-all-outputs uploading with filesystem metadata as properties ''' - with temporary_project('TestDXBashHelpers.test_app1 temporary project') as dxproj: + """Tests dx-upload-all-outputs uploading with filesystem metadata as properties""" + with temporary_project( + "TestDXBashHelpers.test_app1 temporary project" + ) as dxproj: env = update_environ(DX_PROJECT_CONTEXT_ID=dxproj.get_id()) # Upload some files for use by the applet @@ -282,33 +343,90 @@ def test_xattr_parameters(self): # Build the applet, patching in the bash helpers from the # local checkout - applet_id = build_app_with_bash_helpers(os.path.join(TEST_APPS, 'xattr_properties'), dxproj.get_id()) + applet_id = build_app_with_bash_helpers( + os.path.join(TEST_APPS, "xattr_properties"), dxproj.get_id() + ) # Run the applet applet_args = ["-iseq1=A.txt", "-iseq2=B.txt", "-iref=A.txt", "-iref=B.txt"] - cmd_args = ['dx', 'run', '--yes', '--watch', applet_id] + cmd_args = ["dx", "run", "--yes", "--watch", applet_id] cmd_args.extend(applet_args) run(cmd_args, env=env) - @unittest.skipUnless(testutil.TEST_RUN_JOBS, 'skipping test that would run a job') + @unittest.skipUnless(testutil.TEST_RUN_JOBS, "skipping test that would run a job") + def test_job_identity_token(self): + """Tests dx-jobutil-get-identity-token script""" + with temporary_project( + "TestDXBashHelpers.test_app1 temporary project" + ) as dxproj: + env = update_environ(DX_PROJECT_CONTEXT_ID=dxproj.get_id()) + + # Build the applet, patching in the bash helpers from the + # local checkout + applet_id = build_app_with_bash_helpers( + os.path.join(TEST_JWT), dxproj.get_id() + ) + + # pass in the audience with the --aud flag + applet_args = [ + "-iaudience=fake.compute.team", + ] + cmd_args = ["dx", "run", "--yes", "--watch", applet_id] + cmd_args.extend(applet_args) + token = run(cmd_args, env=env) + self.assertIsInstance(token, str) + + # pass in the audience and subject_claims + applet_args = [ + "-iaudience=fake.compute.team", + "-isubject_claims=job_id,root_execution_id", + ] + cmd_args = ["dx", "run", "--yes", "--watch", applet_id] + cmd_args.extend(applet_args) + token = run(cmd_args, env=env) + self.assertIsInstance(token, str) + + # pass invalid aud (missing) + with pytest.raises(Exception): + applet_args = [ + "-isubject_claims=job_id,root_execution_id", + ] + cmd_args = ["dx", "run", "--yes", "--watch", applet_id] + cmd_args.extend(applet_args) + + run(cmd_args, env=env) + + # pass invalid subject_claims + with pytest.raises(Exception): + applet_args = [ + "-isubject_claims=apples,bananas", + ] + cmd_args = ["dx", "run", "--yes", "--watch", applet_id] + cmd_args.extend(applet_args) + + run(cmd_args, env=env) + + @unittest.skipUnless(testutil.TEST_RUN_JOBS, "skipping test that would run a job") def test_file_optional(self): - ''' Tests that optional and non-optional file output arguments are - handled correctly ''' - with temporary_project('TestDXBashHelpers.test_app1 temporary project') as dxproj: + """Tests that optional and non-optional file output arguments are + handled correctly""" + with temporary_project( + "TestDXBashHelpers.test_app1 temporary project" + ) as dxproj: env = update_environ(DX_PROJECT_CONTEXT_ID=dxproj.get_id()) # Build the applet, patching in the bash helpers from the # local checkout applet_id = build_app_with_bash_helpers( - os.path.join(TEST_APPS, 'file_optional'), - dxproj.get_id()) + os.path.join(TEST_APPS, "file_optional"), dxproj.get_id() + ) # Run the applet. This checks a correct scenario where # the applet generates: # 1) an empty directory for an optional file output # 2) a file for a non-optional file output. applet_args = ["-icreate_seq3=true"] - cmd_args = ['dx', 'run', '--yes', '--brief', applet_id] + cmd_args = ["dx", "run", "--yes", "--brief", applet_id] cmd_args.extend(applet_args) job_id = run(cmd_args, env=env).strip() dxpy.DXJob(job_id).wait_on_done() @@ -316,7 +434,7 @@ def test_file_optional(self): # Run the applet --- this will not create the seq3 output file. # This should cause an exception from the job manager. applet_args = ["-icreate_seq3=false"] - cmd_args = ['dx', 'run', '--yes', '--brief', applet_id] + cmd_args = ["dx", "run", "--yes", "--brief", applet_id] cmd_args.extend(applet_args) job_id = run(cmd_args, env=env).strip() job = dxpy.DXJob(job_id) @@ -326,65 +444,91 @@ def test_file_optional(self): self.assertEqual(desc["failureReason"], "OutputError") def test_prefix_patterns(self): - """ Tests that the bash prefix variable works correctly, and + """Tests that the bash prefix variable works correctly, and respects patterns. """ - with temporary_project('TestDXBashHelpers.test_app1 temporary project') as dxproj: + with temporary_project( + "TestDXBashHelpers.test_app1 temporary project" + ) as dxproj: env = update_environ(DX_PROJECT_CONTEXT_ID=dxproj.get_id()) - filenames = ["A.bar", "A.json.dot.bar", "A.vcf.pam", "A.foo.bar", - "fooxxx.bam", "A.bar.gz", "x13year23.sam"] + filenames = [ + "A.bar", + "A.json.dot.bar", + "A.vcf.pam", + "A.foo.bar", + "fooxxx.bam", + "A.bar.gz", + "x13year23.sam", + ] for fname in filenames: dxpy.upload_string("1234", project=dxproj.get_id(), name=fname) # Build the applet, patching in the bash helpers from the # local checkout - applet_id = build_app_with_bash_helpers(os.path.join(TEST_APPS, 'prefix_patterns'), dxproj.get_id()) + applet_id = build_app_with_bash_helpers( + os.path.join(TEST_APPS, "prefix_patterns"), dxproj.get_id() + ) # Run the applet - applet_args = ['-iseq1=A.bar', - '-iseq2=A.json.dot.bar', - '-igene=A.vcf.pam', - '-imap=A.foo.bar', - '-imap2=fooxxx.bam', - '-imap3=A.bar', - '-imap4=A.bar.gz', - '-imulti=x13year23.sam'] - cmd_args = ['dx', 'run', '--yes', '--watch', applet_id] + applet_args = [ + "-iseq1=A.bar", + "-iseq2=A.json.dot.bar", + "-igene=A.vcf.pam", + "-imap=A.foo.bar", + "-imap2=fooxxx.bam", + "-imap3=A.bar", + "-imap4=A.bar.gz", + "-imulti=x13year23.sam", + ] + cmd_args = ["dx", "run", "--yes", "--watch", applet_id] cmd_args.extend(applet_args) run(cmd_args, env=env) def test_deepdirs(self): - ''' Tests the use of subdirectories in the output directory ''' + """Tests the use of subdirectories in the output directory""" + def check_output_key(job_output, out_param_name, num_files, dxproj): - ''' check that an output key appears, and has the correct number of files ''' - print('checking output for param={}'.format(out_param_name)) + """check that an output key appears, and has the correct number of files""" + print("checking output for param={}".format(out_param_name)) if out_param_name not in job_output: - raise "Error: key {} does not appear in the job output".format(out_param_name) + raise "Error: key {} does not appear in the job output".format( + out_param_name + ) dxlink_id_list = job_output[out_param_name] if not len(dxlink_id_list) == num_files: - raise Exception("Error: key {} should have {} files, but has {}". - format(out_param_name, num_files, len(dxlink_id_list))) + raise Exception( + "Error: key {} should have {} files, but has {}".format( + out_param_name, num_files, len(dxlink_id_list) + ) + ) def verify_files_in_dir(path, expected_filenames, dxproj): - ''' verify that a particular set of files resides in a directory ''' + """verify that a particular set of files resides in a directory""" dir_listing = dxproj.list_folder(folder=path, only="objects") for elem in dir_listing["objects"]: handler = dxpy.get_handler(elem["id"]) if not isinstance(handler, dxpy.DXFile): continue if handler.name not in expected_filenames: - raise Exception("Error: file {} should reside in directory {}". - format(handler.name, path)) - - with temporary_project('TestDXBashHelpers.test_app1 temporary project') as dxproj: + raise Exception( + "Error: file {} should reside in directory {}".format( + handler.name, path + ) + ) + + with temporary_project( + "TestDXBashHelpers.test_app1 temporary project" + ) as dxproj: env = update_environ(DX_PROJECT_CONTEXT_ID=dxproj.get_id()) # Build the applet, patching in the bash helpers from the # local checkout - applet_id = build_app_with_bash_helpers(os.path.join(TEST_APPS, 'deepdirs'), dxproj.get_id()) + applet_id = build_app_with_bash_helpers( + os.path.join(TEST_APPS, "deepdirs"), dxproj.get_id() + ) # Run the applet - cmd_args = ['dx', 'run', '--yes', '--brief', applet_id] + cmd_args = ["dx", "run", "--yes", "--brief", applet_id] job_id = run(cmd_args, env=env).strip() dxpy.DXJob(job_id).wait_on_done() @@ -405,39 +549,57 @@ def verify_files_in_dir(path, expected_filenames, dxproj): verify_files_in_dir("/clue2", ["Y_1.txt", "Y_2.txt", "Y_3.txt"], dxproj) verify_files_in_dir("/hint2", ["Z_1.txt", "Z_2.txt", "Z_3.txt"], dxproj) verify_files_in_dir("/foo/bar", ["luke.txt"], dxproj) - verify_files_in_dir("/", ["A.txt", "B.txt", "C.txt", "num_chrom.txt"], dxproj) + verify_files_in_dir( + "/", ["A.txt", "B.txt", "C.txt", "num_chrom.txt"], dxproj + ) -@unittest.skipUnless(testutil.TEST_RUN_JOBS and testutil.TEST_BENCHMARKS, - 'skipping tests that would run jobs, or, run benchmarks') +@unittest.skipUnless( + testutil.TEST_RUN_JOBS and testutil.TEST_BENCHMARKS, + "skipping tests that would run jobs, or, run benchmarks", +) +@unittest.skip("Temporarily disabled") class TestDXBashHelpersBenchmark(DXTestCase): def create_file_of_size(self, fname, size_bytes): - assert(size_bytes > 1); + assert size_bytes > 1 try: os.remove(fname) except: pass with open(fname, "wb") as out: out.seek(size_bytes - 1) - out.write('\0') + out.write("\0") def run_applet_with_flags(self, flag_list, num_files, file_size_bytes): - with temporary_project('TestDXBashHelpers.test_app1 temporary project') as dxproj: + with temporary_project( + "TestDXBashHelpers.test_app1 temporary project" + ) as dxproj: env = update_environ(DX_PROJECT_CONTEXT_ID=dxproj.get_id()) # Upload file - self.create_file_of_size("A.txt", file_size_bytes); - remote_file = dxpy.upload_local_file(filename="A.txt", project=dxproj.get_id(), folder='/') + self.create_file_of_size("A.txt", file_size_bytes) + remote_file = dxpy.upload_local_file( + filename="A.txt", project=dxproj.get_id(), folder="/" + ) # Build the applet, patching in the bash helpers from the # local checkout - applet_id = build_app_with_bash_helpers(os.path.join(TEST_APPS, 'benchmark'), dxproj.get_id()) + applet_id = build_app_with_bash_helpers( + os.path.join(TEST_APPS, "benchmark"), dxproj.get_id() + ) # Add several files to the output applet_args = [] - applet_args.extend(['-iref=A.txt'] * num_files) - cmd_args = ['dx', 'run', '--yes', '--watch', '--instance-type=mem1_ssd1_x2', applet_id] + applet_args.extend(["-iref=A.txt"] * num_files) + cmd_args = [ + "dx", + "run", + "--yes", + "--watch", + "--instance-type=mem1_ssd1_x2", + applet_id, + ] cmd_args.extend(applet_args) cmd_args.extend(flag_list) run(cmd_args, env=env) @@ -457,29 +619,39 @@ def test_par_100m(self): def test_par_1g(self): self.run_applet_with_flags(["-iparallel=true"], 10, 1024 * 1024 * 1024) + class TestDXJobutilAddOutput(DXTestCase): - data_obj_classes = ['file', 'record', 'applet', 'workflow'] - dummy_ids = [ (obj_class + '-' + DUMMY_HASH) for obj_class in data_obj_classes] + data_obj_classes = ["file", "record", "applet", "workflow"] + dummy_ids = [(obj_class + "-" + DUMMY_HASH) for obj_class in data_obj_classes] dummy_job_id = "job-" + DUMMY_HASH dummy_analysis_id = "analysis-123456789012345678901234" - test_cases = ([["32", 32], - ["3.4", 3.4], - ["true", True], - ["'32 tables'", "32 tables"], - ['\'{"foo": "bar"}\'', {"foo": "bar"}], - [dummy_job_id + ":foo", {"job": dummy_job_id, - "field": "foo"}], - [dummy_analysis_id + ":bar", - {"$dnanexus_link": {"analysis": dummy_analysis_id, - "field": "bar"}}]] + - [[dummy_id, {"$dnanexus_link": dummy_id}] for dummy_id in dummy_ids] + - [["'" + json.dumps({"$dnanexus_link": dummy_id}) + "'", - {"$dnanexus_link": dummy_id}] for dummy_id in dummy_ids]) + test_cases = ( + [ + ["32", 32], + ["3.4", 3.4], + ["true", True], + ["'32 tables'", "32 tables"], + ['\'{"foo": "bar"}\'', {"foo": "bar"}], + [dummy_job_id + ":foo", {"job": dummy_job_id, "field": "foo"}], + [ + dummy_analysis_id + ":bar", + {"$dnanexus_link": {"analysis": dummy_analysis_id, "field": "bar"}}, + ], + ] + + [[dummy_id, {"$dnanexus_link": dummy_id}] for dummy_id in dummy_ids] + + [ + [ + "'" + json.dumps({"$dnanexus_link": dummy_id}) + "'", + {"$dnanexus_link": dummy_id}, + ] + for dummy_id in dummy_ids + ] + ) def test_auto(self): - with tempfile.NamedTemporaryFile(mode='w+') as f: + with tempfile.NamedTemporaryFile(mode="w+") as f: # initialize the file with valid JSON - f.write('{}') + f.write("{}") f.flush() local_filename = f.name cmd_prefix = "dx-jobutil-add-output -o " + local_filename + " " @@ -491,9 +663,9 @@ def test_auto(self): self.assertEqual(result[str(i)], tc[1]) def test_auto_array(self): - with tempfile.NamedTemporaryFile(mode='w+') as f: + with tempfile.NamedTemporaryFile(mode="w+") as f: # initialize the file with valid JSON - f.write('{}') + f.write("{}") f.flush() local_filename = f.name cmd_prefix = "dx-jobutil-add-output --array -o " + local_filename + " " @@ -506,15 +678,17 @@ def test_auto_array(self): self.assertEqual(result[str(i)], [tc[1], tc[1]]) def test_class_specific(self): - with tempfile.NamedTemporaryFile(mode='w+') as f: + with tempfile.NamedTemporaryFile(mode="w+") as f: # initialize the file with valid JSON - f.write('{}') + f.write("{}") f.flush() local_filename = f.name cmd_prefix = "dx-jobutil-add-output -o " + local_filename + " " - class_test_cases = [["boolean", "t", True], - ["boolean", "1", True], - ["boolean", "0", False]] + class_test_cases = [ + ["boolean", "t", True], + ["boolean", "1", True], + ["boolean", "0", False], + ] for i, tc in enumerate(class_test_cases): run(cmd_prefix + " ".join([str(i), "--class " + tc[0], tc[1]])) f.seek(0) @@ -523,33 +697,42 @@ def test_class_specific(self): self.assertEqual(result[str(i)], tc[2]) def test_class_parsing_errors(self): - with tempfile.NamedTemporaryFile(mode='w+') as f: + with tempfile.NamedTemporaryFile(mode="w+") as f: # initialize the file with valid JSON - f.write('{}') + f.write("{}") f.flush() local_filename = f.name cmd_prefix = "dx-jobutil-add-output -o " + local_filename + " " - error_test_cases = ([["int", "3.4"], - ["int", "foo"], - ["float", "foo"], - ["boolean", "something"], - ["hash", "{]"], - ["jobref", "thing"], - ["analysisref", "thing"]] + - [[classname, - "'" + - json.dumps({"dnanexus_link": classname + "-" + DUMMY_HASH}) + - "'"] for classname in self.data_obj_classes]) + error_test_cases = [ + ["int", "3.4"], + ["int", "foo"], + ["float", "foo"], + ["boolean", "something"], + ["hash", "{]"], + ["jobref", "thing"], + ["analysisref", "thing"], + ] + [ + [ + classname, + "'" + + json.dumps({"dnanexus_link": classname + "-" + DUMMY_HASH}) + + "'", + ] + for classname in self.data_obj_classes + ] for i, tc in enumerate(error_test_cases): - with self.assertSubprocessFailure(stderr_regexp='Value could not be parsed', - exit_code=3): + with self.assertSubprocessFailure( + stderr_regexp="Value could not be parsed", exit_code=3 + ): run(cmd_prefix + " ".join([str(i), "--class " + tc[0], tc[1]])) class TestDXJobutilNewJob(DXTestCase): @classmethod def setUpClass(cls): - with testutil.temporary_project(name='dx-jobutil-new-job test project', cleanup=False) as p: + with testutil.temporary_project( + name="dx-jobutil-new-job test project", cleanup=False + ) as p: cls.aux_project = p @classmethod @@ -559,14 +742,18 @@ def tearDownClass(cls): def assertNewJobInputHash(self, cmd_snippet, arguments_hash): cmd = "dx-jobutil-new-job entrypointname " + cmd_snippet + " --test" expected_job_input = {"function": "entrypointname", "input": {}} - env = override_environment(DX_JOB_ID="job-000000000000000000000001", DX_WORKSPACE_ID=self.project) + env = override_environment( + DX_JOB_ID="job-000000000000000000000001", DX_WORKSPACE_ID=self.project + ) output = run(cmd, env=env) expected_job_input.update(arguments_hash) self.assertEqual(json.loads(output), expected_job_input) def assertNewJobError(self, cmd_snippet, exit_code): cmd = "dx-jobutil-new-job entrypointname " + cmd_snippet + " --test" - env = override_environment(DX_JOB_ID="job-000000000000000000000001", DX_WORKSPACE_ID=self.project) + env = override_environment( + DX_JOB_ID="job-000000000000000000000001", DX_WORKSPACE_ID=self.project + ) with self.assertSubprocessFailure(exit_code=exit_code): run(cmd, env=env) @@ -576,14 +763,22 @@ def test_input(self): dxpy.new_dxrecord(name="duplicate_name_record") dxpy.new_dxrecord(name="duplicate_name_record") # In a different project... - third_record = dxpy.new_dxrecord(name="third_record", project=self.aux_project.get_id()) + third_record = dxpy.new_dxrecord( + name="third_record", project=self.aux_project.get_id() + ) test_cases = ( # string ("-ifoo=input_string", {"foo": "input_string"}), # string that looks like a {job,analysis} ID - ("-ifoo=job-012301230123012301230123", {"foo": "job-012301230123012301230123"}), - ("-ifoo=analysis-012301230123012301230123", {"foo": "analysis-012301230123012301230123"}), + ( + "-ifoo=job-012301230123012301230123", + {"foo": "job-012301230123012301230123"}, + ), + ( + "-ifoo=analysis-012301230123012301230123", + {"foo": "analysis-012301230123012301230123"}, + ), # int ("-ifoo=24", {"foo": 24}), # float @@ -592,32 +787,75 @@ def test_input(self): ('-ifoo=\'{"a": "b"}\'', {"foo": {"a": "b"}}), ('-ifoo=\'["a", "b"]\'', {"foo": ["a", "b"]}), # objectName - ("-ifoo=first_record", {"foo": dxpy.dxlink(first_record.get_id(), self.project)}), + ( + "-ifoo=first_record", + {"foo": dxpy.dxlink(first_record.get_id(), self.project)}, + ), # objectId - ("-ifoo=" + first_record.get_id(), {"foo": dxpy.dxlink(first_record.get_id())}), + ( + "-ifoo=" + first_record.get_id(), + {"foo": dxpy.dxlink(first_record.get_id())}, + ), # project:objectName - ("-ifoo=" + self.aux_project.get_id() + ":third_record", - {"foo": dxpy.dxlink(third_record.get_id(), self.aux_project.get_id())}), + ( + "-ifoo=" + self.aux_project.get_id() + ":third_record", + {"foo": dxpy.dxlink(third_record.get_id(), self.aux_project.get_id())}, + ), # project:objectId - ("-ifoo=" + self.aux_project.get_id() + ":" + third_record.get_id(), - {"foo": dxpy.dxlink(third_record.get_id(), self.aux_project.get_id())}), + ( + "-ifoo=" + self.aux_project.get_id() + ":" + third_record.get_id(), + {"foo": dxpy.dxlink(third_record.get_id(), self.aux_project.get_id())}, + ), # same, but wrong project is specified - ("-ifoo=" + self.project + ":" + third_record.get_id(), - {"foo": dxpy.dxlink(third_record.get_id(), self.aux_project.get_id())}), + ( + "-ifoo=" + self.project + ":" + third_record.get_id(), + {"foo": dxpy.dxlink(third_record.get_id(), self.aux_project.get_id())}, + ), # glob ("-ifoo=first*", {"foo": dxpy.dxlink(first_record.get_id(), self.project)}), # JBOR - ("-ifoo=job-012301230123012301230123:outputfield", - {"foo": {"$dnanexus_link": {"job": "job-012301230123012301230123", "field": "outputfield"}}}), + ( + "-ifoo=job-012301230123012301230123:outputfield", + { + "foo": { + "$dnanexus_link": { + "job": "job-012301230123012301230123", + "field": "outputfield", + } + } + }, + ), # order of inputs is preserved from command line to API call - ("-ifoo=first* -ifoo=second_record -ifoo=job-012301230123012301230123:outputfield", - {"foo": [dxpy.dxlink(first_record.get_id(), self.project), - dxpy.dxlink(second_record.get_id(), self.project), - {"$dnanexus_link": {"job": "job-012301230123012301230123", "field": "outputfield"}}]}), - ("-ifoo=job-012301230123012301230123:outputfield -ifoo=first_record -ifoo=second_*", - {"foo": [{"$dnanexus_link": {"job": "job-012301230123012301230123", "field": "outputfield"}}, - dxpy.dxlink(first_record.get_id(), self.project), - dxpy.dxlink(second_record.get_id(), self.project)]}), + ( + "-ifoo=first* -ifoo=second_record -ifoo=job-012301230123012301230123:outputfield", + { + "foo": [ + dxpy.dxlink(first_record.get_id(), self.project), + dxpy.dxlink(second_record.get_id(), self.project), + { + "$dnanexus_link": { + "job": "job-012301230123012301230123", + "field": "outputfield", + } + }, + ] + }, + ), + ( + "-ifoo=job-012301230123012301230123:outputfield -ifoo=first_record -ifoo=second_*", + { + "foo": [ + { + "$dnanexus_link": { + "job": "job-012301230123012301230123", + "field": "outputfield", + } + }, + dxpy.dxlink(first_record.get_id(), self.project), + dxpy.dxlink(second_record.get_id(), self.project), + ] + }, + ), # if there is any ambiguity, the name is left unresolved ("-ifoo=duplicate_name_record", {"foo": "duplicate_name_record"}), ("-ifoo=*record", {"foo": "*record"}), @@ -627,23 +865,29 @@ def test_input(self): ("-ifoo:string=first_record", {"foo": "first_record"}), ('-ifoo:hash=\'{"a": "b"}\'', {"foo": {"a": "b"}}), ('-ifoo:hash=\'["a", "b"]\'', {"foo": ["a", "b"]}), - # Array inputs - # implicit array notation ("-ifoo=24 -ifoo=25", {"foo": [24, 25]}), ("-ifoo=25 -ibar=1 -ifoo=24", {"foo": [25, 24], "bar": 1}), - ("-ifoo=first_record -ifoo=second_record", - {"foo": [dxpy.dxlink(first_record.get_id(), self.project), - dxpy.dxlink(second_record.get_id(), self.project)]}), + ( + "-ifoo=first_record -ifoo=second_record", + { + "foo": [ + dxpy.dxlink(first_record.get_id(), self.project), + dxpy.dxlink(second_record.get_id(), self.project), + ] + }, + ), # different types (unusual, but potentially meaningful if # foo is a json input) ("-ifoo=24 -ifoo=bar", {"foo": [24, "bar"]}), - # explicit array notation is NOT respected (in contexts with # no inputSpec such as this one) ("-ifoo:array:int=24", {"foo": 24}), - ("-ifoo:array:record=first_record", {"foo": dxpy.dxlink(first_record.get_id(), self.project)}), + ( + "-ifoo:array:record=first_record", + {"foo": dxpy.dxlink(first_record.get_id(), self.project)}, + ), ) for cmd_snippet, expected_input_hash in test_cases: @@ -662,17 +906,56 @@ def test_job_arguments(self): ("--name JobName", {"name": "JobName"}), # depends-on - array of strings ("--depends-on foo bar baz", {"dependsOn": ["foo", "bar", "baz"]}), + # headJobOnDemand - boolean + ("--head-job-on-demand", {"headJobOnDemand": True}), # instance type: single instance - string - ("--instance-type foo_bar_baz", - {"systemRequirements": {"entrypointname": { "instanceType": "foo_bar_baz" }}}), + ( + "--instance-type foo_bar_baz", + { + "systemRequirements": { + "entrypointname": {"instanceType": "foo_bar_baz"} + } + }, + ), # instance type: mapping ("--instance-type " + - pipes.quote(json.dumps({"main": "mem2_hdd2_x2" , "other_function": "mem2_hdd2_x1" })), + shlex.quote(json.dumps({"main": "mem2_hdd2_x2" , "other_function": "mem2_hdd2_x1" })), {"systemRequirements": {"main": { "instanceType": "mem2_hdd2_x2" }, "other_function": { "instanceType": "mem2_hdd2_x1" }}}), + ("--instance-type-by-executable " + + shlex.quote(json.dumps({"my_applet": {"main": "mem2_hdd2_x2", + "other_function": "mem3_ssd2_fpga1_x8"}})), + {"systemRequirementsByExecutable": {"my_applet": {"main": {"instanceType": "mem2_hdd2_x2"}, + "other_function": {"instanceType": "mem3_ssd2_fpga1_x8"}}}}), + ("--instance-type-by-executable " + + shlex.quote(json.dumps({"my_applet": {"main": "mem1_ssd1_v2_x2", + "other_function": "mem3_ssd2_fpga1_x8"}})) + + " --extra-args " + + shlex.quote(json.dumps({"systemRequirementsByExecutable": {"my_applet": {"main": {"instanceType": "mem2_hdd2_x2", "clusterSpec": {"initialInstanceCount": 3}}, + "other_function": {"fpgaDriver": "edico-1.4.5"}}}})), + {"systemRequirementsByExecutable": {"my_applet":{"main": { "instanceType": "mem2_hdd2_x2", "clusterSpec":{"initialInstanceCount": 3}}, + "other_function": { "instanceType": "mem3_ssd2_fpga1_x8", "fpgaDriver": "edico-1.4.5"} }}}), + # nvidia driver + ("--instance-type-by-executable " + + shlex.quote(json.dumps({ + "my_applet": { + "main": "mem1_ssd1_v2_x2", + "other_function": "mem2_ssd1_gpu_x16"}})) + + " --extra-args " + + shlex.quote(json.dumps({ + "systemRequirementsByExecutable": { + "my_applet": { + "main": {"instanceType": "mem2_hdd2_x2"}, + "other_function": {"nvidiaDriver": "R535"}}}})), + {"systemRequirementsByExecutable": { + "my_applet": {"main": {"instanceType": "mem2_hdd2_x2"}, + "other_function": {"instanceType": "mem2_ssd1_gpu_x16", + "nvidiaDriver": "R535"}}}}), # properties - mapping - ("--property foo=foo_value --property bar=bar_value", - {"properties": {"foo": "foo_value", "bar": "bar_value"}}), + ( + "--property foo=foo_value --property bar=bar_value", + {"properties": {"foo": "foo_value", "bar": "bar_value"}}, + ), # tags - array of strings ("--tag foo --tag bar --tag baz", {"tags": ["foo", "bar", "baz"]}), ) @@ -680,13 +963,16 @@ def test_job_arguments(self): self.assertNewJobInputHash(cmd_snippet, arguments_hash) def test_extra_arguments(self): - cmd_snippet = "--extra-args " + pipes.quote( - json.dumps({"details": {"d1": "detail1", "d2": 1234}, "foo": "foo_value"})) + cmd_snippet = "--extra-args " + shlex.quote( + json.dumps({"details": {"d1": "detail1", "d2": 1234}, "foo": "foo_value"}) + ) arguments_hash = {"details": {"d1": "detail1", "d2": 1234}, "foo": "foo_value"} self.assertNewJobInputHash(cmd_snippet, arguments_hash) # override previously specified args - cmd_snippet = "--name JobName --extra-args " + pipes.quote(json.dumps({"name": "FinalName"})) + cmd_snippet = "--name JobName --extra-args " + shlex.quote( + json.dumps({"name": "FinalName"}) + ) arguments_hash = {"name": "FinalName"} self.assertNewJobInputHash(cmd_snippet, arguments_hash) @@ -701,15 +987,19 @@ def test_bad_arguments(self): class TestDXBashHelperMethods(unittest.TestCase): def test_limit_threads(self): - ''' Tests that the number of threads used for downloading inputs in parallel is limited ''' + """Tests that the number of threads used for downloading inputs in parallel is limited""" instance_types = InstanceTypesCompleter().instance_types max_threads = 8 for inst in instance_types.values(): - num_threads = _get_num_parallel_threads(max_threads, inst.CPU_Cores, inst.Memory_GB*1024) + num_threads = _get_num_parallel_threads( + max_threads, inst.CPU_Cores, inst.Memory_GB * 1024 + ) self.assertTrue(num_threads >= 1 and num_threads <= max_threads) self.assertTrue(num_threads <= inst.CPU_Cores) - self.assertTrue(num_threads*1200 <= inst.Memory_GB*1024 or num_threads == 1) + self.assertTrue( + num_threads * 1200 <= inst.Memory_GB * 1024 or num_threads == 1 + ) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/src/python/test/test_dx_completion.py b/src/python/test/test_dx_completion.py index 3788d9852f..14f244ca27 100755 --- a/src/python/test/test_dx_completion.py +++ b/src/python/test/test_dx_completion.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # # Copyright (C) 2013-2016 DNAnexus, Inc. # @@ -43,6 +43,7 @@ def tearDownClass(cls): dxpy.api.project_destroy(cls.project_id) for entity_id in cls.ids_to_destroy: dxpy.DXHTTPRequest("/" + entity_id + "/destroy", {}) + dxpy.set_workspace_id(None) def setUp(self): os.environ['IFS'] = IFS @@ -60,7 +61,7 @@ def tearDown(self): if 'completed' not in resp: raise DXError('Error removing folder') completed = resp['completed'] - for var in 'IFS', '_ARGCOMPLETE', '_DX_ARC_DEBUG', 'COMP_WORDBREAKS': + for var in 'IFS', '_ARGCOMPLETE', '_DX_ARC_DEBUG', 'COMP_WORDBREAKS', 'DX_PROJECT_CONTEXT_ID': if var in os.environ: del os.environ[var] @@ -150,7 +151,7 @@ def test_workflow_completion(self): def test_project_completion(self): self.ids_to_destroy.append(dxpy.api.project_new({"name": "to select"})['id']) self.assert_completion("dx select to", "to select\\:") - self.assert_completion("dx select to\ sele", "to select\\:") + self.assert_completion(r"dx select to\ sele", "to select\\:") def test_completion_with_bad_current_project(self): os.environ['DX_PROJECT_CONTEXT_ID'] = '' diff --git a/src/python/test/test_dx_symlink.py b/src/python/test/test_dx_symlink.py index 7abc66e4c1..946f72924f 100755 --- a/src/python/test/test_dx_symlink.py +++ b/src/python/test/test_dx_symlink.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # -*- coding: utf-8 -*- # # Copyright (C) 2013-2019 DNAnexus, Inc. @@ -30,6 +30,7 @@ from dxpy.exceptions import err_exit from dxpy.utils import describe from dxpy_testutil import (chdir, run, TEST_ISOLATED_ENV) +from dxpy.compat import USING_PYTHON2 def setUpTempProject(thing): thing.old_workspace_id = dxpy.WORKSPACE_ID @@ -75,7 +76,7 @@ def md5_checksum(filename): result = result.decode("ascii") return result - +@unittest.skipIf(USING_PYTHON2, 'Python 2 image does not contain aria2c') class TestSymlink(unittest.TestCase): def setUp(self): self.wd = tempfile.mkdtemp() diff --git a/src/python/test/test_dxabs.py b/src/python/test/test_dxabs.py index 7a63cacd92..22d0a716f8 100755 --- a/src/python/test/test_dxabs.py +++ b/src/python/test/test_dxabs.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # -*- coding: utf-8 -*- # # Copyright (C) 2016 DNAnexus, Inc. diff --git a/src/python/test/test_dxasset.py b/src/python/test/test_dxasset.py index acb70a23f3..959a470622 100755 --- a/src/python/test/test_dxasset.py +++ b/src/python/test/test_dxasset.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # -*- coding: utf-8 -*- # # Copyright (C) 2016 DNAnexus, Inc. @@ -102,7 +102,7 @@ def test_build_asset_with_no_dxasset_json(self): def test_build_asset_with_malformed_dxasset_json(self): asset_dir = self.write_asset_directory("asset_with_malform_json", "{") - with self.assertSubprocessFailure(stderr_regexp='Could not parse dxasset\.json', exit_code=1): + with self.assertSubprocessFailure(stderr_regexp=r'Could not parse dxasset\.json', exit_code=1): run("dx build_asset " + asset_dir) @unittest.skipUnless(testutil.TEST_RUN_JOBS, 'skipping test that would run jobs') @@ -362,17 +362,20 @@ def test_build_asset_inside_job(self): @unittest.skipUnless(testutil.TEST_RUN_JOBS, 'skipping test that would run jobs') def test_get_appet_with_asset(self): bundle_name = "test-bundle-depends.tar.gz" + asset_name = "test-asset-depends.tar.gz" bundle_tmp_dir = tempfile.mkdtemp() os.mkdir(os.path.join(bundle_tmp_dir, "a")) with open(os.path.join(bundle_tmp_dir, 'a', 'foo.txt'), 'w') as file_in_bundle: file_in_bundle.write('foo\n') subprocess.check_call(['tar', '-czf', os.path.join(bundle_tmp_dir, bundle_name), '-C', os.path.join(bundle_tmp_dir, 'a'), '.']) + subprocess.check_call(['tar', '-czf', os.path.join(bundle_tmp_dir, asset_name), + '-C', os.path.join(bundle_tmp_dir, 'a'), '.']) bundle_file = dxpy.upload_local_file(filename=os.path.join(bundle_tmp_dir, bundle_name), project=self.project, wait_on_close=True) - asset_file = dxpy.upload_local_file(filename=os.path.join(bundle_tmp_dir, bundle_name), + asset_file = dxpy.upload_local_file(filename=os.path.join(bundle_tmp_dir, asset_name), project=self.project, wait_on_close=True) @@ -413,14 +416,12 @@ def test_get_appet_with_asset(self): self.assertTrue(os.path.exists(os.path.join("asset_depends", "dxapp.json"))) with open(os.path.join("asset_depends", "dxapp.json")) as fh: applet_spec = json.load(fh) - self.assertEqual([{"name": "asset-lib-test", - "project": self.project, - "folder": "/", - "version": "0.0.1"} - ], - applet_spec["runSpec"]["assetDepends"]) - self.assertEqual([{"name": bundle_name, "id": {"$dnanexus_link": bundle_file.get_id()}}], - applet_spec["runSpec"]["bundledDepends"]) + current_region = dxpy.describe(self.project).get("region") + regional_options = applet_spec["regionalOptions"][current_region] + self.assertIn({"name": bundle_name, "id": {"$dnanexus_link": bundle_file.get_id()}}, + regional_options["bundledDepends"]) + self.assertIn({"name": asset_name, "id": {"$dnanexus_link": asset_file.get_id()}}, + regional_options["bundledDepends"]) @unittest.skipUnless(testutil.TEST_RUN_JOBS, 'skipping test that would run jobs') def test_set_assetbundle_tarball_property(self): diff --git a/src/python/test/test_dxclient.py b/src/python/test/test_dxclient.py old mode 100644 new mode 100755 index e430806b25..1592147fe6 --- a/src/python/test/test_dxclient.py +++ b/src/python/test/test_dxclient.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # -*- coding: utf-8 -*- # # Copyright (C) 2013-2016 DNAnexus, Inc. @@ -21,7 +21,7 @@ import os, sys, unittest, json, tempfile, subprocess, shutil, re, base64, random, time import filecmp -import pipes +import shlex import stat import hashlib import collections @@ -209,11 +209,11 @@ def test_remove_folders(self): class TestApiDebugOutput(DXTestCase): def test_dx_debug_shows_request_id(self): (stdout, stderr) = run("_DX_DEBUG=1 dx ls", also_return_stderr=True) - self.assertRegex(stderr, "POST \d{13}-\d{1,6} http", + self.assertRegex(stderr, r"POST \d{13}-\d{1,6} http", msg="stderr does not appear to contain request ID") def test_dx_debug_shows_timestamp(self): - timestamp_regex = "\[\d{1,15}\.\d{0,8}\]" + timestamp_regex = r"\[\d{1,15}\.\d{0,8}\]" (stdout, stderr) = run("_DX_DEBUG=1 dx ls", also_return_stderr=True) self.assertRegex(stderr, timestamp_regex, msg="Debug log does not contain a timestamp") @@ -320,7 +320,7 @@ def test_dx_api(self): fd.close() run("dx api {p} describe --input {fn}".format(p=self.project, fn=fd.name)) - @pytest.mark.TRACEABILITY_MATRIX + @pytest.mark.TRACEABILITY_ISOLATED_ENV @testutil.update_traceability_matrix(["DNA_CLI_PROJ_INVITE_USER"]) @unittest.skipUnless(testutil.TEST_NO_RATE_LIMITS, 'skipping tests that need rate limits to be disabled') @@ -332,7 +332,7 @@ def test_dx_invite(self): with self.assertSubprocessFailure(stderr_regexp="invalid choice", exit_code=2): run(("dx invite alice.nonexistent : ПРОСМОТР").format(p=self.project)) - @pytest.mark.TRACEABILITY_MATRIX + @pytest.mark.TRACEABILITY_ISOLATED_ENV @testutil.update_traceability_matrix(["DNA_CLI_PROJ_REVOKE_USER_PERMISSIONS"]) @unittest.skipUnless(testutil.TEST_NO_RATE_LIMITS, 'skipping tests that need rate limits to be disabled') @@ -380,14 +380,14 @@ def test_dx_set_details_with_file(self): # Test -f with valid JSON file. record_id = run("dx new record Ψ2 --brief").strip() - run("dx set_details Ψ2 -f " + pipes.quote(tmp_file.name)) + run("dx set_details Ψ2 -f " + shlex.quote(tmp_file.name)) dxrecord = dxpy.DXRecord(record_id) details = dxrecord.get_details() self.assertEqual({"foo": "bar"}, details, msg="dx set_details -f with valid JSON input file failed.") # Test --details-file with valid JSON file. record_id = run("dx new record Ψ3 --brief").strip() - run("dx set_details Ψ3 --details-file " + pipes.quote(tmp_file.name)) + run("dx set_details Ψ3 --details-file " + shlex.quote(tmp_file.name)) dxrecord = dxpy.DXRecord(record_id) details = dxrecord.get_details() self.assertEqual({"foo": "bar"}, details, @@ -400,16 +400,16 @@ def test_dx_set_details_with_file(self): # Test above with invalid JSON file. record_id = run("dx new record Ψ4 --brief").strip() with self.assertSubprocessFailure(stderr_regexp="JSON", exit_code=3): - run("dx set_details Ψ4 -f " + pipes.quote(tmp_invalid_file.name)) + run("dx set_details Ψ4 -f " + shlex.quote(tmp_invalid_file.name)) # Test command with (-f or --details-file) and CL JSON. with self.assertSubprocessFailure(stderr_regexp="Error: Cannot provide both -f/--details-file and details", exit_code=3): - run("dx set_details Ψ4 '{ \"foo\":\"bar\" }' -f " + pipes.quote(tmp_file.name)) + run("dx set_details Ψ4 '{ \"foo\":\"bar\" }' -f " + shlex.quote(tmp_file.name)) # Test piping JSON from STDIN. record_id = run("dx new record Ψ5 --brief").strip() - run("cat " + pipes.quote(tmp_file.name) + " | dx set_details Ψ5 -f -") + run("cat " + shlex.quote(tmp_file.name) + " | dx set_details Ψ5 -f -") dxrecord = dxpy.DXRecord(record_id) details = dxrecord.get_details() self.assertEqual({"foo": "bar"}, details, msg="dx set_details -f - with valid JSON input failed.") @@ -656,7 +656,7 @@ def test_dx_remove_project_by_name(self): self.assertEqual(run("dx find projects --brief --name {name}".format(name=project_name)), "") @unittest.skipUnless(testutil.TEST_ISOLATED_ENV, 'skipping test that requires presence of test user') - @pytest.mark.TRACEABILITY_MATRIX + @pytest.mark.TRACEABILITY_ISOLATED_ENV @testutil.update_traceability_matrix(["DNA_API_PROJ_VIEW_SHAREES","DNA_API_PROJ_ADD_USERS"]) def test_dx_project_invite_without_email(self): user_id = 'user-bob' @@ -903,7 +903,6 @@ def _test_dx_ssh(self, project, instance_type): runSpec={"code": "sleep 1200", "interpreter": "bash", "distribution": "Ubuntu", "release": "20.04", "version":"0", - "execDepends": [{"name": "dx-toolkit"}], "systemRequirements": {"*": {"instanceType": instance_type}}}, inputSpec=[], outputSpec=[], dxapi="1.0.0", version="1.0.0", @@ -1123,7 +1122,6 @@ def test_dx_run_debug_on(self): crash_applet = dxpy.api.applet_new(dict(name="crash", runSpec={"code": "exit 5", "interpreter": "bash", "distribution": "Ubuntu", "release": "20.04", "version": "0", - "execDepends": [{"name": "dx-toolkit"}], "systemRequirements": {"*": {"instanceType": "mem2_ssd1_v2_x2"}}}, inputSpec=[], outputSpec=[], dxapi="1.0.0", version="1.0.0", @@ -1153,8 +1151,8 @@ def test_dx_run_debug_on_all(self): with self.configure_ssh() as wd: crash_applet = dxpy.api.applet_new(dict(name="crash", runSpec={"code": "exit 5", "interpreter": "bash", - "distribution": "Ubuntu", "release": "20.04", "version":"0", - "execDepends": [{"name": "dx-toolkit"}]}, + "distribution": "Ubuntu", "release": "20.04", "version":"0" + }, inputSpec=[], outputSpec=[], dxapi="1.0.0", version="1.0.0", project=self.project))["id"] @@ -1164,6 +1162,121 @@ def test_dx_run_debug_on_all(self): job_desc = dxpy.describe(job_id) self.assertEqual(job_desc["debug"]['debugOn'], ['AppError', 'AppInternalError', 'ExecutionError']) + @unittest.skipUnless(testutil.TEST_RUN_JOBS, "Skipping test that would run jobs") + def test_dx_run_allow_ssh(self): + with self.configure_ssh() as wd: + applet_id = dxpy.api.applet_new({"project": self.project, + "dxapi": "1.0.0", + "runSpec": {"interpreter": "bash", + "distribution": "Ubuntu", + "release": "20.04", + "version": "0", + "code": "sleep 60"} + })['id'] + # Single IP + allow_ssh = {"1.2.3.4"} + job_id = run("dx run {} --yes --brief --allow-ssh 1.2.3.4".format(applet_id), + env=override_environment(HOME=wd)).strip() + job_desc = dxpy.describe(job_id) + job_allow_ssh = job_desc['allowSSH'] + self.assertEqual(allow_ssh, set(job_allow_ssh)) + run("dx terminate {}".format(job_id), env=override_environment(HOME=wd)) + + # Multiple IPs + allow_ssh = {"1.2.3.4", "5.6.7.8"} + job_id = run("dx run {} --yes --brief --allow-ssh 1.2.3.4 --allow-ssh 5.6.7.8".format(applet_id), + env=override_environment(HOME=wd)).strip() + job_desc = dxpy.describe(job_id) + job_allow_ssh = job_desc['allowSSH'] + self.assertEqual(allow_ssh, set(job_allow_ssh)) + run("dx terminate {}".format(job_id), env=override_environment(HOME=wd)) + + # Get client IP from system/whoami + client_ip = dxpy.api.system_whoami({"fields": {"clientIp": True}}).get('clientIp') + + # dx run --ssh automatically retrieves and adds client IP + allow_ssh = {client_ip} + job_id = run("dx run {} --yes --brief --allow-ssh ".format(applet_id), + env=override_environment(HOME=wd)).strip() + job_desc = dxpy.describe(job_id) + job_allow_ssh = job_desc['allowSSH'] + self.assertEqual(allow_ssh, set(job_allow_ssh)) + run("dx terminate {}".format(job_id), env=override_environment(HOME=wd)) + + # dx run --allow-ssh --allow-ssh 1.2.3.4 automatically retrieves and adds client IP + allow_ssh = {"1.2.3.4", client_ip} + job_id = run("dx run {} --yes --brief --allow-ssh 1.2.3.4 --allow-ssh ".format(applet_id), + env=override_environment(HOME=wd)).strip() + job_desc = dxpy.describe(job_id) + job_allow_ssh = job_desc['allowSSH'] + self.assertEqual(allow_ssh, set(job_allow_ssh)) + run("dx terminate {}".format(job_id), env=override_environment(HOME=wd)) + + @unittest.skipUnless(testutil.TEST_RUN_JOBS, "Skipping test that would run jobs") + def test_dx_ssh_allow_ssh(self): + with self.configure_ssh() as wd: + applet_id = dxpy.api.applet_new({"project": self.project, + "dxapi": "1.0.0", + "runSpec": {"interpreter": "bash", + "distribution": "Ubuntu", + "release": "20.04", + "version": "0", + "code": "sleep 60"} + })['id'] + job_id = run("dx run {} --yes --brief".format(applet_id), + env=override_environment(HOME=wd)).strip() + job_desc = dxpy.describe(job_id) + # No SSH access by default + self.assertIsNone(job_desc.get('allowSSH', None)) + + client_ip = dxpy.api.system_whoami({"fields": {"clientIp": True}}).get('clientIp') + allow_ssh = {client_ip} + # dx ssh retrieves client IP and adds it with job-xxxx/update + dx = pexpect.spawn("dx ssh " + job_id, + env=override_environment(HOME=wd), + **spawn_extra_args) + time.sleep(3) + dx.close() + job_allow_ssh = dxpy.describe(job_id)['allowSSH'] + self.assertEqual(allow_ssh, set(job_allow_ssh)) + + allow_ssh = {client_ip, "1.2.3.4"} + # dx ssh --allow-ssh 1.2.3.4 adds IP with job-xxxx/update + dx1 = pexpect.spawn("dx ssh --allow-ssh 1.2.3.4 " + job_id, + env=override_environment(HOME=wd), + **spawn_extra_args) + time.sleep(3) + dx1.close() + job_allow_ssh = dxpy.describe(job_id)['allowSSH'] + self.assertEqual(allow_ssh, set(job_allow_ssh)) + run("dx terminate {}".format(job_id), env=override_environment(HOME=wd)) + + # dx ssh --no-firewall-update does not add client IP + allow_ssh = {"1.2.3.4"} + job_id = run("dx run {} --yes --brief --allow-ssh 1.2.3.4".format(applet_id), + env=override_environment(HOME=wd)).strip() + dx2 = pexpect.spawn("dx ssh --no-firewall-update " + job_id, + env=override_environment(HOME=wd), + **spawn_extra_args) + time.sleep(3) + dx2.close() + job_allow_ssh = dxpy.describe(job_id)['allowSSH'] + self.assertEqual(allow_ssh, set(job_allow_ssh)) + run("dx terminate {}".format(job_id), env=override_environment(HOME=wd)) + + # dx ssh --ssh-proxy adds client IP and proxy IP + allow_ssh = {client_ip, "5.6.7.8"} + job_id = run("dx run {} --yes --brief".format(applet_id), + env=override_environment(HOME=wd)).strip() + dx3 = pexpect.spawn("dx ssh --ssh-proxy 5.6.7.8:22 " + job_id, + env=override_environment(HOME=wd), + **spawn_extra_args) + time.sleep(3) + dx3.close() + job_allow_ssh = dxpy.describe(job_id)['allowSSH'] + self.assertEqual(allow_ssh, set(job_allow_ssh)) + run("dx terminate {}".format(job_id), env=override_environment(HOME=wd)) + @pytest.mark.TRACEABILITY_MATRIX @testutil.update_traceability_matrix(["DNA_CLI_HELP_JUPYTER_NOTEBOOK"]) @unittest.skipUnless(testutil.TEST_RUN_JOBS, "Skipping test that would run jobs") @@ -1257,7 +1370,7 @@ def test_dx_http_request_handles_auth_errors(self): max_retries=0) def test_dx_api_error_msg(self): - error_regex = "Request Time=\d{1,15}\.\d{0,8}, Request ID=\d{13}-\d{1,6}" + error_regex = r"Request Time=\d{1,15}\.\d{0,8}, Request ID=\d{13}-\d{1,6}" with self.assertSubprocessFailure(stderr_regexp=error_regex, exit_code=3): run("dx api file-InvalidFileID describe") @@ -1605,6 +1718,14 @@ def test_dx_upload_mult_paths_with_dest(self): self.assertIn(os.path.basename(fd.name), listing) listing = run("dx ls /destdir/a").split("\n") self.assertIn(os.path.basename(fd2.name), listing) + + def test_dx_upload_mult_hidden(self): + with testutil.TemporaryFile() as fd: + with testutil.TemporaryFile() as fd2: + with temporary_project("test_dx_upload_mult_hidden", select=True) as p: + stdout = run("dx upload {} {} --visibility hidden".format(fd.name, fd2.name)) + self.assertIn("hidden", stdout) + self.assertNotIn("visible", stdout) def test_dx_upload_empty_file(self): with testutil.TemporaryFile() as fd: @@ -1651,8 +1772,8 @@ def main(): "dxapi": "1.0.0", "inputSpec": [], "outputSpec": [{"name": test_file_name, "class": "file"}], - "runSpec": {"code": code_str, "interpreter": "python2.7", - "distribution": "Ubuntu", "release": "14.04"}, + "runSpec": {"code": code_str, "interpreter": "python3", + "distribution": "Ubuntu", "release": "20.04", "version": "0"}, "version": "1.0.0"} applet_id = dxpy.api.applet_new(app_spec)['id'] applet = dxpy.DXApplet(applet_id) @@ -2152,7 +2273,7 @@ def test_user_describe_self_shows_bill_to(self): @unittest.skipUnless(testutil.TEST_ISOLATED_ENV, 'skipping test that would create apps') - @pytest.mark.TRACEABILITY_MATRIX + @pytest.mark.TRACEABILITY_ISOLATED_ENV @testutil.update_traceability_matrix(["DNA_API_APP_DELETE"]) def test_describe_deleted_app(self): applet_id = dxpy.api.applet_new({"project": self.project, @@ -2171,11 +2292,42 @@ def test_describe_deleted_app(self): # make second app with no default tag app_new_output2 = dxpy.api.app_new({"name": "app_to_delete", "applet": applet_id, - "version": "1.0.1"}) + "version": "1.0.1"}) dxpy.api.app_delete(app_new_output2["id"]) run("dx describe " + app_new_output2["id"]) + @pytest.mark.TRACEABILITY_MATRIX + @testutil.update_traceability_matrix(["DNA_CLI_APPLET_DESCRIBE"]) + def test_describe_applet_with_bundled_objects(self): + # create bundledDepends: applet, workflow, file as asset record + bundled_applet_id = self.test_applet_id + bundled_wf_id = self.create_workflow(self.project).get_id() + bundled_file_id = create_file_in_project("my_file", self.project) + bundled_record_details = {"archiveFileId": {"$dnanexus_link": bundled_file_id}} + bundled_record_id = dxpy.new_dxrecord(project=self.project, folder="/", types=["AssetBundle"], + details=bundled_record_details, name="my_record", close=True).get_id() + dxpy.DXFile(bundled_file_id).set_properties({"AssetBundle": bundled_record_id}) + + caller_applet_spec = self.create_applet_spec(self.project) + caller_applet_spec["name"] = "caller_applet" + caller_applet_spec["runSpec"]["bundledDepends"] = [{"name": "my_first_applet", "id": {"$dnanexus_link": bundled_applet_id}}, + {"name": "my_workflow", "id": {"$dnanexus_link": bundled_wf_id}}, + {"name": "my_file", "id": {"$dnanexus_link": bundled_file_id}}] + caller_applet_id = dxpy.api.applet_new(caller_applet_spec)['id'] + + # "dx describe output" should have applet/workflow/record ids in bundledDepends + caller_applet_desc = run('dx describe {}'.format(caller_applet_id)).replace(' ', '').replace('\n', '') + self.assertIn(bundled_applet_id, caller_applet_desc) + self.assertIn(bundled_wf_id, caller_applet_desc) + self.assertIn(bundled_record_id, caller_applet_desc) + + # "dx describe --json" output should have applet/workflow/file ids in bundledDepends + caller_applet_desc_json = run('dx describe {} --json'.format(caller_applet_id)) + self.assertIn(bundled_applet_id, caller_applet_desc_json) + self.assertIn(bundled_wf_id, caller_applet_desc_json) + self.assertIn(bundled_file_id, caller_applet_desc_json) + @unittest.skipUnless(testutil.TEST_ISOLATED_ENV, 'skipping test that would create global workflows') def test_describe_global_workflow(self): @@ -2190,6 +2342,37 @@ def test_describe_global_workflow(self): self.assertIn("Workflow Outputs", by_id) self.assertIn("Billed to", by_id) + @unittest.skipUnless(testutil.TEST_RUN_JOBS, + 'skipping test that would run jobs') + def test_describe_analysis_verbose(self): + analysis_input = {"foo": 100} + dxworkflow = self.create_workflow(self.project) + dxanalysis = dxworkflow.run(workflow_input=analysis_input) + + analysis_desc_verbose = run("dx describe {} --verbose".format(dxanalysis.get_id())) + analysis_desc_json = run("dx describe {} --json".format(dxanalysis.get_id())) + analysis_desc_verbose_json = run("dx describe {} --verbose --json".format(dxanalysis.get_id())) + self.assertTrue(all(key in analysis_desc_verbose for key in ["Run Sys Reqs", "Run Sys Reqs by Exec", "Merged Sys Reqs By Exec", "Run Stage Sys Reqs"])) + self.assertTrue(all(key in analysis_desc_verbose for key in ["ID", "Job name", "Executable name", "Class", "Workspace", "Project context"])) + self.assertFalse(any(key in json.loads(analysis_desc_json) for key in ['runSystemRequirements', 'runSystemRequirementsByExecutable', 'mergedSystemRequirementsByExecutable', 'runStageSystemRequirements'])) + self.assertTrue(all(key in json.loads(analysis_desc_json) for key in ['id','name','executable','class','workspace','project'])) + self.assertTrue(all(key in json.loads(analysis_desc_verbose_json) for key in ['runSystemRequirements', 'runSystemRequirementsByExecutable', 'mergedSystemRequirementsByExecutable', 'runStageSystemRequirements'])) + self.assertTrue(all(key in json.loads(analysis_desc_verbose_json) for key in ['id','name','executable','class','workspace','project'])) + + @unittest.skipUnless(testutil.TEST_RUN_JOBS, + 'skipping test that would run jobs') + def test_describe_job_verbose(self): + job_input = {"number": 100} + dxapplet = dxpy.DXApplet(self.test_applet_id) + dxjob = dxapplet.run(applet_input=job_input) + + job_desc_verbose = run("dx describe {} --verbose".format(dxjob.get_id())) + job_desc_json = run("dx describe {} --json".format(dxjob.get_id())) + job_desc_verbose_json = run("dx describe {} --verbose --json".format(dxjob.get_id())) + self.assertTrue(all(key in job_desc_verbose for key in ["Run Sys Reqs", "Run Sys Reqs by Exec", "Merged Sys Reqs By Exec"])) + self.assertFalse(any(key in json.loads(job_desc_json) for key in ['runSystemRequirements', 'runSystemRequirementsByExecutable', 'mergedSystemRequirementsByExecutable', 'runStageSystemRequirements'])) + self.assertTrue(all(key in json.loads(job_desc_verbose_json) for key in ['runSystemRequirements', 'runSystemRequirementsByExecutable', 'mergedSystemRequirementsByExecutable'])) + class TestDXClientRun(DXTestCase): def setUp(self): self.other_proj_id = run("dx new project other --brief").strip() @@ -2203,7 +2386,35 @@ def test_dx_run_disallow_project_and_folder(self): with self.assertRaisesRegex(subprocess.CalledProcessError, "Options --project and --folder/--destination cannot be specified together.\nIf specifying both a project and a folder, please include them in the --folder option."): run("dx run bogusapplet --project project-bogus --folder bogusfolder") + @contextmanager + def configure_ssh(self, use_alternate_config_dir=False): + original_ssh_public_key = None + try: + config_subdir = "dnanexus_config_alternate" if use_alternate_config_dir else ".dnanexus_config" + user_id = dxpy.whoami() + original_ssh_public_key = dxpy.api.user_describe(user_id).get('sshPublicKey') + wd = tempfile.mkdtemp() + config_dir = os.path.join(wd, config_subdir) + os.mkdir(config_dir) + if use_alternate_config_dir: + os.environ["DX_USER_CONF_DIR"] = config_dir + dx_ssh_config = pexpect.spawn("dx ssh_config", + env=override_environment(HOME=wd), + **spawn_extra_args) + dx_ssh_config.logfile = sys.stdout + dx_ssh_config.setwinsize(20, 90) + dx_ssh_config.expect("Select an SSH key pair") + dx_ssh_config.sendline("0") + dx_ssh_config.expect("Enter passphrase") + dx_ssh_config.sendline() + dx_ssh_config.expect("again") + dx_ssh_config.sendline() + dx_ssh_config.expect("Your account has been configured for use with SSH") + yield wd + finally: + if original_ssh_public_key: + dxpy.api.user_update(user_id, {"sshPublicKey": original_ssh_public_key}) @pytest.mark.TRACEABILITY_MATRIX @testutil.update_traceability_matrix(["DNA_CLI_APP_RUN_APPLET","DNA_API_DATA_OBJ_RUN_APPLET"]) @@ -2581,7 +2792,7 @@ def test_dx_resolve_check_resolution_needed(self): # If describing an entity ID fails, then a ResolutionError should be # raised - with self.assertRaisesRegex(ResolutionError, "The entity record-\d+ could not be found"): + with self.assertRaisesRegex(ResolutionError, r"The entity record-\d+ could not be found"): check_resolution("some_path", self.project, "/", "record-123456789012345678901234") def test_dx_run_depends_on_success(self): @@ -2695,6 +2906,7 @@ def test_dx_run_jbor_array_ref(self): "runSpec": {"interpreter": "bash", "distribution": "Ubuntu", "release": "14.04", + "systemRequirements": {"*": {"instanceType": "mem2_ssd1_v2_x2"}}, "bundledDepends": [], "execDepends": [], "code": ''' @@ -2749,7 +2961,9 @@ def _get_analysis_id(dx_run_output): "dxapi": "1.0.0", "runSpec": {"interpreter": "bash", "distribution": "Ubuntu", - "release": "14.04", + "release": "20.04", + "version": "0", + "systemRequirements": {"*": {"instanceType": "mem2_ssd1_v2_x2"}}, "code": ""}, "access": {"project": "VIEW", "allProjects": "VIEW", @@ -2766,50 +2980,112 @@ def _get_analysis_id(dx_run_output): run("dx terminate " + normal_job_id) run("dx terminate " + high_priority_job_id) - # --watch implies --priority high - try: - dx_run_output = run("dx run myapplet -y --watch --brief") - except subprocess.CalledProcessError: - # ignore any watching errors; just want to test requested - # priority - pass - watched_job_id = dx_run_output.split('\n')[0] - watched_job_desc = dxpy.describe(watched_job_id) - self.assertEqual(watched_job_desc['applet'], applet_id) - self.assertEqual(watched_job_desc['priority'], 'high') - - # don't actually need it to run - run("dx terminate " + watched_job_id) - - # --ssh implies --priority high - try: - dx_run_output = run("dx run myapplet -y --ssh --brief") - except subprocess.CalledProcessError: - # ignore any ssh errors; just want to test requested - # priority - pass - watched_job_id = dx_run_output.split('\n')[0] - watched_job_desc = dxpy.describe(watched_job_id) - self.assertEqual(watched_job_desc['applet'], applet_id) - self.assertEqual(watched_job_desc['priority'], 'high') - - # don't actually need it to run - run("dx terminate " + watched_job_id) - - # --allow-ssh implies --priority high - try: - dx_run_output = run("dx run myapplet -y --allow-ssh --brief") - except subprocess.CalledProcessError: - # ignore any ssh errors; just want to test requested - # priority - pass - watched_job_id = dx_run_output.split('\n')[0] - watched_job_desc = dxpy.describe(watched_job_id) - self.assertEqual(watched_job_desc['applet'], applet_id) - self.assertEqual(watched_job_desc['priority'], 'high') - - # don't actually need it to run - run("dx terminate " + watched_job_id) + with self.configure_ssh(use_alternate_config_dir=False) as wd: + # --watch implies high priority when --priority is not specified + try: + dx_run_output = run("dx run myapplet -y --watch --brief") + watched_job_id = dx_run_output.split('\n')[0] + watched_job_desc = dxpy.describe(watched_job_id) + self.assertEqual(watched_job_desc['applet'], applet_id) + self.assertEqual(watched_job_desc['priority'], 'high') + + # don't actually need it to run + run("dx terminate " + watched_job_id) + except subprocess.CalledProcessError as e: + # ignore any watching errors; just want to test requested + # priority + print(e.output) + pass + + # --ssh implies high priority when --priority is not specified + try: + dx_run_output = run("dx run myapplet -y --ssh --brief", + env=override_environment(HOME=wd)) + ssh_job_id = dx_run_output.split('\n')[0] + ssh_job_desc = dxpy.describe(ssh_job_id) + self.assertEqual(ssh_job_desc['applet'], applet_id) + self.assertEqual(ssh_job_desc['priority'], 'high') + + # don't actually need it to run + run("dx terminate " + ssh_job_id) + except subprocess.CalledProcessError as e: + # ignore any ssh errors; just want to test requested + # priority + print(e.output) + pass + + # --allow-ssh implies high priority when --priority is not specified + try: + dx_run_output = run("dx run myapplet -y --allow-ssh --brief", + env=override_environment(HOME=wd)) + allow_ssh_job_id = dx_run_output.split('\n')[0] + allow_ssh_job_desc = dxpy.describe(allow_ssh_job_id) + self.assertEqual(allow_ssh_job_desc['applet'], applet_id) + self.assertEqual(allow_ssh_job_desc['priority'], 'high') + + # don't actually need it to run + run("dx terminate " + allow_ssh_job_id) + except subprocess.CalledProcessError as e: + # ignore any ssh errors; just want to test requested + # priority + print(e.output) + pass + + # warning when --priority is normal/low with --watch + try: + watched_run_output = run("dx run myapplet -y --watch --priority normal") + watched_job_id = re.search('job-[A-Za-z0-9]{24}', watched_run_output).group(0) + watched_job_desc = dxpy.describe(watched_job_id) + self.assertEqual(watched_job_desc['applet'], applet_id) + self.assertEqual(watched_job_desc['priority'], 'normal') + for string in ["WARNING", "normal", "interrupting interactive work"]: + self.assertIn(string, watched_run_output) + + # don't actually need it to run + run("dx terminate " + watched_job_id) + except subprocess.CalledProcessError as e: + # ignore any watch errors; just want to test requested + # priority + print(e.output) + pass + + # no warning when --brief and --priority is normal/low with --allow-ssh + try: + allow_ssh_run_output = run("dx run myapplet -y --allow-ssh --priority normal --brief", + env=override_environment(HOME=wd)) + allow_ssh_job_id = re.search('job-[A-Za-z0-9]{24}', allow_ssh_run_output).group(0) + allow_ssh_job_desc = dxpy.describe(allow_ssh_job_id) + self.assertEqual(allow_ssh_job_desc['applet'], applet_id) + self.assertEqual(allow_ssh_job_desc['priority'], 'normal') + for string in ["WARNING", "normal", "interrupting interactive work"]: + self.assertNotIn(string, allow_ssh_run_output) + + # don't actually need it to run + run("dx terminate " + allow_ssh_job_id) + except subprocess.CalledProcessError as e: + # ignore any ssh errors; just want to test requested + # priority + print(e.output) + pass + + # no warning when --priority is high with --ssh + try: + ssh_run_output = run("dx run myapplet -y --ssh --priority high", + env=override_environment(HOME=wd)) + ssh_job_id = re.search('job-[A-Za-z0-9]{24}', ssh_run_output).group(0) + ssh_job_desc = dxpy.describe(ssh_job_id) + self.assertEqual(ssh_job_desc['applet'], applet_id) + self.assertEqual(ssh_job_desc['priority'], 'high') + for string in ["interrupting interactive work"]: + self.assertNotIn(string, ssh_run_output) + + # don't actually need it to run + run("dx terminate " + ssh_job_id) + except subprocess.CalledProcessError as e: + # ignore any ssh errors; just want to test requested + # priority + print(e.output) + pass # errors with self.assertSubprocessFailure(exit_code=2): @@ -2895,6 +3171,44 @@ def _get_analysis_id(dx_run_output): analysis_id = _get_analysis_id(dx_run_output) self.assertEqual(dxpy.describe(analysis_id)["priority"], "normal") + @unittest.skipUnless(testutil.TEST_RUN_JOBS, + 'skipping tests that would run jobs') + def test_dx_run_head_job_on_demand(self): + applet_id = dxpy.api.applet_new({"project": self.project, + "name": "myapplet4", + "dxapi": "1.0.0", + "runSpec": {"interpreter": "bash", + "distribution": "Ubuntu", + "release": "20.04", + "version": "0", + "systemRequirements": {"*": {"instanceType": "mem2_ssd1_v2_x2"}}, + "code": ""}, + "access": {"project": "VIEW", + "allProjects": "VIEW", + "network": []}})["id"] + special_field_query_json = json.loads('{"fields":{"headJobOnDemand":true}}') + normal_job_id = run("dx run myapplet4 --brief -y").strip() + normal_job_desc = dxpy.api.job_describe(normal_job_id) + self.assertEqual(normal_job_desc.get("headJobOnDemand"), None) + normal_job_desc = dxpy.api.job_describe(normal_job_id, special_field_query_json) + self.assertEqual(normal_job_desc["headJobOnDemand"], False) + + head_on_demand_job_id = run("dx run myapplet4 --head-job-on-demand --brief -y").strip() + head_on_demand_job_desc = dxpy.api.job_describe(head_on_demand_job_id, special_field_query_json) + self.assertEqual(head_on_demand_job_desc["headJobOnDemand"], True) + # don't actually need these to run + run("dx terminate " + normal_job_id) + run("dx terminate " + head_on_demand_job_id) + + # shown in help + dx_help_output = run("dx help run") + self.assertIn("--head-job-on-demand", dx_help_output) + + # error code 3 when run on a workflow and a correct error message + workflow_id = run("dx new workflow --brief").strip() + with self.assertSubprocessFailure(exit_code=3, stderr_text="--head-job-on-demand cannot be used when running workflows"): + run("dx run {workflow_id} --head-job-on-demand -y".format(workflow_id=workflow_id)) + def test_dx_run_tags_and_properties(self): # success applet_id = dxpy.api.applet_new({"project": self.project, @@ -2967,12 +3281,111 @@ def test_dx_run_extra_args(self): with self.assertSubprocessFailure(stderr_regexp='JSON', exit_code=3): run("dx run " + applet_id + " --extra-args not-a-JSON-string") + def test_dx_run_sys_reqs(self): + app_spec = {"project": self.project, + "dxapi": "1.0.0", + "runSpec": {"interpreter": "bash", + "distribution": "Ubuntu", + "release": "20.04", + "version": "0", + "code": "echo 'hello'", + "systemRequirements": { + "main": { + "instanceType": "mem2_hdd2_x1", + "clusterSpec": {"type": "spark", + "initialInstanceCount": 1, + "version": "2.4.4", + "bootstrapScript": "x.sh"} + }, + "other": { + "instanceType": "mem2_hdd2_x4", + "clusterSpec": {"type": "spark", + "initialInstanceCount": 5, + "version": "2.4.4", + "bootstrapScript": "x.sh"} + }, + } + } + } + applet_id = dxpy.api.applet_new(app_spec)['id'] + requested_inst_type_by_exec = { + applet_id: { + "main": "mem2_ssd1_v2_x2", + "other": "mem2_hdd2_x1"}} + + (stdout, stderr) = run('_DX_DEBUG=2 dx run ' + applet_id + ' ' + + '--instance-type mem2_hdd2_x2 ' + + '--instance-count 15 ' + + '--instance-type-by-executable ' + + '\'' + + json.dumps(requested_inst_type_by_exec) + '\'', + also_return_stderr=True) + expected_sys_reqs_by_exec = '"systemRequirementsByExecutable": ' + \ + json.dumps({ + applet_id: { + "main": {"instanceType": "mem2_ssd1_v2_x2"}, + "other": {"instanceType": "mem2_hdd2_x1"}}}) + self.assertIn(expected_sys_reqs_by_exec, stderr) + + # parsing error + with self.assertSubprocessFailure(stderr_regexp='JSON', exit_code=3): + run("dx run " + applet_id + + " --instance-type-by-executable not-a-JSON-string") + + def test_dx_run_clone_nvidia_driver(self): + """ + Run the applet and clone the origin job. Verify nvidiaDriver value. + """ + build_nvidia_version = "R535" + run_nvidia_version = "R470" + + applet_id = dxpy.api.applet_new({"project": self.project, + "dxapi": "1.0.0", + "runSpec": {"interpreter": "bash", + "distribution": "Ubuntu", + "release": "20.04", + "version": "0", + "code": "echo 'hello'", + "systemRequirements": { + "*": { + "instanceType": "mem2_hdd2_x1", + "nvidiaDriver": build_nvidia_version + } + }} + })['id'] + + # Run with unchanged nvidia version (build value) + origin_job_id = run(f"dx run {applet_id} --brief -y").strip().split('\n')[-1] + origin_job_desc = dxpy.api.job_describe(origin_job_id) + assert origin_job_desc["systemRequirements"]["*"]["nvidiaDriver"] == build_nvidia_version + + cloned_job_id = run(f"dx run --clone {origin_job_id} --brief -y").strip() + cloned_job_desc = dxpy.api.job_describe(cloned_job_id) + assert cloned_job_desc["systemRequirements"]["*"]["nvidiaDriver"] == build_nvidia_version + + # Change nvidia driver version in runtime - origin job (run value) + extra_args = json.dumps({"systemRequirements": {"*": {"nvidiaDriver": run_nvidia_version}}}) + origin_job_id_nvidia_override = run(f"dx run {applet_id} --extra-args '{extra_args}' --brief -y").strip().split('\n')[-1] + origin_job_desc = dxpy.api.job_describe(origin_job_id_nvidia_override) + assert origin_job_desc["systemRequirements"]["*"]["nvidiaDriver"] == run_nvidia_version + + cloned_job_id_nvidia_override = run(f"dx run --clone {origin_job_id_nvidia_override} --brief -y").strip() + cloned_job_desc = dxpy.api.job_describe(cloned_job_id_nvidia_override) + assert cloned_job_desc["systemRequirements"]["*"]["nvidiaDriver"] == run_nvidia_version + + # Change nvidia driver version in runtime - cloned job (build value) + extra_args = json.dumps({"systemRequirements": {"*": {"nvidiaDriver": build_nvidia_version}}}) + cloned_job_id_nvidia_override = run(f"dx run --clone {origin_job_id_nvidia_override} --extra-args '{extra_args}' --brief -y").strip() + cloned_job_desc = dxpy.api.job_describe(cloned_job_id_nvidia_override) + assert cloned_job_desc["systemRequirements"]["*"]["nvidiaDriver"] == build_nvidia_version + def test_dx_run_clone(self): applet_id = dxpy.api.applet_new({"project": self.project, "dxapi": "1.0.0", "runSpec": {"interpreter": "bash", "distribution": "Ubuntu", - "release": "14.04", + "release": "20.04", + "version": "0", "code": "echo 'hello'"} })['id'] other_applet_id = dxpy.api.applet_new({"project": self.project, @@ -2980,8 +3393,31 @@ def test_dx_run_clone(self): "runSpec": {"interpreter": "bash", "code": "echo 'hello'", "distribution": "Ubuntu", - "release": "14.04"} - })['id'] + "release": "20.04", + "version": "0", + "systemRequirements": { + "main": { + "instanceType": "mem2_hdd2_x1", + "clusterSpec": {"type": "spark", + "initialInstanceCount": 1, + "version": "2.4.4", + "bootstrapScript": "x.sh"} + }, + "some_ep": { + "instanceType": "mem2_hdd2_x2", + "clusterSpec": {"type": "spark", + "initialInstanceCount": 3, + "version": "2.4.4", + "bootstrapScript": "x.sh"} + }, + "*": { + "instanceType": "mem2_hdd2_x4", + "clusterSpec": {"type": "spark", + "initialInstanceCount": 5, + "version": "2.4.4", + "bootstrapScript": "x.sh"}}} + }} + )['id'] def check_new_job_metadata(new_job_desc, cloned_job_desc, overridden_fields=[]): ''' @@ -3012,17 +3448,23 @@ def check_new_job_metadata(new_job_desc, cloned_job_desc, overridden_fields=[]): orig_job_id = run("dx run " + applet_id + ' -inumber=32 --name jobname --folder /output ' + '--instance-type mem2_hdd2_x2 ' + + '--instance-type-by-executable \'{"' + applet_id + '": {"*": "mem1_ssd1_v2_x2"}}\' ' '--tag Ψ --tag $hello.world ' + '--property Σ_1^n=n --property $hello.=world ' + '--priority normal ' + - '--brief -y').strip() - orig_job_desc = dxpy.api.job_describe(orig_job_id) + '--brief -y').strip().split('\n')[-1] + orig_job_desc = dxpy.api.job_describe(orig_job_id, {"defaultFields": True, + "fields":{ + "runSystemRequirements":True, "runSystemRequirementsByExecutable":True, "mergedSystemRequirementsByExecutable":True}} ) # control self.assertEqual(orig_job_desc['name'], 'jobname') self.assertEqual(orig_job_desc['project'], self.project) self.assertEqual(orig_job_desc['folder'], '/output') self.assertEqual(orig_job_desc['input'], {'number': 32}) - self.assertEqual(orig_job_desc['systemRequirements'], {'*': {'instanceType': 'mem2_hdd2_x2'}}) + self.assertEqual(orig_job_desc['systemRequirements'], {'*': {'instanceType': 'mem1_ssd1_v2_x2'}}) + self.assertEqual(orig_job_desc['runSystemRequirements'], {'*': {'instanceType': 'mem2_hdd2_x2'}}) + self.assertEqual(orig_job_desc['runSystemRequirementsByExecutable'], {applet_id: {'*': {'instanceType': 'mem1_ssd1_v2_x2'}}}) + self.assertEqual(orig_job_desc['mergedSystemRequirementsByExecutable'], {applet_id: {'*': {'instanceType': 'mem1_ssd1_v2_x2'}}}) # clone the job @@ -3032,13 +3474,15 @@ def check_new_job_metadata(new_job_desc, cloned_job_desc, overridden_fields=[]): check_new_job_metadata(new_job_desc, orig_job_desc) def get_new_job_desc(cmd_suffix): - new_job_id = run("dx run --clone " + orig_job_id + " --brief -y " + cmd_suffix).strip() - return dxpy.api.job_describe(new_job_id) + new_job_id = run("dx run --clone " + orig_job_id + " --brief -y " + cmd_suffix).strip().split('\n')[-1] + return dxpy.api.job_describe(new_job_id, {"defaultFields": True, + "fields":{ + "runSystemRequirements":True, "runSystemRequirementsByExecutable":True, "mergedSystemRequirementsByExecutable":True}}) # override applet new_job_desc = get_new_job_desc(other_applet_id) self.assertEqual(new_job_desc['applet'], other_applet_id) - check_new_job_metadata(new_job_desc, orig_job_desc, overridden_fields=['applet']) + check_new_job_metadata(new_job_desc, orig_job_desc, overridden_fields=['applet', 'systemRequirements']) # override name new_job_desc = get_new_job_desc("--name newname") @@ -3092,7 +3536,14 @@ def get_new_job_desc(cmd_suffix): self.assertEqual(new_job_desc['input'], {"number2": 42}) check_new_job_metadata(new_job_desc, orig_job_desc, overridden_fields=['input']) + # --instance-type override: original job with universal instance type # override the blanket instance type + orig_job_id = run("dx run " + applet_id + + ' --instance-type mem1_ssd1_v2_x2 ' + + '--brief -y').strip().split('\n')[-1] + orig_job_desc = dxpy.api.job_describe(orig_job_id, {"defaultFields": True, + "fields": {"runSystemRequirements": True, + "runSystemRequirementsByExecutable": True, "mergedSystemRequirementsByExecutable": True}}) new_job_desc = get_new_job_desc("--instance-type mem2_hdd2_x1") self.assertEqual(new_job_desc['systemRequirements'], {'*': {'instanceType': 'mem2_hdd2_x1'}}) @@ -3104,13 +3555,13 @@ def get_new_job_desc(cmd_suffix): json.dumps({"some_ep": "mem2_hdd2_x1", "some_other_ep": "mem2_hdd2_x4"}) + "'") self.assertEqual(new_job_desc['systemRequirements'], - {'*': {'instanceType': 'mem2_hdd2_x2'}, + {'*': {'instanceType': 'mem1_ssd1_v2_x2'}, 'some_ep': {'instanceType': 'mem2_hdd2_x1'}, 'some_other_ep': {'instanceType': 'mem2_hdd2_x4'}}) check_new_job_metadata(new_job_desc, orig_job_desc, overridden_fields=['systemRequirements']) - # new original job with entry point-specific systemRequirements + # --instance-type override: original job with entry point-specific systemRequirements orig_job_id = run("dx run " + applet_id + " --instance-type '{\"some_ep\": \"mem2_hdd2_x1\"}' --brief -y").strip() orig_job_desc = dxpy.api.job_describe(orig_job_id) @@ -3135,6 +3586,144 @@ def get_new_job_desc(cmd_suffix): {'some_ep': {'instanceType': 'mem2_hdd2_x2'}}) check_new_job_metadata(new_job_desc, orig_job_desc, overridden_fields=['systemRequirements']) + # --instance-type override: original job with entrypoint specific systemRequirements + orig_job_id = run("dx run " + applet_id + + " --instance-type '{\"some_ep\": \"mem2_hdd2_x1\", \"*\": \"mem2_hdd2_x2\"}' --brief -y").strip() + orig_job_desc = dxpy.api.job_describe(orig_job_id) + self.assertEqual(orig_job_desc['systemRequirements'], + {'some_ep': {'instanceType': 'mem2_hdd2_x1'}, + '*': {'instanceType': 'mem2_hdd2_x2'}}) + + # override all entry points + new_job_desc = get_new_job_desc("--instance-type mem2_hdd2_v2_x2") + self.assertEqual(new_job_desc['systemRequirements'], {'*': {'instanceType': 'mem2_hdd2_v2_x2'}}) + check_new_job_metadata(new_job_desc, orig_job_desc, overridden_fields=['systemRequirements']) + + # override all entry points with wildcard entry point, which is treated in the same way as specific ones + new_job_desc = get_new_job_desc("--instance-type '" + + json.dumps({"*": "mem2_hdd2_v2_x2"}) + "'") + self.assertEqual(new_job_desc['systemRequirements'], + {'some_ep': {'instanceType': 'mem2_hdd2_x1'}, + '*': {'instanceType': 'mem2_hdd2_v2_x2'}}) + check_new_job_metadata(new_job_desc, orig_job_desc, overridden_fields=['systemRequirements']) + + # override a different entry point; original is merged and overrided + new_job_desc = get_new_job_desc("--instance-type '" + + json.dumps({"some_other_ep": "mem2_hdd2_x4", + "*": "mem2_hdd2_v2_x2"}) + "'") + self.assertEqual(new_job_desc['systemRequirements'], + {'some_other_ep': {'instanceType': 'mem2_hdd2_x4'}, + 'some_ep': {'instanceType': 'mem2_hdd2_x1'}, + '*': {'instanceType': 'mem2_hdd2_v2_x2'}}) + check_new_job_metadata(new_job_desc, orig_job_desc, overridden_fields=['systemRequirements']) + + def check_instance_count(job_desc , entrypoints , expected_counts): + for ep, count in zip(entrypoints, expected_counts): + self.assertEqual(job_desc['systemRequirements'][ep]["clusterSpec"]["initialInstanceCount"], count) + + # --instance-count override: new original job with universal instance count + orig_job_id = run("dx run " + other_applet_id + + " --instance-count 2 --brief -y").strip() + orig_job_desc = dxpy.api.job_describe(orig_job_id) + check_instance_count(orig_job_desc, ["main", "some_ep","*"], [2, 2, 2]) + + # override all entry points + new_job_desc = get_new_job_desc("--instance-count 4") + check_instance_count(new_job_desc, ["main", "some_ep", "*"], [4, 4, 4]) + check_new_job_metadata(new_job_desc, orig_job_desc, + overridden_fields=['systemRequirements']) + + # override single entry point + new_job_desc = get_new_job_desc("--instance-count '" + + json.dumps({"some_ep": 6}) + "'") + check_instance_count(new_job_desc, ["main", "some_ep", "*"], [2, 6, 2]) + check_new_job_metadata(new_job_desc, orig_job_desc, + overridden_fields=['systemRequirements']) + + # override wildcard entry point + new_job_desc = get_new_job_desc("--instance-count '" + + json.dumps({"*": 8}) + "'") + check_instance_count(new_job_desc, ["main", "some_ep", "*"], [2, 2, 8]) + check_new_job_metadata(new_job_desc, orig_job_desc, + overridden_fields=['systemRequirements']) + + # --instance-count override: new original job with entrypoint specific instance count + orig_job_id = run("dx run " + other_applet_id + + " --instance-count '{\"some_ep\": \"2\"}' --brief -y").strip() + orig_job_desc = dxpy.api.job_describe(orig_job_id) + check_instance_count(orig_job_desc, ["main", "some_ep","*"], [1, 2, 5]) + + # override all entry points + new_job_desc = get_new_job_desc("--instance-count 4") + check_instance_count(new_job_desc, ["main", "some_ep", "*"], [4, 4, 4]) + check_new_job_metadata(new_job_desc, orig_job_desc, + overridden_fields=['systemRequirements']) + + # override single entry point + new_job_desc = get_new_job_desc("--instance-count '" + + json.dumps({"some_ep": 6}) + "'") + check_instance_count(new_job_desc, ["main", "some_ep", "*"], [1, 6, 5]) + check_new_job_metadata(new_job_desc, orig_job_desc, + overridden_fields=['systemRequirements']) + + # override wildcard entry point + new_job_desc = get_new_job_desc("--instance-count '" + + json.dumps({"*": 8}) + "'") + check_instance_count(new_job_desc, ["main", "some_ep", "*"], [1, 2, 8]) + check_new_job_metadata(new_job_desc, orig_job_desc, + overridden_fields=['systemRequirements']) + + # fpgaDriver/nvidiaDriver override: new original job with extra_args + orig_job_id = run("dx run " + other_applet_id + + " --instance-count 2 --brief -y " + + "--extra-args '" + + json.dumps({"systemRequirements": {"some_ep": + {"clusterSpec": {"initialInstanceCount": 12, "bootstrapScript": "z.sh"}, + "fpgaDriver": "edico-1.4.5", + "nvidiaDriver": "R535"}}}) + "'").strip() + orig_job_desc = dxpy.api.job_describe(orig_job_id) + check_instance_count(orig_job_desc, ["main", "some_ep","*"], [2, 12, 2]) + # --instance-type and --instance-count override: instance type and cluster spec are resolved independently + new_job_desc = get_new_job_desc("--instance-count '" + + json.dumps({"some_ep":6, + "*": 8}) + "' " + + "--instance-type '" + + json.dumps({"main": "mem2_hdd2_x4", + "*": "mem2_hdd2_v2_x2"}) + "'") + check_instance_count(new_job_desc, ["main", "some_ep", "*"], [2, 6, 8]) + check_new_job_metadata(new_job_desc, orig_job_desc, + overridden_fields=['systemRequirements']) + + self.assertEqual(new_job_desc['systemRequirements']['main']['instanceType'], 'mem2_hdd2_x4') + self.assertEqual(new_job_desc['systemRequirements']['some_ep']['instanceType'], 'mem2_hdd2_x2') + self.assertEqual(new_job_desc['systemRequirements']['*']['instanceType'], 'mem2_hdd2_v2_x2') + + self.assertEqual(new_job_desc['systemRequirements']['some_ep']['fpgaDriver'], 'edico-1.4.5') + self.assertEqual(new_job_desc['systemRequirements']['some_ep']['nvidiaDriver'], 'R535') + self.assertEqual(new_job_desc['systemRequirements']['some_ep']['clusterSpec']['bootstrapScript'], 'z.sh') + + # --instance-type and --instance-type-by-executable override + orig_job_id = run("dx run " + other_applet_id + + " --instance-type mem2_hdd2_x2" + + " --instance-type-by-executable \'" + + json.dumps({other_applet_id: {"some_ep": "mem1_ssd1_v2_x2", "some_other_ep": "mem2_hdd2_x4"}}) + "\'" + + " --brief -y").strip().split('\n')[-1] + orig_job_desc = dxpy.api.job_describe(orig_job_id, {"defaultFields": True, "fields": { + "runSystemRequirements": True, "runSystemRequirementsByExecutable": True, "mergedSystemRequirementsByExecutable": True}}) + + new_job_desc = get_new_job_desc("--instance-type-by-executable \'" + + json.dumps({other_applet_id: {"some_ep": "mem1_ssd1_v2_x8", "*": "mem2_hdd2_x1"}}) + "\'") + + # cloned from original job systemRequirements + self.assertEqual(new_job_desc["runSystemRequirements"], orig_job_desc["systemRequirements"]) + self.assertEqual( + new_job_desc['runSystemRequirementsByExecutable'][other_applet_id]['some_ep']['instanceType'], 'mem1_ssd1_v2_x8') + self.assertEqual( + new_job_desc['runSystemRequirementsByExecutable'][other_applet_id]['*']['instanceType'], 'mem2_hdd2_x1') + # cloned from original job mergedSystemRequirements + self.assertEqual( + new_job_desc['runSystemRequirementsByExecutable'][other_applet_id]['some_other_ep']['instanceType'], 'mem2_hdd2_x4') + @unittest.skipUnless(testutil.TEST_RUN_JOBS, 'skipping tests that would run jobs') def test_dx_describe_job_with_resolved_jbors(self): @@ -3142,10 +3731,11 @@ def test_dx_describe_job_with_resolved_jbors(self): "dxapi": "1.0.0", "inputSpec": [{"name": "array", "class": "array:int"}], "outputSpec": [{"name": "array", "class": "array:int"}], - "runSpec": {"interpreter": "python2.7", + "runSpec": {"interpreter": "python3", "distribution": "Ubuntu", - "release": "14.04", - "code": '''#!/usr/bin/env python + "release": "20.04", + "version": "0", + "code": '''#!/usr/bin/env python3 @dxpy.entry_point('main') def main(array): @@ -3169,7 +3759,7 @@ def main(array): while second_job_handler.describe()['state'] in ['idle', 'waiting_on_input']: time.sleep(0.1) second_job_desc = run("dx describe " + second_job_handler.get_id()) - first_job_res = first_job_handler.get_id() + ":array => [ 0, 1, 5 ]" + first_job_res = first_job_handler.get_id() + ":array => [ 0,\n 1, 5 ]" self.assertIn(first_job_res, second_job_desc) # Launch another job which depends on the first done job and @@ -3192,10 +3782,11 @@ def test_dx_run_ssh_no_config(self): "dxapi": "1.0.0", "inputSpec": [], "outputSpec": [], - "runSpec": {"interpreter": "python2.7", + "runSpec": {"interpreter": "python3", "distribution": "Ubuntu", - "release": "14.04", - "code": '''#!/usr/bin/env python + "release": "20.04", + "version": "0", + "code": '''#!/usr/bin/env python3 @dxpy.entry_point('main') def main(): @@ -3279,7 +3870,7 @@ def test_dx_run_workflow(self): self.assertIn('stage_0.number = 32', analysis_desc) self.assertIn('foo', analysis_desc) analysis_desc = json.loads(run("dx describe " + analysis_id + " --json")) - time.sleep(2) # May need to wait for job to be created in the system + time.sleep(20) # May need to wait for job to be created in the system job_desc = run("dx describe " + analysis_desc["stages"][0]["execution"]["id"]) self.assertIn(' number = 32', job_desc) @@ -3335,7 +3926,7 @@ def test_dx_run_workflow(self): self.assertIn('foo', analysis_desc) analysis_desc = json.loads(run("dx describe --json " + analysis_id )) self.assertTrue(analysis_desc["runInput"], {"foo": 747}) - time.sleep(2) # May need to wait for job to be created in the system + time.sleep(20) # May need to wait for job to be created in the system job_desc = run("dx describe " + analysis_desc["stages"][0]["execution"]["id"]) self.assertIn(' number = 474', job_desc) @@ -3346,7 +3937,7 @@ def test_dx_run_workflow(self): @unittest.skipUnless(testutil.TEST_RUN_JOBS, 'skipping test that runs jobs') def test_dx_run_clone_analysis(self): - dxpy.api.applet_new({ + applet_id = dxpy.api.applet_new({ "project": self.project, "name": "myapplet", "dxapi": "1.0.0", @@ -3355,8 +3946,9 @@ def test_dx_run_clone_analysis(self): "runSpec": {"interpreter": "bash", "distribution": "Ubuntu", "release": "14.04", + "systemRequirements": {"*": {"instanceType": "mem2_ssd1_v2_x2"}}, "code": "dx-jobutil-add-output number 32"} - }) + })["id"] # make a workflow with the stage twice run("dx new workflow myworkflow") @@ -3373,44 +3965,45 @@ def test_dx_run_clone_analysis(self): " -i0.number=52 --brief -y").strip() change_inst_type_analysis_id = run("dx run --clone " + analysis_id + " --instance-type mem2_hdd2_x2 --brief -y").strip() + change_inst_type_by_exec_analysis_id = run("dx run --clone " + analysis_id + + " --instance-type-by-executable \'" + + json.dumps({applet_id: {"*": "mem2_ssd1_v2_x2"}}) + + "\' --brief -y").strip() time.sleep(25) # May need to wait for any new jobs to be created in the system # make assertions for test cases orig_analysis_desc = dxpy.describe(analysis_id) - # no change: expect both stages to have reused jobs - no_change_analysis_desc = dxpy.describe(no_change_analysis_id) - print(no_change_analysis_desc) - self.assertEqual(no_change_analysis_desc['stages'][0]['execution']['id'], - orig_analysis_desc['stages'][0]['execution']['id']) - self.assertEqual(no_change_analysis_desc['stages'][1]['execution']['id'], - orig_analysis_desc['stages'][1]['execution']['id']) - # change an input: new job for that stage change_an_input_analysis_desc = dxpy.describe(change_an_input_analysis_id) self.assertEqual(change_an_input_analysis_desc['stages'][0]['execution']['input'], {"number": 52}) - # second stage still the same - self.assertEqual(change_an_input_analysis_desc['stages'][1]['execution']['id'], - orig_analysis_desc['stages'][1]['execution']['id']) # change inst type: only affects stage with different inst type change_inst_type_analysis_desc = dxpy.describe(change_inst_type_analysis_id) - # first stage still the same - self.assertEqual(change_inst_type_analysis_desc['stages'][0]['execution']['id'], - orig_analysis_desc['stages'][0]['execution']['id']) - # second stage different + self.assertNotEqual(change_inst_type_analysis_desc['stages'][1]['execution']['id'], orig_analysis_desc['stages'][1]['execution']['id']) self.assertEqual(change_inst_type_analysis_desc['stages'][1]['execution']['instanceType'], 'mem2_hdd2_x2') + # change inst type by executable: only affects stage with different inst type + change_inst_type_by_exec_analysis_desc = dxpy.describe(change_inst_type_by_exec_analysis_id) + + self.assertEqual(change_inst_type_by_exec_analysis_desc['stages'][0]['execution']['instanceType'],'mem2_ssd1_v2_x2') + self.assertEqual(change_inst_type_by_exec_analysis_desc['stages'][1]['execution']['instanceType'],'mem2_ssd1_v2_x2') + # Cannot provide workflow executable (ID or name) with --clone analysis error_mesg = 'cannot be provided when re-running an analysis' with self.assertSubprocessFailure(stderr_regexp=error_mesg, exit_code=3): run("dx run myworkflow --clone " + analysis_id) + # Cannot provide --instance-count when running workflow + error_mesg = '--instance-count is not supported for workflows' + with self.assertSubprocessFailure(stderr_regexp=error_mesg, exit_code=3): + run("dx run --instance-count 5 --clone " + analysis_id) + # Run in a different project and add some metadata try: other_proj_id = run("dx new project 'cloned analysis project' --brief").strip() @@ -3429,7 +4022,7 @@ def test_dx_run_clone_analysis(self): finally: run("dx rmproject -y " + other_proj_id) - @unittest.skipUnless(testutil.TEST_RUN_JOBS, 'skipping test that runs jobs') + @unittest.skip("Skipping per DEVEX-2195") def test_dx_run_workflow_prints_cached_executions(self): applet_id = dxpy.api.applet_new({"project": self.project, "name": "myapplet", @@ -3439,6 +4032,7 @@ def test_dx_run_workflow_prints_cached_executions(self): "runSpec": {"interpreter": "bash", "distribution": "Ubuntu", "release": "14.04", + "systemRequirements": {"*": {"instanceType": "mem2_ssd1_v2_x2"}}, "code": "dx-jobutil-add-output number 32"} })['id'] workflow_id = run("dx new workflow myworkflow --brief").strip() @@ -3466,7 +4060,7 @@ def test_dx_run_workflow_prints_cached_executions(self): self.assertNotIn('will reuse results from a previous analysis', run_output) self.assertNotIn(job_id, run_output) - @unittest.skipUnless(testutil.TEST_RUN_JOBS, 'skipping test that runs jobs') + @unittest.skip("Skipping per DEVEX-2195") def test_dx_run_workflow_with_inst_type_requests(self): applet_id = dxpy.api.applet_new({"project": self.project, "name": "myapplet", @@ -3476,6 +4070,7 @@ def test_dx_run_workflow_with_inst_type_requests(self): "runSpec": {"interpreter": "bash", "distribution": "Ubuntu", "release": "14.04", + "systemRequirements": {"*": {"instanceType": "mem2_ssd1_v2_x2"}}, "code": ""} })['id'] @@ -3492,6 +4087,12 @@ def test_dx_run_workflow_with_inst_type_requests(self): stg_req_id = run('dx run myworkflow --instance-type an=awful=name=mem2_hdd2_x2 ' + '--instance-type second=mem2_hdd2_x1 -y --brief').strip() + # request for an executable + exec_req_id = run("dx run myworkflow" + + " --instance-type-by-executable \'" + + json.dumps({applet_id: {"*": "mem2_ssd1_v2_x2"}}) + + "\' --brief -y").strip() + time.sleep(10) # give time for all jobs to be populated no_req_desc = dxpy.describe(no_req_id) @@ -3509,6 +4110,11 @@ def test_dx_run_workflow_with_inst_type_requests(self): 'mem2_hdd2_x2') self.assertEqual(stg_req_desc['stages'][1]['execution']['instanceType'], 'mem2_hdd2_x1') + exec_req_desc = dxpy.describe(exec_req_id) + self.assertEqual(exec_req_desc['stages'][0]['execution']['instanceType'], + 'mem2_ssd1_v2_x2') + self.assertEqual(exec_req_desc['stages'][1]['execution']['instanceType'], + 'mem2_ssd1_v2_x2') # request for a stage specifically (by index); if same inst # type as before, should reuse results @@ -3528,6 +4134,7 @@ def test_dx_run_workflow_with_stage_folders(self): "runSpec": {"interpreter": "bash", "distribution": "Ubuntu", "release": "14.04", + "systemRequirements": {"*": {"instanceType": "mem2_ssd1_v2_x2"}}, "code": ""} })['id'] workflow_id = run("dx new workflow myworkflow --brief").strip() @@ -3578,6 +4185,7 @@ def test_inaccessible_stage(self): "runSpec": {"interpreter": "bash", "distribution": "Ubuntu", "release": "14.04", + "systemRequirements": {"*": {"instanceType": "mem2_ssd1_v2_x2"}}, "code": "exit 1"} })['id'] workflow_id = run("dx new workflow myworkflow --brief").strip() @@ -3593,7 +4201,7 @@ def test_inaccessible_stage(self): self.assertIn("inaccessible", list_output) # run refuses to run it - with self.assertSubprocessFailure(stderr_regexp='following inaccessible stage\(s\)', + with self.assertSubprocessFailure(stderr_regexp=r'following inaccessible stage\(s\)', exit_code=3): run("dx run myworkflow") @@ -3632,7 +4240,8 @@ def test_dx_new_workflow(self): "inputSpec": [], "outputSpec": [], "runSpec": {"interpreter": "bash", "code": "", - "distribution": "Ubuntu", "release": "14.04"} + "distribution": "Ubuntu", "release": "14.04", + "systemRequirements": {"*": {"instanceType": "mem2_ssd1_v2_x2"}}} })['id'] run("dx add stage wØrkflØwname " + applet_id) @@ -3686,6 +4295,7 @@ def test_dx_describe_workflow(self): "runSpec": {"interpreter": "bash", "distribution": "Ubuntu", "release": "14.04", + "systemRequirements": {"*": {"instanceType": "mem2_ssd1_v2_x2"}}, "code": "exit 0"} })['id'] first_stage = run("dx add stage " + workflow_id + " -inumber=10 " + applet_id + @@ -3707,6 +4317,7 @@ def test_dx_add_remove_list_stages(self): "runSpec": {"interpreter": "bash", "distribution": "Ubuntu", "release": "14.04", + "systemRequirements": {"*": {"instanceType": "mem2_ssd1_v2_x2"}}, "code": "exit 0"} })['id'] stage_ids = [] @@ -3876,6 +4487,7 @@ def test_dx_update_stage(self): "runSpec": {"interpreter": "bash", "distribution": "Ubuntu", "release": "14.04", + "systemRequirements": {"*": {"instanceType": "mem2_ssd1_v2_x2"}}, "code": "exit 0"} })['id'] stage_id = run("dx add stage " + workflow_id + " " + applet_id + " --brief").strip() @@ -4222,7 +4834,7 @@ def test_dx_build_get_build_workflow(self): def test_build_worklow_malformed_dxworkflow_json(self): workflow_dir = self.write_workflow_directory("dxbuilt_workflow", "{") - with self.assertSubprocessFailure(stderr_regexp='Could not parse dxworkflow\.json file', exit_code=3): + with self.assertSubprocessFailure(stderr_regexp=r'Could not parse dxworkflow\.json file', exit_code=3): run("dx build " + workflow_dir) @@ -4286,6 +4898,8 @@ def test_dx_update_global_workflow(self): # The ID should not be updated self.assertEqual(gwf_id, updated_desc["id"]) + @unittest.skipUnless(testutil.TEST_ISOLATED_ENV, + 'skipping test that would create global workflows') def test_build_multi_region_workflow_with_applet(self): gwf_name = "gwf_{t}_multi_region".format(t=int(time.time())) dxworkflow_json = dict(self.dxworkflow_spec, name=gwf_name) @@ -4295,9 +4909,87 @@ def test_build_multi_region_workflow_with_applet(self): json.dumps(dxworkflow_json), readme_content="Workflow Readme Please") - error_msg = "Building a global workflow with applets in more than one region is not yet supported" - with self.assertRaisesRegexp(DXCalledProcessError, error_msg): - run("dx build --globalworkflow --json " + workflow_dir) + gwf_desc = json.loads(run('dx build --globalworkflow ' + workflow_dir + ' --json')) + gwf_regional_options = gwf_desc["regionalOptions"] + self.assertIn("aws:us-east-1", gwf_regional_options) + self.assertNotIn("azure:westus",gwf_regional_options) + + @unittest.skipUnless(testutil.TEST_ISOLATED_ENV, "skipping test that would create global workflows") + def test_build_workflow_with_regional_resources(self): + with temporary_project(region="aws:us-east-1") as aws_proj: + aws_file_id = create_file_in_project("aws_file", aws_proj.get_id()) + + gwf_name = "gwf_{t}_with_regional_resources".format(t=int(time.time())) + dxworkflow_json = dict( + self.dxworkflow_spec, + name=gwf_name, + regionalOptions={ + "aws:us-east-1": dict(resources=[aws_file_id]), + }, + ) + workflow_dir = self.write_workflow_directory( + gwf_name, + json.dumps(dxworkflow_json), + readme_content="Workflow Readme Please", + ) + + gwf_desc = json.loads(run("dx build --globalworkflow " + workflow_dir + " --json")) + + self.assertIn("regionalOptions", gwf_desc) + gwf_regional_options = gwf_desc["regionalOptions"] + self.assertIn("aws:us-east-1", gwf_regional_options) + + # Make sure additional resources were cloned to the workflow containers + # in the specified regions + aws_container = gwf_regional_options["aws:us-east-1"]["resources"] + aws_obj_id_list = dxpy.api.container_list_folder(aws_container, {"folder": "/"}) + self.assertIn(aws_file_id, [item["id"] for item in aws_obj_id_list["objects"]]) + + error_msg = "--region and the 'regionalOptions' key in the JSON file or --extra-args do not agree" + with self.assertRaisesRegex(DXCalledProcessError, error_msg): + run("dx build --globalworkflow --region azure:westus --json " + workflow_dir) + + @unittest.skipUnless(testutil.TEST_ISOLATED_ENV, "skipping test that would create global workflows") + def test_build_workflow_with_regional_resources_from_extra_args(self): + with temporary_project(region="aws:us-east-1") as aws_proj: + aws_file_id = create_file_in_project("aws_file", aws_proj.get_id()) + + gwf_name = "gwf_{t}_with_regional_resources_from_extra_args".format(t=int(time.time())) + dxworkflow_json = dict(self.dxworkflow_spec,name=gwf_name) + workflow_dir = self.write_workflow_directory( + gwf_name, + json.dumps(dxworkflow_json), + readme_content="Workflow Readme Please", + ) + + extra_args = json.dumps({"regionalOptions":{"aws:us-east-1": {"resources":aws_proj.get_id()}}}) + + error_msg = "--region and the 'regionalOptions' key in the JSON file or --extra-args do not agree" + with self.assertRaisesRegex(DXCalledProcessError, error_msg): + run("dx build --globalworkflow --region azure:westus --extra-args \'{}\' --json {}".format(extra_args, workflow_dir)) + + gwf_desc = json.loads(run("dx build --globalworkflow --json {} --extra-args \'{}\'".format(workflow_dir, extra_args))) + + self.assertIn("regionalOptions", gwf_desc) + gwf_regional_options = gwf_desc["regionalOptions"] + self.assertIn("aws:us-east-1", gwf_regional_options) + + # Make sure additional resources were cloned to the workflow containers + # in the specified regions + aws_container = gwf_regional_options["aws:us-east-1"]["resources"] + aws_obj_id_list = dxpy.api.container_list_folder(aws_container, {"folder": "/"}) + self.assertIn(aws_file_id, [item["id"] for item in aws_obj_id_list["objects"]]) + + def test_build_workflow_in_invalid_multi_regions(self): + gwf_name = "gwf_{t}_multi_region".format(t=int(time.time())) + dxworkflow_json = dict(self.dxworkflow_spec, name=gwf_name) + workflow_dir = self.write_workflow_directory(gwf_name, + json.dumps(dxworkflow_json), + readme_content="Workflow Readme Please") + + error_msg = "The applet {} is not available".format(self.test_applet_id) + with self.assertRaisesRegex(DXCalledProcessError, error_msg): + run("dx build --globalworkflow --region azure:westus --json " + workflow_dir) @unittest.skipUnless(testutil.TEST_ISOLATED_ENV, 'skipping test that would create global workflows') @@ -4309,9 +5001,10 @@ def create_multi_reg_app(): "version": "0.0.111", "runSpec": { "file": "code.py", - "interpreter": "python2.7", + "interpreter": "python3", "distribution": "Ubuntu", - "release": "14.04" + "release": "20.04", + "version": "0" }, "inputSpec": [], "outputSpec": [], @@ -4374,7 +5067,7 @@ def test_dx_find_apps_and_globalworkflows_category(self): @unittest.skipUnless(testutil.TEST_ISOLATED_ENV, 'skipping test that creates apps') - @pytest.mark.TRACEABILITY_MATRIX + @pytest.mark.TRACEABILITY_ISOLATED_ENV @testutil.update_traceability_matrix(["DNA_API_APP_PUBLISH"]) def test_dx_find_apps(self): test_applet_id = dxpy.api.applet_new({"name": "my_find_applet", @@ -4457,7 +5150,7 @@ def test_dx_find_apps(self): @unittest.skipUnless(testutil.TEST_ISOLATED_ENV, 'skipping test that creates global workflows') - @pytest.mark.TRACEABILITY_MATRIX + @pytest.mark.TRACEABILITY_ISOLATED_ENV @testutil.update_traceability_matrix(["DNA_CLI_WORKFLOW_LIST_AVAILABLE_WORKFLOWS_GLOBALWF"]) def test_dx_find_globalworkflows(self): test_applet_id = dxpy.api.applet_new({"name": "my_find_applet", @@ -4550,7 +5243,7 @@ def test_dx_find_data_formatted(self): record_id = dxpy.new_dxrecord(project=self.project, name="find_data_formatting", close=True).get_id() self.assertRegex( run("dx find data --name " + "find_data_formatting").strip(), - r"^closed\s+\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\s+/find_data_formatting \(" + record_id + "\)$" + r"^closed\s+\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\s+/find_data_formatting \(" + record_id + r"\)$" ) @pytest.mark.TRACEABILITY_MATRIX @@ -4811,17 +5504,17 @@ def test_dx_find_data_by_region(self): def test_dx_find_projects(self): unique_project_name = 'dx find projects test ' + str(time.time()) with temporary_project(unique_project_name) as unique_project: - self.assertEqual(run("dx find projects --name " + pipes.quote(unique_project_name)), + self.assertEqual(run("dx find projects --name " + shlex.quote(unique_project_name)), unique_project.get_id() + ' : ' + unique_project_name + ' (ADMINISTER)\n') - self.assertEqual(run("dx find projects --brief --name " + pipes.quote(unique_project_name)), + self.assertEqual(run("dx find projects --brief --name " + shlex.quote(unique_project_name)), unique_project.get_id() + '\n') - json_output = json.loads(run("dx find projects --json --name " + pipes.quote(unique_project_name))) + json_output = json.loads(run("dx find projects --json --name " + shlex.quote(unique_project_name))) self.assertEqual(len(json_output), 1) self.assertEqual(json_output[0]['id'], unique_project.get_id()) @unittest.skipUnless(testutil.TEST_ISOLATED_ENV, 'skipping test that depends on a public project only defined in the nucleus integration tests') - @pytest.mark.TRACEABILITY_MATRIX + @pytest.mark.TRACEABILITY_ISOLATED_ENV @testutil.update_traceability_matrix(["DNA_API_PROJ_VIEW_PUBLIC_PROJECTS"]) def test_dx_find_public_projects(self): unique_project_name = 'dx find public projects test ' + str(time.time()) @@ -4836,15 +5529,15 @@ def test_dx_find_projects_by_created(self): created_project_name = 'dx find projects test ' + str(time.time()) with temporary_project(created_project_name) as unique_project: self.assertEqual(run("dx find projects --created-after=-1d --brief --name " + - pipes.quote(created_project_name)), unique_project.get_id() + '\n') + shlex.quote(created_project_name)), unique_project.get_id() + '\n') self.assertEqual(run("dx find projects --created-before=" + str(int(time.time() + 1000) * 1000) + - " --brief --name " + pipes.quote(created_project_name)), + " --brief --name " + shlex.quote(created_project_name)), unique_project.get_id() + '\n') self.assertEqual(run("dx find projects --created-after=-1d --created-before=" + str(int(time.time() + 1000) * 1000) + " --brief --name " + - pipes.quote(created_project_name)), unique_project.get_id() + '\n') + shlex.quote(created_project_name)), unique_project.get_id() + '\n') self.assertEqual(run("dx find projects --created-after=" + str(int(time.time() + 1000) * 1000) + " --name " - + pipes.quote(created_project_name)), "") + + shlex.quote(created_project_name)), "") def test_dx_find_projects_by_region(self): awseast = "aws:us-east-1" @@ -4852,7 +5545,7 @@ def test_dx_find_projects_by_region(self): created_project_name = 'dx find projects test ' + str(time.time()) with temporary_project(created_project_name, region=awseast) as unique_project: self.assertEqual(run("dx find projects --region {} --brief --name {}".format( - awseast, pipes.quote(created_project_name))), + awseast, shlex.quote(created_project_name))), unique_project.get_id() + '\n') self.assertIn(unique_project.get_id(), run("dx find projects --region {} --brief".format(awseast))) @@ -4942,10 +5635,10 @@ def test_dx_find_projects_by_property(self): def test_dx_find_projects_phi(self): projectName = "tempProject+{t}".format(t=time.time()) with temporary_project(name=projectName) as project_1: - res = run('dx find projects --phi true --brief --name ' + pipes.quote(projectName)) + res = run('dx find projects --phi true --brief --name ' + shlex.quote(projectName)) self.assertTrue(len(res) == 0, "Expected no PHI projects to be found") - res = run('dx find projects --phi false --brief --name ' + pipes.quote(projectName)).strip().split('\n') + res = run('dx find projects --phi false --brief --name ' + shlex.quote(projectName)).strip().split('\n') self.assertTrue(len(res) == 1, "Expected to find one project") self.assertTrue(res[0] == project_1.get_id()) @@ -5007,8 +5700,9 @@ def test_find_executions(self): ], outputSpec=[{"name": "mappings", "class": "record"}], runSpec={"code": "def main(): pass", - "interpreter": "python2.7", - "distribution": "Ubuntu", "release": "14.04", + "interpreter": "python3", + "distribution": "Ubuntu", "release": "20.04", + "version": "0", "execDepends": [{"name": "python-numpy"}]}) dxrecord = dxpy.new_dxrecord() dxrecord.close() @@ -5080,6 +5774,23 @@ def test_find_executions(self): self.assertEqual(len(run("dx find jobs "+options2).splitlines()), 0) self.assertEqual(len(run("dx find analyses "+options2).splitlines()), 0) + # JSON output + def get_ids(data): + return [item['id'] for item in json.loads(data)] + + options2 = "--user=self --json" + self.assertEqual(len(get_ids(run("dx find executions " + options2))), 4) + self.assertEqual(len(get_ids(run("dx find jobs " + options2))), 3) + self.assertEqual(len(get_ids(run("dx find analyses " + options2))), 1) + options3 = options2 + " --origin-jobs" + self.assertEqual(len(get_ids(run("dx find executions " + options3))), 4) + self.assertEqual(len(get_ids(run("dx find jobs " + options3))), 3) + self.assertEqual(len(get_ids(run("dx find analyses " + options3))), 1) + options3 = options2 + " --all-jobs" + self.assertEqual(len(get_ids(run("dx find executions " + options3))), 4) + self.assertEqual(len(get_ids(run("dx find jobs " + options3))), 3) + self.assertEqual(len(get_ids(run("dx find analyses " + options3))), 1) + # Search by tag options2 = options + " --all-jobs --brief" options3 = options2 + " --tag foo" @@ -5103,6 +5814,41 @@ def test_find_executions(self): self.assert_cmd_gives_ids("dx find jobs "+options3, [job_id]) self.assert_cmd_gives_ids("dx find analyses "+options3, []) + + @unittest.skipUnless(testutil.TEST_RUN_JOBS, + 'skipping test that would run jobs') + def test_dx_find_internet_usage_IPs(self): + dxapplet = dxpy.DXApplet() + dxapplet.new(name="test_applet", + dxapi="1.0.0", + inputSpec=[{"name": "chromosomes", "class": "record"}, + {"name": "rowFetchChunk", "class": "int"} + ], + outputSpec=[{"name": "mappings", "class": "record"}], + runSpec={"code": "def main(): pass", + "interpreter": "python3", + "distribution": "Ubuntu", "release": "20.04", + "version": "0", + "execDepends": [{"name": "python-numpy"}]}) + dxrecord = dxpy.new_dxrecord() + dxrecord.close() + prog_input = {"chromosomes": {"$dnanexus_link": dxrecord.get_id()}, + "rowFetchChunk": 100} + dxapplet.run(applet_input=prog_input) + dxjob = dxapplet.run(applet_input=prog_input, + tags=["foo", "bar"], + properties={"foo": "baz"}) + + cd("{project_id}:/".format(project_id=dxapplet.get_proj_id())) + + output1 = run("dx find jobs --user=self --verbose --json") + output2 = run("dx describe {} --verbose --json".format(dxjob.get_id())) + output3 = run("dx describe {} --verbose".format(dxjob.get_id())) + + self.assertIn("internetUsageIPs", output1) + self.assertIn("internetUsageIPs", output2) + self.assertIn("Internet Usage IPs", output3) + @unittest.skipUnless(testutil.TEST_RUN_JOBS, 'skipping test that would run a job') def test_find_analyses_run_by_jobs(self): @@ -5204,7 +5950,24 @@ def test_find_analyses_run_by_jobs(self): self.assert_cmd_gives_ids("dx find jobs "+options2, []) self.assert_cmd_gives_ids("dx find analyses "+options2, []) - @pytest.mark.TRACEABILITY_MATRIX + # JSON output + def get_ids(data): + return set([item['id'] for item in json.loads(data)]) + + options2 = "--json --user=self --project=" + temp_proj_id + self.assertEqual(get_ids(run("dx find executions " + options2)), set([job_id, analysis_id, subjob_id])) + self.assertEqual(get_ids(run("dx find jobs " + options2)), set([job_id, subjob_id])) + self.assertEqual(get_ids(run("dx find analyses " + options2)), set([analysis_id])) + options3 = options2 + " --origin-jobs" + self.assertEqual(get_ids(run("dx find executions " + options3)), set([job_id, subjob_id])) + self.assertEqual(get_ids(run("dx find jobs " + options3)), set([job_id, subjob_id])) + self.assertEqual(get_ids(run("dx find analyses " + options3)), set([])) + options3 = options2 + " --all-jobs" + self.assertEqual(get_ids(run("dx find executions " + options3)), set([job_id, analysis_id, subjob_id])) + self.assertEqual(get_ids(run("dx find jobs " + options3)), set([job_id, subjob_id])) + self.assertEqual(get_ids(run("dx find analyses " + options3)), set([analysis_id])) + + @pytest.mark.TRACEABILITY_ISOLATED_ENV @testutil.update_traceability_matrix(["DNA_CLI_ORG_LIST_ORGS"]) @unittest.skipUnless(testutil.TEST_ISOLATED_ENV, 'skipping test that requires presence of test org') @@ -5213,7 +5976,7 @@ def test_find_orgs(self): self.assertTrue(dxpy.api.org_describe(org_with_billable_activities)["allowBillableActivities"]) org_without_billable_activities = "org-members_without_billing_rights" self.assertFalse(dxpy.api.org_describe(org_without_billable_activities)["allowBillableActivities"]) - orgs_with_admin = ["org-piratelabs", "org-auth_file_app_download"] + orgs_with_admin = ["org-piratelabs", "org-auth_file_app_download", "org-team_odd"] for org_with_admin in orgs_with_admin: self.assertTrue(dxpy.api.org_describe(org_with_admin)["level"] == "ADMIN") @@ -5294,7 +6057,7 @@ def test_dx_find_org_members_negative(self): with self.assertSubprocessFailure(stderr_regexp='error: argument --level: expected one argument', exit_code=2): run("dx find org members org-piratelabs --level") - @pytest.mark.TRACEABILITY_MATRIX + @pytest.mark.TRACEABILITY_ISOLATED_ENV @testutil.update_traceability_matrix(["DNA_CLI_ORG_LIST_MEMBERS", "DNA_API_ORG_FIND_MEMBERS"]) def test_dx_find_org_members(self): @@ -5326,7 +6089,7 @@ def test_dx_find_org_members_format(self): # Assert that return format is like: " : ()" levels = "(?:ADMIN|MEMBER)" output = run(cmd.format(opts="")).strip().split("\n") - pattern = "^user-[a-zA-Z0-9]* : .* \(" + levels + "\)$" + pattern = r"^user-[a-zA-Z0-9]* : .* \(" + levels + r"\)$" for result in output: self.assertRegex(result, pattern) @@ -5370,7 +6133,7 @@ def test_dx_find_org_projects_invalid(self): with self.assertSubprocessFailure(stderr_regexp='expected one argument', exit_code=2): run(cmd.format(opts="--phi")) - @pytest.mark.TRACEABILITY_MATRIX + @pytest.mark.TRACEABILITY_ISOLATED_ENV @testutil.update_traceability_matrix(["DNA_CLI_ORG_LIST_PROJECTS", "DNA_API_ORG_FIND_PROJECTS"]) def test_dx_find_org_projects(self): @@ -5506,7 +6269,7 @@ def test_dx_find_org_projects_format(self): # Assert that return format is like: "" levels = "(?:ADMINISTER|CONTRIBUTE|UPLOAD|VIEW|NONE)" output = run(cmd.format(opts="")).strip().split("\n") - pattern = "^project-[a-zA-Z0-9]{24} : .* \(" + levels + "\)$" + pattern = r"^project-[a-zA-Z0-9]{24} : .* \(" + levels + r"\)$" for result in output: self.assertRegex(result, pattern) @@ -5524,15 +6287,15 @@ def test_dx_find_org_projects_phi(self): project1_id = project_1.get_id() dxpy.api.project_update(project1_id, {"billTo": self.org_id}) - res = run('dx find org projects org-piratelabs --phi true --brief --name ' + pipes.quote(projectName)) + res = run('dx find org projects org-piratelabs --phi true --brief --name ' + shlex.quote(projectName)) self.assertTrue(len(res) == 0, "Expected no PHI projects to be found") - res = run('dx find org projects org-piratelabs --phi false --brief --name ' + pipes.quote(projectName)).strip().split("\n") + res = run('dx find org projects org-piratelabs --phi false --brief --name ' + shlex.quote(projectName)).strip().split("\n") self.assertTrue(len(res) == 1, "Expected to find one project") self.assertEqual(res[0], project1_id) - @pytest.mark.TRACEABILITY_MATRIX + @pytest.mark.TRACEABILITY_ISOLATED_ENV @testutil.update_traceability_matrix(["DNA_CLI_APP_LIST_APPS_ORG", "DNA_API_ORG_FIND_APPS"]) def test_dx_find_org_apps(self): @@ -5556,7 +6319,7 @@ def test_dx_find_org_apps(self): # Same as above, without the --brief flag, so we need to destructure formatting lengthy_outputs = run("dx find org apps {}".format(self.org_id)).rstrip().split("\n") - pattern = "^(\s\s|(\s\S)*x(\s\S)*)[a-zA-Z0-9_]*\s\([a-zA-Z0-9_]*\),\sv[0-9.]*$" + pattern = r"^(\s\s|(\s\S)*x(\s\S)*)[a-zA-Z0-9_]*\s\([a-zA-Z0-9_]*\),\sv[0-9.]*$" for lengthy_output in lengthy_outputs: self.assertRegex(lengthy_output, pattern) @@ -5598,7 +6361,7 @@ def test_create_new_org_negative(self): "error: argument --member-list-visibility: invalid choice"): run('dx new org --member-list-visibility NONE') - @pytest.mark.TRACEABILITY_MATRIX + @pytest.mark.TRACEABILITY_ISOLATED_ENV @testutil.update_traceability_matrix(["DNA_CLI_ORG_CREATE", "DNA_API_ORG_CREATE", "DNA_API_ORG_DESCRIBE"]) @@ -5768,7 +6531,7 @@ def test_org_update_negative(self): with self.assertSubprocessFailure(stderr_regexp="--project-transfer-ability.*invalid", exit_code=2): run("dx update org {o} --project-transfer-ability PUBLIC".format(o=self.org_id)) - @pytest.mark.TRACEABILITY_MATRIX + @pytest.mark.TRACEABILITY_ISOLATED_ENV @testutil.update_traceability_matrix(["DNA_CLI_ORG_UPDATE_INFORMATION"]) def test_org_update(self): def get_name_and_policies(org_id=None): @@ -5873,7 +6636,7 @@ def test_dx_new_project_with_region(self): with self.assertRaisesRegex(subprocess.CalledProcessError, "InvalidInput"): run("dx new project --brief --region aws:not-a-region InvalidRegionProject") - @pytest.mark.TRACEABILITY_MATRIX + @pytest.mark.TRACEABILITY_ISOLATED_ENV @testutil.update_traceability_matrix(["DNA_CLI_PROJ_CREATE_NEW_PROJECT", "DNA_API_USR_MGMT_SET_BILLING_ACCOUNT", "DNA_API_ORG_ALLOW_BILLABLE_ACTIVITIES"]) @@ -5951,7 +6714,7 @@ def setUp(self): def tearDown(self): super(TestDXClientNewUser, self).tearDown() - @pytest.mark.TRACEABILITY_MATRIX + @pytest.mark.TRACEABILITY_ISOLATED_ENV @testutil.update_traceability_matrix(["DNA_CLI_USR_MGMT_NEW_USER"]) def test_create_user_account_and_set_bill_to_negative(self): username, email = generate_unique_username_email() @@ -5983,8 +6746,8 @@ def test_create_user_account_and_set_bill_to_negative(self): run(" ".join([cmd, invalid_opts])) resource_not_found_opts = [ - "--username {u} --email {e} --first {f} --org does_not_exist".format( - u=username, e=email, f=first), + "--username {u} --email {e} --first {f} --on-behalf-of {o} --org does_not_exist".format( + u=username, e=email, f=first, o=self.org_id), ] for invalid_opts in resource_not_found_opts: with self.assertRaisesRegex(subprocess.CalledProcessError, @@ -6009,7 +6772,33 @@ def test_create_user_account_and_set_bill_to_negative(self): with self.assertRaisesRegex(subprocess.CalledProcessError, "DXCLIError"): run(" ".join([cmd, invalid_opts])) - + + def test_create_user_on_behalf_of(self): + username, email = generate_unique_username_email() + first = "Asset" + cmd = "dx new user" + baseargs = "--username {u} --email {e} --first {f}".format(u=username, e=email, f=first) + user_id = run(" ".join([cmd, baseargs,"--on-behalf-of {o} --brief".format(o=self.org_id)])).strip() + self._assert_user_desc(user_id, {"first": first}) + + def test_create_user_on_behalf_of_negative(self): + username, email = generate_unique_username_email() + first = "Asset2" + cmd = "dx new user" + baseargs = "--username {u} --email {e} --first {f}".format(u=username, e=email, f=first) + + # no org specified + with self.assertRaisesRegex(subprocess.CalledProcessError, + "error: argument --on-behalf-of: expected one argument"): + run(" ".join([cmd, baseargs, "--on-behalf-of"])) + # creating user on behalf of org that does not exist + with self.assertRaisesRegex(subprocess.CalledProcessError, + "ResourceNotFound"): + run(" ".join([cmd, baseargs, "--on-behalf-of org-does_not_exist"])) + # creating user for org in which the adder does not have ADMIN permissions + with self.assertRaisesRegex(subprocess.CalledProcessError, + "(PermissionDenied)|(ResourceNotFound)"): + run(" ".join([cmd, baseargs, "--on-behalf-of org-dnanexus"])) def test_self_signup_negative(self): # How to unset context? @@ -6043,7 +6832,7 @@ def test_create_user_account_only(self): "last": last, "middle": middle}) - @pytest.mark.TRACEABILITY_MATRIX + @pytest.mark.TRACEABILITY_ISOLATED_ENV @testutil.update_traceability_matrix(["DNA_API_ORG_ADD_USER"]) def test_create_user_account_and_invite_to_org(self): # TODO: Test --no-email flag. @@ -6053,13 +6842,14 @@ def test_create_user_account_and_invite_to_org(self): # Grant default org membership level and permission flags. username, email = generate_unique_username_email() - user_id = run("{cmd} --username {u} --email {e} --first {f} --org {o} --brief".format( + user_id = run("{cmd} --username {u} --email {e} --first {f} --on-behalf-of {o} --org {o} --brief".format( cmd=cmd, u=username, e=email, f=first, o=self.org_id)).strip() self._assert_user_desc(user_id, {"first": first}) exp = { "level": "MEMBER", "allowBillableActivities": False, + "cloudIntegrationManagement": False, "appAccess": True, "projectAccess": "CONTRIBUTE", "id": user_id @@ -6071,13 +6861,14 @@ def test_create_user_account_and_invite_to_org(self): # has uppercase chars. username, email = generate_unique_username_email() username = username.upper() - user_id = run("{cmd} --username {u} --email {e} --first {f} --org {o} --brief".format( + user_id = run("{cmd} --username {u} --email {e} --first {f} --on-behalf-of {o} --org {o} --brief".format( cmd=cmd, u=username, e=email, f=first, o=self.org_id)).strip() self._assert_user_desc(user_id, {"first": first}) exp = { "level": "MEMBER", "allowBillableActivities": False, + "cloudIntegrationManagement": False, "appAccess": True, "projectAccess": "CONTRIBUTE", "id": user_id @@ -6087,13 +6878,14 @@ def test_create_user_account_and_invite_to_org(self): # Grant custom org membership level and permission flags. username, email = generate_unique_username_email() - user_id = run("{cmd} --username {u} --email {e} --first {f} --org {o} --level {l} --allow-billable-activities --no-app-access --project-access {pa} --brief".format( + user_id = run("{cmd} --username {u} --email {e} --first {f} --on-behalf-of {o} --org {o} --level {l} --allow-billable-activities --no-app-access --project-access {pa} --brief".format( cmd=cmd, u=username, e=email, f=first, o=self.org_id, l="MEMBER", pa="VIEW")).strip() self._assert_user_desc(user_id, {"first": first}) exp = { "level": "MEMBER", "allowBillableActivities": True, + "cloudIntegrationManagement": False, "appAccess": False, "projectAccess": "VIEW", "id": user_id @@ -6104,13 +6896,14 @@ def test_create_user_account_and_invite_to_org(self): # Grant ADMIN org membership level; ignore all other org permission # options. username, email = generate_unique_username_email() - user_id = run("{cmd} --username {u} --email {e} --first {f} --org {o} --level {l} --no-app-access --project-access {pa} --brief".format( + user_id = run("{cmd} --username {u} --email {e} --first {f} --on-behalf-of {o} --org {o} --level {l} --no-app-access --project-access {pa} --brief".format( cmd=cmd, u=username, e=email, f=first, o=self.org_id, l="ADMIN", pa="VIEW")).strip() self._assert_user_desc(user_id, {"first": first}) exp = { "level": "ADMIN", "allowBillableActivities": True, + "cloudIntegrationManagement": False, "appAccess": True, "projectAccess": "ADMINISTER", "id": user_id @@ -6132,6 +6925,7 @@ def test_create_user_account_and_set_bill_to(self): exp = { "level": "MEMBER", "allowBillableActivities": True, + "cloudIntegrationManagement": False, "appAccess": True, "projectAccess": "VIEW", "id": user_id @@ -6148,6 +6942,7 @@ def test_create_user_account_and_set_bill_to(self): exp = { "level": "ADMIN", "allowBillableActivities": True, + "cloudIntegrationManagement": False, "appAccess": True, "projectAccess": "ADMINISTER", "id": user_id @@ -6214,7 +7009,7 @@ def tearDown(self): self._remove_user(self.user_id) super(TestDXClientMembership, self).tearDown() - @pytest.mark.TRACEABILITY_MATRIX + @pytest.mark.TRACEABILITY_ISOLATED_ENV @testutil.update_traceability_matrix(["DNA_CLI_ORG_ADD_MEMBER"]) def test_add_membership_default(self): cmd = "dx add member {o} {u} --level {l}" @@ -6223,6 +7018,7 @@ def test_add_membership_default(self): exp_membership = {"id": self.user_id, "level": "ADMIN", "allowBillableActivities": True, + "cloudIntegrationManagement": False, "appAccess": True, "projectAccess": "ADMINISTER"} membership = self._org_find_members(self.user_id) @@ -6234,6 +7030,7 @@ def test_add_membership_default(self): exp_membership = {"id": self.user_id, "level": "MEMBER", "allowBillableActivities": False, + "cloudIntegrationManagement": False, "appAccess": True, "projectAccess": "CONTRIBUTE"} membership = self._org_find_members(self.user_id) @@ -6247,6 +7044,7 @@ def test_add_membership_with_options(self): exp_membership = {"id": self.user_id, "level": "ADMIN", "allowBillableActivities": True, + "cloudIntegrationManagement": False, "appAccess": True, "projectAccess": "ADMINISTER"} membership = self._org_find_members(self.user_id) @@ -6259,6 +7057,7 @@ def test_add_membership_with_options(self): exp_membership = {"id": self.user_id, "level": "MEMBER", "allowBillableActivities": True, + "cloudIntegrationManagement": False, "appAccess": False, "projectAccess": "NONE"} membership = self._org_find_members(self.user_id) @@ -6283,7 +7082,7 @@ def test_add_membership_negative(self): with self.assertRaisesRegex(subprocess.CalledProcessError, "DXCLIError"): run(" ".join([cmd, self.org_id, self.username, "--level ADMIN"])) - @pytest.mark.TRACEABILITY_MATRIX + @pytest.mark.TRACEABILITY_ISOLATED_ENV @testutil.update_traceability_matrix(["DNA_CLI_ORG_REMOVE_MEMBER", "DNA_API_ORG_REMOVE_USER"]) def test_remove_membership_default(self): @@ -6292,6 +7091,7 @@ def test_remove_membership_default(self): exp_membership = {"id": self.user_id, "level": "ADMIN", "allowBillableActivities": True, + "cloudIntegrationManagement": False, "appAccess": True, "projectAccess": "ADMINISTER"} membership = self._org_find_members(self.user_id) @@ -6307,6 +7107,7 @@ def test_remove_membership_interactive_conf(self): exp_membership = {"id": self.user_id, "level": "ADMIN", "allowBillableActivities": True, + "cloudIntegrationManagement": False, "appAccess": True, "projectAccess": "ADMINISTER"} membership = self._org_find_members(self.user_id) @@ -6354,6 +7155,7 @@ def test_remove_membership_interactive_conf_format(self): exp_membership = {"id": self.user_id, "level": "ADMIN", "allowBillableActivities": True, + "cloudIntegrationManagement": False, "appAccess": True, "projectAccess": "ADMINISTER"} membership = self._org_find_members(self.user_id) @@ -6406,7 +7208,7 @@ def test_remove_membership_negative(self): with self.assertRaises(subprocess.CalledProcessError): run(" ".join([cmd, invalid_opts])) - @pytest.mark.TRACEABILITY_MATRIX + @pytest.mark.TRACEABILITY_ISOLATED_ENV @testutil.update_traceability_matrix(["DNA_CLI_ORG_UPDATE_USER_MEMBERSHIP", "DNA_API_ORG_CHANGE_USER_PERMISSIONS"]) def test_update_membership_positive(self): @@ -6416,6 +7218,7 @@ def test_update_membership_positive(self): exp_membership = {"id": self.user_id, "level": "ADMIN", "allowBillableActivities": True, + "cloudIntegrationManagement": False, "appAccess": True, "projectAccess": "ADMINISTER"} membership = self._org_find_members(self.user_id) @@ -6426,6 +7229,7 @@ def test_update_membership_positive(self): exp_membership = {"id": self.user_id, "level": "MEMBER", "allowBillableActivities": False, + "cloudIntegrationManagement": False, "projectAccess": "VIEW", "appAccess": True} membership = self._org_find_members(self.user_id) @@ -6436,6 +7240,7 @@ def test_update_membership_positive(self): exp_membership = {"id": self.user_id, "level": "MEMBER", "allowBillableActivities": True, + "cloudIntegrationManagement": False, "projectAccess": "VIEW", "appAccess": False} @@ -6450,6 +7255,7 @@ def test_update_membership_to_member_without_membership_flags(self): exp = {"id": self.user_id, "level": "ADMIN", "allowBillableActivities": True, + "cloudIntegrationManagement": False, "projectAccess": "ADMINISTER", "appAccess": True} membership_response = self._org_find_members(self.user_id) @@ -6460,6 +7266,7 @@ def test_update_membership_to_member_without_membership_flags(self): "level": "MEMBER", "allowBillableActivities": False, "projectAccess": "CONTRIBUTE", + "cloudIntegrationManagement": False, "appAccess": True} membership_response = self._org_find_members(self.user_id) self.assertEqual(membership_response, exp) @@ -6469,6 +7276,7 @@ def test_update_membership_to_member_without_membership_flags(self): exp = {"id": self.user_id, "level": "MEMBER", "allowBillableActivities": True, + "cloudIntegrationManagement": False, "projectAccess": "CONTRIBUTE", "appAccess": True} membership_response = self._org_find_members(self.user_id) @@ -6518,6 +7326,7 @@ def test_add_update_remove_membership(self): exp_membership = {"id": self.user_id, "level": "MEMBER", "allowBillableActivities": False, + "cloudIntegrationManagement": False, "appAccess": True, "projectAccess": "UPLOAD"} membership = self._org_find_members(self.user_id) @@ -6534,6 +7343,7 @@ def test_add_update_remove_membership(self): exp_membership = {"id": self.user_id, "level": "ADMIN", "allowBillableActivities": True, + "cloudIntegrationManagement": False, "appAccess": True, "projectAccess": "ADMINISTER"} membership = self._org_find_members(self.user_id) @@ -6560,6 +7370,7 @@ def test_add_update_remove_membership_with_user_id(self): exp_membership = {"id": self.user_id, "level": "MEMBER", "allowBillableActivities": False, + "cloudIntegrationManagement": False, "appAccess": True, "projectAccess": "UPLOAD"} membership = self._org_find_members(self.user_id) @@ -6576,6 +7387,7 @@ def test_add_update_remove_membership_with_user_id(self): exp_membership = {"id": self.user_id, "level": "ADMIN", "allowBillableActivities": True, + "cloudIntegrationManagement": False, "appAccess": True, "projectAccess": "ADMINISTER"} membership = self._org_find_members(self.user_id) @@ -6619,7 +7431,7 @@ def test_update_strings(self): #Update items one by one. for item in update_items: - run(self.cmd.format(pid=self.project, item=item, n=pipes.quote(update_items[item]))) + run(self.cmd.format(pid=self.project, item=item, n=shlex.quote(update_items[item]))) describe_input = {} describe_input[item] = 'true' self.assertEqual(self.project_describe(describe_input)[item], @@ -6635,14 +7447,14 @@ def test_update_multiple_items(self): 'protected': 'false'} update_project_output = check_output(["dx", "update", "project", self.project, "--name", - pipes.quote(update_items['name']), "--summary", update_items['summary'], "--description", + shlex.quote(update_items['name']), "--summary", update_items['summary'], "--description", update_items['description'], "--protected", update_items['protected']]) update_project_json = json.loads(update_project_output); self.assertTrue("id" in update_project_json) self.assertEqual(self.project, update_project_json["id"]) update_project_output = check_output(["dx", "update", "project", self.project, "--name", - pipes.quote(update_items['name']), "--summary", update_items['summary'], "--description", + shlex.quote(update_items['name']), "--summary", update_items['summary'], "--description", update_items['description'], "--protected", update_items['protected'], "--brief"]) self.assertEqual(self.project, update_project_output.rstrip("\n")) @@ -6667,7 +7479,7 @@ def test_update_project_by_name(self): project_name = self.project_describe(describe_input)['name'] new_name = 'Another Project Name' + str(time.time()) - run(self.cmd.format(pid=project_name, item='name', n=pipes.quote(new_name))) + run(self.cmd.format(pid=project_name, item='name', n=shlex.quote(new_name))) result = self.project_describe(describe_input) self.assertEqual(result['name'], new_name) @@ -6802,9 +7614,20 @@ def test_build_workflow_invalid_project_context(self): # will be enabled in the region of the project context # (if regionalOptions or --region are not set) env = override_environment(DX_PROJECT_CONTEXT_ID='project-B00000000000000000000000') - with self.assertRaisesRegexp(subprocess.CalledProcessError, "ResourceNotFound"): + with self.assertRaisesRegex(subprocess.CalledProcessError, "ResourceNotFound"): run("dx build --create-globalworkflow --json " + workflow_dir, env=env) + # this is NOT a global workflow + def test_build_workflow_with_tree_tat_threshold(self): + wf_name = "workflow_build_tree_tat_threshold" + dxworkflow_json = dict(self.dxworkflow_spec, name=wf_name) + dxworkflow_json.update({"treeTurnaroundTimeThreshold": 2}) + workflow_dir = self.write_workflow_directory(wf_name, + json.dumps(dxworkflow_json)) + new_workflow_with_tat = run_and_parse_json("dx build --json " + workflow_dir) + workflow_describe = dxpy.get_handler(new_workflow_with_tat["id"]).describe() + self.assertEqual(workflow_describe['treeTurnaroundTimeThreshold'], 2) + @unittest.skipUnless(testutil.TEST_ISOLATED_ENV, 'skipping test that would create global workflows') def test_build_workflow_with_bill_to(self): @@ -6827,9 +7650,92 @@ def test_build_workflow_with_bill_to(self): new_gwf = json.loads(run("dx build --globalworkflow --bill-to {} --json {}".format(org_id, workflow_dir))) self.assertEqual(new_gwf["billTo"], org_id) + @unittest.skipUnless(testutil.TEST_ISOLATED_ENV, + 'skipping test that requires presence of test org') + def test_build_workflow_without_bill_to_rights(self): + alice_id = "user-alice" + unbillable_org_id = "org-members_without_billing_rights" + + # --bill-to is set to org-members_without_billing_rights with dx build + gwf_name = "globalworkflow_build_to_org_without_billing_rights" + dxworkflow_json = dict(self.dxworkflow_spec, name=gwf_name) + workflow_dir = self.write_workflow_directory(gwf_name, + json.dumps(dxworkflow_json)) + with self.assertSubprocessFailure(stderr_regexp='You are not a member in {} with allowBillableActivities permission.'.format(unbillable_org_id), exit_code=3): + run("dx build --globalworkflow --bill-to {} --json {}".format(unbillable_org_id, workflow_dir)) + + def test_build_workflow_with_invalid_bill_to(self): + other_user_id = "user-bob" + nonexist_org_id = "org-not_exist" + + # --bill-to is set to another user + gwf_name = "globalworkflow_build_bill_to_another_user" + dxworkflow_json = dict(self.dxworkflow_spec, name=gwf_name) + workflow_dir = self.write_workflow_directory(gwf_name, + json.dumps(dxworkflow_json)) + with self.assertSubprocessFailure(stderr_regexp='Cannot request another user to be the "billTo"', exit_code=3): + run("dx build --globalworkflow --bill-to {} --json {}".format(other_user_id, workflow_dir)) + + # --bill-to is set to an non exist org + gwf_name = "globalworkflow_build_to_nonexist_org" + dxworkflow_json = dict(self.dxworkflow_spec, name=gwf_name) + workflow_dir = self.write_workflow_directory(gwf_name, + json.dumps(dxworkflow_json)) + with self.assertSubprocessFailure(stderr_regexp='Cannot retrieve billing information for {}.'.format(nonexist_org_id), exit_code=3): + run("dx build --globalworkflow --bill-to {} --json {}".format(nonexist_org_id, workflow_dir)) + + def test_build_globalworkflow_from_nonexist_workflow(self): + # build global workflow from nonexist workflow + source_wf = "workflow-B00000000000000000000000" + with self.assertSubprocessFailure(stderr_regexp="The entity {} could not be found".format(source_wf), exit_code=3): + run("dx build --globalworkflow --from {} --version 0.0.1".format(source_wf)) + + def test_build_globalworkflow_without_version_override(self): + # build global workflow without specified version + source_wf_id = self.create_workflow(project_id=self.project).get_id() + with self.assertSubprocessFailure(stderr_regexp="--version must be specified when using the --from option", exit_code=2): + run("dx build --globalworkflow --from {}".format(source_wf_id)) + + def test_build_globalworkflow_with_workflow_path(self): + # build global workflow without specified version + source_wf_name = "globalworkflow_build_from_workflow" + source_wf_dir = "/source_wf_dir/" + dxworkflow_json = dict(self.create_workflow_spec(self.project), name=source_wf_name, folder=source_wf_dir,parents=True) + source_wf_id = self.create_workflow(project_id=self.project,workflow_spec=dxworkflow_json).get_id() + + # after resolving the path, force exiting the building process by forcing args conflict + with self.assertSubprocessFailure(stderr_regexp="--version must be specified when using the --from option", exit_code=2): + run("dx build --globalworkflow --from :{}".format(source_wf_id)) + with self.assertSubprocessFailure(stderr_regexp="--version must be specified when using the --from option", exit_code=2): + run("dx build --globalworkflow --from {}:{}".format(self.project, source_wf_id)) + with self.assertSubprocessFailure(stderr_regexp="--version must be specified when using the --from option", exit_code=2): + run("dx build --globalworkflow --from {}:{}{}".format(self.project, source_wf_dir, source_wf_name)) + + def test_build_globalworkflow_from_old_WDL_workflow(self): + SUPPORTED_DXCOMPILER_VERSION = "2.8.0" + # build global workflow from WDL workflows + gwf_name = "globalworkflow_build_from_wdl_workflow" + dxworkflow_json = dict(self.dxworkflow_spec, name=gwf_name) + + # Here we are using a non-WDL workflow to attempt to build a global workflow and only mock a WDL workflow by adding a dxCompiler tag + dxworkflow_json["tags"]="dxCompiler" + workflow_dir = self.write_workflow_directory(gwf_name, + json.dumps(dxworkflow_json)) + # reject building gwf if the WDL workflow spec doesn't have the dxCompiler version in its details + with self.assertSubprocessFailure(stderr_regexp="Cannot find the dxCompiler version", exit_code=3): + run("dx build --globalworkflow --version 0.0.1 {}".format(workflow_dir)) + + # mock the dxCompiler version that built the workflow + dxworkflow_json.update({"details": {"version":"0.0.1"}}) + workflow_dir = self.write_workflow_directory(gwf_name, + json.dumps(dxworkflow_json)) + # reject building gwf if the source WDL workflow is built by unsupported dxCompiler + with self.assertSubprocessFailure(stderr_regexp="Source workflow " + dxworkflow_json["name"] + r" is not compiled using dxCompiler \(version>=" + SUPPORTED_DXCOMPILER_VERSION + r"\) that supports creating global workflows.", exit_code=3): + run("dx build --globalworkflow {}".format(workflow_dir)) + @unittest.skipUnless(testutil.TEST_ISOLATED_ENV, 'skipping test that would create global workflows') - @pytest.mark.TRACEABILITY_MATRIX + @pytest.mark.TRACEABILITY_ISOLATED_ENV @testutil.update_traceability_matrix(["DNA_CLI_WORKFLOW_REMOVE_AUTHORIZED_USERS_GLOBALWF", "DNA_CLI_WORKFLOW_LIST_AUTHORIZED_USERS_GLOBALWF", "DNA_CLI_WORKFLOW_ADD_AUTHORIZED_USERS_GLOBALWF"]) @@ -6882,7 +7788,7 @@ def test_dx_add_list_remove_users_of_global_workflows(self): run('dx remove users wf_test_dx_users nonexistentuser') run('dx remove users wf_test_dx_users piratelabs') - @pytest.mark.TRACEABILITY_MATRIX + @pytest.mark.TRACEABILITY_ISOLATED_ENV @testutil.update_traceability_matrix(["DNA_CLI_WORKFLOW_ADD_DEVELOPERS_GLOBALWF", "DNA_CLI_WORKFLOW_LIST_DEVELOPERS_GLOBALWF", "DNA_CLI_WORKFLOW_REMOVE_DEVELOPERS_GLOBALWF"]) @@ -6945,7 +7851,7 @@ def test_dx_add_list_remove_developers_of_global_workflows(self): run('dx remove developers wf_test_dx_developers piratelabs') - @pytest.mark.TRACEABILITY_MATRIX + @pytest.mark.TRACEABILITY_ISOLATED_ENV @testutil.update_traceability_matrix(["DNA_CLI_WORKFLOW_PUBLISH_GLOBALWF"]) @unittest.skipUnless(testutil.TEST_ISOLATED_ENV, 'skipping test that would create global workflows') @@ -7125,6 +8031,17 @@ def test_build_applet(self): self.assertEqual(applet_describe["id"], applet_describe["id"]) self.assertEqual(applet_describe["name"], "minimal_applet") + @pytest.mark.TRACEABILITY_MATRIX + @testutil.update_traceability_matrix(["DNA_CLI_APP_UPLOAD_BUILD_NEW_APPLET"]) + def test_build_applet_with_extra_args(self): + app_spec = dict(self.base_app_spec, name="minimal_applet_to_run") + app_dir = self.write_app_directory("minimal_åpplet", json.dumps(app_spec), "code.py") + applet_id = run_and_parse_json("dx build " + app_dir + ' -y --brief' + ' --extra-args \'{"name": "applet_with_new_name"}\'')["id"] + applet_describe = dxpy.get_handler(applet_id).describe() + self.assertEqual(applet_describe["class"], "applet") + self.assertEqual(applet_describe["id"], applet_describe["id"]) + self.assertEqual(applet_describe["name"], "applet_with_new_name") + def test_dx_build_applet_dxapp_json_created_with_makefile(self): app_name = "nodxapp_applet" app_dir = self.write_app_directory(app_name, None, "code.py") @@ -7202,6 +8119,20 @@ def test_build_applet_and_run_immediately(self): self.assertEqual(job_desc['name'], 'minimal_applet_to_run') self.assertEqual(job_desc['priority'], 'normal') + + @unittest.skipUnless(testutil.TEST_RUN_JOBS, 'skipping test that would run jobs') + def test_build_applet_tree_tat_threshold_and_run(self): + applet_spec = dict(self.base_app_spec, name="minimal_applet_with_tat_threshold") + applet_spec.update({"treeTurnaroundTimeThreshold": 2}) + applet_dir = self.write_app_directory("minimal_applet_with_tat_threshold", json.dumps(applet_spec), "code.py") + new_applet_with_tat = run_and_parse_json("dx build --json " + applet_dir) + applet_describe = dxpy.get_handler(new_applet_with_tat["id"]).describe() + self.assertEqual(applet_describe['treeTurnaroundTimeThreshold'], 2) + job_id = run("dx run {} --yes --brief".format(applet_describe["id"])).strip() + job_describe = dxpy.describe(job_id) + self.assertEqual(job_describe['selectedTreeTurnaroundTimeThreshold'], 2) + self.assertEqual(job_describe['selectedTreeTurnaroundTimeThresholdFrom'], "executable") + @unittest.skipUnless(testutil.TEST_RUN_JOBS, 'skipping test that would run jobs') def test_remote_build_applet_and_run_immediately(self): app_spec = dict(self.base_app_spec, name="minimal_remote_build_applet_to_run") @@ -7286,8 +8217,8 @@ def test_build_applet_warnings(self): "summary": "a summary sentence.", "description": "foo", "dxapi": "1.0.0", - "runSpec": {"file": "code.py", "interpreter": "python2.7", - "distribution": "Ubuntu", "release": "14.04"}, + "runSpec": {"file": "code.py", "interpreter": "python3", + "distribution": "Ubuntu", "release": "20.04", "version": "0"}, "inputSpec": [{"name": "34", "class": "int"}], "outputSpec": [{"name": "92", "class": "string"}], "version": "1.0.0", @@ -7365,8 +8296,8 @@ def test_build_app_suggestions(self): app_spec = { "name": "test_build_app_suggestions", "dxapi": "1.0.0", - "runSpec": {"file": "code.py", "interpreter": "python2.7", - "distribution": "Ubuntu", "release": "14.04"}, + "runSpec": {"file": "code.py", "interpreter": "python3", + "distribution": "Ubuntu", "release": "20.04", "version": "0"}, "inputSpec": [{"name": "testname", "class": "file", "suggestions": []}], "outputSpec": [], "version": "0.0.1" @@ -7410,8 +8341,8 @@ def test_build_app_suggestions(self): def test_build_app_suggestions_success(self): app_spec = {"name": "test_build_app_suggestions", "dxapi": "1.0.0", - "runSpec": {"file": "code.py", "interpreter": "python2.7", - "distribution": "Ubuntu", "release": "14.04"}, + "runSpec": {"file": "code.py", "interpreter": "python3", + "distribution": "Ubuntu", "release": "20.04", "version": "0"}, "inputSpec": [{"name": "testname", "class": "file", "suggestions": []}], "outputSpec": [], "version": "0.0.1"} @@ -7427,17 +8358,17 @@ def test_build_app_suggestions_success(self): def test_build_applet_with_no_dxapp_json(self): app_dir = self.write_app_directory("åpplet_with_no_dxapp_json", None, "code.py") - with self.assertSubprocessFailure(stderr_regexp='does not contain dxapp\.json', exit_code=3): + with self.assertSubprocessFailure(stderr_regexp=r'does not contain dxapp\.json', exit_code=3): run("dx build " + app_dir) def test_build_applet_with_malformed_dxapp_json(self): app_dir = self.write_app_directory("åpplet_with_malformed_dxapp_json", "{", "code.py") - with self.assertSubprocessFailure(stderr_regexp='Could not parse dxapp\.json file', exit_code=3): + with self.assertSubprocessFailure(stderr_regexp=r'Could not parse dxapp\.json file', exit_code=3): run("dx build " + app_dir) @unittest.skipUnless(testutil.TEST_ISOLATED_ENV, 'skipping test that would create apps') - @pytest.mark.TRACEABILITY_MATRIX + @pytest.mark.TRACEABILITY_ISOLATED_ENV @testutil.update_traceability_matrix(["DNA_API_APP_DESCRIBE"]) def test_build_single_region_app_without_regional_options(self): # Backwards-compatible. @@ -7958,7 +8889,7 @@ def test_build_app_with_region(self): @unittest.skipUnless(testutil.TEST_ISOLATED_ENV, 'skipping test that would create apps') - @pytest.mark.TRACEABILITY_MATRIX + @pytest.mark.TRACEABILITY_ISOLATED_ENV @testutil.update_traceability_matrix(["DNA_API_APP_CREATE"]) def test_build_app_with_bill_to(self): alice_id = "user-alice" @@ -8000,8 +8931,8 @@ def test_invalid_execdepends(self): "dxapi": "1.0.0", "runSpec": { "file": "code.py", - "interpreter": "python2.7", - "distribution": "Ubuntu", "release": "14.04", + "interpreter": "python3", + "distribution": "Ubuntu", "release": "20.04", "version": "0", "execDepends": {"name": "oops"} }, "inputSpec": [], @@ -8009,7 +8940,7 @@ def test_invalid_execdepends(self): "version": "1.0.0" } app_dir = self.write_app_directory("invalid_execdepends", json.dumps(app_spec), "code.py") - with self.assertSubprocessFailure(stderr_regexp="Expected runSpec\.execDepends to"): + with self.assertSubprocessFailure(stderr_regexp=r"Expected runSpec\.execDepends to"): run("dx build --json " + app_dir) def test_invalid_authorized_users(self): @@ -8035,9 +8966,9 @@ def test_deps_without_network_access(self): app_spec = dict(self.base_app_spec, name="test_deps_without_network_access", runSpec={"execDepends": [{"name": "ddd", "package_manager": "pip"}], "file": "code.py", - "interpreter": "python2.7", + "interpreter": "python3", "distribution": "Ubuntu", - "release": "14.04"}) + "release": "20.04", "version": "0"}) app_dir = self.write_app_directory("deps_without_network_access", json.dumps(app_spec), "code.py") @@ -8110,7 +9041,7 @@ def test_update_app_categories(self): self.assertEqual(json.loads(run("dx api " + app_id + " listCategories"))["categories"], ['B']) @unittest.skipUnless(testutil.TEST_ISOLATED_ENV, 'skipping test that would create apps') - @pytest.mark.TRACEABILITY_MATRIX + @pytest.mark.TRACEABILITY_ISOLATED_ENV @testutil.update_traceability_matrix(["DNA_API_APP_LIST_AUTHORIZED_USERS","DNA_API_APP_ADD_AUTHORIZED_USER"]) def test_update_app_authorized_users(self): app0_spec = dict(self.base_app_spec, name="update_app_authorized_users") @@ -8132,7 +9063,7 @@ def test_update_app_authorized_users(self): self.assertEqual(json.loads(run("dx api " + app_id + " listAuthorizedUsers"))["authorizedUsers"], ["user-eve"]) - @pytest.mark.TRACEABILITY_MATRIX + @pytest.mark.TRACEABILITY_ISOLATED_ENV @testutil.update_traceability_matrix(["DNA_CLI_APP_ADD_AUTHORIZED_USERS_APP", "DNA_CLI_APP_LIST_AUTHORIZED_USERS_APP", "DNA_CLI_APP_REMOVE_AUTHORIZED_USERS_APP"]) @@ -8185,7 +9116,7 @@ def test_dx_add_list_remove_users_of_apps(self): run('dx remove users test_dx_users nonexistentuser') run('dx remove users test_dx_users piratelabs') - @pytest.mark.TRACEABILITY_MATRIX + @pytest.mark.TRACEABILITY_ISOLATED_ENV @testutil.update_traceability_matrix(["DNA_CLI_APP_ADD_DEVELOPERS_APP", "DNA_CLI_APP_LIST_DEVELOPERS_APP", "DNA_CLI_APP_REMOVE_DEVELOPERS_APP", @@ -8271,6 +9202,12 @@ def test_build_failure(self): def test_syntax_checks(self): app_spec = dict(self.base_app_spec, name="syntax_checks") + if USING_PYTHON2: + app_spec['runSpec']['interpreter'] = 'python2.7' + else: + app_spec['runSpec']['interpreter'] = 'python3' + app_spec['runSpec']['release'] = '20.04' + app_dir = self.write_app_directory("syntax_checks", json.dumps(app_spec), code_filename="code.py", @@ -8285,7 +9222,7 @@ def test_build_and_run_applet_remote(self): app_spec = { "name": "build_applet_remote", "dxapi": "1.0.0", - "runSpec": {"file": "code.py", "interpreter": "python2.7", "distribution": "Ubuntu", "release": "14.04"}, + "runSpec": {"file": "code.py", "interpreter": "python3", "distribution": "Ubuntu", "release": "20.04", "version": "0"}, "inputSpec": [ {"name": "in1", "class": "int"}, ], @@ -8316,7 +9253,7 @@ def test_applet_help(self): app_spec = { "name": "applet_help", "dxapi": "1.0.0", - "runSpec": {"file": "code.py", "interpreter": "python2.7", "distribution": "Ubuntu", "release": "14.04"}, + "runSpec": {"file": "code.py", "interpreter": "python3", "distribution": "Ubuntu", "release": "20.04", "version": "0"}, "inputSpec": [ {"name": "reads", "class": "array:file", "type": "LetterReads", "label": "Reads", "help": "One or more Reads table objects."}, @@ -8943,7 +9880,7 @@ def test_asset_depends_using_name(self): with self.assertSubprocessFailure(stderr_regexp="No asset bundle was found", exit_code=3): app_spec = dict(self.base_app_spec, name="asset_depends", runSpec = {"assetDepends": [{"name": record_name, "version": "0.0.1", "project": self.project}], - "file": "code.py", "distribution": "Ubuntu", "release": "14.04", "interpreter": "python2.7"}) + "file": "code.py", "distribution": "Ubuntu", "release": "20.04", "interpreter": "python3", "version": "0"}) app_dir = self.write_app_directory("asset_depends", json.dumps(app_spec), "code.py") asset_applet = json.loads(run("dx build --json {app_dir}".format(app_dir=app_dir)))["id"] run("dx build --json {app_dir}".format(app_dir=app_dir)) @@ -8951,7 +9888,7 @@ def test_asset_depends_using_name(self): # success: asset found app_spec = dict(self.base_app_spec, name="asset_depends", runSpec = {"assetDepends": [{"name": record_name, "version": "0.0.1", "project": self.project, "folder": "/record_subfolder"}], - "file": "code.py", "distribution": "Ubuntu", "release": "14.04", "interpreter": "python2.7"}) + "file": "code.py", "distribution": "Ubuntu", "release": "20.04", "interpreter": "python3", "version": "0"}) app_dir = self.write_app_directory("asset_depends", json.dumps(app_spec), "code.py") asset_applet = json.loads(run("dx build --json {app_dir}".format(app_dir=app_dir)))["id"] @@ -8966,7 +9903,7 @@ def test_asset_depends_using_name(self): with self.assertSubprocessFailure(stderr_regexp="Found more than one asset record that matches", exit_code=3): app_spec = dict(self.base_app_spec, name="asset_depends_fail", runSpec = {"assetDepends": [{"name": record_name, "version": "0.0.1", "project": self.project, "folder": "/record_subfolder"}], - "file": "code.py", "distribution": "Ubuntu", "release": "14.04", "interpreter": "python2.7"}) + "file": "code.py", "distribution": "Ubuntu", "release": "20.04", "interpreter": "python3", "version": "0"}) app_dir = self.write_app_directory("asset_depends_fail", json.dumps(app_spec), "code.py") asset_applet = json.loads(run("dx build --json {app_dir}".format(app_dir=app_dir)))["id"] run("dx build --json {app_dir}".format(app_dir=app_dir)) @@ -8987,7 +9924,7 @@ def test_asset_depends_using_id(self): app_spec = dict(self.base_app_spec, name="asset_depends", runSpec={"assetDepends": [{"id": record.get_id()}], - "file": "code.py", "distribution": "Ubuntu", "release": "14.04", "interpreter": "python2.7"}) + "file": "code.py", "distribution": "Ubuntu", "release": "20.04", "interpreter": "python3", "version": "0"}) app_dir = self.write_app_directory("asset_depends", json.dumps(app_spec), "code.py") asset_applet = run_and_parse_json("dx build --json {app_dir}".format(app_dir=app_dir))["id"] self.assertEqual( @@ -9010,7 +9947,7 @@ def test_asset_depends_failure(self): app_spec = dict(self.base_app_spec, name="asset_depends", runSpec={"assetDepends": [{"name": record_name, "version": "0.1.1", "project": self.project}], - "file": "code.py", "distribution": "Ubuntu", "release": "14.04", "interpreter": "python2.7"}) + "file": "code.py", "distribution": "Ubuntu", "release": "20.04", "interpreter": "python3", "version": "0"}) app_dir = self.write_app_directory("asset_depends", json.dumps(app_spec), "code.py") with self.assertSubprocessFailure(stderr_regexp="No asset bundle was found", exit_code=3): run("dx build --json {app_dir}".format(app_dir=app_dir)) @@ -9030,7 +9967,7 @@ def test_asset_depends_malform_details(self): app_spec = dict(self.base_app_spec, name="asset_depends", runSpec={"assetDepends": [{"name": record_name, "version": "0.0.1", "project": self.project}], - "file": "code.py", "distribution": "Ubuntu", "release": "14.04", "interpreter": "python2.7"}) + "file": "code.py", "distribution": "Ubuntu", "release": "20.04", "interpreter": "python3", "version": "0"}) app_dir = self.write_app_directory("asset_depends", json.dumps(app_spec), "code.py") with self.assertSubprocessFailure(stderr_regexp="The required field 'archiveFileId'", exit_code=3): run("dx build --json {app_dir}".format(app_dir=app_dir)) @@ -9052,8 +9989,8 @@ def test_asset_depends_clone(self): with temporary_project('test_select_project', select=True): app_spec = dict(self.base_app_spec, name="asset_depends", runSpec={"assetDepends": [{"id": record.get_id()}], - "file": "code.py", "distribution": "Ubuntu", "release": "14.04", - "interpreter": "python2.7"}) + "file": "code.py", "distribution": "Ubuntu", "release": "20.04", + "interpreter": "python3", "version": "0"}) app_dir = self.write_app_directory("asset_depends", json.dumps(app_spec), "code.py") run("dx build --json {app_dir}".format(app_dir=app_dir)) temp_record_id = run("dx ls {asset} --brief".format(asset=record_name)).strip() @@ -9079,7 +10016,7 @@ def test_dry_run_does_not_clone_asset_depends(self): app_spec = dict(self.base_app_spec, name=app_name, runSpec={"file": "code.py", - "interpreter": "python2.7", "distribution": "Ubuntu", "release": "14.04", + "interpreter": "python3", "distribution": "Ubuntu", "release": "20.04", "version": "0", "assetDepends": [{"id": record.get_id()}]}) app_dir = self.write_app_directory(app_name, json.dumps(app_spec), "code.py") run("dx build --dry-run {app_dir}".format(app_dir=app_dir)) @@ -9102,7 +10039,7 @@ def test_asset_depends_clone_app(self): app_spec = dict(self.base_app_spec, name="asset_depends", runSpec={"assetDepends": [{"name": record_name, "version": "0.0.1", "project": self.project}], - "file": "code.py", "distribution": "Ubuntu", "release": "14.04", "interpreter": "python2.7"}) + "file": "code.py", "distribution": "Ubuntu", "release": "20.04", "interpreter": "python3", "version": "0"}) app_dir = self.write_app_directory("asset_depends", json.dumps(app_spec), "code.py") asset_applet = run_and_parse_json("dx build --json {app_dir}".format(app_dir=app_dir))["id"] @@ -9115,7 +10052,7 @@ def test_asset_depends_clone_app(self): @unittest.skipUnless(testutil.TEST_ISOLATED_ENV, 'skipping test that would create app') - @pytest.mark.TRACEABILITY_MATRIX + @pytest.mark.TRACEABILITY_ISOLATED_ENV @testutil.update_traceability_matrix(["DNA_CLI_APP_PUBLISH"]) def test_dx_publish_app(self): app_name = "dx_publish_app" @@ -9237,7 +10174,7 @@ def test_get_workflow(self): with self.assertSubprocessFailure(stderr_regexp='already exists', exit_code=3): run("dx get -o destdir_withfile get_workflow") - @pytest.mark.TRACEABILITY_MATRIX + @pytest.mark.TRACEABILITY_ISOLATED_ENV @testutil.update_traceability_matrix(["DNA_CLI_DATA_OBJ_DOWNLOAD_EXECUTABLE"]) @unittest.skipUnless(testutil.TEST_ISOLATED_ENV, 'skipping test that would create global workflows') def test_get_global_workflow(self): @@ -9313,9 +10250,10 @@ def test_get_applet(self): "dxapi": "1.0.0", "runSpec": { "file": "code.py", - "interpreter": "python2.7", + "interpreter": "python3", "distribution": "Ubuntu", - "release": "14.04"}, + "release": "20.04", + "version": "0"}, "inputSpec": [{ "name": "in1", "help": "A help for in1 input param", @@ -9341,13 +10279,14 @@ def test_get_applet(self): "tags": ["bar"], "properties": {"sample_id": "123456"}, "details": {"key1": "value1"}, - "ignoreReuse": False + "ignoreReuse": False, + "treeTurnaroundTimeThreshold": 2 } # description and developerNotes should be un-inlined back to files output_app_spec = dict((k, v) for (k, v) in list(app_spec.items()) if k not in ('description', 'developerNotes')) - output_app_spec["runSpec"] = {"file": "src/code.py", "interpreter": "python2.7", - "distribution": "Ubuntu", "release": "14.04", "version": "0"} + output_app_spec["runSpec"] = {"file": "src/code.py", "interpreter": "python3", "headJobOnDemand": False, "inheritParentRestartOnPolicy": False, + "distribution": "Ubuntu", "release": "20.04", "version": "0"} output_app_spec["regionalOptions"] = {"aws:us-east-1": {"systemRequirements": {}}} @@ -9394,6 +10333,7 @@ def test_get_applet(self): self.assertNotIn("description", output_json) self.assertNotIn("developerNotes", output_json) + self.assertEqual(output_json["treeTurnaroundTimeThreshold"], 2) with open(os.path.join("get_applet", "Readme.md")) as fh: self.assertEqual("Description\n", fh.read()) with open(os.path.join("get_applet", "Readme.developer.md")) as fh: @@ -9463,7 +10403,7 @@ def test_get_applet_omit_resources(self): app_spec = { "name": "get_applet", "dxapi": "1.0.0", - "runSpec": {"file": "code.py", "interpreter": "python2.7", "distribution": "Ubuntu", "release": "14.04"}, + "runSpec": {"file": "code.py", "interpreter": "python3", "distribution": "Ubuntu", "release": "20.04", "version": "0"}, "inputSpec": [{"name": "in1", "class": "file"}], "outputSpec": [{"name": "out1", "class": "file"}], "description": "Description\n", @@ -9476,8 +10416,8 @@ def test_get_applet_omit_resources(self): # description and developerNotes should be un-inlined back to files output_app_spec = dict((k, v) for (k, v) in list(app_spec.items()) if k not in ('description', 'developerNotes')) - output_app_spec["runSpec"] = {"file": "src/code.py", "interpreter": "python2.7", - "distribution": "Ubuntu", "release": "14.04", "version": "0"} + output_app_spec["runSpec"] = {"file": "src/code.py", "interpreter": "python3", + "distribution": "Ubuntu", "release": "20.04", "version": "0"} app_dir = self.write_app_directory("get_åpplet", json.dumps(app_spec), "code.py", code_content="import os\n") @@ -9491,14 +10431,17 @@ def test_get_applet_omit_resources(self): with open(os.path.join("get_applet", "dxapp.json")) as f2: output_json = json.load(f2) - self.assertIn("bundledDepends", output_json["runSpec"]) + current_region = dxpy.describe(self.project).get("region") + regional_options = output_json["regionalOptions"][current_region] + self.assertIn("bundledDepends", regional_options) seenResources = False - for bd in output_json["runSpec"]["bundledDepends"]: + for bd in regional_options["bundledDepends"]: if bd["name"] == "resources.tar.gz": seenResources = True break self.assertTrue(seenResources) + @unittest.skip("skipping per DEVEX-2161") def test_get_applet_field_cleanup(self): # TODO: not sure why self.assertEqual doesn't consider # assertEqual to pass unless the strings here are unicode strings @@ -9508,8 +10451,8 @@ def test_get_applet_field_cleanup(self): # dxapp.json so as not to pollute it. app_spec = dict(self.base_applet_spec, name="get_applet_field_cleanup") output_app_spec = app_spec.copy() - output_app_spec["runSpec"] = {"file": "src/code.py", "interpreter": "python2.7", - "distribution": "Ubuntu", "release": "14.04", "version": "0"} + output_app_spec["runSpec"] = {"file": "src/code.py", "interpreter": "python3", "headJobOnDemand": False, + "distribution": "Ubuntu", "release": "20.04", "version": "0"} output_app_spec["regionalOptions"] = {u'aws:us-east-1': {u'systemRequirements': {}}} app_dir = self.write_app_directory("get_åpplet_field_cleanup", json.dumps(app_spec), "code.py", @@ -9528,13 +10471,14 @@ def test_get_applet_field_cleanup(self): self.assertFalse(os.path.exists(os.path.join("get_applet", "Readme.md"))) self.assertFalse(os.path.exists(os.path.join("get_applet", "Readme.developer.md"))) + @unittest.skipUnless(sys.platform.startswith("win"), "Windows only test") def test_get_applet_on_windows(self): # This test is to verify that "dx get applet" works correctly on windows, # making sure the resource directory is downloaded. app_spec = dict(self.base_applet_spec, name="get_applet_windows") output_app_spec = app_spec.copy() - output_app_spec["runSpec"] = {"file": "src/code.py", "interpreter": "python2.7", - "distribution": "Ubuntu", "release": "14.04", "version": "0"} + output_app_spec["runSpec"] = {"file": "src/code.py", "interpreter": "python3", "headJobOnDemand": False, + "distribution": "Ubuntu", "release": "20.04", "version": "0"} output_app_spec["regionalOptions"] = {u'aws:us-east-1': {u'systemRequirements': {}}} app_dir = self.write_app_directory("get_åpplet_windows", json.dumps(app_spec), "code.py", @@ -9568,8 +10512,8 @@ def make_app(self, name, open_source=True, published=True, authorized_users=[], "name": name, "title": "Sir", "dxapi": "1.0.0", - "runSpec": {"file": "code.py", "interpreter": "python2.7", - "distribution": "Ubuntu", "release": "14.04", "version": "0"}, + "runSpec": {"file": "code.py", "interpreter": "python3", + "distribution": "Ubuntu", "release": "20.04", "version": "0"}, "inputSpec": [{"name": "in1", "class": "file"}], "outputSpec": [{"name": "out1", "class": "file"}], "description": "Description\n", @@ -9581,10 +10525,10 @@ def make_app(self, name, open_source=True, published=True, authorized_users=[], # description and developerNotes should be un-inlined back to files output_app_spec = dict((k, v) - for (k, v) in app_spec.iteritems() + for (k, v) in app_spec.items() if k not in ('description', 'developerNotes')) - output_app_spec["runSpec"] = {"file": "src/code.py", "interpreter": "python2.7", - "distribution": "Ubuntu", "release": "14.04", "version": "0"} + output_app_spec["runSpec"] = {"file": "src/code.py", "interpreter": "python3", + "distribution": "Ubuntu", "release": "20.04", "version": "0"} app_dir = self.write_app_directory(name, json.dumps(app_spec), @@ -9630,7 +10574,7 @@ def assert_app_get_initialized(self, name, app_spec): black_list.append('authorizedUsers') filtered_app_spec = dict((k, v) - for (k, v) in app_spec.iteritems() + for (k, v) in app_spec.items() if k not in black_list) self.assertNotIn("description", output_json) @@ -9639,7 +10583,8 @@ def assert_app_get_initialized(self, name, app_spec): self.assertNotIn("systemRequirements", output_json["runSpec"]) self.assertNotIn("systemRequirementsByRegion", output_json["runSpec"]) - self.assertDictSubsetOf(filtered_app_spec, output_json) + # assetDepends is now dumped as bundledDepends, assertion no longer valid + # self.assertDictSubsetOf(filtered_app_spec, output_json) self.assertFileContentsEqualsString([name, "src", "code.py"], @@ -9776,8 +10721,8 @@ def test_get_app_omit_resources(self): "name": app_name, "title": "Sir", "dxapi": "1.0.0", - "runSpec": {"file": "code.py", "interpreter": "python2.7", - "distribution": "Ubuntu", "release": "14.04"}, + "runSpec": {"file": "code.py", "interpreter": "python3", + "distribution": "Ubuntu", "release": "20.04", "version": "0"}, "inputSpec": [{"name": "in1", "class": "file"}], "outputSpec": [{"name": "out1", "class": "file"}], "description": "Description\n", @@ -9789,7 +10734,7 @@ def test_get_app_omit_resources(self): output_app_spec = dict((k, v) for (k, v) in app_spec.iteritems() if k not in ('description', 'developerNotes')) - output_app_spec["runSpec"] = {"file": "src/code.py", "interpreter": "python2.7", + output_app_spec["runSpec"] = {"file": "src/code.py", "interpreter": "python3", "version": "0", "distribution": "Ubuntu", "release": "14.04"} app_dir = self.write_app_directory(app_name, @@ -9816,15 +10761,17 @@ def test_get_app_omit_resources(self): self.assertFalse(os.path.exists(os.path.join(app_name, "resources"))) output_json = json.load(open(os.path.join(app_name, "dxapp.json"))) - self.assertTrue("bundledDepends" in output_json["runSpec"]) + current_region = dxpy.describe(self.project).get("region") + regional_options = output_json["regionalOptions"][current_region] + self.assertIn("bundledDepends", regional_options) seenResources = False - for bd in output_json["runSpec"]["bundledDepends"]: + for bd in regional_options["bundledDepends"]: if bd["name"] == "resources.tar.gz": seenResources = True break self.assertTrue(seenResources) - @pytest.mark.TRACEABILITY_MATRIX + @pytest.mark.TRACEABILITY_ISOLATED_ENV @testutil.update_traceability_matrix(["DNA_CLI_APP_LIST_AVAILABLE_APPS", "DNA_CLI_APP_INSTALL_APP", "DNA_CLI_APP_UNINSTALL_APP", @@ -9840,8 +10787,8 @@ def test_uninstall_app(self): "name": name, "title": name, "dxapi": "1.0.0", - "runSpec": {"file": "code.py", "interpreter": "python2.7", - "distribution": "Ubuntu", "release": "14.04"}, + "runSpec": {"file": "code.py", "interpreter": "python3", + "distribution": "Ubuntu", "release": "20.04", "version": "0"}, "inputSpec": [], "outputSpec": [], "description": "Description\n", @@ -9905,8 +10852,110 @@ def test_get_preserves_system_requirements(self): with open(path_to_dxapp_json, "r") as fh: app_spec = json.load(fh) self.assertEqual(app_spec["regionalOptions"], regional_options) - + @staticmethod + def create_asset(tarball_name, record_name, proj): + asset_archive = dxpy.upload_string("foo", name=tarball_name, project=proj.get_id(),hidden=True, wait_on_close=True,) + asset = dxpy.new_dxrecord( + project=proj.get_id(), + details={"archiveFileId": {"$dnanexus_link": asset_archive.get_id()}}, + properties={"version": "0.0.1", }, + close=True, + types=["AssetBundle"] + ) + asset_archive.set_properties({"AssetBundle": asset.get_id()}) + return asset.get_id() + + @staticmethod + def gen_file_tar(fname, tarballname, proj_id): + with open(fname, 'w') as f: + f.write("foo") + + with tarfile.open(tarballname, 'w:gz') as f: + f.add(fname) + + dxfile = dxpy.upload_local_file(tarballname, name=tarballname, project=proj_id, + media_type="application/gzip", wait_on_close=True) + # remove local file + os.remove(tarballname) + os.remove(fname) + return dxfile + + @unittest.skipUnless(testutil.TEST_ISOLATED_ENV and testutil.TEST_AZURE, + 'skipping test that would create apps') + def test_get_permitted_regions(self): + + app_name = "app_{t}_multi_region_app_from_permitted_region".format(t=int(time.time())) + with temporary_project(region="aws:us-east-1") as aws_proj: + with temporary_project(region="azure:westus") as azure_proj: + aws_bundled_dep = self.gen_file_tar("test_file", "bundle.tar.gz", aws_proj.get_id()) + azure_bundled_dep = self.gen_file_tar("test_file", "bundle.tar.gz", azure_proj.get_id()) + + aws_asset = self.create_asset("asset.tar.gz","asset_record", aws_proj) + azure_asset = self.create_asset("asset.tar.gz", "asset_record", azure_proj) + + aws_sys_reqs = dict(main=dict(instanceType="mem2_hdd2_x1")) + azure_sys_reqs = dict(main=dict(instanceType="azure:mem2_ssd1_x1")) + + regional_options = { + "aws:us-east-1": dict( + systemRequirements=aws_sys_reqs, + bundledDepends=[{"name": "bundle.tar.gz", + "id": {"$dnanexus_link": aws_bundled_dep.get_id()}}], + assetDepends=[{"id": aws_asset}], + ), + "azure:westus": dict( + systemRequirements=azure_sys_reqs, + bundledDepends=[{"name": "bundle.tar.gz", + "id": {"$dnanexus_link": azure_bundled_dep.get_id()}}], + assetDepends=[{"id": azure_asset}], + ) + } + + app_id, app_spec = self.make_app(app_name, regional_options=regional_options, authorized_users=["PUBLIC"]) + app_desc = dxpy.api.app_get(app_id) + + # use current selected project as the source + # assets are not downloaded but kept in regionalOptions as bundleDepends + with chdir(tempfile.mkdtemp()), temporary_project(region="aws:us-east-1", select=True) as temp_project: + (stdout, stderr) = run("dx get {app_id}".format(app_id=app_id), also_return_stderr=True) + self.assertIn("Trying to download resources from the current region aws:us-east-1", stderr) + self.assertIn("Unpacking resource bundle.tar.gz", stderr) + self.assertIn("Unpacking resource resources.tar.gz", stderr) + self.assert_app_get_initialized(app_name, app_spec) + + path_to_dxapp_json = "./{app_name}/dxapp.json".format(app_name=app_name) + with open(path_to_dxapp_json, "r") as fh: + out_spec = json.load(fh) + + self.assertIn("regionalOptions", out_spec) + out_regional_options = out_spec["regionalOptions"] + + self.assertEqual(out_regional_options["aws:us-east-1"]["systemRequirements"], aws_sys_reqs) + self.assertEqual(out_regional_options["azure:westus"]["systemRequirements"], azure_sys_reqs) + + def get_asset_spec(asset_id): + tarball_id = dxpy.DXRecord(asset_id).describe( + fields={'details'})["details"]["archiveFileId"]["$dnanexus_link"] + tarball_name = dxpy.DXFile(tarball_id).describe()["name"] + return {"name": tarball_name, "id": {"$dnanexus_link": tarball_id}} + + self.assertEqual(out_regional_options["aws:us-east-1"]["bundledDepends"], [get_asset_spec(aws_asset)]) + self.assertEqual(out_regional_options["azure:westus"]["bundledDepends"], [get_asset_spec(azure_asset)]) + + # omit resources + # use current selected project as the source + with chdir(tempfile.mkdtemp()), temporary_project(region="aws:us-east-1", select=True) as temp_project: + (stdout, stderr) = run("dx get {app_id} --omit-resources".format(app_id=app_id), also_return_stderr=True) + self.assertFalse(os.path.exists(os.path.join(app_name, "resources"))) + + path_to_dxapp_json = "./{app_name}/dxapp.json".format(app_name=app_name) + with open(path_to_dxapp_json, "r") as fh: + out_spec = json.load(fh) + out_regional_options = out_spec["regionalOptions"] + + self.assertEqual(out_regional_options["aws:us-east-1"]["bundledDepends"], app_desc["runSpec"]["bundledDependsByRegion"]["aws:us-east-1"]) + self.assertEqual(out_regional_options["azure:westus"]["bundledDepends"], app_desc["runSpec"]["bundledDependsByRegion"]["azure:westus"]) @unittest.skipUnless(testutil.TEST_TCSH, 'skipping tests that require tcsh to be installed') class TestTcshEnvironment(unittest.TestCase): def test_tcsh_dash_c(self): @@ -10119,7 +11168,7 @@ def test_long_output(self): rec = dxpy.new_dxrecord(project=self.project, name="foo", close=True) o = run("dx ls -l") # state modified name id - self.assertRegex(o, r"closed\s+\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\s+foo \(" + rec.get_id() + "\)") + self.assertRegex(o, r"closed\s+\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\s+foo \(" + rec.get_id() + r"\)") class TestDXTree(DXTestCase): @@ -10135,7 +11184,7 @@ def test_tree(self): rec = dxpy.new_dxrecord(project=self.project, name="foo", close=True) o = run("dx tree -l") self.assertRegex(o.strip(), - r".\n└── closed\s+\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\s+foo \(" + rec.get_id() + "\)") + r".\n└── closed\s+\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\s+foo \(" + rec.get_id() + r"\)") class TestDXGenerateBatchInputs(DXTestCase): @@ -10212,22 +11261,39 @@ def test_dx_run_specific_project(self): "runSpec": {"interpreter": "bash", "distribution": "Ubuntu", "release": "16.04", + "systemRequirements": {"*": {"instanceType": "mem2_ssd1_v2_x2"}}, "code": "dx-jobutil-add-output number 32"} }) run("dx run myapplet -inumber=5 --project %s" % temp_project.name) + def test_applet_prefix_resolve_does_not_send_app_describe_request(self): + id = 'applet-xxxxasdfasdfasdfasdfas' + with self.assertSubprocessFailure( + # there should be no app- or globalworkflow- in the stderr + stderr_regexp=r"\A((?!app\-|globalworkflow\-)[\s\S])*\Z", + exit_code=3): + run("_DX_DEBUG=2 dx run {}".format(id)) + + def test_workflow_prefix_resolve_does_not_send_app_describe_request(self): + id = 'workflow-xxxxasdfasfasdf' + with self.assertSubprocessFailure( + # there should be no app- or globalworkflow- in the stderr + stderr_regexp=r"\A((?!app\-|globalworkflow\-)[\s\S])*\Z", + exit_code=3): + run("_DX_DEBUG=2 dx run {}".format(id)) + class TestDXUpdateApp(DXTestCaseBuildApps): @unittest.skipUnless(testutil.TEST_ISOLATED_ENV, 'skipping test that creates apps') - @pytest.mark.TRACEABILITY_MATRIX + @pytest.mark.TRACEABILITY_ISOLATED_ENV @testutil.update_traceability_matrix(["DNA_API_APP_UPDATE"]) def test_update_app(self): # Build and publish app with initial version app_spec = { "name": "test_app_update", "dxapi": "1.0.0", - "runSpec": {"file": "code.py", "interpreter": "python2.7", - "distribution": "Ubuntu", "release": "14.04"}, + "runSpec": {"file": "code.py", "interpreter": "python3", + "distribution": "Ubuntu", "release": "20.04", "version": "0"}, "inputSpec": [], "outputSpec": [], "version": "0.0.1"} @@ -10242,8 +11308,8 @@ def test_update_app(self): app_spec_2 = { "name": "test_app_update", "dxapi": "1.0.0", - "runSpec": {"file": "code.py", "interpreter": "python2.7", - "distribution": "Ubuntu", "release": "14.04"}, + "runSpec": {"file": "code.py", "interpreter": "python3", + "distribution": "Ubuntu", "release": "20.04", "version": "0"}, "inputSpec": [], "outputSpec": [], "version": "0.0.2"} @@ -10254,6 +11320,290 @@ def test_update_app(self): self.assertEqual(app_2['name'], app_spec_2['name']) self.assertEqual(app_2['version'], "0.0.2") +class TestDXArchive(DXTestCase): + @classmethod + def setUpClass(cls): + # setup two projects + cls.proj_archive_name = "dx_test_archive" + cls.proj_unarchive_name = "dx_test_unarchive" + cls.usr = dxpy.whoami() + cls.bill_to = dxpy.api.user_describe(cls.usr)['billTo'] + cls.is_admin = True if dxpy.api.org_describe(cls.bill_to).get('level') == 'ADMIN' or cls.bill_to == cls.usr else False + cls.rootdir = '/' + cls.proj_archive_id = dxpy.api.project_new({'name': cls.proj_archive_name, 'billTo': cls.bill_to})['id'] + cls.proj_unarchive_id = dxpy.api.project_new({'name': cls.proj_unarchive_name, 'billTo': cls.bill_to})['id'] + cls.counter = 1 + + # create_folder_in_project(cls.proj_archive_id, cls.rootdir) + # create_folder_in_project(cls.proj_unarchive_id, cls.rootdir) + + @classmethod + def tearDownClass(cls): + dxpy.api.project_remove_folder(cls.proj_archive_id, {"folder": cls.rootdir, "recurse":True}) + dxpy.api.project_remove_folder(cls.proj_unarchive_id, {"folder": cls.rootdir,"recurse":True}) + + dxpy.api.project_destroy(cls.proj_archive_id,{"terminateJobs": True}) + dxpy.api.project_destroy(cls.proj_unarchive_id,{"terminateJobs": True}) + + @classmethod + def gen_uniq_fname(cls): + cls.counter += 1 + return "file_{}".format(cls.counter) + + def test_archive_files(self): + # archive a list of files + fname1 = self.gen_uniq_fname() + fname2 = self.gen_uniq_fname() + fid1 = create_file_in_project(fname1, self.proj_archive_id,folder=self.rootdir) + fid2 = create_file_in_project(fname2, self.proj_archive_id,folder=self.rootdir) + + run("dx archive -y {}:{} {}:{}{}".format( + self.proj_archive_id,fid1, + self.proj_archive_id,self.rootdir,fname2)) + + time.sleep(10) + self.assertEqual(dxpy.describe(fid1)["archivalState"],"archived") + self.assertEqual(dxpy.describe(fid2)["archivalState"],"archived") + + # invalid project id or file id + # error raises from API + with self.assertSubprocessFailure(stderr_regexp="InvalidInput", exit_code=3): + run("dx archive -y {}:{}".format( + self.proj_archive_id, "file-B00000000000000000000000")) + with self.assertSubprocessFailure(stderr_regexp="ResourceNotFound", exit_code=3): + run("dx archive -y {}:{}".format( + "project-B00000000000000000000000",fid1)) + + def test_archive_files_allmatch(self): + # archive all matched names without prompt + fname_allmatch = "file_allmatch" + fid1 = create_file_in_project(fname_allmatch, self.proj_archive_id,folder=self.rootdir) + fid2 = create_file_in_project(fname_allmatch, self.proj_archive_id,folder=self.rootdir) + + run("dx archive -y -a {}:{}{}".format(self.proj_archive_id,self.rootdir,fname_allmatch)) + time.sleep(10) + self.assertEqual(dxpy.describe(fid1)["archivalState"],"archived") + self.assertEqual(dxpy.describe(fid2)["archivalState"],"archived") + + # archive all matched names with picking + fname_allmatch2 = "file_allmatch2" + fid3 = create_file_in_project(fname_allmatch2, self.proj_archive_id,folder=self.rootdir) + fid4 = create_file_in_project(fname_allmatch2, self.proj_archive_id,folder=self.rootdir) + + dx_archive_confirm = pexpect.spawn("dx archive -y {}:{}{}".format(self.proj_archive_id,self.rootdir,fname_allmatch2), + logfile=sys.stderr, + **spawn_extra_args) + dx_archive_confirm.expect('for all: ') + dx_archive_confirm.sendline("*") + + dx_archive_confirm.expect(pexpect.EOF, timeout=30) + dx_archive_confirm.close() + + time.sleep(10) + self.assertEqual(dxpy.describe(fid3)["archivalState"],"archived") + self.assertEqual(dxpy.describe(fid4)["archivalState"],"archived") + + def test_archive_folder(self): + subdir = 'subfolder/' + dxpy.api.project_new_folder(self.proj_archive_id, {"folder": self.rootdir+subdir, "parents":True}) + + fname_root = self.gen_uniq_fname() + fname_subdir1 = self.gen_uniq_fname() + fid_root = create_file_in_project(fname_root, self.proj_archive_id, folder=self.rootdir) + fid_subdir1 = create_file_in_project(fname_subdir1, self.proj_archive_id, folder=self.rootdir+subdir) + + # archive subfolder + run("dx archive -y {}:{}".format(self.proj_archive_id,self.rootdir+subdir)) + time.sleep(10) + self.assertEqual(dxpy.describe(fid_subdir1)["archivalState"],"archived") + + + fname_subdir2 = self.gen_uniq_fname() + fid_subdir2 = create_file_in_project(fname_subdir2, self.proj_archive_id, folder=self.rootdir+subdir) + + # archive files in root dir only + run("dx archive -y --no-recurse {}:{}".format(self.proj_archive_id,self.rootdir)) + time.sleep(10) + self.assertEqual(dxpy.describe(fid_root)["archivalState"],"archived") + self.assertEqual(dxpy.describe(fid_subdir2)["archivalState"],"live") + + # archive all files in root dir recursively + run("dx archive -y {}:{}".format(self.proj_archive_id,self.rootdir)) + time.sleep(20) + self.assertEqual(dxpy.describe(fid_root)["archivalState"],"archived") + self.assertEqual(dxpy.describe(fid_subdir1)["archivalState"],"archived") + self.assertEqual(dxpy.describe(fid_subdir2)["archivalState"],"archived") + + # invalid folder path + with self.assertSubprocessFailure(stderr_regexp="ResourceNotFound", exit_code=3): + run("dx archive -y {}:{}".format(self.proj_archive_id,self.rootdir+'invalid/')) + + # def test_archive_filename_with_forwardslash(self): + # subdir = 'x/' + # dxpy.api.project_new_folder(self.proj_archive_id, {"folder": self.rootdir+subdir, "parents":True}) + + # fname = r'x\\/' + # fid_root = create_file_in_project(fname, self.proj_archive_id, folder=self.rootdir) + # fid_subdir1 = create_file_in_project(fname, self.proj_archive_id, folder=self.rootdir+subdir) + + # # run("dx archive -y {}:{}".format(self.proj_archive_id,subdir)) + # # time.sleep(10) + # # self.assertEqual(dxpy.describe(fid_subdir1)["archivalState"],"archived") + + # run("dx archive -y {}:{}".format(self.proj_archive_id, subdir+fname)) + # time.sleep(10) + # self.assertEqual(dxpy.describe(fid_root)["archivalState"],"archived") + + # with self.assertSubprocessFailure(stderr_regexp="Expecting either a single folder or a list of files for each API request", exit_code=3): + # run("dx archive -y {}:{} {}:{}".format( + # self.proj_archive_id, fname, + # self.proj_archive_id, subdir)) + + def test_archive_equivalent_paths(self): + with temporary_project("other_project",select=True) as temp_project: + test_projectid = temp_project.get_id() + fname = self.gen_uniq_fname() + fid = create_file_in_project(fname, test_projectid,folder=self.rootdir) + run("dx select {}".format(test_projectid)) + project_prefix = ["", ":", test_projectid+":", "other_project"+":"] + affix = ["", "/"] + input = "" + for p in project_prefix: + for a in affix: + input += " {}{}{}".format(p,a,fname) + print(input) + dx_archive_confirm = pexpect.spawn("dx archive {}".format(input), + logfile=sys.stderr, + **spawn_extra_args) + dx_archive_confirm.expect('Will tag 1') + dx_archive_confirm.sendline("n") + dx_archive_confirm.expect(pexpect.EOF, timeout=30) + dx_archive_confirm.close() + + input = "" + fp = create_folder_in_project(test_projectid,'/foo/') + for p in project_prefix: + for a in affix: + input += " {}{}{}".format(p,a,'foo/') + print(input) + dx_archive_confirm = pexpect.spawn("dx archive {}".format(input), + logfile=sys.stderr, + **spawn_extra_args) + dx_archive_confirm.expect('{}:/foo'.format(test_projectid)) + dx_archive_confirm.sendline("n") + dx_archive_confirm.expect(pexpect.EOF, timeout=30) + dx_archive_confirm.close() + + def test_archive_invalid_paths(self): + # mixed file and folder path + fname1 = self.gen_uniq_fname() + fid1 = create_file_in_project(fname1, self.proj_archive_id,folder=self.rootdir) + + with self.assertSubprocessFailure(stderr_regexp="Expecting either a single folder or a list of files for each API request", exit_code=3): + run("dx archive -y {}:{} {}:{}".format( + self.proj_archive_id,fid1, + self.proj_archive_id,self.rootdir)) + + with self.assertSubprocessFailure(stderr_regexp="is invalid. Please check the inputs or check --help for example inputs.", exit_code=3): + run("dx archive -y {}:{}:{}".format( + self.proj_archive_id,self.rootdir,fid1)) + + # invalid project name + with self.assertSubprocessFailure(stderr_regexp="Cannot find project with name {}".format("invalid_project_name"), exit_code=3): + run("dx archive -y {}:{}".format("invalid_project_name",fid1)) + + # no project context + with without_project_context(): + with self.assertSubprocessFailure(stderr_regexp="Cannot find current project. Please check the environment.", + exit_code=3): + run("dx archive -y {}".format(fid1)) + + # invalid file name + with self.assertSubprocessFailure(stderr_regexp="Input '{}' is not found as a file in project '{}'".format("invalid_file_name",self.proj_archive_id), exit_code=3): + run("dx archive -y {}:{}".format(self.proj_archive_id,"invalid_file_name")) + + # files in different project + with temporary_project("other_project",select=False) as temp_project: + test_projectid = temp_project.get_id() + with select_project(test_projectid): + fid2 = create_file_in_project("temp_file", trg_proj_id=test_projectid,folder=self.rootdir) + with self.assertSubprocessFailure(stderr_regexp="All paths must refer to files/folder in a single project", exit_code=3): + run("dx archive -y {}:{} {}:{}".format( + self.proj_archive_id,fid1, + test_projectid,fid2)) + with self.assertSubprocessFailure(stderr_regexp="All paths must refer to files/folder in a single project", exit_code=3): + run("dx archive -y {}:{} :{}".format( + self.proj_archive_id,fid1, + fid2)) + with self.assertSubprocessFailure(stderr_regexp="All paths must refer to files/folder in a single project", exit_code=3): + run("dx archive -y {}:{} {}".format( + self.proj_archive_id,fid1, + fid2)) + + repeated_name = '/foo' + fid = create_file_in_project(repeated_name, self.proj_archive_id) + fp = create_folder_in_project(self.proj_archive_id,repeated_name) + # invalid file name + with self.assertSubprocessFailure(stderr_regexp="Expecting either a single folder or a list of files for each API request", exit_code=3): + run("dx archive -y {}:{} {}:{}/".format(self.proj_archive_id,repeated_name, + self.proj_archive_id,repeated_name)) + + def test_archive_allcopies(self): + fname = self.gen_uniq_fname() + fname_allcopies = self.gen_uniq_fname() + fid = create_file_in_project(fname, self.proj_archive_id) + fid_allcopy = create_file_in_project(fname_allcopies, self.proj_archive_id,folder=self.rootdir) + + with temporary_project(name="other_project",select=False) as temp_project: + test_projectid = temp_project.get_id() + # dxpy.api.project_update(test_projectid, {"billTo": self.bill_to}) + dxpy.DXFile(dxid=fid, project=self.proj_archive_id).clone(test_projectid, folder=self.rootdir) + + run("dx archive -y {}:{}".format(self.proj_archive_id,fid)) + time.sleep(5) + self.assertEqual(dxpy.describe(fid)["archivalState"],"archival") + with select_project(test_projectid): + self.assertEqual(dxpy.describe(fid)["archivalState"],"live") + + dxpy.DXFile(dxid=fid_allcopy, project=self.proj_archive_id).clone(test_projectid, folder=self.rootdir).get_id() + + if self.is_admin: + run("dx archive -y --all-copies {}:{}".format(self.proj_archive_id,fid_allcopy)) + time.sleep(20) + self.assertEqual(dxpy.describe(fid_allcopy)["archivalState"],"archived") + with select_project(test_projectid): + self.assertEqual(dxpy.describe(fid_allcopy)["archivalState"],"archived") + else: + with self.assertSubprocessFailure(stderr_regexp="Must be an admin of {} to archive all copies".format(self.bill_to)): + run("dx archive -y --all-copies {}:{}".format(self.proj_archive_id, fid_allcopy)) + + def test_unarchive_dryrun(self): + fname1 = self.gen_uniq_fname() + fname2 = self.gen_uniq_fname() + fid1 = create_file_in_project(fname1, self.proj_unarchive_id, folder=self.rootdir) + fid2 = create_file_in_project(fname2, self.proj_unarchive_id, folder=self.rootdir) + _ = dxpy.api.project_archive(self.proj_unarchive_id, {"folder": self.rootdir}) + time.sleep(15) + + dx_archive_confirm = pexpect.spawn("dx unarchive {}:{}".format(self.proj_unarchive_id,fid1), + logfile=sys.stderr, + **spawn_extra_args) + dx_archive_confirm.expect('Will tag') + dx_archive_confirm.sendline("n") + dx_archive_confirm.expect(pexpect.EOF, timeout=30) + dx_archive_confirm.close() + + self.assertEqual(dxpy.describe(fid1)["archivalState"],"archived") + + output = run("dx unarchive -y {}:{}".format(self.proj_unarchive_id,fid1)) + time.sleep(20) + self.assertIn("Tagged 1 file(s) for unarchival", output) + self.assertEqual(dxpy.describe(fid1)["archivalState"],"unarchiving") + + output = run("dx unarchive -y {}:{}".format(self.proj_unarchive_id,self.rootdir)) + time.sleep(20) + self.assertIn("Tagged 1 file(s) for unarchival", output) + self.assertEqual(dxpy.describe(fid2)["archivalState"],"unarchiving") if __name__ == '__main__': if 'DXTEST_FULL' not in os.environ: diff --git a/src/python/test/test_dxpy.py b/src/python/test/test_dxpy.py index 49cd5db5fd..d5b713afec 100755 --- a/src/python/test/test_dxpy.py +++ b/src/python/test/test_dxpy.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # -*- coding: utf-8 -*- # # Copyright (C) 2013-2016 DNAnexus, Inc. @@ -26,10 +26,9 @@ import platform import pytest import re +import certifi -import requests -from requests.packages.urllib3.exceptions import SSLError -import OpenSSL +from urllib3.exceptions import SSLError, NewConnectionError import dxpy import dxpy_testutil as testutil @@ -38,6 +37,7 @@ from dxpy.utils.resolver import resolve_path, resolve_existing_path, ResolutionError, is_project_explicit import dxpy.app_builder as app_builder import dxpy.executable_builder as executable_builder +import dxpy.workflow_builder as workflow_builder from dxpy.compat import USING_PYTHON2 @@ -137,7 +137,9 @@ def test_new(self): self.assertEqual(desc["downloadRestricted"], False) self.assertEqual(desc["containsPHI"], False) self.assertEqual(desc["databaseUIViewOnly"], False) + self.assertEqual(desc["externalUploadRestricted"], False) self.assertEqual(desc["tags"], []) + self.assertEqual(desc["databaseResultsRestricted"], None) prop = dxpy.api.project_describe(dxproject.get_id(), {'fields': {'properties': True}}) self.assertEqual(prop['properties'], {}) @@ -189,7 +191,9 @@ def test_update_describe(self): protected=True, restricted=True, download_restricted=True, + external_upload_restricted=False, allowed_executables=["applet-abcdefghijklmnopqrstuzwx"], + database_results_restricted=10, description="new description") desc = dxproject.describe() self.assertEqual(desc["id"], self.proj_id) @@ -198,15 +202,18 @@ def test_update_describe(self): self.assertEqual(desc["protected"], True) self.assertEqual(desc["restricted"], True) self.assertEqual(desc["downloadRestricted"], True) + self.assertEqual(desc["externalUploadRestricted"], False) self.assertEqual(desc["description"], "new description") self.assertEqual(desc["allowedExecutables"][0], "applet-abcdefghijklmnopqrstuzwx") + self.assertEqual(desc["databaseResultsRestricted"], 10) self.assertTrue("created" in desc) - dxproject.update(restricted=False, download_restricted=False, unset_allowed_executables=True) + dxproject.update(restricted=False, download_restricted=False, unset_allowed_executables=True, unset_database_results_restricted=True) desc = dxproject.describe() self.assertEqual(desc["restricted"], False) self.assertEqual(desc["downloadRestricted"], False) - self.assertTrue("allowedExecutables" not in desc) + self.assertEqual(desc["allowedExecutables"], None) + self.assertEqual(desc["databaseResultsRestricted"], None) def test_new_list_remove_folders(self): dxproject = dxpy.DXProject() @@ -349,11 +356,11 @@ def test_job_detection(self): else: env = dict(os.environ, DX_JOB_ID=b'job-00000000000000000000') buffer_size = subprocess.check_output( - 'python -c "import dxpy; print(dxpy.bindings.dxfile.DEFAULT_BUFFER_SIZE)"', shell=True, env=env) + 'python3 -c "import dxpy; print(dxpy.bindings.dxfile.DEFAULT_BUFFER_SIZE)"', shell=True, env=env) self.assertEqual(int(buffer_size), 96 * 1024 * 1024) del env['DX_JOB_ID'] buffer_size = subprocess.check_output( - 'python -c "import dxpy; print(dxpy.bindings.dxfile.DEFAULT_BUFFER_SIZE)"', shell=True, env=env) + 'python3 -c "import dxpy; print(dxpy.bindings.dxfile.DEFAULT_BUFFER_SIZE)"', shell=True, env=env) self.assertEqual(int(buffer_size), 16 * 1024 * 1024) def test_generate_read_requests(self): @@ -918,6 +925,9 @@ def test_download_folder(self): self.assertTrue(os.path.isfile(filename)) self.assertEqual("{}-th\n file\n content\n".format(i + 3), self.read_entire_file(filename)) + # DEVEX-2023 Do not create incorrect empty folders that share the same prefix for recursive download + # Check that /a/lpha does not exist + dxproject.new_folder("/alpha", parents=True) # Checking download to existing structure dxpy.download_folder(self.proj_id, a_dest_dir, folder="/a", overwrite=True) path = [] @@ -926,6 +936,8 @@ def test_download_folder(self): filename = os.path.join(os.path.join(*path), "file_{}.txt".format(i + 2)) self.assertTrue(os.path.isfile(filename)) self.assertEqual("{}-th\n file\n content\n".format(i + 2), self.read_entire_file(filename)) + self.assertFalse(os.path.isdir(os.path.join(a_dest_dir, 'pha'))) + # Checking download to existing structure fails w/o overwrite flag with self.assertRaises(DXFileError): @@ -1308,16 +1320,22 @@ def test_run_dxapplet_and_job_metadata(self): def main(): pass ''', - "interpreter": "python2.7", - "distribution": "Ubuntu", "release": "14.04", - "execDepends": [{"name": "python-numpy"}]}) + "interpreter": "python3", + "distribution": "Ubuntu", "release": "20.04", + "version": "0", + "execDepends": [{"name": "python-numpy"}], + "systemRequirements": { + "*": { + "nvidiaDriver": "R535" + } + }}) dxrecord = dxpy.new_dxrecord() dxrecord.close() prog_input = {"chromosomes": {"$dnanexus_link": dxrecord.get_id()}, "rowFetchChunk": 100} dxjob = dxapplet.run(applet_input=prog_input, details={"$dnanexus_link": "hello world"}, tags=['foo', '$foo.bar'], properties={'$dnanexus_link.foo': 'barbaz'}, - priority="normal") + priority="normal", nvidia_driver={"*": {"nvidiaDriver": "R470"}}) jobdesc = dxjob.describe() self.assertEqual(jobdesc["class"], "job") self.assertEqual(jobdesc["function"], "main") @@ -1337,6 +1355,7 @@ def main(): self.assertEqual(len(jobdesc["properties"]), 1) self.assertEqual(jobdesc["properties"]["$dnanexus_link.foo"], "barbaz") self.assertEqual(jobdesc["priority"], "normal") + self.assertEqual(jobdesc["systemRequirements"]["*"]["nvidiaDriver"], "R470") # Test setting tags and properties on job dxjob.add_tags(["foo", "bar", "foo"]) @@ -1406,8 +1425,8 @@ def test_run_workflow_and_analysis_metadata(self): {"name": "othernumber", "class": "int"}], outputSpec=[{"name": "number", "class": "int"}], runSpec={"code": self.codeSpec, - "distribution": "Ubuntu", "release": "14.04", - "interpreter": "python2.7"}) + "distribution": "Ubuntu", "release": "20.04", + "version": "0", "interpreter": "python3"}) stage_id = dxpy.api.workflow_add_stage(dxworkflow.get_id(), {"editVersion": 0, "name": "stagename", @@ -1445,8 +1464,8 @@ def test_run_locked_workflow(self): dxapi="1.04", inputSpec=[{"name": "number", "class": "int"}], outputSpec=[{"name": "number", "class": "int"}], - runSpec={"code": self.codeSpec, "interpreter": "python2.7", - "distribution": "Ubuntu", "release": "14.04"}) + runSpec={"code": self.codeSpec, "interpreter": "python3", + "distribution": "Ubuntu", "release": "20.04", "version": "0"}) stage0 = {'id': 'stage_0', 'executable': dxapplet.get_id(), 'input': {'number': {'$dnanexus_link': {'workflowInputField': 'foo'}}}} @@ -1561,33 +1580,6 @@ def test_run_workflow_with_stage_folders(self): self.assertEqual(desc['stages'][0]['execution']['folder'], '/output/baz') self.assertEqual(desc['stages'][1]['execution']['folder'], '/output/quux') - @unittest.skipUnless(testutil.TEST_RUN_JOBS, 'skipping test that would run a job') - def test_run_workflow_with_rerun_stages(self): - dxworkflow = dxpy.new_dxworkflow() - dxapplet = dxpy.DXApplet() - dxapplet.new(name="test_applet", - dxapi="1.04", - inputSpec=[], - outputSpec=[], - runSpec={"code": '', "interpreter": "bash", "distribution": "Ubuntu", "release": "14.04"}) - stage_id = dxworkflow.add_stage(dxapplet, name="stagename", folder="foo") - - # make initial analysis - dxanalysis = dxworkflow.run({}) - job_ids = [dxanalysis.describe()['stages'][0]['execution']['id']] - dxanalysis.wait_on_done(timeout=500) - - # empty rerun_stages should reuse results - rerun_analysis = dxworkflow.run({}, rerun_stages=[]) - self.assertEqual(rerun_analysis.describe()['stages'][0]['execution']['id'], - job_ids[0]) - - # use various identifiers to rerun the job - for value in ['*', 0, stage_id, 'stagename']: - rerun_analysis = dxworkflow.run({}, rerun_stages=[value]) - job_ids.append(rerun_analysis.describe()['stages'][0]['execution']['id']) - self.assertNotIn(job_ids[-1], job_ids[:-1]) - @unittest.skipUnless(testutil.TEST_RUN_JOBS, 'skipping test that may run a job') def test_run_workflow_errors(self): dxworkflow = dxpy.DXWorkflow(dxpy.api.workflow_new({"project": self.proj_id})['id']) @@ -1597,8 +1589,8 @@ def test_run_workflow_errors(self): inputSpec=[{"name": "number", "class": "int"}], outputSpec=[{"name": "number", "class": "int"}], runSpec={"code": self.codeSpec, - "interpreter": "python2.7", - "distribution": "Ubuntu", "release": "14.04"}) + "interpreter": "python3", "version":"0", + "distribution": "Ubuntu", "release": "20.04"}) dxworkflow.add_stage(dxapplet, name='stagename') # Can't specify the same input more than once (with a @@ -1976,8 +1968,8 @@ def test_create_app(self): ], outputSpec=[{"name": "mappings", "class": "record"}], runSpec={"code": "def main(): pass", - "interpreter": "python2.7", - "distribution": "Ubuntu", "release": "14.04", + "interpreter": "python3", "version":"0", + "distribution": "Ubuntu", "release": "20.04", "execDepends": [{"name": "python-numpy"}]}) dxapp = dxpy.DXApp() my_userid = dxpy.whoami() @@ -2022,8 +2014,8 @@ def test_add_and_remove_tags(self): ], outputSpec=[{"name": "mappings", "class": "record"}], runSpec={"code": "def main(): pass", - "interpreter": "python2.7", - "distribution": "Ubuntu", "release": "14.04", + "interpreter": "python3", "version": "0", + "distribution": "Ubuntu", "release": "20.04", "execDepends": [{"name": "python-numpy"}]}) dxapp = dxpy.DXApp() my_userid = dxpy.whoami() @@ -2353,8 +2345,8 @@ def test_find_executions(self): ], outputSpec=[{"name": "mappings", "class": "record"}], runSpec={"code": "def main(): pass", - "interpreter": "python2.7", - "distribution": "Ubuntu", "release": "14.04", + "interpreter": "python3", "version": "0", + "distribution": "Ubuntu", "release": "20.04", "execDepends": [{"name": "python-numpy"}]}) dxrecord = dxpy.new_dxrecord() dxrecord.close() @@ -2435,6 +2427,15 @@ def test_find_executions(self): for method in methods: with self.assertRaises(DXError): method(**query) + + def test_find_one_fails_if_zero_ok_not_bool(self): + with self.assertRaises(DXError): + dxpy.find_one_project(1) + with self.assertRaises(DXError): + dxpy.find_one_data_object("foo") + with self.assertRaises(DXError): + dxpy.find_one_app([]) + class TestPrettyPrint(unittest.TestCase): def test_string_escaping(self): @@ -2445,6 +2446,17 @@ def test_string_escaping(self): self.assertEqual(pretty_print.escape_unicode_string("\n\\"), "\\n\\\\") self.assertEqual(pretty_print.escape_unicode_string("ïñtérnaçiònale"), "ïñtérnaçiònale") + def test_format_timedelta(self): + self.assertEqual(pretty_print.format_timedelta(0), "0 miliseconds") + self.assertEqual(pretty_print.format_timedelta(0, in_seconds=True), "0 seconds") + self.assertEqual(pretty_print.format_timedelta(1), "1 miliseconds") + self.assertEqual(pretty_print.format_timedelta(1, in_seconds=True), "1 seconds") + self.assertEqual(pretty_print.format_timedelta(1, in_seconds=True, auto_singulars=True), "1 second") + self.assertEqual(pretty_print.format_timedelta(129 * 60, in_seconds=True, largest_units="minutes"), "129 minutes") + self.assertEqual(pretty_print.format_timedelta(129 * 60, in_seconds=True, largest_units="days"), "2 hours, 9 minutes") + self.assertEqual(pretty_print.format_timedelta(365 * 24 * 60 * 60 + 8 * 60 * 60 + 1 * 60 + 3, in_seconds=True), "1 years, 8 hours, 1 minutes, 3 seconds") + self.assertEqual(pretty_print.format_timedelta(365 * 24 * 60 * 60 + 8 * 60 * 60 + 1 * 60 + 3, in_seconds=True, auto_singulars=True), "1 year, 8 hours, 1 minute, 3 seconds") + class TestWarn(unittest.TestCase): def test_warn(self): warn("testing, one two three...") @@ -2506,7 +2518,7 @@ def test_generic_exception_not_retryable(self): def test_bad_host(self): # Verify that the exception raised is one that dxpy would # consider to be retryable, but truncate the actual retry loop - with self.assertRaises(requests.packages.urllib3.exceptions.NewConnectionError) as exception_cm: + with self.assertRaises(NewConnectionError) as exception_cm: dxpy.DXHTTPRequest('http://doesnotresolve.dnanexus.com/', {}, prepend_srv=False, always_retry=False, max_retries=1) self.assertTrue(dxpy._is_retryable_exception(exception_cm.exception)) @@ -2514,7 +2526,7 @@ def test_bad_host(self): def test_connection_refused(self): # Verify that the exception raised is one that dxpy would # consider to be retryable, but truncate the actual retry loop - with self.assertRaises(requests.packages.urllib3.exceptions.NewConnectionError) as exception_cm: + with self.assertRaises(NewConnectionError) as exception_cm: # Connecting to a port on which there is no server running dxpy.DXHTTPRequest('http://localhost:20406', {}, prepend_srv=False, always_retry=False, max_retries=1) self.assertTrue(dxpy._is_retryable_exception(exception_cm.exception)) @@ -2524,10 +2536,13 @@ def test_case_insensitive_response_headers(self): res = dxpy.DXHTTPRequest("/system/whoami", {}, want_full_response=True) self.assertTrue("CONTENT-type" in res.headers) + @unittest.skip("skipping per DEVEX-2161") + # NOTE: Re-enabling this test may require adding a recent version of + # pyopenssl to requirements_test.txt - see DEVEX-2263 for details. def test_ssl_options(self): dxpy.DXHTTPRequest("/system/whoami", {}, verify=False) - dxpy.DXHTTPRequest("/system/whoami", {}, verify=requests.certs.where()) - dxpy.DXHTTPRequest("/system/whoami", {}, verify=requests.certs.where(), cert_file=None, key_file=None) + dxpy.DXHTTPRequest("/system/whoami", {}, verify=certifi.where()) + dxpy.DXHTTPRequest("/system/whoami", {}, verify=certifi.where(), cert_file=None, key_file=None) with self.assertRaises(TypeError): dxpy.DXHTTPRequest("/system/whoami", {}, cert="nonexistent") if dxpy.APISERVER_PROTOCOL == "https": @@ -2976,7 +2991,7 @@ def assertItemsEqual(self, a, b): self.assertEqual(sorted(a), sorted(b)) code = '''@dxpy.entry_point('main')\ndef main():\n pass''' - run_spec = {"code": code, "interpreter": "python2.7", "distribution": "Ubuntu", "release": "14.04"} + run_spec = {"code": code, "interpreter": "python3", "distribution": "Ubuntu", "release": "20.04", "version": "0"} # Create an applet using DXApplet.new def create_applet(self, name="app_name"): @@ -3172,6 +3187,83 @@ def test_assert_consistent_regions(self): with self.assertRaises(app_builder.AppBuilderException): assert_consistent_regions({"aws:us-east-1": None}, ["azure:westus"], app_builder.AppBuilderException) +class TestWorkflowBuilderUtils(testutil.DXTestCaseBuildWorkflows): + def setUp(self): + super(TestWorkflowBuilderUtils, self).setUp() + self.temp_file_path = tempfile.mkdtemp() + self.dxworkflow_spec = self.create_dxworkflow_spec() + self.workflow_dir = self.write_workflow_directory('test_clean_workflow_spec', + json.dumps(self.dxworkflow_spec)) + + def create_dxworkflow_spec(self): + dxworkflow_spec = {"name": "my_workflow", + "title": "This is a beautiful workflow", + "summary": "", # empty string + "project": self.project, + "types": [], # empty array + "tags": ["dxCompiler"], + "outputFolder": None, # empty value + "description":"", + "properties": {}, # empty dict + "details": {"detail1": "", # empty value + "version": "2.8.1-SNAPSHOT", + "detail3": []}, + "dxapi": "1.0.0", + "stages": [{"id": "stage_0", + "name": "stage_0_name", + "executable": self.test_applet_id, + "input": {"number": 777}, + "folder": "/stage_0_output", + "executionPolicy": { + "restartOn": {}, # nested empty value + "onNonRestartableFailure": "failStage"}, + "systemRequirements": {"main": {"instanceType": "mem1_ssd1_x2"}}}, + {"id": "stage_1", + "folder": None, # nested empty value + "executable": self.test_applet_id, + "input": {"number": {"$dnanexus_link": {"stage": "stage_0", + "outputField": "number"}}}}]} + return dxworkflow_spec + + def tearDown(self): + super(TestWorkflowBuilderUtils, self).tearDown() + + def test_clean_json_spec(self): + ''' + Test the cleanup function used when dx build --globalworkflow --from + Empty key value pairs in the source workflow spec need to be removed, for example "folder", + when the source spec is fetched from dxCompiler workflow + ''' + cleanup_empty_keys = workflow_builder._cleanup_empty_keys + + # case 1: if building from dxworkflow.json + with open(os.path.join(self.workflow_dir, "dxworkflow.json")) as json_spec_file: + json_spec_from_file = dxpy.utils.json_load_raise_on_duplicates(json_spec_file) + clean_json_spec = cleanup_empty_keys(json_spec_from_file) + + for e in ["types", "description", "properties"]: + self.assertNotIn(e, clean_json_spec) + + self.assertEqual({'version': '2.8.1-SNAPSHOT'}, clean_json_spec["details"]) + + self.assertNotIn("restartOn", clean_json_spec["stages"][0]["executionPolicy"]) + for e in ["folder","executionPolicy","systemRequirements"]: + self.assertNotIn(e, clean_json_spec["stages"][1]) + + # build a test workflow with the cleaned json spec, and fetch its description + # which will add some fields that are empty + workflow_id = dxpy.api.workflow_new(clean_json_spec)["id"] + json_spec_from_workflow = dxpy.DXWorkflow(workflow_id).describe(fields={"properties","details"},default_fields=True) + + clean_json_spec = cleanup_empty_keys(json_spec_from_workflow) + for e in ["types", "description", "properties"]: + self.assertNotIn(e, clean_json_spec) + + self.assertEqual({'version': '2.8.1-SNAPSHOT'}, clean_json_spec["details"]) + + self.assertNotIn("restartOn", clean_json_spec["stages"][0]["executionPolicy"]) + for e in ["folder","executionPolicy","systemRequirements"]: + self.assertNotIn(e, clean_json_spec["stages"][1]) class TestApiWrappers(unittest.TestCase): @pytest.mark.TRACEABILITY_MATRIX @@ -3182,7 +3274,6 @@ def test_system_greet(self): auth=None) assert 'messages' in greeting - if __name__ == '__main__': if dxpy.AUTH_HELPER is None: sys.exit(1, 'Error: Need to be logged in to run these tests') diff --git a/src/python/test/test_dxpy_utils.py b/src/python/test/test_dxpy_utils.py index bf6d971ab4..d17bd740e5 100755 --- a/src/python/test/test_dxpy_utils.py +++ b/src/python/test/test_dxpy_utils.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # -*- coding: utf-8 -*- # # Copyright (C) 2013-2016 DNAnexus, Inc. @@ -121,6 +121,7 @@ def test_dxjsonencoder(self): '{"a": [{"b": {"$dnanexus_link": "file-xxxxxxxxxxxxxxxxxxxxxxxx"}}, {"$dnanexus_link": "record-rrrrrrrrrrrrrrrrrrrrrrrr"}]}') class TestEDI(DXExecDependencyInstaller): + __test__ = False def __init__(self, *args, **kwargs): self.command_log, self.message_log = [], [] DXExecDependencyInstaller.__init__(self, *args, **kwargs) @@ -154,9 +155,9 @@ def test_install_bundled_dependencies(self): job_desc={"region": "azure:westus"}) with self.assertRaisesRegex(DXError, 'file-assetwest'): edi.install() - with self.assertRaisesRegex(KeyError, 'aws:cn-north-1'): + with self.assertRaisesRegex(KeyError, 'aws:eu-central-1'): self.get_edi({"bundledDependsByRegion": bundled_depends_by_region}, - job_desc={"region": "aws:cn-north-1"}) + job_desc={"region": "aws:eu-central-1"}) def test_dx_execdepends_installer(self): def assert_cmd_ran(edi, regexp): @@ -223,7 +224,7 @@ def assert_log_contains(edi, regexp): self.assertNotRegex("\n".join(edi.command_log), "w00t") for name in "w00t", "f1": assert_log_contains(edi, - "Skipping dependency {} because it is inactive in stage \(function\) main".format(name)) + r"Skipping dependency {} because it is inactive in stage \(function\) main".format(name)) edi = self.get_edi({"execDepends": [{"name": "git", "stages": ["foo", "bar"]}]}, job_desc={"function": "foo"}) diff --git a/src/python/test/test_dxunpack.py b/src/python/test/test_dxunpack.py index d266990639..bdf0062541 100755 --- a/src/python/test/test_dxunpack.py +++ b/src/python/test/test_dxunpack.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # -*- coding: utf-8 -*- # # Copyright (C) 2016 DNAnexus, Inc. @@ -27,7 +27,7 @@ from dxpy_testutil import (DXTestCase) from dxpy_testutil import chdir - +@unittest.skip("Temporarily disabled") class TestDXUnpack(DXTestCase): def test_file_name_with_special_chars_locally(self): # create a tar.gz file with spaces, quotes and escape chars in its name diff --git a/src/python/test/test_extract_assay.py b/src/python/test/test_extract_assay.py new file mode 100755 index 0000000000..5b862974a2 --- /dev/null +++ b/src/python/test/test_extract_assay.py @@ -0,0 +1,799 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (C) 2022 DNAnexus, Inc. +# +# This file is part of dx-toolkit (DNAnexus platform client libraries). +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy +# of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +# Run manually with python2 and python3 src/python/test/test_extract_assay.py + +import unittest +import dxpy +import os +import subprocess +import json +import sys + +from unittest.mock import patch +from io import StringIO + +from parameterized import parameterized + +from dxpy_testutil import cd +from dxpy.dx_extract_utils.filter_to_payload import ( + retrieve_geno_bins, + basic_filter, + location_filter, + generate_assay_filter, + final_payload, + validate_JSON, +) +from dxpy.dx_extract_utils.germline_utils import ( + filter_results, + _produce_loci_dict, + infer_genotype_type +) +from dxpy.cli.dataset_utilities import ( + DXDataset, + resolve_validate_record_path, + get_assay_name_info, +) +from dxpy.dx_extract_utils.input_validation import validate_filter_applicable_genotype_types + + +python_version = sys.version_info.major + +dirname = os.path.dirname(__file__) + + +class TestDXExtractAssay(unittest.TestCase): + @classmethod + def setUpClass(cls): + test_project_name = "dx-toolkit_test_data" + cls.test_v1_record = "{}:/Extract_Assay_Germline/test01_dataset".format( + test_project_name + ) + cls.test_record = "{}:/Extract_Assay_Germline/test01_v1_0_1_dataset".format( + test_project_name + ) + cls.test_non_alt_record = "{}:/Extract_Assay_Germline/test03_dataset".format( + test_project_name + ) + cls.output_folder = os.path.join(dirname, "extract_assay_germline/test_output/") + cls.malformed_json_dir = os.path.join(dirname, "ea_malformed_json") + cls.proj_id = list( + dxpy.find_projects(describe=False, level="VIEW", name=test_project_name) + )[0]["id"] + cd(cls.proj_id + ":/") + + ############ + # Unit Tests + ############ + + # Test retrieve_geno_bins + def test_get_assay_name_info(self): + # Set to true for the list assay utilities response instead of the normal functionality + list_assays = False + # When assay name is none, function looks for and selects first assay of type somatic that it finds + assay_name = None + friendly_assay_type = "germline" + project, entity_result, resp, dataset_project = resolve_validate_record_path( + self.test_record + ) + dataset_id = resp["dataset"] + rec_descriptor = DXDataset(dataset_id, project=dataset_project).get_descriptor() + # Expected Results + expected_assay_name = "test01_assay" + expected_assay_id = "cc5dcc31-000c-4a2c-b225-ecad6233a0a3" + expected_ref_genome = "GRCh38.92" + expected_additional_descriptor_info = { + "exclude_refdata": True, + "exclude_halfref": True, + "exclude_nocall": True, + "genotype_type_table": "genotype_alt_read_optimized", + } + + ( + selected_assay_name, + selected_assay_id, + selected_ref_genome, + additional_descriptor_info, + ) = get_assay_name_info( + list_assays=False, + assay_name=assay_name, + path=self.test_record, + friendly_assay_type=friendly_assay_type, + rec_descriptor=rec_descriptor, + ) + + self.assertEqual(expected_assay_name, selected_assay_name) + self.assertEqual(expected_assay_id, selected_assay_id) + self.assertEqual(expected_ref_genome, selected_ref_genome) + self.assertEqual(expected_additional_descriptor_info, additional_descriptor_info) + + def test_retrieve_geno_bins(self): + # list_of_genes, project, genome_reference + list_of_genes = ["ENSG00000173213"] + genome_reference = "GRCh38.92" + expected_output = [{"chr": "18", "start": "47390", "end": "49557"}] + self.assertEqual( + retrieve_geno_bins(list_of_genes, self.proj_id, genome_reference), + expected_output, + ) + + def test_basic_filter_allele(self): + table = "allele" + friendly_name = "rsid" + values = ["rs1342568097"] + genome_reference = "GRCh38.92" + + expected_output = { + "allele$dbsnp151_rsid": [{"condition": "any", "values": ["rs1342568097"]}] + } + + self.assertEqual( + basic_filter(table, friendly_name, values, self.proj_id, genome_reference), + expected_output, + ) + + def test_basic_filter_annotation(self): + table = "annotation" + friendly_name = "gene_id" + values = ["ENSG00000173213"] + genome_reference = "GRCh38.92" + + expected_output = { + "annotation$gene_id": [ + { + "condition": "in", + "values": ["ENSG00000173213"], + "geno_bins": [{"chr": "18", "start": "47390", "end": "49557"}], + } + ] + } + + self.assertEqual( + basic_filter(table, friendly_name, values, self.proj_id, genome_reference), + expected_output, + ) + + def test_basic_filter_genotype(self): + table = "genotype" + friendly_name = "allele_id" + values = ["18_47361_T_G"] + genome_reference = "GRCh38.92" + + expected_output = { + "allele$a_id": [{"condition": "in", "values": ["18_47361_T_G"]}] + } + + self.assertEqual( + basic_filter(table, friendly_name, values, self.proj_id, genome_reference), + expected_output, + ) + + def test_location_filter(self): + location_list = [ + { + "chromosome": "18", + "starting_position": "47361", + "ending_position": "47364", + } + ] + + expected_output = { + "allele$a_id": [ + { + "condition": "in", + "values": [], + "geno_bins": [{"chr": "18", "start": 47361, "end": 47364}], + } + ] + } + + self.assertEqual(location_filter(location_list, "allele"), expected_output) + + def test_genotype_location_filter(self): + location_list = [ + { + "chromosome": "18", + "starting_position": "47361", + } + ] + + expected_output = { + "genotype$a_id": [ + { + "condition": "in", + "values": [], + "geno_bins": [{"chr": "18", "start": 47361, "end": 47361}], + } + ] + } + + self.assertEqual(location_filter(location_list, "genotype"), expected_output) + + def test_generate_assay_filter(self): + # A small payload, uses allele_rsid.json + full_input_dict = {"rsid": ["rs1342568097"]} + name = "test01_assay" + id = "c6e9c0ea-5752-4299-8de2-8620afba7b82" + genome_reference = "GRCh38.92" + filter_type = "allele" + + expected_output = { + "assay_filters": { + "name": "test01_assay", + "id": "c6e9c0ea-5752-4299-8de2-8620afba7b82", + "filters": { + "allele$dbsnp151_rsid": [ + {"condition": "any", "values": ["rs1342568097"]} + ] + }, + "logic": "and", + } + } + + self.assertEqual( + generate_assay_filter( + full_input_dict, + name, + id, + self.proj_id, + genome_reference, + filter_type, + ), + expected_output, + ) + + def test_final_payload(self): + full_input_dict = {"rsid": ["rs1342568097"]} + name = "test01_assay" + id = "c6e9c0ea-5752-4299-8de2-8620afba7b82" + genome_reference = "GRCh38.92" + filter_type = "allele" + + expected_output_payload = { + "project_context": self.proj_id, + "order_by": [{"allele_id":"asc"}], + "fields": [ + {"allele_id": "allele$a_id"}, + {"chromosome": "allele$chr"}, + {"starting_position": "allele$pos"}, + {"ref": "allele$ref"}, + {"alt": "allele$alt"}, + {"rsid": "allele$dbsnp151_rsid"}, + {"allele_type": "allele$allele_type"}, + {"dataset_alt_freq": "allele$alt_freq"}, + {"gnomad_alt_freq": "allele$gnomad201_alt_freq"}, + {"worst_effect": "allele$worst_effect"}, + ], + "adjust_geno_bins": False, + "raw_filters": { + "assay_filters": { + "name": "test01_assay", + "id": "c6e9c0ea-5752-4299-8de2-8620afba7b82", + "filters": { + "allele$dbsnp151_rsid": [ + {"condition": "any", "values": ["rs1342568097"]} + ] + }, + "logic": "and", + } + }, + "is_cohort": True, + "distinct": True, + } + + expected_output_fields = [ + "allele_id", + "chromosome", + "starting_position", + "ref", + "alt", + "rsid", + "allele_type", + "dataset_alt_freq", + "gnomad_alt_freq", + "worst_effect", + ] + + test_payload, test_fields = final_payload( + full_input_dict, + name, + id, + self.proj_id, + genome_reference, + filter_type, + ) + + self.assertEqual( + test_payload, + expected_output_payload, + ) + self.assertEqual(test_fields, expected_output_fields) + + def test_validate_json(self): + filter = { + "rsid": ["rs1342568097"], + "type": ["SNP", "Del", "Ins"], + "dataset_alt_af": {"min": 1e-05, "max": 0.5}, + "gnomad_alt_af": {"min": 1e-05, "max": 0.5}, + } + type = "allele" + + try: + validate_JSON(filter, type) + except: + self.fail("This just needs to complete without error") + + def test_malformed_json(self): + for filter_type in ["allele", "annotation", "genotype"]: + malformed_json_filenames = os.listdir( + os.path.join(self.malformed_json_dir, filter_type) + ) + for name in malformed_json_filenames: + file_path = os.path.join(self.malformed_json_dir, filter_type, name) + with open(file_path, "r") as infile: + filter = json.load(infile) + with self.assertRaises(SystemExit) as cm: + validate_JSON(filter, filter_type) + self.assertEqual(cm.exception.code, 1) + + def test_bad_rsid(self): + filter = {"rsid": ["rs1342568097","rs1342568098"]} + test_project = "dx-toolkit_test_data" + test_record = "{}:Extract_Assay_Germline/test01_v1_0_1_dataset".format(test_project) + + command = ["dx", "extract_assay", "germline", test_record, "--retrieve-allele", json.dumps(filter)] + process = subprocess.Popen(command, stderr=subprocess.PIPE, universal_newlines=True) + expected_error_message = "At least one rsID provided in the filter is not present in the provided dataset or cohort" + self.assertTrue(expected_error_message in process.communicate()[1]) + + def test_duplicate_rsid(self): + table = "allele" + friendly_name = "rsid" + values = ["rs1342568097", "rs1342568097"] + genome_reference = "GRCh38.92" + + expected_output = { + "allele$dbsnp151_rsid": [{"condition": "any", "values": ["rs1342568097"]}] + } + + self.assertEqual( + basic_filter(table, friendly_name, values, self.proj_id, genome_reference), + expected_output, + ) + + # Test filters for exclusion options + def test_no_call_warning(self): + # This is a test of the validate_filter_applicable_genotype_types function "no-call", "ref" requested + filter_dict = {"genotype_type": ["no-call", "ref"]} + exclude_nocall = True + exclude_refdata = True + infer_nocall = False + expected_warnings = [ + "WARNING: Filter requested genotype type 'no-call', genotype entries of this type were not ingested in the provided dataset and the --infer-nocall flag is not set!", + "WARNING: Filter requested genotype type 'ref', genotype entries of this type were not ingested in the provided dataset and the --infer-ref flag is not set!" + ] + with patch("sys.stderr", new=StringIO()) as fake_err: + validate_filter_applicable_genotype_types( + infer_nocall, infer_ref=False, filter_dict=filter_dict, + exclude_refdata=exclude_refdata, exclude_nocall=exclude_nocall, exclude_halfref=False + ) + output = fake_err.getvalue().strip() + for warning in expected_warnings: + self.assertIn(warning, output) + + def test_no_genotype_type_warning(self): + # This is a test of the validate_filter_applicable_genotype_types function no genotype type requested + filter_dict = {"genotype_type": []} + exclude_nocall = True + + with patch("sys.stderr", new=StringIO()) as fake_err: + validate_filter_applicable_genotype_types( + infer_nocall=False, infer_ref=False, filter_dict=filter_dict, + exclude_refdata=False, exclude_nocall=exclude_nocall, exclude_halfref=False + ) + output = fake_err.getvalue().strip() + self.assertEqual(output, "WARNING: No genotype type requested in the filter. All genotype types will be returned. Genotype entries of type 'no-call' were not ingested in the provided dataset and the --infer-nocall flag is not set!") + + def test_no_genotype_type_warning_exclude_halfref(self): + # This is a test of the validate_filter_applicable_genotype_types function half genotype type requested + filter_dict = {"genotype_type": []} + exclude_halfref = True + + with patch("sys.stderr", new=StringIO()) as fake_err: + validate_filter_applicable_genotype_types( + infer_nocall=False, infer_ref=False, filter_dict=filter_dict, + exclude_refdata=False, exclude_nocall=False, exclude_halfref=exclude_halfref + ) + output = fake_err.getvalue().strip() + self.assertEqual(output, "WARNING: No genotype type requested in the filter. All genotype types will be returned. 'half-ref' genotype entries (0/.) were not ingested in the provided dataset!") + + def test_filter_results(self): + # Define sample input data + results = [ + { + "sample_id": "SAMPLE_1", + "allele_id": "1_1076145_A_AT", + "locus_id": "1_1076145_A_T", + "chromosome": "1", + "starting_position": 1076145, + "ref": "A", + "alt": "AT", + "genotype_type": "het-alt", + }, + { + "sample_id": "SAMPLE_2", + "allele_id": "1_1076146_A_AT", + "locus_id": "1_1076146_A_T", + "chromosome": "1", + "starting_position": 1076146, + "ref": "A", + "alt": "AT", + "genotype_type": "het-alt", + }, + { + "sample_id": "SAMPLE_3", + "allele_id": "1_1076147_A_AT", + "locus_id": "1_1076147_A_T", + "chromosome": "1", + "starting_position": 1076147, + "ref": "A", + "alt": "AT", + "genotype_type": "ref", + }, + ] + # Call the function to filter the results + filtered_results = filter_results( + results=results, key="genotype_type", restricted_values=["het-alt"] + ) + + # Define the expected output + expected_output = [ + { + "sample_id": "SAMPLE_3", + "allele_id": "1_1076147_A_AT", + "locus_id": "1_1076147_A_T", + "chromosome": "1", + "starting_position": 1076147, + "ref": "A", + "alt": "AT", + "genotype_type": "ref", + }, + ] + + # Assert that the filtered results match the expected output + self.assertEqual(filtered_results, expected_output) + + def test_produce_loci_dict(self): + # Define the input data + loci = [ + { + "locus_id": "18_47361_A_T", + "chromosome": "18", + "starting_position": 47361, + "ref": "A", + }, + { + "locus_id": "X_1000_C_A", + "chromosome": "X", + "starting_position": 1000, + "ref": "C", + }, + { + "locus_id": "1_123_A_.", + "chromosome": "1", + "starting_position": 123, + "ref": "A", + }, + ] + results_entries = [ + { + "locus_id": "18_47361_A_T", + "allele_id": "18_47361_A_T", + "sample_id": "sample1", + "chromosome": "18", + "starting_position": 47361, + "ref": "A", + "alt": "T", + }, + { + "locus_id": "18_47361_A_T", + "allele_id": "18_47361_A_G", + "sample_id": "sample2", + "chromosome": "18", + "starting_position": 47361, + "ref": "A", + "alt": "G", + }, + { + "locus_id": "X_1000_C_A", + "allele_id": "X_1000_C_A", + "sample_id": "sample1", + "chromosome": "X", + "starting_position": 1000, + "ref": "C", + "alt": "A", + }, + ] + + # Define the expected output + expected_output = { + "18_47361_A_T": { + "samples": {"sample1", "sample2"}, + "entry": { + "allele_id": None, + "locus_id": "18_47361_A_T", + "chromosome": "18", + "starting_position": 47361, + "ref": "A", + "alt": None, + }, + }, + "X_1000_C_A": { + "samples": {"sample1"}, + "entry": { + "allele_id": None, + "locus_id": "X_1000_C_A", + "chromosome": "X", + "starting_position": 1000, + "ref": "C", + "alt": None, + }, + }, + "1_123_A_.": { + "samples": set(), + "entry": { + "allele_id": None, + "locus_id": "1_123_A_.", + "chromosome": "1", + "starting_position": 123, + "ref": "A", + "alt": None, + }, + }, + } + + # Call the function + result = _produce_loci_dict(loci, results_entries) + + # Assert the result + self.assertEqual(result, expected_output) + + def test_infer_genotype_type(self): + samples = ["SAMPLE_1", "SAMPLE_2", "SAMPLE_3"] + loci = [ + { + "locus_id": "1_1076145_A_T", + "chromosome": "1", + "starting_position": 1076145, + "ref": "A", + }, + { + "locus_id": "2_1042_G_CC", + "chromosome": "2", + "starting_position": 1042, + "ref": "G", + }, + ] + result_entries = [ + { + "sample_id": "SAMPLE_2", + "allele_id": "1_1076145_A_AT", + "locus_id": "1_1076145_A_T", + "chromosome": "1", + "starting_position": 1076145, + "ref": "A", + "alt": "AT", + "genotype_type": "het-alt", + }, + { + "sample_id": "SAMPLE_3", + "allele_id": "1_1076145_A_T", + "locus_id": "1_1076145_A_T", + "chromosome": "1", + "starting_position": 1076145, + "ref": "A", + "alt": "T", + "genotype_type": "hom-ref", + }, + ] + type_to_infer = "no-call" + + expected_output = [ + { + "sample_id": "SAMPLE_1", + "allele_id": None, + "locus_id": "1_1076145_A_T", + "chromosome": "1", + "starting_position": 1076145, + "ref": "A", + "alt": None, + "genotype_type": "no-call", + }, + { + "sample_id": "SAMPLE_1", + "allele_id": None, + "locus_id": "2_1042_G_CC", + "chromosome": "2", + "starting_position": 1042, + "ref": "G", + "alt": None, + "genotype_type": "no-call", + }, + { + "sample_id": "SAMPLE_2", + "allele_id": None, + "locus_id": "2_1042_G_CC", + "chromosome": "2", + "starting_position": 1042, + "ref": "G", + "alt": None, + "genotype_type": "no-call", + }, + { + "sample_id": "SAMPLE_3", + "allele_id": None, + "locus_id": "2_1042_G_CC", + "chromosome": "2", + "starting_position": 1042, + "ref": "G", + "alt": None, + "genotype_type": "no-call", + }, + ] + + output = infer_genotype_type(samples, loci, result_entries, type_to_infer) + self.assertEqual(output, result_entries + expected_output) + ########## + # Normal Command Lines + ########## + + def test_json_help(self): + """Check successful call of help for the retrieve allele filter""" + command = ["dx", "extract_assay", "germline", "fakepath", "--retrieve-allele", "--json-help"] + process = subprocess.Popen(command, stdout=subprocess.PIPE, universal_newlines=True) + expected_help = """# Filters and respective definitions +# +# rsid: rsID associated with an allele or set of alleles. If multiple values +# are provided, the conditional search will be, "OR." For example, ["rs1111", +# "rs2222"], will search for alleles which match either "rs1111" or "rs2222". +# String match is case sensitive. Duplicate values are permitted and will be +# handled silently. +# +# type: Type of allele. Accepted values are "SNP", "Ins", "Del", "Mixed". If +# multiple values are provided, the conditional search will be, "OR." For +# example, ["SNP", "Ins"], will search for variants which match either "SNP" +# or "Ins". String match is case sensitive. +# +# dataset_alt_af: Dataset alternate allele frequency, a json object with +# empty content or two sets of key/value pair: {min: 0.1, max:0.5}. Accepted +# numeric value for each key is between and including 0 and 1. If a user +# does not want to apply this filter but still wants this information in the +# output, an empty json object should be provided. +# +# gnomad_alt_af: gnomAD alternate allele frequency. a json object with empty +# content or two sets of key/value pair: {min: 0.1, max:0.5}. Accepted value +# for each key is between 0 and 1. If a user does not want to apply this +# filter but still wants this information in the output, an empty json object +# should be provided. +# +# location: Genomic range in the reference genome where the starting position +# of alleles fall into. If multiple values are provided in the list, the +# conditional search will be, "OR." String match is case sensitive. +# +# JSON filter template for --retrieve-allele +{ + "rsid": ["rs11111", "rs22222"], + "type": ["SNP", "Del", "Ins"], + "dataset_alt_af": {"min": 0.001, "max": 0.05}, + "gnomad_alt_af": {"min": 0.001, "max": 0.05}, + "location": [ + { + "chromosome": "1", + "starting_position": "10000", + "ending_position": "20000" + }, + { + "chromosome": "X", + "starting_position": "500", + "ending_position": "1700" + } + ] +} +""" + self.assertEqual(expected_help, process.communicate()[0]) + + def test_generic_help(self): + """Test the generic help message""" + command = "dx extract_assay germline -h > /dev/null" + subprocess.check_call(command, shell=True) + + def test_list_assays(self): + command = ["dx", "extract_assay", "germline", self.test_record, "--list-assays"] + process = subprocess.Popen(command, stdout=subprocess.PIPE, universal_newlines=True) + self.assertEqual("test01_assay", process.communicate()[0].strip()) + + + def test_assay_name(self): + """A test of the --assay-name functionality, returns the same output.""" + allele_rsid_filter = json.dumps({"rsid": ["rs1342568097"]}) + command1 = ["dx", "extract_assay", "germline", self.test_record, "--assay-name", "test01_assay", "--retrieve-allele", allele_rsid_filter, "-o", "-"] + process1 = subprocess.Popen(command1, stdout=subprocess.PIPE, universal_newlines=True) + command2 = ["dx", "extract_assay", "germline", self.test_record, "--retrieve-allele", allele_rsid_filter, "-o", "-"] + process2 = subprocess.Popen(command2, stdout=subprocess.PIPE, universal_newlines=True) + self.assertEqual(process1.communicate(), process2.communicate()) + + + @parameterized.expand([ + ("test_record", ["ref", "het-ref", "hom", "het-alt", "half", "no-call"]), + ("test_v1_record", ["het-ref", "hom", "het-alt", "half"]), + ]) + def test_retrieve_genotype(self, record, genotype_types): + """Testing --retrieve-genotype functionality""" + allele_genotype_type_filter = json.dumps({ + "allele_id": ["18_47408_G_A"], + "genotype_type": genotype_types, + }) + expected_result = "sample_1_3\t18_47408_G_A\t18_47408_G_A\t18\t47408\tG\tA\thet-ref" + command = ["dx", "extract_assay", "germline", getattr(self, record), "--retrieve-genotype", allele_genotype_type_filter, "-o", "-"] + process = subprocess.Popen(command, stdout=subprocess.PIPE, universal_newlines=True) + self.assertIn(expected_result, process.communicate()[0]) + + + @unittest.skip("Vizserver implementation of PMUX-1652 needs to be deployed") + def test_retrieve_non_alt_genotype(self): + """Testing --retrieve-genotype functionality""" + location_genotype_type_filter = json.dumps({ + "location": [{ + "chromosome": "20", + "starting_position": "14370", + }], + "genotype_type": ["ref", "half", "no-call"] + }) + # not a comprehensive list + expected_results = [ + "S01_m_m\t\t20_14370_G_A\t20\t14370\tG\t\tno-call", + "S02_m_0\t\t20_14370_G_A\t20\t14370\tG\t\thalf", + "S06_0_m\t\t20_14370_G_A\t20\t14370\tG\t\thalf", + "S07_0_0\t\t20_14370_G_A\t20\t14370\tG\t\tref", + ] + command = ["dx", "extract_assay", "germline", self.test_non_alt_record, "--retrieve-genotype", location_genotype_type_filter, "-o", "-"] + process = subprocess.Popen(command, stdout=subprocess.PIPE, universal_newlines=True) + result = process.communicate()[0] + [self.assertIn(expected_result, result) for expected_result in expected_results] + + ########### + # Malformed command lines + ########### + + def test_filter_mutex(self): + print("testing filter mutex") + """Ensure that the failure mode of multiple filter types being provided is caught""" + # Grab two random filter JSONs of different types + allele_json = '{"rsid": ["rs1342568097"]}' + annotation_json = '{"allele_id": ["18_47408_G_A"]}' + command = ["dx", "extract_assay", "germline", self.test_record, "--retrieve-allele", allele_json, "--retrieve-annotation", annotation_json, "-o", "-"] + + process = subprocess.Popen(command, stderr=subprocess.PIPE, universal_newlines=True) + expected_error_message = "dx extract_assay germline: error: argument --retrieve-annotation: not allowed with argument --retrieve-allele" + self.assertTrue(expected_error_message in process.communicate()[1]) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/python/test/test_extract_dataset.py b/src/python/test/test_extract_dataset.py new file mode 100755 index 0000000000..08a1695d99 --- /dev/null +++ b/src/python/test/test_extract_dataset.py @@ -0,0 +1,210 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (C) 2022 DNAnexus, Inc. +# +# This file is part of dx-toolkit (DNAnexus platform client libraries). +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy +# of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +from __future__ import print_function, unicode_literals, division, absolute_import + +import unittest +import tempfile +import shutil +import os +import re +import subprocess +import pandas as pd +import dxpy +from dxpy_testutil import cd, chdir + +dirname = os.path.dirname(__file__) + +class TestDXExtractDataset(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.test_file_dir = os.path.join(dirname, "extract_dataset_test_files") + proj_name = "dx-toolkit_test_data" + proj_id = list(dxpy.find_projects(describe=False, level='VIEW', name=proj_name))[0]['id'] + cd(proj_id + ":/") + + def test_e2e_dataset_ddd(self): + dataset_record = "dx-toolkit_test_data:Extract_Dataset/extract_dataset_test" + out_directory = tempfile.mkdtemp() + cmd = ["dx", "extract_dataset", dataset_record, "-ddd", "-o", out_directory] + subprocess.check_call(cmd) + self.end_to_end_ddd(out_directory=out_directory, rec_name = "extract_dataset_test") + + def test_e2e_cohortbrowser_ddd(self): + cohort_record = "dx-toolkit_test_data:Extract_Dataset/Combined_Cohort_Test" + out_directory = tempfile.mkdtemp() + cmd = ["dx", "extract_dataset", cohort_record, "-ddd", "-o", out_directory] + subprocess.check_call(cmd) + self.end_to_end_ddd(out_directory=out_directory, rec_name = "Combined_Cohort_Test") + + def test_e2e_dataset_sql(self): + dataset_record = "dx-toolkit_test_data:Extract_Dataset/extract_dataset_test" + truth_output = "SELECT `patient_0001_1`.`patient_id` AS `patient.patient_id`, `patient_0001_1`.`name` AS `patient.name`, `patient_0002_1`.`weight` AS `patient.weight`, `patient_0001_1`.`date_of_birth` AS `patient.date_of_birth`, `patient_0002_1`.`verified_dtm` AS `patient.verified_dtm`, `test_1`.`test_id` AS `test.test_id`, `trial_visit_0001_1`.`visit_id` AS `trial_visit.visit_id`, `baseline_0001_1`.`baseline_id` AS `baseline.baseline_id`, `hospital_0001_1`.`hospital_id` AS `hospital.hospital_id`, `doctor_0001_1`.`doctor_id` AS `doctor.doctor_id` FROM `database_[a-z0-9]{24}__extract_dataset_test`.`patient_0001` AS `patient_0001_1` LEFT OUTER JOIN `database_[a-z0-9]{24}__extract_dataset_test`.`patient_0002` AS `patient_0002_1` ON `patient_0001_1`.`patient_id` = `patient_0002_1`.`patient_id` LEFT OUTER JOIN `database_[a-z0-9]{24}__extract_dataset_test`.`trial_visit_0001` AS `trial_visit_0001_1` ON `patient_0001_1`.`patient_id` = `trial_visit_0001_1`.`visit_patient_id` LEFT OUTER JOIN `database_[a-z0-9]{24}__extract_dataset_test`.`test` AS `test_1` ON `trial_visit_0001_1`.`visit_id` = `test_1`.`test_visit_id` LEFT OUTER JOIN `database_[a-z0-9]{24}__extract_dataset_test`.`baseline_0001` AS `baseline_0001_1` ON `patient_0001_1`.`patient_id` = `baseline_0001_1`.`b_patient_id` LEFT OUTER JOIN `database_[a-z0-9]{24}__extract_dataset_test`.`hospital_0001` AS `hospital_0001_1` ON `patient_0001_1`.`hid` = `hospital_0001_1`.`hospital_id` LEFT OUTER JOIN `database_[a-z0-9]{24}__extract_dataset_test`.`doctor_0001` AS `doctor_0001_1` ON `trial_visit_0001_1`.`visit_did` = `doctor_0001_1`.`doctor_id`;" + cmd = ["dx", "extract_dataset", dataset_record, "--fields", "patient.patient_id, patient.name,patient.weight, patient.date_of_birth, patient.verified_dtm, test.test_id, trial_visit.visit_id, baseline.baseline_id, hospital.hospital_id , doctor.doctor_id", + "--sql", "-o", "-"] + process = subprocess.Popen(cmd, stdout=subprocess.PIPE, universal_newlines=True) + stdout = process.communicate()[0] + self.assertTrue(re.match(truth_output,stdout.strip())) + + def test_e2e_cohortbrowser_sql(self): + cohort_record = "dx-toolkit_test_data:Extract_Dataset/Combined_Cohort_Test" + truth_output = "SELECT `patient_0001_1`.`patient_id` AS `patient.patient_id`, `patient_0001_1`.`name` AS `patient.name`, `patient_0002_1`.`weight` AS `patient.weight`, `patient_0001_1`.`date_of_birth` AS `patient.date_of_birth`, `patient_0002_1`.`verified_dtm` AS `patient.verified_dtm`, `test_1`.`test_id` AS `test.test_id`, `trial_visit_0001_1`.`visit_id` AS `trial_visit.visit_id`, `baseline_0001_1`.`baseline_id` AS `baseline.baseline_id`, `hospital_0001_1`.`hospital_id` AS `hospital.hospital_id`, `doctor_0001_1`.`doctor_id` AS `doctor.doctor_id` FROM `database_[a-z0-9]{24}__extract_dataset_test`.`patient_0001` AS `patient_0001_1` LEFT OUTER JOIN `database_[a-z0-9]{24}__extract_dataset_test`.`patient_0002` AS `patient_0002_1` ON `patient_0001_1`.`patient_id` = `patient_0002_1`.`patient_id` LEFT OUTER JOIN `database_[a-z0-9]{24}__extract_dataset_test`.`trial_visit_0001` AS `trial_visit_0001_1` ON `patient_0001_1`.`patient_id` = `trial_visit_0001_1`.`visit_patient_id` LEFT OUTER JOIN `database_[a-z0-9]{24}__extract_dataset_test`.`test` AS `test_1` ON `trial_visit_0001_1`.`visit_id` = `test_1`.`test_visit_id` LEFT OUTER JOIN `database_[a-z0-9]{24}__extract_dataset_test`.`baseline_0001` AS `baseline_0001_1` ON `patient_0001_1`.`patient_id` = `baseline_0001_1`.`b_patient_id` LEFT OUTER JOIN `database_[a-z0-9]{24}__extract_dataset_test`.`hospital_0001` AS `hospital_0001_1` ON `patient_0001_1`.`hid` = `hospital_0001_1`.`hospital_id` LEFT OUTER JOIN `database_[a-z0-9]{24}__extract_dataset_test`.`doctor_0001` AS `doctor_0001_1` ON `trial_visit_0001_1`.`visit_did` = `doctor_0001_1`.`doctor_id` WHERE `patient_0001_1`.`patient_id` IN \(SELECT `cohort_query`.`patient_id` FROM \(SELECT `patient_0001_1`.`patient_id` AS `patient_id` FROM `database_[a-z0-9]{24}__extract_dataset_test`.`patient_0001` AS `patient_0001_1` WHERE `patient_0001_1`.`patient_id` IN \(SELECT `patient_id` FROM \(SELECT DISTINCT `cohort_subquery`.`patient_id` AS `patient_id` FROM \(SELECT DISTINCT `patient_0001_1`.`patient_id` AS `patient_id`, `patient_0001_1`.`hid` AS `hid` FROM `database_[a-z0-9]{24}__extract_dataset_test`.`patient_0001` AS `patient_0001_1` WHERE EXISTS \(SELECT `hospital_0001_1`.`hospital_id` AS `hospital_id` FROM `database_[a-z0-9]{24}__extract_dataset_test`.`hospital_0001` AS `hospital_0001_1` WHERE `hospital_0001_1`.`hospital_id` BETWEEN 2 AND 5 AND `hospital_0001_1`.`hospital_id` = `patient_0001_1`.`hid`\)\) AS `cohort_subquery` INTERSECT SELECT DISTINCT `patient_0001_1`.`patient_id` AS `patient_id` FROM `database_[a-z0-9]{24}__extract_dataset_test`.`patient_0001` AS `patient_0001_1` WHERE `patient_0001_1`.`patient_id` BETWEEN 2 AND 9\)\)\) AS `cohort_query`\);" + cmd = ["dx", "extract_dataset", cohort_record, "--fields", "patient.patient_id, patient.name,patient.weight, patient.date_of_birth, patient.verified_dtm, test.test_id, trial_visit.visit_id, baseline.baseline_id, hospital.hospital_id , doctor.doctor_id", + "--sql", "-o", "-"] + process = subprocess.Popen(cmd, stdout=subprocess.PIPE, universal_newlines=True) + stdout = process.communicate()[0] + self.assertTrue(re.match(truth_output,stdout.strip())) + + def test_e2e_dataset_fields(self): + dataset_record = "dx-toolkit_test_data:Extract_Dataset/extract_dataset_test" + out_directory = tempfile.mkdtemp() + cmd = ["dx", "extract_dataset", dataset_record, "--fields", "patient.patient_id, patient.name,patient.weight, patient.date_of_birth, patient.verified_dtm, test.test_id, trial_visit.visit_id, baseline.baseline_id, hospital.hospital_id , doctor.doctor_id", + "-o", out_directory] + subprocess.check_call(cmd) + truth_file = "dx-toolkit_test_data:Extract_Dataset/extract_dataset_test.csv" + self.end_to_end_fields(out_directory=out_directory, rec_name = "extract_dataset_test.csv", truth_file=truth_file) + + def test_e2e_cohortbrowser_fields(self): + cohort_record = "dx-toolkit_test_data:Extract_Dataset/Combined_Cohort_Test" + out_directory = tempfile.mkdtemp() + cmd = ["dx", "extract_dataset", cohort_record, "--fields", "patient.patient_id, patient.name,patient.weight, patient.date_of_birth, patient.verified_dtm, test.test_id, trial_visit.visit_id, baseline.baseline_id, hospital.hospital_id , doctor.doctor_id", + "-o", out_directory] + subprocess.check_call(cmd) + truth_file = "dx-toolkit_test_data:Extract_Dataset/Combined_Cohort_Test.csv" + self.end_to_end_fields(out_directory=out_directory, rec_name = "Combined_Cohort_Test.csv", truth_file=truth_file) + + def test_e2e_dataset_fields_file(self): + dataset_record = "dx-toolkit_test_data:Extract_Dataset/extract_dataset_test" + out_directory = tempfile.mkdtemp() + input_fields = os.path.join(self.test_file_dir , "fields_file.txt") + cmd = ["dx", "extract_dataset", dataset_record, "--fields-file", input_fields, + "-o", out_directory] + subprocess.check_call(cmd) + truth_file = "dx-toolkit_test_data:Extract_Dataset/extract_dataset_test.csv" + self.end_to_end_fields(out_directory=out_directory, rec_name = "extract_dataset_test.csv", truth_file=truth_file) + + def test_e2e_cohortbrowser_fields_file(self): + cohort_record = "dx-toolkit_test_data:Extract_Dataset/Combined_Cohort_Test" + out_directory = tempfile.mkdtemp() + input_fields = os.path.join(self.test_file_dir , "fields_file.txt") + cmd = ["dx", "extract_dataset", cohort_record, "--fields-file", input_fields, + "-o", out_directory] + subprocess.check_call(cmd) + truth_file = "dx-toolkit_test_data:Extract_Dataset/Combined_Cohort_Test.csv" + self.end_to_end_fields(out_directory=out_directory, rec_name = "Combined_Cohort_Test.csv", truth_file=truth_file) + + def test_e2e_fields_file_and_fields_negative(self): + dataset_record = "dx-toolkit_test_data:Extract_Dataset/extract_dataset_test" + out_directory = tempfile.mkdtemp() + expected_error = "dx extract_dataset: error: only one of the arguments, --fields or --fields-file, may be supplied at a given time" + input_fields = os.path.join(self.test_file_dir , "fields_file.txt") + cmd = ["dx", "extract_dataset", dataset_record, "--fields", "patient.patient_id", "--fields-file", input_fields, + "-o", out_directory] + process = subprocess.Popen(cmd, stderr=subprocess.PIPE, universal_newlines=True) + stderr = process.communicate()[1] + self.assertTrue(expected_error in stderr, msg = stderr) + + def test_file_already_exists(self): + cohort_record = "dx-toolkit_test_data:Extract_Dataset/Combined_Cohort_Test" + out_directory = tempfile.mkdtemp() + open(os.path.join(out_directory, "Combined_Cohort_Test.csv"), 'w').close() + cmd = ["dx", "extract_dataset", cohort_record, "--fields", "patient.patient_id,patient.name", "-o", out_directory] + process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True) + stdout = process.communicate()[0] + self.assertTrue("Error: path already exists" in stdout.strip()) + shutil.rmtree(out_directory) + + def test_list_entities(self): + dataset_record = "dx-toolkit_test_data:Extract_Dataset/extract_dataset_test" + truth_output = "baseline\tBaseline Test\ndoctor\tDoctors\nhospital\tHospitals\npatient\tPatients\ntest\tTests Performed\ntrial_visit\tVisits" + cmd = ["dx", "extract_dataset", dataset_record, "--list-entities"] + process = subprocess.Popen(cmd, stdout=subprocess.PIPE, universal_newlines=True) + stdout = process.communicate()[0] + self.assertTrue(truth_output in stdout) + + def test_list_entities_negative(self): + dataset_record = "dx-toolkit_test_data:Extract_Dataset/extract_dataset_test" + expected_error_contents = ["--list-entities cannot be specified with:", "--sql"] + cmd = ["dx", "extract_dataset", dataset_record, "--list-entities", "--sql"] + process = subprocess.Popen(cmd, stderr=subprocess.PIPE, universal_newlines=True) + stderr = process.communicate()[1] + for error_content in expected_error_contents: + self.assertTrue(error_content in stderr) + + def test_list_fields(self): + dataset_record = "dx-toolkit_test_data:Extract_Dataset/extract_dataset_test" + truth_output = "test.result_cat\tResult Class\ntest.results\tRaw Result\ntest.test_date\tTest Date\ntest.test_id\tTest ID\ntest.test_type\tTest\ntest.test_visit_id\ttest_visit_id" + entities = "test" + cmd = ["dx", "extract_dataset", dataset_record, "--list-fields", "--entities", entities] + process = subprocess.Popen(cmd, stdout=subprocess.PIPE, universal_newlines=True) + stdout = process.communicate()[0] + self.assertTrue(truth_output in stdout) + + def test_list_fields_negative(self): + dataset_record = "dx-toolkit_test_data:Extract_Dataset/extract_dataset_test" + expected_error_contents = ["The following entity/entities cannot be found:", "tests"] + entities = "tests" + cmd = ["dx", "extract_dataset", dataset_record, "--list-fields","--entities", entities] + process = subprocess.Popen(cmd, stderr=subprocess.PIPE, universal_newlines=True) + stderr = process.communicate()[1] + for error_content in expected_error_contents: + self.assertTrue(error_content in stderr) + + def end_to_end_ddd(self, out_directory, rec_name): + truth_files_directory = tempfile.mkdtemp() + with chdir(truth_files_directory): + cmd = ["dx", "download", "dx-toolkit_test_data:Extract_Dataset/data_dictionary.csv", + "dx-toolkit_test_data:Extract_Dataset/codings.csv", + "dx-toolkit_test_data:Extract_Dataset/entity_dictionary.csv"] + subprocess.check_call(cmd) + os.chdir("..") + truth_file_list = os.listdir(truth_files_directory) + + for file in truth_file_list: + dframe1 = pd.read_csv(os.path.join(truth_files_directory, file)).dropna(axis=1, how='all').sort_index(axis=1) + fil_nam = rec_name + "." + file + dframe2 = pd.read_csv(os.path.join(out_directory, fil_nam)).dropna(axis=1, how='all').sort_index(axis=1) + if file == 'codings.csv': + #continue + dframe1 = dframe1.sort_values(by=['code','coding_name'], axis=0).reset_index(drop=True) + dframe2 = dframe2.sort_values(by=['code','coding_name'], axis=0).reset_index(drop=True) + elif file in ['entity_dictionary.csv', 'data_dictionary.csv']: + dframe1 = dframe1.sort_values(by='entity', axis=0).reset_index(drop=True) + dframe2 = dframe2.sort_values(by='entity', axis=0).reset_index(drop=True) + self.assertTrue(dframe1.equals(dframe2)) + + shutil.rmtree(out_directory) + shutil.rmtree(truth_files_directory) + + def end_to_end_fields(self, out_directory, rec_name, truth_file): + truth_files_directory = tempfile.mkdtemp() + with chdir(truth_files_directory): + cmd = ["dx", "download", truth_file] + subprocess.check_call(cmd) + os.chdir("..") + dframe1 = pd.read_csv(os.path.join(truth_files_directory,os.listdir(truth_files_directory)[0])) + dframe1 = dframe1.sort_values(by=list(dframe1.columns), axis=0).reset_index(drop=True) + dframe2 = pd.read_csv(os.path.join(out_directory, rec_name)) + dframe2 = dframe2.sort_values(by=list(dframe2.columns), axis=0).reset_index(drop=True) + self.assertTrue(dframe1.equals(dframe2)) + + shutil.rmtree(out_directory) + shutil.rmtree(truth_files_directory) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/src/python/test/test_extract_expression.py b/src/python/test/test_extract_expression.py new file mode 100644 index 0000000000..251058655e --- /dev/null +++ b/src/python/test/test_extract_expression.py @@ -0,0 +1,1242 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (C) 2023 DNAnexus, Inc. +# +# This file is part of dx-toolkit (DNAnexus platform client libraries). +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy +# of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +# Run manually with python3 src/python/test/test_extract_expression.py TestDXExtractExpression + +from __future__ import absolute_import + +import unittest +import subprocess +import sys +import os +import dxpy +import copy +import json +import tempfile +import csv +from collections import OrderedDict +import pandas as pd + +import shutil +from dxpy_testutil import cd, chdir +from dxpy.bindings.apollo.json_validation_by_schema import JSONValidator +from dxpy.utils.resolver import resolve_existing_path + +from dxpy.bindings.apollo.schemas.assay_filtering_json_schemas import ( + EXTRACT_ASSAY_EXPRESSION_JSON_SCHEMA, +) +from dxpy.bindings.apollo.cmd_line_options_validator import ArgsValidator +from dxpy.bindings.apollo.schemas.input_arguments_validation_schemas import ( + EXTRACT_ASSAY_EXPRESSION_INPUT_ARGS_SCHEMA, +) +from dxpy.bindings.apollo.vizclient import VizClient + +from dxpy.bindings.apollo.data_transformations import transform_to_expression_matrix +from dxpy.cli.output_handling import write_expression_output +from dxpy.cli.help_messages import EXTRACT_ASSAY_EXPRESSION_JSON_TEMPLATE +from dxpy.bindings.dxrecord import DXRecord +from dxpy.bindings.apollo.dataset import Dataset + +from dxpy.bindings.apollo.vizserver_filters_from_json_parser import JSONFiltersValidator +from dxpy.bindings.apollo.schemas.assay_filtering_conditions import ( + EXTRACT_ASSAY_EXPRESSION_FILTERING_CONDITIONS, +) +from dxpy.bindings.apollo.vizserver_payload_builder import VizPayloadBuilder +from dxpy.exceptions import err_exit + + +dirname = os.path.dirname(__file__) + +python_version = sys.version_info.major + +if python_version == 2: + sys.path.append("./expression_test_assets") + from expression_test_input_dict import ( + CLIEXPRESS_TEST_INPUT, + VIZPAYLOADERBUILDER_TEST_INPUT, + EXPRESSION_CLI_JSON_FILTERS, + ) + from expression_test_expected_output_dict import VIZPAYLOADERBUILDER_EXPECTED_OUTPUT + +else: + from expression_test_assets.expression_test_input_dict import ( + CLIEXPRESS_TEST_INPUT, + VIZPAYLOADERBUILDER_TEST_INPUT, + EXPRESSION_CLI_JSON_FILTERS, + ) + from expression_test_assets.expression_test_expected_output_dict import ( + VIZPAYLOADERBUILDER_EXPECTED_OUTPUT, + ) + + +class TestDXExtractExpression(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.maxDiff = None + test_project_name = "dx-toolkit_test_data" + cls.proj_id = list( + dxpy.find_projects(describe=False, level="VIEW", name=test_project_name) + )[0]["id"] + cd(cls.proj_id + ":/") + cls.general_input_dir = os.path.join(dirname, "expression_test_files/input/") + cls.general_output_dir = os.path.join(dirname, "expression_test_files/output/") + # Make an output directory if it doesn't already exists + if not os.path.exists(cls.general_output_dir): + os.makedirs(cls.general_output_dir) + cls.json_schema = EXTRACT_ASSAY_EXPRESSION_JSON_SCHEMA + cls.input_args_schema = EXTRACT_ASSAY_EXPRESSION_INPUT_ARGS_SCHEMA + cls.cohort_browser_record = ( + cls.proj_id + ":/Extract_Expression/cohort_browser_object" + ) + cls.expression_dataset_name = "molecular_expression1.dataset" + cls.expression_dataset = cls.proj_id + ":/" + cls.expression_dataset_name + cls.combined_expression_cohort_name = "Combined_Expression_Cohort" + cls.combined_expression_cohort = ( + cls.proj_id + ":/" + cls.combined_expression_cohort_name + ) + # In python3, str(type(object)) looks like <{0} 'obj_class'> but in python 2, it would be + # This impacts our expected error messages + cls.type_representation = "class" + if python_version == 2: + cls.type_representation = "type" + + cls.default_entity_describe = { + "id": cls.expression_dataset, + "project": cls.proj_id, + "class": "record", + "sponsored": False, + "name": "fake_assay", + "types": ["Dataset"], + "state": "closed", + "hidden": False, + "links": ["database-xxxx", "file-zzzzzz"], + "folder": "/", + "tags": [], + "created": 0, + "modified": 0, + "createdBy": { + "user": "user-test", + "job": "job-xyz", + "executable": "app-xyz", + }, + "size": 0, + "properties": {}, + "details": { + "descriptor": {"$dnanexus_link": "file-xyz"}, + "version": "3.0", + "schema": "ds-molecular_expression_quantification", + "databases": [{"assay": {"$dnanexus_link": "database-yyyyyy"}}], + "name": "fake_assay", + "description": "Dataset: assay", + }, + } + + cls.default_parser_dict = { + "apiserver_host": None, + "apiserver_port": None, + "apiserver_protocol": None, + "project_context_id": None, + "workspace_id": None, + "security_context": None, + "auth_token": None, + "env_help": None, + "version": None, + "command": "extract_assay", + "path": None, + "list_assays": False, + "retrieve_expression": False, + "additional_fields_help": False, + "assay_name": None, + "filter_json": None, + "filter_json_file": None, + "json_help": False, + "sql": False, + "additional_fields": None, + "expression_matrix": False, + "delim": None, + "output": None, + } + + cls.vizserver_data_mock_response = { + "results": [ + { + "feature_id": "ENST00000450305", + "sample_id": "sample_2", + "expression": 50, + "strand": "+", + }, + { + "feature_id": "ENST00000456328", + "sample_id": "sample_2", + "expression": 90, + "strand": "+", + }, + { + "feature_id": "ENST00000488147", + "sample_id": "sample_2", + "expression": 20, + "strand": "-", + }, + ] + } + cls.argparse_expression_help_message = os.path.join( + dirname, "help_messages/extract_expression_help_message.txt" + ) + + @classmethod + def input_arg_error_handler(cls, message): + raise ValueError(message) + + @classmethod + def json_error_handler(cls, message): + raise ValueError(message) + + @classmethod + def common_value_error_handler(cls, message): + raise ValueError(message) + + @classmethod + def tearDownClass(cls): + shutil.rmtree(cls.general_output_dir) + + # + # Helper functions used by different types of tests + # + + def common_negative_filter_test(self, json_name, expected_error_message): + input_json = CLIEXPRESS_TEST_INPUT["malformed"][json_name] + validator = JSONValidator( + self.json_schema, error_handler=self.json_error_handler + ) + + with self.assertRaises(ValueError) as cm: + validator.validate(input_json) + + self.assertEqual(expected_error_message, str(cm.exception).strip()) + + def common_positive_filter_test(self, json_name): + input_json = CLIEXPRESS_TEST_INPUT["valid"][json_name] + + validator = JSONValidator( + self.json_schema, error_handler=self.json_error_handler + ) + + validator.validate(input_json) + + def common_input_args_test(self, input_argument_dict, expected_error_message): + # Deep copy the default parser dictionary + parser_dict = {key: value for key, value in self.default_parser_dict.items()} + for input_argument in input_argument_dict: + if input_argument in self.default_parser_dict: + parser_dict[input_argument] = input_argument_dict[input_argument] + else: + print("unrecognized argument in input args") + return False + + input_arg_validator = ArgsValidator( + parser_dict, + EXTRACT_ASSAY_EXPRESSION_INPUT_ARGS_SCHEMA, + error_handler=self.input_arg_error_handler, + ) + with self.assertRaises(ValueError) as cm: + input_arg_validator.validate_input_combination() + + self.assertEqual(expected_error_message, str(cm.exception).strip()) + + # + # Expression matrix tests + # + + def test_basic_exp_matrix_transform(self): + vizserver_results = [ + { + "feature_id": "ENST00000450305", + "sample_id": "sample_2", + "expression": 50, + }, + { + "feature_id": "ENST00000456328", + "sample_id": "sample_2", + "expression": 90, + }, + { + "feature_id": "ENST00000488147", + "sample_id": "sample_2", + "expression": 20, + }, + ] + expected_output = [ + { + "ENST00000450305": 50, + "ENST00000456328": 90, + "ENST00000488147": 20, + "sample_id": "sample_2", + } + ] + + transformed_results, colnames = transform_to_expression_matrix( + vizserver_results + ) + self.assertEqual(expected_output, transformed_results) + + def test_two_sample_exp_transform(self): + vizserver_results = [ + { + "feature_id": "ENST00000450305", + "sample_id": "sample_2", + "expression": 50, + }, + { + "feature_id": "ENST00000456328", + "sample_id": "sample_1", + "expression": 90, + }, + { + "feature_id": "ENST00000488147", + "sample_id": "sample_2", + "expression": 20, + }, + ] + + expected_output = [ + { + "sample_id": "sample_2", + "ENST00000450305": 50, + "ENST00000488147": 20, + "ENST00000456328": None, + }, + { + "sample_id": "sample_1", + "ENST00000456328": 90, + "ENST00000450305": None, + "ENST00000488147": None, + }, + ] + + transformed_results, colnames = transform_to_expression_matrix( + vizserver_results + ) + self.assertEqual(expected_output, transformed_results) + + def test_two_sample_feat_id_overlap_exp_trans(self): + vizserver_results = [ + { + "feature_id": "ENST00000450305", + "sample_id": "sample_2", + "expression": 50, + }, + { + "feature_id": "ENST00000450305", + "sample_id": "sample_1", + "expression": 77, + }, + { + "feature_id": "ENST00000456328", + "sample_id": "sample_1", + "expression": 90, + }, + { + "feature_id": "ENST00000488147", + "sample_id": "sample_2", + "expression": 20, + }, + ] + expected_output = [ + { + "sample_id": "sample_2", + "ENST00000450305": 50, + "ENST00000488147": 20, + "ENST00000456328": None, + }, + { + "sample_id": "sample_1", + "ENST00000450305": 77, + "ENST00000456328": 90, + "ENST00000488147": None, + }, + ] + + transformed_results, colnames = transform_to_expression_matrix( + vizserver_results + ) + self.assertEqual(expected_output, transformed_results) + + def test_exp_transform_output_compatibility(self): + vizserver_results = [ + { + "feature_id": "ENST00000450305", + "sample_id": "sample_2", + "expression": 50, + }, + { + "feature_id": "ENST00000450305", + "sample_id": "sample_1", + "expression": 77, + }, + { + "feature_id": "ENST00000456328", + "sample_id": "sample_1", + "expression": 90, + }, + { + "feature_id": "ENST00000488147", + "sample_id": "sample_2", + "expression": 20, + }, + ] + + # The replace statement removes tabs(actually blocks of 4 spaces) that have been inserted + # for readability in this python file + expected_result = """sample_id,ENST00000450305,ENST00000456328,ENST00000488147 + sample_2,50,,20 + sample_1,77,90,""".replace( + " ", "" + ) + + transformed_results, colnames = transform_to_expression_matrix( + vizserver_results + ) + output_path = os.path.join(self.general_output_dir, "exp_transform_compat.csv") + # Generate the formatted output file + write_expression_output( + output_path, ",", False, transformed_results, colnames=colnames + ) + + with open(output_path, "r") as infile: + data = infile.read() + self.assertEqual(expected_result.strip(), data.strip()) + + # + # Positive output tests + # + + def test_output_data_format(self): + expected_result = """feature_id,sample_id,expression,strand + ENST00000450305,sample_2,50,+ + ENST00000456328,sample_2,90,+ + ENST00000488147,sample_2,20,-""".replace( + " ", "" + ) + if python_version == 2: + expected_result = "feature_id,expression,strand,sample_id\nENST00000450305,50,+,sample_2\nENST00000456328,90,+,sample_2\nENST00000488147,20,-,sample_2" + output_path = os.path.join( + self.general_output_dir, "extract_assay_expression_data.csv" + ) + # Generate the formatted output file + write_expression_output( + output_path, + ",", + False, + self.vizserver_data_mock_response["results"], + ) + # Read the output file back in and compare to expected result + # Since the test should fail if the formatting is wrong, not just if the data is wrong, we + # can do a simple string comparison + with open(output_path, "r") as infile: + data = infile.read() + self.assertEqual(expected_result.strip(), data.strip()) + + def test_output_sql_format(self): + sql_mock_response = { + "sql": "SELECT `expression_1`.`feature_id` AS `feature_id`, `expression_1`.`sample_id` AS `sample_id`, `expression_1`.`value` AS `expression`, `expr_annotation_1`.`strand` AS `strand` FROM `database_gypg8qq06j8kzzp2yybfbzfk__enst_short_multiple_assays2`.`expression` AS `expression_1` LEFT OUTER JOIN `database_gypg8qq06j8kzzp2yybfbzfk__enst_short_multiple_assays2`.`expr_annotation` AS `expr_annotation_1` ON `expression_1`.`feature_id` = `expr_annotation_1`.`feature_id` WHERE `expression_1`.`value` >= 1 AND `expr_annotation_1`.`chr` = '1' AND (`expr_annotation_1`.`end` BETWEEN 7 AND 250000000 OR `expr_annotation_1`.`start` BETWEEN 7 AND 250000000 OR `expr_annotation_1`.`end` >= 250000000 AND `expr_annotation_1`.`start` <= 7)" + } + expected_result = "SELECT `expression_1`.`feature_id` AS `feature_id`, `expression_1`.`sample_id` AS `sample_id`, `expression_1`.`value` AS `expression`, `expr_annotation_1`.`strand` AS `strand` FROM `database_gypg8qq06j8kzzp2yybfbzfk__enst_short_multiple_assays2`.`expression` AS `expression_1` LEFT OUTER JOIN `database_gypg8qq06j8kzzp2yybfbzfk__enst_short_multiple_assays2`.`expr_annotation` AS `expr_annotation_1` ON `expression_1`.`feature_id` = `expr_annotation_1`.`feature_id` WHERE `expression_1`.`value` >= 1 AND `expr_annotation_1`.`chr` = '1' AND (`expr_annotation_1`.`end` BETWEEN 7 AND 250000000 OR `expr_annotation_1`.`start` BETWEEN 7 AND 250000000 OR `expr_annotation_1`.`end` >= 250000000 AND `expr_annotation_1`.`start` <= 7)" + output_path = os.path.join( + self.general_output_dir, "extract_assay_expression_sql.csv" + ) + # Generate the formatted output file + write_expression_output( + arg_output=output_path, + arg_delim=",", + arg_sql=True, + output_listdict_or_string=sql_mock_response["sql"], + ) + # Read the output file back in and compare to expected result + # Since the test should fail if the formatting is wrong, not just if the data is wrong, we + # can do a simple string comparison + with open(output_path, "r") as infile: + data = infile.read() + self.assertEqual(expected_result.strip(), data.strip()) + + # + # Negative output tests + # + + def test_output_sql_not_string(self): + expected_error_message = "Expected SQL query to be a string" + with self.assertRaises(ValueError) as cm: + write_expression_output( + arg_output="-", + arg_delim=",", + arg_sql=True, + output_listdict_or_string=["not a string-formatted SQL query"], + error_handler=self.common_value_error_handler, + ) + err_msg = str(cm.exception).strip() + self.assertEqual(expected_error_message, err_msg) + + def test_output_bad_delimiter(self): + bad_delim = "|" + expected_error_message = "Unsupported delimiter: {}".format(bad_delim) + with self.assertRaises(ValueError) as cm: + write_expression_output( + arg_output="-", + arg_delim=bad_delim, + arg_sql=False, + output_listdict_or_string=self.vizserver_data_mock_response["results"], + save_uncommon_delim_to_txt=False, + error_handler=self.common_value_error_handler, + ) + err_msg = str(cm.exception).strip() + self.assertEqual(expected_error_message, err_msg) + + # EM-14 + def test_output_already_exist(self): + output_path = os.path.join( + self.general_output_dir, "already_existing_output.csv" + ) + expected_error_message = ( + "{} already exists. Please specify a new file path".format(output_path) + ) + + with open(output_path, "w") as outfile: + outfile.write("this output file already created") + + with self.assertRaises(ValueError) as cm: + write_expression_output( + arg_output=output_path, + arg_delim=",", + arg_sql=False, + output_listdict_or_string=self.vizserver_data_mock_response["results"], + save_uncommon_delim_to_txt=False, + error_handler=self.common_value_error_handler, + ) + + err_msg = str(cm.exception).strip() + self.assertEqual(expected_error_message, err_msg) + + def test_output_is_directory(self): + output_path = os.path.join(self.general_output_dir, "directory") + expected_error_message = ( + "{} is a directory. Please specify a new file path".format(output_path) + ) + os.mkdir(output_path) + with self.assertRaises(ValueError) as cm: + write_expression_output( + arg_output=output_path, + arg_delim=",", + arg_sql=False, + output_listdict_or_string=self.vizserver_data_mock_response["results"], + save_uncommon_delim_to_txt=False, + error_handler=self.common_value_error_handler, + ) + + err_msg = str(cm.exception).strip() + self.assertEqual(expected_error_message, err_msg) + + # EM-1 + # Test PATH argument not provided + def test_path_missing(self): + input_dict = {} + expected_error_message = ( + 'At least one of the following arguments is required: "Path", "--json-help"' + ) + self.common_input_args_test(input_dict, expected_error_message) + + # EM-10 + # When --additional-fields-help is presented with other options + def test_additional_fields_help_other_options(self): + expected_error_message = '"--additional-fields-help" cannot be passed with any option other than "--retrieve-expression".' + input_dict = { + "path": self.expression_dataset, + "assay_name": "test_assay", + "additional_fields_help": True, + } + self.common_input_args_test(input_dict, expected_error_message) + + # EM-11 + # When invalid additional fields are passed + def invalid_additional_fields(self): + expected_error_message = "One or more of the supplied fields using --additional-fields are invalid. Please run --additional-fields-help for a list of valid fields" + input_dict = { + "path": self.expression_dataset, + "retrieve_expression": True, + "filter_json": r'{"annotation": {"feature_id": ["ENSG0000001", "ENSG00000002"]}}', + "additional_fields": "feature_name,bad_field", + } + self.common_input_args_test(input_dict, expected_error_message) + + # EM-12 + # When –list-assays is presented with other options + def test_list_assays_assay_name(self): + expected_error_message = ( + '"--list-assays" cannot be presented with other options' + ) + input_dict = { + "path": self.expression_dataset, + "list_assays": True, + "assay_name": "fake_assay", + } + self.common_input_args_test(input_dict, expected_error_message) + + # EM-17 + # When the .json file provided does not exist + def test_json_file_not_exist(self): + missing_json_path = os.path.join(self.general_input_dir, "nonexistent.json") + expected_error_message = ( + "JSON file {} provided to --retrieve-expression does not exist".format( + missing_json_path + ) + ) + command = [ + "dx", + "extract_assay", + "expression", + self.expression_dataset, + "--retrieve-expression", + "--filter-json-file", + missing_json_path, + ] + + process = subprocess.Popen( + command, stderr=subprocess.PIPE, universal_newlines=True + ) + actual_err_msg = process.communicate()[1] + # print(actual_err_msg) + + if python_version == 2: + self.assertIn("No such file or directory", actual_err_msg) + else: + self.assertIn(expected_error_message, actual_err_msg) + + # EM-21 + # When --json-help is passed with another option from --assay-name, --sql, --additional-fields, --expression-matix, --output + def test_json_help_other_option(self): + expected_error_message = '"--json-help" cannot be passed with any option other than "--retrieve-expression".' + input_dict = { + "path": self.expression_dataset, + "json_help": True, + "assay_name": "test_assay", + } + self.common_input_args_test(input_dict, expected_error_message) + + # EM-23 + # --expression-matrix/-em cannot be used with --sql + def test_exp_matrix_sql(self): + expected_error_message = ( + '"--expression-matrix"/"-em" cannot be passed with the flag, "--sql".' + ) + input_dict = { + "path": self.expression_dataset, + "expression_matrix": True, + "retrieve_expression": True, + "filter_json": r'{"annotation": {"feature_name": ["BRCA2"]}}', + "sql": True, + } + self.common_input_args_test(input_dict, expected_error_message) + + # Malformed input json tests + # EM-18, EM-19, EM-20 + # + + def test_annotation_conflicting_keys(self): + self.common_negative_filter_test( + "annotation_conflicting_keys", + "For annotation, exactly one of feature_name or feature_id must be provided in the supplied JSON object.", + ) + + def test_annotation_id_type(self): + self.common_negative_filter_test( + "annotation_id_type", + "Key 'feature_id' has an invalid type. Expected <{0} 'list'> but got <{0} 'dict'>".format( + self.type_representation + ).format( + self.type_representation + ), + ) + + def test_annotation_name_type(self): + self.common_negative_filter_test( + "annotation_name_type", + "Key 'feature_name' has an invalid type. Expected <{0} 'list'> but got <{0} 'dict'>".format( + self.type_representation + ), + ) + + def test_annotation_type(self): + self.common_negative_filter_test( + "annotation_type", + "Key 'annotation' has an invalid type. Expected <{0} 'dict'> but got <{0} 'list'>".format( + self.type_representation + ), + ) + + def test_bad_dependent_conditional(self): + self.common_negative_filter_test( + "bad_dependent_conditional", + "When expression is present, one of the following keys must be also present: annotation, location.", + ) + + def test_bad_toplevel_key(self): + self.common_negative_filter_test( + "bad_toplevel_key", "Found following invalid filters: ['not_real_key']" + ) + + def test_conflicting_toplevel(self): + self.common_negative_filter_test( + "conflicting_toplevel", + "Exactly one of location or annotation must be provided in the supplied JSON object.", + ) + + # EM-15 + def test_empty_dict(self): + self.common_negative_filter_test( + "empty_dict", "Input JSON must be a non-empty dict." + ) + + def test_expression_max_type(self): + self.common_negative_filter_test( + "expression_max_type", + "Key 'max_value' has an invalid type. Expected (<{0} 'int'>, <{0} 'float'>) but got <{0} 'str'>".format( + self.type_representation + ), + ) + + def test_expression_min_type(self): + self.common_negative_filter_test( + "expression_min_type", + "Key 'min_value' has an invalid type. Expected (<{0} 'int'>, <{0} 'float'>) but got <{0} 'str'>".format( + self.type_representation + ), + ) + + def test_expression_type(self): + self.common_negative_filter_test( + "expression_type", + "Key 'expression' has an invalid type. Expected <{0} 'dict'> but got <{0} 'list'>".format( + self.type_representation + ), + ) + + def test_location_chrom_type(self): + self.common_negative_filter_test( + "location_chrom_type", + "Key 'chromosome' has an invalid type. Expected <{0} 'str'> but got <{0} 'int'>".format( + self.type_representation + ), + ) + + def test_location_end_type(self): + self.common_negative_filter_test( + "location_end_type", + "Key 'ending_position' has an invalid type. Expected <{0} 'str'> but got <{0} 'int'>".format( + self.type_representation + ), + ) + + def test_location_item_type(self): + self.common_negative_filter_test( + "location_item_type", + "Expected list items within 'location' to be of type <{0} 'dict'> but got <{0} 'list'> instead.".format( + self.type_representation + ), + ) + + def test_location_missing_chr(self): + self.common_negative_filter_test( + "location_missing_chr", + "Required key 'chromosome' was not found in the input JSON.", + ) + + def test_location_missing_end(self): + self.common_negative_filter_test( + "location_missing_end", + "Required key 'ending_position' was not found in the input JSON.", + ) + + def test_location_missing_start(self): + self.common_negative_filter_test( + "location_missing_start", + "Required key 'starting_position' was not found in the input JSON.", + ) + + def test_location_start_type(self): + self.common_negative_filter_test( + "location_start_type", + "Key 'starting_position' has an invalid type. Expected <{0} 'str'> but got <{0} 'int'>".format( + self.type_representation + ), + ) + + def test_location_type(self): + self.common_negative_filter_test( + "location_type", + "Key 'location' has an invalid type. Expected <{0} 'list'> but got <{0} 'dict'>".format( + self.type_representation + ), + ) + + def test_sample_id_type(self): + self.common_negative_filter_test( + "sample_id_type", + "Key 'sample_id' has an invalid type. Expected <{0} 'list'> but got <{0} 'dict'>".format( + self.type_representation + ), + ) + + # + # Correct JSON inputs + # + + def test_annotation_feature_id(self): + self.common_positive_filter_test("annotation_feature_id") + + def test_annotation_feature_name(self): + self.common_positive_filter_test("annotation_feature_name") + + def test_dependent_conditional_annotation(self): + self.common_positive_filter_test("dependent_conditional_annotation") + + def test_dependent_conditional_location(self): + self.common_positive_filter_test("dependent_conditional_location") + + def test_expression_max_only(self): + self.common_positive_filter_test("expression_max_only") + + def test_expression_min_and_max(self): + self.common_positive_filter_test("expression_min_and_max") + + def test_expression_min_only(self): + self.common_positive_filter_test("expression_min_only") + + def test_multi_location(self): + self.common_positive_filter_test("multi_location") + + def test_single_location(self): + self.common_positive_filter_test("single_location") + + ##### Test argparse's --help output + def test_argparse_help_txt(self): + expected_result = self.argparse_expression_help_message + with open(expected_result) as f: + # lines = f.readlines() + file = f.read() + process = subprocess.check_output("dx extract_assay expression -h", shell=True) + help_output = process.decode() + + # In Python 3 self.assertEqual(file,help_output) passes, + # However in Python 2 it fails due to some differences in where linebreaks appear in the text + self.assertEqual( + file.replace(" ", "").replace("\n", ""), + help_output.replace(" ", "").replace("\n", ""), + ) + + #### Test --json-help + def test_json_help_template(self): + process = subprocess.check_output( + "dx extract_assay expression --retrieve-expression fakepath --json-help", + shell=True, + ) + self.assertIn(EXTRACT_ASSAY_EXPRESSION_JSON_TEMPLATE, process.decode()) + self.assertIn( + "Additional descriptions of filtering keys and permissible values", + process.decode(), + ) + + def load_record_via_dataset_class(self, record_path): + _, _, entity = resolve_existing_path(record_path) + entity_describe = entity["describe"] + record = DXRecord(entity_describe["id"], entity_describe["project"]) + dataset, cohort_info = Dataset.resolve_cohort_to_dataset(record) + + return dataset, cohort_info, record + + def test_dataset_class_basic(self): + dataset, cohort, record = self.load_record_via_dataset_class( + self.expression_dataset + ) + + record_details = record.describe( + default_fields=True, fields={"properties", "details"} + ) + + self.assertIsNone(cohort) + self.assertEqual( + dataset.descriptor_file_dict["name"], self.expression_dataset_name + ) + self.assertIn("vizserver", dataset.visualize_info["url"]) + self.assertEqual("3.0", dataset.visualize_info["version"]) + self.assertEqual("3.0", dataset.visualize_info["datasetVersion"]) + self.assertEqual( + dataset.descriptor_file, + record_details["details"]["descriptor"]["$dnanexus_link"], + ) + self.assertIn( + "molecular_expression1", dataset.assay_names_list("molecular_expression") + ) + self.assertEqual(dataset.detail_describe["types"], record_details["types"]) + + def test_dataset_class_cohort_resolution(self): + dataset, cohort, record = self.load_record_via_dataset_class( + self.combined_expression_cohort + ) + + record_details = record.describe( + default_fields=True, fields={"properties", "details"} + ) + expected_dataset_id = record_details["details"]["dataset"]["$dnanexus_link"] + expected_dataset_describe = DXRecord(expected_dataset_id).describe( + default_fields=True, fields={"properties", "details"} + ) + expected_descriptor_id = expected_dataset_describe["details"]["descriptor"][ + "$dnanexus_link" + ] + + self.assertIsNotNone(cohort) + self.assertIn("SELECT `sample_id`", cohort["details"]["baseSql"]) + self.assertIn("pheno_filters", cohort["details"]["filters"]) + self.assertIn("CohortBrowser", cohort["types"]) + self.assertEqual(dataset.get_id(), expected_dataset_id) + self.assertEqual(dataset.descriptor_file, expected_descriptor_id) + self.assertIn( + "molecular_expression1", dataset.assay_names_list("molecular_expression") + ) + self.assertEqual( + "molecular_expression", + dataset.descriptor_file_dict["assays"][0]["generalized_assay_model"], + ) + self.assertIn("Dataset", dataset.detail_describe["types"]) + self.assertIn("vizserver", dataset.vizserver_url) + + ### Test VizPayloadBuilder Class + + # Genomic location filters + # genomic + cohort + def test_vizpayloadbuilder_location_cohort(self): + self.common_vizpayloadbuilder_test_helper_method( + self.combined_expression_cohort, "test_vizpayloadbuilder_location_cohort" + ) + + def test_vizpayloadbuilder_location_multiple(self): + self.common_vizpayloadbuilder_test_helper_method( + self.expression_dataset, "test_vizpayloadbuilder_location_multiple" + ) + + # Annotation filters + def test_vizpayloadbuilder_annotation_feature_name(self): + self.common_vizpayloadbuilder_test_helper_method( + self.expression_dataset, "test_vizpayloadbuilder_annotation_feature_name" + ) + + def test_vizpayloadbuilder_annotation_feature_id(self): + self.common_vizpayloadbuilder_test_helper_method( + self.expression_dataset, "test_vizpayloadbuilder_annotation_feature_id" + ) + + # Expression filters (with location or annotation) + # expression + annotation - ID + def test_vizpayloadbuilder_expression_min(self): + self.common_vizpayloadbuilder_test_helper_method( + self.expression_dataset, "test_vizpayloadbuilder_expression_min" + ) + + # expression + annotation - name + def test_vizpayloadbuilder_expression_max(self): + self.common_vizpayloadbuilder_test_helper_method( + self.expression_dataset, "test_vizpayloadbuilder_expression_max" + ) + + # expression + location + def test_vizpayloadbuilder_expression_mixed(self): + self.common_vizpayloadbuilder_test_helper_method( + self.expression_dataset, "test_vizpayloadbuilder_expression_mixed" + ) + + # Sample filter + def test_vizpayloadbuilder_sample(self): + self.common_vizpayloadbuilder_test_helper_method( + self.expression_dataset, "test_vizpayloadbuilder_sample", data_test=False + ) + + # General (mixed) filters + def test_vizpayloadbuilder_location_sample_expression(self): + if python_version == 2: + # The expected query is essentially the same as the one in Python 3 + # The only issue is that the order of sub-queries is slightly different in Python 2 + # This is very likely due to the fact that Python 2 changes the order of keys in payload dict + # Therefore, the final query is constructred slightly differently + self.assertTrue(True) + else: + self.common_vizpayloadbuilder_test_helper_method( + self.expression_dataset, + "test_vizpayloadbuilder_location_sample_expression", + data_test=False, + ) + + def test_vizpayloadbuilder_annotation_sample_expression(self): + if python_version == 2: + # The expected query is essentially the same as the one in Python 3 + # The only issue is that the order of sub-queries is slightly different in Python 2 + # This is very likely due to the fact that Python 2 changes the order of keys in payload dict + # Therefore, the final query is constructred slightly differently + self.assertTrue(True) + else: + self.common_vizpayloadbuilder_test_helper_method( + self.expression_dataset, + "test_vizpayloadbuilder_annotation_sample_expression", + data_test=False, + ) + + def common_vizpayloadbuilder_test_helper_method( + self, record_path, test_name, data_test=True + ): + _, _, entity = resolve_existing_path(record_path) + entity_describe = entity["describe"] + record_id = entity_describe["id"] + + record = DXRecord(record_id) + dataset, cohort_info = Dataset.resolve_cohort_to_dataset(record) + dataset_id = dataset.dataset_id + + if cohort_info: + BASE_SQL = cohort_info.get("details").get("baseSql") + COHORT_FILTERS = cohort_info.get("details").get("filters") + IS_COHORT = True + else: + BASE_SQL = None + COHORT_FILTERS = None + IS_COHORT = False + + url = dataset.vizserver_url + project = dataset.project_id + + # vizserver_filters_from_json_parser.JSONFiltersValidator using the CLIEXPRESS schema + schema = EXTRACT_ASSAY_EXPRESSION_FILTERING_CONDITIONS + _db_columns_list = schema["output_fields_mapping"].get("default") + + # JSONFiltersValidator to build the complete payload + json_input = VIZPAYLOADERBUILDER_TEST_INPUT[test_name] + input_json_parser = JSONFiltersValidator(json_input, schema) + vizserver_raw_filters = input_json_parser.parse() + + # VizClient to submit the payload and get a response + client = VizClient(url, project) + + viz = VizPayloadBuilder( + project_context=project, + output_fields_mapping=_db_columns_list, + filters={"filters": COHORT_FILTERS} if IS_COHORT else None, + order_by=EXTRACT_ASSAY_EXPRESSION_FILTERING_CONDITIONS["order_by"], + limit=None, + base_sql=BASE_SQL, + is_cohort=IS_COHORT, + error_handler=err_exit, + ) + + assay_1_name = dataset.descriptor_file_dict["assays"][0]["name"] + assay_1_id = dataset.descriptor_file_dict["assays"][0]["uuid"] + + viz.assemble_assay_raw_filters( + assay_name=assay_1_name, assay_id=assay_1_id, filters=vizserver_raw_filters + ) + vizserver_payload = viz.build() + + vizserver_response_data = client.get_data(vizserver_payload, dataset_id)[ + "results" + ] + vizserver_response_sql = client.get_raw_sql(vizserver_payload, dataset_id)[ + "sql" + ] + + data_output = vizserver_response_data + sql_output = vizserver_response_sql + + if data_test: + exp_data_output = VIZPAYLOADERBUILDER_EXPECTED_OUTPUT[test_name][ + "expected_data_output" + ] + # assertCountEqual asserts that two iterables have the same elements, ignoring order + self.assertCountEqual(data_output, exp_data_output) + + exp_sql_output = VIZPAYLOADERBUILDER_EXPECTED_OUTPUT[test_name][ + "expected_sql_output" + ] + if isinstance(exp_sql_output, list): + # Some of the sub-queries may have slightly different order due to the way the keys are ordered in the payload dict + # In other words, the queries are still correct, but the order of sub-queries may be different + # This usually happens in Python 2 + self.assertIn(sql_output, exp_sql_output) + else: + self.assertEqual(sql_output, exp_sql_output) + + def run_dx_extract_assay_expression_cmd( + self, + dataset_or_cohort, + filters_json, + additional_fields, + sql, + output="-", + extra_args=None, + subprocess_run=False, + ): + command = [ + "dx", + "extract_assay", + "expression", + dataset_or_cohort, + "--retrieve-expression", + "--filter-json", + str(filters_json).replace("'", '"'), + "-o", + output, + ] + + if sql: + command.append("--sql") + + if additional_fields: + command.extend(["--additional-fields", additional_fields]) + + if extra_args: + command.append(extra_args) + + if subprocess_run: + process = subprocess.run( + command, capture_output=True, text=True, check=False + ) + + else: + process = subprocess.check_output( + command, + universal_newlines=True, + ) + + return process + + def test_dx_extract_cmd_location_expression_sample_sql(self): + expected_sql_query = [ + "SELECT `expression_1`.`feature_id` AS `feature_id`, `expression_1`.`sample_id` AS `sample_id`, `expression_1`.`value` AS `expression`, `expr_annotation_1`.`gene_name` AS `feature_name`, `expr_annotation_1`.`chr` AS `chrom`, `expr_annotation_1`.`start` AS `start` FROM `database_gzky7400vgpyzy621q43gkkf__molecular_expression1_db`.`expression` AS `expression_1` LEFT OUTER JOIN `database_gzky7400vgpyzy621q43gkkf__molecular_expression1_db`.`expr_annotation` AS `expr_annotation_1` ON `expression_1`.`feature_id` = `expr_annotation_1`.`feature_id` WHERE (`expr_annotation_1`.`chr` = '11' AND (`expr_annotation_1`.`start` BETWEEN 8693350 AND 67440200 OR `expr_annotation_1`.`end` BETWEEN 8693350 AND 67440200 OR `expr_annotation_1`.`start` <= 8693350 AND `expr_annotation_1`.`end` >= 67440200) OR `expr_annotation_1`.`chr` = 'X' AND (`expr_annotation_1`.`start` BETWEEN 148500700 AND 148994424 OR `expr_annotation_1`.`end` BETWEEN 148500700 AND 148994424 OR `expr_annotation_1`.`start` <= 148500700 AND `expr_annotation_1`.`end` >= 148994424) OR `expr_annotation_1`.`chr` = '17' AND (`expr_annotation_1`.`start` BETWEEN 75228160 AND 75235759 OR `expr_annotation_1`.`end` BETWEEN 75228160 AND 75235759 OR `expr_annotation_1`.`start` <= 75228160 AND `expr_annotation_1`.`end` >= 75235759)) AND `expression_1`.`value` >= 25.63 AND `expression_1`.`sample_id` IN ('sample_1', 'sample_2') ORDER BY `feature_id` ASC, `sample_id` ASC", + "SELECT `expression_1`.`feature_id` AS `feature_id`, `expression_1`.`sample_id` AS `sample_id`, `expression_1`.`value` AS `expression`, `expr_annotation_1`.`gene_name` AS `feature_name`, `expr_annotation_1`.`chr` AS `chrom`, `expr_annotation_1`.`start` AS `start` FROM `database_gzky7400vgpyzy621q43gkkf__molecular_expression1_db`.`expression` AS `expression_1` LEFT OUTER JOIN `database_gzky7400vgpyzy621q43gkkf__molecular_expression1_db`.`expr_annotation` AS `expr_annotation_1` ON `expression_1`.`feature_id` = `expr_annotation_1`.`feature_id` WHERE (`expr_annotation_1`.`chr` = '11' AND (`expr_annotation_1`.`end` BETWEEN 8693350 AND 67440200 OR `expr_annotation_1`.`start` BETWEEN 8693350 AND 67440200 OR `expr_annotation_1`.`end` >= 67440200 AND `expr_annotation_1`.`start` <= 8693350) OR `expr_annotation_1`.`chr` = 'X' AND (`expr_annotation_1`.`end` BETWEEN 148500700 AND 148994424 OR `expr_annotation_1`.`start` BETWEEN 148500700 AND 148994424 OR `expr_annotation_1`.`end` >= 148994424 AND `expr_annotation_1`.`start` <= 148500700) OR `expr_annotation_1`.`chr` = '17' AND (`expr_annotation_1`.`end` BETWEEN 75228160 AND 75235759 OR `expr_annotation_1`.`start` BETWEEN 75228160 AND 75235759 OR `expr_annotation_1`.`end` >= 75235759 AND `expr_annotation_1`.`start` <= 75228160)) AND `expression_1`.`value` >= 25.63 AND `expression_1`.`sample_id` IN ('sample_1', 'sample_2') ORDER BY `feature_id` ASC, `sample_id` ASC", + ] + response = self.run_dx_extract_assay_expression_cmd( + self.expression_dataset, + EXPRESSION_CLI_JSON_FILTERS["positive_test"]["location_expression_sample"], + "chrom,start,feature_name", + True, + "-", + ) + self.assertIn(response.strip(), expected_sql_query) + + def test_dx_extract_cmd_location_expression_sample_data(self): + response = self.run_dx_extract_assay_expression_cmd( + self.expression_dataset, + EXPRESSION_CLI_JSON_FILTERS["positive_test"]["location_expression_sample"], + "chrom,start,feature_name", + False, + "-", + ) + response = response.splitlines() + response_list = [s.split(",") for s in response] + column_names = response_list[0] + response_df = pd.DataFrame(response_list[1:], columns=column_names) + + expected_present_row = response_df.loc[ + (response_df["feature_id"] == "ENST00000683201") + & (response_df["expression"] == "27") + & (response_df["start"] == "57805541") + & (response_df["chrom"] == "11") + & (response_df["feature_name"] == "CTNND1") + & (response_df["sample_id"] == "sample_2") + ] + + expected_X_chrom_response = response_df.loc[ + (response_df["chrom"] == "X") & (response_df["sample_id"] == "sample_2") + ] + + self.assertEqual(len(response_df), 9398) + self.assertEqual( + set(column_names), + set( + [ + "feature_id", + "sample_id", + "expression", + "feature_name", + "chrom", + "start", + ] + ), + ) + self.assertEqual(set(response_df.sample_id), set(["sample_1", "sample_2"])) + self.assertEqual(len(set(response_df.feature_id.unique())), 5929) + self.assertEqual(len(expected_present_row), 1) + self.assertEqual(len(expected_X_chrom_response), 5) + + def test_dx_extract_cmd_sample_ids_with_additional_fields(self): + expected_sql_query = "SELECT `expression_1`.`feature_id` AS `feature_id`, `expression_1`.`sample_id` AS `sample_id`, `expression_1`.`value` AS `expression`, `expr_annotation_1`.`gene_name` AS `feature_name`, `expr_annotation_1`.`chr` AS `chrom`, `expr_annotation_1`.`start` AS `start`, `expr_annotation_1`.`end` AS `end`, `expr_annotation_1`.`strand` AS `strand` FROM `database_gzky7400vgpyzy621q43gkkf__molecular_expression1_db`.`expression` AS `expression_1` LEFT OUTER JOIN `database_gzky7400vgpyzy621q43gkkf__molecular_expression1_db`.`expr_annotation` AS `expr_annotation_1` ON `expression_1`.`feature_id` = `expr_annotation_1`.`feature_id` WHERE `expression_1`.`sample_id` IN ('sample_1') ORDER BY `feature_id` ASC, `sample_id` ASC" + response = self.run_dx_extract_assay_expression_cmd( + self.expression_dataset, + EXPRESSION_CLI_JSON_FILTERS["positive_test"][ + "sample_id_with_additional_fields" + ], + "chrom,start,end,strand,feature_name", + True, + "-", + ) + self.assertEqual(response.strip(), expected_sql_query) + + def test_negative_dx_extract_cmd_empty_json(self): + expected_error = "No filter JSON is passed with --retrieve-expression or input JSON for --retrieve-expression does not contain valid filter information." + response = self.run_dx_extract_assay_expression_cmd( + self.expression_dataset, + EXPRESSION_CLI_JSON_FILTERS["negative_test"]["empty_json"], + None, + True, + "-", + subprocess_run=True, + ) + self.assertIn(expected_error, response.stderr) + + def test_negative_dx_extract_cmd_invalid_location_range(self): + expected_error = "Range cannot be greater than 250000000 for location" + response = self.run_dx_extract_assay_expression_cmd( + self.expression_dataset, + EXPRESSION_CLI_JSON_FILTERS["negative_test"]["large_location_range"], + None, + False, + subprocess_run=True, + ) + self.assertIn(expected_error, response.stderr) + + def test_negative_dx_extract_cmd_too_many_sample_ids(self): + expected_error = "Too many items given in field sample_id, maximum is 100" + response = self.run_dx_extract_assay_expression_cmd( + self.expression_dataset, + EXPRESSION_CLI_JSON_FILTERS["negative_test"]["sample_id_maxitem_limit"], + None, + False, + subprocess_run=True, + ) + self.assertIn(expected_error, response.stderr) + + +# Start the test +if __name__ == "__main__": + unittest.main() diff --git a/src/python/test/test_extract_somatic.py b/src/python/test/test_extract_somatic.py new file mode 100644 index 0000000000..c61128477f --- /dev/null +++ b/src/python/test/test_extract_somatic.py @@ -0,0 +1,445 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (C) 2023 DNAnexus, Inc. +# +# This file is part of dx-toolkit (DNAnexus platform client libraries). +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy +# of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +# Run manually with python3 src/python/test/test_extract_somatic.py + +# Unit testing for the dx extract_assay somatic command +# Similar to the dx extract_assay germline tests + + +import dxpy +import unittest +import os +import sys +import subprocess +import shutil + +from dxpy_testutil import cd +from dxpy.cli.dataset_utilities import ( + get_assay_name_info, + resolve_validate_record_path, + DXDataset, +) +from dxpy.dx_extract_utils.somatic_filter_payload import ( + basic_filter, + location_filter, + generate_assay_filter, + somatic_final_payload, +) + +dirname = os.path.dirname(__file__) + +python_version = sys.version_info.major + +class TestDXExtractSomatic(unittest.TestCase): + @classmethod + def setUpClass(cls): + test_project_name = "dx-toolkit_test_data" + cls.proj_id = list( + dxpy.find_projects(describe=False, level="VIEW", name=test_project_name) + )[0]["id"] + cd(cls.proj_id + ":/") + cls.general_input_dir = os.path.join(dirname, "clisam_test_filters/input/") + cls.general_output_dir = os.path.join(dirname, "clisam_test_filters/output/") + + # + # Select test suite + # + cls.dataset = "single_assay" + + if cls.dataset == "single_assay": + # Single assay + cls.test_record = "{}:/Extract_Assay_Somatic/test_single_assay_202306231200_new".format( + test_project_name + ) + elif cls.dataset == "multi_assay_sciprod_1347_v2": + # multi assay dataset + cls.test_record = ( + "{}:/Extract_Assay_Somatic/test_datasets/SCIPROD-1347/sciprod_1347_v2".format( + test_project_name + ) + ) + elif cls.dataset == "small_original": + cls.test_record = "{}:test_datasets/assay_title_annot_complete".format(test_project_name) + + cls.e2e_filter_directory = os.path.join(cls.general_input_dir, cls.dataset, "e2e") + cls.e2e_output_directory = os.path.join(cls.general_output_dir, cls.dataset, "e2e_output") + + # Ensure output directories exist + if not os.path.exists(cls.e2e_output_directory): + os.makedirs(cls.e2e_output_directory) + + @classmethod + def tearDownClass(cls): + shutil.rmtree(cls.general_output_dir) + + ############ + # Unit Tests + ############ + + def test_get_assay_name_info(self): + # Set to true for the list assay utilities response instead of the normal functionality + list_assays = False + # When assay name is none, function looks for and selects first assay of type somatic that it finds + assay_name = None + friendly_assay_type = "somatic" + project, entity_result, resp, dataset_project = resolve_validate_record_path( + self.test_record + ) + dataset_id = resp["dataset"] + rec_descriptor = DXDataset(dataset_id, project=dataset_project).get_descriptor() + # Expected Results + expected_assay_name = "test_single_assay_202306231200" + expected_assay_id = "5c359e55-0639-46bc-bbf3-5eb22d5a5780" + expected_ref_genome = "GRCh38.109" + expected_additional_descriptor_info = {} + + ( + selected_assay_name, + selected_assay_id, + selected_ref_genome, + additional_descriptor_info, + ) = get_assay_name_info( + list_assays=False, + assay_name=assay_name, + path=self.test_record, + friendly_assay_type=friendly_assay_type, + rec_descriptor=rec_descriptor, + ) + + self.assertEqual(expected_assay_name, selected_assay_name) + self.assertEqual(expected_assay_id, selected_assay_id) + self.assertEqual(expected_ref_genome, selected_ref_genome) + self.assertEqual(expected_additional_descriptor_info, additional_descriptor_info) + + def test_basic_filter(self): + print("testing basic filter") + table = "variant_read_optimized" + friendly_name = "allele_id" + values = ["chr21_40590995_C_C"] + project_context = None + genome_reference = None + + expected_output = { + "variant_read_optimized$allele_id": [ + {"condition": "in", "values": ["chr21_40590995_C_C"]} + ] + } + + self.assertEqual( + basic_filter(table, friendly_name, values), + expected_output, + ) + + def test_location_filter(self): + print("testing location filter") + raw_location_list = [ + { + "chromosome": "chr21", + "starting_position": "100", + "ending_position": "50000000", + } + ] + expected_output = { + "variant_read_optimized$allele_id": [ + { + "condition": "in", + "values": [], + "geno_bins": [ + { + "chr": "21", + "start": 100, + "end": 50000000 + } + ] + }, + ] + } + expected_chrom = ['chr21'] + loc_filter, chrom = location_filter(raw_location_list) + self.assertEqual(loc_filter, expected_output) + self.assertEqual(chrom, expected_chrom) + + def test_generate_assay_filter(self): + print("testing generate assay filter") + full_input_dict = {"allele": {"allele_id": ["chr21_40590995_C_C"]}} + name = "test_single_assay_202306231200" + id = "0c69a39f-a34f-4030-a866-5056c8112da4" + project_context = "project-GX0Jpp00ZJ46qYPq5G240k1k" + expected_output = { + "assay_filters": { + "name": "test_single_assay_202306231200", + "id": "0c69a39f-a34f-4030-a866-5056c8112da4", + "logic": "and", + "filters": { + "variant_read_optimized$allele_id": [ + {"condition": "in", "values": ["chr21_40590995_C_C"]} + ], + "variant_read_optimized$tumor_normal": [ + {"condition": "is", "values": "tumor"} + ], + } + } + } + self.assertEqual( + generate_assay_filter(full_input_dict, name, id, project_context), + expected_output, + ) + + def test_somatic_final_payload(self): + print("testing somatic final payload") + full_input_dict = {"allele": {"allele_id": ["chr21_40590995_C_C"]}} + name = "test_single_assay_202306231200" + id = "0c69a39f-a34f-4030-a866-5056c8112da4" + project_context = "project-GX0Jpp00ZJ46qYPq5G240k1k" + expected_output = { + "project_context": "project-GX0Jpp00ZJ46qYPq5G240k1k", + "fields": [ + {"assay_sample_id": "variant_read_optimized$assay_sample_id"}, + {"allele_id": "variant_read_optimized$allele_id"}, + {"CHROM": "variant_read_optimized$CHROM"}, + {"POS": "variant_read_optimized$POS"}, + {"REF": "variant_read_optimized$REF"}, + {"allele": "variant_read_optimized$allele"}, + ], + "order_by": [ + {"CHROM":"asc"}, + {"POS":"asc"}, + {"allele_id":"asc"}, + {"assay_sample_id":"asc"} + ], + "raw_filters": { + "assay_filters": { + "name": "test_single_assay_202306231200", + "id": "0c69a39f-a34f-4030-a866-5056c8112da4", + "logic": "and", + "filters": { + "variant_read_optimized$allele_id": [ + { + "condition": "in", + "values": ["chr21_40590995_C_C"], + } + ], + "variant_read_optimized$tumor_normal": [ + {"condition": "is", "values": "tumor"} + ], + } + } + }, + "distinct": True, + "adjust_geno_bins": False + } + expected_output_fields = [ + "assay_sample_id", + "allele_id", + "CHROM", + "POS", + "REF", + "allele", + ] + test_payload, test_fields = somatic_final_payload(full_input_dict, name, id, project_context, genome_reference=None, additional_fields=None, include_normal=False) + self.assertEqual(test_payload, expected_output) + self.assertEqual(test_fields, expected_output_fields) + + def test_somatic_final_payload_location(self): + print("testing somatic final payload with location filter") + full_input_dict = {"location":[{"chromosome":"chrUn_JTFH01000732v1_decoy","starting_position":"40", "ending_position":"45"}]} + name = "assay_dummy" + id = "id_dummy" + project_context = "project-dummy" + expected_output = { + "filters": { + "variant_read_optimized$allele_id": [ + { + "condition": "in", + "values": [], + "geno_bins": [ + { + "chr": "Other", + "start": 40, + "end": 45 + } + ] + } + ], + "variant_read_optimized$CHROM": [ + { + "condition": "in", + "values": [ + "chrUn_JTFH01000732v1_decoy" + ] + } + ], + "variant_read_optimized$tumor_normal": [ + { + "condition": "is", + "values": "tumor" + } + ] + } + } + + test_payload, _ = somatic_final_payload(full_input_dict, name, id, project_context, genome_reference=None, additional_fields=None, include_normal=False) + self.assertEqual(test_payload["raw_filters"]["assay_filters"]["filters"], expected_output["filters"]) + + def test_multiple_empty_required_keys(self): + print("testing multiple empty required keys") + full_input_dict = {"location":[{"chromosome":"chr21","starting_position":"40", "ending_position":"45"}], + "allele": {"allele_id": []}, + "annotation": {"gene": [], "symbol": [], "feature": []}} + name = "assay_dummy" + id = "id_dummy" + project_context = "project-dummy" + expected_output = { + "filters": { + "variant_read_optimized$allele_id": [ + { + "condition": "in", + "values": [], + "geno_bins": [ + { + "chr": "21", + "start": 40, + "end": 45 + } + ] + } + ], + "variant_read_optimized$CHROM": [ + { + "condition": "in", + "values": [ + "chr21" + ] + } + ], + "variant_read_optimized$tumor_normal": [ + { + "condition": "is", + "values": "tumor" + } + ] + } + } + test_payload, _ = somatic_final_payload(full_input_dict, name, id, project_context, genome_reference=None, additional_fields=None, include_normal=False) + self.assertEqual(test_payload["raw_filters"]["assay_filters"]["filters"], expected_output["filters"]) + + def test_additional_fields(self): + print("testing --additional-fields") + input_filter_path = os.path.join(self.e2e_filter_directory, "single_location.json") + output_path = os.path.join( + self.general_output_dir, self.dataset, "e2e_output", "additional_fields_output.tsv" + ) + + command = 'dx extract_assay somatic {} --retrieve-variant {} --output {} --additional-fields "{}"'.format( + self.test_record, + input_filter_path, + output_path, + "sample_id,tumor_normal,symbolic_type", + ) + + process = subprocess.check_output(command, shell=True) + + def test_tumor_normal(self): + print("testing --include-normal-sample") + input_filter_path = os.path.join(self.e2e_filter_directory, "single_location.json") + output_path = os.path.join( + self.general_output_dir, self.dataset, "e2e_output", "tumor_normal_output.tsv" + ) + + command = 'dx extract_assay somatic {} --retrieve-variant {} --output {} --include-normal-sample --additional-fields "{}"'.format( + self.test_record, + input_filter_path, + output_path, + "sample_id,tumor_normal", + ) + + process = subprocess.check_output(command, shell=True) + + def test_retrieve_meta_info(self): + print("testing --retrieve-meta-info") + expected_result = b"e79cdc96ab517d8d3eebafa8ffe4469b -\n" + + if python_version == 2: + # subprocess pipe doesn't work with python 2, just check to make sure the command runs in that case + command = "dx extract_assay somatic {} --retrieve-meta-info --output - > /dev/null".format(self.test_record) + #print(command) + process = subprocess.check_output(command,shell=True) + else: + with subprocess.Popen( + ["dx", "extract_assay", "somatic", self.test_record, "--retrieve-meta-info", "--output", "-"], + stdout=subprocess.PIPE, + ) as p1: + p2 = subprocess.Popen( + ["md5sum"], stdin=p1.stdout, stdout=subprocess.PIPE + ) + out, err = p2.communicate() + + self.assertEqual(expected_result, out) + + #### + # Input validation test + #### + + def test_malformed_json(self): + # For somatic assays, json validation is not in a single function + malformed_json_dir = os.path.join(self.general_input_dir, "malformed_json") + malformed_json_filenames = os.listdir(malformed_json_dir) + for name in malformed_json_filenames: + filter_path = os.path.join(malformed_json_dir, name) + command = "dx extract_assay somatic {} --retrieve-variant {}".format( + self.test_record, + os.path.join(malformed_json_dir, filter_path), + ) + try: + process = subprocess.check_output( + command, + shell=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + print("Uh oh, malformed JSON passed detection, file is {}".format(name)) + except: + print("malformed json {} detected succesfully".format(name)) + + ##### + # E2E tests + ##### + + def test_e2e_filters(self): + print("Testing e2e filters") + filter_files = os.listdir(self.e2e_filter_directory) + + for filter_name in filter_files: + print("testing {}".format(filter_name)) + output_filename = filter_name[:-5] + "_output.tsv" + command = ( + "dx extract_assay somatic {} --retrieve-variant {} --output {}".format( + self.test_record, + os.path.join(self.e2e_filter_directory, filter_name), + os.path.join(self.e2e_output_directory, output_filename), + ) + ) + process = subprocess.check_call(command, shell=True) + # print(command) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/python/test/test_nextflow.py b/src/python/test/test_nextflow.py new file mode 100755 index 0000000000..11e29baf39 --- /dev/null +++ b/src/python/test/test_nextflow.py @@ -0,0 +1,564 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (C) 2013-2016 DNAnexus, Inc. +# +# This file is part of dx-toolkit (DNAnexus platform client libraries). +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy +# of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +from __future__ import print_function, unicode_literals, division, absolute_import +from parameterized import parameterized + +import tempfile +import shutil +import os +import sys +import unittest +import json +from dxpy.nextflow.nextflow_templates import get_nextflow_src, get_nextflow_dxapp +from dxpy.nextflow.nextflow_utils import get_template_dir +from dxpy.nextflow.collect_images import bundle_docker_images + +import uuid +from dxpy_testutil import (DXTestCase, DXTestCaseBuildNextflowApps, run, chdir) +import dxpy_testutil as testutil +from dxpy.compat import USING_PYTHON2, str, sys_encoding, open +from dxpy.utils.resolver import ResolutionError +import dxpy +from dxpy.nextflow.nextflow_builder import prepare_custom_inputs +from dxpy.nextflow.nextflow_utils import find_readme +if USING_PYTHON2: + spawn_extra_args = {} +else: + # Python 3 requires specifying the encoding + spawn_extra_args = {"encoding": "utf-8"} +from pathlib import Path + +THIS_DIR = Path(__file__).parent + + +input1 = { + "class": "file", + "name": "first_input", + "optional": True, + "help": "(Optional) First input", + "label": "Test" +} +input2 = { + "class": "string", + "name": "second_input", + "help": "second input", + "label": "Test2" +} +input3 = { + "class": "file", + "name": "third_input", + "help": "(Nextflow pipeline optional)third input", + "label": "Test3" +} +input4 = { + "class": "file", + "name": "fourth_input", + "help": "(Nextflow pipeline required)fourth input", + "label": "Test4" +} + + +class TestNextflowUtils(DXTestCase): + + @parameterized.expand([ + [None, 'file1.txt', 'file2.txt', 'main.nf'], + ['README.txt', 'README.txt', 'readme.txt', 'main.nf'], + ['README.md', 'README.md', 'main.nf'], + [None] + ]) + def test_searching_readmes(self, expected, *file_list): + temp_dir = tempfile.mkdtemp() + try: + # creating a folder with files for find_readme + for filename in file_list: + file_path = os.path.join(temp_dir, filename) + open(file_path, 'a').close() + actual = find_readme(temp_dir) + finally: + shutil.rmtree(temp_dir) + assert actual == expected + + +class TestNextflowTemplates(DXTestCase): + + def test_dxapp(self): + dxapp = get_nextflow_dxapp() + self.assertEqual(dxapp.get("name"), "python") # name is by default set to the resources directory name + self.assertEqual(dxapp.get("details", {}).get("repository"), "local") + + @parameterized.expand([ + [input1], + [input2], + [input1, input2] + ]) + def test_dxapp_custom_input(self, *inputs): + with open(os.path.join(str(get_template_dir()), 'dxapp.json'), 'r') as f: + default_dxapp = json.load(f) + + inputs = list(inputs) + dxapp = get_nextflow_dxapp(custom_inputs=inputs) + self.assertEqual(dxapp.get("inputSpec"), inputs + default_dxapp.get("inputSpec")) + + @unittest.skipIf(USING_PYTHON2, + 'Skipping as the Nextflow template from which applets are built is for Py3 interpreter only') + def test_src_basic(self): + src = get_nextflow_src() + self.assertTrue("#!/usr/bin/env bash" in src) + self.assertTrue("nextflow" in src) + + @unittest.skipIf(USING_PYTHON2, + 'Skipping as the Nextflow template from which applets are built is for Py3 interpreter only') + def test_src_profile(self): + src = get_nextflow_src(profile="test_profile") + self.assertTrue("-profile test_profile" in src) + + @unittest.skipIf(USING_PYTHON2, + 'Skipping as the Nextflow template from which applets are built is for Py3 interpreter only') + def test_src_inputs(self): + ''' + Tests that code that handles custom nextflow input parameters (e.g. from nextflow schema) with different classes + are properly added in the applet source script. These input arguments should be appended to nextflow cmd as runtime parameters + ''' + src = get_nextflow_src(custom_inputs=[input1, input2, input3, input4]) + # case 1: file input, need to convert from dnanexus link to its file path inside job workspace + self.assertTrue("if [ -n \"${}\" ];".format(input1.get("name")) in src) + value1 = 'dx://${DX_WORKSPACE_ID}:/$(echo ${%s} | jq .[$dnanexus_link] -r | xargs -I {} dx describe {} --json | jq -r .name)' % input1.get( + "name") + self.assertTrue("applet_runtime_inputs+=(--{} \"{}\")".format(input1.get("name"), value1) in src) + # case 2: string input, need no conversion + self.assertTrue("if [ -n \"${}\" ];".format(input2.get("name")) in src) + value2 = '${%s}' % input2.get("name") + self.assertTrue("applet_runtime_inputs+=(--{} \"{}\")".format(input2.get("name"), value2) in src) + # case 3: file input (nextflow pipeline optional), same as case 1 + self.assertTrue("if [ -n \"${}\" ];".format(input3.get("name")) in src) + value3 = 'dx://${DX_WORKSPACE_ID}:/$(echo ${%s} | jq .[$dnanexus_link] -r | xargs -I {} dx describe {} --json | jq -r .name)' % input3.get( + "name") + self.assertTrue("applet_runtime_inputs+=(--{} \"{}\")".format(input3.get("name"), value3) in src) + # case 4: file input (nextflow pipeline required), same as case 1 + self.assertTrue("if [ -n \"${}\" ];".format(input4.get("name")) in src) + value4 = 'dx://${DX_WORKSPACE_ID}:/$(echo ${%s} | jq .[$dnanexus_link] -r | xargs -I {} dx describe {} --json | jq -r .name)' % input4.get( + "name") + self.assertTrue("applet_runtime_inputs+=(--{} \"{}\")".format(input4.get("name"), value4) in src) + + self.assertTrue("dx-download-all-inputs --parallel --except {} --except {} --except {}".format( + input1.get("name"), input3.get("name"), input4.get("name")) in src) + + def test_prepare_inputs(self): + inputs = prepare_custom_inputs(schema_file=THIS_DIR / "nextflow/schema2.json") + names = [i["name"] for i in inputs] + self.assertTrue( + "input" in names and "outdir" in names and "save_merged_fastq" in names) + self.assertEqual(len(names), 3) + + def test_prepare_inputs_single(self): + inputs = prepare_custom_inputs(schema_file=THIS_DIR / "nextflow/schema3.json") + self.assertEqual(len(inputs), 1) + i = inputs[0] + self.assertEqual(i["name"], "outdir") + self.assertEqual(i["title"], "outdir") + self.assertEqual(i["help"], "(Nextflow pipeline required) out_directory help text") + self.assertEqual(i["hidden"], False) + self.assertEqual(i["class"], "string") + + def test_prepare_inputs_large_file(self): + inputs = prepare_custom_inputs(schema_file=THIS_DIR / "nextflow/schema1.json") + self.assertEqual(len(inputs), 93) + + +class TestDXBuildNextflowApplet(DXTestCaseBuildNextflowApps): + + def test_dx_build_nextflow_default_metadata(self): + # Name of folder containing *.nf + pipeline_name = "hello" + applet_dir = self.write_nextflow_applet_directory( + pipeline_name, existing_nf_file_path=self.base_nextflow_nf) + applet_id = json.loads( + run("dx build --nextflow --json " + applet_dir))["id"] + applet = dxpy.DXApplet(applet_id) + desc = applet.describe() + self.assertEqual(desc["name"], pipeline_name) + self.assertEqual(desc["title"], pipeline_name) + self.assertEqual(desc["summary"], pipeline_name) + + details = applet.get_details() + self.assertEqual(details["repository"], "local") + + def test_dx_build_nextflow_with_abs_and_relative_path(self): + pipeline_name = "hello_abs" + applet_dir = self.write_nextflow_applet_directory( + pipeline_name, existing_nf_file_path=self.base_nextflow_nf) + applet_id = json.loads( + run("dx build --nextflow --json " + applet_dir))["id"] + app = dxpy.describe(applet_id) + self.assertEqual(app["name"], pipeline_name) + + pipeline_name = "hello_abs_with_trailing_slash" + applet_dir = self.write_nextflow_applet_directory( + pipeline_name, existing_nf_file_path=self.base_nextflow_nf) + applet_id = json.loads( + run("dx build --nextflow --json " + applet_dir + "/"))["id"] + app = dxpy.describe(applet_id) + self.assertEqual(app["name"], pipeline_name) + + pipeline_name = "hello_rel" + applet_dir = self.write_nextflow_applet_directory( + pipeline_name, existing_nf_file_path=self.base_nextflow_nf) + with chdir(applet_dir): + applet_id = json.loads( + run("dx build --nextflow . --json".format(applet_dir)))["id"] + app = dxpy.describe(applet_id) + self.assertEqual(app["name"], pipeline_name) + + def test_dx_build_nextflow_with_space_in_name(self): + pipeline_name = "hello pipeline" + applet_dir = self.write_nextflow_applet_directory( + pipeline_name, existing_nf_file_path=self.base_nextflow_nf) + applet_id = json.loads( + run("dx build --nextflow '{}' --json".format(applet_dir)))["id"] + app = dxpy.describe(applet_id) + self.assertEqual(app["name"], pipeline_name) + + def test_dx_build_nextflow_with_extra_args(self): + # Name of folder containing *.nf + pipeline_name = "hello" + applet_dir = self.write_nextflow_applet_directory( + pipeline_name, existing_nf_file_path=self.base_nextflow_nf) + + # Override metadata values + extra_args = '{"name": "name-9Oxvx2tCZe", "title": "Title VsnhPeFBqt", "summary": "Summary 3E7fFfEXdB"}' + applet_id = json.loads(run( + "dx build --nextflow '{}' --json --extra-args '{}'".format(applet_dir, extra_args)))["id"] + + applet = dxpy.DXApplet(applet_id) + desc = applet.describe() + self.assertEqual(desc["name"], json.loads(extra_args)["name"]) + self.assertEqual(desc["title"], json.loads(extra_args)["title"]) + self.assertEqual(desc["summary"], json.loads(extra_args)["summary"]) + + details = applet.get_details() + self.assertEqual(details["repository"], "local") + + @unittest.skipUnless(testutil.TEST_NF_DOCKER, + 'skipping tests that require docker') + def test_bundle_docker_images(self): + image_refs = [ + { + "engine": "docker", + "process": "proc1", + "digest": "sha256:cca7bbfb3cd4dc1022f00cee78c51aa46ecc3141188f0dd520978a620697e7ad", + "image_name": "busybox", + "tag": "1.36" + }, + { + "engine": "docker", + "process": "proc2", + "digest": "sha256:cca7bbfb3cd4dc1022f00cee78c51aa46ecc3141188f0dd520978a620697e7ad", + "image_name": "busybox", + "tag": "1.36" + } + ] + bundled_images = bundle_docker_images(image_refs) + self.assertEqual(len(bundled_images), 1) + + @unittest.skipUnless(testutil.TEST_RUN_JOBS, + 'skipping tests that would run jobs') + @unittest.skipUnless(testutil.TEST_NF_DOCKER, + 'skipping tests that require docker') + def test_dx_build_nextflow_from_local_cache_docker(self): + applet_id = json.loads( + run("dx build --brief --nextflow '{}' --cache-docker".format(self.base_nextflow_docker)).strip() + )["id"] + + applet = dxpy.DXApplet(applet_id) + desc = applet.describe() + dependencies = desc.get("runSpec").get("bundledDepends") + docker_dependency = [x for x in dependencies if x["name"] == "bash"] + self.assertEqual(len(docker_dependency), 1) + details = applet.get_details() + self.assertTrue(details["repository"].startswith("project-")) + + + @unittest.skipUnless(testutil.TEST_RUN_JOBS, + 'skipping tests that would run jobs') + def test_dx_build_nextflow_from_repository_default_metadata(self): + pipeline_name = "hello" + hello_repo_url = "https://github.com/nextflow-io/hello" + applet_json = run( + "dx build --nextflow --repository '{}' --brief".format(hello_repo_url)).strip() + applet_id = json.loads(applet_json).get("id") + + applet = dxpy.DXApplet(applet_id) + desc = applet.describe() + self.assertEqual(desc["name"], pipeline_name) + self.assertEqual(desc["title"], pipeline_name) + self.assertEqual(desc["summary"], pipeline_name) + + details = applet.get_details() + self.assertEqual(details["repository"], hello_repo_url) + + @unittest.skipUnless(testutil.TEST_RUN_JOBS, + 'skipping tests that would run jobs') + def test_dx_build_nextflow_from_repository_destination(self): + hello_repo_url = "https://github.com/nextflow-io/hello" + folder = "/test_dx_build_nextflow_from_repository_destination/{}".format(str(uuid.uuid4().hex)) + run("dx mkdir -p {}".format(folder)) + applet_json = run( + "dx build --nextflow --repository '{}' --brief --destination {}".format(hello_repo_url, folder)).strip() + applet_id = json.loads(applet_json).get("id") + + applet = dxpy.DXApplet(applet_id) + desc = applet.describe() + self.assertEqual(desc["folder"], folder) + + @unittest.skipUnless(testutil.TEST_RUN_JOBS, + 'skipping tests that would run jobs') + def test_dx_build_nextflow_from_repository_with_extra_args(self): + hello_repo_url = "https://github.com/nextflow-io/hello" + + # Override metadata values + extra_args = '{"name": "name-l1DeZYnTyQ", "title": "Title KkWUaqpHh1", "summary": "Summary Yqf37VpDTY"}' + applet_json = run("dx build --nextflow --repository '{}' --extra-args '{}' --brief".format(hello_repo_url, extra_args)).strip() + + applet_id = json.loads(applet_json).get("id") + applet = dxpy.DXApplet(applet_id) + desc = applet.describe() + self.assertEqual(desc["name"], json.loads(extra_args)["name"]) + self.assertEqual(desc["title"], json.loads(extra_args)["title"]) + self.assertEqual(desc["summary"], json.loads(extra_args)["summary"]) + + details = applet.get_details() + self.assertEqual(details["repository"], hello_repo_url) + + def test_dx_build_nextflow_with_destination(self): + pipeline_name = "hello" + applet_dir = self.write_nextflow_applet_directory( + pipeline_name, existing_nf_file_path=self.base_nextflow_nf) + applet_id = json.loads( + run("dx build --nextflow --json --destination MyApplet " + applet_dir))["id"] + applet = dxpy.DXApplet(applet_id) + desc = applet.describe() + self.assertEqual(desc["name"], "MyApplet") + self.assertEqual(desc["title"], pipeline_name) + self.assertEqual(desc["summary"], pipeline_name) + + +class TestRunNextflowApplet(DXTestCaseBuildNextflowApps): + + # @unittest.skipUnless(testutil.TEST_RUN_JOBS, + # 'skipping tests that would run jobs') + @unittest.skip("skipping flaky test; to be fixed separately") + def test_dx_run_retry_fail(self): + pipeline_name = "retryMaxRetries" + nextflow_file = THIS_DIR / "nextflow/RetryMaxRetries/main.nf" + applet_dir = self.write_nextflow_applet_directory( + pipeline_name, existing_nf_file_path=nextflow_file) + applet_id = json.loads( + run("dx build --nextflow --json " + applet_dir))["id"] + applet = dxpy.DXApplet(applet_id) + + job = applet.run({}) + self.assertRaises(dxpy.exceptions.DXJobFailureError, job.wait_on_done) + desc = job.describe() + self.assertEqual(desc.get("properties", {}).get("nextflow_errorStrategy"), "retry-exceedsMaxValue") + + errored_subjob = dxpy.DXJob(desc.get("properties", {})["nextflow_errored_subjob"]) + self.assertRaises(dxpy.exceptions.DXJobFailureError, errored_subjob.wait_on_done) + subjob_desc = errored_subjob.describe() + self.assertEqual(subjob_desc.get("properties").get("nextflow_errorStrategy"), "retry-exceedsMaxValue") + self.assertEqual(subjob_desc.get("properties").get("nextflow_errored_subjob"), "self") + + @unittest.skipUnless(testutil.TEST_RUN_JOBS, + 'skipping tests that would run jobs') + def test_dx_run_nextflow_with_additional_parameters(self): + pipeline_name = "hello" + applet_dir = self.write_nextflow_applet_directory(pipeline_name, existing_nf_file_path=self.base_nextflow_nf) + applet_id = json.loads(run("dx build --nextflow --json " + applet_dir))["id"] + applet = dxpy.DXApplet(applet_id) + + job = applet.run({ + "nextflow_pipeline_params": "--input 'Printed test message'", + "nextflow_top_level_opts": "-quiet" + }) + + watched_run_output = run("dx watch {}".format(job.get_id())) + self.assertIn("hello STDOUT Printed test message world!", watched_run_output) + # Running with the -quiet option reduces the amount of log and the lines such as: + # STDOUT Launching `/home/dnanexus/hello/main.nf` [run-c8804f26-2eac-48d2-9a1a-a707ad1189eb] DSL2 - revision: 72a5d52d07 + # are not printed + self.assertNotIn("Launching", watched_run_output) + + @unittest.skipUnless(testutil.TEST_RUN_JOBS, + 'skipping tests that would run jobs') + def test_dx_run_nextflow_by_cloning(self): + pipeline_name = "hello" + applet_dir = self.write_nextflow_applet_directory( + pipeline_name, existing_nf_file_path=self.base_nextflow_nf) + applet_id = json.loads( + run("dx build --nextflow --json " + applet_dir))["id"] + applet = dxpy.DXApplet(applet_id) + + orig_job = applet.run({ + "preserve_cache": True, + "debug" : True + }) + + orig_job.wait_on_done() + orig_job_desc = orig_job.describe() + self.assertDictSubsetOf({"nextflow_executable": "hello", + "nextflow_preserve_cache": "true"}, orig_job_desc["properties"]) + + orig_job.set_properties( + {"extra_user_prop": "extra_value", "nextflow_preserve_cache": "invalid_boolean", "nextflow_nonexistent_prop": "nonexistent_nextflow_prop_value"}) + + new_job_id = run("dx run --clone " + + orig_job.get_id() + " --brief -y ").strip() + dxpy.DXJob(new_job_id).wait_on_done() + new_job_desc = dxpy.api.job_describe(new_job_id) + self.assertDictSubsetOf({"nextflow_executable": "hello", "nextflow_preserve_cache": "true", + "extra_user_prop": "extra_value"}, new_job_desc["properties"]) + self.assertNotIn("nextflow_nonexistent_prop", new_job_desc["properties"]) + + @unittest.skipUnless(testutil.TEST_RUN_JOBS, + 'skipping tests that would run jobs') + def test_dx_run_nextflow_with_unsupported_runtime_opts(self): + pipeline_name = "hello" + applet_dir = self.write_nextflow_applet_directory( + pipeline_name, existing_nf_file_path=self.base_nextflow_nf) + applet_id = json.loads( + run("dx build --nextflow --json " + applet_dir))["id"] + applet = dxpy.DXApplet(applet_id) + + job = applet.run({ + "nextflow_run_opts": "-w user_workdir", + }) + self.assertRaises(dxpy.exceptions.DXJobFailureError, job.wait_on_done) + desc = job.describe() + job_desc = dxpy.DXJob(job.get_id()).describe() + self.assertEqual(job_desc["failureReason"], "AppError") + self.assertIn("Please remove workDir specification", + job_desc["failureMessage"]) + + @unittest.skipUnless(testutil.TEST_RUN_JOBS, + 'skipping tests that would run jobs') + def test_dx_run_nextflow_with_publishDir(self): + pipeline_name = "cat_ls" + # extra_args = '{"name": "testing_cat_ls"}' + applet_dir = self.write_nextflow_applet_directory( + pipeline_name, nf_file_name="main.nf", existing_nf_file_path=THIS_DIR / "nextflow/publishDir/main.nf") + applet_id = json.loads(run( + "dx build --nextflow '{}' --json".format(applet_dir)))["id"] + desc = dxpy.describe(applet_id) + + # Run with "dx run". + dxfile = dxpy.upload_string("foo", name="foo.txt", folder="/a/b/c", + project=self.project, parents=True, wait_on_close=True) + inFile_path = "dx://{}:/a/b/c/foo.txt".format(self.project) + inFolder_path = "dx://{}:/a/".format(self.project) + outdir = "nxf_outdir" + pipeline_args = "'--outdir {} --inFile {} --inFolder {}'".format( + outdir, inFile_path, inFolder_path) + + job_id = run( + "dx run {applet_id} -idebug=true -inextflow_pipeline_params={pipeline_args} --folder :/test-cat-ls/ -y --brief".format( + applet_id=applet_id, pipeline_args=pipeline_args) + ).strip() + job_handler = dxpy.DXJob(job_id) + job_handler.wait_on_done() + job_desc = dxpy.describe(job_id) + + # the output files will be: ls_folder.txt, cat_file.txt + self.assertEqual(len(job_desc["output"]["published_files"]), 2) + + @unittest.skipUnless(testutil.TEST_RUN_JOBS, + 'skipping tests that would run jobs') + def test_dx_run_override_profile(self): + pipeline_name = "profile_test" + + applet_dir = self.write_nextflow_applet_directory_from_folder(pipeline_name, THIS_DIR / "nextflow/profile/") + applet_id = json.loads(run( + "dx build --nextflow --profile test '{}' --json".format(applet_dir)))["id"] + job_id = run( + "dx run {applet_id} -y -inextflow_run_opts=\"-profile second\" --brief".format(applet_id=applet_id) + ).strip() + + job_handler = dxpy.DXJob(job_id) + job_handler.wait_on_done() + watched_run_output = run("dx watch {} --no-follow".format(job_id)) + + self.assertTrue("second_config world!" in watched_run_output, "second_config world! test was NOT found in the job log of {job_id}".format(job_id=job_id)) + self.assertTrue("test_config world!" not in watched_run_output, "test_config world! test was found in the job log of {job_id}, but it should have been overriden".format(job_id=job_id)) + + @unittest.skipUnless(testutil.TEST_RUN_JOBS, + 'skipping tests that would run jobs') + def test_dx_run_nextflow_with_soft_conf_files(self): + pipeline_name = "print_env" + applet_dir = self.write_nextflow_applet_directory( + pipeline_name, existing_nf_file_path=THIS_DIR / "nextflow/print_env_nextflow_soft_confs/main.nf") + applet_id = json.loads(run( + "dx build --nextflow '{}' --json".format(applet_dir)))["id"] + + # Run with "dx run". + first_config = dxpy.upload_local_file(THIS_DIR / "nextflow/print_env_nextflow_soft_confs/first.config", project=self.project, wait_on_close=True).get_id() + second_config = dxpy.upload_local_file(THIS_DIR / "nextflow/print_env_nextflow_soft_confs/second.config", project=self.project, wait_on_close=True).get_id() + + job_id = run( + "dx run {applet_id} -idebug=true -inextflow_soft_confs={first_config} -inextflow_soft_confs={second_config} --brief -y".format( + applet_id=applet_id, first_config=first_config, second_config=second_config) + ).strip() + job_handler = dxpy.DXJob(job_id) + job_handler.wait_on_done() + watched_run_output = run("dx watch {} --no-follow".format(job_id)) + self.assertTrue("-c /home/dnanexus/in/nextflow_soft_confs/0/first.config -c /home/dnanexus/in/nextflow_soft_confs/1/second.config" in watched_run_output) + # env var ALPHA specified in first.config and second.config + # the value in second.config overrides the one in first.config + self.assertTrue("The env var ALPHA is: runtime alpha 2" in watched_run_output) + # env var BETA specified in first.config only + self.assertTrue("The env var BETA is: runtime beta 1" in watched_run_output) + + @unittest.skipUnless(testutil.TEST_RUN_JOBS, + 'skipping tests that would run jobs') + def test_dx_run_nextflow_with_runtime_param_file(self): + pipeline_name = "print_params" + applet_dir = self.write_nextflow_applet_directory( + pipeline_name, existing_nf_file_path=THIS_DIR / "nextflow/print_param_nextflow_params_file/main.nf") + applet_id = json.loads(run( + "dx build --nextflow '{}' --json".format(applet_dir)))["id"] + + # Run with "dx run". + params_file = dxpy.upload_local_file(THIS_DIR / "nextflow/print_param_nextflow_params_file/params_file.yml", project=self.project, wait_on_close=True).get_id() + + job_id = run( + "dx run {applet_id} -idebug=true -inextflow_params_file={params_file} -inextflow_pipeline_params=\"--BETA 'CLI beta'\" --brief -y".format( + applet_id=applet_id, params_file=params_file) + ).strip() + job_handler = dxpy.DXJob(job_id) + job_handler.wait_on_done() + watched_run_output = run("dx watch {} --no-follow".format(job_id)) + + self.assertTrue("-params-file /home/dnanexus/in/nextflow_params_file/params_file.yml" in watched_run_output) + # precedence of the input parameter values: nextflow_params_file < nextflow_pipeline_params < other applet runtime inputs parsed from nextflow schema + self.assertTrue("The parameter ALPHA is: param file alpha" in watched_run_output) + self.assertTrue("The parameter BETA is: CLI beta" in watched_run_output) + +if __name__ == '__main__': + if 'DXTEST_FULL' not in os.environ: + sys.stderr.write( + 'WARNING: env var DXTEST_FULL is not set; tests that create apps or run jobs will not be run\n') + unittest.main() + diff --git a/src/python/test/test_nextflow_ImageRef.py b/src/python/test/test_nextflow_ImageRef.py new file mode 100755 index 0000000000..9acf14b16b --- /dev/null +++ b/src/python/test/test_nextflow_ImageRef.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright (C) 2013-2016 DNAnexus, Inc. +# +# This file is part of dx-toolkit (DNAnexus platform client libraries). +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy +# of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +from __future__ import print_function, unicode_literals, division, absolute_import + +import os +import sys +import unittest + +from parameterized import parameterized +from dxpy_testutil import DXTestCase, TEST_NF_DOCKER +from dxpy.nextflow.ImageRef import ImageRef, DockerImageRef + +class TestImageRef(DXTestCase): + + @parameterized.expand([ + ["proc1", "sha256aasdfadfadfafddasfdsfa"] + ]) + def test_ImageRef_cache(self, process, digest): + image_ref = ImageRef(process, digest) + with self.assertRaises(NotImplementedError) as err: + _ = image_ref._cache("file_name") + self.assertEqual( + err.exception, + "Abstract class. Method not implemented. Use the concrete implementations." + ) + + + @parameterized.expand([ + ["proc1", "sha256:3fbc632167424a6d997e74f52b878d7cc478225cffac6bc977eedfe51c7f4e79", "busybox", "1.36"] + ]) + @unittest.skipUnless(TEST_NF_DOCKER, + 'skipping tests that require docker') + def test_DockerImageRef_cache(self, process, digest, image_name, tag): + image_ref = DockerImageRef(process=process, digest=digest, image_name=image_name, tag=tag) + bundle_dx_file_id = image_ref.bundled_depends + self.assertEqual( + bundle_dx_file_id, + { + "name": "busybox_1.36", + "id": {"$dnanexus_link": image_ref._dx_file_id} + } + ) + + +if __name__ == '__main__': + if 'DXTEST_FULL' not in os.environ: + sys.stderr.write( + 'WARNING: env var DXTEST_FULL is not set; tests that create apps or run jobs will not be run\n') + unittest.main() diff --git a/src/python/test/test_nextflow_ImageRefFactory.py b/src/python/test/test_nextflow_ImageRefFactory.py new file mode 100644 index 0000000000..5e948386ec --- /dev/null +++ b/src/python/test/test_nextflow_ImageRefFactory.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright (C) 2013-2016 DNAnexus, Inc. +# +# This file is part of dx-toolkit (DNAnexus platform client libraries). +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy +# of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +from __future__ import print_function, unicode_literals, division, absolute_import + +import os +import sys +import unittest + +from parameterized import parameterized +from dxpy_testutil import DXTestCase +from dxpy.compat import USING_PYTHON2 +from dxpy.nextflow.ImageRefFactory import ImageRefFactory, ImageRefFactoryError +from dxpy.nextflow.ImageRef import DockerImageRef + +if USING_PYTHON2: + spawn_extra_args = {} +else: + # Python 3 requires specifying the encoding + spawn_extra_args = {"encoding": "utf-8"} + + +class TestImageRef(DXTestCase): + @parameterized.expand([ + [{"engine": "docker", "process": "proc1", "digest": "sha256aasdfadfadfafddasfdsfa"}] + ]) + @unittest.skipIf(USING_PYTHON2, + 'Skipping Python 3 code') + def test_ImageRefFactory(self, image_ref): + image_ref_factory = ImageRefFactory(image_ref) + image = image_ref_factory.get_image() + self.assertTrue(isinstance(image, DockerImageRef)) + + @parameterized.expand([ + [{"process": "proc1", "digest": "sha256aasdfadfadfafddasfdsfa"}, "Provide the container engine"], + [{"engine": "singularity", "process": "proc1", "digest": "sha256aasdfadfadfafddasfdsfa"}, "Unsupported container engine: singularity"] + ]) + @unittest.skipIf(USING_PYTHON2, + 'Skipping Python 3 code') + def test_ImageRefFactory_errors(self, image_ref, exception): + with self.assertRaises(ImageRefFactoryError) as err: + image_ref_factory = ImageRefFactory(image_ref) + _ = image_ref_factory.get_image() + self.assertEqual(err.exception, exception) + + + +if __name__ == '__main__': + if 'DXTEST_FULL' not in os.environ: + sys.stderr.write( + 'WARNING: env var DXTEST_FULL is not set; tests that create apps or run jobs will not be run\n') + unittest.main() diff --git a/src/python/test/utils/__init__.py b/src/python/test/utils/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/python/test/utils/test_version.py b/src/python/test/utils/test_version.py new file mode 100644 index 0000000000..74fd1b9e03 --- /dev/null +++ b/src/python/test/utils/test_version.py @@ -0,0 +1,47 @@ +import pytest + +from parameterized import parameterized +from dxpy_testutil import DXTestCase + +from dxpy.utils.version import Version + + +class TestVersion(DXTestCase): + + def test_version(self): + assert Version("2") > Version("1") + assert Version("2") >= Version("1") + assert Version("1") >= Version("1") + assert Version("1") < Version("3") + assert Version("1") <= Version("3") + assert Version("1") <= Version("1") + assert Version("1") == Version("1") + assert Version("2.1") > Version("2") + assert Version("2.1") >= Version("2") + assert Version("2.2") > Version("2.1") + assert Version("2.2") >= Version("2.1") + assert Version("2.1") >= Version("2.1") + assert Version("2.1") < Version("2.2") + assert Version("2.1") <= Version("2.2") + assert Version("2.1") <= Version("2.1") + assert Version("2.1") == Version("2.1") + assert Version("2.1.1") > Version("2.1") + assert Version("2.1.1") >= Version("2.1") + assert Version("2.1.2") > Version("2.1.1") + assert Version("2.1.2") >= Version("2.1.1") + assert Version("2.1.1") >= Version("2.1.1") + assert Version("2.1.1") < Version("2.1.2") + assert Version("2.1.1") <= Version("2.1.2") + assert Version("2.1.1") <= Version("2.1.1") + assert Version("2.1.2") == Version("2.1.2") + + @parameterized.expand([ + (None, ), + ("1.2.3.4", ), + ("1.b.3", ), + ("2.0 beta", ), + ("version", ) + ]) + def test_version_invalid(self, version): + with pytest.raises(Exception): + Version(version)