dxpy 0.402.0__tar.gz → 0.404.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. {dxpy-0.402.0 → dxpy-0.404.0}/PKG-INFO +1 -1
  2. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/app_builder.py +54 -1
  3. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/cli/parsers.py +39 -0
  4. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/nextflow/app_asset_projects_ids_prod.json +1 -0
  5. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/nextflow/app_asset_projects_ids_staging.json +1 -0
  6. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/nextflow/awscli_assets.json +1 -0
  7. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/nextflow/awscli_assets.staging.json +1 -0
  8. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/nextflow/default_nextflow_instance_types.json +1 -0
  9. dxpy-0.404.0/dxpy/nextflow/nextaur_assets.json +11 -0
  10. dxpy-0.404.0/dxpy/nextflow/nextaur_assets.staging.json +11 -0
  11. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/nextflow/nextflow_assets.json +1 -0
  12. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/nextflow/nextflow_assets.staging.json +1 -0
  13. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/scripts/dx.py +18 -3
  14. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/system_requirements.py +2 -2
  15. dxpy-0.404.0/dxpy/toolkit_version.py +1 -0
  16. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy.egg-info/PKG-INFO +1 -1
  17. {dxpy-0.402.0 → dxpy-0.404.0}/test/test_dxclient.py +107 -0
  18. {dxpy-0.402.0 → dxpy-0.404.0}/test/test_dxpy.py +329 -0
  19. {dxpy-0.402.0 → dxpy-0.404.0}/test/test_extract_expression.py +28 -32
  20. dxpy-0.402.0/dxpy/nextflow/nextaur_assets.json +0 -10
  21. dxpy-0.402.0/dxpy/nextflow/nextaur_assets.staging.json +0 -10
  22. dxpy-0.402.0/dxpy/toolkit_version.py +0 -1
  23. {dxpy-0.402.0 → dxpy-0.404.0}/MANIFEST.in +0 -0
  24. {dxpy-0.402.0 → dxpy-0.404.0}/Readme.md +0 -0
  25. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/__init__.py +0 -0
  26. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/api.py +0 -0
  27. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/app_categories.py +0 -0
  28. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/asset_builder.py +0 -0
  29. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/bindings/__init__.py +0 -0
  30. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/bindings/apollo/__init__.py +0 -0
  31. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/bindings/apollo/cmd_line_options_validator.py +0 -0
  32. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/bindings/apollo/data_transformations.py +0 -0
  33. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/bindings/apollo/dataset.py +0 -0
  34. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/bindings/apollo/json_validation_by_schema.py +0 -0
  35. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/bindings/apollo/schemas/__init__.py +0 -0
  36. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/bindings/apollo/schemas/assay_filtering_conditions.py +0 -0
  37. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/bindings/apollo/schemas/assay_filtering_json_schemas.py +0 -0
  38. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/bindings/apollo/schemas/input_arguments_validation_schemas.py +0 -0
  39. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/bindings/apollo/vizclient.py +0 -0
  40. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/bindings/apollo/vizserver_filters_from_json_parser.py +0 -0
  41. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/bindings/apollo/vizserver_payload_builder.py +0 -0
  42. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/bindings/auth.py +0 -0
  43. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/bindings/download_all_inputs.py +0 -0
  44. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/bindings/dxanalysis.py +0 -0
  45. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/bindings/dxapp.py +0 -0
  46. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/bindings/dxapp_container_functions.py +0 -0
  47. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/bindings/dxapplet.py +0 -0
  48. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/bindings/dxdatabase.py +0 -0
  49. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/bindings/dxdatabase_functions.py +0 -0
  50. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/bindings/dxdataobject_functions.py +0 -0
  51. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/bindings/dxfile.py +0 -0
  52. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/bindings/dxfile_functions.py +0 -0
  53. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/bindings/dxglobalworkflow.py +0 -0
  54. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/bindings/dxjob.py +0 -0
  55. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/bindings/dxproject.py +0 -0
  56. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/bindings/dxrecord.py +0 -0
  57. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/bindings/dxworkflow.py +0 -0
  58. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/bindings/mount_all_inputs.py +0 -0
  59. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/bindings/search.py +0 -0
  60. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/cli/__init__.py +0 -0
  61. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/cli/cp.py +0 -0
  62. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/cli/dataset_utilities.py +0 -0
  63. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/cli/download.py +0 -0
  64. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/cli/exec_io.py +0 -0
  65. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/cli/help_messages.py +0 -0
  66. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/cli/org.py +0 -0
  67. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/cli/output_handling.py +0 -0
  68. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/cli/workflow.py +0 -0
  69. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/compat.py +0 -0
  70. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/dx_extract_utils/Homo_sapiens_genes_manifest.json +0 -0
  71. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/dx_extract_utils/Homo_sapiens_genes_manifest_staging.json +0 -0
  72. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/dx_extract_utils/Homo_sapiens_genes_manifest_staging_vep.json +0 -0
  73. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/dx_extract_utils/Homo_sapiens_genes_manifest_vep.json +0 -0
  74. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/dx_extract_utils/__init__.py +0 -0
  75. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/dx_extract_utils/cohort_filter_payload.py +0 -0
  76. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/dx_extract_utils/column_conditions.json +0 -0
  77. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/dx_extract_utils/column_conversion.json +0 -0
  78. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/dx_extract_utils/filter_to_payload.py +0 -0
  79. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/dx_extract_utils/germline_utils.py +0 -0
  80. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/dx_extract_utils/input_validation.py +0 -0
  81. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/dx_extract_utils/input_validation_somatic.py +0 -0
  82. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/dx_extract_utils/retrieve_allele_schema.json +0 -0
  83. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/dx_extract_utils/retrieve_annotation_schema.json +0 -0
  84. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/dx_extract_utils/retrieve_bins.py +0 -0
  85. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/dx_extract_utils/retrieve_genotype_schema.json +0 -0
  86. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/dx_extract_utils/return_columns_allele.json +0 -0
  87. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/dx_extract_utils/return_columns_annotation.json +0 -0
  88. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/dx_extract_utils/return_columns_genotype.json +0 -0
  89. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/dx_extract_utils/return_columns_genotype_only.json +0 -0
  90. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/dx_extract_utils/somatic_filter_payload.py +0 -0
  91. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/dxlog.py +0 -0
  92. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/exceptions.py +0 -0
  93. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/executable_builder.py +0 -0
  94. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/nextflow/ImageRef.py +0 -0
  95. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/nextflow/ImageRefFactory.py +0 -0
  96. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/nextflow/__init__.py +0 -0
  97. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/nextflow/collect_images.py +0 -0
  98. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/nextflow/nextflow_builder.py +0 -0
  99. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/nextflow/nextflow_templates.py +0 -0
  100. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/nextflow/nextflow_utils.py +0 -0
  101. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/packages/__init__.py +0 -0
  102. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/scripts/__init__.py +0 -0
  103. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/scripts/dx_app_wizard.py +0 -0
  104. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/scripts/dx_build_app.py +0 -0
  105. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/scripts/dx_build_applet.py +0 -0
  106. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/ssh_tunnel_app_support.py +0 -0
  107. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/templating/__init__.py +0 -0
  108. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/templating/bash.py +0 -0
  109. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/templating/python.py +0 -0
  110. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/templating/templates/Readme.md +0 -0
  111. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/templating/templates/bash/basic/dxapp.json +0 -0
  112. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/templating/templates/bash/basic/src/code.sh +0 -0
  113. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/templating/templates/bash/parallelized/dxapp.json +0 -0
  114. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/templating/templates/bash/parallelized/src/code.sh +0 -0
  115. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/templating/templates/bash/scatter-process-gather/dxapp.json +0 -0
  116. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/templating/templates/bash/scatter-process-gather/src/code.sh +0 -0
  117. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/templating/templates/nextflow/dxapp.json +0 -0
  118. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/templating/templates/nextflow/src/nextflow.sh +0 -0
  119. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/templating/templates/python/basic/dxapp.json +0 -0
  120. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/templating/templates/python/basic/src/code.py +0 -0
  121. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/templating/templates/python/basic/test/test.py +0 -0
  122. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/templating/templates/python/parallelized/dxapp.json +0 -0
  123. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/templating/templates/python/parallelized/src/code.py +0 -0
  124. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/templating/templates/python/parallelized/test/test.py +0 -0
  125. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/templating/templates/python/scatter-process-gather/dxapp.json +0 -0
  126. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/templating/templates/python/scatter-process-gather/src/code.py +0 -0
  127. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/templating/templates/python/scatter-process-gather/test/test.py +0 -0
  128. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/templating/utils.py +0 -0
  129. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/utils/__init__.py +0 -0
  130. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/utils/batch_utils.py +0 -0
  131. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/utils/completer.py +0 -0
  132. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/utils/config.py +0 -0
  133. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/utils/describe.py +0 -0
  134. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/utils/exec_utils.py +0 -0
  135. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/utils/executable_unbuilder.py +0 -0
  136. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/utils/file_handle.py +0 -0
  137. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/utils/file_load_utils.py +0 -0
  138. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/utils/genomic_utils.py +0 -0
  139. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/utils/job_log_client.py +0 -0
  140. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/utils/local_exec_utils.py +0 -0
  141. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/utils/pathmatch.py +0 -0
  142. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/utils/pretty_print.py +0 -0
  143. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/utils/printing.py +0 -0
  144. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/utils/resolver.py +0 -0
  145. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/utils/spelling_corrector.py +0 -0
  146. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/utils/version.py +0 -0
  147. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy/workflow_builder.py +0 -0
  148. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy.egg-info/SOURCES.txt +0 -0
  149. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy.egg-info/dependency_links.txt +0 -0
  150. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy.egg-info/entry_points.txt +0 -0
  151. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy.egg-info/not-zip-safe +0 -0
  152. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy.egg-info/requires.txt +0 -0
  153. {dxpy-0.402.0 → dxpy-0.404.0}/dxpy.egg-info/top_level.txt +0 -0
  154. {dxpy-0.402.0 → dxpy-0.404.0}/requirements.txt +0 -0
  155. {dxpy-0.402.0 → dxpy-0.404.0}/requirements_setuptools.txt +0 -0
  156. {dxpy-0.402.0 → dxpy-0.404.0}/requirements_test.txt +0 -0
  157. {dxpy-0.402.0 → dxpy-0.404.0}/scripts/dx-clone-asset +0 -0
  158. {dxpy-0.402.0 → dxpy-0.404.0}/scripts/dx-docker +0 -0
  159. {dxpy-0.402.0 → dxpy-0.404.0}/scripts/dx-download-all-inputs +0 -0
  160. {dxpy-0.402.0 → dxpy-0.404.0}/scripts/dx-fetch-bundled-depends +0 -0
  161. {dxpy-0.402.0 → dxpy-0.404.0}/scripts/dx-generate-dxapp +0 -0
  162. {dxpy-0.402.0 → dxpy-0.404.0}/scripts/dx-jobutil-add-output +0 -0
  163. {dxpy-0.402.0 → dxpy-0.404.0}/scripts/dx-jobutil-dxlink +0 -0
  164. {dxpy-0.402.0 → dxpy-0.404.0}/scripts/dx-jobutil-get-identity-token +0 -0
  165. {dxpy-0.402.0 → dxpy-0.404.0}/scripts/dx-jobutil-new-job +0 -0
  166. {dxpy-0.402.0 → dxpy-0.404.0}/scripts/dx-jobutil-parse-link +0 -0
  167. {dxpy-0.402.0 → dxpy-0.404.0}/scripts/dx-jobutil-report-error +0 -0
  168. {dxpy-0.402.0 → dxpy-0.404.0}/scripts/dx-log-stream +0 -0
  169. {dxpy-0.402.0 → dxpy-0.404.0}/scripts/dx-mount-all-inputs +0 -0
  170. {dxpy-0.402.0 → dxpy-0.404.0}/scripts/dx-notebook-reconnect +0 -0
  171. {dxpy-0.402.0 → dxpy-0.404.0}/scripts/dx-print-bash-vars +0 -0
  172. {dxpy-0.402.0 → dxpy-0.404.0}/scripts/dx-upload-all-outputs +0 -0
  173. {dxpy-0.402.0 → dxpy-0.404.0}/setup.cfg +0 -0
  174. {dxpy-0.402.0 → dxpy-0.404.0}/setup.py +0 -0
  175. {dxpy-0.402.0 → dxpy-0.404.0}/test/test_batch.py +0 -0
  176. {dxpy-0.402.0 → dxpy-0.404.0}/test/test_create_cohort.py +0 -0
  177. {dxpy-0.402.0 → dxpy-0.404.0}/test/test_describe.py +0 -0
  178. {dxpy-0.402.0 → dxpy-0.404.0}/test/test_dx-docker.py +0 -0
  179. {dxpy-0.402.0 → dxpy-0.404.0}/test/test_dx_app_wizard.py +0 -0
  180. {dxpy-0.402.0 → dxpy-0.404.0}/test/test_dx_bash_helpers.py +0 -0
  181. {dxpy-0.402.0 → dxpy-0.404.0}/test/test_dx_completion.py +0 -0
  182. {dxpy-0.402.0 → dxpy-0.404.0}/test/test_dx_symlink.py +0 -0
  183. {dxpy-0.402.0 → dxpy-0.404.0}/test/test_dxabs.py +0 -0
  184. {dxpy-0.402.0 → dxpy-0.404.0}/test/test_dxasset.py +0 -0
  185. {dxpy-0.402.0 → dxpy-0.404.0}/test/test_dxfile_functions.py +0 -0
  186. {dxpy-0.402.0 → dxpy-0.404.0}/test/test_dxpy_utils.py +0 -0
  187. {dxpy-0.402.0 → dxpy-0.404.0}/test/test_dxunpack.py +0 -0
  188. {dxpy-0.402.0 → dxpy-0.404.0}/test/test_extract_assay.py +0 -0
  189. {dxpy-0.402.0 → dxpy-0.404.0}/test/test_extract_dataset.py +0 -0
  190. {dxpy-0.402.0 → dxpy-0.404.0}/test/test_extract_somatic.py +0 -0
  191. {dxpy-0.402.0 → dxpy-0.404.0}/test/test_nextflow.py +0 -0
  192. {dxpy-0.402.0 → dxpy-0.404.0}/test/test_nextflow_ImageRef.py +0 -0
  193. {dxpy-0.402.0 → dxpy-0.404.0}/test/test_nextflow_ImageRefFactory.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dxpy
