sliderule-cli 5.6.1__tar.gz → 5.6.3__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 (25) hide show
  1. {sliderule_cli-5.6.1/src/sliderule_cli.egg-info → sliderule_cli-5.6.3}/PKG-INFO +1 -1
  2. {sliderule_cli-5.6.1 → sliderule_cli-5.6.3/src/sliderule_cli.egg-info}/PKG-INFO +1 -1
  3. sliderule_cli-5.6.3/src/sliderule_cli.egg-info/scm_version.json +8 -0
  4. {sliderule_cli-5.6.1 → sliderule_cli-5.6.3}/src/sliderule_cluster/main.py +16 -15
  5. {sliderule_cli-5.6.1 → sliderule_cli-5.6.3}/src/sliderule_provisioner/main.py +16 -4
  6. {sliderule_cli-5.6.1 → sliderule_cli-5.6.3}/src/sliderule_runner/database.py +1 -0
  7. {sliderule_cli-5.6.1 → sliderule_cli-5.6.3}/src/sliderule_runner/main.py +90 -41
  8. sliderule_cli-5.6.1/src/sliderule_cli.egg-info/scm_version.json +0 -8
  9. {sliderule_cli-5.6.1 → sliderule_cli-5.6.3}/.gitignore +0 -0
  10. {sliderule_cli-5.6.1 → sliderule_cli-5.6.3}/LICENSE +0 -0
  11. {sliderule_cli-5.6.1 → sliderule_cli-5.6.3}/README.md +0 -0
  12. {sliderule_cli-5.6.1 → sliderule_cli-5.6.3}/pyproject.toml +0 -0
  13. {sliderule_cli-5.6.1 → sliderule_cli-5.6.3}/setup.cfg +0 -0
  14. {sliderule_cli-5.6.1 → sliderule_cli-5.6.3}/src/sliderule_cli.egg-info/SOURCES.txt +0 -0
  15. {sliderule_cli-5.6.1 → sliderule_cli-5.6.3}/src/sliderule_cli.egg-info/dependency_links.txt +0 -0
  16. {sliderule_cli-5.6.1 → sliderule_cli-5.6.3}/src/sliderule_cli.egg-info/entry_points.txt +0 -0
  17. {sliderule_cli-5.6.1 → sliderule_cli-5.6.3}/src/sliderule_cli.egg-info/requires.txt +0 -0
  18. {sliderule_cli-5.6.1 → sliderule_cli-5.6.3}/src/sliderule_cli.egg-info/scm_file_list.json +0 -0
  19. {sliderule_cli-5.6.1 → sliderule_cli-5.6.3}/src/sliderule_cli.egg-info/top_level.txt +0 -0
  20. {sliderule_cli-5.6.1 → sliderule_cli-5.6.3}/src/sliderule_cluster/__init__.py +0 -0
  21. {sliderule_cli-5.6.1 → sliderule_cli-5.6.3}/src/sliderule_provisioner/__init__.py +0 -0
  22. {sliderule_cli-5.6.1 → sliderule_cli-5.6.3}/src/sliderule_recorder/__init__.py +0 -0
  23. {sliderule_cli-5.6.1 → sliderule_cli-5.6.3}/src/sliderule_recorder/environment.yml +0 -0
  24. {sliderule_cli-5.6.1 → sliderule_cli-5.6.3}/src/sliderule_recorder/main.py +0 -0
  25. {sliderule_cli-5.6.1 → sliderule_cli-5.6.3}/src/sliderule_runner/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sliderule-cli
3
- Version: 5.6.1
3
+ Version: 5.6.3
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.1
3
+ Version: 5.6.3
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.6.3",
3
+ "distance": 0,
4
+ "node": "ge620b5509bb5a993af03131885c3a6f23922cb24",
5
+ "dirty": false,
6
+ "branch": "main",
7
+ "node_date": "2026-09-30"
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()
@@ -69,6 +69,14 @@ class Tool:
69
69
  def info(self):
70
70
  return self.session.provisioner.info()
71
71
 
