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.
Files changed (155) hide show
  1. {morphcloud-0.1.110 → morphcloud-0.1.111}/PKG-INFO +6 -8
  2. {morphcloud-0.1.110 → morphcloud-0.1.111}/README.md +5 -7
  3. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/volumes/client.py +5 -82
  4. {morphcloud-0.1.110 → morphcloud-0.1.111}/pyproject.toml +1 -1
  5. {morphcloud-0.1.110 → morphcloud-0.1.111}/tests/unit/test_cli_volumes.py +24 -0
  6. {morphcloud-0.1.110 → morphcloud-0.1.111}/tests/unit/test_volumes_client.py +29 -39
  7. {morphcloud-0.1.110 → morphcloud-0.1.111}/.github/workflows/publish.yaml +0 -0
  8. {morphcloud-0.1.110 → morphcloud-0.1.111}/.gitignore +0 -0
  9. {morphcloud-0.1.110 → morphcloud-0.1.111}/LICENSE +0 -0
  10. {morphcloud-0.1.110 → morphcloud-0.1.111}/Makefile +0 -0
  11. {morphcloud-0.1.110 → morphcloud-0.1.111}/conf.py +0 -0
  12. {morphcloud-0.1.110 → morphcloud-0.1.111}/examples/browser_example.py +0 -0
  13. {morphcloud-0.1.110 → morphcloud-0.1.111}/examples/devbox_example.py +0 -0
  14. {morphcloud-0.1.110 → morphcloud-0.1.111}/index.rst +0 -0
  15. {morphcloud-0.1.110 → morphcloud-0.1.111}/make.bat +0 -0
  16. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/__init__.py +0 -0
  17. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/_asyncify.py +0 -0
  18. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/_bash_interpreter.py +0 -0
  19. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/_http_transport.py +0 -0
  20. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/_llm.py +0 -0
  21. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/_scramble.py +0 -0
  22. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/_ssh.py +0 -0
  23. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/_utils.py +0 -0
  24. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/api.py +0 -0
  25. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/cli.py +0 -0
  26. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/cli_helpers.py +0 -0
  27. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/config.py +0 -0
  28. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/__init__.py +0 -0
  29. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/cli.py +0 -0
  30. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/client.py +0 -0
  31. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/config.py +0 -0
  32. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/__init__.py +0 -0
  33. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/admin/__init__.py +0 -0
  34. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/admin/client.py +0 -0
  35. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/admin/raw_client.py +0 -0
  36. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/aliases/__init__.py +0 -0
  37. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/aliases/client.py +0 -0
  38. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/aliases/raw_client.py +0 -0
  39. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/client.py +0 -0
  40. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/core/__init__.py +0 -0
  41. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/core/api_error.py +0 -0
  42. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/core/client_wrapper.py +0 -0
  43. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/core/datetime_utils.py +0 -0
  44. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/core/file.py +0 -0
  45. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/core/force_multipart.py +0 -0
  46. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/core/http_client.py +0 -0
  47. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/core/http_response.py +0 -0
  48. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/core/jsonable_encoder.py +0 -0
  49. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/core/pydantic_utilities.py +0 -0
  50. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/core/query_encoder.py +0 -0
  51. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/core/remove_none_from_dict.py +0 -0
  52. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/core/request_options.py +0 -0
  53. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/core/serialization.py +0 -0
  54. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/devboxes_actions/__init__.py +0 -0
  55. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/devboxes_actions/client.py +0 -0
  56. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/devboxes_actions/raw_client.py +0 -0
  57. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/devboxes_core/__init__.py +0 -0
  58. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/devboxes_core/client.py +0 -0
  59. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/devboxes_core/raw_client.py +0 -0
  60. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/devboxes_lifecycle/__init__.py +0 -0
  61. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/devboxes_lifecycle/client.py +0 -0
  62. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/devboxes_lifecycle/raw_client.py +0 -0
  63. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/errors/__init__.py +0 -0
  64. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/errors/unprocessable_entity_error.py +0 -0
  65. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/templates/__init__.py +0 -0
  66. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/templates/client.py +0 -0
  67. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/templates/raw_client.py +0 -0
  68. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/tmux/__init__.py +0 -0
  69. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/tmux/client.py +0 -0
  70. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/tmux/raw_client.py +0 -0
  71. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/__init__.py +0 -0
  72. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/alias_response.py +0 -0
  73. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/branch_response.py +0 -0
  74. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/delete_response.py +0 -0
  75. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/devbox_detail_list_response.py +0 -0
  76. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/devbox_list_response.py +0 -0
  77. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/devbox_refs.py +0 -0
  78. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/devbox_response.py +0 -0
  79. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/devbox_spec.py +0 -0
  80. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/devbox_ssh_credentials_response.py +0 -0
  81. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/devbox_summary_response.py +0 -0
  82. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/http_service.py +0 -0
  83. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/http_service_auth_mode.py +0 -0
  84. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/http_validation_error.py +0 -0
  85. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/networking.py +0 -0
  86. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/secret_value_response.py +0 -0
  87. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/snapshot_response.py +0 -0
  88. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/success_response.py +0 -0
  89. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/template_cache_request.py +0 -0
  90. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/template_cache_start_response.py +0 -0
  91. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/template_detail.py +0 -0
  92. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/template_list_response.py +0 -0
  93. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/template_share_response.py +0 -0
  94. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/template_status.py +0 -0
  95. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/template_step_model.py +0 -0
  96. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/template_step_status.py +0 -0
  97. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/template_summary.py +0 -0
  98. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/timeline_event.py +0 -0
  99. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/tmux_debug_info_response.py +0 -0
  100. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/tmux_install_response.py +0 -0
  101. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/tmux_new_session_response.py +0 -0
  102. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/tmux_send_keys_response.py +0 -0
  103. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/tmux_session.py +0 -0
  104. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/tmux_sessions_list.py +0 -0
  105. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/tmux_snapshot_response.py +0 -0
  106. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/tmux_target.py +0 -0
  107. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/ttl.py +0 -0
  108. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/upload_response.py +0 -0
  109. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/validation_error.py +0 -0
  110. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/validation_error_loc_item.py +0 -0
  111. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/types/wake_on.py +0 -0
  112. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/uploads/__init__.py +0 -0
  113. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/uploads/client.py +0 -0
  114. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/uploads/raw_client.py +0 -0
  115. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/user_secrets/__init__.py +0 -0
  116. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/user_secrets/client.py +0 -0
  117. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/gen/user_secrets/raw_client.py +0 -0
  118. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/openapi.json +0 -0
  119. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/template_local_runner.py +0 -0
  120. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/template_runner.py +0 -0
  121. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/devbox/terminals.py +0 -0
  122. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/experimental/__init__.py +0 -0
  123. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/experimental/browser.py +0 -0
  124. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/py.typed +0 -0
  125. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/volumes/__init__.py +0 -0
  126. {morphcloud-0.1.110 → morphcloud-0.1.111}/morphcloud/volumes/cli.py +0 -0
  127. {morphcloud-0.1.110 → morphcloud-0.1.111}/requirements.txt +0 -0
  128. {morphcloud-0.1.110 → morphcloud-0.1.111}/scripts/abuild_smoke.py +0 -0
  129. {morphcloud-0.1.110 → morphcloud-0.1.111}/scripts/increment_version.py +0 -0
  130. {morphcloud-0.1.110 → morphcloud-0.1.111}/tests/integration/__init__.py +0 -0
  131. {morphcloud-0.1.110 → morphcloud-0.1.111}/tests/integration/conftest.py +0 -0
  132. {morphcloud-0.1.110 → morphcloud-0.1.111}/tests/integration/pytest.ini +0 -0
  133. {morphcloud-0.1.110 → morphcloud-0.1.111}/tests/integration/test_as_container.py +0 -0
  134. {morphcloud-0.1.110 → morphcloud-0.1.111}/tests/integration/test_branching.py +0 -0
  135. {morphcloud-0.1.110 → morphcloud-0.1.111}/tests/integration/test_command_execution.py +0 -0
  136. {morphcloud-0.1.110 → morphcloud-0.1.111}/tests/integration/test_file_operations.py +0 -0
  137. {morphcloud-0.1.110 → morphcloud-0.1.111}/tests/integration/test_from_tag_multiple.py +0 -0
  138. {morphcloud-0.1.110 → morphcloud-0.1.111}/tests/integration/test_http_service.py +0 -0
  139. {morphcloud-0.1.110 → morphcloud-0.1.111}/tests/integration/test_metadata.py +0 -0
  140. {morphcloud-0.1.110 → morphcloud-0.1.111}/tests/integration/test_simple.py +0 -0
  141. {morphcloud-0.1.110 → morphcloud-0.1.111}/tests/integration/test_snapshot_operations.py +0 -0
  142. {morphcloud-0.1.110 → morphcloud-0.1.111}/tests/integration/test_ssh_key_rotation.py +0 -0
  143. {morphcloud-0.1.110 → morphcloud-0.1.111}/tests/integration/test_ttl.py +0 -0
  144. {morphcloud-0.1.110 → morphcloud-0.1.111}/tests/integration/test_user.py +0 -0
  145. {morphcloud-0.1.110 → morphcloud-0.1.111}/tests/unit/test_cli_devbox.py +0 -0
  146. {morphcloud-0.1.110 → morphcloud-0.1.111}/tests/unit/test_cli_devbox_terminal.py +0 -0
  147. {morphcloud-0.1.110 → morphcloud-0.1.111}/tests/unit/test_cli_secrets.py +0 -0
  148. {morphcloud-0.1.110 → morphcloud-0.1.111}/tests/unit/test_cli_snapshots.py +0 -0
  149. {morphcloud-0.1.110 → morphcloud-0.1.111}/tests/unit/test_devbox_terminals.py +0 -0
  150. {morphcloud-0.1.110 → morphcloud-0.1.111}/tests/unit/test_http_transport_sharding.py +0 -0
  151. {morphcloud-0.1.110 → morphcloud-0.1.111}/tests/unit/test_profiles.py +0 -0
  152. {morphcloud-0.1.110 → morphcloud-0.1.111}/tests/unit/test_snapshot_ttl.py +0 -0
  153. {morphcloud-0.1.110 → morphcloud-0.1.111}/tests/unit/test_ssh_connect_retry.py +0 -0
  154. {morphcloud-0.1.110 → morphcloud-0.1.111}/tests/unit/test_template_local_runner.py +0 -0
  155. {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.110
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. Object operations use your normal `MORPH_API_KEY`. Bucket creation requires a volumes service API key via `MORPH_VOLUMES_SERVICE_API_KEY`.
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="morph_...",
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 stage \
271
- --api-host stage.morph.so \
272
- --service-base-url https://service.svc.stage.morph.so \
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. Object operations use your normal `MORPH_API_KEY`. Bucket creation requires a volumes service API key via `MORPH_VOLUMES_SERVICE_API_KEY`.
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="morph_...",
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 stage \
240
- --api-host stage.morph.so \
241
- --service-base-url https://service.svc.stage.morph.so \
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
- response = self._service_request(
405
- "POST",
406
- "/service/volume",
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=str(volume_payload.get("name") or normalized_bucket),
415
- created_at=typing.cast(str | None, volume_payload.get("created_at")),
337
+ name=normalized_bucket,
338
+ created_at=None,
416
339
  )
417
340
 
418
341
  def delete_bucket(self, bucket: str) -> None:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "morphcloud"
3
- version = "0.1.110"
3
+ version = "0.1.111"
4
4
  description = "A Python SDK and CLI tool for creating, managing, and interacting with Morph Cloud VMs."
5
5
  authors = [
6
6
  {name = "Morph Labs", email = "jobs@morph.so"}
@@ -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 test_create_bucket_uses_service_api_and_active_org():
94
+ def test_create_bucket_uses_signed_gateway_request():
99
95
  client = _build_client(
100
- json_responses=[
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
- json_data={
110
- "volume": {
111
- "name": "demo",
112
- "created_at": "2026-04-06T00:00:00Z",
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 == "2026-04-06T00:00:00Z"
124
- assert client._json_client.calls[0]["method"] == "GET"
125
- assert client._json_client.calls[0]["url"].endswith("/orgs/active")
126
- assert client._json_client.calls[0]["headers"]["Authorization"] == (
127
- "Bearer morph_test_key"
128
- )
129
- assert client._json_client.calls[1]["method"] == "POST"
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 test_create_bucket_requires_service_api_key():
141
- client = _build_client(volumes_service_api_key="")
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(ValueError) as excinfo:
132
+ with pytest.raises(ApiError) as excinfo:
144
133
  client.create_bucket("demo")
145
134
 
146
- assert "MORPH_VOLUMES_SERVICE_API_KEY" in str(excinfo.value)
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