proximl 0.5.16__py3-none-any.whl → 1.0.0__py3-none-any.whl

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 (56) hide show
  1. examples/local_storage.py +0 -2
  2. proximl/__init__.py +1 -1
  3. proximl/checkpoints.py +56 -57
  4. proximl/cli/__init__.py +6 -3
  5. proximl/cli/checkpoint.py +18 -57
  6. proximl/cli/dataset.py +17 -57
  7. proximl/cli/job/__init__.py +11 -53
  8. proximl/cli/job/create.py +51 -24
  9. proximl/cli/model.py +14 -56
  10. proximl/cli/volume.py +18 -57
  11. proximl/datasets.py +50 -55
  12. proximl/jobs.py +239 -68
  13. proximl/models.py +51 -55
  14. proximl/projects/projects.py +2 -2
  15. proximl/proximl.py +50 -16
  16. proximl/utils/__init__.py +1 -0
  17. proximl/{auth.py → utils/auth.py} +4 -3
  18. proximl/utils/transfer.py +587 -0
  19. proximl/volumes.py +48 -53
  20. {proximl-0.5.16.dist-info → proximl-1.0.0.dist-info}/METADATA +3 -3
  21. {proximl-0.5.16.dist-info → proximl-1.0.0.dist-info}/RECORD +53 -51
  22. tests/integration/test_checkpoints_integration.py +4 -3
  23. tests/integration/test_datasets_integration.py +5 -3
  24. tests/integration/test_jobs_integration.py +33 -27
  25. tests/integration/test_models_integration.py +7 -3
  26. tests/integration/test_volumes_integration.py +2 -2
  27. tests/unit/cli/test_cli_checkpoint_unit.py +312 -1
  28. tests/unit/cloudbender/test_nodes_unit.py +112 -0
  29. tests/unit/cloudbender/test_providers_unit.py +96 -0
  30. tests/unit/cloudbender/test_regions_unit.py +106 -0
  31. tests/unit/cloudbender/test_services_unit.py +141 -0
  32. tests/unit/conftest.py +23 -10
  33. tests/unit/projects/test_project_data_connectors_unit.py +39 -0
  34. tests/unit/projects/test_project_datastores_unit.py +37 -0
  35. tests/unit/projects/test_project_members_unit.py +46 -0
  36. tests/unit/projects/test_project_services_unit.py +65 -0
  37. tests/unit/projects/test_projects_unit.py +17 -1
  38. tests/unit/test_auth_unit.py +17 -2
  39. tests/unit/test_checkpoints_unit.py +256 -71
  40. tests/unit/test_datasets_unit.py +218 -68
  41. tests/unit/test_exceptions.py +133 -0
  42. tests/unit/test_gpu_types_unit.py +11 -1
  43. tests/unit/test_jobs_unit.py +1014 -95
  44. tests/unit/test_main_unit.py +20 -0
  45. tests/unit/test_models_unit.py +218 -70
  46. tests/unit/test_proximl_unit.py +627 -3
  47. tests/unit/test_volumes_unit.py +211 -70
  48. tests/unit/utils/__init__.py +1 -0
  49. tests/unit/utils/test_transfer_unit.py +4260 -0
  50. proximl/cli/connection.py +0 -61
  51. proximl/connections.py +0 -621
  52. tests/unit/test_connections_unit.py +0 -182
  53. {proximl-0.5.16.dist-info → proximl-1.0.0.dist-info}/LICENSE +0 -0
  54. {proximl-0.5.16.dist-info → proximl-1.0.0.dist-info}/WHEEL +0 -0
  55. {proximl-0.5.16.dist-info → proximl-1.0.0.dist-info}/entry_points.txt +0 -0
  56. {proximl-0.5.16.dist-info → proximl-1.0.0.dist-info}/top_level.txt +0 -0
@@ -1,15 +1,24 @@
1
1
  import re
2
2
  import json
3
3
  import click
4
- from unittest.mock import AsyncMock, patch
4
+ from unittest.mock import AsyncMock, patch, Mock
5
5
  from pytest import mark, fixture, raises
6
6
 
7
7
  pytestmark = [mark.cli, mark.unit, mark.checkpoints]
8
8
 
9
9
  from proximl.cli import checkpoint as specimen