72
+ # S3 Access
73
+ def s3access(self):
74
+ credentials = self.session.provisioner.s3access()
75
+ print(f'export AWS_ACCESS_KEY_ID={credentials["access_key_id"]}')
76
+ print(f'export AWS_SECRET_ACCESS_KEY={credentials["secret_access_key"]}')
77
+ print(f'export AWS_SESSION_TOKEN={credentials["session_token"]}')
78
+ print(f'export AWS_CREDENTIAL_EXPIRATION={credentials["expiration"]}')
79
+
72
80
  # Authenticate
73
81
  def authenticate(self):
74
82
  return self.session.authenticate(force_login=True)
@@ -81,7 +89,7 @@ class Tool:
81
89
  def main():
82
90
 
83
91
  # command line arguments
84
- parser = argparse.ArgumentParser(prog="sliderule-runner", description="""SlideRule Runner""")
92
+ parser = argparse.ArgumentParser(prog="sliderule-provisioner", description="""SlideRule Provisioner""")
85
93
  subparsers = parser.add_subparsers(dest="command", required=True)
86
94
 
87
95
  # options shared by every subcommand; parents= lets them appear after the command name
@@ -89,7 +97,6 @@ def main():
89
97
  common.add_argument('--domain', type=str, default="slideruleearth.io")
90
98
  common.add_argument('--cluster', type=str, default="developers")
91
99
  common.add_argument('--user_service', action='store_true', default=False)
92
- common.add_argument('--concise', action='store_true', default=False)
93
100
  common.add_argument('--verbose', action='store_true', default=False)
94
101
  common.add_argument('--timezone', type=str, default="America/New_York")
95
102
 
@@ -120,7 +127,7 @@ def main():
120
127
 
121
128
  # report
122
129
  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")
130
+ report.add_argument('--kind', type=str, default="cluster", help="'cluster' for cluster report, 'test' for test report")
124
131
  report.set_defaults(func=Tool.report)
125
132
 
126
133
  # test
@@ -132,6 +139,10 @@ def main():
132
139
  info = subparsers.add_parser("info", parents=[common], help="display information about provisioner")
133
140
  info.set_defaults(func=Tool.info)
134
141
 
142
+ # s3access
143
+ s3access = subparsers.add_parser("s3access", parents=[common], help="sets AWS credentials by outputting shell commands to 'eval' into current environment")
144
+ s3access.set_defaults(func=Tool.s3access)
145
+
135
146
  # authenticate
136
147
  authenticate = subparsers.add_parser("authenticate", parents=[common], help="force re-authentication of user account")
137
148
  authenticate.set_defaults(func=Tool.authenticate)
@@ -145,7 +156,8 @@ def main():
145
156
  # route command
146
157
  try:
147
158
  result = args.func(tool)
148
- print(f'{json.dumps(result, indent=2)}')
159
+ if result:
160
+ print(f'{json.dumps(result, indent=2)}')
149
161
  except Exception as e:
150
162
  if args.verbose: raise
151
163
  print(f"Unhandled error: {e}")
@@ -14,6 +14,7 @@
14
14
  # },
15
15
  # "complete": <true|false>,
16
16
  # "results": [ {result 1}, {result 2}, ... {result N} ]
17
+ # "jobs": { <child_job_id>: {child job status} }
17
18
  # },
18
19
  # ...
19
20
  # }
@@ -4,6 +4,7 @@ import boto3
4
4
  import random
5
5
  import string
6
6
  import argparse
7
+ import traceback
7
8
  from sliderule import sliderule
8
9
  from .database import Database, JobState, QueuePriority, JobStatus
9
10
  from pathlib import Path
@@ -27,7 +28,7 @@ class Tool:
27
28
  # open database
28
29
  self.database = Database(args.database)
29
30
  # create sliderule session
30
- self.session = sliderule.create_session(verbose=args.verbose)
31
+ self.session = sliderule.create_session(domain=args.domain, verbose=args.verbose)
31
32
  self.session.authenticate() # gives privileges to access SlideRule Runner
32
33
  # aws clients
33
34
  self.s3 = boto3.client("s3", region_name="us-west-2")
