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.
Files changed (30) hide show
  1. snakemake_executor_plugin_aws_batch_flex/__init__.py +8 -0
  2. snakemake_executor_plugin_aws_batch_flex/bootstrap.py +376 -0
  3. snakemake_executor_plugin_aws_batch_flex/bundle.py +142 -0
  4. snakemake_executor_plugin_aws_batch_flex/check.py +129 -0
  5. snakemake_executor_plugin_aws_batch_flex/condacache.py +184 -0
  6. snakemake_executor_plugin_aws_batch_flex/executor.py +725 -0
  7. snakemake_executor_plugin_aws_batch_flex/headjob.py +244 -0
  8. snakemake_executor_plugin_aws_batch_flex/hints.py +75 -0
  9. snakemake_executor_plugin_aws_batch_flex/hostenv.py +221 -0
  10. snakemake_executor_plugin_aws_batch_flex/imageinfo.py +207 -0
  11. snakemake_executor_plugin_aws_batch_flex/jobdefs.py +221 -0
  12. snakemake_executor_plugin_aws_batch_flex/jobfixes.py +65 -0
  13. snakemake_executor_plugin_aws_batch_flex/jobmonitor.py +146 -0
  14. snakemake_executor_plugin_aws_batch_flex/naming.py +16 -0
  15. snakemake_executor_plugin_aws_batch_flex/orphans.py +51 -0
  16. snakemake_executor_plugin_aws_batch_flex/patches.py +196 -0
  17. snakemake_executor_plugin_aws_batch_flex/preflight.py +165 -0
  18. snakemake_executor_plugin_aws_batch_flex/profiles.py +98 -0
  19. snakemake_executor_plugin_aws_batch_flex/remote.py +409 -0
  20. snakemake_executor_plugin_aws_batch_flex/settings.py +482 -0
  21. snakemake_executor_plugin_aws_batch_flex/sizing.py +75 -0
  22. snakemake_executor_plugin_aws_batch_flex/sources.py +83 -0
  23. snakemake_executor_plugin_aws_batch_flex/tags.py +76 -0
  24. snakemake_executor_plugin_aws_batch_flex/transfer.py +193 -0
  25. snakemake_executor_plugin_aws_batch_flex/usage.py +208 -0
  26. snakemake_executor_plugin_aws_batch_flex-0.1.0.dist-info/METADATA +343 -0
  27. snakemake_executor_plugin_aws_batch_flex-0.1.0.dist-info/RECORD +30 -0
  28. snakemake_executor_plugin_aws_batch_flex-0.1.0.dist-info/WHEEL +4 -0
  29. snakemake_executor_plugin_aws_batch_flex-0.1.0.dist-info/entry_points.txt +3 -0
  30. snakemake_executor_plugin_aws_batch_flex-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,184 @@
