proximl 0.5.3__py3-none-any.whl → 0.5.5__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.
proximl/__init__.py CHANGED
@@ -13,5 +13,5 @@ logging.basicConfig(
13
13
  logger = logging.getLogger(__name__)
14
14
 
15
15
 
16
- __version__ = "0.5.3"
16
+ __version__ = "0.5.5"
17
17
  __all__ = "ProxiML"
proximl/cli/__init__.py CHANGED
@@ -142,9 +142,7 @@ def configure(config):
142
142
  project for project in projects if project.id == active_project_id
143
143
  ]
144
144
 
145
- active_project_name = (
146
- active_project[0].name if len(active_project) else "UNSET"
147
- )
145
+ active_project_name = active_project[0].name if len(active_project) else "UNSET"
148
146
 
149
147
  click.echo(f"Current Active Project: {active_project_name}")
150
148
 
@@ -154,9 +152,7 @@ def configure(config):
154
152
  show_choices=True,
155
153
  default=active_project_name,
156
154
  )
157
- selected_project = [
158
- project for project in projects if project.name == name
159
- ]
155
+ selected_project = [project for project in projects if project.name == name]
160
156
  config.proximl.client.set_active_project(selected_project[0].id)
161
157
 
162
158
 
@@ -164,6 +160,7 @@ from proximl.cli.connection import connection
164
160
  from proximl.cli.dataset import dataset
165
161
  from proximl.cli.model import model
166
162
  from proximl.cli.checkpoint import checkpoint
163
+ from proximl.cli.volume import volume
167
164
  from proximl.cli.environment import environment
168
165
  from proximl.cli.gpu import gpu
169
166
  from proximl.cli.job import job
proximl/cli/job/create.py CHANGED
@@ -389,7 +389,7 @@ def notebook(
389
389
  ],
390
390
  case_sensitive=False,
391
391
  ),
392
- default="rtx3090",
392
+ default=["rtx3090"],
393
393
  multiple=True,
394
394
  show_default=True,
395
395
  help="GPU type.",
@@ -732,7 +732,7 @@ def training(
732
732
  ],
733
733
  case_sensitive=False,
734
734
  ),
735
- default="rtx3090",
735
+ default=["rtx3090"],
736
736
  show_default=True,
737
737
  multiple=True,
738
738
  help="GPU type.",
@@ -1099,7 +1099,7 @@ def from_json(config, attach, connect, file):
1099
1099
  ],
1100
1100
  case_sensitive=False,
1101
1101
  ),
1102
- default="rtx3090",
1102
+ default=["rtx3090"],
1103
1103
  show_default=True,
1104
1104
  multiple=True,
1105
1105
  help="GPU type.",
