morphcloud 0.1.114__tar.gz → 0.1.116__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 (175) hide show
  1. {morphcloud-0.1.114 → morphcloud-0.1.116}/PKG-INFO +2 -2
  2. morphcloud-0.1.116/morphcloud/_disk_resize.py +154 -0
  3. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/api.py +131 -0
  4. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/cli.py +1 -1
  5. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/instances.py +41 -2
  6. {morphcloud-0.1.114 → morphcloud-0.1.116}/pyproject.toml +2 -2
  7. {morphcloud-0.1.114 → morphcloud-0.1.116}/.github/workflows/publish.yaml +0 -0
  8. {morphcloud-0.1.114 → morphcloud-0.1.116}/.gitignore +0 -0
  9. {morphcloud-0.1.114 → morphcloud-0.1.116}/LICENSE +0 -0
  10. {morphcloud-0.1.114 → morphcloud-0.1.116}/Makefile +0 -0
  11. {morphcloud-0.1.114 → morphcloud-0.1.116}/README.md +0 -0
  12. {morphcloud-0.1.114 → morphcloud-0.1.116}/conf.py +0 -0
  13. {morphcloud-0.1.114 → morphcloud-0.1.116}/examples/browser_example.py +0 -0
  14. {morphcloud-0.1.114 → morphcloud-0.1.116}/examples/devbox_example.py +0 -0
  15. {morphcloud-0.1.114 → morphcloud-0.1.116}/index.rst +0 -0
  16. {morphcloud-0.1.114 → morphcloud-0.1.116}/make.bat +0 -0
  17. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/__init__.py +0 -0
  18. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/_asyncify.py +0 -0
  19. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/_bash_interpreter.py +0 -0
  20. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/_http_transport.py +0 -0
  21. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/_scramble.py +0 -0
  22. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/_ssh.py +0 -0
  23. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/_synapse.py +0 -0
  24. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/_utils.py +0 -0
  25. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/cli_helpers.py +0 -0
  26. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/cli_runtime.py +0 -0
  27. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/config.py +0 -0
  28. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/__init__.py +0 -0
  29. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/cli.py +0 -0
  30. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/client.py +0 -0
  31. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/config.py +0 -0
  32. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/__init__.py +0 -0
  33. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/admin/__init__.py +0 -0
  34. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/admin/client.py +0 -0
  35. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/admin/raw_client.py +0 -0
  36. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/aliases/__init__.py +0 -0
  37. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/aliases/client.py +0 -0
  38. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/aliases/raw_client.py +0 -0
  39. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/client.py +0 -0
  40. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/core/__init__.py +0 -0
  41. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/core/api_error.py +0 -0
  42. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/core/client_wrapper.py +0 -0
  43. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/core/datetime_utils.py +0 -0
  44. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/core/file.py +0 -0
  45. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/core/force_multipart.py +0 -0
  46. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/core/http_client.py +0 -0
  47. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/core/http_response.py +0 -0
  48. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/core/jsonable_encoder.py +0 -0
  49. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/core/pydantic_utilities.py +0 -0
  50. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/core/query_encoder.py +0 -0
  51. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/core/remove_none_from_dict.py +0 -0
  52. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/core/request_options.py +0 -0
  53. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/core/serialization.py +0 -0
  54. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/devboxes_actions/__init__.py +0 -0
  55. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/devboxes_actions/client.py +0 -0
  56. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/devboxes_actions/raw_client.py +0 -0
  57. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/devboxes_core/__init__.py +0 -0
  58. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/devboxes_core/client.py +0 -0
  59. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/devboxes_core/raw_client.py +0 -0
  60. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/devboxes_lifecycle/__init__.py +0 -0
  61. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/devboxes_lifecycle/client.py +0 -0
  62. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/devboxes_lifecycle/raw_client.py +0 -0
  63. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/errors/__init__.py +0 -0
  64. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/errors/unprocessable_entity_error.py +0 -0
  65. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/templates/__init__.py +0 -0
  66. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/templates/client.py +0 -0
  67. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/templates/raw_client.py +0 -0
  68. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/tmux/__init__.py +0 -0
  69. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/tmux/client.py +0 -0
  70. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/tmux/raw_client.py +0 -0
  71. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/types/__init__.py +0 -0
  72. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/types/alias_response.py +0 -0
  73. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/types/branch_response.py +0 -0
  74. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/types/delete_response.py +0 -0
  75. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/types/devbox_detail_list_response.py +0 -0
  76. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/types/devbox_list_response.py +0 -0
  77. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/types/devbox_refs.py +0 -0
  78. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/types/devbox_response.py +0 -0
  79. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/types/devbox_spec.py +0 -0
  80. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/types/devbox_ssh_credentials_response.py +0 -0
  81. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/types/devbox_summary_response.py +0 -0
  82. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/types/http_service.py +0 -0
  83. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/types/http_service_auth_mode.py +0 -0
  84. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/types/http_validation_error.py +0 -0
  85. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/types/networking.py +0 -0
  86. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/types/secret_value_response.py +0 -0
  87. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/types/snapshot_response.py +0 -0
  88. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/types/success_response.py +0 -0
  89. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/types/template_cache_request.py +0 -0
  90. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/types/template_cache_start_response.py +0 -0
  91. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/types/template_detail.py +0 -0
  92. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/types/template_list_response.py +0 -0
  93. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/types/template_share_response.py +0 -0
  94. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/types/template_status.py +0 -0
  95. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/types/template_step_model.py +0 -0
  96. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/types/template_step_status.py +0 -0
  97. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/types/template_summary.py +0 -0
  98. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/types/timeline_event.py +0 -0
  99. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/types/tmux_debug_info_response.py +0 -0
  100. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/types/tmux_install_response.py +0 -0
  101. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/types/tmux_new_session_response.py +0 -0
  102. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/types/tmux_send_keys_response.py +0 -0
  103. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/types/tmux_session.py +0 -0
  104. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/types/tmux_sessions_list.py +0 -0
  105. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/types/tmux_snapshot_response.py +0 -0
  106. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/types/tmux_target.py +0 -0
  107. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/types/ttl.py +0 -0
  108. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/types/upload_response.py +0 -0
  109. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/types/validation_error.py +0 -0
  110. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/types/validation_error_loc_item.py +0 -0
  111. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/types/wake_on.py +0 -0
  112. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/uploads/__init__.py +0 -0
  113. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/uploads/client.py +0 -0
  114. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/uploads/raw_client.py +0 -0
  115. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/user_secrets/__init__.py +0 -0
  116. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/user_secrets/client.py +0 -0
  117. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/gen/user_secrets/raw_client.py +0 -0
  118. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/openapi.json +0 -0
  119. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/template_local_runner.py +0 -0
  120. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/template_runner.py +0 -0
  121. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox/terminals.py +0 -0
  122. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/devbox_commands.py +0 -0
  123. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/experimental/__init__.py +0 -0
  124. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/experimental/browser.py +0 -0
  125. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/images.py +0 -0
  126. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/plugin_commands.py +0 -0
  127. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/plugins/__init__.py +0 -0
  128. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/plugins/cli.py +0 -0
  129. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/plugins/installer.py +0 -0
  130. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/profiles.py +0 -0
  131. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/py.typed +0 -0
  132. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/simple_index/__init__.py +0 -0
  133. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/simple_index/client.py +0 -0
  134. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/snapshots.py +0 -0
  135. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/users.py +0 -0
  136. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/volume_commands.py +0 -0
  137. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/volumes/__init__.py +0 -0
  138. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/volumes/cli.py +0 -0
  139. {morphcloud-0.1.114 → morphcloud-0.1.116}/morphcloud/volumes/client.py +0 -0
  140. {morphcloud-0.1.114 → morphcloud-0.1.116}/scripts/abuild_smoke.py +0 -0
  141. {morphcloud-0.1.114 → morphcloud-0.1.116}/scripts/increment_version.py +0 -0
  142. {morphcloud-0.1.114 → morphcloud-0.1.116}/tests/integration/__init__.py +0 -0
  143. {morphcloud-0.1.114 → morphcloud-0.1.116}/tests/integration/conftest.py +0 -0
  144. {morphcloud-0.1.114 → morphcloud-0.1.116}/tests/integration/pytest.ini +0 -0
  145. {morphcloud-0.1.114 → morphcloud-0.1.116}/tests/integration/test_as_container.py +0 -0
  146. {morphcloud-0.1.114 → morphcloud-0.1.116}/tests/integration/test_branching.py +0 -0
  147. {morphcloud-0.1.114 → morphcloud-0.1.116}/tests/integration/test_command_execution.py +0 -0
  148. {morphcloud-0.1.114 → morphcloud-0.1.116}/tests/integration/test_file_operations.py +0 -0
  149. {morphcloud-0.1.114 → morphcloud-0.1.116}/tests/integration/test_from_tag_multiple.py +0 -0
  150. {morphcloud-0.1.114 → morphcloud-0.1.116}/tests/integration/test_http_service.py +0 -0
  151. {morphcloud-0.1.114 → morphcloud-0.1.116}/tests/integration/test_managed_ssh_keys.py +0 -0
  152. {morphcloud-0.1.114 → morphcloud-0.1.116}/tests/integration/test_metadata.py +0 -0
  153. {morphcloud-0.1.114 → morphcloud-0.1.116}/tests/integration/test_simple.py +0 -0
  154. {morphcloud-0.1.114 → morphcloud-0.1.116}/tests/integration/test_snapshot_operations.py +0 -0
  155. {morphcloud-0.1.114 → morphcloud-0.1.116}/tests/integration/test_ssh_key_rotation.py +0 -0
  156. {morphcloud-0.1.114 → morphcloud-0.1.116}/tests/integration/test_ttl.py +0 -0
  157. {morphcloud-0.1.114 → morphcloud-0.1.116}/tests/integration/test_user.py +0 -0
  158. {morphcloud-0.1.114 → morphcloud-0.1.116}/tests/unit/test_cli_devbox.py +0 -0
  159. {morphcloud-0.1.114 → morphcloud-0.1.116}/tests/unit/test_cli_devbox_terminal.py +0 -0
  160. {morphcloud-0.1.114 → morphcloud-0.1.116}/tests/unit/test_cli_managed_ssh_keys.py +0 -0
  161. {morphcloud-0.1.114 → morphcloud-0.1.116}/tests/unit/test_cli_plugins.py +0 -0
  162. {morphcloud-0.1.114 → morphcloud-0.1.116}/tests/unit/test_cli_secrets.py +0 -0
  163. {morphcloud-0.1.114 → morphcloud-0.1.116}/tests/unit/test_cli_snapshots.py +0 -0
  164. {morphcloud-0.1.114 → morphcloud-0.1.116}/tests/unit/test_cli_volumes.py +0 -0
  165. {morphcloud-0.1.114 → morphcloud-0.1.116}/tests/unit/test_devbox_terminals.py +0 -0
  166. {morphcloud-0.1.114 → morphcloud-0.1.116}/tests/unit/test_http_transport_sharding.py +0 -0
  167. {morphcloud-0.1.114 → morphcloud-0.1.116}/tests/unit/test_managed_ssh_keys_api.py +0 -0
  168. {morphcloud-0.1.114 → morphcloud-0.1.116}/tests/unit/test_plugin_installer.py +0 -0
  169. {morphcloud-0.1.114 → morphcloud-0.1.116}/tests/unit/test_profiles.py +0 -0
  170. {morphcloud-0.1.114 → morphcloud-0.1.116}/tests/unit/test_simple_index_client.py +0 -0
  171. {morphcloud-0.1.114 → morphcloud-0.1.116}/tests/unit/test_snapshot_ttl.py +0 -0
  172. {morphcloud-0.1.114 → morphcloud-0.1.116}/tests/unit/test_ssh_connect_retry.py +0 -0
  173. {morphcloud-0.1.114 → morphcloud-0.1.116}/tests/unit/test_template_local_runner.py +0 -0
  174. {morphcloud-0.1.114 → morphcloud-0.1.116}/tests/unit/test_template_runner.py +0 -0
  175. {morphcloud-0.1.114 → morphcloud-0.1.116}/tests/unit/test_volumes_client.py +0 -0
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: morphcloud
3
- Version: 0.1.114
3
+ Version: 0.1.116
4
4
  Summary: A Python SDK and CLI tool for creating, managing, and interacting with Morph Cloud VMs.
