sliderule-cli 5.6.1__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.
- sliderule_cli-5.6.1.dist-info/METADATA +21 -0
- sliderule_cli-5.6.1.dist-info/RECORD +16 -0
- sliderule_cli-5.6.1.dist-info/WHEEL +5 -0
- sliderule_cli-5.6.1.dist-info/entry_points.txt +5 -0
- sliderule_cli-5.6.1.dist-info/licenses/LICENSE +12 -0
- sliderule_cli-5.6.1.dist-info/top_level.txt +4 -0
- sliderule_cluster/__init__.py +0 -0
- sliderule_cluster/main.py +194 -0
- sliderule_provisioner/__init__.py +0 -0
- sliderule_provisioner/main.py +154 -0
- sliderule_recorder/__init__.py +0 -0
- sliderule_recorder/environment.yml +12 -0
- sliderule_recorder/main.py +533 -0
- sliderule_runner/__init__.py +0 -0
- sliderule_runner/database.py +106 -0
- sliderule_runner/main.py +267 -0
sliderule_runner/main.py
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import json
|
|
3
|
+
import boto3
|
|
4
|
+
import random
|
|
5
|
+
import string
|
|
6
|
+
import argparse
|
|
7
|
+
from sliderule import sliderule
|
|
8
|
+
from .database import Database, JobState, QueuePriority, JobStatus
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
tqdm = importlib.import_module("tqdm").tqdm
|
|
13
|
+
except Exception:
|
|
14
|
+
print(f"tqdm unavailable, progress of operations will not be reported")
|
|
15
|
+
def tqdm(iterable, **kwargs):
|
|
16
|
+
return iterable
|
|
17
|
+
|
|
18
|
+
#########################################
|
|
19
|
+
# Tool
|
|
20
|
+
#########################################
|
|
21
|
+
|
|
22
|
+
class Tool:
|
|
23
|
+
|
|
24
|
+
# Constructor
|
|
25
|
+
def __init__(self, args):
|
|
26
|
+
self.args = args
|
|
27
|
+
# open database
|
|
28
|
+
self.database = Database(args.database)
|
|
29
|
+
# create sliderule session
|
|
30
|
+
self.session = sliderule.create_session(verbose=args.verbose)
|
|
31
|
+
self.session.authenticate() # gives privileges to access SlideRule Runner
|
|
32
|
+
# aws clients
|
|
33
|
+
self.s3 = boto3.client("s3", region_name="us-west-2")
|
|
34
|
+
|
|
35
|
+
# Load Remote File from S3
|
|
36
|
+
def __load_remote_file(self, bucket, key):
|
|
37
|
+
obj = self.s3.get_object(Bucket=bucket, Key=key)
|
|
38
|
+
contents = obj["Body"].read().decode("utf-8")
|
|
39
|
+
return json.loads(contents)
|
|
40
|
+
|
|
41
|
+
# Get Results for a Job Run
|
|
42
|
+
# output of job must include the following fields for this to work:
|
|
43
|
+
# {
|
|
44
|
+
# "status": <boolean status of run>,
|
|
45
|
+
# "start": <start time in seconds of run>,
|
|
46
|
+
# "stop": <stop time in seconds of run>,
|
|
47
|
+
# "outputs": [<output1>, <output2>, ... <outputN>]
|
|
48
|
+
# }
|
|
49
|
+
def __get_results(self, run_url):
|
|
50
|
+
results = []
|
|
51
|
+
bucket = run_url.split("s3://")[-1].split("/")[0]
|
|
52
|
+
prefix = "/".join(run_url.split("s3://")[-1].split("/")[1:])
|
|
53
|
+
receipt = self.__load_remote_file(bucket, f"{prefix}/receipt.json") # {"name": ..., "username": ... "args": <path to arg file>, "environment": ...}
|
|
54
|
+
args_list = self.__load_remote_file(bucket, receipt["args"])
|
|
55
|
+
for i in tqdm(range(len(args_list)), total=len(args_list), desc=f"{run_url}", unit="granule"):
|
|
56
|
+
try:
|
|
57
|
+
result = {
|
|
58
|
+
"file": f"{prefix}/result{i}.json",
|
|
59
|
+
"environment": receipt["environment"],
|
|
60
|
+
"arg": args_list[i]
|
|
61
|
+
}
|
|
62
|
+
rsps = self.__load_remote_file(bucket, f"{prefix}/result{i}.json")
|
|
63
|
+
try:
|
|
64
|
+
result |= {
|
|
65
|
+
"status": rsps["status"] and JobStatus.SUCCESS or JobStatus.FAILURE,
|
|
66
|
+
"duration": rsps["status"] and (rsps["stop"] - rsps["start"]) or 0.0,
|
|
67
|
+
"outputs": rsps["outputs"]
|
|
68
|
+
}
|
|
69
|
+
except Exception as e:
|
|
70
|
+
result |= {
|
|
71
|
+
"status": JobStatus.UNSUPPORTED,
|
|
72
|
+
"result": rsps
|
|
73
|
+
}
|
|
74
|
+
except Exception as e:
|
|
75
|
+
result |= {
|
|
76
|
+
"status": JobStatus.ERROR,
|
|
77
|
+
"error": f"{e}"
|
|
78
|
+
}
|
|
79
|
+
results.append(result)
|
|
80
|
+
return results
|
|
81
|
+
|
|
82
|
+
# Archive Database
|
|
83
|
+
def archive_database(self):
|
|
84
|
+
self.database.write(self.args.archive)
|
|
85
|
+
self.database.remove()
|
|
86
|
+
|
|
87
|
+
# Submit Job
|
|
88
|
+
def submit_job(self):
|
|
89
|
+
# pull out arguments
|
|
90
|
+
name = self.args.name
|
|
91
|
+
script_file = self.args.script
|
|
92
|
+
arguments_file = self.args.arguments
|
|
93
|
+
batch_size = self.args.batch_size
|
|
94
|
+
vcpus = self.args.vcpus
|
|
95
|
+
memory = self.args.memory
|
|
96
|
+
image = self.args.image
|
|
97
|
+
queue = self.args.queue
|
|
98
|
+
# read script
|
|
99
|
+
with open(script_file, "r") as file:
|
|
100
|
+
script = file.read()
|
|
101
|
+
# read arguments
|
|
102
|
+
with open(arguments_file, "r") as file:
|
|
103
|
+
arguments = [line.strip() for line in file.readlines()]
|
|
104
|
+
# process job in batches
|
|
105
|
+
for i in range(0, len(arguments), batch_size):
|
|
106
|
+
# build and check name
|
|
107
|
+
job_name = f"{name}_{i}"
|
|
108
|
+
if job_name in self.database.submissions:
|
|
109
|
+
unique = ''.join(random.choices(string.ascii_lowercase, k=3))
|
|
110
|
+
job_name = f"{name}_{unique}_{i}"
|
|
111
|
+
# submit & save job
|
|
112
|
+
args_list = arguments[i:i+batch_size]
|
|
113
|
+
rsps = self.session.runner.submit(name=job_name, script=script, args=args_list, optional_args={"vcpus":vcpus, "memory":memory, "image":image, "queue":queue})
|
|
114
|
+
self.database.submissions[job_name] = rsps | {"complete": False}
|
|
115
|
+
print(f"Submitted job {job_name} using script {script_file} with {len(args_list)} entries: {rsps}")
|
|
116
|
+
|
|
117
|
+
# Scrape Submissions
|
|
118
|
+
def scrape_submissions(self):
|
|
119
|
+
arg_list = []
|
|
120
|
+
for name,submission in self.database.submissions.items():
|
|
121
|
+
if self.args.name and self.args.name != name: continue
|
|
122
|
+
try:
|
|
123
|
+
if submission["complete"]:
|
|
124
|
+
print(f"Scraping {name} ...")
|
|
125
|
+
for result in submission["results"]:
|
|
126
|
+
if result["status"] in self.args.status:
|
|
127
|
+
print(f"{result["arg"]}")
|
|
128
|
+
arg_list.append(result["arg"])
|
|
129
|
+
else:
|
|
130
|
+
print(f"Skipping {name} because it is still pending")
|
|
131
|
+
except Exception as e:
|
|
132
|
+
print(f"Failed to scrape {name}: {e}")
|
|
133
|
+
if self.args.output:
|
|
134
|
+
with open(self.args.output, "w") as file:
|
|
135
|
+
for arg in arg_list:
|
|
136
|
+
file.write(f"{arg}\n")
|
|
137
|
+
|
|
138
|
+
# Get Status
|
|
139
|
+
def get_status(self):
|
|
140
|
+
queue = self.args.queue
|
|
141
|
+
for name,job in self.database.submissions.items():
|
|
142
|
+
complete = job["complete"]
|
|
143
|
+
print(f"Statusing {name} ...")
|
|
144
|
+
if not complete:
|
|
145
|
+
report = self.session.runner.queue(job_id=job["job_id"], queue=queue)["report"]
|
|
146
|
+
self.database.submissions[name]["status"] = report
|
|
147
|
+
if sum([report[s] for s in [JobState.SUBMITTED, JobState.PENDING, JobState.RUNNABLE, JobState.STARTING, JobState.RUNNING]]) == 0:
|
|
148
|
+
print(f"Job {name} complete, reading results ...")
|
|
149
|
+
self.database.submissions[name]["complete"] = True
|
|
150
|
+
self.database.submissions[name]["results"] = self.__get_results(job["run_url"])
|
|
151
|
+
else:
|
|
152
|
+
print(f"Job {name} still pending")
|
|
153
|
+
print(",".join([f"{c:>30}" for c in ["NAME"]] + [f"{c:>10}" for c in list(JobState)]))
|
|
154
|
+
for name,job in self.database.submissions.items():
|
|
155
|
+
print(",".join([f"{c:>30}" for c in [name]] + [f"{c:>10}" for c in [job["status"][state] for state in list(JobState)]]))
|
|
156
|
+
|
|
157
|
+
# Generate Report
|
|
158
|
+
def generate_report(self):
|
|
159
|
+
stats = {status.value: 0 for status in JobStatus}
|
|
160
|
+
duration = {"avg": 0.0, "total": 0.0}
|
|
161
|
+
processed = 0
|
|
162
|
+
pending = 0
|
|
163
|
+
errors = 0
|
|
164
|
+
for name,submission in self.database.submissions.items():
|
|
165
|
+
if self.args.name and self.args.name != name: continue
|
|
166
|
+
if submission["complete"]:
|
|
167
|
+
for result in submission["results"]:
|
|
168
|
+
try:
|
|
169
|
+
stats[result["status"]] += 1
|
|
170
|
+
duration["total"] += result["duration"]
|
|
171
|
+
processed += 1
|
|
172
|
+
except Exception as e:
|
|
173
|
+
errors += 1
|
|
174
|
+
else:
|
|
175
|
+
pending += 1
|
|
176
|
+
if processed > 0:
|
|
177
|
+
duration["avg"] = duration["total"] / processed
|
|
178
|
+
print("Processed:", processed)
|
|
179
|
+
print("Pending:", pending)
|
|
180
|
+
print("Errors:", errors)
|
|
181
|
+
print("Status:", json.dumps(stats, indent=2))
|
|
182
|
+
print("Duration:", json.dumps(duration, indent=2))
|
|
183
|
+
|
|
184
|
+
# Cancel Job
|
|
185
|
+
def cancel_job(self):
|
|
186
|
+
name = self.args.name
|
|
187
|
+
queue = self.args.queue
|
|
188
|
+
job_id = self.database.submissions[name]["job_id"]
|
|
189
|
+
rsps = self.session.runner.cancel(job_list=[job_id], queue=queue)
|
|
190
|
+
print(f"Cancelled submission {name}: {rsps}")
|
|
191
|
+
|
|
192
|
+
# Finish
|
|
193
|
+
def finish(self):
|
|
194
|
+
if not self.args.dryrun:
|
|
195
|
+
# save database
|
|
196
|
+
self.database.write()
|
|
197
|
+
|
|
198
|
+
#########################################
|
|
199
|
+
# Main
|
|
200
|
+
#########################################
|
|
201
|
+
|
|
202
|
+
def main():
|
|
203
|
+
|
|
204
|
+
# command line arguments
|
|
205
|
+
parser = argparse.ArgumentParser(prog="sliderule-runner", description="""SlideRule Runner""")
|
|
206
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
207
|
+
|
|
208
|
+
# options shared by every subcommand; parents= lets them appear after the command name
|
|
209
|
+
common = argparse.ArgumentParser(add_help=False)
|
|
210
|
+
common.add_argument('--database', type=Path, default=Path.home() / ".cache" / "sliderule" / "runner_database.json")
|
|
211
|
+
common.add_argument('--queue', type=QueuePriority, default=QueuePriority.DEFAULT, choices=list(QueuePriority))
|
|
212
|
+
common.add_argument('--verbose', action='store_true', default=False)
|
|
213
|
+
common.add_argument('--dryrun', action='store_true', default=False)
|
|
214
|
+
|
|
215
|
+
# archive
|
|
216
|
+
archive = subparsers.add_parser("archive", parents=[common], help="save and clear the database")
|
|
217
|
+
archive.add_argument('archive', metavar="<full path to archive file>")
|
|
218
|
+
archive.set_defaults(func=Tool.archive_database)
|
|
219
|
+
|
|
220
|
+
# submit
|
|
221
|
+
submit = subparsers.add_parser("submit", parents=[common], help="submit a job")
|
|
222
|
+
submit.add_argument('name', metavar="<name>")
|
|
223
|
+
submit.add_argument('script', metavar="<script.lua>", type=Path)
|
|
224
|
+
submit.add_argument('arguments', metavar="<arguments.txt>", type=Path)
|
|
225
|
+
submit.add_argument('--vcpus', type=int, default=4)
|
|
226
|
+
submit.add_argument('--memory', type=int, default=16000)
|
|
227
|
+
submit.add_argument('--batch_size', type=int, default=10000)
|
|
228
|
+
submit.add_argument('--image', type=str, default="sliderule:latest")
|
|
229
|
+
submit.set_defaults(func=Tool.submit_job)
|
|
230
|
+
|
|
231
|
+
# scrape
|
|
232
|
+
scrape = subparsers.add_parser("scrape", parents=[common], help="generate list of arguments from jobs with provided job status")
|
|
233
|
+
scrape.add_argument('--status', type=JobStatus, nargs='+', default=[JobStatus.FAILURE, JobStatus.UNSUPPORTED, JobStatus.ERROR])
|
|
234
|
+
scrape.add_argument('--name', type=str, default=None) # name of submission
|
|
235
|
+
scrape.add_argument('--output', type=str, default=None)
|
|
236
|
+
scrape.set_defaults(func=Tool.scrape_submissions)
|
|
237
|
+
|
|
238
|
+
# status
|
|
239
|
+
status = subparsers.add_parser("status", parents=[common], help="display status of submitted jobs")
|
|
240
|
+
status.set_defaults(func=Tool.get_status)
|
|
241
|
+
|
|
242
|
+
# report
|
|
243
|
+
report = subparsers.add_parser("report", parents=[common], help="generate report of submitted jobs")
|
|
244
|
+
report.add_argument('--name', type=str, default=None) # name of submission
|
|
245
|
+
report.set_defaults(func=Tool.generate_report)
|
|
246
|
+
|
|
247
|
+
# cancel
|
|
248
|
+
cancel = subparsers.add_parser("cancel", parents=[common], help="cancel a submitted job")
|
|
249
|
+
cancel.add_argument('--name', type=str, required=True) # name of submission
|
|
250
|
+
cancel.set_defaults(func=Tool.cancel_job)
|
|
251
|
+
|
|
252
|
+
# parse command line
|
|
253
|
+
args = parser.parse_args()
|
|
254
|
+
|
|
255
|
+
# create tool
|
|
256
|
+
tool = Tool(args)
|
|
257
|
+
|
|
258
|
+
# route command
|
|
259
|
+
try:
|
|
260
|
+
args.func(tool)
|
|
261
|
+
tool.finish() # only execute if tool function completed successfully
|
|
262
|
+
except Exception as e:
|
|
263
|
+
if args.verbose: raise
|
|
264
|
+
print(f"Unhandled error: {e}")
|
|
265
|
+
|
|
266
|
+
# running via direct invocation
|
|
267
|
+
if __name__ == "__main__": main()
|