proximl/cli/volume.py ADDED
@@ -0,0 +1,235 @@
1
+ import click
2
+ from proximl.cli import cli, pass_config, search_by_id_name
3
+
4
+
5
+ def pretty_size(num):
6
+ if not num:
7
+ num = 0.0
8
+ s = (" B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB")
9
+ n = 0
10
+ while num > 1023:
11
+ num = num / 1024
12
+ n += 1
13
+ return f"{num:.2f} {s[n]}"
14
+
15
+
16
+ @cli.group()
17
+ @pass_config
18
+ def volume(config):
19
+ """proxiML volume commands."""
20
+ pass
21
+
22
+
23
+ @volume.command()
24
+ @click.argument("volume", type=click.STRING)
25
+ @pass_config
26
+ def attach(config, volume):
27
+ """
28
+ Attach to volume and show creation logs.
29
+
30
+ VOLUME may be specified by name or ID, but ID is preferred.
31
+ """
32
+ volumes = config.proximl.run(config.proximl.client.volumes.list())
33
+
34
+ found = search_by_id_name(volume, volumes)
35
+ if None is found:
36
+ raise click.UsageError("Cannot find specified volume.")
37
+
38
+ try:
39
+ config.proximl.run(found.attach())
40
+ return config.proximl.run(found.disconnect())
41
+ except:
42
+ try:
43
+ config.proximl.run(found.disconnect())
44
+ except:
45
+ pass
46
+ raise
47
+
48
+
49
+ @volume.command()
50
+ @click.option(
51
+ "--attach/--no-attach",
52
+ default=True,
53
+ show_default=True,
54
+ help="Auto attach to volume and show creation logs.",
55
+ )
56
+ @click.argument("volume", type=click.STRING)
57
+ @pass_config
58
+ def connect(config, volume, attach):
59
+ """
60
+ Connect local source to volume and begin upload.
61
+
62
+ VOLUME may be specified by name or ID, but ID is preferred.
63
+ """
64
+ volumes = config.proximl.run(config.proximl.client.volumes.list())
65
+
66
+ found = search_by_id_name(volume, volumes)
67
+ if None is found:
68
+ raise click.UsageError("Cannot find specified volume.")
69
+
70
+ try:
71
+ if attach:
72
+ config.proximl.run(found.connect(), found.attach())
73
+ return config.proximl.run(found.disconnect())
74
+ else:
75
+ return config.proximl.run(found.connect())
76
+ except:
77
+ try:
78
+ config.proximl.run(found.disconnect())
79
+ except:
80
+ pass
81
+ raise
82
+
83
+
84
+ @volume.command()
85
+ @click.option(
86
+ "--attach/--no-attach",
87
+ default=True,
88
+ show_default=True,
89
+ help="Auto attach to volume and show creation logs.",
90
+ )
91
+ @click.option(
92
+ "--connect/--no-connect",
93
+ default=True,
94
+ show_default=True,
95
+ help="Auto connect source and start volume creation.",
96
+ )
97
+ @click.option(
98
+ "--source",
99
+ "-s",
100
+ type=click.Choice(["local"], case_sensitive=False),
101
+ default="local",
102
+ show_default=True,
103
+ help="Dataset source type.",
104
+ )
105
+ @click.argument("name", type=click.STRING)
106
+ @click.argument("capacity", type=click.INT)
107
+ @click.argument(
108
+ "path", type=click.Path(exists=True, file_okay=False, resolve_path=True)
109
+ )
110
+ @pass_config
111
+ def create(config, attach, connect, source, name, capacity, path):
112
+ """
113
+ Create a volume.
114
+
115
+ A volume with maximum size CAPACITY is created with the specified NAME using a local source at the PATH
116
+ specified. PATH should be a local directory containing the source data for
117
+ a local source or a URI for all other source types.
118
+ """
119
+
120
+ if source == "local":
121
+ volume = config.proximl.run(
122
+ config.proximl.client.volumes.create(
123
+ name=name, source_type="local", source_uri=path, capacity=capacity
124
+ )
125
+ )
126
+
127
+ try:
128
+ if connect and attach:
129
+ config.proximl.run(volume.attach(), volume.connect())
130
+ return config.proximl.run(volume.disconnect())
131
+ elif connect:
132
+ return config.proximl.run(volume.connect())
133
+ else:
134
+ raise click.UsageError(
135
+ "Abort!\n"
136
+ "No logs to show for local sourced volume without connect."
137
+ )
138
+ except:
139
+ try:
140
+ config.proximl.run(volume.disconnect())
141
+ except:
142
+ pass
143
+ raise
144
+
145
+
146
+ @volume.command()
147
+ @click.argument("volume", type=click.STRING)
148
+ @pass_config
149
+ def disconnect(config, volume):
150
+ """
151
+ Disconnect and clean-up volume upload.
152
+
153
+ VOLUME may be specified by name or ID, but ID is preferred.
154
+ """
155
+ volumes = config.proximl.run(config.proximl.client.volumes.list())
156
+
157
+ found = search_by_id_name(volume, volumes)
158
+ if None is found:
159
+ raise click.UsageError("Cannot find specified volume.")
160
+
161
+ return config.proximl.run(found.disconnect())
162
+
163
+
164
+ @volume.command()
165
+ @pass_config
166
+ def list(config):
167
+ """List volumes."""
168
+ data = [
169
+ ["ID", "STATUS", "NAME", "CAPACITY"],
170
+ ["-" * 80, "-" * 80, "-" * 80, "-" * 80],
171
+ ]
172
+
173
+ volumes = config.proximl.run(config.proximl.client.volumes.list())
174
+
175
+ for volume in volumes:
176
+ data.append(
177
+ [
178
+ volume.id,
179
+ volume.status,
180
+ volume.name,
181
+ volume.capacity,
182
+ ]
183
+ )
184
+ for row in data:
185
+ click.echo(
186
+ "{: >38.36} {: >13.11} {: >40.38} {: >14.12}" "".format(*row),
187
+ file=config.stdout,
188
+ )
189
+
190
+
191
+ @volume.command()
192
+ @click.option(
193
+ "--force/--no-force",
194
+ default=False,
195
+ show_default=True,
196
+ help="Force removal.",
197
+ )
198
+ @click.argument("volume", type=click.STRING)
199
+ @pass_config
200
+ def remove(config, volume, force):
201
+ """
202
+ Remove a volume.
203
+
204
+ VOLUME may be specified by name or ID, but ID is preferred.
205
+ """
206
+ volumes = config.proximl.run(config.proximl.client.volumes.list())
207
+
208
+ found = search_by_id_name(volume, volumes)
209
+ if None is found:
210
+ if force:
211
+ config.proximl.run(found.client.volumes.remove(volume))
212
+ else:
213
+ raise click.UsageError("Cannot find specified volume.")
214
+
215
+ return config.proximl.run(found.remove(force=force))
216
+
217
+
218
+ @volume.command()
219
+ @click.argument("volume", type=click.STRING)
220
+ @click.argument("name", type=click.STRING)
221
+ @pass_config
222
+ def rename(config, volume, name):
223
+ """
224
+ Renames a volume.
225
+
226
+ VOLUME may be specified by name or ID, but ID is preferred.
227
+ """
228
+ try:
229
+ volume = config.proximl.run(config.proximl.client.volumes.get(volume))
230
+ if volume is None:
231
+ raise click.UsageError("Cannot find specified volume.")
232
+ except:
233
+ raise click.UsageError("Cannot find specified volume.")
234
+
235
+ return config.proximl.run(volume.rename(name=name))
proximl/exceptions.py CHANGED
@@ -97,14 +97,27 @@ class CheckpointError(ProxiMLException):
97
97
  return self._status