3
- Version: 0.402.0
3
+ Version: 0.404.0
4
4
  Summary: DNAnexus Platform API bindings for Python
5
5
  Home-page: https://github.com/dnanexus/dx-toolkit
6
6
  Author: Aleksandra Zalcman, Andrey Kislyuk, Anurag Biyani, Geet Duggal, Katherine Lai, Kurt Jensen, Marek Hrvol, Ohad Rodeh, Phil Sung
@@ -64,12 +64,65 @@ class AppBuilderException(Exception):
64
64
  """
65
65
  pass
66
66
 
67
+ def _validate_system_requirements(system_requirements):
68
+ """
69
+ Validates systemRequirements section of dxapp.json for instanceTypeSelector constraints.
70
+
71
+ :param system_requirements: The systemRequirements section from runSpec
72
+ :type system_requirements: dict
73
+ :raises AppBuilderException: If validation fails
74
+ """
75
+ if not isinstance(system_requirements, dict):
76
+ return
77
+
78
+ for entry_point, requirements in system_requirements.items():
79
+ if not isinstance(requirements, dict):
80
+ continue
81
+
82
+ has_instance_type = 'instanceType' in requirements
83
+ has_instance_type_selector = 'instanceTypeSelector' in requirements
84
+ has_cluster_spec = 'clusterSpec' in requirements
85
+
86
+ # Check mutual exclusivity: instanceType and instanceTypeSelector
87
+ if has_instance_type and has_instance_type_selector:
88
+ raise AppBuilderException(
89
+ "instanceType and instanceTypeSelector keywords are mutually exclusive "
90
+ "in systemRequirements for entry point '{}'".format(entry_point)
91
+ )
92
+
93
+ # Check mutual exclusivity: instanceTypeSelector and clusterSpec
94
+ if has_instance_type_selector and has_cluster_spec:
95
+ raise AppBuilderException(
96
+ "instanceTypeSelector and clusterSpec keywords are mutually exclusive "
97
+ "in systemRequirements for entry point '{}'".format(entry_point)
98
+ )
99
+
67
100
  def _validate_applet_spec(applet_spec):
68
101
  if 'runSpec' not in applet_spec:
69
102
  raise AppBuilderException("Required field 'runSpec' not found in dxapp.json")
70
103
 
104
+ # Validate systemRequirements for instanceTypeSelector constraints in runSpec
105
+ if 'systemRequirements' in applet_spec.get('runSpec', {}):
106
+ _validate_system_requirements(applet_spec['runSpec']['systemRequirements'])
107
+
108
+ # Validate systemRequirements in regionalOptions for each region
109
+ regional_options = applet_spec.get('regionalOptions', {})
110
+ if isinstance(regional_options, dict):
111
+ for region, options in regional_options.items():
112
+ if isinstance(options, dict) and 'systemRequirements' in options:
113
+ _validate_system_requirements(options['systemRequirements'])
114
+
71
115
  def _validate_app_spec(app_spec):
72
- pass
116
+ # Validate systemRequirements for instanceTypeSelector constraints in runSpec
117
+ if 'runSpec' in app_spec and 'systemRequirements' in app_spec['runSpec']:
118
+ _validate_system_requirements(app_spec['runSpec']['systemRequirements'])
119
+
120
+ # Validate systemRequirements in regionalOptions for each region
121
+ regional_options = app_spec.get('regionalOptions', {})
122
+ if isinstance(regional_options, dict):
123
+ for region, options in regional_options.items():
124
+ if isinstance(options, dict) and 'systemRequirements' in options:
125
+ _validate_system_requirements(options['systemRequirements'])
73
126
 
74
127
  def _get_applet_spec(src_dir):
75
128
  applet_spec_file = os.path.join(src_dir, "dxapp.json")
@@ -222,6 +222,45 @@ def process_extra_args(args):
222
222
  except:
223
223
  raise DXParserError('Value given for --extra-args could not be parsed as JSON')
224
224
 
225
+ # Validate that instanceTypeSelector is not specified in systemRequirements
226
+ def check_system_requirements(sys_reqs, context="systemRequirements"):
227
+ if isinstance(sys_reqs, dict):
228
+ for entry_point, reqs in sys_reqs.items():
229
+ if isinstance(reqs, dict) and 'instanceTypeSelector' in reqs:
230
+ raise DXParserError(
231
+ 'instanceTypeSelector cannot be specified in runtime --extra-args. '
232
+ 'It can only be defined in the dxapp.json at build time.'
233
+ )
234
+
235
+ if 'systemRequirements' in args.extra_args:
236
+ check_system_requirements(args.extra_args['systemRequirements'])
237
+
238
+ # Check systemRequirementsByExecutable (double-nested structure: executable -> entrypoint -> requirements)
239
+ if 'systemRequirementsByExecutable' in args.extra_args:
240
+ sys_reqs_by_exec = args.extra_args['systemRequirementsByExecutable']
241
+ if isinstance(sys_reqs_by_exec, dict):
242
+ for executable_id, entrypoints in sys_reqs_by_exec.items():
243
+ if isinstance(entrypoints, dict):
244
+ check_system_requirements(entrypoints, f"systemRequirementsByExecutable.{executable_id}")
245
+
246
+ # Also check regionalOptions
247
+ if 'regionalOptions' in args.extra_args:
248
+ regional_opts = args.extra_args['regionalOptions']
249
+ if isinstance(regional_opts, dict):
250
+ for region, region_spec in regional_opts.items():
251
+ if isinstance(region_spec, dict):
252
+ if 'systemRequirements' in region_spec:
253
+ check_system_requirements(region_spec['systemRequirements'],
254
+ f"regionalOptions.{region}.systemRequirements")
255
+ # Also check systemRequirementsByExecutable within regionalOptions
256
+ if 'systemRequirementsByExecutable' in region_spec:
257
+ sys_reqs_by_exec = region_spec['systemRequirementsByExecutable']
258
+ if isinstance(sys_reqs_by_exec, dict):
259
+ for executable_id, entrypoints in sys_reqs_by_exec.items():
260
+ if isinstance(entrypoints, dict):
261
+ check_system_requirements(entrypoints,
262
+ f"regionalOptions.{region}.systemRequirementsByExecutable.{executable_id}")
263
+
225
264
  exec_input_args = argparse.ArgumentParser(add_help=False)
226
265
  exec_input_args.add_argument('-i', '--input', help=fill('An input to be added using "<input name>[:<class>]=<input value>" (provide "class" if there is no input spec; it can be any job IO class, e.g. "string", "array:string", or "array"; if "class" is "array" or not specified, the value will be attempted to be parsed as JSON and is otherwise treated as a string)', width_adjustment=-24), action='append')
227
266
  exec_input_args.add_argument('-j', '--input-json', help=fill('The full input JSON (keys=input field names, values=input field values)', width_adjustment=-24))
@@ -6,5 +6,6 @@
6
6
  "aws:us-east-1": "project-B6JG897KGbkGb6Z7pQ9Q02jG",
7
7
  "azure:westeurope": "project-FGZZFkjBPJqBXq1X84pq4VJf",
8
8
  "azure:westus": "project-F3vk1q09FX8Jxxp20pq6z8P9",
9
+ "azure:uksouth-ofh": "project-GK404k924z9199Bv062PkYZk",
9
10
  "oci:us-ashburn-1": "project-J1q0ZJV6VQg98Q4Bz36JbQKJ"
10
11
  }
@@ -6,5 +6,6 @@
6
6
  "aws:us-east-1": "project-G0zG4B808VFFYVz84FQkvF1K",
7
7
  "azure:westeurope": "project-G0zG4BQBq1XQby2F4FqQpPXv",
8
8
  "azure:westus": "project-G0zG4B09ZJ5gx1Vz4Fb8Q9XX",
9
+ "azure:uksouth-ofh": "project-GJkPq8V2X73240P46Y5FKZ41",
9
10
  "oci:us-ashburn-1": "project-J1q0ZJV6z2pJXY2Qqv8xvX5G"
10
11
  }
@@ -4,6 +4,7 @@
4
4
  "aws:eu-west-2-g": "record-Gyv6ykBKQqy2K41YGfqpxYvF",
5
5
  "aws:me-south-1": "record-Gyv70B932Vy4bg78PFvbB4Q7",
6
6
  "aws:us-east-1": "record-Gyv6Q9j0PPgJQJ72226Gz7j7",
7
+ "azure:uksouth-ofh": "record-J5XVQjV24ZVFvPPbqVpBFzxB",
7
8
  "azure:westeurope": "record-Gyv73f0BKJkvjx5FPJq6F22b",
8
9
  "azure:westus": "record-Gyv77Vj9zv30Pfx8V890xQ6Q",
9
10
  "oci:us-ashburn-1": "record-J30Z62V6ZVX41GJBgkxbkFzp"
@@ -4,6 +4,7 @@
4
4
  "aws:eu-west-2-g": "record-GyqkvqpK06ffFb9V8g80f7Gg",
5
5
  "aws:me-south-1": "record-Gyqp5F13fKQ44GfVP0VkKZ0z",
6
6
  "aws:us-east-1": "record-GyqkB3j08Gb14Pf0P8x5Xq5Q",
7
+ "azure:uksouth-ofh": "record-J5XV1P92FJvy4V4KGX8PYZ1j",
7
8
  "azure:westeurope": "record-Gyqp8g8B3BVZ4GfVP0VkKZ15",
8
9
  "azure:westus": "record-GyqpG7Q9Z3044GfVP0VkKZ1J",
9
10
  "oci:us-ashburn-1": "record-J30Xb1k6p6JxZFPzfpBfY6Qq"
@@ -5,6 +5,7 @@
5
5
  "aws:me-south-1": "mem2_ssd1_v2_x4",
6
6
  "azure:westeurope": "azure:mem2_ssd1_x4",
7
7
  "azure:westus": "azure:mem2_ssd1_x4",
8
+ "azure:uksouth-ofh": "azure:mem2_ssd1_v2_x4",
8
9
  "aws:eu-west-2-g": "mem2_ssd1_v2_x4",
9
10
  "oci:us-ashburn-1": "oci:mem2_ssd1_v3i_x4"
10
11
  }
@@ -0,0 +1,11 @@
1
+ {
2
+ "aws:ap-southeast-2": "record-J5XQ90Q50QbxxqP4yF4JyxXf",
3
+ "aws:eu-central-1": "record-J5XQ908432kGV11kvkK73b0v",
4
+ "aws:eu-west-2-g": "record-J5XQ92XKJPJBJgj0byvp2qV1",
5
+ "aws:me-south-1": "record-J5XQ901386jgpGzp66zq4qVx",
6
+ "aws:us-east-1": "record-J5XQ5Bj0yYBJ6pGz3901JJ0g",
7
+ "azure:uksouth-ofh": "record-J5XQBV12vBB36yj477qXyb2X",
8
+ "azure:westeurope": "record-J5XQBQQByqk2pkk6GxVK996f",
9
+ "azure:westus": "record-J5XQG489v4kqvPPbqVpBBkvx",
10
+ "oci:us-ashburn-1": "record-J5XQ9Gk6Pgxp2jpKZvJfV781"
11
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "aws:ap-southeast-2": "record-J5XPkB85vKPFvbFZ9YpXkGqG",
3
+ "aws:eu-central-1": "record-J5XPjyQ46yBGk8yJ74vJ21FZ",
4
+ "aws:eu-west-2-g": "record-J5XPj7XKfyYFvbFZ9YpXkGqB",
5
+ "aws:me-south-1": "record-J5XPjq93zvkxQX5kK6P57176",
6
+ "aws:us-east-1": "record-J5XPVJQ0xYgJP4FFG50239Bq",
7
+ "azure:uksouth-ofh": "record-J5XPpz92V3ZGk8yJ74vJ21Fk",
8
+ "azure:westeurope": "record-J5XPpvjBb99zGPPPBxP4pQQ1",
9
+ "azure:westus": "record-J5XPj689xYV7F14FQK7Q677f",
10
+ "oci:us-ashburn-1": "record-J5XPp8V66pvqvbFZ9YpXkGqK"
11
+ }
@@ -4,6 +4,7 @@
4
4
  "aws:eu-west-2-g": "record-GzyP8FXKkjkqfjBjjZzFv68F",
5
5
  "aws:me-south-1": "record-GzyP8593PgfZjYXPJP9FFqV4",
6
6
  "aws:us-east-1": "record-GzyP6X00F3gq8p88gP7PQFkx",
7
+ "azure:uksouth-ofh": "record-J5XVQjk272xZYBv0JK6JKxQ8",
7
8
  "azure:westeurope": "record-GzyPB98BB2b867v2KK1BXx8p",
8
9
  "azure:westus": "record-GzyPB6j9b5Yy81G8XV89jkZX",
9
10
  "oci:us-ashburn-1": "record-J30Z63V67qG7Y9ZfxjPpp90G"
@@ -4,6 +4,7 @@
4
4
  "aws:eu-west-2-g": "record-GzyJ2Z2KppXG1ZXvQ2kp08YQ",
5
5
  "aws:me-south-1": "record-GzyJ2Y93z3b2Q4202Jpx1j78",
6
6
  "aws:us-east-1": "record-GzyJ19j08Ffppvfvgx9bZzfz",
7
+ "azure:uksouth-ofh": "record-J5XV0Y92093xP4FFG50239K9",
7
8
  "azure:westeurope": "record-GzyJ4QjByJz29GJg3K3Gv2yB",
8
9
  "azure:westus": "record-GzyJ45Q9xQPB36YfkbQQxpp8",
9
10
  "oci:us-ashburn-1": "record-J30Xbb16kfQfgJqjXGf7561F"
@@ -30,7 +30,7 @@ from ..compat import sys_encoding, basestring
30
30
  import dxpy
31
31
  from dxpy.scripts import dx_build_app
32
32
  from dxpy import workflow_builder
33
- from dxpy.exceptions import PermissionDenied, InvalidState, ResourceNotFound
33
+ from dxpy.exceptions import PermissionDenied, InvalidState, ResourceNotFound, DXCLIError
34
34
 
35
35
  from ..cli import try_call, prompt_for_yn, INTERACTIVE_CLI
36
36
  from ..cli import workflow as workflow_cli
@@ -3191,27 +3191,39 @@ def run_body(args, executable, dest_proj, dest_path, preset_inputs=None, input_n
3191
3191
  # 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
3192
3192
  cloned_system_requirements = copy.deepcopy(args.cloned_job_desc).get("systemRequirements", {})
3193
3193
  cloned_instance_type = SystemRequirementsDict.from_sys_requirements(cloned_system_requirements, _type='instanceType')
3194
+ cloned_instance_type_selector = SystemRequirementsDict.from_sys_requirements(cloned_system_requirements, _type='instanceTypeSelector')
3194
3195
  cloned_cluster_spec = SystemRequirementsDict.from_sys_requirements(cloned_system_requirements, _type='clusterSpec')
3195
3196
  cloned_fpga_driver = SystemRequirementsDict.from_sys_requirements(cloned_system_requirements, _type='fpgaDriver')
3196
3197
  cloned_nvidia_driver = SystemRequirementsDict.from_sys_requirements(cloned_system_requirements, _type='nvidiaDriver')
3197
3198
  cloned_system_requirements_by_executable = args.cloned_job_desc.get("mergedSystemRequirementsByExecutable", {}) or {}
3198
3199
  else:
3199
3200
  cloned_system_requirements = {}
3200
- cloned_instance_type, cloned_cluster_spec, cloned_fpga_driver, cloned_nvidia_driver = (
3201
- SystemRequirementsDict({}), SystemRequirementsDict({}), SystemRequirementsDict({}), SystemRequirementsDict({}))
3201
+ cloned_instance_type, cloned_instance_type_selector, cloned_cluster_spec, cloned_fpga_driver, cloned_nvidia_driver = (
3202
+ SystemRequirementsDict({}), SystemRequirementsDict({}), SystemRequirementsDict({}), SystemRequirementsDict({}), SystemRequirementsDict({}))
3202
3203
  cloned_system_requirements_by_executable = {}
3203
3204
 
3204
3205
  # convert runtime --instance-type into mapping {entrypoint:{'instanceType':xxx}}
3205
3206
  # here the args.instance_type no longer contains specifications for stage sys reqs
3206
3207
  if args.instance_type:
3208
+ # Runtime --instance-type overrides instanceTypeSelector from the executable's runSpec
3209
+ # When instanceType is specified at runtime, instanceTypeSelector should not be passed to the API
3207
3210
  requested_instance_type = SystemRequirementsDict.from_instance_type(args.instance_type)
3208
3211
  if not isinstance(args.instance_type, basestring):
3209
3212
  requested_instance_type = SystemRequirementsDict(merge(cloned_instance_type.as_dict(), requested_instance_type.as_dict()))
3210
3213
  else:
3214
+ # If no runtime instanceType, use cloned instanceType (not instanceTypeSelector)
3215
+ # instanceTypeSelector from the executable's runSpec will be resolved by the API
3211
3216
  requested_instance_type = cloned_instance_type
3212
3217
 
3213
3218
  # convert runtime --instance-count into mapping {entrypoint:{'clusterSpec':{'initialInstanceCount': N}}})
3214
3219
  if args.instance_count:
3220
+ # Validate that we're not mixing clusterSpec with instanceTypeSelector
3221
+ # clusterSpec and instanceTypeSelector are mutually exclusive at build time
3222
+ # However, if instanceType is provided (either from runtime or cloned job), it overrides instanceTypeSelector
3223
+ # So we only raise an error if instanceTypeSelector exists AND no instanceType override is present
3224
+ if cloned_instance_type_selector.as_dict() and not requested_instance_type.as_dict():
3225
+ raise DXCLIError("Cannot specify --instance-count when cloning a job that uses instanceTypeSelector "
3226
+ "without providing --instance-type. instanceTypeSelector and clusterSpec are mutually exclusive.")
3215
3227
  # retrieve the full cluster spec defined in executable's runSpec.systemRequirements
3216
3228
  # and overwrite the field initialInstanceCount with the runtime mapping
3217
3229
  requested_instance_count = SystemRequirementsDict.from_instance_count(args.instance_count)
@@ -3232,8 +3244,11 @@ def run_body(args, executable, dest_proj, dest_path, preset_inputs=None, input_n
3232
3244
  requested_fpga_driver = cloned_fpga_driver
3233
3245
  requested_nvidia_driver = cloned_nvidia_driver
3234
3246
 
3247
+
3235
3248
  # combine the requested instance type, full cluster spec, fpga spec, nvidia spec
3236
3249
  # into the runtime systemRequirements
3250
+ # Note: instanceTypeSelector is NOT included here as it's build-time only
3251
+ # If runtime instanceType was provided, it overrides instanceTypeSelector
3237
3252
  requested_system_requirements = (requested_instance_type + requested_cluster_spec + requested_fpga_driver +
3238
3253
  requested_nvidia_driver).as_dict()
3239
3254
 
@@ -83,9 +83,9 @@ class SystemRequirementsDict(object):
83
83
  """