@@ -36,7 +37,10 @@ class Tool:
36
37
  def __load_remote_file(self, bucket, key):
37
38
  obj = self.s3.get_object(Bucket=bucket, Key=key)
38
39
  contents = obj["Body"].read().decode("utf-8")
39
- return json.loads(contents)
40
+ try:
41
+ return json.loads(contents)
42
+ except Exception:
43
+ return contents
40
44
 
41
45
  # Get Results for a Job Run
42
46
  # output of job must include the following fields for this to work:
@@ -51,15 +55,22 @@ class Tool:
51
55
  bucket = run_url.split("s3://")[-1].split("/")[0]
52
56
  prefix = "/".join(run_url.split("s3://")[-1].split("/")[1:])
53
57
  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"])
58
+ args_bucket = receipt["args"].split("s3://")[-1].split("/")[0]
59
+ args_file = "/".join(receipt["args"].split("s3://")[-1].split("/")[1:])
60
+ args_content = self.__load_remote_file(args_bucket, args_file)
61
+ if isinstance(args_content, list):
62
+ args_list = args_content
63
+ elif isinstance(args_content, dict) or isinstance(args_content, str):
64
+ args_list = [args_content]
55
65
  for i in tqdm(range(len(args_list)), total=len(args_list), desc=f"{run_url}", unit="granule"):
66
+ result_file = f"{prefix}/result{len(args_list) > 1 and str(i) or ''}.json"
56
67
  try:
57
68
  result = {
58
- "file": f"{prefix}/result{i}.json",
69
+ "file": f"s3://{bucket}/{result_file}",
59
70
  "environment": receipt["environment"],
60
71
  "arg": args_list[i]
61
72
  }
62
- rsps = self.__load_remote_file(bucket, f"{prefix}/result{i}.json")
73
+ rsps = self.__load_remote_file(bucket, result_file)
63
74
  try:
64
75
  result |= {
65
76
  "status": rsps["status"] and JobStatus.SUCCESS or JobStatus.FAILURE,
@@ -89,7 +100,8 @@ class Tool:
89
100
  # pull out arguments
90
101
  name = self.args.name
91
102
  script_file = self.args.script
92
- arguments_file = self.args.arguments
103
+ arguments_input = self.args.arguments
104
+ arg_as_str = self.args.arg_as_str
93
105
  batch_size = self.args.batch_size
94
106
  vcpus = self.args.vcpus
95
107
  memory = self.args.memory
@@ -99,9 +111,12 @@ class Tool:
99
111
  with open(script_file, "r") as file:
100
112
  script = file.read()
101
113
  # read arguments
102
- with open(arguments_file, "r") as file:
103
- arguments = [line.strip() for line in file.readlines()]
104
- # process job in batches
114
+ if arg_as_str:
115
+ arguments = [str(arguments_input)]
116
+ else:
117
+ with open(arguments_input, "r") as file:
118
+ arguments = [line.strip() for line in file.readlines()]
119
+ # process job in batches
105
120
  for i in range(0, len(arguments), batch_size):
106
121
  # build and check name
107
122
  job_name = f"{name}_{i}"
@@ -111,29 +126,34 @@ class Tool:
111
126
  # submit & save job
112
127
  args_list = arguments[i:i+batch_size]
113
128
  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}
129
+ self.database.submissions[job_name] = rsps | {"complete": False, "job_size": len(args_list)}
115
130
  print(f"Submitted job {job_name} using script {script_file} with {len(args_list)} entries: {rsps}")
116
131
 
117
132
  # Scrape Submissions
118
133
  def scrape_submissions(self):
119
- arg_list = []
134
+ output_list = []
120
135
  for name,submission in self.database.submissions.items():
121
136
  if self.args.name and self.args.name != name: continue
122
137
  try:
123
138
  if submission["complete"]:
124
139
  print(f"Scraping {name} ...")
125
- for result in submission["results"]:
140
+ for i in range(len(submission["results"])):
141
+ result = submission["results"][i]
126
142
  if result["status"] in self.args.status:
