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.
Files changed (26) hide show
  1. {sliderule_cli-5.6.2/src/sliderule_cli.egg-info → sliderule_cli-5.7.0}/PKG-INFO +1 -1
  2. {sliderule_cli-5.6.2 → sliderule_cli-5.7.0/src/sliderule_cli.egg-info}/PKG-INFO +1 -1
  3. sliderule_cli-5.7.0/src/sliderule_cli.egg-info/scm_version.json +8 -0
  4. {sliderule_cli-5.6.2 → sliderule_cli-5.7.0}/src/sliderule_cluster/main.py +16 -15
  5. {sliderule_cli-5.6.2 → sliderule_cli-5.7.0}/src/sliderule_provisioner/main.py +25 -5
  6. sliderule_cli-5.7.0/src/sliderule_runner/main.py +379 -0
  7. sliderule_cli-5.6.2/src/sliderule_cli.egg-info/scm_version.json +0 -8
  8. sliderule_cli-5.6.2/src/sliderule_runner/main.py +0 -298
  9. {sliderule_cli-5.6.2 → sliderule_cli-5.7.0}/.gitignore +0 -0
  10. {sliderule_cli-5.6.2 → sliderule_cli-5.7.0}/LICENSE +0 -0
  11. {sliderule_cli-5.6.2 → sliderule_cli-5.7.0}/README.md +0 -0
  12. {sliderule_cli-5.6.2 → sliderule_cli-5.7.0}/pyproject.toml +0 -0
  13. {sliderule_cli-5.6.2 → sliderule_cli-5.7.0}/setup.cfg +0 -0
  14. {sliderule_cli-5.6.2 → sliderule_cli-5.7.0}/src/sliderule_cli.egg-info/SOURCES.txt +0 -0
  15. {sliderule_cli-5.6.2 → sliderule_cli-5.7.0}/src/sliderule_cli.egg-info/dependency_links.txt +0 -0
  16. {sliderule_cli-5.6.2 → sliderule_cli-5.7.0}/src/sliderule_cli.egg-info/entry_points.txt +0 -0
  17. {sliderule_cli-5.6.2 → sliderule_cli-5.7.0}/src/sliderule_cli.egg-info/requires.txt +0 -0
  18. {sliderule_cli-5.6.2 → sliderule_cli-5.7.0}/src/sliderule_cli.egg-info/scm_file_list.json +0 -0
  19. {sliderule_cli-5.6.2 → sliderule_cli-5.7.0}/src/sliderule_cli.egg-info/top_level.txt +0 -0
  20. {sliderule_cli-5.6.2 → sliderule_cli-5.7.0}/src/sliderule_cluster/__init__.py +0 -0
  21. {sliderule_cli-5.6.2 → sliderule_cli-5.7.0}/src/sliderule_provisioner/__init__.py +0 -0
  22. {sliderule_cli-5.6.2 → sliderule_cli-5.7.0}/src/sliderule_recorder/__init__.py +0 -0
  23. {sliderule_cli-5.6.2 → sliderule_cli-5.7.0}/src/sliderule_recorder/environment.yml +0 -0
  24. {sliderule_cli-5.6.2 → sliderule_cli-5.7.0}/src/sliderule_recorder/main.py +0 -0
  25. {sliderule_cli-5.6.2 → sliderule_cli-5.7.0}/src/sliderule_runner/__init__.py +0 -0
  26. {sliderule_cli-5.6.2 → sliderule_cli-5.7.0}/src/sliderule_runner/database.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sliderule-cli
3
- Version: 5.6.2
3
+ Version: 5.7.0
4
4
  Summary: SlideRule Command Line Tool
5
5
  License-Expression: BSD-3-Clause
6
6
  Requires-Python: >=3.9
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sliderule-cli
3
- Version: 5.6.2
3
+ Version: 5.7.0
4
4
  Summary: SlideRule Command Line Tool
5
5
  License-Expression: BSD-3-Clause
6
6
  Requires-Python: >=3.9
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "5.7.0",
3
+ "distance": 0,
4
+ "node": "g084d9e1b17c9db584fa57119be4c8c89da45b645",
5
+ "dirty": false,
6
+ "branch": "main",
7
+ "node_date": "2026-10-22"
8
+ }
@@ -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
- rsps = self.session.source("whoami", parm={}, path="/discovery", retries=0, rethrow=True)
46
- result = json.dumps(rsps)
47
- self.__display_result(result)
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
- rsps = self.session.source("status", parm=parm, path="/discovery", retries=0, rethrow=True)
57
- result = json.dumps(rsps)
58
- self.__display_result(result)
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('"') or 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=True)
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 longitude pairs defining the area of interest; e.g. lon1 lat1 lon2 lat2 lon3 lat3 ... lon1 lat1")
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-runner", description="""SlideRule Runner""")
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="'cluster' for cluster report, 'test' for test report")
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
- print(f'{json.dumps(result, indent=2)}')
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,8 +0,0 @@
1
- {
2
- "tag": "5.6.2",
3
- "distance": 0,
4
- "node": "g76fb3116a5b77c6fbeb8f8df0a2cdffe60a1418b",
5
- "dirty": false,
6
- "branch": "main",
7
- "node_date": "2026-09-23"
8
- }
@@ -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