morphcloud 0.1.76__tar.gz → 0.1.78__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 (46) hide show
  1. {morphcloud-0.1.76 → morphcloud-0.1.78}/PKG-INFO +1 -1
  2. {morphcloud-0.1.76 → morphcloud-0.1.78}/morphcloud/experimental/browser.py +4 -0
  3. {morphcloud-0.1.76 → morphcloud-0.1.78}/morphcloud/sandbox/__init__.py +1 -1
  4. {morphcloud-0.1.76 → morphcloud-0.1.78}/morphcloud/sandbox/_sandbox.py +43 -0
  5. {morphcloud-0.1.76 → morphcloud-0.1.78}/pyproject.toml +1 -1
  6. morphcloud-0.1.78/tests/integration/test_sandbox.py +315 -0
  7. {morphcloud-0.1.76 → morphcloud-0.1.78}/uv.lock +4 -4
  8. {morphcloud-0.1.76 → morphcloud-0.1.78}/.github/workflows/publish.yaml +0 -0
  9. {morphcloud-0.1.76 → morphcloud-0.1.78}/.gitignore +0 -0
  10. {morphcloud-0.1.76 → morphcloud-0.1.78}/LICENSE +0 -0
  11. {morphcloud-0.1.76 → morphcloud-0.1.78}/Makefile +0 -0
  12. {morphcloud-0.1.76 → morphcloud-0.1.78}/README.md +0 -0
  13. {morphcloud-0.1.76 → morphcloud-0.1.78}/conf.py +0 -0
  14. {morphcloud-0.1.76 → morphcloud-0.1.78}/examples/browser_example.py +0 -0
  15. {morphcloud-0.1.76 → morphcloud-0.1.78}/examples/openai_agents_sdk/main.py +0 -0
  16. {morphcloud-0.1.76 → morphcloud-0.1.78}/examples/pydantic_ai/main.py +0 -0
  17. {morphcloud-0.1.76 → morphcloud-0.1.78}/index.rst +0 -0
  18. {morphcloud-0.1.76 → morphcloud-0.1.78}/make.bat +0 -0
  19. {morphcloud-0.1.76 → morphcloud-0.1.78}/morphcloud/__init__.py +0 -0
  20. {morphcloud-0.1.76 → morphcloud-0.1.78}/morphcloud/_asyncify.py +0 -0
  21. {morphcloud-0.1.76 → morphcloud-0.1.78}/morphcloud/_bash_interpreter.py +0 -0
  22. {morphcloud-0.1.76 → morphcloud-0.1.78}/morphcloud/_llm.py +0 -0
  23. {morphcloud-0.1.76 → morphcloud-0.1.78}/morphcloud/_scramble.py +0 -0
  24. {morphcloud-0.1.76 → morphcloud-0.1.78}/morphcloud/_ssh.py +0 -0
  25. {morphcloud-0.1.76 → morphcloud-0.1.78}/morphcloud/_utils.py +0 -0
  26. {morphcloud-0.1.76 → morphcloud-0.1.78}/morphcloud/api.py +0 -0
  27. {morphcloud-0.1.76 → morphcloud-0.1.78}/morphcloud/cli.py +0 -0
  28. {morphcloud-0.1.76 → morphcloud-0.1.78}/morphcloud/computer/__init__.py +0 -0
  29. {morphcloud-0.1.76 → morphcloud-0.1.78}/morphcloud/computer/_computer.py +0 -0
  30. {morphcloud-0.1.76 → morphcloud-0.1.78}/morphcloud/experimental/__init__.py +0 -0
  31. {morphcloud-0.1.76 → morphcloud-0.1.78}/morphcloud/py.typed +0 -0
  32. {morphcloud-0.1.76 → morphcloud-0.1.78}/requirements.txt +0 -0
  33. {morphcloud-0.1.76 → morphcloud-0.1.78}/scripts/increment_version.py +0 -0
  34. {morphcloud-0.1.76 → morphcloud-0.1.78}/tests/integration/__init__.py +0 -0
  35. {morphcloud-0.1.76 → morphcloud-0.1.78}/tests/integration/pytest.ini +0 -0
  36. {morphcloud-0.1.76 → morphcloud-0.1.78}/tests/integration/test_as_container.py +0 -0
  37. {morphcloud-0.1.76 → morphcloud-0.1.78}/tests/integration/test_branching.py +0 -0
  38. {morphcloud-0.1.76 → morphcloud-0.1.78}/tests/integration/test_command_execution.py +0 -0
  39. {morphcloud-0.1.76 → morphcloud-0.1.78}/tests/integration/test_file_operations.py +0 -0
  40. {morphcloud-0.1.76 → morphcloud-0.1.78}/tests/integration/test_from_tag_multiple.py +0 -0
  41. {morphcloud-0.1.76 → morphcloud-0.1.78}/tests/integration/test_http_service.py +0 -0
  42. {morphcloud-0.1.76 → morphcloud-0.1.78}/tests/integration/test_metadata.py +0 -0
  43. {morphcloud-0.1.76 → morphcloud-0.1.78}/tests/integration/test_simple.py +0 -0
  44. {morphcloud-0.1.76 → morphcloud-0.1.78}/tests/integration/test_snapshot_operations.py +0 -0
  45. {morphcloud-0.1.76 → morphcloud-0.1.78}/tests/integration/test_ssh_key_rotation.py +0 -0
  46. {morphcloud-0.1.76 → morphcloud-0.1.78}/tests/integration/test_ttl.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: morphcloud
