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
proximl/volumes.py CHANGED
@@ -10,7 +10,7 @@ from .exceptions import (
10
10
  SpecificationError,
11
11
  ProxiMLException,
12
12
  )
13
- from .connections import Connection
13
+ from proximl.utils.transfer import upload, download
14
14
 
15
15
 
16
16
  class Volumes(object):
@@ -56,7 +56,9 @@ class Volumes(object):
56
56
  return volume
57
57
 
58
58
  async def remove(self, id, **kwargs):
59
- await self.proximl._query(f"/volume/{id}", "DELETE", dict(**kwargs, force=True))
59
+ await self.proximl._query(
60
+ f"/volume/{id}", "DELETE", dict(**kwargs, force=True)
61
+ )
60
62
 
61
63
 
62
64
  class Volume:
@@ -120,56 +122,45 @@ class Volume:
120
122
  )
121
123
  return resp
122
124
 
123
- async def get_connection_utility_url(self):
124
- resp = await self.proximl._query(
125
- f"/volume/{self._id}/download",
126
- "GET",
127
- dict(project_uuid=self._project_uuid),
128
- )
129
- return resp
130
-
131
- def get_connection_details(self):
132
- if self._volume.get("vpn"):
133
- details = dict(
134
- entity_type="volume",
135
- project_uuid=self._volume.get("project_uuid"),
136
- cidr=self._volume.get("vpn").get("cidr"),
137
- ssh_port=self._volume.get("vpn").get("client").get("ssh_port"),
138
- input_path=(
139
- self._volume.get("source_uri")
140
- if self.status in ["new", "downloading"]
141
- else None
142
- ),
143
- output_path=(
144
- self._volume.get("output_uri")
145
- if self.status == "exporting"
146
- else None
147
- ),
148
- )
149
- else:
150
- details = dict()
151
- return details
152
-
153
125
  async def connect(self):
154
- if self.status in ["ready", "failed"]:
155
- raise SpecificationError(
156
- "status",
157
- f"You can only connect to downloading or exporting volumes.",
158
- )
159
- if self.status == "new":
160
- await self.wait_for("downloading")
161
- connection = Connection(
162
- self.proximl, entity_type="volume", id=self.id, entity=self
163
- )
164
- await connection.start()
165
- return connection.status
126
+ if self.status not in ["downloading", "exporting"]:
127
+ if self.status == "new":
128
+ await self.wait_for("downloading")
129
+ else:
130
+ raise SpecificationError(
131
+ "status",
132
+ f"You can only connect to downloading or exporting volumes.",
133
+ )
166
134
 
167
- async def disconnect(self):
168
- connection = Connection(
169
- self.proximl, entity_type="volume", id=self.id, entity=self
170
- )
171
- await connection.stop()
172
- return connection.status
135
+ # Refresh to get latest entity data
136
+ await self.refresh()
137
+
138
+ if self.status == "downloading":
139
+ # Upload task - get auth_token, hostname, and source_uri from volume
140
+ auth_token = self._volume.get("auth_token")
141
+ hostname = self._volume.get("hostname")
142
+ source_uri = self._volume.get("source_uri")
143
+
144
+ if not auth_token or not hostname or not source_uri:
145
+ raise SpecificationError(
146
+ "status",
147
+ f"Volume in downloading status missing required connection properties (auth_token, hostname, source_uri).",
148
+ )
149
+
150
+ await upload(hostname, auth_token, source_uri)
151
+ elif self.status == "exporting":
152
+ # Download task - get auth_token, hostname, and output_uri from volume
153
+ auth_token = self._volume.get("auth_token")
154
+ hostname = self._volume.get("hostname")
155
+ output_uri = self._volume.get("output_uri")
156
+
157
+ if not auth_token or not hostname or not output_uri:
158
+ raise SpecificationError(
159
+ "status",
160
+ f"Volume in exporting status missing required connection properties (auth_token, hostname, output_uri).",
161
+ )
162
+
163
+ await download(hostname, auth_token, output_uri)
173
164
 
174
165
  async def remove(self, force=False):
