almost working impl

This commit is contained in:
setop 2023-02-27 01:53:29 +01:00
parent 89aa834bc7
commit ee25e5dd62
4 changed files with 240 additions and 191 deletions

18
TODO.md
View File

@ -1,15 +1,15 @@
- [ ] implement MVP - [+] implement MVP
- [+] minimal catalog - [+] minimal catalog
- [+] search catalog - [+] search catalog
- [+] local current versions - [+] local current versions
- [ ] latest version, github strategy - [+] latest version, github strategy
- [ ] intall, github strategy - [+] intall, github strategy
- [ ] code style - [*] code style => blake, defaults
- [ ] tests - [ ] tests
- [ ] - [ ] cli x (not there, there with old version, there with latest version)
- [ ] choose forge(s), one will be the golden source of the catalog - [ ] choose forge(s), one will be the golden source of the catalog
* github * github
@ -27,7 +27,8 @@
- [ ] GH release - [ ] GH release
- [ ] pypi package - [ ] pypi package
- [ ] open issue in managed tool forge for them to add cliget install method - [ ] open an issue in managed tool forge asking to add support for `--version` and semver
- [ ] open an issue in managed tool forge asking to add cliget install method
- [ ] communicate on geeks'platform : HN, lobsters, reddit, linuxfr - [ ] communicate on geeks'platform : HN, lobsters, reddit, linuxfr
@ -44,7 +45,4 @@
- [ ] build tests - [ ] build tests
- [ ] async loading of versions ; fill output when available - [ ] async loading of versions ; fill output when available
- [ ] cache GH response for a given time - [ ] cache GH response for a given time
- [ ] explore a way to combine docopt and baker

View File

@ -281,7 +281,7 @@ websocat:
wrk: wrk:
desc: Modern HTTP benchmarking tool desc: Modern HTTP benchmarking tool
github: wg/wrk:wq github: wg/wrk
ww: ww:
name: webwormhole name: webwormhole

152
cliget.py
View File

