morphcloud 0.1.110__tar.gz → 0.1.111__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.110 → morphcloud-0.1.111}/PKG-INFO +6 -8
- {morphcloud-0.1.110 → morphcloud-0.1.111}/README.md +5 -7
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/volumes/client.py +5 -82
- {morphcloud-0.1.110 → morphcloud-0.1.111}/pyproject.toml +1 -1
- {morphcloud-0.1.110 → morphcloud-0.1.111}/tests/unit/test_cli_volumes.py +24 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/tests/unit/test_volumes_client.py +29 -39
- {morphcloud-0.1.110 → morphcloud-0.1.111}/.github/workflows/publish.yaml +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/.gitignore +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/LICENSE +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/Makefile +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/conf.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/examples/browser_example.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/examples/devbox_example.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/index.rst +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/make.bat +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/__init__.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/_asyncify.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/_bash_interpreter.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/_http_transport.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/_llm.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/_scramble.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/_ssh.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/_utils.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/api.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/cli.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/cli_helpers.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/config.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/__init__.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/cli.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/client.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/config.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/__init__.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/admin/__init__.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/admin/client.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/admin/raw_client.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/aliases/__init__.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/aliases/client.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/aliases/raw_client.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/client.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/core/__init__.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/core/api_error.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/core/client_wrapper.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/core/datetime_utils.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/core/file.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/core/force_multipart.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/core/http_client.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/core/http_response.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/core/jsonable_encoder.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/core/pydantic_utilities.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/core/query_encoder.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/core/remove_none_from_dict.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/core/request_options.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/core/serialization.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/devboxes_actions/__init__.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/devboxes_actions/client.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/devboxes_actions/raw_client.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/devboxes_core/__init__.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/devboxes_core/client.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/devboxes_core/raw_client.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/devboxes_lifecycle/__init__.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/devboxes_lifecycle/client.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/devboxes_lifecycle/raw_client.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/errors/__init__.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/errors/unprocessable_entity_error.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/templates/__init__.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/templates/client.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/templates/raw_client.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/tmux/__init__.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/tmux/client.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/tmux/raw_client.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/__init__.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/alias_response.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/branch_response.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/delete_response.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/devbox_detail_list_response.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/devbox_list_response.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/devbox_refs.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/devbox_response.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/devbox_spec.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/devbox_ssh_credentials_response.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/devbox_summary_response.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/http_service.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/http_service_auth_mode.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/http_validation_error.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/networking.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/secret_value_response.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/snapshot_response.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/success_response.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/template_cache_request.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/template_cache_start_response.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/template_detail.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/template_list_response.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/template_share_response.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/template_status.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/template_step_model.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/template_step_status.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/template_summary.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/timeline_event.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/tmux_debug_info_response.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/tmux_install_response.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/tmux_new_session_response.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/tmux_send_keys_response.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/tmux_session.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/tmux_sessions_list.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/tmux_snapshot_response.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/tmux_target.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/ttl.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/upload_response.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/validation_error.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/validation_error_loc_item.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/wake_on.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/uploads/__init__.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/uploads/client.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/uploads/raw_client.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/user_secrets/__init__.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/user_secrets/client.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/user_secrets/raw_client.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/openapi.json +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/template_local_runner.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/template_runner.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/terminals.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/experimental/__init__.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/experimental/browser.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/py.typed +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/volumes/__init__.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/volumes/cli.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/requirements.txt +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/scripts/abuild_smoke.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/scripts/increment_version.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/tests/integration/__init__.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/tests/integration/conftest.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/tests/integration/pytest.ini +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/tests/integration/test_as_container.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/tests/integration/test_branching.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/tests/integration/test_command_execution.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/tests/integration/test_file_operations.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/tests/integration/test_from_tag_multiple.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/tests/integration/test_http_service.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/tests/integration/test_metadata.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/tests/integration/test_simple.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/tests/integration/test_snapshot_operations.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/tests/integration/test_ssh_key_rotation.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/tests/integration/test_ttl.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/tests/integration/test_user.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/tests/unit/test_cli_devbox.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/tests/unit/test_cli_devbox_terminal.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/tests/unit/test_cli_secrets.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/tests/unit/test_cli_snapshots.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/tests/unit/test_devbox_terminals.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/tests/unit/test_http_transport_sharding.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/tests/unit/test_profiles.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/tests/unit/test_snapshot_ttl.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/tests/unit/test_ssh_connect_retry.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/tests/unit/test_template_local_runner.py +0 -0
- {morphcloud-0.1.110 → morphcloud-0.1.111}/tests/unit/test_template_runner.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: morphcloud
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.111
|
|
4
4
|
Summary: A Python SDK and CLI tool for creating, managing, and interacting with Morph Cloud VMs.
|
|
5
5
|
Author-email: Morph Labs <jobs@morph.so>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -218,7 +218,7 @@ morphcloud instance copy -r ./local_dir inst_123:/remote/dir
|
|
|
218
218
|
|
|
219
219
|
### Volumes
|
|
220
220
|
|
|
221
|
-
Morph Volumes exposes an S3-compatible object store.
|
|
221
|
+
Morph Volumes exposes an S3-compatible object store. Bucket lifecycle and object operations use your normal `MORPH_API_KEY`.
|
|
222
222
|
|
|
223
223
|
```python
|
|
224
224
|
import os
|
|
@@ -226,8 +226,7 @@ import os
|
|
|
226
226
|
from morphcloud import MorphCloudClient
|
|
227
227
|
|
|
228
228
|
client = MorphCloudClient(
|
|
229
|
-
api_key="
|
|
230
|
-
volumes_service_api_key=os.environ["MORPH_VOLUMES_SERVICE_API_KEY"],
|
|
229
|
+
api_key=os.environ["MORPH_API_KEY"],
|
|
231
230
|
)
|
|
232
231
|
|
|
233
232
|
client.volumes.create_bucket("my-volume-bucket")
|
|
@@ -267,10 +266,9 @@ morphcloud volumes rm -r s3://my-volume-bucket/datasets/
|
|
|
267
266
|
If you use profiles with non-default endpoints, the volumes gateway can be configured explicitly:
|
|
268
267
|
|
|
269
268
|
```bash
|
|
270
|
-
morphcloud profile set
|
|
271
|
-
--api-host
|
|
272
|
-
--
|
|
273
|
-
--volumes-base-url https://volumes.svc.stage.morph.so
|
|
269
|
+
morphcloud profile set prod \
|
|
270
|
+
--api-host cloud.morph.so \
|
|
271
|
+
--volumes-base-url https://volumes.svc.cloud.morph.so
|
|
274
272
|
```
|
|
275
273
|
|
|
276
274
|
### Interactive Tools
|
|
@@ -187,7 +187,7 @@ morphcloud instance copy -r ./local_dir inst_123:/remote/dir
|
|
|
187
187
|
|
|
188
188
|
### Volumes
|
|
189
189
|
|
|
190
|
-
Morph Volumes exposes an S3-compatible object store.
|
|
190
|
+
Morph Volumes exposes an S3-compatible object store. Bucket lifecycle and object operations use your normal `MORPH_API_KEY`.
|
|
191
191
|
|
|
192
192
|
```python
|
|
193
193
|
import os
|
|
@@ -195,8 +195,7 @@ import os
|
|
|
195
195
|
from morphcloud import MorphCloudClient
|
|
196
196
|
|
|
197
197
|
client = MorphCloudClient(
|
|
198
|
-
api_key="
|
|
199
|
-
volumes_service_api_key=os.environ["MORPH_VOLUMES_SERVICE_API_KEY"],
|
|
198
|
+
api_key=os.environ["MORPH_API_KEY"],
|
|
200
199
|
)
|
|
201
200
|
|
|
202
201
|
client.volumes.create_bucket("my-volume-bucket")
|
|
@@ -236,10 +235,9 @@ morphcloud volumes rm -r s3://my-volume-bucket/datasets/
|
|
|
236
235
|
If you use profiles with non-default endpoints, the volumes gateway can be configured explicitly:
|
|
237
236
|
|
|
238
237
|
```bash
|
|
239
|
-
morphcloud profile set
|
|
240
|
-
--api-host
|
|
241
|
-
--
|
|
242
|
-
--volumes-base-url https://volumes.svc.stage.morph.so
|
|
238
|
+
morphcloud profile set prod \
|
|
239
|
+
--api-host cloud.morph.so \
|
|
240
|
+
--volumes-base-url https://volumes.svc.cloud.morph.so
|
|
243
241
|
```
|
|
244
242
|
|
|
245
243
|
### Interactive Tools
|
|
@@ -4,7 +4,6 @@ import datetime as dt
|
|
|
4
4
|
import hashlib
|
|
5
5
|
import hmac
|
|
6
6
|
import json
|
|
7
|
-
import os
|
|
8
7
|
import re
|
|
9
8
|
import typing
|
|
10
9
|
import xml.etree.ElementTree as ET
|
|
@@ -186,19 +185,11 @@ class VolumesClient:
|
|
|
186
185
|
def __init__(self, client: MorphCloudClient):
|
|
187
186
|
self._client = client
|
|
188
187
|
self.base_url = str(client.volumes_base_url or "").rstrip("/")
|
|
189
|
-
self.service_base_url = str(client.service_base_url or "").rstrip("/")
|
|
190
|
-
self.volumes_service_api_key = str(
|
|
191
|
-
getattr(client, "volumes_service_api_key", None)
|
|
192
|
-
or os.environ.get("MORPH_VOLUMES_SERVICE_API_KEY")
|
|
193
|
-
or ""
|
|
194
|
-
).strip()
|
|
195
|
-
self._active_organization_id: str | None = None
|
|
196
188
|
transport = build_http_transport()
|
|
197
189
|
self._http_client = httpx.Client(
|
|
198
190
|
timeout=None,
|
|
199
191
|
transport=transport,
|
|
200
192
|
)
|
|
201
|
-
self._json_client = httpx.Client(timeout=30, transport=transport)
|
|
202
193
|
|
|
203
194
|
def _compose_url(
|
|
204
195
|
self,
|
|
@@ -319,69 +310,6 @@ class VolumesClient:
|
|
|
319
310
|
)
|
|
320
311
|
return response
|
|
321
312
|
|
|
322
|
-
def _require_volumes_service_api_key(self) -> str:
|
|
323
|
-
if not self.volumes_service_api_key:
|
|
324
|
-
raise ValueError(
|
|
325
|
-
"Bucket creation requires MORPH_VOLUMES_SERVICE_API_KEY or "
|
|
326
|
-
"MorphCloudClient(..., volumes_service_api_key=...)."
|
|
327
|
-
)
|
|
328
|
-
return self.volumes_service_api_key
|
|
329
|
-
|
|
330
|
-
def _get_active_organization_id(self) -> str:
|
|
331
|
-
if self._active_organization_id:
|
|
332
|
-
return self._active_organization_id
|
|
333
|
-
|
|
334
|
-
response = self._json_client.request(
|
|
335
|
-
method="GET",
|
|
336
|
-
url=f"{self._client.base_url.rstrip('/')}/orgs/active",
|
|
337
|
-
headers={"Authorization": f"Bearer {self._client.api_key}"},
|
|
338
|
-
)
|
|
339
|
-
if response.is_error:
|
|
340
|
-
raise ApiError(
|
|
341
|
-
f"Volumes organization lookup failed for url '{response.url}'",
|
|
342
|
-
response.status_code,
|
|
343
|
-
_parse_error_body(response.text),
|
|
344
|
-
)
|
|
345
|
-
|
|
346
|
-
payload = response.json()
|
|
347
|
-
if not isinstance(payload, dict):
|
|
348
|
-
raise ValueError("Unexpected active organization response.")
|
|
349
|
-
|
|
350
|
-
organization_id = (
|
|
351
|
-
payload.get("organization_id")
|
|
352
|
-
or payload.get("id")
|
|
353
|
-
or (payload.get("organization") or {}).get("id")
|
|
354
|
-
)
|
|
355
|
-
if not organization_id:
|
|
356
|
-
raise ValueError("Could not determine the active organization for volumes.")
|
|
357
|
-
|
|
358
|
-
self._active_organization_id = str(organization_id)
|
|
359
|
-
return self._active_organization_id
|
|
360
|
-
|
|
361
|
-
def _service_request(
|
|
362
|
-
self,
|
|
363
|
-
method: str,
|
|
364
|
-
path: str,
|
|
365
|
-
*,
|
|
366
|
-
json_body: typing.Mapping[str, typing.Any] | None = None,
|
|
367
|
-
) -> httpx.Response:
|
|
368
|
-
response = self._json_client.request(
|
|
369
|
-
method=method.upper(),
|
|
370
|
-
url=f"{self.service_base_url}{path}",
|
|
371
|
-
headers={
|
|
372
|
-
"Authorization": (f"Bearer {self._require_volumes_service_api_key()}"),
|
|
373
|
-
"X-Morph-Organization-ID": self._get_active_organization_id(),
|
|
374
|
-
},
|
|
375
|
-
json=dict(json_body or {}),
|
|
376
|
-
)
|
|
377
|
-
if response.is_error:
|
|
378
|
-
raise ApiError(
|
|
379
|
-
f"Volumes control-plane request failed for url '{response.url}'",
|
|
380
|
-
response.status_code,
|
|
381
|
-
_parse_error_body(response.text),
|
|
382
|
-
)
|
|
383
|
-
return response
|
|
384
|
-
|
|
385
313
|
def list_buckets(self) -> list[VolumeBucket]:
|
|
386
314
|
response = self._request(
|
|
387
315
|
"GET",
|
|
@@ -401,18 +329,13 @@ class VolumesClient:
|
|
|
401
329
|
|
|
402
330
|
def create_bucket(self, bucket: str) -> VolumeBucket:
|
|
403
331
|
normalized_bucket = _normalize_bucket(bucket)
|
|
404
|
-
|
|
405
|
-
"
|
|
406
|
-
|
|
407
|
-
json_body={"name": normalized_bucket},
|
|
332
|
+
self._request(
|
|
333
|
+
"PUT",
|
|
334
|
+
self._compose_url(bucket=normalized_bucket),
|
|
408
335
|
)
|
|
409
|
-
payload = response.json()
|
|
410
|
-
volume_payload = payload.get("volume") if isinstance(payload, dict) else None
|
|
411
|
-
if not isinstance(volume_payload, dict):
|
|
412
|
-
raise ValueError("Unexpected create volume response from service API.")
|
|
413
336
|
return VolumeBucket(
|
|
414
|
-
name=
|
|
415
|
-
created_at=
|
|
337
|
+
name=normalized_bucket,
|
|
338
|
+
created_at=None,
|
|
416
339
|
)
|
|
417
340
|
|
|
418
341
|
def delete_bucket(self, bucket: str) -> None:
|
|
@@ -50,6 +50,30 @@ def test_volumes_ls_lists_buckets(monkeypatch):
|
|
|
50
50
|
assert "beta" in result.output
|
|
51
51
|
|
|
52
52
|
|
|
53
|
+
def test_volumes_mb_creates_bucket(monkeypatch):
|
|
54
|
+
import morphcloud.cli as cli_mod
|
|
55
|
+
import morphcloud.volumes.cli as volumes_cli
|
|
56
|
+
|
|
57
|
+
_install_noop_spinner(monkeypatch)
|
|
58
|
+
|
|
59
|
+
calls = []
|
|
60
|
+
stub_volumes = types.SimpleNamespace(
|
|
61
|
+
create_bucket=lambda bucket: calls.append(bucket),
|
|
62
|
+
)
|
|
63
|
+
monkeypatch.setattr(
|
|
64
|
+
volumes_cli,
|
|
65
|
+
"get_client",
|
|
66
|
+
lambda: types.SimpleNamespace(volumes=stub_volumes),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
runner = CliRunner()
|
|
70
|
+
result = runner.invoke(cli_mod.cli, ["volumes", "mb", "demo"])
|
|
71
|
+
|
|
72
|
+
assert result.exit_code == 0, result.output
|
|
73
|
+
assert calls == ["demo"]
|
|
74
|
+
assert "s3://demo" in result.output
|
|
75
|
+
|
|
76
|
+
|
|
53
77
|
def test_volumes_cp_uploads_to_bucket_prefix(monkeypatch, tmp_path):
|
|
54
78
|
import morphcloud.cli as cli_mod
|
|
55
79
|
import morphcloud.volumes.cli as volumes_cli
|
|
@@ -57,19 +57,15 @@ class StubHTTPClient:
|
|
|
57
57
|
def _build_client(
|
|
58
58
|
*,
|
|
59
59
|
s3_responses=(),
|
|
60
|
-
json_responses=(),
|
|
61
|
-
volumes_service_api_key="morph_service_key",
|
|
62
60
|
):
|
|
63
61
|
morph_client = types.SimpleNamespace(
|
|
64
62
|
api_key="morph_test_key",
|
|
65
63
|
base_url="https://cloud.morph.so/api",
|
|
66
64
|
service_base_url="https://service.svc.stage.morph.so",
|
|
67
65
|
volumes_base_url="https://volumes.svc.stage.morph.so",
|
|
68
|
-
volumes_service_api_key=volumes_service_api_key,
|
|
69
66
|
)
|
|
70
67
|
client = VolumesClient(morph_client)
|
|
71
68
|
client._http_client = StubHTTPClient(s3_responses)
|
|
72
|
-
client._json_client = StubHTTPClient(json_responses)
|
|
73
69
|
return client
|
|
74
70
|
|
|
75
71
|
|
|
@@ -95,55 +91,49 @@ def test_list_buckets_parses_xml():
|
|
|
95
91
|
assert buckets[0].created_at == "2026-04-01T00:00:00Z"
|
|
96
92
|
|
|
97
93
|
|
|
98
|
-
def
|
|
94
|
+
def test_create_bucket_uses_signed_gateway_request():
|
|
99
95
|
client = _build_client(
|
|
100
|
-
|
|
101
|
-
StubResponse(
|
|
102
|
-
json_data={
|
|
103
|
-
"organization_id": "organization_demo",
|
|
104
|
-
"organization": {"id": "organization_demo"},
|
|
105
|
-
},
|
|
106
|
-
url="https://cloud.morph.so/api/orgs/active",
|
|
107
|
-
),
|
|
96
|
+
s3_responses=[
|
|
108
97
|
StubResponse(
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
},
|
|
115
|
-
url="https://service.svc.stage.morph.so/service/volume",
|
|
116
|
-
),
|
|
98
|
+
status_code=200,
|
|
99
|
+
headers={"Location": "https://volumes.svc.stage.morph.so/demo"},
|
|
100
|
+
url="https://volumes.svc.stage.morph.so/demo",
|
|
101
|
+
)
|
|
117
102
|
]
|
|
118
103
|
)
|
|
119
104
|
|
|
120
105
|
bucket = client.create_bucket("demo")
|
|
121
106
|
|
|
122
107
|
assert bucket.name == "demo"
|
|
123
|
-
assert bucket.created_at
|
|
124
|
-
assert client.
|
|
125
|
-
assert client.
|
|
126
|
-
assert client.
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
assert client._json_client.calls[1]["url"].endswith("/service/volume")
|
|
131
|
-
assert client._json_client.calls[1]["headers"]["Authorization"] == (
|
|
132
|
-
"Bearer morph_service_key"
|
|
108
|
+
assert bucket.created_at is None
|
|
109
|
+
assert len(client._http_client.calls) == 1
|
|
110
|
+
assert client._http_client.calls[0]["method"] == "PUT"
|
|
111
|
+
assert client._http_client.calls[0]["url"] == "https://volumes.svc.stage.morph.so/demo"
|
|
112
|
+
assert client._http_client.calls[0]["content"] is None
|
|
113
|
+
assert client._http_client.calls[0]["headers"]["Authorization"].startswith(
|
|
114
|
+
"AWS4-HMAC-SHA256 "
|
|
133
115
|
)
|
|
134
|
-
assert client._json_client.calls[1]["headers"]["X-Morph-Organization-ID"] == (
|
|
135
|
-
"organization_demo"
|
|
136
|
-
)
|
|
137
|
-
assert client._json_client.calls[1]["json"] == {"name": "demo"}
|
|
138
116
|
|
|
139
117
|
|
|
140
|
-
def
|
|
141
|
-
client = _build_client(
|
|
118
|
+
def test_create_bucket_raises_api_error_on_conflict():
|
|
119
|
+
client = _build_client(
|
|
120
|
+
s3_responses=[
|
|
121
|
+
StubResponse(
|
|
122
|
+
status_code=409,
|
|
123
|
+
text=(
|
|
124
|
+
"<Error><Code>BucketAlreadyExists</Code>"
|
|
125
|
+
"<Message>bucket already exists</Message></Error>"
|
|
126
|
+
),
|
|
127
|
+
url="https://volumes.svc.stage.morph.so/demo",
|
|
128
|
+
)
|
|
129
|
+
]
|
|
130
|
+
)
|
|
142
131
|
|
|
143
|
-
with pytest.raises(
|
|
132
|
+
with pytest.raises(ApiError) as excinfo:
|
|
144
133
|
client.create_bucket("demo")
|
|
145
134
|
|
|
146
|
-
assert
|
|
135
|
+
assert excinfo.value.status_code == 409
|
|
136
|
+
assert "BucketAlreadyExists" in excinfo.value.response_body
|
|
147
137
|
|
|
148
138
|
|
|
149
139
|
def test_list_directory_derives_prefixes_from_flat_listing():
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/core/remove_none_from_dict.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/devboxes_actions/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/devboxes_actions/raw_client.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/devboxes_lifecycle/__init__.py
RENAMED
|
File without changes
|
{morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/devboxes_lifecycle/client.py
RENAMED
|
File without changes
|
{morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/devboxes_lifecycle/raw_client.py
RENAMED
|
File without changes
|
|
File without changes
|
{morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/errors/unprocessable_entity_error.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/devbox_detail_list_response.py
RENAMED
|
File without changes
|
{morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/devbox_list_response.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/devbox_summary_response.py
RENAMED
|
File without changes
|
|
File without changes
|
{morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/http_service_auth_mode.py
RENAMED
|
File without changes
|
{morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/http_validation_error.py
RENAMED
|
File without changes
|
|
File without changes
|
{morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/secret_value_response.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/template_cache_request.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/template_list_response.py
RENAMED
|
File without changes
|
{morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/template_share_response.py
RENAMED
|
File without changes
|
|
File without changes
|
{morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/template_step_model.py
RENAMED
|
File without changes
|
{morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/template_step_status.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/tmux_debug_info_response.py
RENAMED
|
File without changes
|
{morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/tmux_install_response.py
RENAMED
|
File without changes
|
{morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/tmux_new_session_response.py
RENAMED
|
File without changes
|
{morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/tmux_send_keys_response.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/tmux_snapshot_response.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/validation_error_loc_item.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|