sliderule-cli 5.6.2__tar.gz → 5.7.0__tar.gz
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.2/src/sliderule_cli.egg-info → sliderule_cli-5.7.0}/PKG-INFO +1 -1
- {sliderule_cli-5.6.2 → sliderule_cli-5.7.0/src/sliderule_cli.egg-info}/PKG-INFO +1 -1
- sliderule_cli-5.7.0/src/sliderule_cli.egg-info/scm_version.json +8 -0
- {sliderule_cli-5.6.2 → sliderule_cli-5.7.0}/src/sliderule_cluster/main.py +16 -15
- {sliderule_cli-5.6.2 → sliderule_cli-5.7.0}/src/sliderule_provisioner/main.py +25 -5
- sliderule_cli-5.7.0/src/sliderule_runner/main.py +379 -0
- sliderule_cli-5.6.2/src/sliderule_cli.egg-info/scm_version.json +0 -8
- sliderule_cli-5.6.2/src/sliderule_runner/main.py +0 -298
- {sliderule_cli-5.6.2 → sliderule_cli-5.7.0}/.gitignore +0 -0
- {sliderule_cli-5.6.2 → sliderule_cli-5.7.0}/LICENSE +0 -0
- {sliderule_cli-5.6.2 → sliderule_cli-5.7.0}/README.md +0 -0
- {sliderule_cli-5.6.2 → sliderule_cli-5.7.0}/pyproject.toml +0 -0
- {sliderule_cli-5.6.2 → sliderule_cli-5.7.0}/setup.cfg +0 -0
- {sliderule_cli-5.6.2 → sliderule_cli-5.7.0}/src/sliderule_cli.egg-info/SOURCES.txt +0 -0
- {sliderule_cli-5.6.2 → sliderule_cli-5.7.0}/src/sliderule_cli.egg-info/dependency_links.txt +0 -0
- {sliderule_cli-5.6.2 → sliderule_cli-5.7.0}/src/sliderule_cli.egg-info/entry_points.txt +0 -0
- {sliderule_cli-5.6.2 → sliderule_cli-5.7.0}/src/sliderule_cli.egg-info/requires.txt +0 -0
- {sliderule_cli-5.6.2 → sliderule_cli-5.7.0}/src/sliderule_cli.egg-info/scm_file_list.json +0 -0
- {sliderule_cli-5.6.2 → sliderule_cli-5.7.0}/src/sliderule_cli.egg-info/top_level.txt +0 -0
- {sliderule_cli-5.6.2 → sliderule_cli-5.7.0}/src/sliderule_cluster/__init__.py +0 -0
- {sliderule_cli-5.6.2 → sliderule_cli-5.7.0}/src/sliderule_provisioner/__init__.py +0 -0
- {sliderule_cli-5.6.2 → sliderule_cli-5.7.0}/src/sliderule_recorder/__init__.py +0 -0
- {sliderule_cli-5.6.2 → sliderule_cli-5.7.0}/src/sliderule_recorder/environment.yml +0 -0
- {sliderule_cli-5.6.2 → sliderule_cli-5.7.0}/src/sliderule_recorder/main.py +0 -0
- {sliderule_cli-5.6.2 → sliderule_cli-5.7.0}/src/sliderule_runner/__init__.py +0 -0
- {sliderule_cli-5.6.2 → sliderule_cli-5.7.0}/src/sliderule_runner/database.py +0 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import sys
|
|
1
2
|
import json
|
|
2
3
|
import uuid
|
|
3
4
|
import tempfile
|
|
@@ -42,10 +43,9 @@ class Tool:
|
|
|
42
43
|
|
|
43
44
|
# whoami
|
|
44
45
|
def whoami(self):
|
|
45
|
-
|
|
46
|
-
result =
|
|
47
|
-
|
|
48
|
-
return result
|
|
46
|
+
result = self.session.source("whoami", parm={}, path="/discovery", retries=0, rethrow=True)
|
|
47
|
+
self.__display_result(result, reformat=True)
|
|
48
|
+
return json.dumps(result)
|
|
49
49
|
|
|
50
50
|
# status
|
|
51
51
|
def status(self):
|
|
@@ -53,22 +53,21 @@ class Tool:
|
|
|
53
53
|
parm = {"service":self.session.service}
|
|
54
54
|
else:
|
|
55
55
|
parm = {}
|
|
56
|
-
|
|
57
|
-
result =
|
|
58
|
-
|
|
59
|
-
return result
|
|
56
|
+
result = self.session.source("status", parm=parm, path="/discovery", retries=0, rethrow=True)
|
|
57
|
+
self.__display_result(result, reformat=True)
|
|
58
|
+
return json.dumps(result)
|
|
60
59
|
|
|
61
60
|
# version
|
|
62
61
|
def version(self):
|
|
63
62
|
result = sliderule.get_version(session=self.session)
|
|
64
63
|
self.__display_result(result, reformat=True)
|
|
65
|
-
return result
|
|
64
|
+
return json.dumps(result)
|
|
66
65
|
|
|
67
66
|
# defaults
|
|
68
67
|
def defaults(self):
|
|
69
68
|
result = sliderule.source("defaults", session=self.session)
|
|
70
69
|
self.__display_result(result, reformat=True)
|
|
71
|
-
return result
|
|
70
|
+
return json.dumps(result)
|
|
72
71
|
|
|
73
72
|
# earthdata
|
|
74
73
|
def earthdata(self):
|
|
@@ -84,13 +83,13 @@ class Tool:
|
|
|
84
83
|
}.items() if v is not None })
|
|
85
84
|
result = sliderule.source("earthdata", parms, rethrow=True, session=self.session)
|
|
86
85
|
self.__display_result(result, reformat=True)
|
|
87
|
-
return result
|
|
86
|
+
return json.dumps(result)
|
|
88
87
|
|
|
89
88
|
# run
|
|
90
89
|
def run(self):
|
|
91
90
|
# get request parameters
|
|
92
|
-
if self.args.parms.startswith('
|
|
93
|
-
parms = self.args.parms
|
|
91
|
+
if self.args.parms.startswith('{'):
|
|
92
|
+
parms = json.loads(self.args.parms)
|
|
94
93
|
else:
|
|
95
94
|
parms = self.__get_parms()
|
|
96
95
|
# build output parameters (if not supplied in request)
|
|
@@ -117,7 +116,7 @@ class Tool:
|
|
|
117
116
|
break
|
|
118
117
|
# set and return results
|
|
119
118
|
result = output_path
|
|
120
|
-
self.__display_result(result, reformat=
|
|
119
|
+
self.__display_result(result, reformat=False)
|
|
121
120
|
return result
|
|
122
121
|
|
|
123
122
|
#########################################
|
|
@@ -157,7 +156,7 @@ def main():
|
|
|
157
156
|
# earthdata
|
|
158
157
|
earthdata = subparsers.add_parser("earthdata", parents=[common], help="earthdata search")
|
|
159
158
|
earthdata.add_argument('--asset', type=str, default="icesat2", help="SlideRule asset name")
|
|
160
|
-
earthdata.add_argument('--poly', type=float, nargs='+', default=None, help="closed counter-clockwise list of latitude
|
|
159
|
+
earthdata.add_argument('--poly', type=float, nargs='+', default=None, help="closed counter-clockwise list of longitude/latitude pairs defining the area of interest; e.g. lon1 lat1 lon2 lat2 lon3 lat3 ... lon1 lat1")
|
|
161
160
|
earthdata.add_argument('--geojson', type=str, default=None, help="geojson filename defining the area of interest")
|
|
162
161
|
earthdata.add_argument('--bbox', type=float, nargs='+', default=None, help="bounding box defining the area of interest; e.g. lon_ll lat_ll lon_ur lat_ur")
|
|
163
162
|
earthdata.add_argument('--t0', type=str, default=None, help="start time as an ISO datetime string YYYY-MM-DDTHH:MM:SSZ") # datetime.fromisoformat(args.t0)
|
|
@@ -166,6 +165,7 @@ def main():
|
|
|
166
165
|
earthdata.add_argument('--short_name', type=str, default=None, help="CMR dataset name")
|
|
167
166
|
earthdata.add_argument('--with_meta', action='store_true', default=False, help="return metadata along with query results")
|
|
168
167
|
earthdata.add_argument('--name_filter', type=str, default=None, help="regular expression to evaluate against resource names returned by query")
|
|
168
|
+
earthdata.add_argument('--parms', type=str, default=None, help="string or filename containing json parameters to use in request")
|
|
169
169
|
earthdata.set_defaults(func=Tool.earthdata)
|
|
170
170
|
|
|
171
171
|
# run
|
|
@@ -189,6 +189,7 @@ def main():
|
|
|
189
189
|
except Exception as e:
|
|
190
190
|
if args.verbose: raise
|
|
191
191
|
print(f"Unhandled error: {e}")
|
|
192
|
+
sys.exit(1) # returns error back to system
|
|
192
193
|
|
|
193
194
|
# running via direct invocation
|
|
194
195
|
if __name__ == "__main__": main()
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import sys
|
|
1
2
|
import json
|
|
2
3
|
import argparse
|
|
3
4
|
from datetime import datetime
|
|
@@ -69,6 +70,20 @@ class Tool:
|
|
|
69
70
|
def info(self):
|
|
70
71
|
return self.session.provisioner.info()
|
|
71
72
|
|
|
73
|
+
# S3 Access
|
|
74
|
+
def s3access(self):
|
|
75
|
+
credentials = self.session.provisioner.s3access()
|
|
76
|
+
fmt = {
|
|
77
|
+
"bash": "export {}='{}'",
|
|
78
|
+
"powershell": "$env:{}='{}'",
|
|
79
|
+
"cmd": 'set "{}={}"',
|
|
80
|
+
}[self.args.shell]
|
|
81
|
+
for name, key in [("AWS_ACCESS_KEY_ID", "access_key_id"),
|
|
82
|
+
("AWS_SECRET_ACCESS_KEY", "secret_access_key"),
|
|
83
|
+
("AWS_SESSION_TOKEN", "session_token"),
|
|
84
|
+
("AWS_CREDENTIAL_EXPIRATION", "expiration")]:
|
|
85
|
+
print(fmt.format(name, credentials[key]))
|
|
86
|
+
|
|
72
87
|
# Authenticate
|
|
73
88
|
def authenticate(self):
|
|
74
89
|
return self.session.authenticate(force_login=True)
|
|
@@ -81,7 +96,7 @@ class Tool:
|
|
|
81
96
|
def main():
|
|
82
97
|
|
|
83
98
|
# command line arguments
|
|
84
|
-
parser = argparse.ArgumentParser(prog="sliderule-
|
|
99
|
+
parser = argparse.ArgumentParser(prog="sliderule-provisioner", description="""SlideRule Provisioner""")
|
|
85
100
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
86
101
|
|
|
87
102
|
# options shared by every subcommand; parents= lets them appear after the command name
|
|
@@ -89,7 +104,6 @@ def main():
|
|
|
89
104
|
common.add_argument('--domain', type=str, default="slideruleearth.io")
|
|
90
105
|
common.add_argument('--cluster', type=str, default="developers")
|
|
91
106
|
common.add_argument('--user_service', action='store_true', default=False)
|
|
92
|
-
common.add_argument('--concise', action='store_true', default=False)
|
|
93
107
|
common.add_argument('--verbose', action='store_true', default=False)
|
|
94
108
|
common.add_argument('--timezone', type=str, default="America/New_York")
|
|
95
109
|
|
|
@@ -120,7 +134,7 @@ def main():
|
|
|
120
134
|
|
|
121
135
|
# report
|
|
122
136
|
report = subparsers.add_parser("report", parents=[common], help="report metadata of active clusters")
|
|
123
|
-
report.add_argument('--kind', type=str, default="clusters", help="'
|
|
137
|
+
report.add_argument('--kind', type=str, default="clusters", help="'clusters' for cluster report, 'test' for test report")
|
|
124
138
|
report.set_defaults(func=Tool.report)
|
|
125
139
|
|
|
126
140
|
# test
|
|
@@ -132,6 +146,11 @@ def main():
|
|
|
132
146
|
info = subparsers.add_parser("info", parents=[common], help="display information about provisioner")
|
|
133
147
|
info.set_defaults(func=Tool.info)
|
|
134
148
|
|
|
149
|
+
# s3access
|
|
150
|
+
s3access = subparsers.add_parser("s3access", parents=[common], help="sets AWS credentials by outputting shell commands to 'eval' into current environment")
|
|
151
|
+
s3access.add_argument('--shell', type=str, default="bash", choices=["bash", "powershell", "cmd"])
|
|
152
|
+
s3access.set_defaults(func=Tool.s3access)
|
|
153
|
+
|
|
135
154
|
# authenticate
|
|
136
155
|
authenticate = subparsers.add_parser("authenticate", parents=[common], help="force re-authentication of user account")
|
|
137
156
|
authenticate.set_defaults(func=Tool.authenticate)
|
|
@@ -145,10 +164,11 @@ def main():
|
|
|
145
164
|
# route command
|
|
146
165
|
try:
|
|
147
166
|
result = args.func(tool)
|
|
148
|
-
|
|
167
|
+
if result:
|
|
168
|
+
print(f'{json.dumps(result, indent=2)}')
|
|
149
169
|
except Exception as e:
|
|
150
170
|
if args.verbose: raise
|
|
151
|
-
print(f"Unhandled error: {e}")
|
|
171
|
+
print(f"Unhandled error: {e}", file=sys.stderr)
|
|
152
172
|
|
|
153
173
|
# running via direct invocation
|
|
154
174
|
if __name__ == "__main__": main()
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import sys
|
|
3
|
+
import json
|
|
4
|
+
import boto3
|
|
5
|
+
import random
|
|
6
|
+
import string
|
|
7
|
+
import argparse
|
|
8
|
+
import traceback
|
|
9
|
+
from sliderule import sliderule
|
|
10
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
11
|
+
from .database import Database, JobState, QueuePriority, JobStatus
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
tqdm = importlib.import_module("tqdm").tqdm
|
|
16
|
+
except Exception:
|
|
17
|
+
print(f"tqdm unavailable, progress of operations will not be reported")
|
|
18
|
+
def tqdm(iterable, **kwargs):
|
|
19
|
+
return iterable
|
|
20
|
+
|
|
21
|
+
#########################################
|
|
22
|
+
# Tool
|
|
23
|
+
#########################################
|
|
24
|
+
|
|
25
|
+
class Tool:
|
|
26
|
+
|
|
27
|
+
# Constructor
|
|
28
|
+
def __init__(self, args):
|
|
29
|
+
self.args = args
|
|
30
|
+
# open database
|
|
31
|
+
self.database = Database(args.database)
|
|
32
|
+
# create sliderule session
|
|
33
|
+
if args.net:
|
|
34
|
+
self.session = sliderule.create_session(domain=args.domain, verbose=args.verbose, rethrow=True)
|
|
35
|
+
self.session.authenticate() # gives privileges to access SlideRule Runner
|
|
36
|
+
# aws clients
|
|
37
|
+
self.s3 = boto3.client("s3", region_name="us-west-2")
|
|
38
|
+
|
|
39
|
+
# Split out bucket and key from url
|
|
40
|
+
def __parse_url(self, url):
|
|
41
|
+
path = url.split("s3://")[-1]
|
|
42
|
+
bucket = path.split("/")[0]
|
|
43
|
+
key = '/'.join(path.split("/")[1:])
|
|
44
|
+
return bucket, key
|
|
45
|
+
|
|
46
|
+
# Load Remote File from S3
|
|
47
|
+
def __load_remote_file(self, bucket, key, parse_json=True):
|
|
48
|
+
obj = self.s3.get_object(Bucket=bucket, Key=key)
|
|
49
|
+
contents = obj["Body"].read().decode("utf-8")
|
|
50
|
+
if parse_json:
|
|
51
|
+
try:
|
|
52
|
+
return json.loads(contents)
|
|
53
|
+
except Exception:
|
|
54
|
+
pass
|
|
55
|
+
return contents
|
|
56
|
+
|
|
57
|
+
# Get Result for a Job Run
|
|
58
|
+
# output of job must include the following fields for this to work:
|
|
59
|
+
# {
|
|
60
|
+
# "status": <boolean status of run>,
|
|
61
|
+
# "start": <start time in seconds of run>,
|
|
62
|
+
# "stop": <stop time in seconds of run>,
|
|
63
|
+
# "outputs": [<output1>, <output2>, ... <outputN>]
|
|
64
|
+
# }
|
|
65
|
+
def __get_result(self, results, i):
|
|
66
|
+
result = results[i]
|
|
67
|
+
bucket, result_file = self.__parse_url(result["file"])
|
|
68
|
+
try:
|
|
69
|
+
rsps = self.__load_remote_file(bucket, result_file)
|
|
70
|
+
try:
|
|
71
|
+
result |= {
|
|
72
|
+
"status": rsps["status"] and JobStatus.SUCCESS or JobStatus.FAILURE,
|
|
73
|
+
"duration": rsps["status"] and (rsps["stop"] - rsps["start"]) or 0.0,
|
|
74
|
+
"outputs": rsps["outputs"]
|
|
75
|
+
}
|
|
76
|
+
except Exception as e:
|
|
77
|
+
result |= {
|
|
78
|
+
"status": JobStatus.UNSUPPORTED,
|
|
79
|
+
"result": rsps,
|
|
80
|
+
"error": f"{e}"
|
|
81
|
+
}
|
|
82
|
+
except Exception as e:
|
|
83
|
+
result |= {
|
|
84
|
+
"status": JobStatus.ERROR,
|
|
85
|
+
"error": f"{e}"
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
# Get all results for a submission
|
|
89
|
+
def __get_results(self, run_url, job_size):
|
|
90
|
+
bucket, prefix = self.__parse_url(run_url)
|
|
91
|
+
receipt = self.__load_remote_file(bucket, f"{prefix}/receipt.json") # {"name": ..., "username": ... "args": <path to arg file>, "environment": ...}
|
|
92
|
+
args_bucket, args_file = self.__parse_url(receipt["args"])
|
|
93
|
+
args_content = self.__load_remote_file(args_bucket, args_file, job_size > 1)
|
|
94
|
+
if isinstance(args_content, list):
|
|
95
|
+
args_list = args_content
|
|
96
|
+
else:
|
|
97
|
+
args_list = [args_content]
|
|
98
|
+
if len(args_list) != job_size:
|
|
99
|
+
print(f"Incorrect number of arguments {len(args_list)}, expected {job_size}")
|
|
100
|
+
return None
|
|
101
|
+
results = [{
|
|
102
|
+
"file": f"s3://{bucket}/{prefix}/result{len(args_list) > 1 and str(i) or ''}.json",
|
|
103
|
+
"environment": receipt["environment"],
|
|
104
|
+
"arg": args_list[i]
|
|
105
|
+
} for i in range(len(args_list))]
|
|
106
|
+
with ThreadPoolExecutor(max_workers=self.args.concurrency) as executor:
|
|
107
|
+
futures = [executor.submit(self.__get_result, results, i) for i in range(len(results))]
|
|
108
|
+
for future in tqdm(as_completed(futures), total=len(results), desc=f"{run_url}", unit="job"):
|
|
109
|
+
future.result()
|
|
110
|
+
return results
|
|
111
|
+
|
|
112
|
+
# Archive Database
|
|
113
|
+
def archive_database(self):
|
|
114
|
+
self.database.write(self.args.archive)
|
|
115
|
+
self.database.remove()
|
|
116
|
+
|
|
117
|
+
# Prune Database
|
|
118
|
+
def prune_database(self):
|
|
119
|
+
name = self.args.name
|
|
120
|
+
if name in self.database.submissions:
|
|
121
|
+
print(f"Pruning submission {name} from database")
|
|
122
|
+
del self.database.submissions[name]
|
|
123
|
+
else:
|
|
124
|
+
print(f"Unable to find submission {name} in database")
|
|
125
|
+
|
|
126
|
+
# Submit Job
|
|
127
|
+
def submit_job(self):
|
|
128
|
+
# pull out arguments
|
|
129
|
+
name = self.args.name
|
|
130
|
+
script_file = self.args.script
|
|
131
|
+
arguments_input = self.args.arguments
|
|
132
|
+
arg_as_str = self.args.arg_as_str
|
|
133
|
+
batch_size = self.args.batch_size
|
|
134
|
+
vcpus = self.args.vcpus
|
|
135
|
+
memory = self.args.memory
|
|
136
|
+
image = self.args.image
|
|
137
|
+
queue = self.args.queue
|
|
138
|
+
secrets_file = self.args.secrets
|
|
139
|
+
# read script
|
|
140
|
+
with open(script_file, "r") as file:
|
|
141
|
+
script = file.read()
|
|
142
|
+
# read arguments
|
|
143
|
+
if arg_as_str:
|
|
144
|
+
arguments = [str(arguments_input)]
|
|
145
|
+
else:
|
|
146
|
+
with open(arguments_input, "r") as file:
|
|
147
|
+
arguments = [line.strip() for line in file.readlines() if line.strip()]
|
|
148
|
+
# read secrets
|
|
149
|
+
if secrets_file:
|
|
150
|
+
with open(secrets_file, "r") as file:
|
|
151
|
+
secret_values = json.load(file)
|
|
152
|
+
else:
|
|
153
|
+
secret_values = None
|
|
154
|
+
# process job in batches
|
|
155
|
+
for i in range(0, len(arguments), batch_size):
|
|
156
|
+
# build and check name
|
|
157
|
+
job_name = f"{name}_{i}"
|
|
158
|
+
if job_name in self.database.submissions:
|
|
159
|
+
if self.args.dups_ok:
|
|
160
|
+
unique = ''.join(random.choices(string.ascii_lowercase, k=3))
|
|
161
|
+
job_name = f"{name}_{unique}_{i}"
|
|
162
|
+
else:
|
|
163
|
+
print(f"Skipping duplicate submission: {job_name}")
|
|
164
|
+
continue
|
|
165
|
+
# submit & save job
|
|
166
|
+
args_list = arguments[i:i+batch_size]
|
|
167
|
+
rsps = self.session.runner.submit(name=job_name, script=script, args=args_list, optional_args={"vcpus":vcpus, "memory":memory, "image":image, "queue":queue}, secret_values=secret_values)
|
|
168
|
+
self.database.submissions[job_name] = rsps | {"complete": False, "job_size": len(args_list)}
|
|
169
|
+
print(f"Submitted job {job_name} using script {script_file} with {len(args_list)} entries: {rsps}")
|
|
170
|
+
|
|
171
|
+
# Scrape Submissions
|
|
172
|
+
def scrape_submissions(self):
|
|
173
|
+
output_list = []
|
|
174
|
+
for name,submission in self.database.submissions.items():
|
|
175
|
+
if self.args.name and self.args.name != name: continue
|
|
176
|
+
try:
|
|
177
|
+
if submission["complete"]:
|
|
178
|
+
print(f"Scraping {name} ...")
|
|
179
|
+
for i in range(len(submission["results"])):
|
|
180
|
+
result = submission["results"][i]
|
|
181
|
+
if result["status"] in self.args.status:
|
|
182
|
+
if self.args.verbose:
|
|
183
|
+
print(f'{i:-5d}: {result}\n')
|
|
184
|
+
output_list.append(json.dumps(result))
|
|
185
|
+
else:
|
|
186
|
+
print(f'{i:-5d}: {result["arg"]}')
|
|
187
|
+
output_list.append(result["arg"])
|
|
188
|
+
else:
|
|
189
|
+
print(f"Skipping {name} because it is still pending")
|
|
190
|
+
except Exception as e:
|
|
191
|
+
print(f"Failed to scrape {name}: {e}")
|
|
192
|
+
if self.args.output:
|
|
193
|
+
with open(self.args.output, "w") as file:
|
|
194
|
+
for output in output_list:
|
|
195
|
+
file.write(f"{output}\n")
|
|
196
|
+
|
|
197
|
+
# Get Status
|
|
198
|
+
def get_status(self):
|
|
199
|
+
queue = self.args.queue
|
|
200
|
+
for name,job in self.database.submissions.items():
|
|
201
|
+
complete = job["complete"]
|
|
202
|
+
sys.stdout.write(f"Statusing {name} ... ")
|
|
203
|
+
sys.stdout.flush()
|
|
204
|
+
if not complete:
|
|
205
|
+
if job["job_size"] > 1: # array processing
|
|
206
|
+
queue_status = self.session.runner.queue(job_id=job["job_id"], queue=queue, verbose=True)
|
|
207
|
+
child_jobs = queue_status["jobs"]
|
|
208
|
+
self.database.submissions[name]["jobs"] = {}
|
|
209
|
+
for child_job in child_jobs:
|
|
210
|
+
self.database.submissions[name]["jobs"][int(child_job["index"])] = child_job
|
|
211
|
+
else: # single job
|
|
212
|
+
queue_status = self.session.runner.queue(job_id=job["job_id"], queue=queue, verbose=False)
|
|
213
|
+
# report
|
|
214
|
+
report = queue_status["report"]
|
|
215
|
+
self.database.submissions[name]["status"] = report
|
|
216
|
+
jobs_in_progress = sum([report[s] for s in [JobState.SUBMITTED, JobState.PENDING, JobState.RUNNABLE, JobState.STARTING, JobState.RUNNING]])
|
|
217
|
+
jobs_complete = sum([report[s] for s in [JobState.SUCCEEDED, JobState.FAILED]])
|
|
218
|
+
if jobs_in_progress == 0 and jobs_complete > 0:
|
|
219
|
+
print("complete")
|
|
220
|
+
results = self.__get_results(job["run_url"], self.database.submissions[name]["job_size"])
|
|
221
|
+
if results:
|
|
222
|
+
self.database.submissions[name]["results"] = results
|
|
223
|
+
self.database.submissions[name]["complete"] = True
|
|
224
|
+
else:
|
|
225
|
+
print(f"still pending")
|
|
226
|
+
else:
|
|
227
|
+
print("") # flush with new line
|
|
228
|
+
if self.args.verbose:
|
|
229
|
+
print(",".join([f"{c:>30}" for c in ["NAME"]] + [f"{c:>10}" for c in ["INDEX", "STATUS"]]))
|
|
230
|
+
for name,job in self.database.submissions.items():
|
|
231
|
+
for index, child_job in job["jobs"].items():
|
|
232
|
+
print(",".join([f"{c:>30}" for c in [name]] + [f"{c:>10}" for c in [index, child_job["status"]]]))
|
|
233
|
+
else:
|
|
234
|
+
print(",".join([f"{c:>30}" for c in ["NAME"]] + [f"{c:>10}" for c in list(JobState)]))
|
|
235
|
+
for name,job in self.database.submissions.items():
|
|
236
|
+
print(",".join([f"{c:>30}" for c in [name]] + [f"{c:>10}" for c in [job["status"][state] for state in list(JobState)]]))
|
|
237
|
+
|
|
238
|
+
# Generate Report
|
|
239
|
+
def generate_report(self):
|
|
240
|
+
print(",".join([f"{c:>30}" for c in ["name"]] + [f"{c:>12}" for c in list(JobStatus)] + [f"{c:>12}" for c in ["duration"]]))
|
|
241
|
+
for name,submission in self.database.submissions.items():
|
|
242
|
+
if self.args.name and self.args.name != name: continue
|
|
243
|
+
stats = {status.value: 0 for status in JobStatus}
|
|
244
|
+
duration = {"avg": 0.0, "total": 0.0, "cnt": 0}
|
|
245
|
+
if submission["complete"]:
|
|
246
|
+
for result in submission["results"]:
|
|
247
|
+
stats[result["status"]] += 1
|
|
248
|
+
if ("duration" in result) and (result["status"] == JobStatus.SUCCESS):
|
|
249
|
+
duration["total"] += result["duration"]
|
|
250
|
+
duration["cnt"] += 1
|
|
251
|
+
if duration["cnt"] > 0:
|
|
252
|
+
duration["avg"] = duration["total"] / duration["cnt"]
|
|
253
|
+
print(",".join([f"{c:>30}" for c in [name]] + [f"{c:>12}" for c in [stats[status] for status in list(JobStatus)]] + [f"{c:>12.3}" for c in [duration["avg"]]]))
|
|
254
|
+
|
|
255
|
+
# Cancel Job
|
|
256
|
+
def cancel_job(self):
|
|
257
|
+
name = self.args.name
|
|
258
|
+
queue = self.args.queue
|
|
259
|
+
job_id = self.database.submissions[name]["job_id"]
|
|
260
|
+
rsps = self.session.runner.cancel(job_list=[job_id], queue=queue)
|
|
261
|
+
print(f"Cancelled submission {name}: {rsps}")
|
|
262
|
+
|
|
263
|
+
# Get Logs
|
|
264
|
+
def get_logs(self):
|
|
265
|
+
name = self.args.name
|
|
266
|
+
index = self.args.index
|
|
267
|
+
if index is not None:
|
|
268
|
+
job_id = self.database.submissions[name]["jobs"][str(index)]["job_id"]
|
|
269
|
+
else:
|
|
270
|
+
job_id = self.database.submissions[name]["job_id"]
|
|
271
|
+
events = self.session.runner.logs(job_id=job_id)
|
|
272
|
+
for event in events:
|
|
273
|
+
print("->", event)
|
|
274
|
+
|
|
275
|
+
# Finish
|
|
276
|
+
def finish(self):
|
|
277
|
+
if not self.args.ro:
|
|
278
|
+
# save database
|
|
279
|
+
self.database.write()
|
|
280
|
+
|
|
281
|
+
#########################################
|
|
282
|
+
# Main
|
|
283
|
+
#########################################
|
|
284
|
+
|
|
285
|
+
def main():
|
|
286
|
+
|
|
287
|
+
# ----------------------------------------------------------------------------
|
|
288
|
+
# The sliderule-runner has a set of commands along with common options. Each
|
|
289
|
+
# command has three attributes:
|
|
290
|
+
# func - the function to call when that subcommand is issued
|
|
291
|
+
# ro - "read-only", i.e. is the database written to or only read from
|
|
292
|
+
# net - "network", i.e. does a sliderule session need to be created
|
|
293
|
+
# ----------------------------------------------------------------------------
|
|
294
|
+
|
|
295
|
+
# command line arguments
|
|
296
|
+
parser = argparse.ArgumentParser(prog="sliderule-runner", description="""SlideRule Runner""")
|
|
297
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
298
|
+
|
|
299
|
+
# options shared by every subcommand; parents= lets them appear after the command name
|
|
300
|
+
common = argparse.ArgumentParser(add_help=False)
|
|
301
|
+
common.add_argument('--domain', type=str, default="slideruleearth.io")
|
|
302
|
+
common.add_argument('--database', type=Path, default=Path.home() / ".cache" / "sliderule" / "runner_database.json")
|
|
303
|
+
common.add_argument('--queue', type=QueuePriority, default=QueuePriority.DEFAULT, choices=list(QueuePriority))
|
|
304
|
+
common.add_argument('--verbose', action='store_true', default=False)
|
|
305
|
+
|
|
306
|
+
# archive
|
|
307
|
+
archive = subparsers.add_parser("archive", parents=[common], help="save and clear the database")
|
|
308
|
+
archive.add_argument('archive', metavar="<full path to archive file>")
|
|
309
|
+
archive.set_defaults(func=Tool.archive_database, ro=True, net=False)
|
|
310
|
+
|
|
311
|
+
# prune
|
|
312
|
+
prune = subparsers.add_parser("prune", parents=[common], help="remove a submission from the database")
|
|
313
|
+
prune.add_argument('name', type=str, metavar="<name>")
|
|
314
|
+
prune.set_defaults(func=Tool.prune_database, ro=False, net=False)
|
|
315
|
+
|
|
316
|
+
# submit
|
|
317
|
+
submit = subparsers.add_parser("submit", parents=[common], help="submit a job")
|
|
318
|
+
submit.add_argument('name', type=str, metavar="<name>")
|
|
319
|
+
submit.add_argument('script', type=str, metavar="<script file>")
|
|
320
|
+
submit.add_argument('arguments', type=str, metavar="<arguments file>")
|
|
321
|
+
submit.add_argument('--arg_as_str', action='store_true', default=False) # changes arguments to be just a string instead of a file
|
|
322
|
+
submit.add_argument('--dups_ok', action='store_true', default=False) # allow a duplicate submission (will provide random unique qualitfier to name)
|
|
323
|
+
submit.add_argument('--vcpus', type=int, default=4)
|
|
324
|
+
submit.add_argument('--memory', type=int, default=16000)
|
|
325
|
+
submit.add_argument('--batch_size', type=int, default=10000)
|
|
326
|
+
submit.add_argument('--image', type=str, default="pysr:latest")
|
|
327
|
+
submit.add_argument('--secrets', type=Path, default=None) # contents of file must be json
|
|
328
|
+
submit.set_defaults(func=Tool.submit_job, ro=False, net=True)
|
|
329
|
+
|
|
330
|
+
# scrape
|
|
331
|
+
scrape = subparsers.add_parser("scrape", parents=[common], help="generate list of arguments from jobs with provided job status")
|
|
332
|
+
scrape.add_argument('--status', type=JobStatus, nargs='+', default=[JobStatus.FAILURE, JobStatus.UNSUPPORTED, JobStatus.ERROR])
|
|
333
|
+
scrape.add_argument('--name', type=str, default=None) # name of submission
|
|
334
|
+
scrape.add_argument('--output', type=str, default=None)
|
|
335
|
+
scrape.set_defaults(func=Tool.scrape_submissions, ro=True, net=False)
|
|
336
|
+
|
|
337
|
+
# status
|
|
338
|
+
status = subparsers.add_parser("status", parents=[common], help="display status of submitted jobs")
|
|
339
|
+
status.add_argument('--concurrency',type=int, default=20)
|
|
340
|
+
status.set_defaults(func=Tool.get_status, ro=False, net=True)
|
|
341
|
+
|
|
342
|
+
# report
|
|
343
|
+
report = subparsers.add_parser("report", parents=[common], help="generate report of completed jobs")
|
|
344
|
+
report.add_argument('--name', type=str, default=None) # name of submission
|
|
345
|
+
report.set_defaults(func=Tool.generate_report, ro=True, net=False)
|
|
346
|
+
|
|
347
|
+
# cancel
|
|
348
|
+
cancel = subparsers.add_parser("cancel", parents=[common], help="cancel a submitted job")
|
|
349
|
+
cancel.add_argument('--name', type=str, required=True) # name of submission
|
|
350
|
+
cancel.set_defaults(func=Tool.cancel_job, ro=True, net=True)
|
|
351
|
+
|
|
352
|
+
# logs
|
|
353
|
+
logs = subparsers.add_parser("logs", parents=[common], help="get log messages for a child job")
|
|
354
|
+
logs.add_argument('--name', type=str, required=True) # name of submission
|
|
355
|
+
logs.add_argument('--index', type=int, default=None) # job index
|
|
356
|
+
logs.set_defaults(func=Tool.get_logs, ro=True, net=True)
|
|
357
|
+
|
|
358
|
+
# parse command line
|
|
359
|
+
args = parser.parse_args()
|
|
360
|
+
|
|
361
|
+
# create tool
|
|
362
|
+
tool = Tool(args)
|
|
363
|
+
|
|
364
|
+
# route command
|
|
365
|
+
exit_status = 0
|
|
366
|
+
try:
|
|
367
|
+
args.func(tool)
|
|
368
|
+
except Exception as e:
|
|
369
|
+
print(f"Unhandled error: {e}")
|
|
370
|
+
traceback.print_exc()
|
|
371
|
+
exit_status = 1
|
|
372
|
+
finally:
|
|
373
|
+
tool.finish()
|
|
374
|
+
|
|
375
|
+
# return back to caller
|
|
376
|
+
sys.exit(exit_status)
|
|
377
|
+
|
|
378
|
+
# running via direct invocation
|
|
379
|
+
if __name__ == "__main__": main()
|
|
@@ -1,298 +0,0 @@
|
|
|
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 i in range(len(submission["results"])):
|
|
126
|
-
result = submission["results"][i]
|
|
127
|
-
if result["status"] in self.args.status:
|
|
128
|
-
print(f'{i:-5d}: {result["arg"]}')
|
|
129
|
-
arg_list.append(result["arg"])
|
|
130
|
-
else:
|
|
131
|
-
print(f"Skipping {name} because it is still pending")
|
|
132
|
-
except Exception as e:
|
|
133
|
-
print(f"Failed to scrape {name}: {e}")
|
|
134
|
-
if self.args.output:
|
|
135
|
-
with open(self.args.output, "w") as file:
|
|
136
|
-
for arg in arg_list:
|
|
137
|
-
file.write(f"{arg}\n")
|
|
138
|
-
|
|
139
|
-
# Get Status
|
|
140
|
-
def get_status(self):
|
|
141
|
-
queue = self.args.queue
|
|
142
|
-
for name,job in self.database.submissions.items():
|
|
143
|
-
complete = job["complete"]
|
|
144
|
-
print(f"Statusing {name} ...")
|
|
145
|
-
if not complete:
|
|
146
|
-
queue_status = self.session.runner.queue(job_id=job["job_id"], queue=queue, verbose=True)
|
|
147
|
-
# jobs
|
|
148
|
-
child_jobs = queue_status["jobs"]
|
|
149
|
-
self.database.submissions[name]["jobs"] = {}
|
|
150
|
-
for child_job in child_jobs:
|
|
151
|
-
self.database.submissions[name]["jobs"][int(child_job["index"])] = child_job
|
|
152
|
-
# report
|
|
153
|
-
report = queue_status["report"]
|
|
154
|
-
self.database.submissions[name]["status"] = report
|
|
155
|
-
jobs_in_progress = sum([report[s] for s in [JobState.SUBMITTED, JobState.PENDING, JobState.RUNNABLE, JobState.STARTING, JobState.RUNNING]])
|
|
156
|
-
jobs_complete = sum([report[s] for s in [JobState.SUCCEEDED, JobState.FAILED]])
|
|
157
|
-
if jobs_in_progress == 0 and jobs_complete > 0:
|
|
158
|
-
print(f"Job {name} complete, reading results ...")
|
|
159
|
-
self.database.submissions[name]["complete"] = True
|
|
160
|
-
self.database.submissions[name]["results"] = self.__get_results(job["run_url"])
|
|
161
|
-
else:
|
|
162
|
-
print(f"Job {name} still pending")
|
|
163
|
-
if self.args.verbose:
|
|
164
|
-
print(",".join([f"{c:>30}" for c in ["NAME"]] + [f"{c:>10}" for c in ["INDEX", "STATUS"]]))
|
|
165
|
-
for name,job in self.database.submissions.items():
|
|
166
|
-
for index, child_job in job["jobs"].items():
|
|
167
|
-
print(",".join([f"{c:>30}" for c in [name]] + [f"{c:>10}" for c in [index, child_job["status"]]]))
|
|
168
|
-
else:
|
|
169
|
-
print(",".join([f"{c:>30}" for c in ["NAME"]] + [f"{c:>10}" for c in list(JobState)]))
|
|
170
|
-
for name,job in self.database.submissions.items():
|
|
171
|
-
print(",".join([f"{c:>30}" for c in [name]] + [f"{c:>10}" for c in [job["status"][state] for state in list(JobState)]]))
|
|
172
|
-
|
|
173
|
-
# Generate Report
|
|
174
|
-
def generate_report(self):
|
|
175
|
-
stats = {status.value: 0 for status in JobStatus}
|
|
176
|
-
duration = {"avg": 0.0, "total": 0.0}
|
|
177
|
-
processed = 0
|
|
178
|
-
pending = 0
|
|
179
|
-
errors = 0
|
|
180
|
-
for name,submission in self.database.submissions.items():
|
|
181
|
-
if self.args.name and self.args.name != name: continue
|
|
182
|
-
if submission["complete"]:
|
|
183
|
-
for result in submission["results"]:
|
|
184
|
-
try:
|
|
185
|
-
stats[result["status"]] += 1
|
|
186
|
-
duration["total"] += result["duration"]
|
|
187
|
-
processed += 1
|
|
188
|
-
except Exception as e:
|
|
189
|
-
errors += 1
|
|
190
|
-
else:
|
|
191
|
-
pending += 1
|
|
192
|
-
if processed > 0:
|
|
193
|
-
duration["avg"] = duration["total"] / processed
|
|
194
|
-
print("Processed:", processed)
|
|
195
|
-
print("Pending:", pending)
|
|
196
|
-
print("Errors:", errors)
|
|
197
|
-
print("Status:", json.dumps(stats, indent=2))
|
|
198
|
-
print("Duration:", json.dumps(duration, indent=2))
|
|
199
|
-
|
|
200
|
-
# Cancel Job
|
|
201
|
-
def cancel_job(self):
|
|
202
|
-
name = self.args.name
|
|
203
|
-
queue = self.args.queue
|
|
204
|
-
job_id = self.database.submissions[name]["job_id"]
|
|
205
|
-
rsps = self.session.runner.cancel(job_list=[job_id], queue=queue)
|
|
206
|
-
print(f"Cancelled submission {name}: {rsps}")
|
|
207
|
-
|
|
208
|
-
# Get Logs
|
|
209
|
-
def get_logs(self):
|
|
210
|
-
name = self.args.name
|
|
211
|
-
index = self.args.index
|
|
212
|
-
job_id = self.database.submissions[name]["jobs"][str(index)]["job_id"]
|
|
213
|
-
events = self.session.runner.logs(job_id=job_id)
|
|
214
|
-
for event in events:
|
|
215
|
-
print(event)
|
|
216
|
-
|
|
217
|
-
# Finish
|
|
218
|
-
def finish(self):
|
|
219
|
-
if not self.args.dryrun and "archive" not in self.args:
|
|
220
|
-
# save database
|
|
221
|
-
self.database.write()
|
|
222
|
-
|
|
223
|
-
#########################################
|
|
224
|
-
# Main
|
|
225
|
-
#########################################
|
|
226
|
-
|
|
227
|
-
def main():
|
|
228
|
-
|
|
229
|
-
# command line arguments
|
|
230
|
-
parser = argparse.ArgumentParser(prog="sliderule-runner", description="""SlideRule Runner""")
|
|
231
|
-
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
232
|
-
|
|
233
|
-
# options shared by every subcommand; parents= lets them appear after the command name
|
|
234
|
-
common = argparse.ArgumentParser(add_help=False)
|
|
235
|
-
common.add_argument('--database', type=Path, default=Path.home() / ".cache" / "sliderule" / "runner_database.json")
|
|
236
|
-
common.add_argument('--queue', type=QueuePriority, default=QueuePriority.DEFAULT, choices=list(QueuePriority))
|
|
237
|
-
common.add_argument('--verbose', action='store_true', default=False)
|
|
238
|
-
common.add_argument('--dryrun', action='store_true', default=False)
|
|
239
|
-
|
|
240
|
-
# archive
|
|
241
|
-
archive = subparsers.add_parser("archive", parents=[common], help="save and clear the database")
|
|
242
|
-
archive.add_argument('archive', metavar="<full path to archive file>")
|
|
243
|
-
archive.set_defaults(func=Tool.archive_database)
|
|
244
|
-
|
|
245
|
-
# submit
|
|
246
|
-
submit = subparsers.add_parser("submit", parents=[common], help="submit a job")
|
|
247
|
-
submit.add_argument('name', metavar="<name>")
|
|
248
|
-
submit.add_argument('script', metavar="<script.lua>", type=Path)
|
|
249
|
-
submit.add_argument('arguments', metavar="<arguments.txt>", type=Path)
|
|
250
|
-
submit.add_argument('--vcpus', type=int, default=4)
|
|
251
|
-
submit.add_argument('--memory', type=int, default=16000)
|
|
252
|
-
submit.add_argument('--batch_size', type=int, default=10000)
|
|
253
|
-
submit.add_argument('--image', type=str, default="sliderule:latest")
|
|
254
|
-
submit.set_defaults(func=Tool.submit_job)
|
|
255
|
-
|
|
256
|
-
# scrape
|
|
257
|
-
scrape = subparsers.add_parser("scrape", parents=[common], help="generate list of arguments from jobs with provided job status")
|
|
258
|
-
scrape.add_argument('--status', type=JobStatus, nargs='+', default=[JobStatus.FAILURE, JobStatus.UNSUPPORTED, JobStatus.ERROR])
|
|
259
|
-
scrape.add_argument('--name', type=str, default=None) # name of submission
|
|
260
|
-
scrape.add_argument('--output', type=str, default=None)
|
|
261
|
-
scrape.set_defaults(func=Tool.scrape_submissions)
|
|
262
|
-
|
|
263
|
-
# status
|
|
264
|
-
status = subparsers.add_parser("status", parents=[common], help="display status of submitted jobs")
|
|
265
|
-
status.set_defaults(func=Tool.get_status)
|
|
266
|
-
|
|
267
|
-
# report
|
|
268
|
-
report = subparsers.add_parser("report", parents=[common], help="generate report of submitted jobs")
|
|
269
|
-
report.add_argument('--name', type=str, default=None) # name of submission
|
|
270
|
-
report.set_defaults(func=Tool.generate_report)
|
|
271
|
-
|
|
272
|
-
# cancel
|
|
273
|
-
cancel = subparsers.add_parser("cancel", parents=[common], help="cancel a submitted job")
|
|
274
|
-
cancel.add_argument('--name', type=str, required=True) # name of submission
|
|
275
|
-
cancel.set_defaults(func=Tool.cancel_job)
|
|
276
|
-
|
|
277
|
-
# logs
|
|
278
|
-
logs = subparsers.add_parser("logs", parents=[common], help="get log messages for a child job")
|
|
279
|
-
logs.add_argument('--name', type=str, required=True) # name of submission
|
|
280
|
-
logs.add_argument('--index', type=int, required=True) # job index
|
|
281
|
-
logs.set_defaults(func=Tool.get_logs)
|
|
282
|
-
|
|
283
|
-
# parse command line
|
|
284
|
-
args = parser.parse_args()
|
|
285
|
-
|
|
286
|
-
# create tool
|
|
287
|
-
tool = Tool(args)
|
|
288
|
-
|
|
289
|
-
# route command
|
|
290
|
-
try:
|
|
291
|
-
args.func(tool)
|
|
292
|
-
tool.finish() # only execute if tool function completed successfully
|
|
293
|
-
except Exception as e:
|
|
294
|
-
if args.verbose: raise
|
|
295
|
-
print(f"Unhandled error: {e}")
|
|
296
|
-
|
|
297
|
-
# running via direct invocation
|
|
298
|
-
if __name__ == "__main__": main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|