@ -36,45 +36,70 @@ from subprocess import run, CalledProcessError, TimeoutExpired
from fuzzywuzzy import fuzz from fuzzywuzzy import fuzz
import requests import requests
def trace(*mess): def trace(*mess):
if "TRACE" in os.environ: if "TRACE" in os.environ:
print("TRACE", mess) print("TRACE", mess)
def info(*mess):
print(*mess)
def warn(*mess): def warn(*mess):
print("WARN", mess) print("WARNING:", *mess)
class DotDict(dict): class DotDict(dict):
def __getattr__(self, name): def __getattr__(self, name):
return self[name] if name in self else None return self[name] if name in self else None
def _load_catalog(options)->dict:
catalog=options.get('__catalog', 'catalog.yaml') def _load_catalog(options) -> dict:
catalog = options.get(
"__catalog", "https://codeberg.org/setop/cliget/raw/branch/main/catalog.yaml"
)
if catalog.startswith("http"):
# IMPROVE cache catalog for some time
r = requests.get(catalog)
o = load(r.content, SafeLoader)
else:
o = load(open(catalog), SafeLoader) o = load(open(catalog), SafeLoader)
trace(o) trace(o)
return { k:DotDict(v) for k,v in o.items()} return {k: DotDict(v) for k, v in o.items()}
def _find_semver(s:str) -> VersionInfo:
ver = VersionInfo(0,0,0) def _find_semver(s: str) -> VersionInfo:
ver = VersionInfo(0, 0, 0)
try: try:
ver = VersionInfo.parse(s) ver = VersionInfo.parse(s)
except ValueError: except ValueError:
try: try:
ver = VersionInfo(*list(i.group(0) for i in re.finditer('\d+', s))[:3]) ver = VersionInfo(*list(i.group(0) for i in re.finditer("\d+", s))[:3])
except Exception as e: except Exception as e:
trace("parse error", e) trace("parse error", e)
return ver return ver
def _local_version(cmd): def _local_version(cmd):
ver = VersionInfo(0) ver = VersionInfo(0)
try: try:
first_line = run([cmd, '--version'], input='', text=True, capture_output=True, check=True, timeout=0.1).stdout.split('\n')[0] first_line = run(
trace(cmd, '=>', first_line) [cmd, "--version"],
input="",
text=True,
capture_output=True,
check=True,
timeout=0.1,
).stdout.split("\n")[0]
trace(cmd, "=>", first_line)
ver = _find_semver(first_line) ver = _find_semver(first_line)
except Exception as e: except Exception as e:
trace("run error", e) trace("run error", e)
return ver return ver
def _internal_list(options) -> tuple[str,VersionInfo]:
def _internal_list(options) -> tuple[str, VersionInfo]:
"""list installed tools and their version""" """list installed tools and their version"""
ctl = _load_catalog(options) ctl = _load_catalog(options)
for cli, props in ctl.items(): for cli, props in ctl.items():
@ -90,19 +115,22 @@ def _internal_list(options) -> tuple[str,VersionInfo]:
except FileNotFoundError: except FileNotFoundError:
trace(cli, "not found") trace(cli, "not found")
def dolist(options): def dolist(options):
for (cli, _, ver) in _internal_list(options): for cli, _, ver in _internal_list(options):
print(cli, ver) print(cli, ver)
def doinfo(options): def doinfo(options):
tool = options.TOOL tool = options.TOOL
ctl = _load_catalog(options) ctl = _load_catalog(options)
if tool in ctl: if tool in ctl:
print(tool) print(tool)
for (k,v) in ctl[tool].items(): for k, v in ctl[tool].items():
print(f' {k}: {v}') print(f" {k}: {v}")
else: else:
warn(f'{tool} not in catalog') warn(f"{tool} not in catalog")
def dosearch(options): def dosearch(options):
pat = options.PAT pat = options.PAT
@ -111,33 +139,42 @@ def dosearch(options):
for cli, props in ctl.items(): for cli, props in ctl.items():
trace(cli, props) trace(cli, props)
rtitle = fuzz.ratio(cli, pat) rtitle = fuzz.ratio(cli, pat)
rdesc = fuzz.partial_token_set_ratio(props['desc'], pat) rname = fuzz.ratio(props.name, pat) if 'name' in props else 0
score = rtitle + rdesc if rtitle>60 else rdesc rdesc = fuzz.partial_token_set_ratio(props.desc, pat)
L.append((cli, props['desc'], score)) score = rdesc + rname
score = rtitle + score if rtitle > 60 else score
L.append((cli, props.desc[:50], score))
L = sorted(L, key=lambda x: -x[-1]) L = sorted(L, key=lambda x: -x[-1])
# TODO format a as table L = [[ "cli", "desc", "rel" ]] + L[:10]
print("\n".join("|".join(map(str,l)) for l in L[:10])) from terminaltables import SingleTable
table = SingleTable(L)
table.inner_row_border=False
print(table.table)
def dolistupdate(options): def dolistupdate(options):
print("look for updatables") print("look for updatables")
for (cli, props, ver) in _internal_list(options): for cli, props, ver in _internal_list(options):
# get last version online # get last version online
if props.github: if props.github:
pass pass
pass pass
def _gh_versions(repo:str) -> [VersionInfo|None]:
[owner, repo] = repo.split("/")
url = f'https://api.github.com/repos/{owner}/{repo}/releases'
response = requests.get(url)
return [ _find_semver(o.get("tag_name")) for o in response.json()]
def _gh_version(repo:str) -> [VersionInfo|None]: def _gh_versions(repo: str) -> [VersionInfo | None]:
[owner, repo] = repo.split("/") [owner, repo] = repo.split("/")
url = f'https://api.github.com/repos/{owner}/{repo}/releases/latest' url = f"https://api.github.com/repos/{owner}/{repo}/releases"
response = requests.get(url)
return [_find_semver(o.get("tag_name")) for o in response.json()]
def _gh_version(repo: str) -> [VersionInfo | None]:
[owner, repo] = repo.split("/")
url = f"https://api.github.com/repos/{owner}/{repo}/releases/latest"
response = requests.get(url) response = requests.get(url)
return _find_semver(response.json().get("tag_name")) return _find_semver(response.json().get("tag_name"))
def doversions(options): def doversions(options):
tools = options.TOOLS tools = options.TOOLS
ctl = _load_catalog(options) ctl = _load_catalog(options)
@ -150,7 +187,8 @@ def doversions(options):
trace(rver) trace(rver)
print(f"{cli} | {lver} | {rver}") print(f"{cli} | {lver} | {rver}")
else: else:
warn(f'{tool} not in catalog') warn(f"{tool} not in catalog")
def doallversions(options): def doallversions(options):
tool = options.TOOL tool = options.TOOL
@ -162,7 +200,8 @@ def doallversions(options):
trace(vers) trace(vers)
print("\n".join(vers)) print("\n".join(vers))
else: else:
warn(f'{tool} not in catalog') warn(f"{tool} not in catalog")
def doinstall(options): def doinstall(options):
tools = options.TOOLS tools = options.TOOLS
@ -173,59 +212,68 @@ def doinstall(options):
if props.github: if props.github:
rver = _gh_version(props.github) rver = _gh_version(props.github)
lver = _local_version(tool) lver = _local_version(tool)
trace(lver,rver) trace(lver, rver)
if rver > lver: if rver > lver:
_perform_install(tool, props.github) _perform_gh_install(tool, props.github)
else: else:
warn(f'{tool} not in catalog') info(f"{tool} is already up do date ({lver})")
else:
warn(f'{tool} has no known install strategy')
else:
warn(f"{tool} not in catalog")
def _match_arch_machine(name:str) -> bool:
def _match_arch_machine(name: str) -> bool:
sysname = os.uname().sysname.lower() # os sysname = os.uname().sysname.lower() # os
machine = os.uname().machine.lower() # arch machine = os.uname().machine.lower() # arch
return name.lower().find(sysname)>0 and name.lower().find(machine)>0 return name.lower().find(sysname) > 0 and name.lower().find(machine) > 0
def _perform_install(cli, repo, version=None):
def _perform_gh_install(cli, repo, version=None):
# get asset list # get asset list
[owner, repo] = repo.split("/") url = f"https://api.github.com/repos/{repo}/releases/latest"
url = f'https://api.github.com/repos/{owner}/{repo}/releases/latest'
r = requests.get(url) r = requests.get(url)
assets = r.json()['assets'] assets = r.json()["assets"]
trace(assets) trace(assets)
# select right asset # select right asset
asset = DotDict(next(filter(lambda x: _match_arch_machine(x['name']),assets))) asset = DotDict(next(filter(lambda x: _match_arch_machine(x["name"]), assets)))
trace(asset.name, asset.url) trace(asset.name, asset.url)
# mkdirs # mkdirs
p = os.path p = os.path
home = os.environ["HOME"] home = os.environ["HOME"]
assetshome = p.join(home, '.cache/cliget/assets') assetshome = p.join(home, ".cache/cliget/assets")
os.makedirs(assetshome, exist_ok=True) os.makedirs(assetshome, exist_ok=True)
location = p.join(assetshome, asset.name) location = p.join(assetshome, asset.name)
trace(f"will dl {location}") trace(f"will dl {location}")
# dl asset if not already there # dl asset if not already there
if not p.exists(location): if not p.exists(location):
dlurl = requests.get(asset.url).json()['browser_download_url'] dlurl = requests.get(asset.url).json()["browser_download_url"]
r = requests.get(dlurl, allow_redirects=True, stream=True) r = requests.get(dlurl, allow_redirects=True, stream=True)
with open(location, 'wb') as fd: with open(location, "wb") as fd:
shutil.copyfileobj(r.raw, fd) shutil.copyfileobj(r.raw, fd)
trace("downloaded") trace("downloaded")
# unpack asset # unpack asset
if not asset.name.endswith('.tar.gz'): if not asset.name.endswith(".tar.gz"):
raise ValueError('package type not handled') raise ValueError("package type not handled")
progloc = p.join(home, '.local/program/'+cli) progloc = p.join(home, ".local/programs", cli)
trace("process tgz") trace("process tgz")
os.makedirs(progloc, exist_ok=True) os.makedirs(progloc, exist_ok=True)
run(["tar", "xfz", location, "-C", progloc]) run(["tar", "xfz", location, "-C", progloc])
# symlink # symlink
os.symlink(p.join(progloc, cli), p.join(home, '.local/bin', cli)) # TODO remove existing symlink
os.symlink(p.join('../programs', cli, cli), p.join(home, ".local/bin", cli))
if __name__ == '__main__':
if __name__ == "__main__":
if "DEBUG" in os.environ: if "DEBUG" in os.environ:
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)
logging.debug("debug is on") logging.debug("debug is on")
else: else:
logging.info("not in debug") logging.info("not in debug")
options = docopt(__doc__, version='Cliget 0.1.0') options = docopt(__doc__, version="Cliget 0.1.0")
options = DotDict({k.replace('-','_'):v for (k,v) in options.items() if v is not None}) options = DotDict(
{k.replace("-", "_"): v for (k, v) in options.items() if v is not None}
)
trace(options) trace(options)
if options.info: if options.info:
doinfo(options) doinfo(options)
@ -238,9 +286,9 @@ if __name__ == '__main__':
elif options.allversions: elif options.allversions:
doallversions(options) doallversions(options)
elif options.install or options.update: elif options.install or options.update:
if not options.__all and len(options.TOOLS)==0: if not options.__all and len(options.TOOLS) == 0:
dolistupdate(options) dolistupdate(options)
elif len(options.TOOLS)>0: elif len(options.TOOLS) > 0:
doinstall(options) doinstall(options)
else: else:
print("not implemented") print("update all not implemented")

View File

@ -5,3 +5,6 @@ semver
thefuzz thefuzz
#python-Levenshtein #python-Levenshtein
requests requests
#tabulate
#termtables
terminaltables