98
98
 
99
99
  def __repr__(self):
100
- return "CheckpointError({self.status}, {self.message})".format(
101
- self=self
102
- )
100
+ return "CheckpointError({self.status}, {self.message})".format(self=self)
103
101
 
104
102
  def __str__(self):
105
- return "CheckpointError({self.status}, {self.message})".format(
106
- self=self
107
- )
103
+ return "CheckpointError({self.status}, {self.message})".format(self=self)
104
+
105
+
106
+ class VolumeError(ProxiMLException):
107
+ def __init__(self, status, data, *args):
108
+ super().__init__(data, *args)
109
+ self._status = status
110
+ self._message = data
111
+
112
+ @property
113
+ def status(self) -> str:
114
+ return self._status
115
+
116
+ def __repr__(self):
117
+ return "VolumeError({self.status}, {self.message})".format(self=self)
118
+
119
+ def __str__(self):
120
+ return "VolumeError({self.status}, {self.message})".format(self=self)
108
121
 
109
122
 
110
123
  class ConnectionError(ProxiMLException):
@@ -130,11 +143,7 @@ class SpecificationError(ProxiMLException):
130
143
  return self._attribute
131
144
 
132
145
  def __repr__(self):
133
- return "SpecificationError({self.attribute}, {self.message})".format(
134
- self=self
135
- )
146
+ return "SpecificationError({self.attribute}, {self.message})".format(self=self)
136
147
 
137
148
  def __str__(self):
138
- return "SpecificationError({self.attribute}, {self.message})".format(
139
- self=self
140
- )
149
+ return "SpecificationError({self.attribute}, {self.message})".format(self=self)
proximl/jobs.py CHANGED
@@ -77,8 +77,7 @@ class Jobs(object):
77
77
  model=model,
78
78
  endpoint=endpoint,
79
79
  source_job_uuid=kwargs.get("source_job_uuid"),
80
- project_uuid=kwargs.get("project_uuid")
81
- or self.proximl.active_project,
80
+ project_uuid=kwargs.get("project_uuid") or self.proximl.active_project,
82
81
  )