84
84
  Returns SystemRequirementsDict encapsulating system requirements.
85
85
  It can extract only entrypoints with specific fields ('clusterSpec',
86
- 'instanceType', etc), depending on the value of _type.
86
+ 'instanceType', 'instanceTypeSelector', etc), depending on the value of _type.
87
87
  """
88
- allowed_types = ['all', 'clusterSpec', 'instanceType', 'fpgaDriver', 'nvidiaDriver']
88
+ allowed_types = ['all', 'clusterSpec', 'instanceType', 'instanceTypeSelector', 'fpgaDriver', 'nvidiaDriver']
89
89
  if _type not in (allowed_types):
90
90
  raise DXError("Expected '_type' to be one of the following: {}".format(allowed_types))
91
91
 
@@ -0,0 +1 @@
1
+ version = '0.404.0'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dxpy
3
- Version: 0.402.0
3
+ Version: 0.404.0
4
4
  Summary: DNAnexus Platform API bindings for Python
5
5
  Home-page: https://github.com/dnanexus/dx-toolkit
6
6
  Author: Aleksandra Zalcman, Andrey Kislyuk, Anurag Biyani, Geet Duggal, Katherine Lai, Kurt Jensen, Marek Hrvol, Ohad Rodeh, Phil Sung
@@ -3280,6 +3280,46 @@ dx-jobutil-add-output record_array $second_record --array
3280
3280
  with self.assertSubprocessFailure(stderr_regexp='JSON', exit_code=3):
3281
3281
  run("dx run " + applet_id + " --extra-args not-a-JSON-string")
3282
3282
 
3283
+ def test_dx_run_extra_args_instanceTypeSelector_rejected(self):
3284
+ # Validate that instanceTypeSelector cannot be specified in runtime extra-args
3285
+ applet_id = dxpy.api.applet_new({"project": self.project,
3286
+ "dxapi": "1.0.0",
3287
+ "runSpec": {"interpreter": "bash",
3288
+ "distribution": "Ubuntu",
3289
+ "release": "14.04",
3290
+ "code": "echo 'hello'"}
3291
+ })['id']
3292
+
3293
+ # Test with wildcard entry point
3294
+ with self.assertSubprocessFailure(stderr_regexp='instanceTypeSelector cannot be specified in runtime', exit_code=3):
3295
+ run('dx run ' + applet_id + ' --extra-args ' +
3296
+ '\'{"systemRequirements": {"*": {"instanceTypeSelector": {"allowedInstanceTypes": ["mem2_ssd1_v2_x2"]}}}}\'')
3297
+
3298
+ # Test with specific entry point
3299
+ with self.assertSubprocessFailure(stderr_regexp='instanceTypeSelector cannot be specified in runtime', exit_code=3):
3300
+ run('dx run ' + applet_id + ' --extra-args ' +
3301
+ '\'{"systemRequirements": {"main": {"instanceTypeSelector": {"allowedInstanceTypes": ["mem2_ssd1_v2_x2"]}}}}\'')
3302
+
3303
+ # Test with regionalOptions
3304
+ with self.assertSubprocessFailure(stderr_regexp='instanceTypeSelector cannot be specified in runtime', exit_code=3):
3305
+ run('dx run ' + applet_id + ' --extra-args ' +
3306
+ '\'{"regionalOptions": {"aws:us-east-1": {"systemRequirements": {"*": {"instanceTypeSelector": {"allowedInstanceTypes": ["mem2_ssd1_v2_x2"]}}}}}}\'')
3307
+
3308
+ # Test with systemRequirementsByExecutable
3309
+ with self.assertSubprocessFailure(stderr_regexp='instanceTypeSelector cannot be specified in runtime', exit_code=3):
3310
+ run('dx run ' + applet_id + ' --extra-args ' +
3311
+ '\'{"systemRequirementsByExecutable": {"' + applet_id + '": {"main": {"instanceTypeSelector": {"allowedInstanceTypes": ["mem2_ssd1_v2_x2"]}}}}}\'')
3312
+
3313
+ # Test with systemRequirementsByExecutable with wildcard entry point
3314
+ with self.assertSubprocessFailure(stderr_regexp='instanceTypeSelector cannot be specified in runtime', exit_code=3):
3315
+ run('dx run ' + applet_id + ' --extra-args ' +
3316
+ '\'{"systemRequirementsByExecutable": {"' + applet_id + '": {"*": {"instanceTypeSelector": {"allowedInstanceTypes": ["mem2_ssd1_v2_x2"]}}}}}\'')
3317
+
3318
+ # Test with regionalOptions.systemRequirementsByExecutable
3319
+ with self.assertSubprocessFailure(stderr_regexp='instanceTypeSelector cannot be specified in runtime', exit_code=3):
3320
+ run('dx run ' + applet_id + ' --extra-args ' +
3321
+ '\'{"regionalOptions": {"aws:us-east-1": {"systemRequirementsByExecutable": {"' + applet_id + '": {"main": {"instanceTypeSelector": {"allowedInstanceTypes": ["mem2_ssd1_v2_x2"]}}}}}}}\'')
3322
+
3283
3323
  def test_dx_run_sys_reqs(self):
3284
3324
  app_spec = {"project": self.project,
3285
3325
  "dxapi": "1.0.0",
@@ -3378,6 +3418,73 @@ dx-jobutil-add-output record_array $second_record --array
3378
3418
  cloned_job_desc = dxpy.api.job_describe(cloned_job_id_nvidia_override)
3379
3419
  assert cloned_job_desc["systemRequirements"]["*"]["nvidiaDriver"] == build_nvidia_version
3380
3420
 
3421
+ def test_dx_run_clone_with_instance_type_selector(self):
3422
+ """
3423
+ Test that when cloning a job from an applet that uses instanceTypeSelector:
3424
+ 1. Cloning without runtime args works - instanceTypeSelector is preserved
3425
+ 2. Cloning with --instance-type works - runtime instanceType overrides instanceTypeSelector
3426
+ 3. Cloning with --instance-count fails - clusterSpec and instanceTypeSelector are mutually exclusive
3427
+ 4. Job description contains instanceTypeSelector in systemRequirements
3428
+ """
3429
+ # Create an applet with instanceTypeSelector
3430
+ applet_id = dxpy.api.applet_new({"project": self.project,
3431
+ "dxapi": "1.0.0",
3432
+ "runSpec": {"interpreter": "bash",
3433
+ "distribution": "Ubuntu",
3434
+ "release": "20.04",
3435
+ "version": "0",
3436
+ "code": "echo 'hello'",
3437
+ "systemRequirements": {
3438
+ "*": {
3439
+ "instanceTypeSelector": {
3440
+ "allowedInstanceTypes": ["mem1_ssd1_x2", "mem2_ssd1_x2"]
3441
+ }
3442
+ }
3443
+ }}
3444
+ })['id']
3445
+
3446
+ # Run the applet to create the origin job
3447
+ # The API will resolve instanceTypeSelector to an actual instanceType
3448
+ origin_job_id = run(f"dx run {applet_id} --brief -y").strip().split('\n')[-1]
3449
+
3450
+ # Verify the origin job description contains instanceTypeSelector
3451
+ origin_job_desc = dxpy.api.job_describe(origin_job_id)
3452
+ assert "systemRequirements" in origin_job_desc
3453
+ assert "*" in origin_job_desc["systemRequirements"]
3454
+ assert "instanceTypeSelector" in origin_job_desc["systemRequirements"]["*"]
3455
+ assert "allowedInstanceTypes" in origin_job_desc["systemRequirements"]["*"]["instanceTypeSelector"]
3456
+
3457
+ # Clone without runtime args - should work, instanceTypeSelector is preserved
3458
+ cloned_job_id_1 = run(f"dx run --clone {origin_job_id} --brief -y").strip()
3459
+ assert cloned_job_id_1.startswith("job-")
3460
+ cloned_job_desc_1 = dxpy.api.job_describe(cloned_job_id_1)
3461
+ # Verify instanceTypeSelector is in the cloned job's systemRequirements
3462
+ assert "instanceTypeSelector" in cloned_job_desc_1["systemRequirements"]["*"]
3463
+
3464
+ # Clone with --instance-type - should work, runtime instanceType overrides instanceTypeSelector
3465
+ cloned_job_id_2 = run(f"dx run --clone {origin_job_id} --instance-type mem2_hdd2_x4 --brief -y").strip()
3466
+ assert cloned_job_id_2.startswith("job-")
3467
+ cloned_job_desc_2 = dxpy.api.job_describe(cloned_job_id_2)
3468
+ # Verify instanceType is used instead of instanceTypeSelector
3469
+ assert "instanceType" in cloned_job_desc_2["systemRequirements"]["*"]
3470
+ assert cloned_job_desc_2["systemRequirements"]["*"]["instanceType"] == "mem2_hdd2_x4"
3471
+ # instanceTypeSelector should not be in the runtime systemRequirements when overridden
3472
+ # TODO uncomment later - for now this is not included in the backend behaviour
3473
+ # assert "instanceTypeSelector" not in cloned_job_desc_2["systemRequirements"]["*"]
3474
+
3475
+ # Clone with --instance-count alone - should fail (clusterSpec and instanceTypeSelector are mutually exclusive)
3476
+ with self.assertSubprocessFailure(stderr_regexp='Cannot specify --instance-count.*instanceTypeSelector.*without providing --instance-type', exit_code=3):
3477
+ run(f"dx run --clone {origin_job_id} --instance-count 3 --brief -y")
3478
+
3479
+ # Clone with both --instance-count and --instance-type - should work (instanceType overrides instanceTypeSelector)
3480
+ cloned_job_id_3 = run(f"dx run --clone {origin_job_id} --instance-count 3 --instance-type mem2_hdd2_x4 --brief -y").strip()
3481
+ assert cloned_job_id_3.startswith("job-")
3482
+ cloned_job_desc_3 = dxpy.api.job_describe(cloned_job_id_3)
3483
+ # Verify instanceType override is applied
3484
+ assert "instanceType" in cloned_job_desc_3["systemRequirements"]["*"]
3485
+ assert cloned_job_desc_3["systemRequirements"]["*"]["instanceType"] == "mem2_hdd2_x4"
3486
+ # Note: When the applet's runSpec doesn't have a clusterSpec defined (only instanceTypeSelector)
3487
+
3381
3488
  def test_dx_run_clone(self):
3382
3489
  applet_id = dxpy.api.applet_new({"project": self.project,
3383
3490
  "dxapi": "1.0.0",