3
- Version: 0.1.76
3
+ Version: 0.1.78
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
@@ -513,6 +513,7 @@ class BrowserSession:
513
513
 
514
514
  # Start instance (don't use context manager to keep it running)
515
515
  instance = snapshot.start(metadata={"name": name}, ttl_seconds=ttl_seconds)
516
+ instance.wait_until_ready()
516
517
 
517
518
  # Verify Chrome installation
518
519
  if verbose:
@@ -670,6 +671,9 @@ class BrowserSession:
670
671
  return session
671
672
 
672
673
  except Exception as e:
674
+ import traceback
675
+
676
+ print(traceback.format_exc(e))
673
677
  raise RuntimeError(f"Failed to create browser session: {e}")
674
678
 
675
679
 
@@ -1 +1 @@
1
- from ._sandbox import ExecutionResult, LanguageSupport, OutputType, Sandbox
1
+ from ._sandbox import ExecutionResult, LanguageSupport, OutputType, Sandbox, SandboxAPI
@@ -266,9 +266,52 @@ class Sandbox:
266
266
  self._ws_connections = ws_connections
267
267
  self._session_id = session_id
268
268
 
269
+ def _discover_existing_kernels(self) -> None:
270
+ """
271
+ Discover and reconnect to existing kernels on the Jupyter server.
272
+ This preserves kernel state when getting an existing sandbox instance.
273
+ """
274
+ try:
275
+ # Get list of existing kernels from Jupyter server
276
+ response = requests.get(f"{self.jupyter_url}/api/kernels", timeout=10.0)
277
+ response.raise_for_status()
278
+ existing_kernels = response.json()
279
+
280
+ # Map kernel specs to our supported languages
281
+ kernel_to_language = {}
282
+ for language in LanguageSupport.get_supported_languages():
283
+ kernel_name = LanguageSupport.get_kernel_name(language)
284
+ kernel_to_language[kernel_name] = language
285
+
286
+ # Connect to existing kernels that match our supported languages
287
+ for kernel_info in existing_kernels:
288
+ kernel_id = kernel_info.get("id")
289
+ kernel_spec = kernel_info.get("name")
290
+
291
+ if kernel_spec in kernel_to_language:
292
+ language = kernel_to_language[kernel_spec]
293
+ # Only connect if we don't already have a kernel for this language
294
+ if language not in self._kernel_ids:
295
+ self._kernel_ids[language] = kernel_id
296
+ # Connect WebSocket to existing kernel
297
+ try:
298
+ self._connect_websocket(kernel_id)
299
+ except ConnectionError:
300
+ # If we can't connect, remove from our tracking
301
+ del self._kernel_ids[language]
302
+
303
+ except requests.RequestException:
304
+ # If we can't discover existing kernels, that's okay
305
+ # New kernels will be created as needed
306
+ pass
307
+ except Exception:
308
+ # Any other error during discovery should not prevent connection
309
+ pass
310
+
269
311
  def connect(self, timeout_seconds: int = 60) -> Sandbox:
270
312
  """Ensure Jupyter service is running and accessible"""
271
313
  self.wait_for_jupyter(timeout_seconds)
314
+ self._discover_existing_kernels()
272
315
  return self
273
316
 
274
317
  def wait_for_jupyter(self, timeout: int = 60) -> bool:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "morphcloud"
3
- version = "0.1.76"
3
+ version = "0.1.78"
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"}
@@ -0,0 +1,315 @@
1
+ """
2
+ Function-scoped tests for Sandbox functionality in MorphCloud SDK.
3
+ """
4
+ import pytest
5
+ import logging
6
+ import uuid
7
+ import os
8
+ import pytest_asyncio
9
+
10
+ from morphcloud.api import MorphCloudClient
11
+ from morphcloud.sandbox import Sandbox, SandboxAPI
12
+
13
+ logger = logging.getLogger("morph-tests")
14
+
15
+ # Mark all tests as asyncio tests
16
+ pytestmark = pytest.mark.asyncio
17
+
18
+ # Configure pytest-asyncio
19
+ def pytest_configure(config):
20
+ config.option.asyncio_default_fixture_loop_scope = "function"
21
+
22
+
23
+ @pytest.fixture
24
+ def api_key():
25
+ """Get API key from environment variable."""
26
+ key = os.environ.get("MORPH_API_KEY")
27
+ if not key:
28
+ pytest.fail("MORPH_API_KEY environment variable must be set")
29
+ return key
30
+
31
+
32
+ @pytest.fixture
33
+ def base_url():
34
+ """Get base URL from environment variable."""
35
+ return os.environ.get("MORPH_BASE_URL")
36
+
37
+
38
+ @pytest_asyncio.fixture
39
+ async def client(api_key, base_url):
40
+ """Create a MorphCloudClient."""
41
+ client = MorphCloudClient(api_key=api_key, base_url=base_url)
42
+ logger.info("Created MorphCloud client")
43
+ return client
44
+
45
+
46
+ @pytest_asyncio.fixture
47
+ async def sandbox_api(client):
48
+ """Create a SandboxAPI instance."""
49
+ api = SandboxAPI(client)
50
+ logger.info("Created SandboxAPI")
51
+ return api
52
+
53
+
54
+ async def test_sandbox_creation_and_connection(client):
55
+ """Test basic sandbox creation and connection."""
56
+ logger.info("Testing sandbox creation and connection")
57
+
58
+ try:
59
+ # Create a new sandbox
60
+ sandbox = Sandbox.new(client=client, ttl_seconds=600)
61
+ logger.info(f"Created sandbox: {sandbox._instance.id}")
62
+
63
+ # Connect to the sandbox
64
+ sandbox.connect()
65
+ logger.info("Connected to sandbox successfully")
66
+
67
+ # Verify sandbox properties
68
+ assert sandbox._instance.id.startswith("morphvm_"), "Sandbox ID should start with 'morphvm_'"
69
+ assert sandbox.jupyter_url is not None, "Sandbox should have a Jupyter URL"
70
+ assert isinstance(sandbox._kernel_ids, dict), "Sandbox should have kernel_ids dictionary"
71
+
72
+ logger.info("Sandbox creation and connection test passed")
73
+
74
+ finally:
75
+ # Clean up
76
+ if 'sandbox' in locals():
77
+ try:
78
+ logger.info(f"Cleaning up sandbox {sandbox._instance.id}")
79
+ sandbox.close()
80
+ sandbox.shutdown()
81
+ logger.info("Sandbox cleaned up successfully")
82
+ except Exception as e:
83
+ logger.error(f"Error cleaning up sandbox: {e}")
84
+
85
+
86
+ async def test_sandbox_code_execution(client):
87
+ """Test code execution in sandbox."""
88
+ logger.info("Testing sandbox code execution")
89
+
90
+ try:
91
+ # Create and connect to sandbox
92
+ sandbox = Sandbox.new(client=client, ttl_seconds=600)
93
+ sandbox.connect()
94
+ logger.info(f"Created and connected to sandbox: {sandbox._instance.id}")
95
+
96
+ # Test Python code execution
97
+ test_value = uuid.uuid4().hex[:8]
98
+ result = sandbox.run_code(f"test_var = '{test_value}'", language="python")
99
+ assert result.success, f"Python code execution failed: {result.error}"
100
+ logger.info("Python variable assignment successful")
101
+
102
+ # Verify the variable was set
103
+ result = sandbox.run_code("print(test_var)", language="python")
104
+ assert result.success, f"Python variable retrieval failed: {result.error}"
105
+ assert test_value in result.text, f"Expected '{test_value}' in output, got: {result.text}"
106
+ logger.info("Python variable retrieval successful")
107
+
108
+ # Test JavaScript code execution
109
+ result = sandbox.run_code("console.log('hello from js');", language="javascript")
110
+ assert result.success, f"JavaScript code execution failed: {result.error}"
111
+ assert "hello from js" in result.text, f"Expected 'hello from js' in output, got: {result.text}"
112
+ logger.info("JavaScript code execution successful")
113
+
114
+ logger.info("Sandbox code execution test passed")
115
+
116
+ finally:
117
+ if 'sandbox' in locals():
118
+ try:
119
+ sandbox.close()
120
+ sandbox.shutdown()
121
+ except Exception as e:
122
+ logger.error(f"Error cleaning up sandbox: {e}")
123
+
124
+
125
+ async def test_kernel_persistence_across_get_calls(client, sandbox_api):
126
+ """Test that kernel state persists when using .get() to retrieve a sandbox."""
127
+ logger.info("Testing kernel persistence across .get() calls")
128
+
129
+ try:
130
+ # Create and connect to first sandbox instance
131
+ sandbox1 = Sandbox.new(client=client, ttl_seconds=600)
132
+ sandbox1.connect()
133
+ logger.info(f"Created sandbox1: {sandbox1._instance.id}")
134
+
135
+ # Set a variable in Python
136
+ test_value = f"kernel_test_{uuid.uuid4().hex[:8]}"
137
+ result1 = sandbox1.run_code(f"persistent_var = '{test_value}'", language="python")
138
+ assert result1.success, f"Failed to set variable: {result1.error}"
139
+ logger.info(f"Set persistent_var to '{test_value}'")
140
+
141
+ # Get the kernel ID for verification
142
+ original_kernel_id = sandbox1._kernel_ids.get("python")
143
+ assert original_kernel_id is not None, "No Python kernel ID found"
144
+ logger.info(f"Original Python kernel ID: {original_kernel_id}")
145
+
146
+ # Retrieve the same sandbox using .get()
147
+ sandbox2 = sandbox_api.get(sandbox1._instance.id)
148
+ sandbox2.connect()
149
+ logger.info(f"Retrieved sandbox2 via .get(): {sandbox2._instance.id}")
150
+
151
+ # Verify kernel ID is preserved
152
+ retrieved_kernel_id = sandbox2._kernel_ids.get("python")
153
+ assert retrieved_kernel_id == original_kernel_id, (
154
+ f"Kernel IDs don't match: original={original_kernel_id}, "
155
+ f"retrieved={retrieved_kernel_id}"
156
+ )
157
+ logger.info("Kernel ID preservation verified")
158
+
159
+ # Verify variable state is preserved
160
+ result2 = sandbox2.run_code("print(persistent_var)", language="python")
161
+ assert result2.success, f"Failed to access persistent variable: {result2.error}"
162
+ assert test_value in result2.text, (
163
+ f"Persistent variable not found. Expected '{test_value}' in output: {result2.text}"
164
+ )
165
+ logger.info("Variable state preservation verified")
166
+
167
+ logger.info("Kernel persistence test passed")
168
+
169
+ finally:
170
+ # Clean up
171
+ for i, sandbox in enumerate([s for s in [locals().get('sandbox1'), locals().get('sandbox2')] if s]):
172
+ try:
173
+ logger.info(f"Cleaning up sandbox{i+1}: {sandbox._instance.id}")
174
+ sandbox.close()
175
+ if i == 0: # Only shutdown once
176
+ sandbox.shutdown()
177
+ except Exception as e:
178
+ logger.error(f"Error cleaning up sandbox{i+1}: {e}")
179
+
180
+
181
+ async def test_multiple_language_kernel_persistence(client, sandbox_api):
182
+ """Test kernel persistence works for multiple programming languages."""
183
+ logger.info("Testing multiple language kernel persistence")
184
+
185
+ try:
186
+ # Create and connect to sandbox
187
+ sandbox1 = Sandbox.new(client=client, ttl_seconds=600)
188
+ sandbox1.connect()
189
+ logger.info(f"Created sandbox1: {sandbox1._instance.id}")
190
+
191
+ # Set variables in different languages
192
+ python_value = f"py_{uuid.uuid4().hex[:8]}"
193
+ js_value = f"js_{uuid.uuid4().hex[:8]}"
194
+
195
+ python_result = sandbox1.run_code(f"py_var = '{python_value}'", language="python")
196
+ assert python_result.success, f"Python execution failed: {python_result.error}"
197
+
198
+ js_result = sandbox1.run_code(f"var js_var = '{js_value}';", language="javascript")
199
+ assert js_result.success, f"JavaScript execution failed: {js_result.error}"
200
+
201
+ logger.info(f"Set variables - Python: {python_value}, JavaScript: {js_value}")
202
+
203
+ # Store original kernel IDs
204
+ original_kernels = sandbox1._kernel_ids.copy()
205
+ logger.info(f"Original kernel IDs: {original_kernels}")
206
+
207
+ # Retrieve sandbox and verify all kernels are preserved
208
+ sandbox2 = sandbox_api.get(sandbox1._instance.id)
209
+ sandbox2.connect()
210
+ logger.info(f"Retrieved sandbox2: {sandbox2._instance.id}")
211
+ logger.info(f"Retrieved kernel IDs: {sandbox2._kernel_ids}")
212
+
213
+ # Check that kernel IDs match for all languages
214
+ for language, kernel_id in original_kernels.items():
215
+ retrieved_kernel_id = sandbox2._kernel_ids.get(language)
216
+ assert retrieved_kernel_id == kernel_id, (
217
+ f"Kernel ID mismatch for {language}: "
218
+ f"original={kernel_id}, retrieved={retrieved_kernel_id}"
219
+ )
220
+
221
+ # Verify variables are accessible
222
+ py_check = sandbox2.run_code("print(py_var)", language="python")
223
+ assert py_check.success and python_value in py_check.text, (
224
+ f"Python variable not preserved: {py_check.text}"
225
+ )
226
+
227
+ js_check = sandbox2.run_code("console.log(js_var);", language="javascript")
228
+ assert js_check.success and js_value in js_check.text, (
229
+ f"JavaScript variable not preserved: {js_check.text}"
230
+ )
231
+
232
+ logger.info("Multiple language kernel persistence test passed")
233
+
234
+ finally:
235
+ # Clean up
236
+ for i, sandbox in enumerate([s for s in [locals().get('sandbox1'), locals().get('sandbox2')] if s]):
237
+ try:
238
+ sandbox.close()
239
+ if i == 0: # Only shutdown once
240
+ sandbox.shutdown()
241
+ except Exception as e:
242
+ logger.error(f"Error cleaning up sandbox{i+1}: {e}")
243
+
244
+
245
+ async def test_kernel_discovery_with_fresh_sandbox(client, sandbox_api):
246
+ """Test that kernel discovery handles fresh sandboxes correctly."""
247
+ logger.info("Testing kernel discovery with fresh sandbox")
248
+
249
+ try:
250
+ # Create sandbox but don't connect yet
251
+ sandbox1 = Sandbox.new(client=client, ttl_seconds=600)
252
+ logger.info(f"Created fresh sandbox: {sandbox1._instance.id}")
253
+
254
+ # Get the sandbox without any prior kernel creation
255
+ sandbox2 = sandbox_api.get(sandbox1._instance.id)
256
+ sandbox2.connect()
257
+ logger.info("Connected to fresh sandbox via .get()")
258
+
259
+ # Should be able to run code (will create new kernel)
260
+ test_value = f"fresh_{uuid.uuid4().hex[:8]}"
261
+ result = sandbox2.run_code(f"print('{test_value}')", language="python")
262
+ assert result.success, f"Failed to run code on fresh sandbox: {result.error}"
263
+ assert test_value in result.text, f"Expected '{test_value}' in output: {result.text}"
264
+
265
+ logger.info("Fresh sandbox kernel discovery test passed")
266
+
267
+ finally:
268
+ # Clean up
269
+ for sandbox in [locals().get('sandbox1'), locals().get('sandbox2')]:
270
+ if sandbox:
271
+ try:
272
+ sandbox.close()
273
+ sandbox.shutdown()
274
+ break # Only shutdown once
275
+ except Exception as e:
276
+ logger.error(f"Error cleaning up sandbox: {e}")
277
+
278
+
279
+ async def test_sandbox_error_handling(client):
280
+ """Test error handling in sandbox operations."""
281
+ logger.info("Testing sandbox error handling")
282
+
283
+ try:
284
+ # Create and connect to sandbox
285
+ sandbox = Sandbox.new(client=client, ttl_seconds=600)
286
+ sandbox.connect()
287
+ logger.info(f"Created sandbox: {sandbox._instance.id}")
288
+
289
+ # Test code with syntax error
290
+ result = sandbox.run_code("print('missing quote)", language="python")
291
+ assert not result.success, "Code with syntax error should fail"
292
+ assert result.error is not None, "Failed code should have error message"
293
+ logger.info("Syntax error handling verified")
294
+
295
+ # Test unsupported language
296
+ result = sandbox.run_code("print('test')", language="unsupported")
297
+ assert not result.success, "Unsupported language should fail"
298
+ assert "Unsupported language" in result.error, f"Expected unsupported language error: {result.error}"
299
+ logger.info("Unsupported language handling verified")
300
+
301
+ # Verify sandbox is still functional after errors
302
+ result = sandbox.run_code("print('still works')", language="python")
303
+ assert result.success, "Sandbox should remain functional after errors"
304
+ assert "still works" in result.text, f"Expected 'still works' in output: {result.text}"
305
+ logger.info("Sandbox resilience after errors verified")
306
+
307
+ logger.info("Sandbox error handling test passed")
308
+
309
+ finally:
310
+ if 'sandbox' in locals():
311
+ try:
312
+ sandbox.close()
313
+ sandbox.shutdown()
314
+ except Exception as e:
315
+ logger.error(f"Error cleaning up sandbox: {e}")
@@ -113,11 +113,11 @@ wheels = [
113
113
 
114
114
  [[package]]
115
115
  name = "certifi"
116
- version = "2025.6.15"
116
+ version = "2025.7.9"
117
117
  source = { registry = "https://pypi.org/simple" }
118
- sdist = { url = "https://files.pythonhosted.org/packages/73/f7/f14b46d4bcd21092d7d3ccef689615220d8a08fb25e564b65d20738e672e/certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b", size = 158753, upload-time = "2025-06-15T02:45:51.329Z" }
118
+ sdist = { url = "https://files.pythonhosted.org/packages/de/8a/c729b6b60c66a38f590c4e774decc4b2ec7b0576be8f1aa984a53ffa812a/certifi-2025.7.9.tar.gz", hash = "sha256:c1d2ec05395148ee10cf672ffc28cd37ea0ab0d99f9cc74c43e588cbd111b079", size = 160386, upload-time = "2025-07-09T02:13:58.874Z" }
119
119
  wheels = [
120
- { url = "https://files.pythonhosted.org/packages/84/ae/320161bd181fc06471eed047ecce67b693fd7515b16d495d8932db763426/certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057", size = 157650, upload-time = "2025-06-15T02:45:49.977Z" },
120
+ { url = "https://files.pythonhosted.org/packages/66/f3/80a3f974c8b535d394ff960a11ac20368e06b736da395b551a49ce950cce/certifi-2025.7.9-py3-none-any.whl", hash = "sha256:d842783a14f8fdd646895ac26f719a061408834473cfc10203f6a575beb15d39", size = 159230, upload-time = "2025-07-09T02:13:57.007Z" },
121
121
  ]
122
122
 
123
123
  [[package]]
@@ -576,7 +576,7 @@ wheels = [
576
576
 
577
577
  [[package]]
578
578
  name = "morphcloud"
579
- version = "0.1.76"
579
+ version = "0.1.78"
580
580
  source = { editable = "." }
581
581
  dependencies = [
582
582
  { name = "anthropic" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes