snakemake-executor-plugin-aws-batch-flex 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- snakemake_executor_plugin_aws_batch_flex/__init__.py +8 -0
- snakemake_executor_plugin_aws_batch_flex/bootstrap.py +376 -0
- snakemake_executor_plugin_aws_batch_flex/bundle.py +142 -0
- snakemake_executor_plugin_aws_batch_flex/check.py +129 -0
- snakemake_executor_plugin_aws_batch_flex/condacache.py +184 -0
- snakemake_executor_plugin_aws_batch_flex/executor.py +725 -0
- snakemake_executor_plugin_aws_batch_flex/headjob.py +244 -0
- snakemake_executor_plugin_aws_batch_flex/hints.py +75 -0
- snakemake_executor_plugin_aws_batch_flex/hostenv.py +221 -0
- snakemake_executor_plugin_aws_batch_flex/imageinfo.py +207 -0
- snakemake_executor_plugin_aws_batch_flex/jobdefs.py +221 -0
- snakemake_executor_plugin_aws_batch_flex/jobfixes.py +65 -0
- snakemake_executor_plugin_aws_batch_flex/jobmonitor.py +146 -0
- snakemake_executor_plugin_aws_batch_flex/naming.py +16 -0
- snakemake_executor_plugin_aws_batch_flex/orphans.py +51 -0
- snakemake_executor_plugin_aws_batch_flex/patches.py +196 -0
- snakemake_executor_plugin_aws_batch_flex/preflight.py +165 -0
- snakemake_executor_plugin_aws_batch_flex/profiles.py +98 -0
- snakemake_executor_plugin_aws_batch_flex/remote.py +409 -0
- snakemake_executor_plugin_aws_batch_flex/settings.py +482 -0
- snakemake_executor_plugin_aws_batch_flex/sizing.py +75 -0
- snakemake_executor_plugin_aws_batch_flex/sources.py +83 -0
- snakemake_executor_plugin_aws_batch_flex/tags.py +76 -0
- snakemake_executor_plugin_aws_batch_flex/transfer.py +193 -0
- snakemake_executor_plugin_aws_batch_flex/usage.py +208 -0
- snakemake_executor_plugin_aws_batch_flex-0.1.0.dist-info/METADATA +343 -0
- snakemake_executor_plugin_aws_batch_flex-0.1.0.dist-info/RECORD +30 -0
- snakemake_executor_plugin_aws_batch_flex-0.1.0.dist-info/WHEEL +4 -0
- snakemake_executor_plugin_aws_batch_flex-0.1.0.dist-info/entry_points.txt +3 -0
- snakemake_executor_plugin_aws_batch_flex-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
"""smkflex: run a whole workflow on AWS Batch, so nothing has to stay running on your machine.
|
|
2
|
+
|
|
3
|
+
`smkflex run` packs the workflow folder and the profile, uploads them next to the workflow's
|
|
4
|
+
files in storage, and submits one "head job" that runs `snakemake` with this plugin. Snakemake's
|
|
5
|
+
.snakemake state (what was made with which code, unfinished jobs) is saved to storage at the end
|
|
6
|
+
of each run and restored at the start of the next, so reruns behave as they do locally.
|
|
7
|
+
|
|
8
|
+
Optional: `snakemake --executor aws-batch-flex` from your own machine works as before."""
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import hashlib
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
import shutil
|
|
15
|
+
import sys
|
|
16
|
+
import tempfile
|
|
17
|
+
import time
|
|
18
|
+
from datetime import datetime, timezone
|
|
19
|
+
|
|
20
|
+
import yaml
|
|
21
|
+
from botocore.exceptions import BotoCoreError, ClientError
|
|
22
|
+
from snakemake_interface_common.exceptions import WorkflowError
|
|
23
|
+
|
|
24
|
+
from . import headjob
|
|
25
|
+
from .bundle import _tag, added_files, clone, head_command, local_commit, make_bundle
|
|
26
|
+
from .check import cmd_check
|
|
27
|
+
from .imageinfo import Entrypoints
|
|
28
|
+
from .jobdefs import JobDefinitions, log_group_of
|
|
29
|
+
from .naming import batch_job_name
|
|
30
|
+
from .orphans import active_jobs, describe_jobs, list_jobs
|
|
31
|
+
from .preflight import check_queue, queue_uses_spot
|
|
32
|
+
from .profiles import Fail, _clients, _load_profile, _Log, workflow_profile_file
|
|
33
|
+
from .settings import from_profile, host_mounts, registry_secrets
|
|
34
|
+
from .sizing import fargate_size
|
|
35
|
+
from .tags import MANAGED_BY
|
|
36
|
+
from .transfer import cmd_pull, cmd_push
|
|
37
|
+
|
|
38
|
+
HEAD_IMAGE = "buildpack-deps:bookworm-scm"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _active_heads(batch, queue, state_key):
|
|
42
|
+
return [d["jobId"] for d in active_jobs(batch, queue) if (d.get("tags") or {}).get("smkflex-state") == state_key]
|
|
43
|
+
|
|
44
|
+
def _check_snakemake_args(args):
|
|
45
|
+
"""Fail on options Snakemake doesn't know (a typo in an smkflex option such as --wiat
|
|
46
|
+
ends up here too), before anything is uploaded or submitted."""
|
|
47
|
+
try:
|
|
48
|
+
from snakemake.cli import get_argument_parser
|
|
49
|
+
except ImportError:
|
|
50
|
+
return
|
|
51
|
+
try:
|
|
52
|
+
_ns, unknown = get_argument_parser().parse_known_args(args)
|
|
53
|
+
except SystemExit:
|
|
54
|
+
raise Fail("snakemake can't use these arguments (see above)") from None
|
|
55
|
+
unknown = [u for u in unknown if u.startswith("-")]
|
|
56
|
+
if unknown:
|
|
57
|
+
raise Fail(f"neither smkflex run nor snakemake has the option(s) {' '.join(unknown)} "
|
|
58
|
+
"(see smkflex run --help and snakemake --help)")
|
|
59
|
+
|
|
60
|
+
def cmd_run(a, snakemake_args):
|
|
61
|
+
_check_snakemake_args(snakemake_args)
|
|
62
|
+
tmp = None
|
|
63
|
+
try:
|
|
64
|
+
if a.git:
|
|
65
|
+
tmp = tempfile.mkdtemp(prefix="smkflex-git-")
|
|
66
|
+
commit = clone(a.git, a.rev, os.path.join(tmp, "repo"))
|
|
67
|
+
root = os.path.join(tmp, "repo", a.path or "")
|
|
68
|
+
if not os.path.isdir(root):
|
|
69
|
+
raise Fail(f"{a.path} isn't a folder in {a.git}")
|
|
70
|
+
print(f"Cloned {a.git} at {commit[:12]}" + (f" ({a.rev})" if a.rev else ""), file=sys.stderr)
|
|
71
|
+
extra = added_files(a.add or [])
|
|
72
|
+
source = {"smkflex-source": _tag(a.git + (f" {a.rev}" if a.rev else "")),
|
|
73
|
+
"smkflex-commit": commit[:12] + ("+added" if extra else "")}
|
|
74
|
+
default_name = re.sub(r"\.git$", "", a.git.rstrip("/").rsplit("/", 1)[-1])
|
|
75
|
+
else:
|
|
76
|
+
if a.rev or a.path:
|
|
77
|
+
raise Fail("--rev and --path go with --git")
|
|
78
|
+
root = "."
|
|
79
|
+
extra = added_files(a.add or [])
|
|
80
|
+
commit = local_commit(root)
|
|
81
|
+
source = {"smkflex-commit": commit + ("+added" if extra else "")} if commit else {}
|
|
82
|
+
default_name = os.path.basename(os.path.abspath("."))
|
|
83
|
+
if extra:
|
|
84
|
+
print(f"Adding {len(extra)} local file(s): " + ", ".join(sorted(extra)[:5])
|
|
85
|
+
+ (" ..." if len(extra) > 5 else ""), file=sys.stderr)
|
|
86
|
+
return _run(a, snakemake_args, root, source, default_name, extra)
|
|
87
|
+
finally:
|
|
88
|
+
if tmp:
|
|
89
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
90
|
+
|
|
91
|
+
def _run(a, snakemake_args, root, source, default_name, extra=None):
|
|
92
|
+
profile_dir, profile = _load_profile(a.profile)
|
|
93
|
+
s = from_profile(profile)
|
|
94
|
+
queue = a.queue or s.queue
|
|
95
|
+
prefix = str(profile.get("default-storage-prefix") or "").rstrip("/")
|
|
96
|
+
role = a.role or s.head_role_arn
|
|
97
|
+
if not queue:
|
|
98
|
+
raise Fail("no queue: set aws-batch-flex-queue in the profile or pass --queue")
|
|
99
|
+
if not prefix or profile.get("default-storage-provider") in (None, "local"):
|
|
100
|
+
raise Fail("smkflex needs remote storage (default-storage-provider and default-storage-prefix "
|
|
101
|
+
"in the profile): the head job's disk is gone when it ends")
|
|
102
|
+
if not role:
|
|
103
|
+
raise Fail("no role for the head job: set aws-batch-flex-head-role-arn in the profile (Terraform "
|
|
104
|
+
"prints it as head_role_arn) or pass --role")
|
|
105
|
+
name = re.sub(r"[^A-Za-z0-9_-]", "-", a.name or default_name)
|
|
106
|
+
# One project (storage prefix) has one state and one run at a time, as Snakemake has one
|
|
107
|
+
# working directory with one lock: two runs would make and overwrite the same outputs.
|
|
108
|
+
state_url = f"{prefix}/.smkflex/state.tar.gz"
|
|
109
|
+
state_key = hashlib.sha256(state_url.encode()).hexdigest()[:16]
|
|
110
|
+
batch, _logs, region = _clients(profile)
|
|
111
|
+
|
|
112
|
+
errors, warnings, detected = check_queue(batch, queue, s.platform)
|
|
113
|
+
for w in warnings:
|
|
114
|
+
print(f"warning: {w}", file=sys.stderr)
|
|
115
|
+
if errors:
|
|
116
|
+
raise Fail("; ".join(errors))
|
|
117
|
+
platform = s.platform or detected or "ec2"
|
|
118
|
+
if queue_uses_spot(batch, queue):
|
|
119
|
+
print(f"warning: queue {queue} uses Spot instances; if AWS takes the head job's instance back, "
|
|
120
|
+
"the whole run stops. Use an on-demand queue for it (--queue).", file=sys.stderr)
|
|
121
|
+
running = _active_heads(batch, queue, state_key)
|
|
122
|
+
if running:
|
|
123
|
+
raise Fail(f"a run of this project ({prefix}) is still going: head job {running[0]}. Wait for "
|
|
124
|
+
f"it or `smkflex cancel {running[0]}` (finished outputs are kept), then run again. "
|
|
125
|
+
"Another set of samples is another project: `smkflex init` in its own folder.")
|
|
126
|
+
|
|
127
|
+
bundle = make_bundle(root, profile_dir, a.max_bundle_mb, extra)
|
|
128
|
+
stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
|
|
129
|
+
bundle_url = f"{prefix}/.smkflex/runs/{stamp}/workflow.tar.gz"
|
|
130
|
+
headjob.put(bundle_url, bundle)
|
|
131
|
+
print(f"Uploaded the workflow ({len(bundle) // 1024} KiB) to {bundle_url}", file=sys.stderr)
|
|
132
|
+
|
|
133
|
+
jobdefs = JobDefinitions(
|
|
134
|
+
batch, _Log(),
|
|
135
|
+
prefix=s.job_definition_prefix,
|
|
136
|
+
platform=platform,
|
|
137
|
+
job_role_arn=role,
|
|
138
|
+
execution_role_arn=s.execution_role_arn,
|
|
139
|
+
log_group=s.log_group,
|
|
140
|
+
assign_public_ip=s.assign_public_ip,
|
|
141
|
+
ephemeral_storage_gib=None,
|
|
142
|
+
tags={},
|
|
143
|
+
mounts=host_mounts(s) if platform == "ec2" else [],
|
|
144
|
+
)
|
|
145
|
+
# Debian 12 with git: Snakemake needs git for pipelines loaded from GitHub (snakedeploy's)
|
|
146
|
+
image = a.image or HEAD_IMAGE
|
|
147
|
+
jobdef = jobdefs.ensure(image, platform)
|
|
148
|
+
vcpu, mem = str(a.vcpus), a.mem_mb
|
|
149
|
+
if platform == "fargate":
|
|
150
|
+
vcpu, mem = fargate_size(a.vcpus, a.mem_mb)
|
|
151
|
+
env = [{"name": "AWS_REGION", "value": region}, {"name": "AWS_DEFAULT_REGION", "value": region}] if region else []
|
|
152
|
+
env.append({"name": "SMKFLEX_BUNDLE_URL", "value": bundle_url})
|
|
153
|
+
resp = batch.submit_job(
|
|
154
|
+
jobName=batch_job_name(f"smkflex-{name}"),
|
|
155
|
+
jobQueue=queue,
|
|
156
|
+
jobDefinition=jobdef,
|
|
157
|
+
containerOverrides={
|
|
158
|
+
"command": Entrypoints(registry_secrets(s), _Log()).command(
|
|
159
|
+
image, head_command(profile, platform, bundle_url, state_url, snakemake_args)),
|
|
160
|
+
"resourceRequirements": [{"type": "VCPU", "value": vcpu}, {"type": "MEMORY", "value": str(mem)}],
|
|
161
|
+
"environment": env,
|
|
162
|
+
},
|
|
163
|
+
tags={"smkflex-workflow": name[:256], "smkflex-state": state_key, **source, **MANAGED_BY},
|
|
164
|
+
propagateTags=True,
|
|
165
|
+
)
|
|
166
|
+
jid = resp["jobId"]
|
|
167
|
+
at = f" at {source['smkflex-commit']}" if source.get("smkflex-commit") else ""
|
|
168
|
+
print(f"Submitted workflow '{name}'{at} as head job {jid} (queue {queue}, {platform}).")
|
|
169
|
+
if a.wait:
|
|
170
|
+
return follow(batch, _logs, jid, follow_logs=True)
|
|
171
|
+
print(f" follow it: smkflex logs {jid} --follow\n cancel it: smkflex cancel {jid}")
|
|
172
|
+
return 0
|
|
173
|
+
|
|
174
|
+
def follow(batch, logs, jid, follow_logs):
|
|
175
|
+
"""Print the head job's log (until it ends, with follow_logs). Returns 0 if it succeeded."""
|
|
176
|
+
token, last_status, group, stream = None, None, None, None
|
|
177
|
+
ended = False
|
|
178
|
+
while True:
|
|
179
|
+
jobs = batch.describe_jobs(jobs=[jid])["jobs"]
|
|
180
|
+
if not jobs:
|
|
181
|
+
raise Fail(f"no job {jid}")
|
|
182
|
+
d = jobs[0]
|
|
183
|
+
status = d["status"]
|
|
184
|
+
done = status in ("SUCCEEDED", "FAILED")
|
|
185
|
+
if status != last_status and follow_logs and not done:
|
|
186
|
+
print(f"[smkflex] head job {jid}: {status}", file=sys.stderr)
|
|
187
|
+
last_status = status
|
|
188
|
+
c = d.get("container", {})
|
|
189
|
+
stream = c.get("logStreamName") or stream
|
|
190
|
+
group = log_group_of(c)
|
|
191
|
+
got = 0
|
|
192
|
+
if stream:
|
|
193
|
+
while True:
|
|
194
|
+
kw = dict(logGroupName=group, logStreamName=stream, startFromHead=True)
|
|
195
|
+
if token:
|
|
196
|
+
kw["nextToken"] = token
|
|
197
|
+
try:
|
|
198
|
+
resp = logs.get_log_events(**kw)
|
|
199
|
+
except ClientError as e:
|
|
200
|
+
if e.response.get("Error", {}).get("Code") == "ResourceNotFoundException":
|
|
201
|
+
break # no log stream yet
|
|
202
|
+
raise
|
|
203
|
+
for e in resp.get("events", []):
|
|
204
|
+
print(e["message"].rstrip())
|
|
205
|
+
got += len(resp.get("events", []))
|
|
206
|
+
# CloudWatch can return empty pages before the end; the end is when the
|
|
207
|
+
# token stops changing
|
|
208
|
+
new = resp.get("nextForwardToken")
|
|
209
|
+
if not new or new == token:
|
|
210
|
+
break
|
|
211
|
+
token = new
|
|
212
|
+
if done and not ended and stream:
|
|
213
|
+
# the last lines can reach CloudWatch a few seconds after the job ends
|
|
214
|
+
ended = True
|
|
215
|
+
time.sleep(3)
|
|
216
|
+
continue
|
|
217
|
+
if done:
|
|
218
|
+
print(f"[smkflex] head job {jid}: {status}", file=sys.stderr)
|
|
219
|
+
if status == "FAILED" and d.get("statusReason"):
|
|
220
|
+
print(f"[smkflex] {d['statusReason']}", file=sys.stderr)
|
|
221
|
+
return 0 if status == "SUCCEEDED" else 1
|
|
222
|
+
if not follow_logs:
|
|
223
|
+
if not stream:
|
|
224
|
+
print(f"[smkflex] head job {jid} is {status}; no log yet", file=sys.stderr)
|
|
225
|
+
return 0
|
|
226
|
+
if not got:
|
|
227
|
+
time.sleep(5)
|
|
228
|
+
|
|
229
|
+
def _job_clients(a):
|
|
230
|
+
"""Clients for commands that only need a job ID: the profile (as the other commands find
|
|
231
|
+
it) for the region, else the AWS config's; --region overrides both."""
|
|
232
|
+
try:
|
|
233
|
+
_dir, profile = _load_profile(a.profile)
|
|
234
|
+
except Fail:
|
|
235
|
+
if a.profile:
|
|
236
|
+
raise
|
|
237
|
+
profile = {}
|
|
238
|
+
if a.region:
|
|
239
|
+
profile["aws-batch-flex-region"] = a.region
|
|
240
|
+
return _clients(profile)
|
|
241
|
+
|
|
242
|
+
def cmd_logs(a):
|
|
243
|
+
batch, logs, _r = _job_clients(a)
|
|
244
|
+
return follow(batch, logs, a.job_id, a.follow)
|
|
245
|
+
|
|
246
|
+
def cmd_cancel(a):
|
|
247
|
+
batch, _logs, _r = _job_clients(a)
|
|
248
|
+
jobs = batch.describe_jobs(jobs=[a.job_id])["jobs"]
|
|
249
|
+
if not jobs:
|
|
250
|
+
raise Fail(f"no job {a.job_id}")
|
|
251
|
+
status = jobs[0]["status"]
|
|
252
|
+
if status in ("SUCCEEDED", "FAILED"):
|
|
253
|
+
print(f"Head job {a.job_id} has already ended ({status}).")
|
|
254
|
+
return 0
|
|
255
|
+
batch.terminate_job(jobId=a.job_id, reason="smkflex cancel")
|
|
256
|
+
if status == "RUNNING":
|
|
257
|
+
print(f"Asked head job {a.job_id} to stop; Snakemake cancels the workflow's jobs and saves "
|
|
258
|
+
"its state (about a minute).")
|
|
259
|
+
else:
|
|
260
|
+
print(f"Cancelled head job {a.job_id} before it started ({status}); no jobs were submitted.")
|
|
261
|
+
return 0
|
|
262
|
+
|
|
263
|
+
def cmd_status(a):
|
|
264
|
+
_dir, profile = _load_profile(a.profile)
|
|
265
|
+
batch, _logs, _r = _clients(profile)
|
|
266
|
+
queue = a.queue or profile.get("aws-batch-flex-queue")
|
|
267
|
+
if not queue:
|
|
268
|
+
raise Fail("no queue: set aws-batch-flex-queue in the profile or pass --queue")
|
|
269
|
+
rows = list_jobs(batch, jobQueue=queue, filters=[{"name": "JOB_NAME", "values": ["smkflex-*"]}])
|
|
270
|
+
rows = sorted(rows, key=lambda j: j.get("createdAt", 0), reverse=True)[:a.limit]
|
|
271
|
+
if not rows:
|
|
272
|
+
print(f"no smkflex runs in queue {queue}")
|
|
273
|
+
return 0
|
|
274
|
+
# the summaries have no tags; the commit is on the job itself
|
|
275
|
+
tags = {d["jobId"]: d.get("tags") or {} for d in describe_jobs(batch, [j["jobId"] for j in rows])}
|
|
276
|
+
for j in rows:
|
|
277
|
+
when = datetime.fromtimestamp(j.get("createdAt", 0) / 1000).strftime("%Y-%m-%d %H:%M")
|
|
278
|
+
t = tags.get(j["jobId"], {})
|
|
279
|
+
what = j.get("jobName", "")[len("smkflex-"):]
|
|
280
|
+
if t.get("smkflex-commit"):
|
|
281
|
+
what += f" {t['smkflex-commit']}" + (f" ({t['smkflex-source']})" if t.get("smkflex-source") else "")
|
|
282
|
+
print(f"{j['jobId']} {j.get('status', ''):<9} {when} {what}")
|
|
283
|
+
return 0
|
|
284
|
+
|
|
285
|
+
IGNORE_LINES = ("data/", "results/", "resources/", "logs/", ".snakemake/")
|
|
286
|
+
|
|
287
|
+
def cmd_init(a):
|
|
288
|
+
"""Set up this folder as a project: its own folder in the bucket (in the workflow profile
|
|
289
|
+
Snakemake loads by itself, the existing one if the pipeline has one) and a .gitignore that
|
|
290
|
+
keeps data out of smkflex run."""
|
|
291
|
+
_dir, profile = _load_profile(a.profile)
|
|
292
|
+
wp = workflow_profile_file(create=True)
|
|
293
|
+
wp_dir = os.path.dirname(wp)
|
|
294
|
+
current = {}
|
|
295
|
+
if os.path.isfile(wp):
|
|
296
|
+
with open(wp) as f:
|
|
297
|
+
current = yaml.safe_load(f) or {}
|
|
298
|
+
base = str(profile.get("default-storage-prefix") or "").rstrip("/")
|
|
299
|
+
name = re.sub(r"[^A-Za-z0-9_.-]", "-", a.name or os.path.basename(os.path.abspath(".")))
|
|
300
|
+
if current.get("default-storage-prefix"):
|
|
301
|
+
prefix = current["default-storage-prefix"]
|
|
302
|
+
print(f"This project already has its folder in storage: {prefix} (in {wp}); left as it is.")
|
|
303
|
+
else:
|
|
304
|
+
if not base:
|
|
305
|
+
raise Fail("the profile has no default-storage-prefix to put the project's folder under")
|
|
306
|
+
if not os.path.isdir("workflow") and not os.path.isfile("Snakefile"):
|
|
307
|
+
print("(no workflow/ or Snakefile here; is this the project's top folder?)", file=sys.stderr)
|
|
308
|
+
prefix = f"{base}/{name}"
|
|
309
|
+
os.makedirs(wp_dir, exist_ok=True)
|
|
310
|
+
with open(wp, "a") as f:
|
|
311
|
+
f.write(f"default-storage-prefix: {prefix}\n")
|
|
312
|
+
print(f"This project's files go to {prefix} (written to {wp}).")
|
|
313
|
+
lines = []
|
|
314
|
+
if os.path.isfile(".gitignore"):
|
|
315
|
+
with open(".gitignore") as f:
|
|
316
|
+
lines = [l.rstrip("\n") for l in f]
|
|
317
|
+
new = [l for l in IGNORE_LINES if l not in lines and l.rstrip("/") not in lines]
|
|
318
|
+
if new:
|
|
319
|
+
with open(".gitignore", "a") as f:
|
|
320
|
+
if lines and lines[-1].strip():
|
|
321
|
+
f.write("\n")
|
|
322
|
+
f.write("# kept out of smkflex run (data reaches the jobs through storage)\n")
|
|
323
|
+
f.write("".join(l + "\n" for l in new))
|
|
324
|
+
print(f"Added {', '.join(new)} to .gitignore.")
|
|
325
|
+
print("\nNext:\n"
|
|
326
|
+
" edit config/ (the pipeline's docs say what goes there)\n"
|
|
327
|
+
" smkflex push data upload input files (data/ here, as the sample sheets name them)\n"
|
|
328
|
+
" snakemake --profile " + os.path.basename(_dir.rstrip("/")) + " or: smkflex run")
|
|
329
|
+
return 0
|
|
330
|
+
|
|
331
|
+
def main(argv=None):
|
|
332
|
+
p = argparse.ArgumentParser(prog="smkflex", description=__doc__.split("\n\n")[0])
|
|
333
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
334
|
+
r = sub.add_parser("run", help="run the workflow in this folder on AWS Batch; other arguments "
|
|
335
|
+
"(e.g. targets, --forceall) go to snakemake")
|
|
336
|
+
r.add_argument("--profile", help="profile folder (default: ./profile, or $SNAKEMAKE_PROFILE)")
|
|
337
|
+
r.add_argument("--git", metavar="URL", help="run the workflow from this git repository instead of this folder "
|
|
338
|
+
"(cloned here with your git login, so private repositories work)")
|
|
339
|
+
r.add_argument("--rev", help="with --git: branch, tag or commit (default: the default branch)")
|
|
340
|
+
r.add_argument("--path", help="with --git: folder in the repository that has the workflow")
|
|
341
|
+
r.add_argument("--add", action="append", metavar="PATH",
|
|
342
|
+
help="send this local file or folder too, at the same path (with --git: over the "
|
|
343
|
+
"repository's, e.g. --add config/ for your own config); repeatable")
|
|
344
|
+
r.add_argument("--name", help="the run's name in smkflex status and AWS Batch "
|
|
345
|
+
"(default: the folder's or repository's name)")
|
|
346
|
+
r.add_argument("--queue", help="queue for the head job (default: the profile's)")
|
|
347
|
+
r.add_argument("--role", help="IAM role for the head job (default: aws-batch-flex-head-role-arn)")
|
|
348
|
+
r.add_argument("--image", help=f"image for the head job (default: {HEAD_IMAGE}, Debian 12 with git)")
|
|
349
|
+
r.add_argument("--vcpus", type=int, default=1)
|
|
350
|
+
r.add_argument("--mem-mb", type=int, default=2048)
|
|
351
|
+
r.add_argument("--max-bundle-mb", type=int, default=100)
|
|
352
|
+
r.add_argument("--wait", action="store_true", help="stay attached and print the log until the run ends")
|
|
353
|
+
for name, text in (("logs", "print a run's Snakemake log"), ("cancel", "stop a run and its jobs")):
|
|
354
|
+
s = sub.add_parser(name, help=text)
|
|
355
|
+
s.add_argument("job_id")
|
|
356
|
+
s.add_argument("--profile")
|
|
357
|
+
s.add_argument("--region")
|
|
358
|
+
if name == "logs":
|
|
359
|
+
s.add_argument("--follow", "-f", action="store_true")
|
|
360
|
+
s = sub.add_parser("pull", help="download files or folders from the workflow's storage, e.g. results")
|
|
361
|
+
s.add_argument("paths", nargs="*", help="paths as the workflow names them, e.g. results or results/summary.tsv")
|
|
362
|
+
s.add_argument("--profile")
|
|
363
|
+
s.add_argument("--to", default=".", help="folder to download into (default: here)")
|
|
364
|
+
s.add_argument("--prefix", help="storage prefix to download from (default: the profile's default-storage-prefix)")
|
|
365
|
+
s.add_argument("--dry-run", action="store_true", help="only list what would be downloaded")
|
|
366
|
+
s.add_argument("--force", action="store_true", help="download files that are already here too")
|
|
367
|
+
s = sub.add_parser("init", help="set up this folder as a project: its own folder in the bucket, and a "
|
|
368
|
+
".gitignore for data")
|
|
369
|
+
s.add_argument("--profile")
|
|
370
|
+
s.add_argument("--name", help="the project's folder name in the bucket (default: this folder's name)")
|
|
371
|
+
s = sub.add_parser("push", help="upload input files or folders to the workflow's storage, at the same paths")
|
|
372
|
+
s.add_argument("paths", nargs="+", help="local paths as the workflow names them: relative (data, "
|
|
373
|
+
"data/reads.fq) or absolute (/shared/refs), as in the config")
|
|
374
|
+
s.add_argument("--profile")
|
|
375
|
+
s.add_argument("--prefix", help="storage prefix to upload to (default: the profile's default-storage-prefix)")
|
|
376
|
+
s.add_argument("--dry-run", action="store_true", help="only list what would be uploaded")
|
|
377
|
+
s.add_argument("--force", action="store_true", help="upload files that are already there too")
|
|
378
|
+
s = sub.add_parser("check", help="check the setup (AWS login, queue, roles, storage, versions)")
|
|
379
|
+
s.add_argument("--profile")
|
|
380
|
+
s = sub.add_parser("status", help="list recent runs")
|
|
381
|
+
s.add_argument("--profile")
|
|
382
|
+
s.add_argument("--queue")
|
|
383
|
+
s.add_argument("--limit", type=int, default=20)
|
|
384
|
+
|
|
385
|
+
a, extra = p.parse_known_args(argv)
|
|
386
|
+
if extra and a.cmd != "run":
|
|
387
|
+
p.error(f"unrecognized arguments: {' '.join(extra)}")
|
|
388
|
+
if extra[:1] == ["--"]:
|
|
389
|
+
extra = extra[1:]
|
|
390
|
+
try:
|
|
391
|
+
if a.cmd == "run":
|
|
392
|
+
return cmd_run(a, extra)
|
|
393
|
+
return {"logs": cmd_logs, "cancel": cmd_cancel, "status": cmd_status, "check": cmd_check,
|
|
394
|
+
"pull": cmd_pull, "push": cmd_push, "init": cmd_init}[a.cmd](a)
|
|
395
|
+
except Fail as e:
|
|
396
|
+
print(f"smkflex: {e}", file=sys.stderr)
|
|
397
|
+
return 2
|
|
398
|
+
except (ClientError, BotoCoreError) as e:
|
|
399
|
+
print(f"smkflex: AWS error: {e}", file=sys.stderr)
|
|
400
|
+
return 2
|
|
401
|
+
except WorkflowError as e: # a setting in the profile
|
|
402
|
+
print(f"smkflex: {e}", file=sys.stderr)
|
|
403
|
+
return 2
|
|
404
|
+
except KeyboardInterrupt:
|
|
405
|
+
return 130
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
if __name__ == "__main__":
|
|
409
|
+
sys.exit(main())
|