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.
- {morphcloud-0.1.113 → morphcloud-0.1.114}/PKG-INFO +3 -11
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/__init__.py +2 -5
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/_asyncify.py +28 -125
- morphcloud-0.1.114/morphcloud/_bash_interpreter.py +146 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/_http_transport.py +12 -35
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/_scramble.py +35 -144
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/_ssh.py +53 -211
- morphcloud-0.1.114/morphcloud/_synapse.py +363 -0
- morphcloud-0.1.114/morphcloud/_utils.py +84 -0
- morphcloud-0.1.114/morphcloud/api.py +2929 -0
- morphcloud-0.1.114/morphcloud/cli.py +845 -0
- morphcloud-0.1.114/morphcloud/cli_helpers.py +75 -0
- morphcloud-0.1.114/morphcloud/cli_runtime.py +52 -0
- morphcloud-0.1.114/morphcloud/config.py +207 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/__init__.py +1 -7
- morphcloud-0.1.114/morphcloud/devbox/cli.py +1110 -0
- morphcloud-0.1.114/morphcloud/devbox/client.py +278 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/config.py +4 -11
- morphcloud-0.1.114/morphcloud/devbox/gen/__init__.py +5 -0
- morphcloud-0.1.114/morphcloud/devbox/gen/admin/__init__.py +1 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/admin/client.py +6 -17
- morphcloud-0.1.114/morphcloud/devbox/gen/admin/raw_client.py +77 -0
- morphcloud-0.1.114/morphcloud/devbox/gen/aliases/__init__.py +1 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/aliases/client.py +7 -20
- morphcloud-0.1.114/morphcloud/devbox/gen/aliases/raw_client.py +145 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/client.py +12 -100
- morphcloud-0.1.114/morphcloud/devbox/gen/core/__init__.py +13 -0
- morphcloud-0.1.114/morphcloud/devbox/gen/core/api_error.py +14 -0
- morphcloud-0.1.114/morphcloud/devbox/gen/core/client_wrapper.py +45 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/core/datetime_utils.py +2 -10
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/core/file.py +6 -33
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/core/force_multipart.py +0 -5
- morphcloud-0.1.114/morphcloud/devbox/gen/core/http_client.py +176 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/core/http_response.py +1 -12
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/core/jsonable_encoder.py +10 -25
- morphcloud-0.1.114/morphcloud/devbox/gen/core/pydantic_utilities.py +178 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/core/query_encoder.py +2 -15
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/core/remove_none_from_dict.py +0 -3
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/core/request_options.py +1 -6
- morphcloud-0.1.114/morphcloud/devbox/gen/core/serialization.py +151 -0
- morphcloud-0.1.114/morphcloud/devbox/gen/devboxes_actions/__init__.py +1 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/devboxes_actions/client.py +38 -175
- morphcloud-0.1.114/morphcloud/devbox/gen/devboxes_actions/raw_client.py +564 -0
- morphcloud-0.1.114/morphcloud/devbox/gen/devboxes_core/__init__.py +1 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/devboxes_core/client.py +17 -50
- morphcloud-0.1.114/morphcloud/devbox/gen/devboxes_core/raw_client.py +232 -0
- morphcloud-0.1.114/morphcloud/devbox/gen/devboxes_lifecycle/__init__.py +1 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/devboxes_lifecycle/client.py +11 -50
- morphcloud-0.1.114/morphcloud/devbox/gen/devboxes_lifecycle/raw_client.py +145 -0
- morphcloud-0.1.114/morphcloud/devbox/gen/errors/__init__.py +2 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/errors/unprocessable_entity_error.py +2 -9
- morphcloud-0.1.114/morphcloud/devbox/gen/templates/__init__.py +1 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/templates/client.py +45 -248
- morphcloud-0.1.114/morphcloud/devbox/gen/templates/raw_client.py +673 -0
- morphcloud-0.1.114/morphcloud/devbox/gen/tmux/__init__.py +1 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/tmux/client.py +34 -207
- morphcloud-0.1.114/morphcloud/devbox/gen/tmux/raw_client.py +565 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/__init__.py +1 -47
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/alias_response.py +1 -7
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/branch_response.py +1 -7
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/delete_response.py +1 -7
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/devbox_detail_list_response.py +1 -7
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/devbox_list_response.py +1 -7
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/devbox_refs.py +1 -7
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/devbox_response.py +1 -7
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/devbox_spec.py +1 -7
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/devbox_ssh_credentials_response.py +1 -7
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/devbox_summary_response.py +1 -7
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/http_service.py +1 -7
- morphcloud-0.1.114/morphcloud/devbox/gen/types/http_service_auth_mode.py +2 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/http_validation_error.py +1 -7
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/networking.py +1 -7
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/secret_value_response.py +1 -7
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/snapshot_response.py +1 -7
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/success_response.py +1 -7
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/template_cache_request.py +1 -7
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/template_cache_start_response.py +1 -7
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/template_detail.py +1 -7
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/template_list_response.py +1 -7
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/template_share_response.py +1 -7
- morphcloud-0.1.114/morphcloud/devbox/gen/types/template_status.py +2 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/template_step_model.py +1 -7
- morphcloud-0.1.114/morphcloud/devbox/gen/types/template_step_status.py +2 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/template_summary.py +1 -7
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/timeline_event.py +1 -7
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/tmux_debug_info_response.py +1 -7
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/tmux_install_response.py +1 -7
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/tmux_new_session_response.py +1 -7
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/tmux_send_keys_response.py +1 -7
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/tmux_session.py +1 -7
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/tmux_sessions_list.py +1 -7
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/tmux_snapshot_response.py +1 -7
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/tmux_target.py +1 -7
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/ttl.py +1 -7
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/upload_response.py +1 -7
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/validation_error.py +1 -7
- morphcloud-0.1.114/morphcloud/devbox/gen/types/validation_error_loc_item.py +2 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/types/wake_on.py +1 -7
- morphcloud-0.1.114/morphcloud/devbox/gen/uploads/__init__.py +1 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/uploads/client.py +10 -59
- morphcloud-0.1.114/morphcloud/devbox/gen/uploads/raw_client.py +159 -0
- morphcloud-0.1.114/morphcloud/devbox/gen/user_secrets/__init__.py +1 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/gen/user_secrets/client.py +6 -17
- morphcloud-0.1.114/morphcloud/devbox/gen/user_secrets/raw_client.py +85 -0
- morphcloud-0.1.114/morphcloud/devbox/template_local_runner.py +397 -0
- morphcloud-0.1.114/morphcloud/devbox/template_runner.py +1270 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/terminals.py +33 -87
- morphcloud-0.1.114/morphcloud/devbox_commands.py +137 -0
- morphcloud-0.1.114/morphcloud/experimental/__init__.py +420 -0
- morphcloud-0.1.114/morphcloud/experimental/browser.py +551 -0
- morphcloud-0.1.114/morphcloud/images.py +26 -0
- morphcloud-0.1.114/morphcloud/instances.py +335 -0
- morphcloud-0.1.114/morphcloud/plugin_commands.py +63 -0
- morphcloud-0.1.114/morphcloud/plugins/cli.py +571 -0
- morphcloud-0.1.114/morphcloud/plugins/installer.py +247 -0
- morphcloud-0.1.114/morphcloud/profiles.py +96 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/simple_index/__init__.py +1 -5
- morphcloud-0.1.114/morphcloud/simple_index/client.py +221 -0
- morphcloud-0.1.114/morphcloud/snapshots.py +106 -0
- morphcloud-0.1.114/morphcloud/users.py +196 -0
- morphcloud-0.1.114/morphcloud/volume_commands.py +36 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/volumes/__init__.py +1 -8
- morphcloud-0.1.114/morphcloud/volumes/cli.py +388 -0
- morphcloud-0.1.114/morphcloud/volumes/client.py +307 -0
- morphcloud-0.1.114/pyproject.toml +33 -0
- morphcloud-0.1.113/morphcloud/_bash_interpreter.py +0 -228
- morphcloud-0.1.113/morphcloud/_llm.py +0 -613
- morphcloud-0.1.113/morphcloud/_utils.py +0 -117
- morphcloud-0.1.113/morphcloud/api.py +0 -4968
- morphcloud-0.1.113/morphcloud/cli.py +0 -2985
- morphcloud-0.1.113/morphcloud/cli_helpers.py +0 -109
- morphcloud-0.1.113/morphcloud/config.py +0 -327
- morphcloud-0.1.113/morphcloud/devbox/cli.py +0 -1889
- morphcloud-0.1.113/morphcloud/devbox/client.py +0 -465
- morphcloud-0.1.113/morphcloud/devbox/gen/__init__.py +0 -114
- morphcloud-0.1.113/morphcloud/devbox/gen/admin/__init__.py +0 -3
- morphcloud-0.1.113/morphcloud/devbox/gen/admin/raw_client.py +0 -138
- morphcloud-0.1.113/morphcloud/devbox/gen/aliases/__init__.py +0 -3
- morphcloud-0.1.113/morphcloud/devbox/gen/aliases/raw_client.py +0 -246
- morphcloud-0.1.113/morphcloud/devbox/gen/core/__init__.py +0 -52
- morphcloud-0.1.113/morphcloud/devbox/gen/core/api_error.py +0 -23
- morphcloud-0.1.113/morphcloud/devbox/gen/core/client_wrapper.py +0 -89
- morphcloud-0.1.113/morphcloud/devbox/gen/core/http_client.py +0 -620
- morphcloud-0.1.113/morphcloud/devbox/gen/core/pydantic_utilities.py +0 -306
- morphcloud-0.1.113/morphcloud/devbox/gen/core/serialization.py +0 -282
- morphcloud-0.1.113/morphcloud/devbox/gen/devboxes_actions/__init__.py +0 -3
- morphcloud-0.1.113/morphcloud/devbox/gen/devboxes_actions/raw_client.py +0 -1213
- morphcloud-0.1.113/morphcloud/devbox/gen/devboxes_core/__init__.py +0 -3
- morphcloud-0.1.113/morphcloud/devbox/gen/devboxes_core/raw_client.py +0 -431
- morphcloud-0.1.113/morphcloud/devbox/gen/devboxes_lifecycle/__init__.py +0 -3
- morphcloud-0.1.113/morphcloud/devbox/gen/devboxes_lifecycle/raw_client.py +0 -298
- morphcloud-0.1.113/morphcloud/devbox/gen/errors/__init__.py +0 -7
- morphcloud-0.1.113/morphcloud/devbox/gen/templates/__init__.py +0 -3
- morphcloud-0.1.113/morphcloud/devbox/gen/templates/raw_client.py +0 -1450
- morphcloud-0.1.113/morphcloud/devbox/gen/tmux/__init__.py +0 -3
- morphcloud-0.1.113/morphcloud/devbox/gen/tmux/raw_client.py +0 -1188
- morphcloud-0.1.113/morphcloud/devbox/gen/types/http_service_auth_mode.py +0 -5
- morphcloud-0.1.113/morphcloud/devbox/gen/types/template_status.py +0 -7
- morphcloud-0.1.113/morphcloud/devbox/gen/types/template_step_status.py +0 -7
- morphcloud-0.1.113/morphcloud/devbox/gen/types/validation_error_loc_item.py +0 -5
- morphcloud-0.1.113/morphcloud/devbox/gen/uploads/__init__.py +0 -3
- morphcloud-0.1.113/morphcloud/devbox/gen/uploads/raw_client.py +0 -332
- morphcloud-0.1.113/morphcloud/devbox/gen/user_secrets/__init__.py +0 -3
- morphcloud-0.1.113/morphcloud/devbox/gen/user_secrets/raw_client.py +0 -146
- morphcloud-0.1.113/morphcloud/devbox/template_local_runner.py +0 -783
- morphcloud-0.1.113/morphcloud/devbox/template_runner.py +0 -1979
- morphcloud-0.1.113/morphcloud/experimental/__init__.py +0 -697
- morphcloud-0.1.113/morphcloud/experimental/browser.py +0 -862
- morphcloud-0.1.113/morphcloud/plugins/cli.py +0 -1164
- morphcloud-0.1.113/morphcloud/plugins/installer.py +0 -569
- morphcloud-0.1.113/morphcloud/simple_index/client.py +0 -415
- morphcloud-0.1.113/morphcloud/volumes/cli.py +0 -575
- morphcloud-0.1.113/morphcloud/volumes/client.py +0 -536
- morphcloud-0.1.113/pyproject.toml +0 -57
- morphcloud-0.1.113/requirements.txt +0 -108
- {morphcloud-0.1.113 → morphcloud-0.1.114}/.github/workflows/publish.yaml +0 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/.gitignore +0 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/LICENSE +0 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/Makefile +0 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/README.md +0 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/conf.py +0 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/examples/browser_example.py +0 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/examples/devbox_example.py +0 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/index.rst +0 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/make.bat +0 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/devbox/openapi.json +0 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/plugins/__init__.py +0 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/morphcloud/py.typed +0 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/scripts/abuild_smoke.py +0 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/scripts/increment_version.py +0 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/integration/__init__.py +0 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/integration/conftest.py +0 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/integration/pytest.ini +0 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/integration/test_as_container.py +0 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/integration/test_branching.py +0 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/integration/test_command_execution.py +0 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/integration/test_file_operations.py +0 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/integration/test_from_tag_multiple.py +0 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/integration/test_http_service.py +0 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/integration/test_managed_ssh_keys.py +0 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/integration/test_metadata.py +0 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/integration/test_simple.py +0 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/integration/test_snapshot_operations.py +0 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/integration/test_ssh_key_rotation.py +0 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/integration/test_ttl.py +0 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/integration/test_user.py +0 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/unit/test_cli_devbox.py +0 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/unit/test_cli_devbox_terminal.py +0 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/unit/test_cli_managed_ssh_keys.py +0 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/unit/test_cli_plugins.py +0 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/unit/test_cli_secrets.py +0 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/unit/test_cli_snapshots.py +0 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/unit/test_cli_volumes.py +0 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/unit/test_devbox_terminals.py +0 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/unit/test_http_transport_sharding.py +0 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/unit/test_managed_ssh_keys_api.py +0 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/unit/test_plugin_installer.py +0 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/unit/test_profiles.py +0 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/unit/test_simple_index_client.py +0 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/unit/test_snapshot_ttl.py +0 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/unit/test_ssh_connect_retry.py +0 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/unit/test_template_local_runner.py +0 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/unit/test_template_runner.py +0 -0
- {morphcloud-0.1.113 → morphcloud-0.1.114}/tests/unit/test_volumes_client.py +0 -0
|
@@ -1,16 +1,10 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: morphcloud
|
|
3
|
-
Version: 0.1.
|
|
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
|
-
|
|
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(
|
|
8
|
+
__version__ = importlib.metadata.version('morphcloud')
|
|
12
9
|
except importlib.metadata.PackageNotFoundError:
|
|
13
|
-
__version__ =
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
128
|
-
qn = getattr(obj,
|
|
95
|
+
mod = getattr(obj, '__module__', None)
|
|
96
|
+
qn = getattr(obj, '__qualname__', None)
|
|
129
97
|
if mod and qn:
|
|
130
|
-
full_key = f
|
|
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(
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
284
|
-
if getattr(value,
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
60
|
-
max_keepalive_connections = _env_int(
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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(
|
|
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)])
|