5
5
  License: Apache-2.0
6
6
  License-File: LICENSE
7
- Requires-Python: >=3.11
7
+ Requires-Python: >=3.10
8
8
  Requires-Dist: click>=8.1.7
9
9
  Requires-Dist: httpx>=0.27.0
10
10
  Requires-Dist: packaging>=24.2
@@ -0,0 +1,154 @@
1
+ """Disk-capacity operation values and local waiting policy.
2
+
3
+ Waiting never changes a remote operation: timeouts and cancellation only stop
4
+ local observation. Every error preserves the request key and operation evidence
5
+ available to that invocation so a caller can safely resume observation or replay.
6
+ """
7
+ from __future__ import annotations
8
+ import math
9
+ import time
10
+ import uuid
11
+ from datetime import datetime
12
+ from typing import Literal
13
+ from urllib.parse import quote
14
+ from pydantic import BaseModel, ConfigDict, Field
15
+
16
+ class DiskResizeOperationError(BaseModel):
17
+ """Service rejection or failure details, including a measured minimum."""
18
+ model_config = ConfigDict(frozen=True)
19
+ code: str
20
+ message: str
21
+ retryable: bool
22
+ minimum_target_mib: int | None = Field(default=None, ge=0)
23
+
24
+ class DiskResizeOperation(BaseModel):
25
+ """An observed disk resize; byte observations are null until known.
26
+
27
+ ``requested_disk_size`` and ``previous_disk_size`` use MiB. A successful
28
+ operation changes effective capacity; backing storage observations may differ.
29
+ ``idempotency_key`` is local recovery context, never inferred by GET calls.
30
+ """
31
+ model_config = ConfigDict(frozen=True)
32
+ object: Literal['instance_disk_resize'] = 'instance_disk_resize'
33
+ id: str = Field(min_length=1)
34
+ instance_id: str = Field(min_length=1)
35
+ previous_disk_size: int = Field(ge=0)
36
+ requested_disk_size: int = Field(ge=256, le=1048576)
37
+ capacity_mode: str
38
+ status: Literal['pending', 'running', 'succeeded', 'failed', 'recovery_required']
39
+ phase: Literal['accepted', 'geometry_ready', 'budget_applied', 'complete']
40
+ capacity_generation: int = Field(ge=0)
41
+ effective_capacity_bytes: int | None = Field(default=None, ge=0)
42
+ statfs_total_bytes: int | None = Field(default=None, ge=0)
43
+ charged_bytes: int | None = Field(default=None, ge=0)
44
+ reserved_bytes: int | None = Field(default=None, ge=0)
45
+ backing_bytes: int | None = Field(default=None, ge=0)
46
+ guest_device_bytes: int | None = Field(default=None, ge=0)
47
+ ext4_geometry_bytes: int | None = Field(default=None, ge=0)
48
+ error: DiskResizeOperationError | None = None
49
+ retry_after_seconds: float = Field(default=1, ge=0, allow_inf_nan=False)
50
+ created_at: datetime
51
+ updated_at: datetime
52
+ completed_at: datetime | None = None
53
+ idempotency_key: str | None = None
54
+
55
+ class DiskResizeError(Exception):
56
+ """Base failure with enough evidence to resume or replay safely."""
57
+
58
+ def __init__(self, message: str, *, operation: DiskResizeOperation | None=None, operation_id: str | None=None, idempotency_key: str | None=None) -> None:
59
+ self.operation = operation
60
+ self.operation_id = operation.id if operation else operation_id
61
+ self.idempotency_key = idempotency_key or (operation.idempotency_key if operation else None)
62
+ recovery = []
63
+ if self.operation_id:
64
+ recovery.append(f'operation_id={self.operation_id}')
65
+ if self.idempotency_key:
66
+ recovery.append(f'idempotency_key={self.idempotency_key}')
67
+ if recovery:
68
+ message += ' (' + ', '.join(recovery) + ')'
69
+ super().__init__(message)
70
+
71
+ class DiskResizeFailed(DiskResizeError):
72
+ """The observed operation failed; inspect its structured error."""
73
+
74
+ class DiskResizeRecoveryRequired(DiskResizeError):
75
+ """The operation requires recovery; do not submit a replacement."""
76
+
77
+ class DiskResizeTimeout(DiskResizeError, TimeoutError):
78
+ """Local waiting expired. The remote operation was not cancelled."""
79
+
80
+ class DiskResizeRequestError(DiskResizeError):
81
+ """A request failed or returned invalid evidence; its cause is preserved."""
82
+
83
+ def __init__(self, message: str, *, cause: Exception, operation: DiskResizeOperation | None=None, operation_id: str | None=None, idempotency_key: str | None=None) -> None:
84
+ self.status_code = getattr(cause, 'status_code', None)
85
+ self.error_code = getattr(cause, 'error_code', None)
86
+ self.response_body = getattr(cause, 'response_body', None)
87
+ if self.status_code is not None:
88
+ message += f'; HTTP {self.status_code}'
89
+ if self.error_code is not None:
90
+ message += f'; {self.error_code}'
91
+ super().__init__(message, operation=operation, operation_id=operation_id, idempotency_key=idempotency_key)
92
+
93
+ class DiskResizeRefreshError(DiskResizeError):
94
+ """Resize succeeded, but refreshing the local Instance failed.
95
+
96
+ ``operation`` remains the successful receipt. Do not repeat the resize to
97
+ refresh an instance; retrieve the instance separately.
98
+ """
99
+
100
+ def request_key(disk_size: int, idempotency_key: str | None) -> str:
101
+ if type(disk_size) is not int or not 256 <= disk_size <= 1048576:
102
+ raise ValueError('disk_size must be an integer MiB value from 256 to 1048576')
103
+ if idempotency_key is None:
104
+ return str(uuid.uuid4())
105
+ if not isinstance(idempotency_key, str) or not 1 <= len(idempotency_key) <= 128:
106
+ raise ValueError('idempotency_key must contain 1 to 128 ASCII characters')
107
+ if any((ord(character) < 33 or ord(character) > 126 for character in idempotency_key)):
108
+ raise ValueError('idempotency_key must contain visible ASCII characters')
109
+ return idempotency_key
110
+
111
+ def deadline(timeout: float) -> float:
112
+ if isinstance(timeout, bool) or not isinstance(timeout, (int, float)):
113
+ raise ValueError('timeout must be a finite positive number of seconds')
114
+ if not math.isfinite(timeout) or timeout <= 0:
115
+ raise ValueError('timeout must be a finite positive number of seconds')
116
+ return time.monotonic() + timeout
117
+
118
+ def remaining(end: float, *, operation: DiskResizeOperation | None=None, operation_id: str | None=None, idempotency_key: str | None=None) -> float:
119
+ seconds = end - time.monotonic()
120
+ if seconds <= 0:
121
+ raise DiskResizeTimeout('Disk resize waiting timed out; remote work may still be running', operation=operation, operation_id=operation_id, idempotency_key=idempotency_key)
122
+ return seconds
123
+
124
+ def operation_path(instance_id: str, operation_id: str | None=None) -> str:
125
+ if not isinstance(instance_id, str) or not instance_id:
126
+ raise ValueError('instance_id must be a nonempty string')
127
+ path = f"/instance/{quote(instance_id, safe='')}/disk-resize"
128
+ if operation_id is not None:
129
+ if not isinstance(operation_id, str) or not operation_id:
130
+ raise ValueError('operation_id must be a nonempty string')
131
+ path += f"/{quote(operation_id, safe='')}"
132
+ return path
133
+
134
+ def parse_operation(payload: object, instance_id: str, *, operation_id: str | None=None, idempotency_key: str | None=None, requested_disk_size: int | None=None, previous: DiskResizeOperation | None=None) -> DiskResizeOperation:
135
+ operation = DiskResizeOperation.model_validate(payload)
136
+ if operation.instance_id != instance_id:
137
+ raise ValueError('Disk resize response instance does not match the request')
138
+ if operation_id is not None and operation.id != operation_id:
139
+ raise ValueError('Disk resize response operation does not match the request')
140
+ if requested_disk_size is not None and operation.requested_disk_size != requested_disk_size:
141
+ raise ValueError('Disk resize response target does not match the request')
142
+ if previous is not None and operation.capacity_generation < previous.capacity_generation:
143
+ raise ValueError('Disk resize response capacity generation regressed')
144
+ return operation.model_copy(update={'idempotency_key': idempotency_key})
145
+
146
+ def terminal(operation: DiskResizeOperation) -> bool:
147
+ if operation.status == 'failed':
148
+ raise DiskResizeFailed('Disk resize failed', operation=operation)
149
+ if operation.status == 'recovery_required':
150
+ raise DiskResizeRecoveryRequired('Disk resize requires recovery', operation=operation)
151
+ return operation.status == 'succeeded'
152
+
153
+ def poll_delay(operation: DiskResizeOperation, end: float) -> float:
154
+ return min(max(operation.retry_after_seconds, 0.1), 5, remaining(end, operation=operation))
@@ -11,6 +11,8 @@ from functools import lru_cache
11
11
  import httpx