10
+ from proximl.cli.checkpoint import pretty_size
10
11
  from proximl.checkpoints import Checkpoint
11
12
 
12
13
 
14
+ def test_pretty_size_zero():
15
+ """Test pretty_size with zero/None (line 7)."""
16
+ result = pretty_size(None)
17
+ assert result == "0.00 B"
18
+ result = pretty_size(0)
19
+ assert result == "0.00 B"
20
+
21
+
13
22
  def test_list(runner, mock_my_checkpoints):
14
23
  with patch("proximl.cli.ProxiML", new=AsyncMock) as mock_proximl:
15
24
  mock_proximl.checkpoints = AsyncMock()
@@ -20,3 +29,305 @@ def test_list(runner, mock_my_checkpoints):
20
29
  print(result)
21
30
  assert result.exit_code == 0
22
31
  mock_proximl.checkpoints.list.assert_called_once()
32
+
33
+
34
+ def test_attach_success(runner, mock_my_checkpoints):
35
+ """Test attach command success (lines 32-38)."""
36
+ with patch("proximl.cli.ProxiML", new=AsyncMock) as mock_proximl:
37
+
38
+ async def list_async():
39
+ return mock_my_checkpoints
40
+
41
+ mock_proximl.checkpoints = AsyncMock()
42
+ mock_proximl.checkpoints.list = Mock(side_effect=lambda: list_async())
43
+
44
+ # Use the first checkpoint from the list
45
+ checkpoint = mock_my_checkpoints[0]
46
+
47
+ async def attach_async():
48
+ return None
49
+
50
+ checkpoint.attach = Mock(return_value=attach_async())
51
+
52
+ with patch("proximl.cli.search_by_id_name", return_value=checkpoint):
53
+ result = runner.invoke(specimen, ["attach", "1"])
54
+ assert result.exit_code == 0
55
+ checkpoint.attach.assert_called_once()
56
+
57
+
58
+ def test_attach_not_found(runner, mock_my_checkpoints):
59
+ """Test attach command when checkpoint not found (line 36)."""
60
+ with patch("proximl.cli.ProxiML", new=AsyncMock) as mock_proximl:
61
+
62
+ async def list_async():
63
+ return mock_my_checkpoints
64
+
65
+ mock_proximl.checkpoints = AsyncMock()
66
+ mock_proximl.checkpoints.list = Mock(side_effect=lambda: list_async())
67
+
68
+ with patch("proximl.cli.search_by_id_name", return_value=None):
69
+ result = runner.invoke(specimen, ["attach", "nonexistent"])
70
+ assert result.exit_code != 0
71
+ assert "Cannot find specified checkpoint" in result.output
72
+
73
+
74
+ def test_connect_with_attach(runner, mock_my_checkpoints):
75
+ """Test connect command with attach (lines 56-65, attach=True)."""
76
+ with patch("proximl.cli.ProxiML", new=AsyncMock) as mock_proximl:
77
+
78
+ async def list_async():
79
+ return mock_my_checkpoints
80
+
81
+ mock_proximl.checkpoints = AsyncMock()
82
+ mock_proximl.checkpoints.list = Mock(side_effect=lambda: list_async())
83
+
84
+ checkpoint = mock_my_checkpoints[0]
85
+
86
+ async def connect_async():
87
+ return None
88
+
89
+ async def attach_async():
90
+ return None
91
+
92
+ checkpoint.connect = Mock(return_value=connect_async())
93
+ checkpoint.attach = Mock(return_value=attach_async())
94
+
95
+ with patch("proximl.cli.search_by_id_name", return_value=checkpoint):
96
+ result = runner.invoke(specimen, ["connect", "1"])
97
+ assert result.exit_code == 0
98
+ checkpoint.connect.assert_called_once()
99
+ checkpoint.attach.assert_called_once()
100
+
101
+
102
+ def test_connect_no_attach(runner, mock_my_checkpoints):
103
+ """Test connect command without attach (lines 56-65, attach=False)."""
104
+ with patch("proximl.cli.ProxiML", new=AsyncMock) as mock_proximl:
105
+
106
+ async def list_async():
107
+ return mock_my_checkpoints
108
+
109
+ mock_proximl.checkpoints = AsyncMock()
110
+ mock_proximl.checkpoints.list = Mock(side_effect=lambda: list_async())
111
+
112
+ checkpoint = mock_my_checkpoints[0]
113
+
114
+ async def connect_async():
115
+ return None
116
+
117
+ checkpoint.connect = Mock(return_value=connect_async())
118
+
119
+ with patch("proximl.cli.search_by_id_name", return_value=checkpoint):
120
+ result = runner.invoke(specimen, ["connect", "--no-attach", "1"])
121
+ assert result.exit_code == 0
122
+ checkpoint.connect.assert_called_once()
123
+
124
+
125
+ def test_connect_not_found(runner, mock_my_checkpoints):
126
+ """Test connect command when checkpoint not found (line 60)."""
127
+ with patch("proximl.cli.ProxiML", new=AsyncMock) as mock_proximl:
128
+
129
+ async def list_async():
130
+ return mock_my_checkpoints
131
+
132
+ mock_proximl.checkpoints = AsyncMock()
133
+ mock_proximl.checkpoints.list = Mock(side_effect=lambda: list_async())
134
+
135
+ with patch("proximl.cli.search_by_id_name", return_value=None):
136
+ result = runner.invoke(specimen, ["connect", "nonexistent"])
137
+ assert result.exit_code != 0
138
+ assert "Cannot find specified checkpoint" in result.output
139
+
140
+
141
+ def test_create_with_connect_and_attach(runner, tmp_path, mock_my_checkpoints):
142
+ """Test create command with connect and attach (lines 103-115)."""
143
+ with patch("proximl.cli.ProxiML", new=AsyncMock) as mock_proximl:
144
+ checkpoint = mock_my_checkpoints[0]
145
+
146
+ async def connect_async():
147
+ return None
148
+
149
+ async def attach_async():
150
+ return None
151
+
152
+ checkpoint.connect = Mock(return_value=connect_async())
153
+ checkpoint.attach = Mock(return_value=attach_async())
154
+
155
+ async def create_async(**kwargs):
156
+ return checkpoint
157
+
158
+ mock_proximl.checkpoints = AsyncMock()
159
+ mock_proximl.checkpoints.create = Mock(
160
+ side_effect=lambda **kwargs: create_async(**kwargs)
161
+ )
162
+
163
+ test_dir = tmp_path / "test_checkpoint"
164
+ test_dir.mkdir()
165
+ result = runner.invoke(
166
+ specimen, ["create", "test-checkpoint", str(test_dir)]
167
+ )
168
+ assert result.exit_code == 0
169
+ mock_proximl.checkpoints.create.assert_called_once()
170
+ checkpoint.connect.assert_called_once()
171
+ checkpoint.attach.assert_called_once()
172
+
173
+
174
+ def test_create_with_connect_no_attach(runner, tmp_path, mock_my_checkpoints):
175
+ """Test create command with connect but no attach (lines 103-115)."""
176
+ with patch("proximl.cli.ProxiML", new=AsyncMock) as mock_proximl:
177
+ checkpoint = mock_my_checkpoints[0]
178
+
179
+ async def connect_async():
180
+ return None
181
+
182
+ checkpoint.connect = Mock(return_value=connect_async())
183
+
184
+ async def create_async(**kwargs):
185
+ return checkpoint
186
+
187
+ mock_proximl.checkpoints = AsyncMock()
188
+ mock_proximl.checkpoints.create = Mock(
189
+ side_effect=lambda **kwargs: create_async(**kwargs)
190
+ )
191
+
192
+ test_dir = tmp_path / "test_checkpoint"
193
+ test_dir.mkdir()
194
+ result = runner.invoke(
195
+ specimen,
196
+ ["create", "--no-attach", "test-checkpoint", str(test_dir)],
197
+ )
198
+ assert result.exit_code == 0
199
+ checkpoint.connect.assert_called_once()
200
+
201
+
202
+ def test_create_no_connect(runner, tmp_path):
203
+ """Test create command without connect (lines 103-115, line 115)."""
204
+ mock_checkpoint = Mock(spec=Checkpoint)
205
+
206
+ mock_proximl_runner = Mock()
207
+ mock_proximl_runner.client = Mock()
208
+ mock_proximl_runner.client.checkpoints = Mock()
209
+ mock_proximl_runner.client.checkpoints.create = AsyncMock(
210
+ return_value=mock_checkpoint
211
+ )
212
+ mock_proximl_runner.run = Mock(
213
+ side_effect=lambda x: x if not hasattr(x, "__call__") else x()
214
+ )
215
+
216
+ with patch("proximl.cli.ProxiMLRunner", return_value=mock_proximl_runner):
217
+ test_dir = tmp_path / "test_checkpoint"
218
+ test_dir.mkdir()
219
+ result = runner.invoke(
220
+ specimen,
221
+ ["create", "--no-connect", "test-checkpoint", str(test_dir)],
222
+ )
223
+ assert result.exit_code != 0
224
+ assert "No logs to show" in result.output
225
+
226
+
227
+ def test_list_public(runner, mock_my_checkpoints):
228
+ """Test list_public command (lines 152-171)."""
229
+ with patch("proximl.cli.ProxiML", new=AsyncMock) as mock_proximl:
230
+ mock_proximl.checkpoints = AsyncMock()
231
+ mock_proximl.checkpoints.list_public = AsyncMock(
232
+ return_value=mock_my_checkpoints
233
+ )
234
+
235
+ result = runner.invoke(specimen, ["list-public"])
236
+ assert result.exit_code == 0
237
+ mock_proximl.checkpoints.list_public.assert_called_once()
238
+
239
+
240
+ def test_remove_success(runner, mock_my_checkpoints):
241
+ """Test remove command success (lines 192-201)."""
242
+ with patch("proximl.cli.ProxiML", new=AsyncMock) as mock_proximl:
243
+
244
+ async def list_async():
245
+ return mock_my_checkpoints
246
+
247
+ mock_proximl.checkpoints = AsyncMock()
248
+ mock_proximl.checkpoints.list = Mock(side_effect=lambda: list_async())
249
+
250
+ checkpoint = mock_my_checkpoints[0]
251
+
252
+ async def remove_async():
253
+ return None
254
+
255
+ checkpoint.remove = Mock(return_value=remove_async())
256
+
257
+ with patch("proximl.cli.search_by_id_name", return_value=checkpoint):
258
+ result = runner.invoke(specimen, ["remove", "1"])
259
+ assert result.exit_code == 0
260
+ checkpoint.remove.assert_called_once_with(force=False)
261
+
262
+
263
+ def test_remove_not_found(runner, mock_my_checkpoints):
264
+ """Test remove command when checkpoint not found (lines 192-201)."""
265
+ with patch("proximl.cli.ProxiML", new=AsyncMock) as mock_proximl:
266
+
267
+ async def list_async():
268
+ return mock_my_checkpoints
269
+
270
+ mock_proximl.checkpoints = AsyncMock()
271
+ mock_proximl.checkpoints.list = Mock(side_effect=lambda: list_async())
272
+
273
+ with patch("proximl.cli.search_by_id_name", return_value=None):
274
+ result = runner.invoke(specimen, ["remove", "nonexistent"])
275
+ assert result.exit_code != 0
276
+ assert "Cannot find specified checkpoint" in result.output
277
+
278
+
279
+ def test_rename_success(runner, mock_my_checkpoints):
280
+ """Test rename command success (lines 214-223)."""
281
+ with patch("proximl.cli.ProxiML", new=AsyncMock) as mock_proximl:
282
+ checkpoint = mock_my_checkpoints[0]
283
+
284
+ async def rename_async():
285
+ return None
286
+
287
+ checkpoint.rename = Mock(return_value=rename_async())
288
+
289
+ async def get_async(checkpoint_id):
290
+ return checkpoint
291
+
292
+ mock_proximl.checkpoints = AsyncMock()
293
+ mock_proximl.checkpoints.get = Mock(
294
+ side_effect=lambda checkpoint_id: get_async(checkpoint_id)
295
+ )
296
+
297
+ result = runner.invoke(specimen, ["rename", "1", "new-name"])
298
+ assert result.exit_code == 0
299
+ checkpoint.rename.assert_called_once_with(name="new-name")
300
+
301
+
302
+ def test_rename_not_found_none(runner):
303
+ """Test rename command when checkpoint is None (lines 214-223, line 219)."""
304
+ with patch("proximl.cli.ProxiML", new=AsyncMock) as mock_proximl:
305
+
306
+ async def get_async(checkpoint_id):
307
+ return None
308
+
309
+ mock_proximl.checkpoints = AsyncMock()
310
+ mock_proximl.checkpoints.get = Mock(
311
+ side_effect=lambda checkpoint_id: get_async(checkpoint_id)
312
+ )
313
+
314
+ result = runner.invoke(specimen, ["rename", "nonexistent", "new-name"])
315
+ assert result.exit_code != 0
316
+ assert "Cannot find specified checkpoint" in result.output
317
+
318
+
319
+ def test_rename_not_found_exception(runner):
320
+ """Test rename command when exception occurs (lines 214-223, line 221)."""
321
+ with patch("proximl.cli.ProxiML", new=AsyncMock) as mock_proximl:
322
+
323
+ async def get_async(checkpoint_id):
324
+ raise Exception("Not found")
325
+
326
+ mock_proximl.checkpoints = AsyncMock()
327
+ mock_proximl.checkpoints.get = Mock(
328
+ side_effect=lambda checkpoint_id: get_async(checkpoint_id)
329
+ )
330
+
331
+ result = runner.invoke(specimen, ["rename", "nonexistent", "new-name"])
332
+ assert result.exit_code != 0
333
+ assert "Cannot find specified checkpoint" in result.output
@@ -8,6 +8,7 @@ from aiohttp import WSMessage, WSMsgType
8
8
  import proximl.cloudbender.nodes as specimen
