sima-cli 2.1.19__tar.gz → 2.1.20__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 (246) hide show
  1. {sima_cli-2.1.19/sima_cli.egg-info → sima_cli-2.1.20}/PKG-INFO +1 -1
  2. {sima_cli-2.1.19 → sima_cli-2.1.20}/pyproject.toml +1 -1
  3. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/__version__.py +1 -1
  4. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/cli.py +17 -6
  5. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/cloudex/client.py +55 -4
  6. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/discover/discover.py +4 -0
  7. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/discover/linuxll.py +38 -3
  8. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/download/downloader.py +37 -2
  9. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/install/metadata_installer.py +11 -5
  10. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/models/client.py +40 -15
  11. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/models/commands.py +24 -11
  12. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/models/rendering.py +78 -205
  13. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/peripherals/model.py +17 -3
  14. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/sdk/commands.py +33 -2
  15. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/sdk/install.py +48 -3
  16. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/sdk/neat.py +57 -10
  17. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/sdk/utils.py +548 -15
  18. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/update/host.py +3 -4
  19. sima_cli-2.1.20/sima_cli/update/mirror_catalog.py +11 -0
  20. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/update/netboot.py +77 -14
  21. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/update/overlay.py +19 -7
  22. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/update/remote.py +14 -3
  23. sima_cli-2.1.20/sima_cli/update/reset_overlay.py +92 -0
  24. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/update/swu.py +22 -11
  25. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/update/swu_artifacts.py +17 -6
  26. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/update/swu_certificate.py +1 -2
  27. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/update/swu_target.py +35 -0
  28. {sima_cli-2.1.19 → sima_cli-2.1.20/sima_cli.egg-info}/PKG-INFO +1 -1
  29. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli.egg-info/SOURCES.txt +9 -0
  30. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/peripherals_fixtures.py +6 -1
  31. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_cloudex_client.py +132 -10
  32. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_cloudex_commands.py +24 -0
  33. sima_cli-2.1.20/tests/unit/test_discovery_host_tools.py +103 -0
  34. sima_cli-2.1.20/tests/unit/test_download_retries.py +115 -0
  35. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_generate_cli_markdown_docs.py +16 -0
  36. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_host_update.py +11 -1
  37. sima_cli-2.1.20/tests/unit/test_install_script_terminal.py +153 -0
  38. sima_cli-2.1.20/tests/unit/test_mirror_catalog.py +61 -0
  39. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_models.py +261 -201
  40. sima_cli-2.1.20/tests/unit/test_netboot_autoflash.py +196 -0
  41. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_overlay.py +41 -2
  42. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_peripherals_commands.py +21 -1
  43. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_peripherals_contract.py +11 -2
  44. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_peripherals_model.py +22 -3
  45. sima_cli-2.1.20/tests/unit/test_reset_overlay.py +159 -0
  46. sima_cli-2.1.20/tests/unit/test_sdk_agent_sessions.py +884 -0
  47. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_sdk_image_detection.py +367 -37
  48. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_sdk_model_compiler.py +24 -0
  49. sima_cli-2.1.20/tests/unit/test_selectable_resources.py +98 -0
  50. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_swu.py +55 -7
  51. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_swu_certificate.py +4 -3
  52. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_swu_mirror.py +13 -2
  53. sima_cli-2.1.20/tests/unit/test_swu_target.py +158 -0
  54. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_vscode_extension_pins.py +1 -0
  55. sima_cli-2.1.19/tests/unit/test_netboot_autoflash.py +0 -85
  56. sima_cli-2.1.19/tests/unit/test_swu_target.py +0 -58
  57. {sima_cli-2.1.19 → sima_cli-2.1.20}/LICENSE +0 -0
  58. {sima_cli-2.1.19 → sima_cli-2.1.20}/MANIFEST.in +0 -0
  59. {sima_cli-2.1.19 → sima_cli-2.1.20}/README.md +0 -0
  60. {sima_cli-2.1.19 → sima_cli-2.1.20}/requirements.txt +0 -0
  61. {sima_cli-2.1.19 → sima_cli-2.1.20}/setup.cfg +0 -0
  62. {sima_cli-2.1.19 → sima_cli-2.1.20}/setup.py +0 -0
  63. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/__init__.py +0 -0
  64. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/__main__.py +0 -0
  65. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/app_zoo/__init__.py +0 -0
  66. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/app_zoo/app.py +0 -0
  67. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/app_zoo/commands.py +0 -0
  68. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/auth/__init__.py +0 -0
  69. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/auth/auth0.py +0 -0
  70. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/auth/devportal.py +0 -0
  71. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/auth/login.py +0 -0
  72. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/cloudex/__init__.py +0 -0
  73. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/cloudex/commands.py +0 -0
  74. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/cloudex/installer.py +0 -0
  75. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/data/__init__.py +0 -0
  76. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/data/resources_internal.yaml +0 -0
  77. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/data/resources_public.yaml +0 -0
  78. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/deploy_only/__init__.py +0 -0
  79. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/deploy_only/device/__init__.py +0 -0
  80. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/deploy_only/device/commands.py +0 -0
  81. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/deploy_only/mpk/__init__.py +0 -0
  82. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/deploy_only/mpk/commands.py +0 -0
  83. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/discover/__init__.py +0 -0
  84. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/download/__init__.py +0 -0
  85. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/download/parallel.py +0 -0
  86. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/install/__init__.py +0 -0
  87. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/install/compatibility.py +0 -0
  88. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/install/github_assets.py +0 -0
  89. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/install/hostdriver.py +0 -0
  90. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/install/metadata_info.py +0 -0
  91. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/install/metadata_validator.py +0 -0
  92. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/install/optiview.py +0 -0
  93. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/install/package_builder.py +0 -0
  94. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/install/palette.py +0 -0
  95. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/install/registry.py +0 -0
  96. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/mla/__init__.py +0 -0
  97. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/mla/meminfo.py +0 -0
  98. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/model_zoo/__init__.py +0 -0
  99. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/model_zoo/model.py +0 -0
  100. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/models/__init__.py +0 -0
  101. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/network/__init__.py +0 -0
  102. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/network/network.py +0 -0
  103. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/peripherals/__init__.py +0 -0
  104. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/peripherals/commands.py +0 -0
  105. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/peripherals/sentinel.py +0 -0
  106. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/playbooks/__init__.py +0 -0
  107. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/playbooks/commands.py +0 -0
  108. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/playbooks/manager.py +0 -0
  109. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/sdk/__init__.py +0 -0
  110. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/sdk/browser_ca.py +0 -0
  111. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/sdk/cmdexec.py +0 -0
  112. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/sdk/config.py +0 -0
  113. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/sdk/container_registry.py +0 -0
  114. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/sdk/docker_staging.py +0 -0
  115. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/sdk/linux_devkit_network.py +0 -0
  116. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/sdk/linux_shared_network.py +0 -0
  117. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/sdk/model_compiler.py +0 -0
  118. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/sdk/network_doctor.py +0 -0
  119. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/sdk/preinstall.py +0 -0
  120. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/sdk/requirements.json +0 -0
  121. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/sdk/script.py +0 -0
  122. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/sdk/stop.py +0 -0
  123. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/sdk/uninstall.py +0 -0
  124. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/sdk/vscode_extensions.py +0 -0
  125. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/serial/__init__.py +0 -0
  126. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/serial/serial.py +0 -0
  127. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/storage/__init__.py +0 -0
  128. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/storage/nvme.py +0 -0
  129. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/storage/sdcard.py +0 -0
  130. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/update/__init__.py +0 -0
  131. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/update/ab_state.py +0 -0
  132. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/update/bmaptool.py +0 -0
  133. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/update/bootimg.py +0 -0
  134. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/update/cleanlog.py +0 -0
  135. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/update/elxr.py +0 -0
  136. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/update/emmc.py +0 -0
  137. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/update/fast_upload.py +0 -0
  138. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/update/local.py +0 -0
  139. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/update/netboot_artifacts.py +0 -0
  140. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/update/netboot_device.py +0 -0
  141. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/update/query.py +0 -0
  142. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/update/rootfs.py +0 -0
  143. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/update/staging.py +0 -0
  144. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/update/swap.py +0 -0
  145. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/update/uboot_environment.py +0 -0
  146. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/update/updater.py +0 -0
  147. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/upgrade/__init__.py +0 -0
  148. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/upgrade/selfupdate.py +0 -0
  149. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/utils/__init__.py +0 -0
  150. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/utils/api_common.py +0 -0
  151. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/utils/artifactory.py +0 -0
  152. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/utils/common.py +0 -0
  153. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/utils/config.py +0 -0
  154. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/utils/config_loader.py +0 -0
  155. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/utils/container_registries.py +0 -0
  156. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/utils/deprecation.py +0 -0
  157. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/utils/device_api.py +0 -0
  158. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/utils/disk.py +0 -0
  159. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/utils/docker.py +0 -0
  160. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/utils/env.py +0 -0
  161. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/utils/errors.py +0 -0
  162. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/utils/mpk_api.py +0 -0
  163. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/utils/net.py +0 -0
  164. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/utils/network.py +0 -0
  165. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/utils/pcie.py +0 -0
  166. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/utils/pkg_update_check.py +0 -0
  167. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/utils/serializers.py +0 -0
  168. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/utils/services.py +0 -0
  169. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/utils/streaming.py +0 -0
  170. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/utils/tag.py +0 -0
  171. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/vulcan/__init__.py +0 -0
  172. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/vulcan/artifacts.py +0 -0
  173. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/vulcan/commands.py +0 -0
  174. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli.egg-info/dependency_links.txt +0 -0
  175. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli.egg-info/entry_points.txt +0 -0
  176. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli.egg-info/requires.txt +0 -0
  177. {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli.egg-info/top_level.txt +0 -0
  178. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/__init__.py +0 -0
  179. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/e2e/__init__.py +0 -0
  180. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/__init__.py +0 -0
  181. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_ab_overlay.py +0 -0
  182. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_app_zoo.py +0 -0
  183. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_appzoo_commands.py +0 -0
  184. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_artifact_parallel_integration.py +0 -0
  185. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_auth.py +0 -0
  186. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_auth0.py +0 -0
  187. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_bootimg_defaults.py +0 -0
  188. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_bootimg_recovery.py +0 -0
  189. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_cli.py +0 -0
  190. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_cli_stdio.py +0 -0
  191. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_cli_update.py +0 -0
  192. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_cli_update_rerun.py +0 -0
  193. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_cloudex_installer.py +0 -0
  194. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_code_ui_readiness.py +0 -0
  195. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_container_registries.py +0 -0
  196. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_deprecation.py +0 -0
  197. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_device_discovery_cloudex.py +0 -0
  198. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_docker_utils.py +0 -0
  199. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_download.py +0 -0
  200. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_elxr_update.py +0 -0
  201. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_emmc.py +0 -0
  202. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_fast_upload.py +0 -0
  203. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_firmware.py +0 -0
  204. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_firmware_creation_order.py +0 -0
  205. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_firmware_layout.py +0 -0
  206. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_firmware_selector.py +0 -0
  207. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_install_force.py +0 -0
  208. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_install_stub.py +0 -0
  209. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_localized_docs_links.py +0 -0
  210. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_metadata_installer.py +0 -0
  211. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_metadata_validator.py +0 -0
  212. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_mla_meminfo.py +0 -0
  213. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_model_deployments.py +0 -0
  214. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_model_zoo.py +0 -0
  215. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_modelzoo_commands.py +0 -0
  216. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_netboot.py +0 -0
  217. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_netboot_device.py +0 -0
  218. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_netboot_images.py +0 -0
  219. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_netboot_ip_recovery.py +0 -0
  220. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_netboot_mirror.py +0 -0
  221. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_netboot_trootctl.py +0 -0
  222. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_network_custom_static.py +0 -0
  223. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_network_safety.py +0 -0
  224. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_package_builder.py +0 -0
  225. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_palette_image_discovery.py +0 -0
  226. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_parallel_downloads.py +0 -0
  227. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_pcie.py +0 -0
  228. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_peripherals_sentinel.py +0 -0
  229. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_pkg_update_check.py +0 -0
  230. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_sdk_browser_ca.py +0 -0
  231. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_sdk_container_registry.py +0 -0
  232. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_sdk_host_key_trust.py +0 -0
  233. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_sdk_linux_shared_network.py +0 -0
  234. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_sdk_network_doctor.py +0 -0
  235. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_sdk_preinstall.py +0 -0
  236. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_sdk_uninstall.py +0 -0
  237. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_selfupdate.py +0 -0
  238. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_skills_commands.py +0 -0
  239. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_skills_manager.py +0 -0
  240. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_slot_swap.py +0 -0
  241. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_streaming_downloads.py +0 -0
  242. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_swu_errors.py +0 -0
  243. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_uboot_environment.py +0 -0
  244. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_update_staging.py +0 -0
  245. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_utils.py +0 -0
  246. {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_vulcan.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sima-cli
3
- Version: 2.1.19
3
+ Version: 2.1.20
4
4
  Summary: CLI tool for SiMa Developer Portal to download models, firmware, and apps.
5
5
  Home-page: https://community.sima.ai/
6
6
  Author: SiMa.ai
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sima-cli"
7
- version = "2.1.19"
7
+ version = "2.1.20"
8
8
  description = "CLI tool for SiMa Developer Portal to download models, firmware, and apps."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -1,2 +1,2 @@
1
1
  # sima_cli/__version__.py
2
- __version__ = "2.1.19"
2
+ __version__ = "2.1.20"
@@ -180,7 +180,7 @@ class InspectionAwareGroup(click.Group):
180
180
  _command_name_from_argv(['sima-cli'] + options) == 'bootimg'
181
181
  and any(arg in ('--netboot', '-n', '--autoflash', '-a') for arg in options)
182
182
  )
183
- if _command_name_from_argv(['sima-cli'] + options) == 'update' and any(arg in ('--inspect', '--swap') for arg in options):
183
+ if _command_name_from_argv(['sima-cli'] + options) == 'update' and any(arg in ('--inspect', '--swap', '--reset-overlay') for arg in options):
184
184
  ctx.meta['update_inspect'] = True
185
185
  return super().parse_args(ctx, args)
186
186
 
@@ -363,15 +363,16 @@ def download(ctx, url, dest):
363
363
  "--dryrun",
364
364
  is_flag=True,
365
365
  default=False,
366
- help="For eLxr, preview an update or slot swap without installing, switching slots, or rebooting."
366
+ help="For eLxr, preview an update, slot swap, or overlay reset without making changes."
367
367
  )
368
368
  @click.option("--inspect", "inspect_state", is_flag=True, help="Show eLxr 3.0+ A/B slots and overlay customizations without updating (local or --ip).")
369
+ @click.option("--reset-overlay", is_flag=True, help="Discard local overlay software and settings without updating firmware; reboot to use the clean system (eLxr 3.0+).")
369
370
  @click.option("--swap", is_flag=True, help="Select the available inactive eLxr 3.0+ slot for next boot; use --reboot to restart now.")
370
371
  @click.option("--verbose", is_flag=True, help="Show detailed overlay file categories during eLxr 3.0+ inspection or update.")
371
372
  @click.option("--signing-cert", metavar="URL_OR_FILE", help="SWUpdate PEM verification certificate: HTTP(S) URL or local file. Defaults to the certificate for the selected channel when configured (eLxr 3.0+).")
372
- @click.option("--reboot", is_flag=True, help="Reboot after successful eLxr 3.0+ installation or slot selection; verify remote boot health.")
373
+ @click.option("--reboot", is_flag=True, help="Reboot after successful eLxr 3.0+ installation, slot selection, or overlay reset.")
373
374
  @click.pass_context
374
- def update(ctx, version_or_url, version_option, ip, yes, passwd, flavor, force, troot_only, dryrun, inspect_state, verbose, reboot, signing_cert, swap):
375
+ def update(ctx, version_or_url, version_option, ip, yes, passwd, flavor, force, troot_only, dryrun, inspect_state, verbose, reboot, signing_cert, swap, reset_overlay):
375
376
  """
376
377
  Update a SiMa DevKit, remote device, or Linux PCIe host.
377
378
 
@@ -383,6 +384,10 @@ def update(ctx, version_or_url, version_option, ip, yes, passwd, flavor, force,
383
384
  eLxr 3.0+ uses signed full-system SWU bundles. Use --inspect for A/B
384
385
  state, --ip for remote updates, and --reboot to reboot after installation.
385
386
 
387
+ Use --reset-overlay to discard local software and settings without updating
388
+ firmware or switching slots. Save needed files first; the saved inventory
389
+ contains metadata only. Reboot promptly, or add --reboot. Use --dryrun to preview.
390
+
386
391
  Use --swap to select the valid inactive slot without downloading an update.
387
392
  The current and target versions are shown before confirmation; --yes skips
388
393
  confirmation. Reboot separately to apply, or add --reboot to restart now.
@@ -393,7 +398,8 @@ def update(ctx, version_or_url, version_option, ip, yes, passwd, flavor, force,
393
398
 
394
399
  The verification certificate is downloaded for the selected channel when configured; use
395
400
  --signing-cert URL_OR_FILE for an image signed with your own certificate.
396
- Developer-portal version lookup for 3.0 is not available yet.
401
+ For official eLxr 3.0+ releases, specify -v 3.0.0 (or -v 3.0) to
402
+ download the SWU from docs.sima.ai using your developer-portal login.
397
403
 
398
404
  Internal mode warns that pre-release software may be unstable and is used
399
405
  at your own risk. On eLxr 3.0+, unavailable Artifactory access falls back
@@ -501,6 +507,7 @@ def update(ctx, version_or_url, version_option, ip, yes, passwd, flavor, force,
501
507
  or troot_only
502
508
  or inspect_state
503
509
  or swap
510
+ or reset_overlay
504
511
  or verbose
505
512
  or reboot
506
513
  or force
@@ -518,6 +525,8 @@ def update(ctx, version_or_url, version_option, ip, yes, passwd, flavor, force,
518
525
  return
519
526
 
520
527
  is_elxr = is_devkit_running_elxr()
528
+ if reset_overlay and (inspect_state or swap or version_or_url or force or troot_only or signing_cert is not None or flavor != "auto"):
529
+ raise click.UsageError("--reset-overlay cannot be combined with --inspect, --swap, or installation options.")
521
530
  if swap and (inspect_state or version_or_url or force or troot_only or signing_cert is not None or flavor != "auto"):
522
531
  raise click.UsageError("--swap cannot be combined with --inspect or installation options.")
523
532
  if inspect_state and (version_or_url or dryrun or force or troot_only or reboot or signing_cert is not None or flavor != 'auto'):
@@ -527,7 +536,7 @@ def update(ctx, version_or_url, version_option, ip, yes, passwd, flavor, force,
527
536
  internal=ctx.obj.get("internal", False),
528
537
  auto_confirm=yes or ctx.obj.get("yes", False), dryrun=dryrun,
529
538
  signing_cert=signing_cert, reboot=reboot, inspect=inspect_state, force=force,
530
- troot_only=troot_only, flavor=flavor, local_elxr=is_elxr, verbose=verbose, swap=swap):
539
+ troot_only=troot_only, flavor=flavor, local_elxr=is_elxr, verbose=verbose, swap=swap, reset_overlay=reset_overlay):
531
540
  return
532
541
  except (click.ClickException, click.Abort):
533
542
  raise
@@ -815,6 +824,8 @@ def bootimg_cmd(ctx, version, boardtype, netboot, images, delete_cache, devkit_i
815
824
  else:
816
825
  write_image(version, boardtype, fwtype, internal, flavor='headless', recovery=recovery)
817
826
  click.echo("✅ Boot image successfully written.")
827
+ except click.ClickException:
828
+ raise
818
829
  except Exception as e:
819
830
  click.echo(f"❌ Failed to write boot image: {e}", err=True)
820
831
  ctx.exit(1)
@@ -7,6 +7,7 @@ import json
7
7
  import os
8
8
  import shutil
9
9
  import subprocess
10
+ import sys
10
11
  import time
11
12
  import uuid
12
13
  from pathlib import Path
@@ -17,6 +18,8 @@ import requests
17
18
 
18
19
  DEFAULT_ROOT = Path.home() / ".sima-cli" / "cloudex"
19
20
  FORWARDER_NAME = "kerrigan-p2p-forwarder"
21
+ MACOS_TUNNEL_PATH = "/usr/bin:/bin:/usr/sbin:/sbin:/opt/homebrew/bin:/usr/local/bin"
22
+ MACOS_WIREGUARD_INSTALL = "Install WireGuard CLI support with: brew install wireguard-tools"
20
23
  CONNECTION_SETUP_TIMEOUT_SECONDS = 60
21
24
  DIAGNOSTIC_LOG_LINES = 50
22
25
  SAFE_FORWARDER_STAGES = {
@@ -68,8 +71,8 @@ def decode_key(value):
68
71
  raise CloudExError("--key contains an invalid value")
69
72
  if len(profile["allocation_secret"]) < 32:
70
73
  raise CloudExError("--key contains an invalid allocation secret")
71
- if profile["environment"] != "vulcan-staging":
72
- raise CloudExError("CloudEx currently supports only vulcan-staging allocation keys")
74
+ if profile["environment"] not in {"vulcan-staging", "vulcan-production"}:
75
+ raise CloudExError("CloudEx supports only vulcan-staging or vulcan-production allocation keys")
73
76
  return profile
74
77
 
75
78
 
@@ -393,6 +396,9 @@ def _forwarder_failure(log_path):
393
396
  "start wireguard-go": "the WireGuard interface could not start",
394
397
  "invalid session config": "the native forwarder rejected the session configuration",
395
398
  }
399
+ if sys.platform == "darwin":
400
+ for needle in ("wireguard is unavailable", "start wireguard-go"):
401
+ mappings[needle] += ". " + MACOS_WIREGUARD_INSTALL
396
402
  for needle, message in mappings.items():
397
403
  if needle in detail:
398
404
  return message + suffix
@@ -552,6 +558,50 @@ def _reconcile_existing_connection(profile, store, progress):
552
558
  return None
553
559
 
554
560
 
561
+ def _forwarder_sudo_prefix(progress=None):
562
+ """Install missing macOS tools as the user, then expose them through sudo."""
563
+ prefix = ["sudo", "-n", "--"]
564
+ if sys.platform != "darwin":
565
+ return prefix
566
+ missing = [name for name in ("wg", "wireguard-go")
567
+ if shutil.which(name, path=MACOS_TUNNEL_PATH) is None]
568
+ if missing:
569
+ brew = shutil.which("brew", path=MACOS_TUNNEL_PATH)
570
+ if brew is None:
571
+ raise CloudExError(
572
+ "CloudEx requires {} on macOS. Install Homebrew, then retry CloudEx; "
573
+ "missing WireGuard dependencies will be installed automatically.".format(
574
+ ", ".join(missing)
575
+ )
576
+ )
577
+ packages = ["wireguard-tools" if name == "wg" else name for name in missing]
578
+ if progress:
579
+ progress.update("Installing missing WireGuard dependencies with Homebrew")
580
+ environment = dict(os.environ, PATH=MACOS_TUNNEL_PATH)
581
+ try:
582
+ result = subprocess.run(
583
+ [brew, "install", *packages],
584
+ env=environment, stdin=subprocess.DEVNULL,
585
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=600,
586
+ )
587
+ except (OSError, subprocess.TimeoutExpired) as exc:
588
+ raise CloudExError(
589
+ "Could not install WireGuard dependencies with Homebrew. " + MACOS_WIREGUARD_INSTALL
590
+ ) from exc
591
+ if result.returncode:
592
+ detail = (result.stderr or result.stdout or "Homebrew failed").strip()
593
+ raise CloudExError("WireGuard dependency installation failed: " + detail[-1024:])
594
+ missing = [name for name in ("wg", "wireguard-go")
595
+ if shutil.which(name, path=MACOS_TUNNEL_PATH) is None]
596
+ if missing:
597
+ raise CloudExError(
598
+ "Homebrew completed but CloudEx still cannot find: {}. {}".format(
599
+ ", ".join(missing), MACOS_WIREGUARD_INSTALL
600
+ )
601
+ )
602
+ return prefix + ["/usr/bin/env", "PATH=" + MACOS_TUNNEL_PATH]
603
+
604
+
555
605
  def connect(profile, store, progress, attempts=3, transport="auto"):
556
606
  """Create a session and return its durable local record."""
557
607
  from .installer import ensure_forwarder
@@ -561,6 +611,7 @@ def connect(profile, store, progress, attempts=3, transport="auto"):
561
611
  existing = _reconcile_existing_connection(profile, store, progress)
562
612
  if existing:
563
613
  return existing
614
+ sudo_prefix = _forwarder_sudo_prefix(progress=progress)
564
615
  forwarder = ensure_forwarder(progress=progress)
565
616
  choices = (["p2p"] * attempts + ["routed"] if transport == "auto"
566
617
  else ["routed"] if transport == "turn" else ["p2p"] * attempts)
@@ -610,8 +661,8 @@ def connect(profile, store, progress, attempts=3, transport="auto"):
610
661
  config_path.chmod(0o600)
611
662
  with log_path.open("x", encoding="utf-8") as log:
612
663
  process = subprocess.Popen(
613
- [
614
- "sudo", "-n", "--", str(forwarder), "session", "--role", "user",
664
+ sudo_prefix + [
665
+ str(forwarder), "session", "--role", "user",
615
666
  "--config", str(config_path), "--report", str(report_path),
616
667
  ],
617
668
  stdin=subprocess.DEVNULL,
@@ -5,6 +5,7 @@ import subprocess
5
5
  import json
6
6
  import time
7
7
  import re
8
+ import shutil
8
9
  import psutil
9
10
  from typing import List, Dict
10
11
  from rich.console import Console
@@ -51,6 +52,9 @@ def get_sima_devices_from_arp():
51
52
  try:
52
53
  output = subprocess.check_output(["ip", "neigh", "show"], text=True)
53
54
  except FileNotFoundError:
55
+ if not shutil.which("arp"):
56
+ console.print("[yellow]ARP lookup unavailable: install iproute2 (ip) or net-tools (arp); continuing with multicast discovery.[/yellow]")
57
+ return []
54
58
  output = subprocess.check_output(["arp", "-n"], text=True)
55
59
  pattern = re.compile(
56
60
  r"(?P<ip>\d+\.\d+\.\d+\.\d+).*?(?P<mac>(?:[0-9a-f]{1,2}:){5}[0-9a-f]{1,2})",
@@ -11,9 +11,41 @@ import re
11
11
  import time
12
12
  import shlex
13
13
  import subprocess
14
+ import shutil
15
+ import sys
16
+
17
+ import click
14
18
  from sima_cli.utils.env import get_environment_type
15
19
  from typing import Optional, Dict
16
20
 
21
+ def ensure_discovery_tools():
22
+ """Provision required Linux discovery tools only after an interactive opt-in."""
23
+ if shutil.which('ip'):
24
+ return
25
+ guidance = (
26
+ "Device discovery needs 'ip' (package iproute2). "
27
+ "On Ubuntu/Debian run: sudo apt-get update && sudo apt-get install -y iproute2 ethtool. "
28
+ "For netboot, pass --devkit-ip <IPv4> to bypass auto-discovery."
29
+ )
30
+ if not shutil.which('apt-get') or not sys.stdin.isatty():
31
+ raise click.ClickException(guidance)
32
+ packages = ['iproute2']
33
+ if not shutil.which('ethtool'):
34
+ packages.append('ethtool')
35
+ if not click.confirm('Device discovery needs host packages. Install ' + ' '.join(packages) + '?', default=False):
36
+ raise click.ClickException(guidance)
37
+ prefix = [] if os.geteuid() == 0 else ['sudo']
38
+ if prefix and not shutil.which('sudo'):
39
+ raise click.ClickException(guidance + ' sudo is unavailable; ask an administrator to install them.')
40
+ try:
41
+ subprocess.run(prefix + ['apt-get', 'update'], check=True)
42
+ subprocess.run(prefix + ['apt-get', 'install', '-y', '--no-install-recommends'] + packages, check=True)
43
+ except (OSError, subprocess.CalledProcessError) as error:
44
+ raise click.ClickException(f'Device discovery package installation failed: {error}. {guidance}') from error
45
+ if not shutil.which('ip'):
46
+ raise click.ClickException(guidance)
47
+
48
+
17
49
  def _run(cmd: str, check=False, capture=True) -> subprocess.CompletedProcess:
18
50
  return subprocess.run(
19
51
  shlex.split(cmd),
@@ -24,7 +56,7 @@ def _run(cmd: str, check=False, capture=True) -> subprocess.CompletedProcess:
24
56
  )
25
57
 
26
58
  def _has_nmcli() -> bool:
27
- return _run("which nmcli").returncode == 0
59
+ return shutil.which("nmcli") is not None
28
60
 
29
61
 
30
62
  def _iface_has_ipv4(iface: str) -> bool:
@@ -41,7 +73,7 @@ def _get_mac(iface: str) -> str:
41
73
  def _is_usb_ethernet(iface: str) -> bool:
42
74
  """Return True if this interface is a USB Ethernet device."""
43
75
  dev_path = f"/sys/class/net/{iface}/device"
44
- if not os.path.exists(dev_path):
76
+ if not os.path.exists(dev_path) or not shutil.which("udevadm"):
45
77
  return False
46
78
 
47
79
  cp = _run(f"udevadm info -q property -p {dev_path}", capture=True)
@@ -63,7 +95,7 @@ def _usb_attrs(iface: str) -> Dict[str, str]:
63
95
  Returns possibly empty dict.
64
96
  """