127
- print(f"{result["arg"]}")
128
- arg_list.append(result["arg"])
143
+ if self.args.verbose:
144
+ print(f'{i:-5d}: {result}\n')
145
+ output_list.append(json.dumps(result))
146
+ else:
147
+ print(f'{i:-5d}: {result["arg"]}')
148
+ output_list.append(result["arg"])
129
149
  else:
130
150
  print(f"Skipping {name} because it is still pending")
131
151
  except Exception as e:
132
152
  print(f"Failed to scrape {name}: {e}")
133
153
  if self.args.output:
134
154
  with open(self.args.output, "w") as file:
135
- for arg in arg_list:
136
- file.write(f"{arg}\n")
155
+ for output in output_list:
156
+ file.write(f"{output}\n")
137
157
 
138
158
  # Get Status
139
159
  def get_status(self):
@@ -142,44 +162,52 @@ class Tool:
142
162
  complete = job["complete"]
143
163
  print(f"Statusing {name} ...")
144
164
  if not complete:
145
- report = self.session.runner.queue(job_id=job["job_id"], queue=queue)["report"]
165
+ if job["job_size"] > 1: # array processing, use parent job id
166
+ queue_status = self.session.runner.queue(job_id=job["job_id"], queue=queue, verbose=True)
167
+ child_jobs = queue_status["jobs"]
168
+ self.database.submissions[name]["jobs"] = {}
169
+ for child_job in child_jobs:
170
+ self.database.submissions[name]["jobs"][int(child_job["index"])] = child_job
171
+ else: # single job, use name
172
+ queue_status = self.session.runner.queue(name=name, queue=queue, verbose=False)
173
+ # report
174
+ report = queue_status["report"]
146
175
  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:
176
+ jobs_in_progress = sum([report[s] for s in [JobState.SUBMITTED, JobState.PENDING, JobState.RUNNABLE, JobState.STARTING, JobState.RUNNING]])
177
+ jobs_complete = sum([report[s] for s in [JobState.SUCCEEDED, JobState.FAILED]])
178
+ if jobs_in_progress == 0 and jobs_complete > 0:
148
179
  print(f"Job {name} complete, reading results ...")
149
180
  self.database.submissions[name]["complete"] = True
150
181
  self.database.submissions[name]["results"] = self.__get_results(job["run_url"])
151
182
  else:
152
183
  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)]]))
184
+ if self.args.verbose:
185
+ print(",".join([f"{c:>30}" for c in ["NAME"]] + [f"{c:>10}" for c in ["INDEX", "STATUS"]]))
186
+ for name,job in self.database.submissions.items():
187
+ for index, child_job in job["jobs"].items():
188
+ print(",".join([f"{c:>30}" for c in [name]] + [f"{c:>10}" for c in [index, child_job["status"]]]))
189
+ else:
190
+ print(",".join([f"{c:>30}" for c in ["NAME"]] + [f"{c:>10}" for c in list(JobState)]))
191
+ for name,job in self.database.submissions.items():
192
+ print(",".join([f"{c:>30}" for c in [name]] + [f"{c:>10}" for c in [job["status"][state] for state in list(JobState)]]))
156
193
 
157
194
  # Generate Report
158
195
  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
196
+ print(",".join([f"{c:>30}" for c in ["name"]] + [f"{c:>12}" for c in ["processed"]] + [f"{c:>12}" for c in list(JobStatus)] + [f"{c:>12}" for c in ["duration"]]))
164
197
  for name,submission in self.database.submissions.items():
165
198
  if self.args.name and self.args.name != name: continue
199
+ stats = {status.value: 0 for status in JobStatus}
200
+ duration = {"avg": 0.0, "total": 0.0}
201
+ processed = 0
166
202
  if submission["complete"]:
167
203
  for result in submission["results"]:
168
- try:
169
- stats[result["status"]] += 1
204
+ stats[result["status"]] += 1
205
+ if "duration" in result:
170
206
  duration["total"] += result["duration"]
171
207
  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))
208
+ if processed > 0:
209
+ duration["avg"] = duration["total"] / processed
210
+ print(",".join([f"{c:>30}" for c in [name]] + [f"{c:>12}" for c in [processed]] + [f"{c:>12}" for c in [stats[status] for status in list(JobStatus)]] + [f"{c:>12.3}" for c in [duration["avg"]]]))
183
211
 