9
9
  from proximl.exceptions import (
10
10
  ApiError,
11
+ NodeError,
11
12
  SpecificationError,
12
13
  ProxiMLException,
13
14
  )
@@ -200,3 +201,114 @@ class nodeTests:
200
201
  None,
201
202
  dict(command="report"),
202
203
  )
204
+
205
+ @mark.asyncio
206
+ async def test_node_wait_for_already_at_status(self, node):
207
+ """Test wait_for returns immediately if already at target status."""
208
+ node._status = "active"
209
+ result = await node.wait_for("active")
210
+ assert result is None
211
+
212
+ @mark.asyncio
213
+ async def test_node_wait_for_invalid_status(self, node):
214
+ """Test wait_for raises error for invalid status."""
215
+ with raises(SpecificationError) as exc_info:
216
+ await node.wait_for("invalid_status")
217
+ assert "Invalid wait_for status" in str(exc_info.value.message)
218
+
219
+ @mark.asyncio
220
+ async def test_node_wait_for_timeout_validation(self, node):
221
+ """Test wait_for validates timeout (line 172)."""
222
+ node._status = "new" # Set to different status so timeout check runs
223
+ with raises(SpecificationError) as exc_info:
224
+ await node.wait_for("active", timeout=25 * 60 * 60)
225
+ assert "timeout must be less than" in str(exc_info.value.message)
226
+
227
+ @mark.asyncio
228
+ async def test_node_wait_for_success(self, node, mock_proximl):
229
+ """Test wait_for succeeds when status matches."""
230
+ node._status = "new"
231
+ api_response_new = dict(
232
+ provider_uuid="1",
233
+ region_uuid="a",
234
+ rig_uuid="x",
235
+ status="new",
236
+ )
237
+ api_response_active = dict(
238
+ provider_uuid="1",
239
+ region_uuid="a",
240
+ rig_uuid="x",
241
+ status="active",
242
+ )
243
+ mock_proximl._query = AsyncMock(
244
+ side_effect=[api_response_new, api_response_active]
245
+ )
246
+ with patch("proximl.cloudbender.nodes.asyncio.sleep", new_callable=AsyncMock):
247
+ result = await node.wait_for("active", timeout=10)
248
+ assert result == node
249
+ assert node.status == "active"
250
+
251
+ @mark.asyncio
252
+ async def test_node_wait_for_archived_404(self, node, mock_proximl):
253
+ """Test wait_for handles 404 for archived status."""
254
+ node._status = "active"
255
+ api_error = ApiError(404, {"errorMessage": "Not found"})
256
+ mock_proximl._query = AsyncMock(side_effect=api_error)
257
+ with patch("proximl.cloudbender.nodes.asyncio.sleep", new_callable=AsyncMock):
258
+ await node.wait_for("archived", timeout=10)
259
+
260
+ @mark.asyncio
261
+ async def test_node_wait_for_error_status(self, node, mock_proximl):
262
+ """Test wait_for raises error for errored/failed status."""
263
+ node._status = "new"
264
+ api_response_errored = dict(
265
+ provider_uuid="1",
266
+ region_uuid="a",
267
+ rig_uuid="x",
268
+ status="errored",
269
+ )
270
+ mock_proximl._query = AsyncMock(return_value=api_response_errored)
271
+ with patch("proximl.cloudbender.nodes.asyncio.sleep", new_callable=AsyncMock):
272
+ with raises(NodeError):
273
+ await node.wait_for("active", timeout=10)
274
+
275
+ @mark.asyncio
276
+ async def test_node_wait_for_timeout(self, node, mock_proximl):
277
+ """Test wait_for raises timeout exception."""
278
+ node._status = "new"
279
+ api_response_new = dict(
280
+ provider_uuid="1",
281
+ region_uuid="a",
282
+ rig_uuid="x",
283
+ status="new",
284
+ )
285
+ mock_proximl._query = AsyncMock(return_value=api_response_new)
286
+ with patch("proximl.cloudbender.nodes.asyncio.sleep", new_callable=AsyncMock):
287
+ with raises(ProxiMLException) as exc_info:
288
+ await node.wait_for("active", timeout=0.1)
289
+ assert "Timeout waiting for" in str(exc_info.value.message)
290
+
291
+ @mark.asyncio
292
+ async def test_node_wait_for_api_error_non_404(self, node, mock_proximl):
293
+ """Test wait_for raises ApiError when not 404 for archived (line 189)."""
294
+ node._status = "active"
295
+ api_error = ApiError(500, {"errorMessage": "Server Error"})
296
+ mock_proximl._query = AsyncMock(side_effect=api_error)
297
+ with patch("proximl.cloudbender.nodes.asyncio.sleep", new_callable=AsyncMock):
298
+ with raises(ApiError):
299
+ await node.wait_for("archived", timeout=10)
300
+
301
+ @mark.asyncio
302
+ async def test_node_wait_for_failed_status(self, node, mock_proximl):
303
+ """Test wait_for raises error for failed status (line 191)."""
304
+ node._status = "new"
305
+ api_response_failed = dict(
306
+ provider_uuid="1",
307
+ region_uuid="a",
308
+ rig_uuid="x",
309
+ status="failed",
310
+ )
311
+ mock_proximl._query = AsyncMock(return_value=api_response_failed)
312
+ with patch("proximl.cloudbender.nodes.asyncio.sleep", new_callable=AsyncMock):
313
+ with raises(NodeError):
314
+ await node.wait_for("active", timeout=10)
@@ -139,3 +139,99 @@ class providerTests:
139
139
  mock_proximl._query.assert_called_once_with(f"/provider/1", "GET")
140
140
  assert provider.id == "provider-id-1"
141
141
  assert response.id == "provider-id-1"
142
+
143
+ def test_provider_status_property(self, provider):
144
+ """Test provider status property."""
145
+ provider._status = "ready"
146
+ assert provider.status == "ready"
147
+
148
+ @mark.asyncio
149
+ async def test_provider_wait_for_already_at_status(self, provider):
150
+ """Test wait_for returns immediately if already at target status."""
151
+ provider._status = "ready"
152
+ result = await provider.wait_for("ready")
153
+ assert result is None
154
+
155
+ @mark.asyncio
156
+ async def test_provider_wait_for_invalid_status(self, provider):
157
+ """Test wait_for raises error for invalid status."""
158
+ with raises(SpecificationError) as exc_info:
159
+ await provider.wait_for("invalid_status")
160
+ assert "Invalid wait_for status" in str(exc_info.value.message)
161
+
162
+ @mark.asyncio
163
+ async def test_provider_wait_for_timeout_validation(self, provider):
164
+ """Test wait_for validates timeout."""
165
+ with raises(SpecificationError) as exc_info:
166
+ await provider.wait_for("ready", timeout=25 * 60 * 60)
167
+ assert "timeout must be less than" in str(exc_info.value.message)
168
+
169
+ @mark.asyncio
170
+ async def test_provider_wait_for_success(self, provider, mock_proximl):
171
+ """Test wait_for succeeds when status matches."""
172
+ provider._status = "new"
173
+ api_response_new = dict(
174
+ customer_uuid="a",
175
+ provider_uuid="1",
176
+ status="new",
177
+ )
178
+ api_response_ready = dict(
179
+ customer_uuid="a",
180
+ provider_uuid="1",
181
+ status="ready",
182
+ )
183
+ mock_proximl._query = AsyncMock(
184
+ side_effect=[api_response_new, api_response_ready]
185
+ )
186
+ with patch("proximl.cloudbender.providers.asyncio.sleep", new_callable=AsyncMock):
187
+ result = await provider.wait_for("ready", timeout=10)
188
+ assert result == provider
189
+ assert provider.status == "ready"
190
+
191
+ @mark.asyncio
192
+ async def test_provider_wait_for_archived_404(self, provider, mock_proximl):
193
+ """Test wait_for handles 404 for archived status."""
194
+ provider._status = "ready"
195
+ api_error = ApiError(404, {"errorMessage": "Not found"})
196
+ mock_proximl._query = AsyncMock(side_effect=api_error)
197
+ with patch("proximl.cloudbender.providers.asyncio.sleep", new_callable=AsyncMock):
198
+ await provider.wait_for("archived", timeout=10)
199
+
200
+ @mark.asyncio
201
+ async def test_provider_wait_for_error_status(self, provider, mock_proximl):
202
+ """Test wait_for raises error for errored/failed status."""
203
+ provider._status = "new"
204
+ api_response_errored = dict(
205
+ customer_uuid="a",
206
+ provider_uuid="1",
207
+ status="errored",
208
+ )
209
+ mock_proximl._query = AsyncMock(return_value=api_response_errored)
210
+ with patch("proximl.cloudbender.providers.asyncio.sleep", new_callable=AsyncMock):
211
+ with raises(specimen.ProviderError):
212
+ await provider.wait_for("ready", timeout=10)
213
+
214
+ @mark.asyncio
215
+ async def test_provider_wait_for_timeout(self, provider, mock_proximl):
216
+ """Test wait_for raises timeout exception."""
217
+ provider._status = "new"
218
+ api_response_new = dict(
219
+ customer_uuid="a",
220
+ provider_uuid="1",
221
+ status="new",
222
+ )
223
+ mock_proximl._query = AsyncMock(return_value=api_response_new)
224
+ with patch("proximl.cloudbender.providers.asyncio.sleep", new_callable=AsyncMock):
225
+ with raises(ProxiMLException) as exc_info:
226
+ await provider.wait_for("ready", timeout=0.1)
227
+ assert "Timeout waiting for" in str(exc_info.value.message)
228
+
229
+ @mark.asyncio
230
+ async def test_provider_wait_for_api_error_non_404(self, provider, mock_proximl):
231
+ """Test wait_for raises ApiError when not 404 for archived (line 115)."""
232
+ provider._status = "ready"
233
+ api_error = ApiError(500, {"errorMessage": "Server Error"})
234
+ mock_proximl._query = AsyncMock(side_effect=api_error)
235
+ with patch("proximl.cloudbender.providers.asyncio.sleep", new_callable=AsyncMock):
236
+ with raises(ApiError):
237
+ await provider.wait_for("archived", timeout=10)
@@ -195,3 +195,109 @@ class regionTests:
195
195
  mock_proximl._query.assert_called_once_with(
196
196
  "/provider/1/region/a/checkpoint", "POST", None, expected_payload
197
197
  )
198
+
199
+ @mark.asyncio
200
+ async def test_region_wait_for_already_at_status(self, region):
201
+ """Test wait_for returns immediately if already at target status."""
202
+ region._status = "healthy"
203
+ result = await region.wait_for("healthy")
204
+ assert result is None
205
+
206
+ @mark.asyncio
207
+ async def test_region_wait_for_invalid_status(self, region):
208
+ """Test wait_for raises error for invalid status."""
209
+ with raises(SpecificationError) as exc_info:
210
+ await region.wait_for("invalid_status")
211
+ assert "Invalid wait_for status" in str(exc_info.value.message)
212
+
213
+ @mark.asyncio
214
+ async def test_region_wait_for_timeout_validation(self, region):
215
+ """Test wait_for validates timeout (line 135)."""
216
+ region._status = "new" # Set to different status so timeout check runs
217
+ with raises(SpecificationError) as exc_info:
218
+ await region.wait_for("healthy", timeout=25 * 60 * 60)
219
+ assert "timeout must be less than" in str(exc_info.value.message)
220
+
221
+ @mark.asyncio
222
+ async def test_region_wait_for_success(self, region, mock_proximl):
223
+ """Test wait_for succeeds when status matches."""
224
+ region._status = "new"
225
+ api_response_new = dict(
226
+ provider_uuid="1",
227
+ region_uuid="a",
228
+ status="new",
229
+ )
230
+ api_response_healthy = dict(
231
+ provider_uuid="1",
232
+ region_uuid="a",
233
+ status="healthy",
234
+ )
235
+ mock_proximl._query = AsyncMock(
236
+ side_effect=[api_response_new, api_response_healthy]
237
+ )
238
+ with patch("proximl.cloudbender.regions.asyncio.sleep", new_callable=AsyncMock):
239
+ result = await region.wait_for("healthy", timeout=10)
240
+ assert result == region
241
+ assert region.status == "healthy"
242
+
243
+ @mark.asyncio
244
+ async def test_region_wait_for_archived_404(self, region, mock_proximl):
245
+ """Test wait_for handles 404 for archived status."""
246
+ region._status = "healthy"
247
+ api_error = ApiError(404, {"errorMessage": "Not found"})
248
+ mock_proximl._query = AsyncMock(side_effect=api_error)
249
+ with patch("proximl.cloudbender.regions.asyncio.sleep", new_callable=AsyncMock):
250
+ await region.wait_for("archived", timeout=10)
251
+
252
+ @mark.asyncio
253
+ async def test_region_wait_for_error_status(self, region, mock_proximl):
254
+ """Test wait_for raises error for errored/failed status."""
255
+ region._status = "new"
256
+ api_response_errored = dict(
257
+ provider_uuid="1",
258
+ region_uuid="a",
259
+ status="errored",
260
+ )
261
+ mock_proximl._query = AsyncMock(return_value=api_response_errored)
262
+ with patch("proximl.cloudbender.regions.asyncio.sleep", new_callable=AsyncMock):
263
+ with raises(specimen.RegionError):
264
+ await region.wait_for("healthy", timeout=10)
265
+
266
+ @mark.asyncio
267
+ async def test_region_wait_for_timeout(self, region, mock_proximl):
268
+ """Test wait_for raises timeout exception."""
269
+ region._status = "new"
270
+ api_response_new = dict(
271
+ provider_uuid="1",
272
+ region_uuid="a",
273
+ status="new",
274
+ )
275
+ mock_proximl._query = AsyncMock(return_value=api_response_new)
276
+ with patch("proximl.cloudbender.regions.asyncio.sleep", new_callable=AsyncMock):
277
+ with raises(ProxiMLException) as exc_info:
278
+ await region.wait_for("healthy", timeout=0.1)
279
+ assert "Timeout waiting for" in str(exc_info.value.message)
280
+
281
+ @mark.asyncio
282
+ async def test_region_wait_for_api_error_non_404(self, region, mock_proximl):
283
+ """Test wait_for raises ApiError when not 404 for archived (line 152)."""
284
+ region._status = "healthy"
285
+ api_error = ApiError(500, {"errorMessage": "Server Error"})
286
+ mock_proximl._query = AsyncMock(side_effect=api_error)
287
+ with patch("proximl.cloudbender.regions.asyncio.sleep", new_callable=AsyncMock):
288
+ with raises(ApiError):
289
+ await region.wait_for("archived", timeout=10)
290
+
291
+ @mark.asyncio
292
+ async def test_region_wait_for_failed_status(self, region, mock_proximl):
293
+ """Test wait_for raises error for failed status."""
294
+ region._status = "new"
295
+ api_response_failed = dict(
296
+ provider_uuid="1",
297
+ region_uuid="a",
298
+ status="failed",
299
+ )
300
+ mock_proximl._query = AsyncMock(return_value=api_response_failed)
301
+ with patch("proximl.cloudbender.regions.asyncio.sleep", new_callable=AsyncMock):
302
+ with raises(specimen.RegionError):
303
+ await region.wait_for("healthy", timeout=10)