175
166
  await self.proximl._query(
@@ -208,7 +199,9 @@ class Volume:
208
199
  if msg_handler:
209
200
  msg_handler(data)
210
201
  else:
211
- timestamp = datetime.fromtimestamp(int(data.get("time")) / 1000)
202
+ timestamp = datetime.fromtimestamp(
203
+ int(data.get("time")) / 1000
204
+ )
212
205
  print(
213
206
  f"{timestamp.strftime('%m/%d/%Y, %H:%M:%S')}: {data.get('msg').rstrip()}"
214
207
  )
@@ -237,7 +230,7 @@ class Volume:
237
230
  async def wait_for(self, status, timeout=300):
238
231
  if self.status == status:
239
232
  return
240
- valid_statuses = ["downloading", "ready", "archived"]
233
+ valid_statuses = ["downloading", "ready", "exporting", "archived"]
241
234
  if not status in valid_statuses:
242
235
  raise SpecificationError(
243
236
  "status",
@@ -252,7 +245,9 @@ class Volume:
252
245
  )
253
246
  POLL_INTERVAL_MIN = 5
254
247
  POLL_INTERVAL_MAX = 60
255
- POLL_INTERVAL = max(min(timeout / 60, POLL_INTERVAL_MAX), POLL_INTERVAL_MIN)
248
+ POLL_INTERVAL = max(
249
+ min(timeout / 60, POLL_INTERVAL_MAX), POLL_INTERVAL_MIN
250
+ )
256
251
  retry_count = math.ceil(timeout / POLL_INTERVAL)
257
252
  count = 0
258
253
  while count < retry_count:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: proximl
3
- Version: 0.5.16
3
+ Version: 1.0.0
4
4
  Summary: proxiML client SDK and command line utilities
5
5
  Home-page: https://github.com/proxiML/python-sdk
6
6
  Author: proxiML
@@ -12,8 +12,8 @@ Classifier: Programming Language :: Python :: 3
12
12
  Classifier: Programming Language :: Python :: 3.8
13
13
  Requires-Python: >=3.8
14
14
  Description-Content-Type: text/markdown
15
- Requires-Dist: aiodocker
16
15
  Requires-Dist: aiohttp
16
+ Requires-Dist: aiofiles
17
17
  Requires-Dist: boto3
18
18
  Requires-Dist: Click (>8)
19
19
  Requires-Dist: python-jose[cryptography]
@@ -190,7 +190,7 @@ To list all datasets:
190
190
  proximl dataset list
191
191
  ```
192
192
 
193
- To connect to a job that requires the [connection capability](https://docs.proximl.ai/reference/connection-capability):
193
+ To connect to a job that uses the "local" file transfer method:
194
194
 
195
195
  ```
196
196
  proximl job connect <job ID or name>
@@ -1,28 +1,25 @@
1
1
  examples/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  examples/create_dataset_and_training_job.py,sha256=Fqueoz2KD1MTxnU960iqHUdxvo68Xe64HT5XS55lI9w,1178
3
- examples/local_storage.py,sha256=6K6LMO7ZPI7N2KdBcgqXSvdsqJfjISzN4yRO9YrJqbA,1745
3
+ examples/local_storage.py,sha256=1_bvGoE04ttvKT5MH7Vm8VFiJrw6nF_S7FojlcdC3kY,1687
4
4
  examples/training_inference_pipeline.py,sha256=pxux0QUUtRXxKj2rX-6fPEKBIi43mhRd1zJ-Lf1ZJGI,2334
5
- proximl/__init__.py,sha256=9y__q8yiD7SWW8PzY4Cx6ZFTodmOM-e-L572DUjwATg,433
5
+ proximl/__init__.py,sha256=6bDDwARPoJxFWPJex__P6-Qdw49mnh_w-YXWnlVLSYg,432
6
6
  proximl/__main__.py,sha256=JgErYkiskih8Y6oRwowALtR-rwQhAAdqOYWjQraRIPI,59
7
- proximl/auth.py,sha256=LacGBDAVel5HcJx7JXp1wVn3s0gZc7nf--vDfSFOXYU,26565
8
- proximl/checkpoints.py,sha256=Ezpiab9Wfcmd2h5Slm9U5lekZGXiPzvhDKxXXgla1yo,8790
9
- proximl/connections.py,sha256=0C8VJSkDturQVlzg_3yAkQz8W9NAmgzDCBRK_SrzeUI,20035
10
- proximl/datasets.py,sha256=dBDzf7DZ1rS5LmJPy5oV-JC0PXGTf-NZuBDG1KPyoA4,8600
7
+ proximl/checkpoints.py,sha256=SClBmtCUM4Ql8DWEDw4mpDERJAXE7z9ZnYqEcTwsr-g,8924
8
+ proximl/datasets.py,sha256=m2rza1wdRx0DPWc7cHWcf7J1FNdMrWV59Yxlq7ouVYU,8690
11
9
  proximl/environments.py,sha256=L_cRmau1wJxpGvnJAqgms-GiNdDhiuOntrlBqsdoE3A,1507
12
10
  proximl/exceptions.py,sha256=q1YfsqbLw3y1DJHP1FcMM8xG5CEhQsTIvRfq4v5PyVw,5468
13
11
  proximl/gpu_types.py,sha256=V-EZzE-hDLi5eVQ2_9yGLTm8-Qk1AnnzctfSVC44yLY,1901
14
- proximl/jobs.py,sha256=HiGs2gRKPFZReHLg9DLPsDWEecxZpafWv6XFBSGXYj0,17947
15
- proximl/models.py,sha256=gJQ1C3HGEHScW41PAQS3Q8IKSQSauIUIJxOZvkDzizQ,8284
16
- proximl/proximl.py,sha256=7UyqYbGO9edFJec54xqk_wwahKldXQlY5112Izg3saA,11902
17
- proximl/volumes.py,sha256=qRIgzvJ4NTHuLi7QN_6v61Urns8l4Y9KjWladCIQ2gk,8443
18
- proximl/cli/__init__.py,sha256=R_8ExAKQp67N2xtwGM00gtK3zSoWPGruHAP_XFLddSI,4346
19
- proximl/cli/checkpoint.py,sha256=Iv1i1EAt2LJey2wy2ioQ6-ZysqwRG4kFj0lnE6INZCM,7170
20
- proximl/cli/connection.py,sha256=YiWqRIB9ZfTl30DjDFaJEpXuDrA-Ldl9PEzFFdZ_hFI,1700
21
- proximl/cli/dataset.py,sha256=ueoeicBY8aMLpvpKUIBICnS9GsEnDOj7ZlFmOfjjY4c,6871
12
+ proximl/jobs.py,sha256=lBj1UGPKDQAPbQEAUQ5lPL4-35wHNVDQVUGdtoE8G5Q,24928
13
+ proximl/models.py,sha256=ZigU31SdpsxsnkW5wMt4narvlqVO5MbxTQuV__Mndqo,8339
14
+ proximl/proximl.py,sha256=x0P7D6iMKl1nXWCWMzaHhFo63oxb7RWhf1KPa9zUOkA,12724
15
+ proximl/volumes.py,sha256=uQDVLLJa_-GNdxwo8xh67KcGhlJ8EMHtW_V3B9IfgWA,8533
16
+ proximl/cli/__init__.py,sha256=vtNYq0HJ491CWMjIevCVSnLilCIDQv9LBhZmVWv9N3c,4330
17
+ proximl/cli/checkpoint.py,sha256=mgEQ13XO9FS1SOXW4hTfbN2cqAYY7izSWt2KyrEITJQ,6020
18
+ proximl/cli/dataset.py,sha256=MpAC-AvXXEZXr1PE0ch5TPq1U8v2fecbrTQcvJ8Lhcc,5721
22
19
  proximl/cli/environment.py,sha256=nh7oYbG5oOrZEpZkMkKgvzFXmQJWnFTMw1-YbuvkdFU,1019
23
20
  proximl/cli/gpu.py,sha256=xL8eqM5ca_Ueaj8cWit1iKn34KhaR0StrubVeRU2YQY,883
24
- proximl/cli/model.py,sha256=xdjveIaRPK7MdfrnFygPEuwYRJRW9VqheZ-11XnXDcE,6111
25
- proximl/cli/volume.py,sha256=uyIrKov4zwCjyLyZrEJYoEbIkS0zdU3xSyWZk2BM1kA,6246
21
+ proximl/cli/model.py,sha256=n6hghRf3Krj1K39UD6xg8UvTGIax0XxXrx7hGe8TiSY,4963
22
+ proximl/cli/volume.py,sha256=lACn2vG0R1NMfGLRHl6t9oSUZI2twouv3Y-N3kwri_Q,5135
26
23
  proximl/cli/cloudbender/__init__.py,sha256=ATPj85Be6o4xw8M6vvHfkuLyw1drc_0IlCMh1wSebYQ,592
27
24
  proximl/cli/cloudbender/data_connector.py,sha256=D5xbUzpUGpn_NN-NZQ9vSxOZY610c1LsoBC40DkPwkw,3606
28
25
  proximl/cli/cloudbender/datastore.py,sha256=YBFbobEKjw3s5uYC1krD_Oa-VEz3kjQ5UvPOqkq-NJE,3323
@@ -31,8 +28,8 @@ proximl/cli/cloudbender/node.py,sha256=xxzj68YvpRey2vZQasgYTnwv3x7TnwpuPSSf8Ma5a
31
28
  proximl/cli/cloudbender/provider.py,sha256=qhWbDK1tWi00wQWEYqGw7yGoZx0nEjV40GLHRuuE86c,1726
32
29
  proximl/cli/cloudbender/region.py,sha256=WnSkY4dXKRJ-FNaoxMfmoh6iuUx5dXCNJmEFT34Xtao,2892
33
30
  proximl/cli/cloudbender/service.py,sha256=6NuwlFULJLtOmMy9OFA8GkW4MLvNemAcd_KitQyDXV8,3147
34
- proximl/cli/job/__init__.py,sha256=s8mU2PvCWDcv4gGT3EmjHn8MIZlXBAoayoZKmnKpXnY,6545
35
- proximl/cli/job/create.py,sha256=plRiX9lRqt6lrjWvTIVbLC-EoDgSL0-oYqwIfuWb_ek,35098
31
+ proximl/cli/job/__init__.py,sha256=IaoNWZUn4VHHahTaAk9z7Vr4CXzHk4v3RWHzPcEZP-8,5379
32
+ proximl/cli/job/create.py,sha256=EmIxMO9WyN06fwsAGl7zlUJURa-9z4n3jWOA38ONfcw,35157
36
33
  proximl/cli/project/__init__.py,sha256=ZSiXhsq7O1JwGecsBKBOD1W0VuiwErs-FFsfrXMOiFs,1918
37
34
  proximl/cli/project/credential.py,sha256=5vPI6VBIo_YyvAzfUa9ARmz-LUnkdIVW0zbko9t0Q38,3443
38
35
  proximl/cli/project/data_connector.py,sha256=CKF4snfrzDmeDXb4au8nV2W9jTweRMDZ89U2GAlBedQ,1411
@@ -54,18 +51,21 @@ proximl/projects/credentials.py,sha256=hWz6EUEAgltkAQMDjQVsFlVvfWUbZfY3KoWpldnCr
54
51
  proximl/projects/data_connectors.py,sha256=o2-K40BdF85TBXJgj1UMsznBmNHmnYh1qbZtHmmOa2Y,2216
55
52
  proximl/projects/datastores.py,sha256=8nHpMEHn3UzsIhmPMoXympbRRJ10XkHdyRhBXUVXyeE,2095
56
53
  proximl/projects/members.py,sha256=siREAa-Oac1pLGRHMXmj3ShoB0DVl8uY34eFdTlSezA,2826
57
- proximl/projects/projects.py,sha256=PJYUaXbYMINcvlDQePKUSAz6uO11jg9tj5gJW-cNjGQ,2890
54
+ proximl/projects/projects.py,sha256=2lYStCIQ6_KB411NS_u1Ia2uatKxre1jFS6Lbr2LqzY,2840
58
55
  proximl/projects/secrets.py,sha256=xlfzRONjT2ySJDcq8FYRQmSU2ZJMdPLS8NbePWYNCeM,2160
59
56
  proximl/projects/services.py,sha256=aCESZjqfI9K-iXl8uJQsjEyoX29LuPaNv_TFjK6B2Ts,2771
57
+ proximl/utils/__init__.py,sha256=omDnxAdZ_MWqiK0cfcqPEp1EfRTHUCAC_kJYQa77K6M,39
58
+ proximl/utils/auth.py,sha256=K-JqDmifY83hVqGJvk1qlEEK8fb5KmVqpqQrEX9X-wA,26573
59
+ proximl/utils/transfer.py,sha256=EEQOUVbdu12P4gWcECuZYjozoi8lMcak9dVIz4guGqY,22395
60
60
  tests/integration/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
61
61
  tests/integration/conftest.py,sha256=rqOKgDZ37SZH6CxXZIQSHtx8SF5GC1MlC49Zb5xrzsk,1135
62
- tests/integration/test_checkpoints_integration.py,sha256=2gKhbax96ePIEZqcM4IeqSB0fTOYPh9nexNJtC96LuU,3239
63
- tests/integration/test_datasets_integration.py,sha256=y8NL7fKQvZhJs1r9UAcLjXI0832tHr9XfIaRhHX9aOk,3529
62
+ tests/integration/test_checkpoints_integration.py,sha256=qHD-tzQc6QclqKf5o3pH2phcUps7K0PS3ctqPJls8M0,3219
63
+ tests/integration/test_datasets_integration.py,sha256=o1abXw5vvZ3RvKA-sQNBO3xJSDVgPaX-P4lNTeRnhWk,3518
64
64
  tests/integration/test_environments_integration.py,sha256=7P6pKSyxA7rTwyNCD9HEaM2ablMG8WcBesOzkG-BgsQ,1403
65
65
  tests/integration/test_gpu_types_integration.py,sha256=Zv-yrHcAgKau9BJQvzi92bdrRHhLPl_hbhhzNLEWJ9w,1256
66
- tests/integration/test_jobs_integration.py,sha256=mSgPERDycfphLuLd7Hp3tSo-SpnbACp963x99K3XbtU,25263
67
- tests/integration/test_models_integration.py,sha256=du9i3GsWjwEWei_-5BzfoBneBNcZ-9NQsDdOfn4dzd8,2906
68
- tests/integration/test_volumes_integration.py,sha256=fTsP5_wyspzR_DX_ArXz5Fflq2dEmXtJbhwqCjSQDOA,3299
66
+ tests/integration/test_jobs_integration.py,sha256=sX_DYopEW8mwKepMteyiaHoZMnd5Ct_ZKOjtU3uHZZw,25386
67
+ tests/integration/test_models_integration.py,sha256=2GY_RF_fvARIO67YS4qfAMfndk3NO8abcXMiEhvjH7g,2924
68
+ tests/integration/test_volumes_integration.py,sha256=ecgwQRc83glqHoRwohrDxvNHQ0XLFDwCFbVLWSN6C3M,3268
69
69
  tests/integration/cloudbender/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
70
70
  tests/integration/cloudbender/conftest.py,sha256=K1EQC7U7aBnywJq-QvBZINqHtQm7kq5feDWVKAtp22E,777
71
71
  tests/integration/cloudbender/test_providers_integration.py,sha256=8_1TBxcIpQcET7VStHIsi_-_3j5hs8H-icsKN3qnNa8,1327
@@ -81,21 +81,21 @@ tests/integration/projects/test_projects_members_integration.py,sha256=4i4SeDCDZ
81
81
  tests/integration/projects/test_projects_secrets_integration.py,sha256=TyqItJs-SBzOUwDy3vs_-yrDXeDF8r1_bAovr8C1a_s,1520
82
82
  tests/integration/projects/test_projects_services_integration.py,sha256=cHOGSUpl3rqYJnCrbYp-2Zmn1ngFJTDV9Io7K8fDOp0,1569
83
83
  tests/unit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
84
- tests/unit/conftest.py,sha256=11Z49O4mKXtm6xl18kWPomZvbZJQpqdaDYeJHFQbO1A,35588
85
- tests/unit/test_auth_unit.py,sha256=IJZHT5CZLNfu3MybTdEWIsKlvxNfFYpZ6oWYzbS856g,858
86
- tests/unit/test_checkpoints_unit.py,sha256=FbBM8Ffqy8JGTpWq2zWAWle2CI8A0MB_zABL_9Mezbk,15965
87
- tests/unit/test_connections_unit.py,sha256=LFAZzlrvL9oM8rZJTiC1oA9quw1KA2vMUCc3LV6SjXs,5507
88
- tests/unit/test_datasets_unit.py,sha256=c3suBgQMtzJckHlY8MFwU38WundXobbjb-KXmUboulI,15910
84
+ tests/unit/conftest.py,sha256=LRrAR1KxlVDVuiSgS7krd12CxCaEXU4nG1fV2lUjjY0,35592
85
+ tests/unit/test_auth_unit.py,sha256=3oqdvz2jM-G37QM9FK6oCvS_w3EcVadz7rKawFqt-OU,1413
86
+ tests/unit/test_checkpoints_unit.py,sha256=OcPL61j9AqZWWPQsDxME5GQCud0xowkJuAB-__3OkB0,21914
87
+ tests/unit/test_datasets_unit.py,sha256=rv4qQoIjQaPCMPOGkH4T5sZyi5-cJlIDwtWr3OC0tK0,21135
89
88
  tests/unit/test_environments_unit.py,sha256=HS4NN_cRWGQNDfovCxNGrVRoZI4NcUgsXyzvapNi6Ms,1882
90
- tests/unit/test_exceptions.py,sha256=ASiwzV9z-6jk0Y2qSHEBmGOwKx-hXz5vaHcyApbjSKA,906
91
- tests/unit/test_gpu_types_unit.py,sha256=d2JCQcFgBpkHEBUhKeiyngioEs9cz2qMGM2MZdwg-1o,2054
92
- tests/unit/test_jobs_unit.py,sha256=kCrm2FdogqYgji0Coe9KGaODYEsXmvuV46FN2mUE1Ts,26835
93
- tests/unit/test_models_unit.py,sha256=e9tcTcojLLTPKgx1wdAwP3j1k6C2aBnCbdxBhwKiRoQ,15091
94
- tests/unit/test_proximl_unit.py,sha256=E-er5V1O-4ZVfTO2R-2wGsKN80q0_BVG5ho9Ab9lnE4,1701
95
- tests/unit/test_volumes_unit.py,sha256=6FwEfANDZ2Z_ec3OL3Q12H_ml3TK9q2WgJW8hN-qwrc,15382
89
+ tests/unit/test_exceptions.py,sha256=qD6YV1wwAx-taCXSaBisc1w88-pKujJJPQykjebOIk8,5749
90
+ tests/unit/test_gpu_types_unit.py,sha256=eCPzi3EhKupDhlfIMPCIHCP3HXDM-KiheSsigxKJGIw,2526
91
+ tests/unit/test_jobs_unit.py,sha256=V_9xCupqQwcg_Cz3dtbZDsB9sknlgKJ-Hm6mElR_6zk,63964
92
+ tests/unit/test_main_unit.py,sha256=DSa7gE9D1ccxrPZCC0mUqWqTm7otC-OOGBfHaI_998I,627
93
+ tests/unit/test_models_unit.py,sha256=6YKMHHrAnhxU7RvVpqsf4elzcK8TOaQsuw0dscIUiu8,20127
94
+ tests/unit/test_proximl_unit.py,sha256=q0Sifg6uu8F_l5lUw6Sxifw-q5Y45eI7Bfk3A8o5WmA,23756
95
+ tests/unit/test_volumes_unit.py,sha256=2w3Nv7IqMMK3nXgmEN2LNNmt67hp2omeKNe1CB4_amo,20185
96
96
  tests/unit/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
97
97
  tests/unit/cli/conftest.py,sha256=w6p_2URicywJKUCtY79tSD_mx8cwJtxHbK_Lu3grOYs,236
98
- tests/unit/cli/test_cli_checkpoint_unit.py,sha256=Z4v80C2gqzRXdHons3_82lNC_VqL_YOt53HrUQnzReI,702
98
+ tests/unit/cli/test_cli_checkpoint_unit.py,sha256=9-mtZxHb06b5QyyAXurcbOdRzdSfqBfUGkmZRiiKTgI,11830
99
99
  tests/unit/cli/test_cli_datasets_unit.py,sha256=Hq9kWHMXIG2rI5GWjN-QuCquZ1D4Qaa62Bow0iPDOLk,653
100
100
  tests/unit/cli/test_cli_environment_unit.py,sha256=iiNj_fxiCinq9FZgkwzcO_OLu9O279Hk8nJpFJwDpPg,683
101
101
  tests/unit/cli/test_cli_gpu_unit.py,sha256=xdwIZEZJcJlWSuLBEcLhZXXH9EogZoKRiJlMRxaDa3o,650
@@ -121,21 +121,23 @@ tests/unit/cloudbender/test_data_connectors_unit.py,sha256=ZCnI9Gr2RAcpHiPZ5t2ka
121
121
  tests/unit/cloudbender/test_datastores_unit.py,sha256=8PKa6XRdZ1MoVEwbRxA67qKeI6DrycpLU1RxWeokMnw,5281
122
122
  tests/unit/cloudbender/test_device_configs_unit.py,sha256=wBDnTfrP6yl0K_jWcNU2ADR6iHoe1sRzMpvNfs_dwXs,5714
123
123
  tests/unit/cloudbender/test_devices_unit.py,sha256=C2YTnfIpHlxdidgfbTnlzl72r5O7kqKStQUhWcTTXDg,9103
124
- tests/unit/cloudbender/test_nodes_unit.py,sha256=ehOHkNroiLKNTR09SbnBPpwELE72GcGT6le_YN5aVwQ,6231
125
- tests/unit/cloudbender/test_providers_unit.py,sha256=y63VCqHXb4Yu8sh0kW30-ojRvv9aUa5j1jNkmb46KTc,4373
126
- tests/unit/cloudbender/test_regions_unit.py,sha256=9bvP268gpNyygjh1IEpSSiUt2aP6okv7QOsV1XoaIS0,6299
127
- tests/unit/cloudbender/test_services_unit.py,sha256=V1JHfqNcLjz7d3qtVxPBCKzd5_lcL5iwhvWqQ0kw0J0,5220
124
+ tests/unit/cloudbender/test_nodes_unit.py,sha256=qsiqAVDU_TaLStSBVwKJeK4Y1GzmNCTtGqeSYJJMtP0,10928
125
+ tests/unit/cloudbender/test_providers_unit.py,sha256=acCN_8sjmzg9Wc87Erd6M48-ngWhcsWziPMsIbRDeQM,8588
126
+ tests/unit/cloudbender/test_regions_unit.py,sha256=J_Qn_Vi-FQfMLqIy5T4MwEPUxjZW8Z2cDMpkX7esJSE,10961
127
+ tests/unit/cloudbender/test_services_unit.py,sha256=ZmFaleqzIeIXXDGAqYlochdoVMwgiHQDBg3vxRutc4Q,10956
128
128
  tests/unit/projects/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
129
129
  tests/unit/projects/test_project_credentials_unit.py,sha256=y528DyvIm7uc5mFuwiUPjOMO1dkVcMEeYQqzMmKLOuk,3480
130
- tests/unit/projects/test_project_data_connectors_unit.py,sha256=Yx2yCUgcuN_KfTKynZxse0Nx8jRrSlMygI7R8a-NjE4,3385
131
- tests/unit/projects/test_project_datastores_unit.py,sha256=WZMKkhpEg2dcevJqT7k6QRcYJF6FJfq177BDk2GWOBk,3129
132
- tests/unit/projects/test_project_members_unit.py,sha256=dtbRfP454mviQYn6Dc0Uzyb3BBIuR3Ive6g3mnUAR6M,3466
130
+ tests/unit/projects/test_project_data_connectors_unit.py,sha256=C7IPXiZ6ZhAcgLFbjkkI8JDLt13DLI6eyZZFB1i3tGE,5004
131
+ tests/unit/projects/test_project_datastores_unit.py,sha256=9s_bzl2WrEyysyCaFI77kyY9h-wDGwRnvzHvKO4OiSk,4646
132
+ tests/unit/projects/test_project_members_unit.py,sha256=J9l1sS1U4Uj4zy89xP_3ZOjz5bEklLk4sWJ51H11H3E,5097
133
133
  tests/unit/projects/test_project_secrets_unit.py,sha256=VE9L91FJodcwVGizfF65WYMiHZaF0s2AdW1aiJ3z7xA,3276
134
- tests/unit/projects/test_project_services_unit.py,sha256=PzeNuJRuAG7RkrPWX0FfgFTt6-63FviecrDY06rLQ6A,3331
135
- tests/unit/projects/test_projects_unit.py,sha256=4vMo7TF-SjduoT2ejpuyM3dULslpdqE8-c25M9X-iYc,3810
136
- proximl-0.5.16.dist-info/LICENSE,sha256=ADFxLEZDxKY0j4MdyUd5GNuhQ18rnWH5rOz1ZG7yiOA,1069
137
- proximl-0.5.16.dist-info/METADATA,sha256=Pm7udhIj-sHr1U3trGMxzbBclXIjHue5W9U-uRKxMxc,7345
138
- proximl-0.5.16.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
139
- proximl-0.5.16.dist-info/entry_points.txt,sha256=HmI311IIabkZReMCXu-nGbvIEW-KfaduAOyfiSqt5SY,63
140
- proximl-0.5.16.dist-info/top_level.txt,sha256=-TWqc9tAaxmWmW4c7uYsmzPEYUIoh6z02xxqPbv7Kys,23
141
- proximl-0.5.16.dist-info/RECORD,,
134
+ tests/unit/projects/test_project_services_unit.py,sha256=8CHd6w2koDo7cwIba6_nGuHe5wCnCRv9Q8xsv_F9I7U,6140
135
+ tests/unit/projects/test_projects_unit.py,sha256=J6nXrigjMJONSfKpNzgWOUl0CrB3GYaLmcjnale5zvQ,4409
136
+ tests/unit/utils/__init__.py,sha256=VDy3m4Lfazpey8NMzHWdgcs3T9aaXWuH7pglIjCnZW0,38
137
+ tests/unit/utils/test_transfer_unit.py,sha256=hfPv-6sTnUlwrZdxTzciaF_huXOVkyX662UIGzmO994,188309
138
+ proximl-1.0.0.dist-info/LICENSE,sha256=ADFxLEZDxKY0j4MdyUd5GNuhQ18rnWH5rOz1ZG7yiOA,1069
139
+ proximl-1.0.0.dist-info/METADATA,sha256=9jMrrMKtP_j3VHdT6wukLnztZDDRHzou0NifyfaOz3Y,7287
140
+ proximl-1.0.0.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
141
+ proximl-1.0.0.dist-info/entry_points.txt,sha256=HmI311IIabkZReMCXu-nGbvIEW-KfaduAOyfiSqt5SY,63
142
+ proximl-1.0.0.dist-info/top_level.txt,sha256=-TWqc9tAaxmWmW4c7uYsmzPEYUIoh6z02xxqPbv7Kys,23
143
+ proximl-1.0.0.dist-info/RECORD,,
@@ -23,6 +23,7 @@ class GetCheckpointTests:
23
23
  checkpoint = await checkpoint.wait_for("archived", 60)
24
24
 
25
25
  async def test_get_checkpoints(self, proximl, checkpoint):
26
+ _ = checkpoint
26
27
  checkpoints = await proximl.checkpoints.list()
27
28
  assert len(checkpoints) > 0
28
29
 
@@ -55,7 +56,7 @@ class GetCheckpointTests:
55
56
 
56
57
  @mark.create
57
58
  @mark.asyncio
58
- async def test_checkpoint_wasabi(proximl, capsys):
59
+ async def test_checkpoint_wasabi(proximl):
59
60
  checkpoint = await proximl.checkpoints.create(
60
61
  name="CLI Automated Wasabi",
61
62
  source_type="wasabi",
@@ -72,6 +73,7 @@ async def test_checkpoint_wasabi(proximl, capsys):
72
73
 
73
74
  @mark.create
74
75
  @mark.asyncio
76
+ @mark.local
75
77
  async def test_checkpoint_local(proximl, capsys):
76
78
  checkpoint = await proximl.checkpoints.create(
77
79
  name="CLI Automated Local",
@@ -81,7 +83,6 @@ async def test_checkpoint_local(proximl, capsys):
81
83
  attach_task = asyncio.create_task(checkpoint.attach())
82
84
  connect_task = asyncio.create_task(checkpoint.connect())
83
85
  await asyncio.gather(attach_task, connect_task)
84
- await checkpoint.disconnect()
85
86
  await checkpoint.refresh()
86
87
  status = checkpoint.status
87
88
  size = checkpoint.size
@@ -92,5 +93,5 @@ async def test_checkpoint_local(proximl, capsys):
92
93
  sys.stdout.write(captured.out)
93
94
  sys.stderr.write(captured.err)
94
95
  assert "Starting data upload from local" in captured.out
95
- assert "official/LICENSE 11456 bytes" in captured.out
96
+ assert "official/LICENSE" in captured.out
96
97
  assert "Upload complete" in captured.out
@@ -50,7 +50,9 @@ class GetDatasetTests:
50
50
  async def test_dataset_repr(self, dataset):
51
51
  string = repr(dataset)
52
52
  regex = (
53
- r"^Dataset\( proximl , \*\*{.*'dataset_uuid': '" + dataset.id + r"'.*}\)$"
53
+ r"^Dataset\( proximl , \*\*{.*'dataset_uuid': '"
54
+ + dataset.id
55
+ + r"'.*}\)$"
54
56
  )
55
57
  assert isinstance(string, str)
56
58
  assert re.match(regex, string)
@@ -79,6 +81,7 @@ class GetDatasetTests:
79
81
 
80
82
  @mark.create
81
83
  @mark.asyncio
84
+ @mark.local
82
85
  async def test_dataset_local(proximl, capsys):
83
86
  dataset = await proximl.datasets.create(
84
87
  name="CLI Automated Local",
@@ -88,7 +91,6 @@ async def test_dataset_local(proximl, capsys):
88
91
  attach_task = asyncio.create_task(dataset.attach())
89
92
  connect_task = asyncio.create_task(dataset.connect())
90
93
  await asyncio.gather(attach_task, connect_task)
91
- await dataset.disconnect()
92
94
  await dataset.refresh()
93
95
  status = dataset.status
94
96
  size = dataset.size
@@ -99,5 +101,5 @@ async def test_dataset_local(proximl, capsys):
99
101
  sys.stdout.write(captured.out)
100
102
  sys.stderr.write(captured.err)
101
103
  assert "Starting data upload from local" in captured.out
102
- assert "data_batch_1.bin 30733788 bytes" in captured.out
104
+ assert "data_batch_1.bin" in captured.out
103
105
  assert "Upload complete" in captured.out
@@ -46,7 +46,10 @@ class JobLifeCycleTests:
46
46
  job = await job.wait_for("running")
47
47
  assert job.status == "running"
48
48
  assert job.url
49
- assert extract_domain_suffix(urlparse(job.url).hostname) == "proximl.cloud"
49
+ assert (
50
+ extract_domain_suffix(urlparse(job.url).hostname)
51
+ == "proximl.cloud"
52
+ )
50
53
 
51
54
  async def test_stop_job(self, job):
52
55
  assert job.status == "running"
@@ -204,7 +207,8 @@ class JobAPIResourceValidationTests:
204
207
  disk_size=10,
205
208
  )
206
209
  assert (
207
- "Invalid Request - CPU Count must be a multiple of 4" in error.value.message
210
+ "Invalid Request - CPU Count must be a multiple of 4"
211
+ in error.value.message
208
212
  )
209
213
 
210
214
  async def test_invalid_gpu_count_for_cpu(self, proximl):
@@ -417,6 +421,7 @@ class JobAPIWorkerValidationTests:
417
421
  @mark.asyncio
418
422
  @mark.xdist_group("job_io")
419
423
  class JobIOTests:
424
+ @mark.local
420
425
  async def test_job_local_output(self, proximl, capsys):
421
426
  temp_dir = tempfile.TemporaryDirectory()
422
427
  job = await proximl.jobs.create(
@@ -426,7 +431,7 @@ class JobIOTests:
426
431
  disk_size=10,
427
432
  workers=["python $ML_MODEL_PATH/tensorflow/main.py"],
428
433
  environment=dict(
429
- type="DEEPLEARNING_PY312",
434
+ type="DEEPLEARNING_PY313",
430
435
  env=[
431
436
  dict(
432
437
  key="CHECKPOINT_FILE",
@@ -452,13 +457,13 @@ class JobIOTests:
452
457
  ],
453
458
  ),
454
459
  )
455
- await job.wait_for("waiting for data/model download")
460
+ # Wait for job to reach running status since only output_type is local
461
+ await job.wait_for("running")
456
462
  attach_task = asyncio.create_task(job.attach())
457
463
  connect_task = asyncio.create_task(job.connect())
458
464
  await asyncio.gather(attach_task, connect_task)
459
465
  await job.refresh()
460
466
  assert job.status == "finished"
461
- await job.disconnect()
462
467
  await job.remove()
463
468
  upload_contents = os.listdir(temp_dir.name)
464
469
  temp_dir.cleanup()
@@ -470,9 +475,8 @@ class JobIOTests:
470
475
  captured = capsys.readouterr()
471
476
  sys.stdout.write(captured.out)
472
477
  sys.stderr.write(captured.err)
473
- assert "Epoch 1/2" in captured.out
474
- assert "Epoch 2/2" in captured.out
475
- assert "adding: model.ckpt-0001" in captured.out
478
+ assert "Epoch 1/2" in captured.out or "Epoch 2/2" in captured.out
479
+ assert "model.ckpt-0001" in captured.out
476
480
  assert "Send complete" in captured.out
477
481
 
478
482
  async def test_job_model_input_and_output(self, proximl, capsys):
@@ -513,8 +517,7 @@ class JobIOTests:
513
517
  captured = capsys.readouterr()
514
518
  sys.stdout.write(captured.out)
515
519
  sys.stderr.write(captured.err)
516
- assert "Epoch 1/2" in captured.out
517
- assert "Epoch 2/2" in captured.out
520
+ assert "Epoch 1/2" in captured.out or "Epoch 2/2" in captured.out
518
521
 
519
522
  new_model = await proximl.models.get(workers[0].get("output_uuid"))
520
523
  assert new_model.id
@@ -560,9 +563,12 @@ class JobTypeTests:
560
563
  await job.wait_for("running")
561
564
  await job.refresh()
562
565
  assert job.url
563
- assert extract_domain_suffix(urlparse(job.url).hostname) == "proximl.cloud"
566
+ assert (
567
+ extract_domain_suffix(urlparse(job.url).hostname)
568
+ == "proximl.cloud"
569
+ )
564
570
  tries = 0
565
- await asyncio.sleep(180) ## downloading weights can be slow
571
+ await asyncio.sleep(180) ## downloading weights can be slow
566
572
  async with aiohttp.ClientSession() as session:
567
573
  retry = True
568
574
  while retry:
@@ -640,9 +646,11 @@ class JobTypeTests:
640
646
  captured = capsys.readouterr()
641
647
  sys.stdout.write(captured.out)
642
648
  sys.stderr.write(captured.err)
643
- assert "Epoch 1/2" in captured.out
644
- assert "Epoch 2/2" in captured.out
645
- assert "Uploading s3://proximl-example/output/resnet_cifar10" in captured.out
649
+ assert "Epoch 1/2" in captured.out or "Epoch 2/2" in captured.out
650
+ assert (
651
+ "Uploading s3://proximl-example/output/resnet_cifar10"
652
+ in captured.out
653
+ )
646
654
  assert (
647
655
  "upload: ./model.ckpt-0002.weights.h5 to s3://proximl-example/output/resnet_cifar10/model.ckpt-0002.weights.h5"
648
656
  in captured.out
@@ -680,9 +688,12 @@ class JobFeatureTests:
680
688
  captured = capsys.readouterr()
681
689
  sys.stdout.write(captured.out)
682
690
  sys.stderr.write(captured.err)
683
- assert "Train Epoch: 1 [0/60000 (0%)]" in captured.out
684
- assert "Train Epoch: 1 [59520/60000 (99%)]" in captured.out
691
+ assert (
692
+ "Train Epoch: 1 [0/60000 (0%)]" in captured.out
693
+ or "Train Epoch: 1 [59520/60000 (99%)]" in captured.out
694
+ )
685
695
 
696
+ @mark.local
686
697
  async def test_inference_job(self, proximl, capsys):
687
698
  temp_dir = tempfile.TemporaryDirectory()
688
699
  job = await proximl.jobs.create(
@@ -706,11 +717,11 @@ class JobFeatureTests:
706
717
  )
707
718
  assert job.id
708
719
  await job.wait_for("running")
709
- await job.connect()
710
- await job.attach()
720
+ attach_task = asyncio.create_task(job.attach())
721
+ connect_task = asyncio.create_task(job.connect())
722
+ await asyncio.gather(attach_task, connect_task)
711
723
  await job.refresh()
712
724
  assert job.status == "finished"
713
- await job.disconnect()
714
725
  await job.remove()
715
726
  await job.wait_for("archived")
716
727
  captured = capsys.readouterr()
@@ -719,15 +730,10 @@ class JobFeatureTests:
719
730
  upload_contents = os.listdir(temp_dir.name)
720
731
  temp_dir.cleanup()
721
732
  assert len(upload_contents) >= 3
722
- assert any(
723
- "model.ckpt-0002" in content
724
- for content in upload_contents
725
- )
733
+ assert any("model.ckpt-0002" in content for content in upload_contents)
726
734
 
727
735
  captured = capsys.readouterr()
728
736
  sys.stdout.write(captured.out)
729
737
  sys.stderr.write(captured.err)
730
- assert "Epoch 1/2" in captured.out
731
- assert "Epoch 2/2" in captured.out
732
- assert "Number of regular files transferred: 4" in captured.out
738
+ assert "Epoch 1/2" in captured.out or "Epoch 2/2" in captured.out
733
739
  assert "Send complete" in captured.out
@@ -44,7 +44,11 @@ class GetModelTests:
44
44
 
45
45
  async def test_model_repr(self, model):
46
46
  string = repr(model)
47
- regex = r"^Model\( proximl , \*\*{.*'model_uuid': '" + model.id + r"'.*}\)$"
47
+ regex = (
48
+ r"^Model\( proximl , \*\*{.*'model_uuid': '"
49
+ + model.id
50
+ + r"'.*}\)$"
51
+ )
48
52
  assert isinstance(string, str)
49
53
  assert re.match(regex, string)
50
54
 
@@ -68,6 +72,7 @@ async def test_model_wasabi(proximl, capsys):
68
72
 
69
73
  @mark.create
70
74
  @mark.asyncio
75
+ @mark.local
71
76
  async def test_model_local(proximl, capsys):
72
77
  model = await proximl.models.create(
73
78
  name="CLI Automated Local",
@@ -77,7 +82,6 @@ async def test_model_local(proximl, capsys):
77
82
  attach_task = asyncio.create_task(model.attach())
78
83
  connect_task = asyncio.create_task(model.connect())
79
84
  await asyncio.gather(attach_task, connect_task)
80
- await model.disconnect()
81
85
  await model.refresh()
82
86
  status = model.status
83
87
  size = model.size
@@ -88,5 +92,5 @@ async def test_model_local(proximl, capsys):
88
92
  sys.stdout.write(captured.out)
89
93
  sys.stderr.write(captured.err)
90
94
  assert "Starting data upload from local" in captured.out
91
- assert "official/LICENSE 11456 bytes" in captured.out
95
+ assert "official/LICENSE" in captured.out
92
96
  assert "Upload complete" in captured.out
@@ -74,6 +74,7 @@ async def test_volume_wasabi(proximl, capsys):
74
74
 
75
75
  @mark.create
76
76
  @mark.asyncio
77
+ @mark.local
77
78
  async def test_volume_local(proximl, capsys):
78
79
  volume = await proximl.volumes.create(
79
80
  name="CLI Automated Local",
@@ -84,7 +85,6 @@ async def test_volume_local(proximl, capsys):
84
85
  attach_task = asyncio.create_task(volume.attach())
85
86
  connect_task = asyncio.create_task(volume.connect())
86
87
  await asyncio.gather(attach_task, connect_task)
87
- await volume.disconnect()
88
88
  await volume.refresh()
89
89
  status = volume.status
90
90
  billed_size = volume.billed_size
@@ -97,5 +97,5 @@ async def test_volume_local(proximl, capsys):
97
97
  sys.stdout.write(captured.out)
98
98
  sys.stderr.write(captured.err)
99
99
  assert "Starting data upload from local" in captured.out
100
- assert "official/LICENSE 11456 bytes" in captured.out
100
+ assert "official/LICENSE" in captured.out
101
101
  assert "Upload complete" in captured.out