83
82
  payload = {
84
83
  k: v
@@ -103,9 +102,7 @@ class Jobs(object):
103
102
  return job
104
103
 
105
104
  async def remove(self, id, **kwargs):
106
- await self.proximl._query(
107
- f"/job/{id}", "DELETE", dict(**kwargs, force=True)
108
- )
105
+ await self.proximl._query(f"/job/{id}", "DELETE", dict(**kwargs, force=True))
109
106
 
110
107
 
111
108
  class Job:
@@ -308,18 +305,26 @@ class Job:
308
305
  entity_type="job",
309
306
  project_uuid=self._job.get("project_uuid"),
310
307
  cidr=self.dict.get("vpn").get("cidr"),
311
- ssh_port=self._job.get("vpn").get("client").get("ssh_port")
312
- if self._job.get("vpn").get("client")
313
- else None,
314
- model_path=self._job.get("model").get("source_uri")
315
- if self._job.get("model").get("source_type") == "local"
316
- else None,
317
- input_path=self._job.get("data").get("input_uri")
318
- if self._job.get("data").get("input_type") == "local"
319
- else None,
320
- output_path=self._job.get("data").get("output_uri")
321
- if self._job.get("data").get("output_type") == "local"
322
- else None,
308
+ ssh_port=(
309
+ self._job.get("vpn").get("client").get("ssh_port")
310
+ if self._job.get("vpn").get("client")
311
+ else None
312
+ ),
313
+ model_path=(
314
+ self._job.get("model").get("source_uri")
315
+ if self._job.get("model").get("source_type") == "local"
316
+ else None
317
+ ),
318
+ input_path=(
319
+ self._job.get("data").get("input_uri")
320
+ if self._job.get("data").get("input_type") == "local"
321
+ else None
322
+ ),
323
+ output_path=(
324
+ self._job.get("data").get("output_uri")
325
+ if self._job.get("data").get("output_type") == "local"
326
+ else None
327
+ ),
323
328
  )
324
329
  return details
325
330
 
@@ -396,8 +401,7 @@ class Job:
396
401
 
397
402
  def _get_msg_handler(self, msg_handler):
398
403
  worker_numbers = {
399
- w.get("job_worker_uuid"): ind + 1
400
- for ind, w in enumerate(self._workers)
404
+ w.get("job_worker_uuid"): ind + 1 for ind, w in enumerate(self._workers)
401
405
  }
402
406
  worker_numbers["data_worker"] = 0
403
407
 
@@ -407,9 +411,7 @@ class Job:
407
411
  if msg_handler:
408
412
  msg_handler(data)
409
413
  else:
410
- timestamp = datetime.fromtimestamp(
411
- int(data.get("time")) / 1000
412
- )
414
+ timestamp = datetime.fromtimestamp(int(data.get("time")) / 1000)
413
415
  if len(self._workers) > 1:
414
416
  print(
415
417
  f"{timestamp.strftime('%m/%d/%Y, %H:%M:%S')}: Worker {data.get('worker_number')} - {data.get('msg').rstrip()}"
@@ -422,10 +424,7 @@ class Job:
422
424
  return handler
423
425
 
424
426
  async def attach(self, msg_handler=None):
425
- if (
426
- self.type == "notebook"
427
- and self.status != "waiting for data/model download"
428
- ):
427
+ if self.type == "notebook" and self.status != "waiting for data/model download":
429
428
  raise SpecificationError(
430
429
  "type",
431
430
  "Notebooks cannot be attached to after model download is complete. Use open() instead.",
@@ -442,9 +441,7 @@ class Job:
442
441
  async def copy(self, name, **kwargs):
443
442
  logging.debug(f"copy request - name: {name} ; kwargs: {kwargs}")
444
443
  if self.type != "notebook":
445
- raise SpecificationError(
446
- "job", "Only notebook job types can be copied"
447
- )
444
+ raise SpecificationError("job", "Only notebook job types can be copied")
448
445
 
449
446
  job = await self.proximl.jobs.create(
450
447
  name,
@@ -504,9 +501,7 @@ class Job:
504
501
 
505
502
  POLL_INTERVAL_MIN = 5
506
503
  POLL_INTERVAL_MAX = 60
507
- POLL_INTERVAL = max(
508
- min(timeout / 60, POLL_INTERVAL_MAX), POLL_INTERVAL_MIN
509
- )
504
+ POLL_INTERVAL = max(min(timeout / 60, POLL_INTERVAL_MAX), POLL_INTERVAL_MIN)
510
505
  retry_count = math.ceil(timeout / POLL_INTERVAL)
511
506
  count = 0
512
507
  while count < retry_count:
@@ -519,23 +514,25 @@ class Job:
519
514
  raise e
520
515
  if (
521
516
  self.status == status
522
- or (
523
- self.type == "training"
524
- and status == "finished"
525
- and self.status == "stopped"
526
- )
527
517
  or (
528
518
  status
529
519
  in [
530
520
  "waiting for GPUs",
531
521
  "waiting for resources",
532
522
  ] ## this status could be very short and the polling could miss it
533
- and self.status in ["starting", "provisioning", "running"]
523
+ and self.status
524
+ not in ["new", "waiting for GPUs", "waiting for resources"]
534
525
  )
535
526
  or (
536
527
  status
537
528
  == "waiting for data/model download" ## this status could be very short and the polling could miss it
538
- and self.status in ["starting", "provisioning", "running"]
529
+ and self.status
530
+ not in [
531
+ "new",
532
+ "waiting for GPUs",
533
+ "waiting for resources",
534
+ "waiting for data/model download",
535
+ ]
539
536
  )
540
537
  ):
541
538
  return self
proximl/proximl.py CHANGED
@@ -10,6 +10,7 @@ from proximl.auth import Auth
10
10
  from proximl.datasets import Datasets
11
11
  from proximl.models import Models
12
12
  from proximl.checkpoints import Checkpoints
13
+ from proximl.volumes import Volumes
13
14
  from proximl.jobs import Jobs
14
15
  from proximl.gpu_types import GpuTypes
15
16
  from proximl.environments import Environments
@@ -66,6 +67,7 @@ class ProxiML(object):
66
67
  self.datasets = Datasets(self)
67
68
  self.models = Models(self)
68
69
  self.checkpoints = Checkpoints(self)
70
+ self.volumes = Volumes(self)
69
71
  self.jobs = Jobs(self)
70
72
  self.gpu_types = GpuTypes(self)
71
73
  self.environments = Environments(self)
@@ -117,9 +119,7 @@ class ProxiML(object):
117
119
  )
118
120
  if params:
119
121
  if not isinstance(params, dict):
120
- raise ProxiMLException(
121
- "Query parameters must be a valid dictionary"
122
- )
122
+ raise ProxiMLException("Query parameters must be a valid dictionary")
123
123
  params = {
124
124
  k: (str(v).lower() if isinstance(v, bool) else v)
125
125
  for k, v in params.items()
@@ -155,13 +155,9 @@ class ProxiML(object):
155
155
  content_type = resp.headers.get("content-type", "")
156
156
  resp.close()
157
157
  if content_type == "application/json":
158
- raise ApiError(
159
- resp.status, json.loads(what.decode("utf8"))
160
- )
158
+ raise ApiError(resp.status, json.loads(what.decode("utf8")))
161
159
  else:
162
- raise ApiError(
163
- resp.status, {"message": what.decode("utf8")}
164
- )
160
+ raise ApiError(resp.status, {"message": what.decode("utf8")})
165
161
  results = await resp.json()
166
162
  return results
167
163
 
@@ -273,15 +269,11 @@ class ProxiML(object):
273
269
  logging.debug(f"Websocket Disconnected. Done? {done}")
274
270
  except Exception as e:
275
271
  connection_tries += 1
276
- logging.debug(
277
- f"Connection error: {traceback.format_exc()}"
278
- )
272
+ logging.debug(f"Connection error: {traceback.format_exc()}")
279
273
  if connection_tries == 5:
280
274
  raise ApiError(
281
275
  500,
282
- {
283
- "message": f"Connection error: {traceback.format_exc()}"
284
- },
276
+ {"message": f"Connection error: {traceback.format_exc()}"},
285
277
  )
286
278
 
287
279
  def set_active_project(self, project_uuid):