coiled 1.118.4.dev6__py3-none-any.whl → 1.129.3.dev10__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.
- coiled/batch.py +17 -1
- coiled/capture_environment.py +45 -77
- coiled/cli/batch/run.py +141 -10
- coiled/cli/batch/util.py +28 -0
- coiled/cli/batch/wait.py +57 -47
- coiled/cli/core.py +4 -0
- coiled/cli/curl.py +7 -2
- coiled/cli/file.py +116 -0
- coiled/cli/hello/hello.py +6 -5
- coiled/cli/mpi.py +252 -0
- coiled/cli/notebook/notebook.py +10 -0
- coiled/cli/run.py +53 -10
- coiled/cli/setup/aws.py +48 -12
- coiled/cli/setup/azure.py +50 -1
- coiled/context.py +2 -2
- coiled/credentials/google.py +1 -20
- coiled/filestore.py +458 -0
- coiled/plugins.py +3 -0
- coiled/pypi_conda_map.py +14 -0
- coiled/software_utils.py +140 -5
- coiled/spans.py +2 -0
- coiled/types.py +18 -1
- coiled/utils.py +65 -1
- coiled/v2/cluster.py +25 -3
- coiled/v2/cluster_comms.py +72 -0
- coiled/v2/core.py +7 -0
- {coiled-1.118.4.dev6.dist-info → coiled-1.129.3.dev10.dist-info}/METADATA +1 -1
- {coiled-1.118.4.dev6.dist-info → coiled-1.129.3.dev10.dist-info}/RECORD +31 -26
- {coiled-1.118.4.dev6.dist-info → coiled-1.129.3.dev10.dist-info}/WHEEL +1 -1
- {coiled-1.118.4.dev6.dist-info → coiled-1.129.3.dev10.dist-info}/entry_points.txt +0 -0
- {coiled-1.118.4.dev6.dist-info → coiled-1.129.3.dev10.dist-info}/licenses/LICENSE +0 -0
coiled/filestore.py
ADDED
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
import os
|
|
5
|
+
import time
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
from rich.align import Align
|
|
10
|
+
from rich.console import Group
|
|
11
|
+
from rich.prompt import Confirm
|
|
12
|
+
from rich.status import Status
|
|
13
|
+
|
|
14
|
+
import coiled
|
|
15
|
+
from coiled.cli.curl import sync_request
|
|
16
|
+
from coiled.exceptions import CoiledException
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def wait_until_complete(cluster_id, wait_for_output=True, wait_for_input=False):
|
|
20
|
+
done = False
|
|
21
|
+
attachments = None
|
|
22
|
+
timeout_at = time.monotonic() + 30
|
|
23
|
+
while not done and time.monotonic() < timeout_at:
|
|
24
|
+
attachments = FilestoreManager.get_cluster_attachments(cluster_id)
|
|
25
|
+
if not attachments:
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
done = all(
|
|
29
|
+
a["complete"] for a in attachments if ((wait_for_output and a["output"]) or (wait_for_input and a["input"]))
|
|
30
|
+
)
|
|
31
|
+
if not done:
|
|
32
|
+
time.sleep(2)
|
|
33
|
+
return attachments
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def list_files_ui(fs, name_includes=None):
|
|
37
|
+
blobs = FilestoreManager.get_download_list_with_urls(fs["id"])
|
|
38
|
+
|
|
39
|
+
if name_includes:
|
|
40
|
+
blobs = [blob for blob in blobs if name_includes in blob["relative_path"]]
|
|
41
|
+
|
|
42
|
+
for blob in blobs:
|
|
43
|
+
print(blob["relative_path"])
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def download_from_filestore_with_ui(fs, into=".", name_includes=None):
|
|
47
|
+
if fs:
|
|
48
|
+
# TODO (possible enhancement) if "has files" flag is set then make sure we do see files to download?
|
|
49
|
+
blobs = FilestoreManager.get_download_list_with_urls(fs["id"])
|
|
50
|
+
|
|
51
|
+
if name_includes:
|
|
52
|
+
blobs = [blob for blob in blobs if name_includes in blob["relative_path"]]
|
|
53
|
+
|
|
54
|
+
total_bytes = sum(blob["size"] for blob in blobs)
|
|
55
|
+
|
|
56
|
+
size_label = "Bytes"
|
|
57
|
+
size_scale = 1
|
|
58
|
+
|
|
59
|
+
if total_bytes > 10_000_000:
|
|
60
|
+
size_label = "Mb"
|
|
61
|
+
size_scale = 1_000_000
|
|
62
|
+
elif total_bytes > 10_000:
|
|
63
|
+
size_label = "Kb"
|
|
64
|
+
size_scale = 1_000
|
|
65
|
+
|
|
66
|
+
def progress_title(f=None):
|
|
67
|
+
return Group(
|
|
68
|
+
Align.left(Status(f"Downloading from cloud storage: [green]{fs['name']}[green]", spinner="dots")),
|
|
69
|
+
Align.left(f"Local directory: [green]{into}[/green]"),
|
|
70
|
+
Align.left(f"Currently downloading: [blue]{f or ''}[/blue]"),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
with coiled.utils.SimpleRichProgressPanel.from_defaults(title=progress_title()) as progress:
|
|
74
|
+
done_files = 0
|
|
75
|
+
done_bytes = 0
|
|
76
|
+
|
|
77
|
+
progress.update_progress([
|
|
78
|
+
{"label": "Files", "total": len(blobs), "completed": done_files},
|
|
79
|
+
{
|
|
80
|
+
"label": size_label,
|
|
81
|
+
"total": total_bytes / size_scale if size_scale > 1 else total_bytes,
|
|
82
|
+
"completed": done_bytes / size_scale if size_scale > 1 else done_bytes,
|
|
83
|
+
},
|
|
84
|
+
])
|
|
85
|
+
|
|
86
|
+
for blob in blobs:
|
|
87
|
+
progress.update_title(progress_title(blob["key"]))
|
|
88
|
+
|
|
89
|
+
FilestoreManager.download_from_signed_url(
|
|
90
|
+
local_path=os.path.join(into, blob["relative_path"]),
|
|
91
|
+
url=blob["url"],
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
done_files += 1
|
|
95
|
+
done_bytes += blob["size"]
|
|
96
|
+
|
|
97
|
+
progress.update_progress([
|
|
98
|
+
{"label": "Files", "total": len(blobs), "completed": done_files},
|
|
99
|
+
{
|
|
100
|
+
"label": size_label,
|
|
101
|
+
"total": total_bytes / size_scale if size_scale > 1 else total_bytes,
|
|
102
|
+
"completed": done_bytes / size_scale if size_scale > 1 else done_bytes,
|
|
103
|
+
},
|
|
104
|
+
])
|
|
105
|
+
|
|
106
|
+
progress.update_title(
|
|
107
|
+
Group(
|
|
108
|
+
Align.left(f"Downloaded from cloud storage: [green]{fs['name']}[green]"),
|
|
109
|
+
Align.left(f"Local directory: [green]{into}[/green]"),
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def upload_to_filestore_with_ui(fs, local_dir, file_buffers=None):
|
|
115
|
+
# TODO (future enhancement) send write status
|
|
116
|
+
# this is tricky because status is stored on the "attachment" object, which might not exist yet
|
|
117
|
+
# because we want to be able to upload files before cluster has been created
|
|
118
|
+
# FilestoreManager.post_fs_write_status(fs["id"], "start")
|
|
119
|
+
|
|
120
|
+
def progress_title(f=None):
|
|
121
|
+
return Group(
|
|
122
|
+
Align.left(Status(f"Uploading to cloud storage: [green]{fs['name']}[green]", spinner="dots")),
|
|
123
|
+
Align.left(f"Currently uploading: [blue]{f or ''}[/blue]"),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
files = []
|
|
127
|
+
total_bytes = None
|
|
128
|
+
|
|
129
|
+
if fs:
|
|
130
|
+
if local_dir:
|
|
131
|
+
files_from_path, total_bytes = FilestoreManager.get_files_for_upload(local_dir)
|
|
132
|
+
files.extend(files_from_path)
|
|
133
|
+
if file_buffers:
|
|
134
|
+
files.extend(file_buffers)
|
|
135
|
+
|
|
136
|
+
if files:
|
|
137
|
+
size_label = "Bytes"
|
|
138
|
+
size_scale = 1
|
|
139
|
+
|
|
140
|
+
if total_bytes and total_bytes > 10_000_000:
|
|
141
|
+
size_label = "Mb"
|
|
142
|
+
size_scale = 1_000_000
|
|
143
|
+
elif total_bytes and total_bytes > 10_000:
|
|
144
|
+
size_label = "Kb"
|
|
145
|
+
size_scale = 1_000
|
|
146
|
+
|
|
147
|
+
with coiled.utils.SimpleRichProgressPanel.from_defaults(title=progress_title()) as progress:
|
|
148
|
+
done_files = 0
|
|
149
|
+
done_bytes = 0
|
|
150
|
+
|
|
151
|
+
progress.update_progress([
|
|
152
|
+
{"label": "Files", "total": len(files), "completed": done_files},
|
|
153
|
+
{
|
|
154
|
+
"label": size_label,
|
|
155
|
+
"total": total_bytes / size_scale if size_scale > 1 else total_bytes,
|
|
156
|
+
"completed": done_bytes / size_scale if size_scale > 1 else done_bytes,
|
|
157
|
+
}
|
|
158
|
+
if total_bytes
|
|
159
|
+
else {},
|
|
160
|
+
])
|
|
161
|
+
|
|
162
|
+
# files_for_upload is type list[dict] where each dict has "relative_path" key
|
|
163
|
+
upload_info = FilestoreManager.get_signed_upload_urls(fs["id"], files_for_upload=files)
|
|
164
|
+
|
|
165
|
+
upload_urls = upload_info.get("urls")
|
|
166
|
+
existing_blobs = upload_info.get("existing")
|
|
167
|
+
|
|
168
|
+
for file in files:
|
|
169
|
+
relative_path = file.get("relative_path")
|
|
170
|
+
local_path = file.get("local_path")
|
|
171
|
+
buffer = file.get("buffer")
|
|
172
|
+
if local_path:
|
|
173
|
+
size = file.get("size")
|
|
174
|
+
skip_upload = False
|
|
175
|
+
existing_blob_info = existing_blobs.get(relative_path)
|
|
176
|
+
if existing_blob_info:
|
|
177
|
+
modified = os.path.getmtime(local_path)
|
|
178
|
+
if size == existing_blob_info["size"] and modified < existing_blob_info["modified"]:
|
|
179
|
+
skip_upload = True
|
|
180
|
+
|
|
181
|
+
if not skip_upload:
|
|
182
|
+
progress.batch_title = progress_title(local_path)
|
|
183
|
+
progress.refresh()
|
|
184
|
+
|
|
185
|
+
FilestoreManager.upload_to_signed_url(local_path, upload_urls[relative_path])
|
|
186
|
+
|
|
187
|
+
done_bytes += size
|
|
188
|
+
|
|
189
|
+
elif buffer:
|
|
190
|
+
FilestoreManager.upload_bytes_to_signed_url(buffer, upload_urls[relative_path])
|
|
191
|
+
|
|
192
|
+
done_files += 1
|
|
193
|
+
|
|
194
|
+
progress.update_progress([
|
|
195
|
+
{"label": "Files", "total": len(files), "completed": done_files},
|
|
196
|
+
{
|
|
197
|
+
"label": size_label,
|
|
198
|
+
"total": total_bytes / size_scale if size_scale > 1 else total_bytes,
|
|
199
|
+
"completed": done_bytes / size_scale if size_scale > 1 else done_bytes,
|
|
200
|
+
}
|
|
201
|
+
if total_bytes
|
|
202
|
+
else {},
|
|
203
|
+
])
|
|
204
|
+
|
|
205
|
+
progress.update_title(Align.left(f"Uploaded to cloud storage: [green]{fs['name']}[green]"))
|
|
206
|
+
|
|
207
|
+
# TODO (future enhancement) send write status
|
|
208
|
+
# FilestoreManager.post_fs_write_status(fs["id"], "finish", {"complete": True, "file_count": len(files)})
|
|
209
|
+
|
|
210
|
+
return len(files)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def upload_bytes_to_fs(fs, files):
|
|
214
|
+
def progress_title(f=None):
|
|
215
|
+
return Group(
|
|
216
|
+
Align.left(Status(f"Uploading to cloud storage: [green]{fs['name']}[green]", spinner="dots")),
|
|
217
|
+
Align.left(f"Currently uploading: [blue]{f or ''}[/blue]"),
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
if fs and files:
|
|
221
|
+
with coiled.utils.SimpleRichProgressPanel.from_defaults(title=progress_title()) as progress:
|
|
222
|
+
done_files = 0
|
|
223
|
+
|
|
224
|
+
progress.update_progress([
|
|
225
|
+
{"label": "Files", "total": len(files), "completed": done_files},
|
|
226
|
+
])
|
|
227
|
+
|
|
228
|
+
upload_info = FilestoreManager.get_signed_upload_urls(fs["id"], files_for_upload=files)
|
|
229
|
+
|
|
230
|
+
upload_urls = upload_info.get("urls")
|
|
231
|
+
existing_blobs = upload_info.get("existing")
|
|
232
|
+
|
|
233
|
+
for file in files:
|
|
234
|
+
local_path = file.get("local_path")
|
|
235
|
+
relative_path = file.get("relative_path")
|
|
236
|
+
size = file.get("size")
|
|
237
|
+
skip_upload = False
|
|
238
|
+
existing_blob_info = existing_blobs.get(relative_path)
|
|
239
|
+
if existing_blob_info:
|
|
240
|
+
modified = os.path.getmtime(local_path)
|
|
241
|
+
if size == existing_blob_info["size"] and modified < existing_blob_info["modified"]:
|
|
242
|
+
skip_upload = True
|
|
243
|
+
|
|
244
|
+
if not skip_upload:
|
|
245
|
+
progress.batch_title = progress_title(local_path)
|
|
246
|
+
progress.refresh()
|
|
247
|
+
|
|
248
|
+
FilestoreManager.upload_to_signed_url(local_path, upload_urls[relative_path])
|
|
249
|
+
|
|
250
|
+
done_files += 1
|
|
251
|
+
|
|
252
|
+
progress.update_progress([
|
|
253
|
+
{"label": "Files", "total": len(files), "completed": done_files},
|
|
254
|
+
])
|
|
255
|
+
|
|
256
|
+
progress.update_title(Align.left(f"Uploaded to cloud storage: [green]{fs['name']}[green]"))
|
|
257
|
+
|
|
258
|
+
# TODO (future enhancement) send write status
|
|
259
|
+
# FilestoreManager.post_fs_write_status(fs["id"], "finish", {"complete": True, "file_count": len(files)})
|
|
260
|
+
|
|
261
|
+
return len(files)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def clear_filestores_with_ui(filestores):
|
|
265
|
+
seen = set()
|
|
266
|
+
# see if user wants to delete files from cloud storage now that job is done and results are downloaded
|
|
267
|
+
# TODO (possible feature enhancement)
|
|
268
|
+
# distinguish filestores created for this specific job from "named" filestores made explicitly?
|
|
269
|
+
for fs in filestores:
|
|
270
|
+
if fs["id"] in seen:
|
|
271
|
+
continue
|
|
272
|
+
seen.add(fs["id"])
|
|
273
|
+
if Confirm.ask(f"Clear cloud storage for [green]{fs['name']}[/green]?", default=True):
|
|
274
|
+
FilestoreManager.clear_fs(fs["id"])
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
class FilestoreManagerWithoutHttp:
|
|
278
|
+
# code duplicated between coiled_agent.py and coiled client package
|
|
279
|
+
http2 = False
|
|
280
|
+
|
|
281
|
+
@staticmethod
|
|
282
|
+
def make_req(api_path, post=False, data=None):
|
|
283
|
+
raise NotImplementedError()
|
|
284
|
+
|
|
285
|
+
@classmethod
|
|
286
|
+
def get_filestore(cls, name=None):
|
|
287
|
+
if name:
|
|
288
|
+
return cls.make_req(f"/api/v2/filestore/name/{name}").get("filestores")
|
|
289
|
+
|
|
290
|
+
@classmethod
|
|
291
|
+
def get_or_create_filestores(cls, names, workspace, region):
|
|
292
|
+
return cls.make_req(
|
|
293
|
+
"/api/v2/filestore/list", post=True, data={"names": names, "workspace": workspace, "region": region}
|
|
294
|
+
).get("filestores")
|
|
295
|
+
|
|
296
|
+
@classmethod
|
|
297
|
+
def get_cluster_attachments(cls, cluster_id):
|
|
298
|
+
return cls.make_req(f"/api/v2/filestore/cluster/{cluster_id}").get("attachments")
|
|
299
|
+
|
|
300
|
+
@classmethod
|
|
301
|
+
def get_vm_attachments(cls, vm_role=""):
|
|
302
|
+
return cls.make_req(f"/api/v2/filestore/vm/{vm_role}").get("attachments")
|
|
303
|
+
|
|
304
|
+
@classmethod
|
|
305
|
+
def get_signed_upload_urls(cls, fs_id, files_for_upload):
|
|
306
|
+
paths = [f["relative_path"] for f in files_for_upload] # relative paths
|
|
307
|
+
return cls.make_req(f"/api/v2/filestore/fs/{fs_id}/signed-urls/upload", post=True, data={"paths": paths})
|
|
308
|
+
|
|
309
|
+
@classmethod
|
|
310
|
+
def get_download_list_with_urls(cls, fs_id):
|
|
311
|
+
return cls.make_req(f"/api/v2/filestore/fs/{fs_id}/download-with-urls").get("blobs_with_urls")
|
|
312
|
+
|
|
313
|
+
@classmethod
|
|
314
|
+
def attach_filestores_to_cluster(cls, cluster_id, attachments):
|
|
315
|
+
return cls.make_req(
|
|
316
|
+
"/api/v2/filestore/attach",
|
|
317
|
+
post=True,
|
|
318
|
+
data={
|
|
319
|
+
"cluster_id": cluster_id,
|
|
320
|
+
"attachments": attachments,
|
|
321
|
+
},
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
@classmethod
|
|
325
|
+
def post_fs_write_status(cls, fs_id, action: str, data: dict | None = None):
|
|
326
|
+
# this endpoint uses cluster auth to determine the filestore
|
|
327
|
+
cls.make_req(f"/api/v2/filestore/fs/{fs_id}/status/{action}", post=True, data=data)
|
|
328
|
+
|
|
329
|
+
@classmethod
|
|
330
|
+
def clear_fs(cls, fs_id):
|
|
331
|
+
cls.make_req(f"/api/v2/filestore/fs/{fs_id}/clear", post=True)
|
|
332
|
+
|
|
333
|
+
@staticmethod
|
|
334
|
+
def get_files_for_upload(local_dir):
|
|
335
|
+
files = []
|
|
336
|
+
total_bytes = 0
|
|
337
|
+
|
|
338
|
+
# if we're given a specific file path instead of directory, then mark that file for upload
|
|
339
|
+
if os.path.isfile(local_dir):
|
|
340
|
+
local_path = local_dir
|
|
341
|
+
local_dir = os.path.dirname(local_path)
|
|
342
|
+
relative_path = Path(os.path.relpath(local_path, local_dir)).as_posix()
|
|
343
|
+
size = os.path.getsize(local_path)
|
|
344
|
+
|
|
345
|
+
files.append({"local_path": local_path, "relative_path": relative_path, "size": size})
|
|
346
|
+
total_bytes += size
|
|
347
|
+
|
|
348
|
+
return files, total_bytes
|
|
349
|
+
|
|
350
|
+
ignore_before_ts = 0
|
|
351
|
+
if os.path.exists(os.path.join(local_dir, ".ignore-before")):
|
|
352
|
+
ignore_before_ts = os.path.getmtime(os.path.join(local_dir, ".ignore-before"))
|
|
353
|
+
|
|
354
|
+
for parent_dir, _, children in os.walk(local_dir):
|
|
355
|
+
ignore_file_list = set()
|
|
356
|
+
|
|
357
|
+
if ".ignore-list" in children:
|
|
358
|
+
with open(os.path.join(parent_dir, ".ignore-list")) as f:
|
|
359
|
+
ignore_file_list = set(f.read().split("\n"))
|
|
360
|
+
|
|
361
|
+
for child in children:
|
|
362
|
+
local_path = os.path.join(parent_dir, child)
|
|
363
|
+
|
|
364
|
+
# we use .ignore-before file so that if we're using a directory which already had files
|
|
365
|
+
# (e.g., we're using same directory for inputs and outputs)
|
|
366
|
+
# then we'll only upload new or modified files, not prior unmodified files
|
|
367
|
+
if (
|
|
368
|
+
child.startswith(".ignore")
|
|
369
|
+
or child in ignore_file_list
|
|
370
|
+
or (ignore_before_ts and os.path.getmtime(local_path) < ignore_before_ts)
|
|
371
|
+
):
|
|
372
|
+
continue
|
|
373
|
+
|
|
374
|
+
relative_path = Path(os.path.relpath(local_path, local_dir)).as_posix()
|
|
375
|
+
size = os.path.getsize(local_path)
|
|
376
|
+
|
|
377
|
+
files.append({"local_path": local_path, "relative_path": relative_path, "size": size})
|
|
378
|
+
total_bytes += size
|
|
379
|
+
return files, total_bytes
|
|
380
|
+
|
|
381
|
+
@classmethod
|
|
382
|
+
def upload_to_signed_url(cls, local_path: str, url: str):
|
|
383
|
+
with open(local_path, "rb") as f:
|
|
384
|
+
buffer = io.BytesIO(f.read())
|
|
385
|
+
cls.upload_bytes_to_signed_url(buffer=buffer, url=url)
|
|
386
|
+
|
|
387
|
+
@classmethod
|
|
388
|
+
def upload_bytes_to_signed_url(cls, buffer: io.BytesIO, url: str):
|
|
389
|
+
buffer.seek(0)
|
|
390
|
+
num_bytes = len(buffer.getvalue())
|
|
391
|
+
with httpx.Client(http2=cls.http2) as client:
|
|
392
|
+
headers = {"Content-Type": "binary/octet-stream", "Content-Length": str(num_bytes)}
|
|
393
|
+
if "blob.core.windows.net" in url:
|
|
394
|
+
headers["x-ms-blob-type"] = "BlockBlob"
|
|
395
|
+
# TODO error handling
|
|
396
|
+
client.put(
|
|
397
|
+
url,
|
|
398
|
+
# content must be set to an iterable of bytes, rather than a
|
|
399
|
+
# bytes object (like file.read()) because files >2GB need
|
|
400
|
+
# to be sent in chunks to avoid an OverflowError in the
|
|
401
|
+
# Python stdlib ssl module, and httpx will not chunk up a
|
|
402
|
+
# bytes object automatically.
|
|
403
|
+
content=buffer,
|
|
404
|
+
timeout=60,
|
|
405
|
+
headers=headers,
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
@classmethod
|
|
409
|
+
def download_from_signed_url(cls, local_path, url, max_retries=3, verbose=False):
|
|
410
|
+
# TODO (performance enhancement) check if file already exists, skip if match, warn if not
|
|
411
|
+
os.makedirs(os.path.dirname(local_path), exist_ok=True)
|
|
412
|
+
|
|
413
|
+
if verbose:
|
|
414
|
+
print(f"Downloading file from signed URL: {url} to {local_path}")
|
|
415
|
+
|
|
416
|
+
with httpx.Client(http2=cls.http2) as client:
|
|
417
|
+
for attempt in range(max_retries):
|
|
418
|
+
try:
|
|
419
|
+
with client.stream("GET", url, timeout=60) as response:
|
|
420
|
+
response.raise_for_status()
|
|
421
|
+
with open(local_path, "wb") as f:
|
|
422
|
+
for chunk in response.iter_bytes(chunk_size=8192):
|
|
423
|
+
f.write(chunk)
|
|
424
|
+
return # Success, exit function
|
|
425
|
+
except (httpx.RemoteProtocolError, httpx.ReadTimeout, httpx.ConnectError) as e:
|
|
426
|
+
if attempt < max_retries - 1:
|
|
427
|
+
wait_time = 2**attempt # Exponential backoff: 1s, 2s, 4s
|
|
428
|
+
if verbose:
|
|
429
|
+
print(
|
|
430
|
+
f"Download failed (attempt {attempt + 1}/{max_retries}): {e}. "
|
|
431
|
+
f"Retrying in {wait_time}s..."
|
|
432
|
+
)
|
|
433
|
+
time.sleep(wait_time)
|
|
434
|
+
else:
|
|
435
|
+
if verbose:
|
|
436
|
+
print(f"Download failed after {max_retries} attempts: {e}")
|
|
437
|
+
raise
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
class FilestoreManager(FilestoreManagerWithoutHttp):
|
|
441
|
+
http2 = True
|
|
442
|
+
|
|
443
|
+
@staticmethod
|
|
444
|
+
def make_req(api_path, post=False, data=None):
|
|
445
|
+
workspace = (data or {}).get("workspace")
|
|
446
|
+
with coiled.Cloud(workspace=workspace) as cloud:
|
|
447
|
+
url = f"{cloud.server}{api_path}"
|
|
448
|
+
response = sync_request(
|
|
449
|
+
cloud=cloud,
|
|
450
|
+
url=url,
|
|
451
|
+
method="post" if post else "get",
|
|
452
|
+
json=True,
|
|
453
|
+
data=data,
|
|
454
|
+
json_output=True,
|
|
455
|
+
)
|
|
456
|
+
if isinstance(response, dict) and response.get("error"):
|
|
457
|
+
raise CoiledException(f"\n\n{response['error']}")
|
|
458
|
+
return response
|
coiled/plugins.py
CHANGED
|
@@ -29,9 +29,11 @@ class DaskSchedulerWriteFiles(SchedulerPlugin):
|
|
|
29
29
|
target_dir = os.path.abspath(os.path.expanduser(target_dir))
|
|
30
30
|
if not os.path.exists(target_dir):
|
|
31
31
|
try:
|
|
32
|
+
os.makedirs(os.path.dirname(target_dir), exist_ok=True)
|
|
32
33
|
os.symlink(source_dir, target_dir)
|
|
33
34
|
except Exception:
|
|
34
35
|
logger.exception(f"Error creating symlink from {source_dir} to {target_dir}")
|
|
36
|
+
|
|
35
37
|
if self._symlink_dirs == {"/mount": "./mount"}:
|
|
36
38
|
timeout = parse_timedelta(dask.config.get("coiled.mount-bucket.timeout", "30 s"))
|
|
37
39
|
await wait_for_bucket_mounting(timeout=timeout, logger=logger)
|
|
@@ -58,6 +60,7 @@ class DaskWorkerWriteFiles(WorkerPlugin):
|
|
|
58
60
|
target_dir = os.path.abspath(os.path.expanduser(target_dir))
|
|
59
61
|
if not os.path.exists(target_dir):
|
|
60
62
|
try:
|
|
63
|
+
os.makedirs(os.path.dirname(target_dir), exist_ok=True)
|
|
61
64
|
os.symlink(source_dir, target_dir)
|
|
62
65
|
except Exception:
|
|
63
66
|
logger.exception(f"Error creating symlink from {source_dir} to {target_dir}")
|
coiled/pypi_conda_map.py
CHANGED
|
@@ -33,6 +33,8 @@ PYPI_TO_CONDA = {
|
|
|
33
33
|
"datadotworld": "datadotworld-py",
|
|
34
34
|
"datalad_container": "datalad-container",
|
|
35
35
|
"delegator.py": "delegator",
|
|
36
|
+
"dftd3": "dftd3-python",
|
|
37
|
+
"dftd4": "dftd4-python",
|
|
36
38
|
"dials_data": "dials-data",
|
|
37
39
|
"docker": "docker-py",
|
|
38
40
|
"duckdb": "python-duckdb",
|
|
@@ -78,6 +80,7 @@ PYPI_TO_CONDA = {
|
|
|
78
80
|
"kubernetes": "python-kubernetes",
|
|
79
81
|
"libaio": "python-libaio",
|
|
80
82
|
"libnacl": "libnacl-python-bindings",
|
|
83
|
+
"lmdb": "python-lmdb",
|
|
81
84
|
"matplotlib": "matplotlib-base",
|
|
82
85
|
"md_toc": "md-toc",
|
|
83
86
|
"message_ix": "message-ix",
|
|
@@ -126,14 +129,18 @@ PYPI_TO_CONDA = {
|
|
|
126
129
|
"symengine": "python-symengine",
|
|
127
130
|
"systemd-python": "python-systemd",
|
|
128
131
|
"tables": "pytables",
|
|
132
|
+
"tblite": "tblite-python",
|
|
129
133
|
"termstyle": "python-termstyle",
|
|
130
134
|
"torch": "pytorch",
|
|
135
|
+
"torch-cluster": "pytorch_cluster",
|
|
136
|
+
"torch-sparse": "pytorch_sparse",
|
|
131
137
|
"torch_geometric": "pytorch_geometric",
|
|
132
138
|
"trino": "trino-python-client",
|
|
133
139
|
"typing": "typing",
|
|
134
140
|
"typing-extensions": "typing_extensions",
|
|
135
141
|
"useDAVE": "dave",
|
|
136
142
|
"vsts": "vsts-python-api",
|
|
143
|
+
"xtb": "xtb-python",
|
|
137
144
|
"xxhash": "python-xxhash",
|
|
138
145
|
}
|
|
139
146
|
|
|
@@ -157,6 +164,8 @@ CONDA_TO_PYPI = {
|
|
|
157
164
|
"datamatrix": "python-datamatrix",
|
|
158
165
|
"dave": "useDAVE",
|
|
159
166
|
"delegator": "delegator.py",
|
|
167
|
+
"dftd3-python": "dftd3",
|
|
168
|
+
"dftd4-python": "dftd4",
|
|
160
169
|
"dials-data": "dials_data",
|
|
161
170
|
"docker-py": "docker",
|
|
162
171
|
"dye-score": "dye_score",
|
|
@@ -240,6 +249,7 @@ CONDA_TO_PYPI = {
|
|
|
240
249
|
"python-kaleido": "kaleido",
|
|
241
250
|
"python-kubernetes": "kubernetes",
|
|
242
251
|
"python-libaio": "libaio",
|
|
252
|
+
"python-lmdb": "lmdb",
|
|
243
253
|
"python-mss": "mss",
|
|
244
254
|
"python-neo": "neo",
|
|
245
255
|
"python-node-semver": "node-semver",
|
|
@@ -250,7 +260,9 @@ CONDA_TO_PYPI = {
|
|
|
250
260
|
"python-termstyle": "termstyle",
|
|
251
261
|
"python-xxhash": "xxhash",
|
|
252
262
|
"pytorch": "torch",
|
|
263
|
+
"pytorch_cluster": "torch-cluster",
|
|
253
264
|
"pytorch_geometric": "torch_geometric",
|
|
265
|
+
"pytorch_sparse": "torch-sparse",
|
|
254
266
|
"pywin32-on-windows": "pywin32",
|
|
255
267
|
"qdatamatrix": "python-qdatamatrix",
|
|
256
268
|
"qnotifications": "python-qnotifications",
|
|
@@ -265,8 +277,10 @@ CONDA_TO_PYPI = {
|
|
|
265
277
|
"setuptools-scm": "setuptools_scm",
|
|
266
278
|
"spherical_functions": "spherical-functions",
|
|
267
279
|
"sphinx_rtd_theme": "sphinx-rtd-theme",
|
|
280
|
+
"tblite-python": "tblite",
|
|
268
281
|
"trino-python-client": "trino",
|
|
269
282
|
"typing": "typing",
|
|
270
283
|
"typing_extensions": "typing-extensions",
|
|
271
284
|
"vsts-python-api": "vsts",
|
|
285
|
+
"xtb-python": "xtb",
|
|
272
286
|
}
|