12
12
  from pydantic import BaseModel, Field, PrivateAttr, model_validator
13
13
  from rich.console import Console
14
+ from morphcloud import _disk_resize
15
+ from morphcloud._disk_resize import DiskResizeError, DiskResizeFailed, DiskResizeOperation, DiskResizeOperationError, DiskResizeRecoveryRequired, DiskResizeRefreshError, DiskResizeRequestError, DiskResizeTimeout
14
16
  from morphcloud._http_transport import build_async_http_transport, build_http_transport
15
17
  from morphcloud._utils import StrEnum
16
18
  from morphcloud.config import resolve_settings
@@ -1354,6 +1356,111 @@ class InstanceAPI(BaseAPI):
1354
1356
  response = await self._client._async_http_client.delete(f'/instance/{instance_id}')
1355
1357
  response.raise_for_status()
1356
1358
 
1359
+ def resize_disk(self, instance_id: str, *, disk_size: int, idempotency_key: str | None=None, wait: bool=True, timeout: float=300) -> DiskResizeOperation:
1360
+ """Set absolute disk capacity in MiB without rebooting an eligible instance.
1361
+
1362
+ Waiting is local: timeout never cancels accepted work. Preserve the
1363
+ returned/exception idempotency key to replay after an uncertain response.
1364
+ Decreases must fit existing usage and required headroom.
1365
+ """
1366
+ end = _disk_resize.deadline(timeout)
1367
+ key = _disk_resize.request_key(disk_size, idempotency_key)
1368
+ path = _disk_resize.operation_path(instance_id)
1369
+ if not isinstance(wait, bool):
1370
+ raise ValueError('wait must be a bool')
1371
+ try:
1372
+ response = self._client._http_client.post(path, json={'disk_size': disk_size}, headers={'Idempotency-Key': key}, timeout=min(30, _disk_resize.remaining(end, idempotency_key=key)))
1373
+ response.raise_for_status()
1374
+ operation = _disk_resize.parse_operation(response.json(), instance_id, idempotency_key=key, requested_disk_size=disk_size)
1375
+ except httpx.TimeoutException as exc:
1376
+ raise DiskResizeTimeout('Disk resize submission timed out; replay with the same key', idempotency_key=key) from exc
1377
+ except (ApiError, httpx.HTTPError, ValueError) as exc:
1378
+ raise DiskResizeRequestError('Disk resize submission failed; preserve the key for safe replay', cause=exc, idempotency_key=key) from exc
1379
+ if _disk_resize.terminal(operation) or not wait:
1380
+ return operation
1381
+ return self._wait_disk_resize(instance_id, operation.id, end, operation)
1382
+
1383
+ async def aresize_disk(self, instance_id: str, *, disk_size: int, idempotency_key: str | None=None, wait: bool=True, timeout: float=300) -> DiskResizeOperation:
1384
+ """Async resize_disk; cancelling the caller does not cancel remote work.
1385
+
1386
+ Supply a persistent key before starting if cancellation must be recoverable
1387
+ across processes, including cancellation before an operation ID is known.
1388
+ """
1389
+ end = _disk_resize.deadline(timeout)
1390
+ key = _disk_resize.request_key(disk_size, idempotency_key)
1391
+ path = _disk_resize.operation_path(instance_id)
1392
+ if not isinstance(wait, bool):
1393
+ raise ValueError('wait must be a bool')
1394
+ try:
1395
+ seconds = _disk_resize.remaining(end, idempotency_key=key)
1396
+ response = await asyncio.wait_for(self._client._async_http_client.post(path, json={'disk_size': disk_size}, headers={'Idempotency-Key': key}, timeout=min(30, seconds)), timeout=seconds)
1397
+ response.raise_for_status()
1398
+ operation = _disk_resize.parse_operation(response.json(), instance_id, idempotency_key=key, requested_disk_size=disk_size)
1399
+ except (httpx.TimeoutException, asyncio.TimeoutError) as exc:
1400
+ raise DiskResizeTimeout('Disk resize submission timed out; replay with the same key', idempotency_key=key) from exc
1401
+ except (ApiError, httpx.HTTPError, ValueError) as exc:
1402
+ raise DiskResizeRequestError('Disk resize submission failed; preserve the key for safe replay', cause=exc, idempotency_key=key) from exc
1403
+ if _disk_resize.terminal(operation) or not wait:
1404
+ return operation
1405
+ return await self._await_disk_resize(instance_id, operation.id, end, operation)
1406
+
1407
+ def get_disk_resize(self, instance_id: str, operation_id: str) -> DiskResizeOperation:
1408
+ """Inspect the exact operation, including failed or recovery-required state."""
1409
+ return self._get_disk_resize(instance_id, operation_id, _disk_resize.deadline(30))
1410
+
1411
+ async def aget_disk_resize(self, instance_id: str, operation_id: str) -> DiskResizeOperation:
1412
+ """Async inspection of the exact operation without changing its state."""
1413
+ return await self._aget_disk_resize(instance_id, operation_id, _disk_resize.deadline(30))
1414
+
1415
+ def _get_disk_resize(self, instance_id: str, operation_id: str, end: float, previous: DiskResizeOperation | None=None) -> DiskResizeOperation:
1416
+ path = _disk_resize.operation_path(instance_id, operation_id)
1417
+ key = previous.idempotency_key if previous else None
1418
+ try:
1419
+ response = self._client._http_client.get(path, timeout=min(30, _disk_resize.remaining(end, operation=previous, operation_id=operation_id)))
1420
+ response.raise_for_status()
1421
+ return _disk_resize.parse_operation(response.json(), instance_id, operation_id=operation_id, idempotency_key=key, requested_disk_size=previous.requested_disk_size if previous else None, previous=previous)
1422
+ except httpx.TimeoutException as exc:
1423
+ raise DiskResizeTimeout('Disk resize observation timed out; remote work was not cancelled', operation=previous, operation_id=operation_id) from exc
1424
+ except (ApiError, httpx.HTTPError, ValueError) as exc:
1425
+ raise DiskResizeRequestError('Disk resize observation failed; inspect this operation again', cause=exc, operation=previous, operation_id=operation_id) from exc
1426
+
1427
+ async def _aget_disk_resize(self, instance_id: str, operation_id: str, end: float, previous: DiskResizeOperation | None=None) -> DiskResizeOperation:
1428
+ path = _disk_resize.operation_path(instance_id, operation_id)
1429
+ key = previous.idempotency_key if previous else None
1430
+ try:
1431
+ seconds = _disk_resize.remaining(end, operation=previous, operation_id=operation_id)
1432
+ response = await asyncio.wait_for(self._client._async_http_client.get(path, timeout=min(30, seconds)), timeout=seconds)
1433
+ response.raise_for_status()
1434
+ return _disk_resize.parse_operation(response.json(), instance_id, operation_id=operation_id, idempotency_key=key, requested_disk_size=previous.requested_disk_size if previous else None, previous=previous)
1435
+ except (httpx.TimeoutException, asyncio.TimeoutError) as exc:
1436
+ raise DiskResizeTimeout('Disk resize observation timed out; remote work was not cancelled', operation=previous, operation_id=operation_id) from exc
1437
+ except (ApiError, httpx.HTTPError, ValueError) as exc:
1438
+ raise DiskResizeRequestError('Disk resize observation failed; inspect this operation again', cause=exc, operation=previous, operation_id=operation_id) from exc
1439
+
1440
+ def wait_disk_resize(self, instance_id: str, operation_id: str, *, timeout: float=300) -> DiskResizeOperation:
1441
+ """Wait for this operation; raise typed terminal errors or a local timeout."""
1442
+ return self._wait_disk_resize(instance_id, operation_id, _disk_resize.deadline(timeout))
1443
+
1444
+ async def await_disk_resize(self, instance_id: str, operation_id: str, *, timeout: float=300) -> DiskResizeOperation:
1445
+ """Async wait; cancellation stops observation without cancelling remote work."""
1446
+ return await self._await_disk_resize(instance_id, operation_id, _disk_resize.deadline(timeout))
1447
+
1448
+ def _wait_disk_resize(self, instance_id: str, operation_id: str, end: float, operation: DiskResizeOperation | None=None) -> DiskResizeOperation:
1449
+ while True:
1450
+ if operation is not None:
1451
+ if _disk_resize.terminal(operation):
1452
+ return operation
1453
+ time.sleep(_disk_resize.poll_delay(operation, end))
1454
+ operation = self._get_disk_resize(instance_id, operation_id, end, operation)
1455
+
1456
+ async def _await_disk_resize(self, instance_id: str, operation_id: str, end: float, operation: DiskResizeOperation | None=None) -> DiskResizeOperation:
1457
+ while True:
1458
+ if operation is not None:
1459
+ if _disk_resize.terminal(operation):
1460
+ return operation
1461
+ await asyncio.sleep(_disk_resize.poll_delay(operation, end))
1462
+ operation = await self._aget_disk_resize(instance_id, operation_id, end, operation)
1463
+
1357
1464
  def reboot(self, instance_id: str, *, vcpus: typing.Optional[int]=None, memory: typing.Optional[int]=None, disk_size: typing.Optional[int]=None, metadata: typing.Optional[typing.Dict[str, str]]=None, ttl_seconds: typing.Optional[int]=None, ttl_action: typing.Union[None, typing.Literal['stop', 'pause']]=None) -> Instance:
1358
1465
  """
1359
1466
  Reboot an instance by its ID, optionally applying a new machine configuration.
@@ -1918,6 +2025,30 @@ class Instance(BaseModel):
1918
2025
  response = await self._api._client._async_http_client.post(f'/instance/{self.id}/snapshot', params=params, json=body)
1919
2026
  return Snapshot.model_validate(response.json())._set_api(self._api._client.snapshots)
1920
2027
 
2028
+ def resize_disk(self, *, disk_size: int, idempotency_key: str | None=None, wait: bool=True, timeout: float=300) -> DiskResizeOperation:
2029
+ """Resize disk capacity; refresh local state only after confirmed success.
2030
+
2031
+ A DiskResizeRefreshError retains the successful operation if the later
2032
+ refresh fails. Waiting timeout does not cancel the remote operation.
2033
+ """
2034
+ operation = self._api.resize_disk(self.id, disk_size=disk_size, idempotency_key=idempotency_key, wait=wait, timeout=timeout)
2035
+ if operation.status == 'succeeded':
2036
+ try:
2037
+ self._refresh()
2038
+ except (ApiError, httpx.HTTPError, ValueError) as exc:
2039
+ raise DiskResizeRefreshError('Disk resize succeeded, but instance refresh failed', operation=operation) from exc
2040
+ return operation
2041
+
2042
+ async def aresize_disk(self, *, disk_size: int, idempotency_key: str | None=None, wait: bool=True, timeout: float=300) -> DiskResizeOperation:
2043
+ """Async resize with success evidence preserved across refresh failures."""
2044
+ operation = await self._api.aresize_disk(self.id, disk_size=disk_size, idempotency_key=idempotency_key, wait=wait, timeout=timeout)
2045
+ if operation.status == 'succeeded':
2046
+ try:
2047
+ await self._refresh_async()
2048
+ except (ApiError, httpx.HTTPError, ValueError) as exc:
2049
+ raise DiskResizeRefreshError('Disk resize succeeded, but instance refresh failed', operation=operation) from exc
2050
+ return operation
2051
+
1921
2052
  def reboot(self, *, vcpus: typing.Optional[int]=None, memory: typing.Optional[int]=None, disk_size: typing.Optional[int]=None, metadata: typing.Optional[typing.Dict[str, str]]=None, ttl_seconds: typing.Optional[int]=None, ttl_action: typing.Union[None, typing.Literal['stop', 'pause']]=None) -> None:
1922
2053
  """
1923
2054
  Reboot the instance, optionally applying a new machine configuration.