mirror of
https://github.com/daylinmorgan/viv.git
synced 2024-12-22 02:30:44 -06:00
feat: add filtering to viv list
This commit is contained in:
parent
6dfc9c566a
commit
dadf4dacbf
1 changed files with 202 additions and 35 deletions
237
src/viv/viv.py
237
src/viv/viv.py
|
@ -53,7 +53,7 @@ from typing import (
|
|||
from urllib.error import HTTPError
|
||||
from urllib.request import urlopen
|
||||
|
||||
__version__ = "23.8a1-8-g156f358-dev"
|
||||
__version__ = "23.8a1-10-g7cedb07-dev"
|
||||
|
||||
|
||||
class Spinner:
|
||||
|
@ -146,19 +146,6 @@ class Env:
|
|||
return _path_ok(Path(self.xdg_data_home) / "viv") / "viv.log"
|
||||
|
||||
|
||||
class Cache:
|
||||
def __init__(self) -> None:
|
||||
self.base = Env().viv_cache
|
||||
|
||||
@property
|
||||
def src(self) -> Path:
|
||||
return _path_ok(self.base / "src")
|
||||
|
||||
@property
|
||||
def venv(self) -> Path:
|
||||
return _path_ok(self.base / "venvs")
|
||||
|
||||
|
||||
class Cfg:
|
||||
@property
|
||||
def src(self) -> Path:
|
||||
|
@ -166,6 +153,18 @@ class Cfg:
|
|||
p.parent.mkdir(exist_ok=True, parents=True)
|
||||
return p
|
||||
|
||||
@property
|
||||
def cache_base(self) -> Path:
|
||||
return Env().viv_cache
|
||||
|
||||
@property
|
||||
def cache_src(self) -> Path:
|
||||
return _path_ok(self.cache_base / "src")
|
||||
|
||||
@property
|
||||
def cache_venv(self) -> Path:
|
||||
return _path_ok(self.cache_base / "venvs")
|
||||
|
||||
|
||||
class Ansi:
|
||||
"""control ouptut of ansi(VT100) control codes"""
|
||||
|
@ -503,7 +502,7 @@ if __name__ == "__main__":
|
|||
("CLI", cli),
|
||||
("Running Source", running),
|
||||
("Local Source", local),
|
||||
("Cache", Cache().base),
|
||||
("Cache", Cfg().cache_base),
|
||||
)
|
||||
)
|
||||
+ "\n"
|
||||
|
@ -668,6 +667,39 @@ class CustomHelpFormatter(RawDescriptionHelpFormatter, HelpFormatter):
|
|||
self._add_item(self._format_action, [action])
|
||||
|
||||
|
||||
class KVAppendAction(Action):
|
||||
def __init__(self, *args, keys, **kwargs):
|
||||
self._keys = keys
|
||||
super(KVAppendAction, self).__init__(*args, **kwargs)
|
||||
|
||||
# TODO: add choices option to class?
|
||||
def __call__(self, parser, namespace, values, option_string=None):
|
||||
try:
|
||||
(k, v) = values.split(":")
|
||||
if k not in self._keys:
|
||||
err_quit(
|
||||
"".join(
|
||||
(
|
||||
f"unexpected key: {a.yellow}{k}{a.end} for {self.dest},",
|
||||
" must be one of: ",
|
||||
", ".join((a.style(k, "bold") for k in self._keys)),
|
||||
)
|
||||
)
|
||||
)
|
||||
d = {k: v}
|
||||
except ValueError:
|
||||
err_quit(
|
||||
f"failed to parse key-value for {self.dest}"
|
||||
f'"{a.bold}{values}{a.end}" as k:v'
|
||||
)
|
||||
items = getattr(namespace, self.dest)
|
||||
if not items:
|
||||
items = {}
|
||||
|
||||
items.update(d)
|
||||
setattr(namespace, self.dest, items)
|
||||
|
||||
|
||||
class ArgumentParser(StdArgParser):
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
@ -768,18 +800,18 @@ class Meta:
|
|||
|
||||
@classmethod
|
||||
def load(cls, name: str) -> "Meta":
|
||||
if not (Cache().venv / name / "vivmeta.json").exists():
|
||||
if not (Cfg().cache_venv / name / "vivmeta.json").exists():
|
||||
log.warning(f"possibly corrupted vivenv: {name}")
|
||||
# add empty values for corrupted vivenvs so it will still load
|
||||
return cls(name=name, spec=[""], files=[""], exe="", id="")
|
||||
else:
|
||||
meta = json.loads((Cache().venv / name / "vivmeta.json").read_text())
|
||||
meta = json.loads((Cfg().cache_venv / name / "vivmeta.json").read_text())
|
||||
|
||||
return cls(**meta)
|
||||
|
||||
def write(self, p: Path | None = None) -> None:
|
||||
if not p:
|
||||
p = (Cache().venv) / self.name / "vivmeta.json"
|
||||
p = (Cfg().cache_venv) / self.name / "vivmeta.json"
|
||||
|
||||
p.write_text(json.dumps(self.__dict__))
|
||||
|
||||
|
@ -810,7 +842,7 @@ class ViVenv:
|
|||
self.set_path(path)
|
||||
|
||||
if not metadata:
|
||||
if self.name in (d.name for d in Cache().venv.iterdir()):
|
||||
if self.name in (d.name for d in Cfg().cache_venv.iterdir()):
|
||||
self.loaded = True
|
||||
self.meta = Meta.load(self.name)
|
||||
else:
|
||||
|
@ -835,7 +867,7 @@ class ViVenv:
|
|||
return vivenv
|
||||
|
||||
def set_path(self, path: Path | None = None) -> None:
|
||||
self.path = path if path else Cache().venv / self.name
|
||||
self.path = path if path else Cfg().cache_venv / self.name
|
||||
self.python = str((self.path / "bin" / "python").absolute())
|
||||
self.pip = ("pip", "--python", self.python)
|
||||
|
||||
|
@ -906,6 +938,9 @@ class ViVenv:
|
|||
sys.path = [p for p in (path_to_add, *sys.path) if p != site.USER_SITE]
|
||||
site.addsitedir(path_to_add)
|
||||
|
||||
def files_exist(self) -> bool:
|
||||
return len([f for f in self.meta.files if Path(f).is_file()]) == 0
|
||||
|
||||
def show(self) -> None:
|
||||
_id = (
|
||||
self.meta.id[:8]
|
||||
|
@ -978,7 +1013,7 @@ def use(*packages: str, track_exe: bool = False, name: str = "") -> Path:
|
|||
|
||||
def get_venvs() -> Dict[str, ViVenv]:
|
||||
vivenvs = {}
|
||||
for p in Cache().venv.iterdir():
|
||||
for p in Cfg().cache_venv.iterdir():
|
||||
vivenv = ViVenv.load(p.name)
|
||||
vivenvs[vivenv.name] = vivenv
|
||||
return vivenvs
|
||||
|
@ -1036,7 +1071,7 @@ def fetch_source(reference: str) -> str:
|
|||
(hash := hashlib.sha256()).update(src.encode())
|
||||
sha256 = hash.hexdigest()
|
||||
|
||||
cached_src_file = Cache().src / f"{sha256}.py"
|
||||
cached_src_file = Cfg().cache_src / f"{sha256}.py"
|
||||
|
||||
if not cached_src_file.is_file():
|
||||
cached_src_file.write_text(src)
|
||||
|
@ -1099,10 +1134,105 @@ def deps_block(txt: str) -> List[str]:
|
|||
)
|
||||
|
||||
|
||||
def _parse_date(txt: str) -> datetime:
|
||||
"""attempt to parse datetime string
|
||||
|
||||
acceptable formats `%Y-%m-%d` & `%Y-%m-%dT%H:%M`
|
||||
"""
|
||||
|
||||
try:
|
||||
date = datetime.strptime(
|
||||
txt,
|
||||
"%Y-%m-%d",
|
||||
)
|
||||
return date
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
try:
|
||||
datetime.strptime(txt, "%Y-%m-%dT%H:%M")
|
||||
return date
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
err_quit(
|
||||
f"failed to parse {a.yellow}{txt}{a.end} as datetime\n"
|
||||
"acceptable formats `%Y-%m-%d` & `%Y-%m-%dT%H:%M`"
|
||||
)
|
||||
|
||||
|
||||
class Cache:
|
||||
def __init__(self) -> None:
|
||||
self.vivenvs = self._get_venvs()
|
||||
|
||||
def _get_venvs(self) -> Dict[str, ViVenv]:
|
||||
vivenvs = {}
|
||||
for p in Cfg().cache_venv.iterdir():
|
||||
vivenv = ViVenv.load(p.name)
|
||||
vivenvs[vivenv.name] = vivenv
|
||||
return vivenvs
|
||||
|
||||
@staticmethod
|
||||
def _compare_dates(
|
||||
vivenv: ViVenv, date_name: str, when: str, date: datetime
|
||||
) -> bool:
|
||||
vivenv_date = datetime.strptime(
|
||||
getattr(vivenv.meta, date_name), "%Y-%m-%d %H:%M:%S.%f"
|
||||
)
|
||||
if when == "before":
|
||||
return vivenv_date < date
|
||||
else:
|
||||
return vivenv_date > date
|
||||
|
||||
def _filter_date(self, date_name: str, when: str, date: datetime) -> List[ViVenv]:
|
||||
return {
|
||||
vivenv
|
||||
for _, vivenv in self.vivenvs.items()
|
||||
if self._compare_dates(
|
||||
vivenv,
|
||||
date_name,
|
||||
when,
|
||||
date,
|
||||
)
|
||||
}
|
||||
|
||||
def _filter_file(self, file: str) -> List[ViVenv]:
|
||||
if file == "None":
|
||||
return {
|
||||
vivenv for _, vivenv in self.vivenvs.items() if vivenv.files_exist()
|
||||
}
|
||||
else:
|
||||
p = Path(file).absolute().resolve()
|
||||
if not p.is_file():
|
||||
err_quit(f"Unable to find local file: {file}")
|
||||
return {
|
||||
vivenv
|
||||
for _, vivenv in self.vivenvs.items()
|
||||
if str(p) in vivenv.meta.files
|
||||
}
|
||||
|
||||
def filter(self, filters=Dict[str, str]) -> Dict[str, ViVenv]:
|
||||
vivenv_sets = []
|
||||
|
||||
for k, v in filters.items():
|
||||
if "-" in k: # date-based filters all have hyphen
|
||||
(date_name, when) = k.split("-")
|
||||
vivenv_sets.append(self._filter_date(date_name, when, _parse_date(v)))
|
||||
else:
|
||||
vivenv_sets.append(self._filter_file(v))
|
||||
|
||||
if vivenv_sets:
|
||||
return {vivenv.name: vivenv for vivenv in set.union(*vivenv_sets)}
|
||||
else:
|
||||
return {}
|
||||
|
||||
|
||||
class Viv:
|
||||
def __init__(self) -> None:
|
||||
self.t = Template()
|
||||
self.vivenvs = get_venvs()
|
||||
self._cache = Cache()
|
||||
# compat layer
|
||||
self.vivenvs = self._cache.vivenvs
|
||||
self._get_sources()
|
||||
self.name = "viv" if self.local else "python3 <(curl -fsSL viv.dayl.in/viv.py)"
|
||||
|
||||
|
@ -1208,22 +1338,45 @@ class Viv:
|
|||
|
||||
sys.stdout.write(self.t.frozen_import(path, self.local_source, spec))
|
||||
|
||||
def list(self, quiet: bool, full: bool, use_json: bool) -> None:
|
||||
"""list all vivenvs"""
|
||||
def list(
|
||||
self,
|
||||
quiet: bool,
|
||||
verbose: bool,
|
||||
use_json: bool,
|
||||
filter: List[str],
|
||||
) -> None:
|
||||
"""\
|
||||
list vivenvs
|
||||
|
||||
examples:
|
||||
`viv list \\
|
||||
--filter "accessed-after:2023-08-01"`
|
||||
`viv list -q --filter \\
|
||||
"created-before:$(date -d '2 weeks ago' +'%Y-%m-%d')"`
|
||||
`viv list --filter "file:./script.py"`
|
||||
`viv list --filter "file:None"`
|
||||
"""
|
||||
|
||||
if filter:
|
||||
vivenvs = self._cache.filter(filter)
|
||||
else:
|
||||
vivenvs = self._cache.vivenvs
|
||||
|
||||
if quiet:
|
||||
sys.stdout.write("\n".join(self.vivenvs) + "\n")
|
||||
elif len(self.vivenvs) == 0:
|
||||
sys.stdout.write("\n".join(vivenvs) + "\n")
|
||||
elif len(self._cache.vivenvs) == 0:
|
||||
log.info("no vivenvs setup")
|
||||
elif full:
|
||||
for _, vivenv in self.vivenvs.items():
|
||||
elif len(vivenvs) == 0 and filter:
|
||||
log.info("no vivenvs match filter")
|
||||
elif verbose:
|
||||
for _, vivenv in vivenvs.items():
|
||||
vivenv.tree()
|
||||
elif use_json:
|
||||
sys.stdout.write(
|
||||
json.dumps({k: v.meta.__dict__ for k, v in self.vivenvs.items()})
|
||||
json.dumps({k: v.meta.__dict__ for k, v in vivenvs.items()})
|
||||
)
|
||||
else:
|
||||
for _, vivenv in self.vivenvs.items():
|
||||
for _, vivenv in vivenvs.items():
|
||||
vivenv.show()
|
||||
|
||||
def exe(self, vivenv_id: str, cmd: str, rest: List[str]) -> None:
|
||||
|
@ -1554,17 +1707,31 @@ class Cli:
|
|||
args = {
|
||||
("list",): [
|
||||
Arg(
|
||||
"-f",
|
||||
"--full",
|
||||
help="show full metadata for vivenvs",
|
||||
"-v",
|
||||
"--verbose",
|
||||
help="pretty print full metadata for vivenvs",
|
||||
action="store_true",
|
||||
),
|
||||
Arg(
|
||||
"-q",
|
||||
"--quiet",
|
||||
help="suppress non-essential output",
|
||||
help="show only ids",
|
||||
action="store_true",
|
||||
),
|
||||
Arg(
|
||||
"-f",
|
||||
"--filter",
|
||||
help="filter vivenvs based on key:val",
|
||||
metavar="<key:value>",
|
||||
action=KVAppendAction,
|
||||
keys=[
|
||||
"created-before",
|
||||
"created-after",
|
||||
"accessed-before",
|
||||
"accessed-after",
|
||||
"files",
|
||||
],
|
||||
),
|
||||
],
|
||||
("shim",): [
|
||||
Arg(
|
||||
|
|
Loading…
Reference in a new issue