1
+ """Reuse conda environments between jobs instead of building them in every job.
2
+
3
+ Runs inside the job (in Snakemake's Python, from the job's launcher). Environments are built
4
+ under one fixed folder, so Snakemake gives the same environment the same name in every job
5
+ (its name is a hash of the definition and the folder). Before building one, a job looks:
6
+
7
+ 1. in that folder: on EC2 it's the instance's /var/cache/smkflex-conda, shared by the jobs
8
+ on that machine (a lock makes the others wait while one builds);
9
+ 2. in S3, if a cache URL is set: <url>/<platform>/<name>.tar.gz, uploaded by an earlier job.
10
+
11
+ The folder (<platform>) is the CPU type and the image's glibc version, e.g. x86_64-glibc-2.36,
12
+ so environments are only shared between machines and images that conda would have built the
13
+ same packages for: an x86 build doesn't run on Graviton (arm64).
14
+
15
+ Only the standard library and boto3 (which comes with the S3 storage plugin) are used."""
16
+
17
+ import fcntl
18
+ import os
19
+ import platform
20
+ import shutil
21
+ import sys
22
+ import tarfile
23
+ import tempfile
24
+
25
+ ROOT = os.environ.get("SMK_FLEX_CONDA_ROOT", "/tmp/smkflex-conda")
26
+
27
+
28
+ def platform_key():
29
+ try:
30
+ version = os.confstr("CS_GNU_LIBC_VERSION") or ""
31
+ except (ValueError, OSError, AttributeError):
32
+ version = ""
33
+ return f"{platform.machine() or 'unknown'}-{version.replace(' ', '-') or 'unknown'}"
34
+
35
+
36
+ def env_prefix(root=None):
37
+ return os.path.join(root or ROOT, platform_key())
38
+
39
+
40
+ def _say(msg):
41
+ print(f"[snakemake-batch] {msg}", file=sys.stderr, flush=True)
42
+
43
+
44
+ def _s3(url):
45
+ import boto3
46
+
47
+ bucket, _, key = url[len("s3://"):].partition("/")
48
+ return boto3.client("s3"), bucket, key
49
+
50
+
51
+ def _fetch(url, dest):
52
+ """Copy the cached archive at url (s3:// or a folder path) to dest; False if there's none."""
53
+ if url.startswith("s3://"):
54
+ s3, bucket, key = _s3(url)
55
+ from botocore.exceptions import ClientError
56
+
57
+ try:
58
+ s3.download_file(bucket, key, dest)
59
+ except ClientError as e:
60
+ if e.response.get("Error", {}).get("Code") in ("404", "NoSuchKey"):
61
+ return False
62
+ raise
63
+ return True
64
+ if not os.path.isfile(url):
65
+ return False
66
+ shutil.copyfile(url, dest)
67
+ return True
68
+
69
+
70
+ def _store(src, url):
71
+ if url.startswith("s3://"):
72
+ s3, bucket, key = _s3(url)
73
+ s3.upload_file(src, bucket, key)
74
+ else:
75
+ os.makedirs(os.path.dirname(url), exist_ok=True)
76
+ tmp = url + ".part"
77
+ shutil.copyfile(src, tmp)
78
+ os.replace(tmp, url)
79
+
80
+
81
+ def restore(url, prefix, name):
82
+ """Unpack the cached environment `name` into prefix. True if it's there afterwards."""
83
+ fd, archive = tempfile.mkstemp(dir=prefix, suffix=".tar.gz")
84
+ os.close(fd)
85
+ try:
86
+ if not _fetch(url, archive):
87
+ return False
88
+ with tarfile.open(archive) as t:
89
+ members = t.getmembers()
90
+ if not members or any(not (m.name == name or m.name.startswith(name + "/")
91
+ or m.name == name + ".env_setup_done") for m in members):
92
+ raise ValueError(f"unexpected contents in {url}")
93
+ # the flag file comes last in the archive, so a half-unpacked env has none and
94
+ # Snakemake removes and rebuilds it
95
+ if hasattr(tarfile, "data_filter"):
96
+ # "tar" keeps the env's own permissions and links, unlike the "data" default
97
+ t.extractall(prefix, members=members, filter="tar")
98
+ else:
99
+ t.extractall(prefix, members=members)
100
+ return os.path.exists(os.path.join(prefix, name + ".env_setup_done"))
101
+ finally:
102
+ os.remove(archive)
103
+
104
+
105
+ def save(url, prefix, name):
106
+ fd, archive = tempfile.mkstemp(dir=prefix, suffix=".tar.gz")
107
+ os.close(fd)
108
+ try:
109
+ with tarfile.open(archive, "w:gz", compresslevel=1) as t:
110
+ t.add(os.path.join(prefix, name), arcname=name)
111
+ t.add(os.path.join(prefix, name + ".env_setup_done"), arcname=name + ".env_setup_done")
112
+ _store(archive, url)
113
+ finally:
114
+ os.remove(archive)
115
+
116
+
117
+ def wrap_create(orig, cache_url=None):
118
+ """Wrap snakemake.deployment.conda.Env.create with the instance folder and the S3 cache."""
119
+
120
+ def create(self, dryrun=False):
121
+ if dryrun or getattr(self, "name", None) or getattr(self, "dir", None) or self.is_containerized:
122
+ return orig(self, dryrun)
123
+ path = self.address
124
+ prefix, name = os.path.dirname(path), os.path.basename(path)
125
+ done = path + ".env_setup_done"
126
+ os.makedirs(prefix, exist_ok=True)
127
+ with open(path + ".smkflex-lock", "w") as lock:
128
+ fcntl.flock(lock, fcntl.LOCK_EX) # another job on this machine may be building it
129
+ if os.path.isdir(path) and os.path.exists(done):
130
+ _say(f"conda env {name} was built by an earlier job on this machine")
131
+ return path
132
+ key = f"{cache_url.rstrip('/')}/{platform_key()}/{name}.tar.gz" if cache_url else None
133
+ if key:
134
+ try:
135
+ if restore(key, prefix, name):
136
+ _say(f"conda env {name} restored from {key}")
137
+ return path
138
+ except Exception as e:
139
+ _say(f"could not restore conda env {name} from the cache ({e!r}); building it")
140
+ shutil.rmtree(path, ignore_errors=True)
141
+ result = orig(self, dryrun)
142
+ if key and os.path.exists(done):
143
+ try:
144
+ save(key, prefix, name)
145
+ _say(f"conda env {name} saved to {key}")
146
+ except Exception as e:
147
+ _say(f"could not save conda env {name} to the cache: {e!r}")
148
+ return result
149
+
150
+ create._smkflex = True
151
+ return create
152
+
153
+
154
+ def install(argv, setting):
155
+ """Turn the cache on for this job: set Snakemake's conda prefix (unless the job has one)
156
+ and wrap Env.create. setting is 'instance' or an S3 URL; anything going wrong leaves
157
+ the job as it was."""
158
+ # --deploy-sources only unpacks the workflow before the job's real run: no envs there
159
+ if "--conda-prefix" in argv or "--deploy-sources" in argv:
160
+ return argv
161
+ try:
162
+ prefix = env_prefix()
163
+ os.makedirs(prefix, exist_ok=True)
164
+ # can this job write there? (a unique name: jobs in other containers on the same
165
+ # machine can have the same process ID)
166
+ fd, probe = tempfile.mkstemp(dir=prefix, prefix=".write-test-")
167
+ os.close(fd)
168
+ os.remove(probe)
169
+ import inspect
170
+
171
+ from snakemake.deployment import conda as smk_conda
172
+
173
+ env = smk_conda.Env
174
+ # what wrap_create was written for
175
+ if (list(inspect.signature(env.create).parameters)[:2] != ["self", "dryrun"]
176
+ or not all(hasattr(env, a) for a in ("address", "is_containerized"))):
177
+ raise RuntimeError("this Snakemake's conda Env changed")
178
+ if not getattr(smk_conda.Env.create, "_smkflex", False):
179
+ url = setting if setting.startswith("s3://") or setting.startswith("/") else None
180
+ smk_conda.Env.create = wrap_create(smk_conda.Env.create, url)
181
+ except Exception as e:
182
+ _say(f"conda env cache off for this job: {e!r}")
183
+ return argv
184
+ return argv + ["--conda-prefix", prefix]