morphcloud 0.1.113__tar.gz → 0.1.114__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 (224) hide show
  1. {morphcloud-0.1.113 → morphcloud-0.1.114}/PKG-INFO +3 -11
  2. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/__init__.py +2 -5
  3. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/_asyncify.py +28 -125
  4. morphcloud-0.1.114/morphcloud/_bash_interpreter.py +146 -0
  5. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/_http_transport.py +12 -35
  6. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/_scramble.py +35 -144
  7. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/_ssh.py +53 -211
  8. morphcloud-0.1.114/morphcloud/_synapse.py +363 -0
  9. morphcloud-0.1.114/morphcloud/_utils.py +84 -0
  10. morphcloud-0.1.114/morphcloud/api.py +2929 -0
  11. morphcloud-0.1.114/morphcloud/cli.py +845 -0
  12. morphcloud-0.1.114/morphcloud/cli_helpers.py +75 -0
  13. morphcloud-0.1.114/morphcloud/cli_runtime.py +52 -0
  14. morphcloud-0.1.114/morphcloud/config.py +207 -0
  15. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/__init__.py +1 -7
  16. morphcloud-0.1.114/morphcloud/devbox/cli.py +1110 -0
  17. morphcloud-0.1.114/morphcloud/devbox/client.py +278 -0
  18. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/config.py +4 -11
  19. morphcloud-0.1.114/morphcloud/devbox/gen/__init__.py +5 -0
  20. morphcloud-0.1.114/morphcloud/devbox/gen/admin/__init__.py +1 -0
  21. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/admin/client.py +6 -17
  22. morphcloud-0.1.114/morphcloud/devbox/gen/admin/raw_client.py +77 -0
  23. morphcloud-0.1.114/morphcloud/devbox/gen/aliases/__init__.py +1 -0
  24. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/aliases/client.py +7 -20
  25. morphcloud-0.1.114/morphcloud/devbox/gen/aliases/raw_client.py +145 -0
  26. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/client.py +12 -100
  27. morphcloud-0.1.114/morphcloud/devbox/gen/core/__init__.py +13 -0
  28. morphcloud-0.1.114/morphcloud/devbox/gen/core/api_error.py +14 -0
  29. morphcloud-0.1.114/morphcloud/devbox/gen/core/client_wrapper.py +45 -0
  30. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/core/datetime_utils.py +2 -10
  31. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/core/file.py +6 -33
  32. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/core/force_multipart.py +0 -5
  33. morphcloud-0.1.114/morphcloud/devbox/gen/core/http_client.py +176 -0
  34. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/core/http_response.py +1 -12
  35. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/core/jsonable_encoder.py +10 -25
  36. morphcloud-0.1.114/morphcloud/devbox/gen/core/pydantic_utilities.py +178 -0
  37. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/core/query_encoder.py +2 -15
  38. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/core/remove_none_from_dict.py +0 -3
  39. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/core/request_options.py +1 -6
  40. morphcloud-0.1.114/morphcloud/devbox/gen/core/serialization.py +151 -0
  41. morphcloud-0.1.114/morphcloud/devbox/gen/devboxes_actions/__init__.py +1 -0
  42. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/devboxes_actions/client.py +38 -175
  43. morphcloud-0.1.114/morphcloud/devbox/gen/devboxes_actions/raw_client.py +564 -0
  44. morphcloud-0.1.114/morphcloud/devbox/gen/devboxes_core/__init__.py +1 -0
  45. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/devboxes_core/client.py +17 -50
  46. morphcloud-0.1.114/morphcloud/devbox/gen/devboxes_core/raw_client.py +232 -0
  47. morphcloud-0.1.114/morphcloud/devbox/gen/devboxes_lifecycle/__init__.py +1 -0
  48. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/devboxes_lifecycle/client.py +11 -50
  49. morphcloud-0.1.114/morphcloud/devbox/gen/devboxes_lifecycle/raw_client.py +145 -0
  50. morphcloud-0.1.114/morphcloud/devbox/gen/errors/__init__.py +2 -0
  51. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/errors/unprocessable_entity_error.py +2 -9
  52. morphcloud-0.1.114/morphcloud/devbox/gen/templates/__init__.py +1 -0
  53. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/templates/client.py +45 -248
  54. morphcloud-0.1.114/morphcloud/devbox/gen/templates/raw_client.py +673 -0
  55. morphcloud-0.1.114/morphcloud/devbox/gen/tmux/__init__.py +1 -0
  56. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/tmux/client.py +34 -207
  57. morphcloud-0.1.114/morphcloud/devbox/gen/tmux/raw_client.py +565 -0
  58. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/__init__.py +1 -47
  59. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/alias_response.py +1 -7
  60. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/branch_response.py +1 -7
  61. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/delete_response.py +1 -7
  62. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/devbox_detail_list_response.py +1 -7
  63. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/devbox_list_response.py +1 -7
  64. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/devbox_refs.py +1 -7
  65. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/devbox_response.py +1 -7
  66. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/devbox_spec.py +1 -7
  67. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/devbox_ssh_credentials_response.py +1 -7
  68. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/devbox_summary_response.py +1 -7
  69. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/http_service.py +1 -7
  70. morphcloud-0.1.114/morphcloud/devbox/gen/types/http_service_auth_mode.py +2 -0
  71. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/http_validation_error.py +1 -7
  72. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/networking.py +1 -7
  73. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/secret_value_response.py +1 -7
  74. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/snapshot_response.py +1 -7
  75. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/success_response.py +1 -7
  76. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/template_cache_request.py +1 -7
  77. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/template_cache_start_response.py +1 -7
  78. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/template_detail.py +1 -7
  79. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/template_list_response.py +1 -7
  80. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/template_share_response.py +1 -7
  81. morphcloud-0.1.114/morphcloud/devbox/gen/types/template_status.py +2 -0
  82. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/template_step_model.py +1 -7
  83. morphcloud-0.1.114/morphcloud/devbox/gen/types/template_step_status.py +2 -0
  84. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/template_summary.py +1 -7
  85. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/timeline_event.py +1 -7
  86. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/tmux_debug_info_response.py +1 -7
  87. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/tmux_install_response.py +1 -7
  88. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/tmux_new_session_response.py +1 -7
  89. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/tmux_send_keys_response.py +1 -7
  90. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/tmux_session.py +1 -7
  91. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/tmux_sessions_list.py +1 -7
  92. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/tmux_snapshot_response.py +1 -7
  93. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/tmux_target.py +1 -7
  94. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/ttl.py +1 -7
  95. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/upload_response.py +1 -7
  96. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/validation_error.py +1 -7
  97. morphcloud-0.1.114/morphcloud/devbox/gen/types/validation_error_loc_item.py +2 -0
  98. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/wake_on.py +1 -7
  99. morphcloud-0.1.114/morphcloud/devbox/gen/uploads/__init__.py +1 -0
  100. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/uploads/client.py +10 -59
  101. morphcloud-0.1.114/morphcloud/devbox/gen/uploads/raw_client.py +159 -0
  102. morphcloud-0.1.114/morphcloud/devbox/gen/user_secrets/__init__.py +1 -0
  103. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/user_secrets/client.py +6 -17
  104. morphcloud-0.1.114/morphcloud/devbox/gen/user_secrets/raw_client.py +85 -0
  105. morphcloud-0.1.114/morphcloud/devbox/template_local_runner.py +397 -0
  106. morphcloud-0.1.114/morphcloud/devbox/template_runner.py +1270 -0
  107. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/terminals.py +33 -87
  108. morphcloud-0.1.114/morphcloud/devbox_commands.py +137 -0
  109. morphcloud-0.1.114/morphcloud/experimental/__init__.py +420 -0
  110. morphcloud-0.1.114/morphcloud/experimental/browser.py +551 -0
  111. morphcloud-0.1.114/morphcloud/images.py +26 -0
  112. morphcloud-0.1.114/morphcloud/instances.py +335 -0
  113. morphcloud-0.1.114/morphcloud/plugin_commands.py +63 -0
  114. morphcloud-0.1.114/morphcloud/plugins/cli.py +571 -0
  115. morphcloud-0.1.114/morphcloud/plugins/installer.py +247 -0
  116. morphcloud-0.1.114/morphcloud/profiles.py +96 -0
  117. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/simple_index/__init__.py +1 -5
  118. morphcloud-0.1.114/morphcloud/simple_index/client.py +221 -0
  119. morphcloud-0.1.114/morphcloud/snapshots.py +106 -0
  120. morphcloud-0.1.114/morphcloud/users.py +196 -0
  121. morphcloud-0.1.114/morphcloud/volume_commands.py +36 -0
  122. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/volumes/__init__.py +1 -8
  123. morphcloud-0.1.114/morphcloud/volumes/cli.py +388 -0
  124. morphcloud-0.1.114/morphcloud/volumes/client.py +307 -0
  125. morphcloud-0.1.114/pyproject.toml +33 -0
  126. morphcloud-0.1.113/morphcloud/_bash_interpreter.py +0 -228
  127. morphcloud-0.1.113/morphcloud/_llm.py +0 -613
  128. morphcloud-0.1.113/morphcloud/_utils.py +0 -117
  129. morphcloud-0.1.113/morphcloud/api.py +0 -4968
  130. morphcloud-0.1.113/morphcloud/cli.py +0 -2985
  131. morphcloud-0.1.113/morphcloud/cli_helpers.py +0 -109
  132. morphcloud-0.1.113/morphcloud/config.py +0 -327
  133. morphcloud-0.1.113/morphcloud/devbox/cli.py +0 -1889
  134. morphcloud-0.1.113/morphcloud/devbox/client.py +0 -465
  135. morphcloud-0.1.113/morphcloud/devbox/gen/__init__.py +0 -114
  136. morphcloud-0.1.113/morphcloud/devbox/gen/admin/__init__.py +0 -3
  137. morphcloud-0.1.113/morphcloud/devbox/gen/admin/raw_client.py +0 -138
  138. morphcloud-0.1.113/morphcloud/devbox/gen/aliases/__init__.py +0 -3
  139. morphcloud-0.1.113/morphcloud/devbox/gen/aliases/raw_client.py +0 -246
  140. morphcloud-0.1.113/morphcloud/devbox/gen/core/__init__.py +0 -52
  141. morphcloud-0.1.113/morphcloud/devbox/gen/core/api_error.py +0 -23
  142. morphcloud-0.1.113/morphcloud/devbox/gen/core/client_wrapper.py +0 -89
  143. morphcloud-0.1.113/morphcloud/devbox/gen/core/http_client.py +0 -620
  144. morphcloud-0.1.113/morphcloud/devbox/gen/core/pydantic_utilities.py +0 -306
  145. morphcloud-0.1.113/morphcloud/devbox/gen/core/serialization.py +0 -282
  146. morphcloud-0.1.113/morphcloud/devbox/gen/devboxes_actions/__init__.py +0 -3
  147. morphcloud-0.1.113/morphcloud/devbox/gen/devboxes_actions/raw_client.py +0 -1213
  148. morphcloud-0.1.113/morphcloud/devbox/gen/devboxes_core/__init__.py +0 -3
  149. morphcloud-0.1.113/morphcloud/devbox/gen/devboxes_core/raw_client.py +0 -431
  150. morphcloud-0.1.113/morphcloud/devbox/gen/devboxes_lifecycle/__init__.py +0 -3
  151. morphcloud-0.1.113/morphcloud/devbox/gen/devboxes_lifecycle/raw_client.py +0 -298
  152. morphcloud-0.1.113/morphcloud/devbox/gen/errors/__init__.py +0 -7
  153. morphcloud-0.1.113/morphcloud/devbox/gen/templates/__init__.py +0 -3
  154. morphcloud-0.1.113/morphcloud/devbox/gen/templates/raw_client.py +0 -1450
  155. morphcloud-0.1.113/morphcloud/devbox/gen/tmux/__init__.py +0 -3
  156. morphcloud-0.1.113/morphcloud/devbox/gen/tmux/raw_client.py +0 -1188
  157. morphcloud-0.1.113/morphcloud/devbox/gen/types/http_service_auth_mode.py +0 -5
  158. morphcloud-0.1.113/morphcloud/devbox/gen/types/template_status.py +0 -7
  159. morphcloud-0.1.113/morphcloud/devbox/gen/types/template_step_status.py +0 -7
  160. morphcloud-0.1.113/morphcloud/devbox/gen/types/validation_error_loc_item.py +0 -5
  161. morphcloud-0.1.113/morphcloud/devbox/gen/uploads/__init__.py +0 -3
  162. morphcloud-0.1.113/morphcloud/devbox/gen/uploads/raw_client.py +0 -332
  163. morphcloud-0.1.113/morphcloud/devbox/gen/user_secrets/__init__.py +0 -3
  164. morphcloud-0.1.113/morphcloud/devbox/gen/user_secrets/raw_client.py +0 -146
  165. morphcloud-0.1.113/morphcloud/devbox/template_local_runner.py +0 -783
  166. morphcloud-0.1.113/morphcloud/devbox/template_runner.py +0 -1979
  167. morphcloud-0.1.113/morphcloud/experimental/__init__.py +0 -697
  168. morphcloud-0.1.113/morphcloud/experimental/browser.py +0 -862
  169. morphcloud-0.1.113/morphcloud/plugins/cli.py +0 -1164
  170. morphcloud-0.1.113/morphcloud/plugins/installer.py +0 -569
  171. morphcloud-0.1.113/morphcloud/simple_index/client.py +0 -415
  172. morphcloud-0.1.113/morphcloud/volumes/cli.py +0 -575
  173. morphcloud-0.1.113/morphcloud/volumes/client.py +0 -536
  174. morphcloud-0.1.113/pyproject.toml +0 -57
  175. morphcloud-0.1.113/requirements.txt +0 -108
  176. {morphcloud-0.1.113 → morphcloud-0.1.114}/.github/workflows/publish.yaml +0 -0
  177. {morphcloud-0.1.113 → morphcloud-0.1.114}/.gitignore +0 -0
  178. {morphcloud-0.1.113 → morphcloud-0.1.114}/LICENSE +0 -0
  179. {morphcloud-0.1.113 → morphcloud-0.1.114}/Makefile +0 -0
  180. {morphcloud-0.1.113 → morphcloud-0.1.114}/README.md +0 -0
  181. {morphcloud-0.1.113 → morphcloud-0.1.114}/conf.py +0 -0
  182. {morphcloud-0.1.113 → morphcloud-0.1.114}/examples/browser_example.py +0 -0
  183. {morphcloud-0.1.113 → morphcloud-0.1.114}/examples/devbox_example.py +0 -0
  184. {morphcloud-0.1.113 → morphcloud-0.1.114}/index.rst +0 -0
  185. {morphcloud-0.1.113 → morphcloud-0.1.114}/make.bat +0 -0
  186. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/openapi.json +0 -0
  187. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/plugins/__init__.py +0 -0
  188. {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/py.typed +0 -0
  189. {morphcloud-0.1.113 → morphcloud-0.1.114}/scripts/abuild_smoke.py +0 -0
  190. {morphcloud-0.1.113 → morphcloud-0.1.114}/scripts/increment_version.py +0 -0
  191. {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/integration/__init__.py +0 -0
  192. {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/integration/conftest.py +0 -0
  193. {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/integration/pytest.ini +0 -0
  194. {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/integration/test_as_container.py +0 -0
  195. {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/integration/test_branching.py +0 -0
  196. {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/integration/test_command_execution.py +0 -0
  197. {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/integration/test_file_operations.py +0 -0
  198. {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/integration/test_from_tag_multiple.py +0 -0
  199. {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/integration/test_http_service.py +0 -0
  200. {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/integration/test_managed_ssh_keys.py +0 -0
  201. {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/integration/test_metadata.py +0 -0
  202. {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/integration/test_simple.py +0 -0
  203. {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/integration/test_snapshot_operations.py +0 -0
  204. {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/integration/test_ssh_key_rotation.py +0 -0
  205. {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/integration/test_ttl.py +0 -0
  206. {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/integration/test_user.py +0 -0
  207. {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/unit/test_cli_devbox.py +0 -0
  208. {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/unit/test_cli_devbox_terminal.py +0 -0
  209. {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/unit/test_cli_managed_ssh_keys.py +0 -0
  210. {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/unit/test_cli_plugins.py +0 -0
  211. {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/unit/test_cli_secrets.py +0 -0
  212. {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/unit/test_cli_snapshots.py +0 -0
  213. {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/unit/test_cli_volumes.py +0 -0
  214. {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/unit/test_devbox_terminals.py +0 -0
  215. {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/unit/test_http_transport_sharding.py +0 -0
  216. {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/unit/test_managed_ssh_keys_api.py +0 -0
  217. {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/unit/test_plugin_installer.py +0 -0
  218. {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/unit/test_profiles.py +0 -0
  219. {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/unit/test_simple_index_client.py +0 -0
  220. {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/unit/test_snapshot_ttl.py +0 -0
  221. {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/unit/test_ssh_connect_retry.py +0 -0
  222. {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/unit/test_template_local_runner.py +0 -0
  223. {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/unit/test_template_runner.py +0 -0
  224. {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/unit/test_volumes_client.py +0 -0
@@ -1,16 +1,10 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: morphcloud
3
- Version: 0.1.113
3
+ Version: 0.1.114
4
4
  Summary: A Python SDK and CLI tool for creating, managing, and interacting with Morph Cloud VMs.
5
- Author-email: Morph Labs <jobs@morph.so>
6
5
  License: Apache-2.0
7
6
  License-File: LICENSE
8
- Keywords: cli,cloud,morph,runtimes
9
- Classifier: License :: OSI Approved :: Apache Software License
10
- Classifier: Operating System :: OS Independent
11
- Classifier: Programming Language :: Python :: 3
12
- Requires-Python: >=3.10
13
- Requires-Dist: anthropic>=0.40.0
7
+ Requires-Python: >=3.11
14
8
  Requires-Dist: click>=8.1.7
15
9
  Requires-Dist: httpx>=0.27.0
16
10
  Requires-Dist: packaging>=24.2
@@ -25,8 +19,6 @@ Requires-Dist: rich>=13.9.4
25
19
  Requires-Dist: toml>=0.10.2
26
20
  Requires-Dist: tqdm>=4.67.1
27
21
  Requires-Dist: websocket-client>=1.8.0
28
- Provides-Extra: dev
29
- Requires-Dist: ruff>=0.11.4; extra == 'dev'
30
22
  Description-Content-Type: text/markdown
31
23
 
32
24
  # Morph Cloud Python SDK
@@ -2,13 +2,10 @@
2
2
  Copyright (c) 2024 Morph Labs. All rights reserved.
3
3
  Released under the license as described in the file LICENSE.
4
4
  """
5
-
6
5
  import importlib.metadata
7
-
8
6
  from morphcloud.api import MorphCloudClient
9
-
10
7
  try:
11
- __version__ = importlib.metadata.version("morphcloud")
8
+ __version__ = importlib.metadata.version('morphcloud')
12
9
  except importlib.metadata.PackageNotFoundError:
13
- __version__ = "0.0.0-dev"
10
+ __version__ = '0.0.0-dev'
14
11
  __all__ = []
@@ -10,8 +10,6 @@ Main components:
10
10
  - asyncify_transparent: Marks functions for AST-based rewriting
11
11
  - asyncify: Main decorator for converting sync to async code
12
12
  """
13
-
14
- # Standard library imports
15
13
  import ast
16
14
  import asyncio
17
15
  import functools
@@ -27,18 +25,9 @@ import time
27
25
  import urllib.request
28
26
  from types import FunctionType
29
27
  from typing import Any, Callable, Dict, Optional
30
-
31
- # Third-party imports
32
28
  import requests
33
-
34
- ###############################################################################
35
- # GLOBAL SYNC-TO-ASYNC REGISTRY
36
- ###############################################################################
37
-
38
- # Global registry mapping synchronous functions to their async equivalents
39
29
  SYNC_TO_ASYNC_MAP: Dict[str, Callable[..., Any]] = {}
40
30
 
41
-
42
31
  def register_async_equivalent(sync_obj: Callable, async_obj: Callable):
43
32
  """
44
33
  Register a synchronous function and its asynchronous equivalent.
@@ -49,15 +38,9 @@ def register_async_equivalent(sync_obj: Callable, async_obj: Callable):
49
38
  """
50
39
  mod = sync_obj.__module__
51
40
  qname = sync_obj.__qualname__
52
- key = f"{mod}.{qname}"
41
+ key = f'{mod}.{qname}'
53
42
  SYNC_TO_ASYNC_MAP[key] = async_obj
54
43
 
55
-
56
- ###############################################################################
57
- # ASYNCIFY_TRANSPARENT DECORATOR
58
- ###############################################################################
59
-
60
-
61
44
  def asyncify_transparent(func: Callable):
62
45
  """
63
46
  Mark a function as transparent for AST-based rewriting.
@@ -71,25 +54,16 @@ def asyncify_transparent(func: Callable):
71
54
  Returns:
72
55
  The original function with _asyncify_transparent attribute set
73
56
  """
74
- setattr(func, "_asyncify_transparent", True)
57
+ setattr(func, '_asyncify_transparent', True)
75
58
  return func
76
-
77
-
78
- ###############################################################################
79
- # AST REWRITING LOGIC
80
- ###############################################################################
81
-
82
- # Global storage for async functions created during rewriting
83
59
  __ASYNCIFY_GLOBAL_MAP__ = {}
84
60
 
85
-
86
61
  def store_async_callable(async_func: Callable) -> str:
87
62
  """Store an async function in the global map and return its identifier."""
88
- name = f"__asyncify_func_{id(async_func)}"
63
+ name = f'__asyncify_func_{id(async_func)}'
89
64
  __ASYNCIFY_GLOBAL_MAP__[name] = async_func
90
65
  return name
91
66
 
92
-
93
67
  def resolve_qualified_name(node: ast.expr, globals_dict: dict) -> Optional[str]:
94
68
  """
95
69
  Attempt to resolve a fully qualified name for the function call in the AST node.
@@ -101,7 +75,6 @@ def resolve_qualified_name(node: ast.expr, globals_dict: dict) -> Optional[str]:
101
75
  Returns:
102
76
  Optional[str]: The fully qualified name if found, None otherwise
103
77
  """
104
- # Build the name parts from the AST node
105
78
  parts = []
106
79
  current = node
107
80
  while isinstance(current, ast.Attribute):
@@ -111,28 +84,22 @@ def resolve_qualified_name(node: ast.expr, globals_dict: dict) -> Optional[str]:
111
84
  parts.insert(0, current.id)
112
85
  else:
113
86
  return None
114
-
115
- dotted_name = ".".join(parts)
116
-
117
- # Check for direct match in the async map
87
+ dotted_name = '.'.join(parts)
118
88
  if dotted_name in SYNC_TO_ASYNC_MAP:
119
89
  return dotted_name
120
-
121
- # Handle single-part names (local/global functions)
122
90
  if len(parts) == 1:
123
91
  name = parts[0]
124
92
  if name in globals_dict:
125
93
  obj = globals_dict[name]
126
94
  if callable(obj):
127
- mod = getattr(obj, "__module__", None)
128
- qn = getattr(obj, "__qualname__", None)
95
+ mod = getattr(obj, '__module__', None)
96
+ qn = getattr(obj, '__qualname__', None)
129
97
  if mod and qn:
130
- full_key = f"{mod}.{qn}"
98
+ full_key = f'{mod}.{qn}'
131
99
  if full_key in SYNC_TO_ASYNC_MAP:
132
100
  return full_key
133
101
  return None
134
102
 
135
-
136
103
  class AsyncifyTransformer(ast.NodeTransformer):
137
104
  """AST transformer that converts sync function calls to async equivalents."""
138
105
 
@@ -146,14 +113,7 @@ class AsyncifyTransformer(ast.NodeTransformer):
146
113
  """Transform function definitions to async if they contain async calls."""
147
114
  self.generic_visit(node)
148
115
  if self.found_async_calls:
149
- return ast.AsyncFunctionDef(
150
- name=node.name,
151
- args=node.args,
152
- body=node.body,
153
- decorator_list=node.decorator_list,
154
- returns=node.returns,
155
- type_comment=node.type_comment,
156
- )
116
+ return ast.AsyncFunctionDef(name=node.name, args=node.args, body=node.body, decorator_list=node.decorator_list, returns=node.returns, type_comment=node.type_comment)
157
117
  return node
158
118
 
159
119
  def visit_Call(self, node: ast.Call):
@@ -164,17 +124,12 @@ class AsyncifyTransformer(ast.NodeTransformer):
164
124
  async_func = self.sync_to_async_map[full_name]
165
125
  new_name = store_async_callable(async_func)
166
126
  new_func = ast.Name(id=new_name, ctx=ast.Load())
167
- new_call = ast.Await(
168
- value=ast.Call(func=new_func, args=node.args, keywords=node.keywords)
169
- )
127
+ new_call = ast.Await(value=ast.Call(func=new_func, args=node.args, keywords=node.keywords))
170
128
  self.found_async_calls = True
171
129
  return new_call
172
130
  return node
173
131
 
174
-
175
- def try_ast_rewrite(
176
- func: Callable, sync_to_async_map: Dict[str, Callable]
177
- ) -> Optional[Callable]:
132
+ def try_ast_rewrite(func: Callable, sync_to_async_map: Dict[str, Callable]) -> Optional[Callable]:
178
133
  """
179
134
  Attempt to rewrite a function's AST to convert sync calls to async.
180
135
 
@@ -189,22 +144,17 @@ def try_ast_rewrite(
189
144
  source = inspect.getsource(func)
190
145
  except (OSError, TypeError):
191
146
  return None
192
-
193
147
  source = textwrap.dedent(source)
194
148
  tree = ast.parse(source)
195
-
196
149
  transformer = AsyncifyTransformer(sync_to_async_map, func.__globals__)
197
150
  new_tree = transformer.visit(tree)
198
151
  ast.fix_missing_locations(new_tree)
199
-
200
152
  if not transformer.found_async_calls:
201
153
  return None
202
-
203
- code = compile(new_tree, filename="<asyncify>", mode="exec")
154
+ code = compile(new_tree, filename='<asyncify>', mode='exec')
204
155
  new_globals = func.__globals__.copy()
205
156
  new_globals.update(__ASYNCIFY_GLOBAL_MAP__)
206
157
  exec(code, new_globals)
207
-
208
158
  new_func = new_globals.get(func.__name__, None)
209
159
  if new_func and asyncio.iscoroutinefunction(new_func):
210
160
  new_func.__name__ = func.__name__
@@ -212,12 +162,6 @@ def try_ast_rewrite(
212
162
  return new_func
213
163
  return None
214
164
 
215
-
216
- ###############################################################################
217
- # ASYNCIFY DECORATOR
218
- ###############################################################################
219
-
220
-
221
165
  def asyncify(obj: Any):
222
166
  """
223
167
  Main decorator for converting sync code to async.
@@ -246,133 +190,99 @@ def asyncify(obj: Any):
246
190
  elif isinstance(obj, FunctionType):
247
191
  return _asyncify_function(obj)
248
192
  else:
249
- raise TypeError("asyncify can only be applied to functions or classes")
250
-
193
+ raise TypeError('asyncify can only be applied to functions or classes')
251
194
 
252
195
  def _asyncify_function(func: Callable):
253
196
  """Helper function to asyncify a single function."""
254
197
  async_func = None
255
- if getattr(func, "_asyncify_transparent", False):
256
- # Try AST rewriting for transparent functions
198
+ if getattr(func, '_asyncify_transparent', False):
257
199
  async_func = try_ast_rewrite(func, SYNC_TO_ASYNC_MAP)
258
200
  if async_func is not None:
259
- # Register successful rewrite
260
201
  register_async_equivalent(func, async_func)
261
202
  else:
262
- # Fallback to to_thread
203
+
263
204
  @functools.wraps(func)
264
205
  async def async_func(*args, **kwargs):
265
206
  return await asyncio.to_thread(func, *args, **kwargs)
266
-
267
207
  else:
268
- # Not transparent, always use to_thread
208
+
269
209
  @functools.wraps(func)
270
210
  async def async_func(*args, **kwargs):
271
211
  return await asyncio.to_thread(func, *args, **kwargs)
272
-
273
- # Register in globals
274
- async_name = f"async_{func.__name__}"
212
+ async_name = f'async_{func.__name__}'
275
213
  globals()[async_name] = async_func
276
214
  return func
277
215
 
278
-
279
216
  def _asyncify_class(cls: type):
280
217
  """Helper function to asyncify a class."""
281
218
  new_attrs = {}
282
219
  for name, value in cls.__dict__.items():
283
- if callable(value) and not (name.startswith("__") and name.endswith("__")):
284
- if getattr(value, "_asyncify_transparent", False):
285
- # Try rewriting transparent methods
220
+ if callable(value) and (not (name.startswith('__') and name.endswith('__'))):
221
+ if getattr(value, '_asyncify_transparent', False):
286
222
  maybe_async = try_ast_rewrite(value, SYNC_TO_ASYNC_MAP)
287
223
  if maybe_async is not None:
288
224
  async_method = maybe_async
289
225
  else:
290
- # Fallback to thread
226
+
291
227
  @functools.wraps(value)
292
228
  async def async_method(*args, __value=value, **kwargs):
293
229
  return await asyncio.to_thread(__value, *args, **kwargs)
294
-
295
230
  else:
296
- # Not transparent, use thread
231
+
297
232
  @functools.wraps(value)
298
233
  async def async_method(*args, __value=value, **kwargs):
299
234
  return await asyncio.to_thread(__value, *args, **kwargs)
300
-
301
- async_name = f"async_{name}"
235
+ async_name = f'async_{name}'
302
236
  new_attrs[async_name] = async_method
303
237
  else:
304
238
  new_attrs[name] = value
305
-
306
- new_cls_name = f"Async{cls.__name__}"
239
+ new_cls_name = f'Async{cls.__name__}'
307
240
  new_cls = type(new_cls_name, cls.__bases__, new_attrs)
308
-
309
- # Register in globals
310
241
  globals()[new_cls_name] = new_cls
311
242
  return new_cls
312
243
 
313
-
314
- ###############################################################################
315
- # ASYNC EQUIVALENTS REGISTRATION
316
- ###############################################################################
317
-
318
-
319
- # Standard library async equivalents
320
244
  async def async_requests_get(url, **kwargs):
321
245
  """Async equivalent for requests.get using aiohttp."""
322
246
  import aiohttp
323
-
324
247
  async with aiohttp.ClientSession() as session:
325
248
  async with session.get(url, **kwargs) as response:
326
249
  return await response.text()
327
250
 
328
-
329
251
  async def async_open(*args, **kwargs):
330
252
  """Async equivalent for open using aiofiles."""
331
253
  import aiofiles
332
-
333
254
  return aiofiles.open(*args, **kwargs)
334
255
 
335
-
336
256
  async def async_subprocess_run(*cmd, **kwargs):
337
257
  """Async equivalent for subprocess.run."""
338
- proc = await asyncio.create_subprocess_exec(
339
- *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, **kwargs
340
- )
258
+ proc = await asyncio.create_subprocess_exec(*cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, **kwargs)
341
259
  stdout, stderr = await proc.communicate()
342
- return stdout, stderr, proc.returncode
343
-
260
+ return (stdout, stderr, proc.returncode)
344
261
 
345
262
  async def async_urlopen(url, **kwargs):
346
263
  """Async equivalent for urllib.request.urlopen using aiohttp."""
347
264
  import aiohttp
348
-
349
265
  async with aiohttp.ClientSession() as session:
350
266
  async with session.get(url, **kwargs) as response:
351
267
  return await response.read()
352
268
 
353
-
354
269
  async def async_sqlite_connect(database, **kwargs):
355
270
  """Async equivalent for sqlite3.connect using aiosqlite."""
356
271
  import aiosqlite
357
-
358
272
  return await aiosqlite.connect(database, **kwargs)
359
273
 
360
-
361
274
  async def async_glob(pattern):
362
275
  """Async equivalent for glob.glob using to_thread."""
363
276
  return await asyncio.to_thread(glob.glob, pattern)
364
277
 
365
-
366
- async def async_listdir(path="."):
278
+ async def async_listdir(path='.'):
367
279
  """Async equivalent for os.listdir using to_thread."""
368
280
  return await asyncio.to_thread(os.listdir, path)
369
281
 
370
-
371
282
  async def async_copyfile(src, dst, *, chunk_size=65536):
372
283
  """Async equivalent for shutil.copyfile using aiofiles."""
373
284
  import aiofiles
374
-
375
- async with aiofiles.open(src, "rb") as fsrc, aiofiles.open(dst, "wb") as fdst:
285
+ async with aiofiles.open(src, 'rb') as fsrc, aiofiles.open(dst, 'wb') as fdst:
376
286
  while True:
377
287
  chunk = await fsrc.read(chunk_size)
378
288
  if not chunk:
@@ -380,25 +290,18 @@ async def async_copyfile(src, dst, *, chunk_size=65536):
380
290
  await fdst.write(chunk)
381
291
  return None
382
292
 
383
-
384
293
  async def async_json_load(file):
385
294
  """Async equivalent for json.load using aiofiles."""
386
295
  import aiofiles
387
-
388
- async with aiofiles.open(file, "r") as f:
296
+ async with aiofiles.open(file, 'r') as f:
389
297
  content = await f.read()
390
298
  return json.loads(content)
391
299
 
392
-
393
300
  async def async_json_dump(obj, file):
394
301
  """Async equivalent for json.dump using aiofiles."""
395
302
  import aiofiles
396
-
397
- async with aiofiles.open(file, "w") as f:
303
+ async with aiofiles.open(file, 'w') as f:
398
304
  await f.write(json.dumps(obj))
399
-
400
-
401
- # Register all async equivalents
402
305
  register_async_equivalent(time.sleep, asyncio.sleep)
403
306
  register_async_equivalent(requests.get, async_requests_get)
404
307
  register_async_equivalent(open, async_open)
@@ -0,0 +1,146 @@
1
+ import re
2
+ import time
3
+ from dataclasses import dataclass
4
+ from enum import Enum
5
+ from typing import Dict, Optional, Tuple
6
+ import paramiko
7
+
8
+ class OSCType(Enum):
9
+ """OSC 133 Semantic Prompt Sequence Types"""
10
+ PROMPT_START = 'P'
11
+ COMMAND_START = 'C'
12
+ COMMAND_DONE = 'D'
13
+ PROMPT_CONT = 'A'
14
+ BLOCK_END = 'B'
15
+
16
+ class PromptKind(Enum):
17
+ """OSC 133 Prompt Types (k parameter)"""
18
+ INITIAL = 'i'
19
+ SECONDARY = 's'
20
+
21
+ class ContinuationKind(Enum):
22
+ """OSC 133 Continuation Types (cl parameter)"""
23
+ MESSAGE = 'm'
24
+ PARTIAL = 'p'
25
+ COMPLETE = 'c'
26
+
27
+ @dataclass
28
+ class OSCParams:
29
+ """Structured representation of OSC 133 parameters"""
30
+ type: OSCType
31
+ prompt_kind: Optional[PromptKind] = None
32
+ continuation: Optional[ContinuationKind] = None
33
+ exit_code: Optional[int] = None
34
+ aid: Optional[int] = None
35
+ raw_params: Dict[str, str] = None
36
+
37
+ @classmethod
38
+ def from_sequence(cls, sequence: str) -> 'OSCParams':
39
+ """Parse an OSC sequence into structured parameters"""
40
+ parts = sequence.split(';')
41
+ if not parts:
42
+ raise ValueError(f'Empty sequence')
43
+ type_str = parts[0]
44
+ if type_str == 'B':
45
+ return cls(type=OSCType.BLOCK_END, raw_params={})
46
+ try:
47
+ osc_type = OSCType(type_str)
48
+ except ValueError:
49
+ raise ValueError(f'Unknown sequence type: {type_str}')
50
+ raw_params = {}
51
+ exit_code = None
52
+ if osc_type == OSCType.COMMAND_DONE and len(parts) > 1:
53
+ try:
54
+ exit_code = int(parts[1])
55
+ raw_params['exit_code'] = parts[1]
56
+ except ValueError:
57
+ pass
58
+ for part in parts[1:]:
59
+ if '=' in part:
60
+ key, value = part.split('=', 1)
61
+ raw_params[key] = value
62
+ return cls(type=osc_type, prompt_kind=PromptKind(raw_params['k']) if 'k' in raw_params else None, continuation=ContinuationKind(raw_params['cl']) if 'cl' in raw_params else None, exit_code=exit_code if exit_code is not None else int(raw_params['exit_code']) if 'exit_code' in raw_params else None, aid=int(raw_params['aid']) if 'aid' in raw_params else None, raw_params=raw_params)
63
+
64
+ @dataclass
65
+ class CommandResult:
66
+ prompt: str
67
+ command: str
68
+ output: str
69
+ exit_code: int
70
+ osc_params: Dict[str, OSCParams]
71
+
72
+ class SemanticShellClient:
73
+
74
+ def __init__(self, hostname: str, username: str, port: int=2222):
75
+ self.client = paramiko.SSHClient()
76
+ self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
77
+ self.client.connect(hostname=hostname, username=username, port=port)
78
+ self.channel = self.client.invoke_shell()
79
+ self.buffer = ''
80
+ self._read_until_prompt()
81
+
82
+ def _parse_osc_sequences(self, text: str) -> Dict[str, OSCParams]:
83
+ """Parse all OSC sequences in the text"""
84
+ params = {}
85
+ sequences = re.finditer('\\x1b]133;([^\\x07]*)\\x07', text)
86
+ for i, seq in enumerate(sequences):
87
+ try:
88
+ params[f'seq_{i}'] = OSCParams.from_sequence(seq.group(1))
89
+ except ValueError as e:
90
+ continue
91
+ return params
92
+
93
+ def _split_repl_parts(self, text: str) -> Tuple[str, str, str, Dict[str, OSCParams]]:
94
+ """
95
+ Split the shell output into prompt, command output, and OSC parameters.
96
+ Returns (prompt, command_output, output, osc_params)
97
+ """
98
+ osc_params = self._parse_osc_sequences(text)
99
+ clean_text = re.sub('\\x1b]133;[^\\x07]*\\x07', '', text)
100
+ clean_text = re.sub('\\x1b]122;[^\\x07]*\\x07', '', clean_text)
101
+ lines = clean_text.split('\r\n')
102
+ prompt = lines[-1] if lines else ''
103
+ command_output = lines[0] if len(lines) > 0 else ''
104
+ output = '\n'.join(lines[1:-1]) if len(lines) > 2 else ''
105
+ prompt = re.sub('\\x1b[^m]*m', '', prompt).strip()
106
+ command_output = re.sub('\\x1b[^m]*m', '', command_output).strip()
107
+ output = re.sub('\\x1b[^m]*m', '', output).strip()
108
+ return (prompt, command_output, output, osc_params)
109
+
110
+ def _read_until_prompt(self, timeout: float=30) -> Tuple[str, int]:
111
+ """
112
+ Read the shell output until we see the OSC 133 command completion sequence.
113
+ Returns the output and the exit code.
114
+ """
115
+ start_time = time.time()
116
+ output = []
117
+ exit_code = None
118
+ while time.time() - start_time < timeout:
119
+ if self.channel.recv_ready():
120
+ chunk = self.channel.recv(4096).decode('utf-8')
121
+ self.buffer += chunk
122
+ output.append(chunk)
123
+ completion_match = re.search('\\x1b]133;D;(\\d+);aid=\\d+\\x07', self.buffer)
124
+ if completion_match:
125
+ exit_code = int(completion_match.group(1))
126
+ if re.search('\\x1b]133;A;cl=m;aid=\\d+\\x07', self.buffer):
127
+ complete_output = ''.join(output)
128
+ self.buffer = ''
129
+ return (complete_output, exit_code)
130
+ time.sleep(0.1)
131
+ raise TimeoutError('Timed out waiting for command completion')
132
+
133
+ def execute_command(self, command: str, timeout: float=30) -> CommandResult:
134
+ """
135
+ Execute a command and wait for its completion.
136
+ Returns a CommandResult with structured output and OSC parameters.
137
+ """
138
+ self.channel.send(command + '\n')
139
+ raw_output, exit_code = self._read_until_prompt(timeout)
140
+ prompt, command_output, output, osc_params = self._split_repl_parts(raw_output)
141
+ return CommandResult(prompt=prompt, command=command_output or command, output=output, exit_code=exit_code or 0, osc_params=osc_params)
142
+
143
+ def close(self):
144
+ """Close the SSH connection."""
145
+ self.channel.close()
146
+ self.client.close()
@@ -1,34 +1,30 @@
1
1
  from __future__ import annotations
2
-
3
2
  import asyncio
4
3
  import itertools
5
4
  import os
6
5
  import typing
7
-
8
6
  import httpx
9
7
 
10
-
11
8
  def _env_int(name: str, default: int) -> int:
12
9
  value = os.environ.get(name)
13
- if value is None or value == "":
10
+ if value is None or value == '':
14
11
  return default
15
12
  try:
16
13
  return int(value)
17
14
  except Exception:
18
15
  return default
19
16
 
20
-
21
17
  def _env_float(name: str, default: float) -> float:
22
18
  value = os.environ.get(name)
23
- if value is None or value == "":
19
+ if value is None or value == '':
24
20
  return default
25
21
  try:
26
22
  return float(value)
27
23
  except Exception:
28
24
  return default
29
25
 
30
-
31
26
  class ShardedTransport(httpx.BaseTransport):
27
+
32
28
  def __init__(self, transports: typing.Sequence[httpx.HTTPTransport]) -> None:
33
29
  self._transports = list(transports)
34
30
  self._counter = itertools.count()
@@ -41,8 +37,8 @@ class ShardedTransport(httpx.BaseTransport):
41
37
  for transport in self._transports:
42
38
  transport.close()
43
39
 
44
-
45
40
  class ShardedAsyncTransport(httpx.AsyncBaseTransport):
41
+
46
42
  def __init__(self, transports: typing.Sequence[httpx.AsyncHTTPTransport]) -> None:
47
43
  self._transports = list(transports)
48
44
  self._counter = itertools.count()
@@ -54,39 +50,20 @@ class ShardedAsyncTransport(httpx.AsyncBaseTransport):
54
50
  async def aclose(self) -> None:
55
51
  await asyncio.gather(*(transport.aclose() for transport in self._transports))
56
52
 
57
-
58
53
  def _new_limits() -> httpx.Limits:
59
- max_connections = _env_int("MORPH_HTTP_MAX_CONNECTIONS", 100)
60
- max_keepalive_connections = _env_int(
61
- "MORPH_HTTP_MAX_KEEPALIVE_CONNECTIONS",
62
- max_connections,
63
- )
64
- keepalive_expiry = _env_float("MORPH_HTTP_KEEPALIVE_EXPIRY", 60.0)
65
- return httpx.Limits(
66
- max_connections=max_connections,
67
- max_keepalive_connections=max_keepalive_connections,
68
- keepalive_expiry=keepalive_expiry,
69
- )
70
-
54
+ max_connections = _env_int('MORPH_HTTP_MAX_CONNECTIONS', 100)
55
+ max_keepalive_connections = _env_int('MORPH_HTTP_MAX_KEEPALIVE_CONNECTIONS', max_connections)
56
+ keepalive_expiry = _env_float('MORPH_HTTP_KEEPALIVE_EXPIRY', 60.0)
57
+ return httpx.Limits(max_connections=max_connections, max_keepalive_connections=max_keepalive_connections, keepalive_expiry=keepalive_expiry)
71
58
 
72
59
  def build_http_transport() -> httpx.BaseTransport:
73
- # Spread requests across independent httpcore pools to avoid contention
74
- # in a single transport under high concurrency. Set to 1 to disable.
75
- transport_shards = max(_env_int("MORPH_HTTP_TRANSPORT_SHARDS", 32), 1)
60
+ transport_shards = max(_env_int('MORPH_HTTP_TRANSPORT_SHARDS', 32), 1)
76
61
  if transport_shards == 1:
77
62
  return httpx.HTTPTransport(limits=_new_limits())
78
- return ShardedTransport(
79
- [httpx.HTTPTransport(limits=_new_limits()) for _ in range(transport_shards)]
80
- )
81
-
63
+ return ShardedTransport([httpx.HTTPTransport(limits=_new_limits()) for _ in range(transport_shards)])
82
64
 
83
65
  def build_async_http_transport() -> httpx.AsyncBaseTransport:
84
- transport_shards = max(_env_int("MORPH_HTTP_TRANSPORT_SHARDS", 32), 1)
66
+ transport_shards = max(_env_int('MORPH_HTTP_TRANSPORT_SHARDS', 32), 1)
85
67
  if transport_shards == 1:
86
68
  return httpx.AsyncHTTPTransport(limits=_new_limits())
87
- return ShardedAsyncTransport(
88
- [
89
- httpx.AsyncHTTPTransport(limits=_new_limits())
90
- for _ in range(transport_shards)
91
- ]
92
- )
69
+ return ShardedAsyncTransport([httpx.AsyncHTTPTransport(limits=_new_limits()) for _ in range(transport_shards)])