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.
@@ -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()