65
97
  dev_path = f"/sys/class/net/{iface}/device"
66
- if not os.path.exists(dev_path):
98
+ if not os.path.exists(dev_path) or not shutil.which("udevadm"):
67
99
  return {}
68
100
 
69
101
  cp = _run(f"udevadm info -q property -p {dev_path}", capture=True)
@@ -100,6 +132,8 @@ def _usb_attrs(iface: str) -> Dict[str, str]:
100
132
 
101
133
 
102
134
  def _driver_info(iface: str) -> Dict[str, str]:
135
+ if not shutil.which("ethtool"):
136
+ return {"driver": "", "bus-info": "", "firmware": ""}
103
137
  cp = _run(f"ethtool -i {iface}", capture=True)
104
138
  info = {}
105
139
  for line in cp.stdout.splitlines():
@@ -227,6 +261,7 @@ def suggest_and_switch_to_linklocal():
227
261
  envtype, env_subtype = get_environment_type()
228
262
 
229
263
  if envtype == 'host' and env_subtype == 'linux':
264
+ ensure_discovery_tools()
230
265
  cp = _run("ip -o link", capture=True)
231
266
  ifaces = []
232
267
  for line in cp.stdout.splitlines():
@@ -1,4 +1,5 @@
1
1
  import os
2
+ import time
2
3
  import json