184
212
  # Cancel Job
185
213
  def cancel_job(self):
@@ -189,9 +217,21 @@ class Tool:
189
217
  rsps = self.session.runner.cancel(job_list=[job_id], queue=queue)
190
218
  print(f"Cancelled submission {name}: {rsps}")
191
219
 
220
+ # Get Logs
221
+ def get_logs(self):
222
+ name = self.args.name
223
+ index = self.args.index
224
+ if index is not None:
225
+ job_id = self.database.submissions[name]["jobs"][str(index)]["job_id"]
226
+ else:
227
+ job_id = self.database.submissions[name]["job_id"]
228
+ events = self.session.runner.logs(job_id=job_id)
229
+ for event in events:
230
+ print("->", event)
231
+
192
232
  # Finish
193
233
  def finish(self):
194
- if not self.args.dryrun:
234
+ if not self.args.dryrun and "archive" not in self.args:
195
235
  # save database
196
236
  self.database.write()
197
237
 
@@ -207,6 +247,7 @@ def main():
207
247
 
208
248
  # options shared by every subcommand; parents= lets them appear after the command name
209
249
  common = argparse.ArgumentParser(add_help=False)
250
+ common.add_argument('--domain', type=str, default="slideruleearth.io")
210
251
  common.add_argument('--database', type=Path, default=Path.home() / ".cache" / "sliderule" / "runner_database.json")
211
252
  common.add_argument('--queue', type=QueuePriority, default=QueuePriority.DEFAULT, choices=list(QueuePriority))
212
253
  common.add_argument('--verbose', action='store_true', default=False)
@@ -222,6 +263,7 @@ def main():
222
263
  submit.add_argument('name', metavar="<name>")
223
264
  submit.add_argument('script', metavar="<script.lua>", type=Path)
224
265
  submit.add_argument('arguments', metavar="<arguments.txt>", type=Path)
266
+ submit.add_argument('--arg_as_str', action='store_true', default=False) # changes arguments to be just a string instead of a file
225
267
  submit.add_argument('--vcpus', type=int, default=4)
226
268
  submit.add_argument('--memory', type=int, default=16000)
227
269
  submit.add_argument('--batch_size', type=int, default=10000)
@@ -240,7 +282,7 @@ def main():
240
282
  status.set_defaults(func=Tool.get_status)
241
283
 
242
284
  # report
243
- report = subparsers.add_parser("report", parents=[common], help="generate report of submitted jobs")
285
+ report = subparsers.add_parser("report", parents=[common], help="generate report of completed jobs")
244
286
  report.add_argument('--name', type=str, default=None) # name of submission
245
287
  report.set_defaults(func=Tool.generate_report)
246
288
 
@@ -249,6 +291,12 @@ def main():
249
291
  cancel.add_argument('--name', type=str, required=True) # name of submission
250
292
  cancel.set_defaults(func=Tool.cancel_job)
251
293
 
294
+ # logs
295
+ logs = subparsers.add_parser("logs", parents=[common], help="get log messages for a child job")
296
+ logs.add_argument('--name', type=str, required=True) # name of submission
297
+ logs.add_argument('--index', type=int, default=None) # job index
298
+ logs.set_defaults(func=Tool.get_logs)
299
+
252
300
  # parse command line
253
301
  args = parser.parse_args()
254
302
 
@@ -262,6 +310,7 @@ def main():
262
310
  except Exception as e:
263
311
  if args.verbose: raise
264
312
  print(f"Unhandled error: {e}")
313
+ traceback.print_exc()
265
314
 
266
315
  # running via direct invocation
267
316
  if __name__ == "__main__": main()
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "5.6.1",
3
- "distance": 0,
4
- "node": "g5bc2a6eb3c8fc3927b3ffb14b511be2698a8261c",
5
- "dirty": false,
6
- "branch": "main",
7
- "node_date": "2026-09-18"
8
- }
File without changes
File without changes
File without changes
File without changes