#!/usr/bin/env python import argparse import json import shutil import subprocess from collections import defaultdict from datetime import datetime from pathlib import Path TS_FMT = "%Y%m%d-%H%M%S" class Conda: def __init__(self, root=None, exe=None): self.exe = exe or shutil.which("mamba") or shutil.which("conda") if not self.exe: raise SystemExit("neither conda nor mamba found") root = root or self.get_root() self.root = Path(root) def fork(self, name): envs = self.get_envs() if name not in envs: envs = ", ".join(envs) msg = f'"{name}" is not a known environment.\nChoose from: {envs}' raise SystemExit(msg) basename = name if "@" in name: basename, original_timestamp = name.rsplit("@", 1) original_timestamp = datetime.strptime(original_timestamp, TS_FMT) print(f'extracted basename "{basename}" from {name} (original timestamp: {original_timestamp})') timestamp = datetime.now().strftime(TS_FMT) new = f"{basename}@{timestamp}" self.clone(name, new) def bless(self, name): if "@" not in name: msg = f'"{name}" is not a forked environment' raise SystemExit(msg) basename, timestamp = name.rsplit("@", 1) try: timestamp = datetime.strptime(timestamp, TS_FMT) except ValueError as e: raise SystemExit(e) self.clone(name, basename) def print_envs(self): envs = self.get_envs() for i in envs: print("-", i) def get_envs(self): cmd = [self.exe, "env", "list", "--json"] info = run_and_parse(cmd) envs = info["envs"] envs = sorted(envs) if self.root: envs_root = self.root / "envs" envs = [Path(i) for i in envs] envs = [i.relative_to(envs_root) for i in envs if i.is_relative_to(envs_root)] envs = [str(i) for i in envs] return envs def print_root(self): print(self.root) def get_root(self): info = self.get_info() return info["conda_prefix"] def print_exe(self): print(self.exe) def print_info(self): info = self.get_info() length = maxstrlen(info.keys()) for k, v in sorted(info.items()): k = str(k) + ":" v = str(v) if len(v) >= 100: v = "\n" + v + "\n" print(k.ljust(length), v) def get_info(self): cmd = [self.exe, "info", "--json"] info = run_and_parse(cmd) return info def clone(self, original, new): # conda create --name theclone --clone theoriginal # uses hardlinks # conda create --name thecopy --copy theoriginal # copies files print("clone:", original, "=>", new) cmd = [self.exe, "create", "--name", new, "--clone", original] subprocess.run(cmd) def get_list(self, env): cmd = [self.exe, "list", "--json", "--name", env] pkgs = run_and_parse(cmd) res = defaultdict(list) for p in pkgs: for k in ("name", "version", "build_string", "build_number", "channel"): res[k].append(p[k]) return dict(res) def run_and_parse(cmd): completed = subprocess.run(cmd, capture_output=True) stderr = completed.stderr if stderr: msg = stderr.decode().strip() raise SystemExit(msg) stdout = completed.stdout if stdout: return json.loads(stdout) def maxstrlen(seq): return max(len(str(i)) for i in seq) def handle_clargs(): parser = argparse.ArgumentParser(description="🐍") parser.add_argument("--root", help="conda root folder / prefix (read from the current env, if not given)") parser.add_argument("--exe", help="conda or mamba executable (read from the current env, if not given)") subparsers = parser.add_subparsers(title="commands", dest="command", required=True) parser_envs = subparsers.add_parser("envs", help="print existing envs") parser_root = subparsers.add_parser("root", help="print root") parser_exe = subparsers.add_parser("exe", help="print exe") parser_info = subparsers.add_parser("info", help="print info") parser_fork = subparsers.add_parser("fork", help="fork a conda env") parser_bless = subparsers.add_parser("bless", help="bless a conda env") msg = "name of the conda env" parser_fork.add_argument("name", help=msg) parser_bless.add_argument("name", help=msg) clargs = parser.parse_args() return clargs if __name__ == "__main__": clargs = handle_clargs() conda = Conda(root=clargs.root, exe=clargs.exe) dispatch = { "envs": conda.print_envs, "root": conda.print_root, "exe": conda.print_exe, "info": conda.print_info, "fork": lambda: conda.fork(clargs.name), "bless": lambda: conda.bless(clargs.name) } dispatch[clargs.command]()