3
4
  import hashlib
4
5
  import re
@@ -13,6 +14,40 @@ from sima_cli.utils.config import get_auth_token
13
14
  from sima_cli.auth.login import login
14
15
  from sima_cli.install.github_assets import download_github_asset
15
16
 
17
+ # Separate connect and read limits; a cold CDN request can exceed ten seconds.
18
+ _DOWNLOAD_TIMEOUT = (10, 60)
19
+ _DOWNLOAD_ATTEMPTS = 3
20
+ _RETRYABLE_STATUS_CODES = {429, 500, 502, 503, 504}
21
+
22
+
23
+ def _request_download(request_fn, url, **kwargs):
24
+ """Retry transient failures before response headers, never replay a body."""
25
+ for attempt in range(_DOWNLOAD_ATTEMPTS):
26
+ response = None
27
+ try:
28
+ response = request_fn(url, timeout=_DOWNLOAD_TIMEOUT, **kwargs)
29
+ response.raise_for_status()
30
+ return response
31
+ except requests.exceptions.SSLError:
32
+ raise
33
+ except requests.HTTPError:
34
+ if response is None or response.status_code not in _RETRYABLE_STATUS_CODES:
35
+ if response is not None:
36
+ response.close()
37
+ raise
38
+ response.close()
39
+ if attempt == _DOWNLOAD_ATTEMPTS - 1:
40
+ raise
41
+ except (requests.Timeout, requests.ConnectionError):
42
+ if attempt == _DOWNLOAD_ATTEMPTS - 1:
43
+ raise
44
+ delay = 2 ** attempt
45
+ # Do not print URLs: authenticated downloads can include credentials.
46
+ print(f"⚠️ Transient download request failure; retrying in {delay}s "
47
+ f"(attempt {attempt + 2}/{_DOWNLOAD_ATTEMPTS}).")
48
+ time.sleep(delay)
49
+
50
+
16
51
  def _list_directory_files(url: str, internal: bool = False) -> List[str]:
17
52
  """
18
53
  Attempt to list files in a server-hosted directory with index browsing enabled.
@@ -110,7 +145,7 @@ def download_file_from_url(url: str, dest_folder: str = ".", internal: bool = Fa
110
145
  head_fn = session.head
111
146
 
112
147
  # Only resume a partial when a strong validator identifies its bytes.
113
- head = head_fn(url, headers=headers, timeout=10, allow_redirects=True)
148
+ head = _request_download(head_fn, url, headers=headers, allow_redirects=True)
114
149
  try:
115
150
  head.raise_for_status()
116
151
  raw_size = head.headers.get('content-length')
@@ -148,7 +183,7 @@ def download_file_from_url(url: str, dest_folder: str = ".", internal: bool = Fa
148
183
  headers['Range'] = f'bytes={existing_size}-'
149
184
  headers['If-Range'] = etag
150
185
 
151
- with request_fn(url, stream=True, headers=headers, timeout=30) as r:
186
+ with _request_download(request_fn, url, stream=True, headers=headers) as r:
152
187
  r.raise_for_status()
153
188
  if r.headers.get('Content-Encoding', 'identity').lower() != 'identity':
154
189
  raise ValueError("Server ignored identity content encoding")
@@ -693,11 +693,17 @@ def selectable_resource_handler(metadata):
693
693
  - 'Skip' option appended at the end
694
694
  - Honors `selected: true` and existing `resources` for preselection
695
695
  - Removes unselected items from `selectable-resources` field
696
+ - Skips opt-in resources when stdin is not a terminal
696
697
  """
