clizard 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.
- clizard/__init__.py +6 -0
- clizard/__main__.py +146 -0
- clizard/cli_args.py +95 -0
- clizard/clizard_file.py +55 -0
- clizard/config.py +50 -0
- clizard/core.py +359 -0
- clizard/discover.py +349 -0
- clizard/examples/examples_auto_cli_release_tool.py +31 -0
- clizard/examples/examples_llmlight_app.py +42 -0
- clizard/examples/examples_summarizer.py +16 -0
- clizard/examples/examples_wrap_summarizer.py +63 -0
- clizard/git_info.py +64 -0
- clizard/project_info.py +41 -0
- clizard/scaffold.py +187 -0
- clizard-0.1.0.dist-info/METADATA +174 -0
- clizard-0.1.0.dist-info/RECORD +20 -0
- clizard-0.1.0.dist-info/WHEEL +5 -0
- clizard-0.1.0.dist-info/entry_points.txt +3 -0
- clizard-0.1.0.dist-info/licenses/LICENSE +22 -0
- clizard-0.1.0.dist-info/top_level.txt +1 -0
clizard/__init__.py
ADDED
clizard/__main__.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Default console-script entry point: `clizard`.
|
|
2
|
+
|
|
3
|
+
Run with no args inside a repo to auto-discover everything:
|
|
4
|
+
- the repo's main() (via __main__.py / main.py signature)
|
|
5
|
+
- a Snakemake workflow (Snakefile + config.yaml), if present
|
|
6
|
+
- git remote info (.git/config)
|
|
7
|
+
- pyproject.toml metadata (name, docs url, requirements)
|
|
8
|
+
- .clizard overrides (ascii art, accent color, tips, app name, docs url)
|
|
9
|
+
|
|
10
|
+
Falls back to a bare GenericCLI if nothing is discoverable.
|
|
11
|
+
"""
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from .cli_args import parse_args
|
|
17
|
+
from .core import GenericCLI
|
|
18
|
+
from .git_info import get_git_info
|
|
19
|
+
from .project_info import get_project_info
|
|
20
|
+
from .clizard_file import ensure_clizard_file
|
|
21
|
+
from .discover import (
|
|
22
|
+
find_main, settings_from_main,
|
|
23
|
+
find_snakemake_config, settings_from_snakemake_config, write_snakemake_config,
|
|
24
|
+
)
|
|
25
|
+
from .scaffold import generate_clizard_main
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def build_cli(repo_path="."):
|
|
29
|
+
repo_path = str(Path(repo_path).resolve())
|
|
30
|
+
|
|
31
|
+
git_info = get_git_info(repo_path)
|
|
32
|
+
proj_info = get_project_info(repo_path)
|
|
33
|
+
clz = ensure_clizard_file(repo_path)
|
|
34
|
+
|
|
35
|
+
app_name = clz.get("app_name") or proj_info.get("name") or git_info.get("github_repo") or "clizard"
|
|
36
|
+
docs_url = clz.get("docs_url") or proj_info.get("docs_url")
|
|
37
|
+
|
|
38
|
+
module, main_func, entry_file = find_main(repo_path)
|
|
39
|
+
main_settings, arg_meta, call_style = settings_from_main(main_func) if main_func else ({}, {}, "kwargs")
|
|
40
|
+
|
|
41
|
+
sm_config_path = find_snakemake_config(repo_path)
|
|
42
|
+
sm_settings = settings_from_snakemake_config(sm_config_path) if sm_config_path else {}
|
|
43
|
+
|
|
44
|
+
settings = {
|
|
45
|
+
"path": repo_path,
|
|
46
|
+
"docs_url": docs_url or "",
|
|
47
|
+
**main_settings,
|
|
48
|
+
**sm_settings,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
has_run_target = main_func is not None or sm_config_path is not None
|
|
52
|
+
default_tips = ["/wizard", "/run", "/settings", "/help"] if has_run_target else ["/settings", "/help"]
|
|
53
|
+
|
|
54
|
+
cli = GenericCLI(
|
|
55
|
+
app_name=app_name,
|
|
56
|
+
ascii_art=clz.get("ascii_art"),
|
|
57
|
+
accent_color=clz.get("accent_color", "#d97757"),
|
|
58
|
+
settings=settings,
|
|
59
|
+
tips=clz.get("tips") if clz.get("tips") else default_tips,
|
|
60
|
+
updates=clz.get("updates"),
|
|
61
|
+
)
|
|
62
|
+
cli.arg_meta = arg_meta
|
|
63
|
+
|
|
64
|
+
# Config persists settings across runs (so a value the user explicitly
|
|
65
|
+
# set via /settings, e.g. username="test", survives between sessions).
|
|
66
|
+
# But that means a key first discovered with no default (None) stays
|
|
67
|
+
# None forever in the persisted store, even after main()'s source gains
|
|
68
|
+
# an explicit default later (e.g. clean=True, verbosity=3). Backfill: if
|
|
69
|
+
# the persisted value is still None, adopt the freshly discovered
|
|
70
|
+
# default instead of leaving it stuck.
|
|
71
|
+
for key, val in {**main_settings, **sm_settings}.items():
|
|
72
|
+
if cli.config.get(key) is None and val is not None:
|
|
73
|
+
cli.config.set(key, val)
|
|
74
|
+
|
|
75
|
+
# Only show /run if there's actually something to run.
|
|
76
|
+
if not has_run_target:
|
|
77
|
+
cli._commands.pop("/run", None)
|
|
78
|
+
if "/run" in cli.tips:
|
|
79
|
+
cli.tips = [t for t in cli.tips if t != "/run"]
|
|
80
|
+
|
|
81
|
+
if has_run_target:
|
|
82
|
+
@cli.command("/run", "Run the project's main()/Snakemake workflow with current settings")
|
|
83
|
+
def _cmd_run(prompt):
|
|
84
|
+
if main_func is not None:
|
|
85
|
+
cli.status("Running main()...")
|
|
86
|
+
if call_style == "argv":
|
|
87
|
+
argv = ["clizard"]
|
|
88
|
+
for name, meta in arg_meta.items():
|
|
89
|
+
flag = meta.get("flag")
|
|
90
|
+
if not flag:
|
|
91
|
+
continue
|
|
92
|
+
val = cli.config.get(name)
|
|
93
|
+
if val is None:
|
|
94
|
+
continue
|
|
95
|
+
if meta.get("is_flag"):
|
|
96
|
+
if val:
|
|
97
|
+
argv.append(flag)
|
|
98
|
+
else:
|
|
99
|
+
argv.extend([flag, str(val)])
|
|
100
|
+
old_argv = sys.argv
|
|
101
|
+
sys.argv = argv
|
|
102
|
+
try:
|
|
103
|
+
result = main_func()
|
|
104
|
+
finally:
|
|
105
|
+
sys.argv = old_argv
|
|
106
|
+
else:
|
|
107
|
+
call_kwargs = {k: cli.config.get(k) for k in main_settings}
|
|
108
|
+
result = main_func(**call_kwargs)
|
|
109
|
+
if result is not None:
|
|
110
|
+
cli.assistant_message(str(result))
|
|
111
|
+
|
|
112
|
+
if sm_config_path is not None:
|
|
113
|
+
current_sm = {k: cli.config.get(k) for k in sm_settings}
|
|
114
|
+
write_snakemake_config(sm_config_path, current_sm)
|
|
115
|
+
cmd = ["snakemake", "--configfile", str(sm_config_path), "--cores", "all"]
|
|
116
|
+
console_cmd = " ".join(cmd)
|
|
117
|
+
cli.status(f"Running: {console_cmd}")
|
|
118
|
+
try:
|
|
119
|
+
result = subprocess.run(cmd, cwd=repo_path, capture_output=True, text=True)
|
|
120
|
+
output = (result.stdout + result.stderr).strip()
|
|
121
|
+
cli.assistant_message(f"```\n$ {console_cmd}\n{output[-2000:]}\n```")
|
|
122
|
+
except FileNotFoundError:
|
|
123
|
+
cli.error("snakemake is not installed or not on PATH.")
|
|
124
|
+
|
|
125
|
+
if main_func is not None:
|
|
126
|
+
@cli.command("/scaffold", "Generate clizard_main.py wrapping this project's main()")
|
|
127
|
+
def _cmd_scaffold(prompt):
|
|
128
|
+
try:
|
|
129
|
+
out_path = generate_clizard_main(repo_path)
|
|
130
|
+
cli.assistant_message(f"Wrote `{out_path}`. Run it with:\n\n```\npython {out_path.name}\n```")
|
|
131
|
+
except RuntimeError as e:
|
|
132
|
+
cli.error(str(e))
|
|
133
|
+
|
|
134
|
+
return cli
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def main():
|
|
138
|
+
args = parse_args(app_name="clizard")
|
|
139
|
+
cli = build_cli(repo_path=args.path or ".")
|
|
140
|
+
if args.model:
|
|
141
|
+
cli.config.set("model", args.model)
|
|
142
|
+
cli.run()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
if __name__ == "__main__":
|
|
146
|
+
main()
|
clizard/cli_args.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Argument parsing for GenericCLI-based tools."""
|
|
2
|
+
import argparse
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def build_parser(app_name="Generic CLI", extra_args=None):
|
|
6
|
+
"""Create an argparse.ArgumentParser with common options.
|
|
7
|
+
|
|
8
|
+
extra_args: list of dicts like {"flags": ["--foo"], "kwargs": {...}}
|
|
9
|
+
to let downstream scripts add their own arguments.
|
|
10
|
+
"""
|
|
11
|
+
parser = argparse.ArgumentParser(prog=app_name, description=f"{app_name} - interactive CLI")
|
|
12
|
+
parser.add_argument("--model", default=None, help="Model name/identifier to use")
|
|
13
|
+
parser.add_argument("--path", default=None, help="Working/project path")
|
|
14
|
+
parser.add_argument("--config", default=None, help="Path to a JSON config file")
|
|
15
|
+
parser.add_argument("--name", default=None, help="Override the displayed app name")
|
|
16
|
+
|
|
17
|
+
if extra_args:
|
|
18
|
+
for arg in extra_args:
|
|
19
|
+
parser.add_argument(*arg["flags"], **arg.get("kwargs", {}))
|
|
20
|
+
|
|
21
|
+
return parser
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def parse_args(app_name="Generic CLI", extra_args=None, argv=None):
|
|
25
|
+
parser = build_parser(app_name, extra_args)
|
|
26
|
+
return parser.parse_args(argv)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def auto_cli(parser, args=None, app_name=None, handler=None, run_callback=None, config_path=None):
|
|
30
|
+
"""Build a GenericCLI automatically from an existing argparse.ArgumentParser.
|
|
31
|
+
|
|
32
|
+
Every `--flag` already defined on `parser` becomes an editable setting:
|
|
33
|
+
its current value (parsed from argv, or its default) seeds the CLI's
|
|
34
|
+
`/settings` table, and its `choices`/`type`/`help` are kept so `/settings`
|
|
35
|
+
can validate and cast edits correctly.
|
|
36
|
+
|
|
37
|
+
Parameters
|
|
38
|
+
----------
|
|
39
|
+
parser : argparse.ArgumentParser
|
|
40
|
+
An already-configured parser (e.g. the one built in your existing
|
|
41
|
+
`main()`), or just `parser` before calling `parse_args()` on it.
|
|
42
|
+
args : argparse.Namespace, optional
|
|
43
|
+
Pre-parsed args. If omitted, `parser.parse_args()` is called.
|
|
44
|
+
app_name : str, optional
|
|
45
|
+
Defaults to `parser.prog`.
|
|
46
|
+
handler : callable(prompt, cli) -> str, optional
|
|
47
|
+
Called for free-text input. If omitted, free-text input does nothing
|
|
48
|
+
special; use `run_callback` + a `/run` command instead (see below).
|
|
49
|
+
run_callback : callable(cli) -> str, optional
|
|
50
|
+
If given, a `/run` command is registered that calls
|
|
51
|
+
`run_callback(cli)` using the current settings, mirroring how you'd
|
|
52
|
+
call your script's `run(...)` function with `args.*`.
|
|
53
|
+
config_path : str, optional
|
|
54
|
+
Where to persist settings between sessions.
|
|
55
|
+
|
|
56
|
+
Returns
|
|
57
|
+
-------
|
|
58
|
+
GenericCLI
|
|
59
|
+
"""
|
|
60
|
+
from .core import GenericCLI # local import avoids a circular import
|
|
61
|
+
|
|
62
|
+
if args is None:
|
|
63
|
+
args = parser.parse_args()
|
|
64
|
+
|
|
65
|
+
settings = {}
|
|
66
|
+
arg_meta = {}
|
|
67
|
+
for action in parser._actions:
|
|
68
|
+
dest = action.dest
|
|
69
|
+
if dest in ("help",) or isinstance(action, argparse._HelpAction):
|
|
70
|
+
continue
|
|
71
|
+
settings[dest] = getattr(args, dest, action.default)
|
|
72
|
+
arg_meta[dest] = {
|
|
73
|
+
"choices": list(action.choices) if action.choices else None,
|
|
74
|
+
"type": action.type,
|
|
75
|
+
"help": action.help,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
cli = GenericCLI(
|
|
79
|
+
app_name=app_name or parser.prog or "CLI",
|
|
80
|
+
settings=settings,
|
|
81
|
+
config_path=config_path,
|
|
82
|
+
handler=handler,
|
|
83
|
+
tips=["/run", "/settings", "/docs", "/help"] if run_callback else ["/settings", "/help"],
|
|
84
|
+
)
|
|
85
|
+
cli.arg_meta = arg_meta # exposed for validation / display in /settings
|
|
86
|
+
|
|
87
|
+
if run_callback is not None:
|
|
88
|
+
@cli.command("/run", "Run the script with current settings")
|
|
89
|
+
def _cmd_run(prompt):
|
|
90
|
+
cli.status("Running...")
|
|
91
|
+
result = run_callback(cli)
|
|
92
|
+
if result is not None:
|
|
93
|
+
cli.assistant_message(str(result))
|
|
94
|
+
|
|
95
|
+
return cli
|
clizard/clizard_file.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Read/write the project-local .clizard metadata file.
|
|
2
|
+
|
|
3
|
+
This stores anything that can't be auto-discovered from git/pyproject:
|
|
4
|
+
ascii_art, app_name override, docs_url override, accent_color, tips, etc.
|
|
5
|
+
"""
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
DEFAULT_ASCII = r"""
|
|
10
|
+
.-.
|
|
11
|
+
|o o|
|
|
12
|
+
| = |
|
|
13
|
+
/|___|\
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
CLIZARD_FILENAME = ".clizard"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def load_clizard_file(repo_path="."):
|
|
20
|
+
"""Return the parsed .clizard JSON dict, or {} if absent/invalid."""
|
|
21
|
+
path = Path(repo_path) / CLIZARD_FILENAME
|
|
22
|
+
if not path.exists():
|
|
23
|
+
return {}
|
|
24
|
+
try:
|
|
25
|
+
with open(path, "r") as f:
|
|
26
|
+
return json.load(f)
|
|
27
|
+
except (json.JSONDecodeError, OSError):
|
|
28
|
+
return {}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def save_clizard_file(data: dict, repo_path="."):
|
|
32
|
+
|
|
33
|
+
path = Path(repo_path) / CLIZARD_FILENAME
|
|
34
|
+
with open(path, "w") as f:
|
|
35
|
+
json.dump(data, f, indent=2)
|
|
36
|
+
return path
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def ensure_clizard_file(repo_path=".", **overrides):
|
|
40
|
+
"""Create a .clizard file with sane defaults if one doesn't exist yet."""
|
|
41
|
+
path = Path(repo_path) / CLIZARD_FILENAME
|
|
42
|
+
if path.exists():
|
|
43
|
+
return load_clizard_file(repo_path)
|
|
44
|
+
|
|
45
|
+
data = {
|
|
46
|
+
"app_name": None, # None -> auto from pyproject/git
|
|
47
|
+
"ascii_art": DEFAULT_ASCII,
|
|
48
|
+
"docs_url": None, # None -> auto from pyproject, else docs/index.html
|
|
49
|
+
"accent_color": "#d97757",
|
|
50
|
+
"tips": ["/wizard", "/run", "/settings", "/docs", "/help"],
|
|
51
|
+
"updates": [],
|
|
52
|
+
}
|
|
53
|
+
data.update(overrides)
|
|
54
|
+
save_clizard_file(data, repo_path)
|
|
55
|
+
return data
|
clizard/config.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Settings persistence for GenericCLI."""
|
|
2
|
+
import json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
DEFAULT_CONFIG_DIR = Path.home() / ".config" / "clizard"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Config:
|
|
9
|
+
"""Simple JSON-backed settings store.
|
|
10
|
+
|
|
11
|
+
settings dict holds arbitrary key/values (model, path, theme, etc).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, app_name: str, defaults: dict = None, config_path: str = None):
|
|
15
|
+
self.app_name = app_name
|
|
16
|
+
self.defaults = defaults or {}
|
|
17
|
+
if config_path:
|
|
18
|
+
self.path = Path(config_path)
|
|
19
|
+
else:
|
|
20
|
+
self.path = DEFAULT_CONFIG_DIR / f"{app_name}.json"
|
|
21
|
+
self.settings = dict(self.defaults)
|
|
22
|
+
self.load()
|
|
23
|
+
|
|
24
|
+
def load(self):
|
|
25
|
+
if self.path.exists():
|
|
26
|
+
try:
|
|
27
|
+
with open(self.path, "r") as f:
|
|
28
|
+
data = json.load(f)
|
|
29
|
+
self.settings.update(data)
|
|
30
|
+
except (json.JSONDecodeError, OSError):
|
|
31
|
+
pass
|
|
32
|
+
return self.settings
|
|
33
|
+
|
|
34
|
+
def save(self):
|
|
35
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
36
|
+
with open(self.path, "w") as f:
|
|
37
|
+
json.dump(self.settings, f, indent=2)
|
|
38
|
+
|
|
39
|
+
def get(self, key, fallback=None):
|
|
40
|
+
return self.settings.get(key, fallback)
|
|
41
|
+
|
|
42
|
+
def set(self, key, value):
|
|
43
|
+
self.settings[key] = value
|
|
44
|
+
self.save()
|
|
45
|
+
|
|
46
|
+
def update_from_args(self, args_dict: dict):
|
|
47
|
+
"""Override settings with non-None values from parsed CLI args."""
|
|
48
|
+
for k, v in args_dict.items():
|
|
49
|
+
if v is not None:
|
|
50
|
+
self.settings[k] = v
|