697
698
  selectable = metadata.get("selectable-resources")
698
699
  if not selectable:
699
700
  return metadata
700
701
 
702
+ if sys.stdin is None or not sys.stdin.isatty():
703
+ click.echo("Skipping optional resources because stdin is not a terminal.")
704
+ metadata["selectable-resources"] = []
705
+ return metadata
706
+
701
707
  # ──────────────────────────────────────────────
702
708
  # Build choice list with preselection
703
709
  # ──────────────────────────────────────────────
@@ -1430,8 +1436,8 @@ def _run_installation_script(metadata: Dict, extract_path: str = ".", force: boo
1430
1436
  Behavior:
1431
1437
  • Force installs set FORCE_INSTALL=1 in the script environment.
1432
1438
  • macOS / Linux / eLxr:
1433
- - Run via interactive login shell unless nested inside sima-cli
1434
- - Safely source the first available RC file among:
1439
+ - Run via non-interactive login shell unless nested inside sima-cli
1440
+ - Nested installs source the first available RC file among:
1435
1441
  ~/.zshrc → ~/.bashrc → ~/.bash_profile → ~/.profile
1436
1442
  • Windows:
1437
1443
  - Run via cmd.exe or PowerShell
@@ -1487,9 +1493,9 @@ def _run_installation_script(metadata: Dict, extract_path: str = ".", force: boo
1487
1493
  cmd = ["bash", "-c", f"{rc_source}{script}"]
1488
1494
 
1489
1495
  else:
1490
- # Normal context → interactive login shell
1491
- shell_name = os.path.basename(shell)
1492
- cmd = [shell, "-i", "-l", "-c", script]
1496
+ # Load login setup without enabling interactive job control on
1497
+ # the caller's terminal (including when invoked over ssh -t).
1498
+ cmd = [shell, "-l", "-c", script]
1493
1499
 
1494
1500
  result = subprocess.run(cmd, cwd=extract_path, env=env)
1495
1501
  exit_code = result.returncode
@@ -23,9 +23,9 @@ from sima_cli.download.parallel import PARALLEL_MIN_SIZE, download_parallel, con
23
23
 
24
24
  ORGANIZATION = "sima-neat"
25
25
  REPOSITORY = "models"
26
+ PRODUCTION_BASE_URL = "https://models-registry.neat.sima.ai"
26
27
  STAGING_BASE_URL = "https://models-registry.stg.neat.sima.ai"
27
28
  TRANSIENT_STATUS_CODES = (429, 502, 503, 504)
28
- SUPPORTED_BENCHMARK_SCHEMAS = (1, 2, 3, 4, 5)
29
29
 
30
30
 
31
31
  class ModelRegistryError(RuntimeError):
@@ -51,10 +51,7 @@ def resolve_base_url(staging: bool = False, override: Optional[str] = None) -> s
51
51
  production = os.environ.get("SIMA_MODELS_REGISTRY_PRODUCTION_URL")
52
52
  if production:
53
53
  return normalize_base_url(production)
54
- raise ModelRegistryError(
55
- "The production Model Registry endpoint is not configured. "
56
- "Use --stg or set SIMA_MODELS_REGISTRY_PRODUCTION_URL."
57
- )
54
+ return PRODUCTION_BASE_URL
58
55
 
59
56
 
60
57
  def _new_session() -> requests.Session:
@@ -134,6 +131,17 @@ def latest_runs_by_model_variant(runs: Sequence[Dict[str, Any]]) -> List[Dict[st
134
131
  )
135
132
 
136
133
 
134
+ def _benchmark_fingerprints(model: Dict[str, Any], profile: str, run_ids: Sequence[str]) -> Dict[str, str]:
135
+ """A fingerprint names the builds it measured, so only a row showing exactly the catalog builds keeps it."""
136
+ builds = {
137
+ build.get("run_id") for build in model.get("builds") or []
138
+ if isinstance(build, dict) and build.get("build_profile_id") == profile
139
+ }
140
+ if not run_ids or set(run_ids) != builds:
141
+ return {}
142
+ return (model.get("benchmark_fingerprints") or {}).get(profile) or {}
143
+
144
+
137
145
  def find_latest_run(
138
146
  runs: Sequence[Dict[str, Any]], model_id: str, variant_id: str
139
147
  ) -> Dict[str, Any]:
@@ -458,7 +466,13 @@ class RegistryClient:
458
466
  payload = self._get_json(
459
467
  ("v1", "repositories", ORGANIZATION, REPOSITORY, "catalogs", "current"),
460
468
  params={"git_ref": f"refs/heads/{branch}"},
469
+ allow_not_found=True,
461
470
  )
471
+ if payload is None:
472
+ raise ModelRegistryError(
473
+ f"No published model catalog for branch {branch!r} at {self.base_url} (HTTP 404). "
474
+ "Use models branches to find available branches; no fallback was attempted."
475
+ )
462
476
  catalog = payload.get("catalog") if payload else None
463
477
  document = catalog.get("document") if isinstance(catalog, dict) else None
464
478
  models = document.get("models") if isinstance(document, dict) else None
@@ -513,6 +527,10 @@ class RegistryClient:
513
527
  )
514
528
  artifact_metadata = artifact.get("metadata") or {}
515
529
  model = definitions[model_id]
530
+ shown_run_ids = (
531
+ list((metadata.get("component_run_ids") or {}).values())
532
+ if root.get("package_id") else [run["id"]]
533
+ )
516
534
  searchable = " ".join(str(value) for value in (
517
535
  model_id, profile, model.get("display_name"), model.get("description"),
518
536
  model.get("categories"), branch,
@@ -529,6 +547,7 @@ class RegistryClient:
529
547
  "model_categories": model.get("categories") or {},
530
548
  "size_bytes": artifact_metadata.get("deployment_size_bytes", artifact.get("size_bytes")),
531
549
  "sha256": artifact.get("sha256"),
550
+ "benchmark_fingerprints": _benchmark_fingerprints(model, profile, shown_run_ids),
532
551
  })
533
552
 
534
553
  return sorted(
@@ -607,15 +626,23 @@ class RegistryClient:
607
626
  payload = self._get_json(("v1", "runs", run_id, "metrics"))
608
627
  return payload or {}
609
628
 
610
- def benchmark(self, run_id: str) -> Optional[Dict[str, Any]]:
611
- return self._get_json(
612
- ("v1", "runs", run_id, "benchmarks", "latest"), allow_not_found=True
613
- )
614
-
615
- def accuracy(self, run_id: str) -> Optional[Dict[str, Any]]:
616
- return self._get_json(
617
- ("v1", "runs", run_id, "accuracy", "latest"), allow_not_found=True
629
+ def whole_model_result(self, stage: str, fingerprint: Optional[str]) -> Optional[Dict[str, Any]]:
630
+ """Return the model test result of the newest completed run for a benchmark fingerprint."""
631
+ if not fingerprint:
632
+ return None
633
+ resolved = self._get_json(
634
+ ("v1", "repositories", ORGANIZATION, REPOSITORY, "resolve"),
635
+ params={"run_type": f"model_{stage}", "fingerprint": fingerprint},
636
+ allow_not_found=True,
618
637
  )
638
+ run_id = ((resolved or {}).get("run") or {}).get("id")
639
+ if not run_id:
640
+ return None
641
+ results = [
642
+ result for result in self.test_results(str(run_id)).get("test_results") or []
643
+ if isinstance(result, dict) and result.get("suite") == stage and result.get("name") == "model"
644
+ ]
645
+ return results[-1] if results else None
619
646
 
620
647
  def taxonomy(self) -> Optional[Dict[str, Any]]:
621
648
  return self._get_json(
@@ -629,8 +656,6 @@ class RegistryClient:
629
656
  "provenance": self.provenance(run_id),
630
657
  "tests": self.test_results(run_id),
631
658
  "metrics": self.metrics(run_id),
632
- "benchmark": self.benchmark(run_id),
633
- "accuracy": self.accuracy(run_id),
634
659
  }
635
660
 
636
661
  def artifact_download(self, run_id: str, name: str) -> Dict[str, Any]:
@@ -32,7 +32,7 @@ def _staging_option(function):
32
32
  "staging",
33
33
  is_flag=True,
34
34
  default=False,
35
- help="Use the staging Model Registry.",
35
+ help="Use the staging Model Registry unless SIMA_MODELS_REGISTRY_BASE_URL is set.",
36
36
  )(function)
37
37
 
38
38
 
@@ -107,6 +107,7 @@ def _selected_model_detail(
107
107
  client: RegistryClient, selected: Dict[str, Any]
108
108
  ) -> Dict[str, Any]:
109
109
  selected_metadata = selected.get("metadata") or {}
110
+ fingerprints = selected.get("benchmark_fingerprints") or {}
110
111
  component_run_ids = selected_metadata.get("component_run_ids") or {}
111
112
  if isinstance(component_run_ids, dict) and component_run_ids:
112
113
  components = sorted(
@@ -169,18 +170,30 @@ def _selected_model_detail(
169
170
  for metric in (detail.get("metrics") or {}).get("measurements") or []
170
171
  ]
171
172
  },
172
- "benchmark": next(
173
- (detail.get("benchmark") for _, detail in details if detail.get("benchmark")),
174
- None,
175
- ),
176
- "accuracy": next(
177
- (detail.get("accuracy") for _, detail in details if detail.get("accuracy")),
178
- None,
179
- ),
173
+ "benchmark": client.whole_model_result("performance", fingerprints.get("performance")),
174
+ "accuracy": client.whole_model_result("accuracy", fingerprints.get("accuracy")),
180
175
  }
181
176
 
182
177
 
183
- @click.group(name="models", help="Browse and download Model Registry artifacts.", hidden=True)
178
+ @click.group(name="models", hidden=True, help=(
179
+ "Browse and download Model Registry artifacts from production by default.\n\n"
180
+ "Uses https://models-registry.neat.sima.ai without AWS credentials. "
181
+ "This preview command remains hidden from top-level help.\n\n"
182
+ "SIMA_MODELS_REGISTRY_BASE_URL overrides all CLI endpoint selections, including --stg. "
183
+ "Otherwise --stg/--staging selects https://models-registry.stg.neat.sima.ai; "
184
+ "SIMA_MODELS_REGISTRY_PRODUCTION_URL overrides only the production default. "
185
+ "The Python resolve_base_url helper's explicit override takes precedence over environment variables.\n\n"
186
+ "Branch listings describe registered runs; listed branches may lack a published catalog. "
187
+ "Unpublished catalogs report an error; published catalogs with no matching builds return "
188
+ "an empty models array with --json or a message interactively. "
189
+ "No fallback to another branch or environment is attempted.\n\n"
190
+ "Examples (choose a currently published branch and model):\n\n\b\n"
191
+ " sima-cli models branches --json\n"
192
+ " sima-cli models list --branch BRANCH --json\n"
193
+ " sima-cli models download --branch BRANCH --id MODEL --variant VARIANT --output ./models --json\n"
194
+ " sima-cli models --stg branches --json\n"
195
+ " sima-cli models list --staging --branch develop --json"
196
+ ))
184
197
  @_staging_option
185
198
  @click.pass_context
186
199
  def models_group(ctx: click.Context, staging: bool) -> None:
@@ -260,7 +273,7 @@ def list_command(
260
273
  echo_json({"branch": branch, "query": query, "models": runs})
261
274
  return
262
275
  if not runs:
263
- click.echo("No completed model builds are available on this branch.")
276
+ click.echo(f"No completed model builds match branch {branch!r} and the requested query.")
264
277
  return
265
278
  if not _is_interactive():
266
279
  raise ModelRegistryError(