luvz 0.0.1.dev0__tar.gz
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.
- luvz-0.0.1.dev0/LICENCE +21 -0
- luvz-0.0.1.dev0/PKG-INFO +52 -0
- luvz-0.0.1.dev0/README.md +38 -0
- luvz-0.0.1.dev0/luvz/__init__.py +10 -0
- luvz-0.0.1.dev0/luvz/core/__init__.py +1 -0
- luvz-0.0.1.dev0/luvz/core/context/__init__.py +2 -0
- luvz-0.0.1.dev0/luvz/core/context/command.py +133 -0
- luvz-0.0.1.dev0/luvz/core/context/script.py +199 -0
- luvz-0.0.1.dev0/luvz/core/context/task.py +13 -0
- luvz-0.0.1.dev0/luvz/core/models/script.py +5 -0
- luvz-0.0.1.dev0/luvz/core/runner/__init__.py +13 -0
- luvz-0.0.1.dev0/luvz/core/runner/base.py +167 -0
- luvz-0.0.1.dev0/luvz/core/runner/cli.py +50 -0
- luvz-0.0.1.dev0/luvz/core/runner/interactive.py +217 -0
- luvz-0.0.1.dev0/luvz/modules/console.py +188 -0
- luvz-0.0.1.dev0/luvz/modules/process.py +56 -0
- luvz-0.0.1.dev0/luvz/utils/__init__.py +1 -0
- luvz-0.0.1.dev0/luvz/utils/cli.py +9 -0
- luvz-0.0.1.dev0/luvz/utils/parser.py +44 -0
- luvz-0.0.1.dev0/luvz/utils/path.py +7 -0
- luvz-0.0.1.dev0/luvz.egg-info/PKG-INFO +52 -0
- luvz-0.0.1.dev0/luvz.egg-info/SOURCES.txt +25 -0
- luvz-0.0.1.dev0/luvz.egg-info/dependency_links.txt +1 -0
- luvz-0.0.1.dev0/luvz.egg-info/requires.txt +3 -0
- luvz-0.0.1.dev0/luvz.egg-info/top_level.txt +1 -0
- luvz-0.0.1.dev0/pyproject.toml +17 -0
- luvz-0.0.1.dev0/setup.cfg +4 -0
luvz-0.0.1.dev0/LICENCE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 luvbyte
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
luvz-0.0.1.dev0/PKG-INFO
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: luvz
|
|
3
|
+
Version: 0.0.1.dev0
|
|
4
|
+
Summary: Lazy script builder
|
|
5
|
+
Author-email: luvbyte <lovemelong@protonmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.8
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENCE
|
|
10
|
+
Requires-Dist: cmd2
|
|
11
|
+
Requires-Dist: rich
|
|
12
|
+
Requires-Dist: pydantic
|
|
13
|
+
Dynamic: license-file
|
|
14
|
+
|
|
15
|
+
# luvz 💤
|
|
16
|
+
A lightweight framework for building interactive Python scripts
|
|
17
|
+
|
|
18
|
+
`luvz` is a Python package built on top of [`cmd2`](https://cmd2.readthedocs.io) that makes it easy to build interactive, command-driven Python programs.
|
|
19
|
+
It gives you a simple API for defining commands, handling user input, and extending functionality — perfect for creating REPL-like tools, admin shells, or prototypes.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## ✨ Features
|
|
24
|
+
|
|
25
|
+
- Simple API to create interactive command-line applications
|
|
26
|
+
- Built on `cmd2` for history, tab completion, transcripts, and more
|
|
27
|
+
- Minimal setup — just subclass and add commands
|
|
28
|
+
- Great for prototypes, admin utilities, or sharing interactive tools
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 📦 Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install luvz
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
(or clone and install locally:)
|
|
39
|
+
```bash
|
|
40
|
+
git clone https://github.com/luvbyte/luvz.git
|
|
41
|
+
cd luvz
|
|
42
|
+
pip install .
|
|
43
|
+
```
|
|
44
|
+
---
|
|
45
|
+
## ⚡ Why luvz?
|
|
46
|
+
|
|
47
|
+
Save time — skip boilerplate when building interactive shells
|
|
48
|
+
|
|
49
|
+
Enjoy rich features from cmd2 without the setup
|
|
50
|
+
|
|
51
|
+
Create professional-feeling CLI tools quickly
|
|
52
|
+
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# luvz 💤
|
|
2
|
+
A lightweight framework for building interactive Python scripts
|
|
3
|
+
|
|
4
|
+
`luvz` is a Python package built on top of [`cmd2`](https://cmd2.readthedocs.io) that makes it easy to build interactive, command-driven Python programs.
|
|
5
|
+
It gives you a simple API for defining commands, handling user input, and extending functionality — perfect for creating REPL-like tools, admin shells, or prototypes.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## ✨ Features
|
|
10
|
+
|
|
11
|
+
- Simple API to create interactive command-line applications
|
|
12
|
+
- Built on `cmd2` for history, tab completion, transcripts, and more
|
|
13
|
+
- Minimal setup — just subclass and add commands
|
|
14
|
+
- Great for prototypes, admin utilities, or sharing interactive tools
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 📦 Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install luvz
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
(or clone and install locally:)
|
|
25
|
+
```bash
|
|
26
|
+
git clone https://github.com/luvbyte/luvz.git
|
|
27
|
+
cd luvz
|
|
28
|
+
pip install .
|
|
29
|
+
```
|
|
30
|
+
---
|
|
31
|
+
## ⚡ Why luvz?
|
|
32
|
+
|
|
33
|
+
Save time — skip boilerplate when building interactive shells
|
|
34
|
+
|
|
35
|
+
Enjoy rich features from cmd2 without the setup
|
|
36
|
+
|
|
37
|
+
Create professional-feeling CLI tools quickly
|
|
38
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.0.1"
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import cmd2
|
|
2
|
+
import inspect
|
|
3
|
+
|
|
4
|
+
class Arg:
|
|
5
|
+
def __init__(self, *args, **kwargs):
|
|
6
|
+
self.args = args
|
|
7
|
+
self.kwargs = kwargs
|
|
8
|
+
|
|
9
|
+
@staticmethod
|
|
10
|
+
def Path(*args, **kwargs):
|
|
11
|
+
return Arg(completer=cmd2.Cmd.path_complete, *args, **kwargs)
|
|
12
|
+
|
|
13
|
+
@staticmethod
|
|
14
|
+
def Choice(*args, **kwargs):
|
|
15
|
+
return Arg(choices=[str(name) for name in args], **kwargs)
|
|
16
|
+
|
|
17
|
+
class ScriptCommand:
|
|
18
|
+
def __init__(self, name, func, short=None, desc=None):
|
|
19
|
+
self.name = name
|
|
20
|
+
self.func = func # callable
|
|
21
|
+
# Short description
|
|
22
|
+
self.short = short
|
|
23
|
+
|
|
24
|
+
self.desc = desc or (self.func.__doc__ or "").strip()
|
|
25
|
+
|
|
26
|
+
# if desc or get from func desc
|
|
27
|
+
self.argparser = cmd2.Cmd2ArgumentParser(
|
|
28
|
+
prog=self.name,
|
|
29
|
+
description=self.desc
|
|
30
|
+
)
|
|
31
|
+
# parse args from function
|
|
32
|
+
self._parse_func_args()
|
|
33
|
+
|
|
34
|
+
def has(self, name):
|
|
35
|
+
return name in self._registers
|
|
36
|
+
|
|
37
|
+
def get(self, name, default=None):
|
|
38
|
+
return self._registers.get(name, default)
|
|
39
|
+
|
|
40
|
+
def help_text(self, line):
|
|
41
|
+
return self.argparser.format_help()
|
|
42
|
+
|
|
43
|
+
# Building argparse
|
|
44
|
+
def _parse_func_args(self):
|
|
45
|
+
sig = inspect.signature(self.func)
|
|
46
|
+
for param_name, param in sig.parameters.items():
|
|
47
|
+
if param.kind == inspect.Parameter.VAR_POSITIONAL:
|
|
48
|
+
self._add_argument(param_name, nargs='*', help=f"Extra positional args for {param_name}")
|
|
49
|
+
continue
|
|
50
|
+
elif param.kind == inspect.Parameter.VAR_KEYWORD:
|
|
51
|
+
continue
|
|
52
|
+
|
|
53
|
+
if isinstance(param.annotation, Arg):
|
|
54
|
+
arg = param.annotation
|
|
55
|
+
# if no default / required positional argument
|
|
56
|
+
if param.default == inspect._empty:
|
|
57
|
+
self._add_argument(param_name, *arg.args, **arg.kwargs)
|
|
58
|
+
else:
|
|
59
|
+
self._add_argument(f"--{param_name}", *arg.args, default=param.default, **arg.kwargs)
|
|
60
|
+
continue
|
|
61
|
+
|
|
62
|
+
arg_type = param.annotation if param.annotation != inspect._empty else str
|
|
63
|
+
|
|
64
|
+
if param.default == inspect._empty:
|
|
65
|
+
self._add_argument(param_name, type=arg_type)
|
|
66
|
+
else:
|
|
67
|
+
if arg_type is bool:
|
|
68
|
+
if param.default is False:
|
|
69
|
+
self._add_argument(f"--{param_name}", action="store_true", default=False)
|
|
70
|
+
else:
|
|
71
|
+
self._add_argument(f"--no-{param_name}", action="store_false", dest=param_name, default=True)
|
|
72
|
+
else:
|
|
73
|
+
self._add_argument(f"--{param_name}", type=arg_type, default=param.default)
|
|
74
|
+
|
|
75
|
+
def _add_argument(self, *args, **kwargs):
|
|
76
|
+
self.argparser.add_argument(*args, **kwargs)
|
|
77
|
+
|
|
78
|
+
def emit_func(self, *args, **kwargs):
|
|
79
|
+
return self.func(*args, **kwargs) if callable(self.func) else self.func
|
|
80
|
+
|
|
81
|
+
# args will be cmd2 Namespace()
|
|
82
|
+
def run(self, args):
|
|
83
|
+
import inspect
|
|
84
|
+
|
|
85
|
+
# Convert argparse Namespace to dict
|
|
86
|
+
if not isinstance(args, dict):
|
|
87
|
+
arg_dict = vars(args)
|
|
88
|
+
else:
|
|
89
|
+
arg_dict = args
|
|
90
|
+
|
|
91
|
+
sig = inspect.signature(self.func)
|
|
92
|
+
positional = []
|
|
93
|
+
keywords = {}
|
|
94
|
+
varargs = []
|
|
95
|
+
extra_kwargs = {}
|
|
96
|
+
|
|
97
|
+
for name, param in sig.parameters.items():
|
|
98
|
+
if param.kind == inspect.Parameter.VAR_POSITIONAL:
|
|
99
|
+
# collect *args
|
|
100
|
+
varargs = arg_dict.get(name, [])
|
|
101
|
+
elif param.kind == inspect.Parameter.VAR_KEYWORD:
|
|
102
|
+
# collect remaining kwargs not already matched
|
|
103
|
+
for k, v in arg_dict.items():
|
|
104
|
+
if k not in sig.parameters:
|
|
105
|
+
extra_kwargs[k] = v
|
|
106
|
+
else:
|
|
107
|
+
# normal positional or keyword parameters
|
|
108
|
+
if name in arg_dict:
|
|
109
|
+
if param.default is inspect._empty:
|
|
110
|
+
# no default → positional
|
|
111
|
+
positional.append(arg_dict[name])
|
|
112
|
+
else:
|
|
113
|
+
# has default → keyword
|
|
114
|
+
keywords[name] = arg_dict[name]
|
|
115
|
+
|
|
116
|
+
# Call the function
|
|
117
|
+
return self.func(*positional, *varargs, **keywords, **extra_kwargs)
|
|
118
|
+
|
|
119
|
+
def run_cli(self, args):
|
|
120
|
+
return self.run(self.argparser.parse_args(args))
|
|
121
|
+
|
|
122
|
+
class ScriptCommands:
|
|
123
|
+
def __init__(self):
|
|
124
|
+
self._registers = {}
|
|
125
|
+
|
|
126
|
+
def items(self):
|
|
127
|
+
return self._registers.items()
|
|
128
|
+
|
|
129
|
+
def get(self, name, default=None):
|
|
130
|
+
return self._registers.get(name, default)
|
|
131
|
+
|
|
132
|
+
def add(self, name, func, *args, **kwargs):
|
|
133
|
+
self._registers[name] = ScriptCommand(name, func, *args, **kwargs)
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import cmd2
|
|
4
|
+
import inspect
|
|
5
|
+
|
|
6
|
+
from uuid import uuid4
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from luvz.modules.process import sh
|
|
10
|
+
from luvz.utils.path import ensure_dir
|
|
11
|
+
from luvz.modules.console import AdvConsole
|
|
12
|
+
from luvz.core.models.script import ScriptConfigModel
|
|
13
|
+
|
|
14
|
+
from .task import ScriptTasks
|
|
15
|
+
from .command import ScriptCommands, Arg
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# ---- script events
|
|
19
|
+
|
|
20
|
+
class ScriptEvent:
|
|
21
|
+
def __init__(self, name, func, *args, **kwargs):
|
|
22
|
+
self.name = name
|
|
23
|
+
self.func = func
|
|
24
|
+
|
|
25
|
+
def emit(self, *args, **kwargs):
|
|
26
|
+
return self.func(*args, **kwargs)
|
|
27
|
+
|
|
28
|
+
class ScriptEvents:
|
|
29
|
+
def __init__(self):
|
|
30
|
+
self._registers = {}
|
|
31
|
+
|
|
32
|
+
def add(self, name: str, func, *args, **kwargs) -> ScriptEvent:
|
|
33
|
+
event = ScriptEvent(name, func, *args, **kwargs)
|
|
34
|
+
self._registers.setdefault(name, []).append(event)
|
|
35
|
+
|
|
36
|
+
return event
|
|
37
|
+
|
|
38
|
+
def has(self, name: str):
|
|
39
|
+
return name in self._registers
|
|
40
|
+
|
|
41
|
+
def get(self, name, default=None):
|
|
42
|
+
return self._registers.get(name, default)
|
|
43
|
+
|
|
44
|
+
def emit(self, name, *args, **kwargs):
|
|
45
|
+
for ev in self._registers.get(name, []):
|
|
46
|
+
ev.emit(*args, **kwargs)
|
|
47
|
+
|
|
48
|
+
# ---- Config and ZOptions
|
|
49
|
+
|
|
50
|
+
class ScriptConfig:
|
|
51
|
+
def __init__(self, config: ScriptConfigModel):
|
|
52
|
+
self._config = config
|
|
53
|
+
self.luvz_path = ensure_dir(Path.home() / ".luvz")
|
|
54
|
+
|
|
55
|
+
class ScriptOption:
|
|
56
|
+
def __init__(self, name, value=None, require=False, type=str, choices=(), help=None):
|
|
57
|
+
self.name = name
|
|
58
|
+
self._value = value # store actual value internally
|
|
59
|
+
self._type = type
|
|
60
|
+
self.choices = choices
|
|
61
|
+
self.require = require
|
|
62
|
+
|
|
63
|
+
self.help = help
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def value(self):
|
|
67
|
+
"""Return the value, enforcing 'require' rule."""
|
|
68
|
+
if self.require and self._value is None:
|
|
69
|
+
raise ValueError(f"Required option '{self.name}' is missing a value")
|
|
70
|
+
return self._value
|
|
71
|
+
|
|
72
|
+
@value.setter
|
|
73
|
+
def value(self, new_value):
|
|
74
|
+
# 1. cast the input
|
|
75
|
+
try:
|
|
76
|
+
casted = self._type(new_value)
|
|
77
|
+
except (ValueError, TypeError):
|
|
78
|
+
raise TypeError(
|
|
79
|
+
f"Option '{self.name}' expects type {self._type.__name__}, "
|
|
80
|
+
f"but got {new_value!r} of type {type(new_value).__name__}"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# 2. cast the choices to _type too, once, to be safe
|
|
84
|
+
if self.choices:
|
|
85
|
+
try:
|
|
86
|
+
normalized_choices = tuple(self._type(c) for c in self.choices)
|
|
87
|
+
except (ValueError, TypeError):
|
|
88
|
+
raise ValueError(f"Choices for option '{self.name}' cannot be cast to {self._type.__name__}")
|
|
89
|
+
if casted not in normalized_choices:
|
|
90
|
+
raise ValueError(
|
|
91
|
+
f"Invalid value for option '{self.name}': {casted!r}. "
|
|
92
|
+
f"Allowed choices: {normalized_choices}"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# 3. enforce require
|
|
96
|
+
if self.require and casted is None:
|
|
97
|
+
raise ValueError(f"Cannot set None for required option '{self.name}'")
|
|
98
|
+
|
|
99
|
+
self._value = casted
|
|
100
|
+
|
|
101
|
+
def __str__(self):
|
|
102
|
+
return self.value or ""
|
|
103
|
+
|
|
104
|
+
class ScriptOptions:
|
|
105
|
+
def __init__(self):
|
|
106
|
+
self._options = {}
|
|
107
|
+
|
|
108
|
+
def add(self, name, *args, **kwargs):
|
|
109
|
+
option = ScriptOption(name, *args, **kwargs)
|
|
110
|
+
self._options[name] = option
|
|
111
|
+
return option
|
|
112
|
+
|
|
113
|
+
# sets option else raises error
|
|
114
|
+
def set(self, name, value):
|
|
115
|
+
if name not in self._options:
|
|
116
|
+
raise KeyError(f"Option '{name}' does not exist")
|
|
117
|
+
self._options[name].value = value
|
|
118
|
+
|
|
119
|
+
# returns direct value
|
|
120
|
+
def get(self, name):
|
|
121
|
+
if name not in self._options:
|
|
122
|
+
raise KeyError(f"Option '{name}' does not exist")
|
|
123
|
+
return self._options[name].value
|
|
124
|
+
|
|
125
|
+
def __call__(self, name):
|
|
126
|
+
return self.get(name)
|
|
127
|
+
|
|
128
|
+
# ---- Script Args (argv)
|
|
129
|
+
|
|
130
|
+
class ScriptArgs:
|
|
131
|
+
def __init__(self):
|
|
132
|
+
self._raw_args = sys.argv[1:]
|
|
133
|
+
|
|
134
|
+
def get(self, index, default=None):
|
|
135
|
+
try:
|
|
136
|
+
return self._raw_args[index]
|
|
137
|
+
except ValueError:
|
|
138
|
+
return default
|
|
139
|
+
|
|
140
|
+
def __str__(self):
|
|
141
|
+
return " ".join(self._raw_args)
|
|
142
|
+
|
|
143
|
+
# ---- ZScript
|
|
144
|
+
|
|
145
|
+
class ZScript:
|
|
146
|
+
prompt = "| "
|
|
147
|
+
banner = None
|
|
148
|
+
def __init__(self, name=None, version=None, author=None, desc=None, config={}):
|
|
149
|
+
# script paths
|
|
150
|
+
self.script_full_path = Path(sys.argv[0])
|
|
151
|
+
self.script_path = self.script_full_path.parent
|
|
152
|
+
|
|
153
|
+
# with ext
|
|
154
|
+
self.script_name = os.path.basename(sys.argv[0])
|
|
155
|
+
# script meta
|
|
156
|
+
self.name = (name or Path(sys.argv[0]).with_suffix("").name).capitalize()
|
|
157
|
+
self.desc = desc
|
|
158
|
+
self.author = author
|
|
159
|
+
self.version = version
|
|
160
|
+
|
|
161
|
+
self.scr = AdvConsole()
|
|
162
|
+
self.options = ScriptOptions()
|
|
163
|
+
self.config = ScriptConfig(ScriptConfigModel(**config))
|
|
164
|
+
|
|
165
|
+
self.events = ScriptEvents()
|
|
166
|
+
self.commands = ScriptCommands()
|
|
167
|
+
self.tasks = ScriptTasks()
|
|
168
|
+
|
|
169
|
+
self.sh = sh
|
|
170
|
+
# finally parsing args
|
|
171
|
+
self.args = ScriptArgs()
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def cwd(self):
|
|
175
|
+
return os.getcwd()
|
|
176
|
+
|
|
177
|
+
def arg(self, *args, **kwargs):
|
|
178
|
+
return Arg(*args, **kwargs)
|
|
179
|
+
|
|
180
|
+
def add_option(self, *args, **kwargs):
|
|
181
|
+
return self.options.add(*args, **kwargs)
|
|
182
|
+
|
|
183
|
+
def on(self, name, *args, **kwargs):
|
|
184
|
+
def wrapper(func):
|
|
185
|
+
if name.startswith("luvz:"):
|
|
186
|
+
self.events.add(name[4:], func, *args, **kwargs)
|
|
187
|
+
else:
|
|
188
|
+
self.commands.add(name, func, *args, **kwargs)
|
|
189
|
+
return wrapper
|
|
190
|
+
|
|
191
|
+
def on_event(self, name, *args, **kwargs):
|
|
192
|
+
def wrapper(func):
|
|
193
|
+
self.events.add(name, func, *args, **kwargs)
|
|
194
|
+
return wrapper
|
|
195
|
+
|
|
196
|
+
def __call__(self, cmd):
|
|
197
|
+
if cmd.startswith("$"):
|
|
198
|
+
return self.sh(cmd[1:])
|
|
199
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
# ------ Run types
|
|
4
|
+
def run_script_it(script, intro: bool = True):
|
|
5
|
+
from .interactive import ZScriptRunner
|
|
6
|
+
return ZScriptRunner(script).run(intro=intro)
|
|
7
|
+
|
|
8
|
+
def run_script_cli(script, intro: bool = False):
|
|
9
|
+
from .cli import ZScriptRunnerCli
|
|
10
|
+
return ZScriptRunnerCli(script).run(intro=intro)
|
|
11
|
+
|
|
12
|
+
def run_script(script, *args, **kwargs):
|
|
13
|
+
return run_script_cli(script, *args, **kwargs) if len(sys.argv) > 1 else run_script_it(script, *args, **kwargs)
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
from rich.table import Table
|
|
2
|
+
from luvz.core import __version__
|
|
3
|
+
|
|
4
|
+
import atexit
|
|
5
|
+
|
|
6
|
+
BANNER = r"""
|
|
7
|
+
[blue]███████╗███████╗███████╗[/]
|
|
8
|
+
╚══███╔╝╚══███╔╝╚══███╔╝
|
|
9
|
+
[red] ███╔╝ ███╔╝ ███╔╝ [/]
|
|
10
|
+
███╔╝ ███╔╝ ███╔╝
|
|
11
|
+
[yellow]███████╗███████╗███████╗[/]
|
|
12
|
+
╚══════╝╚══════╝╚══════╝
|
|
13
|
+
"""
|
|
14
|
+
BANNER = r"""
|
|
15
|
+
| _ _ ___ _______
|
|
16
|
+
| | | | | | \ \ / /__ /
|
|
17
|
+
| | | | | | |\ \ / / / /
|
|
18
|
+
| | |__| |_| | \ V / / /_
|
|
19
|
+
| |_____\___/ \_/ /____|
|
|
20
|
+
|
|
21
|
+
"""
|
|
22
|
+
BANNER = r"""
|
|
23
|
+
[blue]██╗ ██╗ ██╗ ██╗ ██╗ ███████╗[/]
|
|
24
|
+
██║ ██║ ██║ ██║ ██║ ╚══███╔╝
|
|
25
|
+
[red]██║ ██║ ██║ ██║ ██║ ███╔╝ [/]
|
|
26
|
+
██║ ██║ ██║ ╚██╗ ██╔╝ ███╔╝
|
|
27
|
+
[yellow]███████╗ ╚██████╔╝ ╚████╔╝ ███████╗[/]
|
|
28
|
+
╚══════╝ ╚═════╝ ╚═══╝ ╚══════╝
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
class RunnerUtils:
|
|
32
|
+
def __init__(self, script):
|
|
33
|
+
self.script = script
|
|
34
|
+
atexit.register(self.__on_exit)
|
|
35
|
+
# emiting init
|
|
36
|
+
self.script.events.emit("init")
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def scr(self): # global scr for runners
|
|
40
|
+
return self.script.scr
|
|
41
|
+
|
|
42
|
+
def print_header(self):
|
|
43
|
+
self.scr.print_center(self.script.banner or BANNER)
|
|
44
|
+
self.scr.print_center(f"luvz: [blue]luvbyte[/blue] | version: [red]{__version__}[/red]")
|
|
45
|
+
|
|
46
|
+
def print_script_header(self):
|
|
47
|
+
# Script header
|
|
48
|
+
version = f" [red]v{self.script.version}[/red]" if self.script.version else ""
|
|
49
|
+
author = self.script.author if self.script.author else "Someone [red]ᥫ᭡[/red]"
|
|
50
|
+
self.scr.print_panel(f"✨ [blue]{self.script.name}[/blue]{version} by [green]{author}[/green]", padding=False)
|
|
51
|
+
|
|
52
|
+
def print_intro(self):
|
|
53
|
+
# if banner found print it
|
|
54
|
+
if self.script.banner:
|
|
55
|
+
self.scr.print_center(self.script.banner)
|
|
56
|
+
# if its False then dont print any banner
|
|
57
|
+
elif self.script.banner is not False:
|
|
58
|
+
self.print_header()
|
|
59
|
+
|
|
60
|
+
# script details
|
|
61
|
+
self.print_script_header()
|
|
62
|
+
|
|
63
|
+
def _print_commands(self, cmd2_commands: bool):
|
|
64
|
+
commands = list(self.script.commands.items())
|
|
65
|
+
|
|
66
|
+
self.scr.print_center(f"[italic magenta]{'No' if not cmd2_commands and len(commands) <= 0 else ''} Available Commands[/italic magenta]")
|
|
67
|
+
|
|
68
|
+
# Split based on with and without short descriptions
|
|
69
|
+
no_short, with_short = [], []
|
|
70
|
+
for name, cmd in commands:
|
|
71
|
+
(with_short if cmd.short else no_short).append((name, getattr(cmd, 'short', None)))
|
|
72
|
+
|
|
73
|
+
if with_short or cmd2_commands:
|
|
74
|
+
table = Table(header_style="bold cyan", border_style="blue", expand=True)
|
|
75
|
+
table.add_column("Command", style="green")
|
|
76
|
+
table.add_column("Description", style="yellow")
|
|
77
|
+
|
|
78
|
+
# Predefined cmd2_commands rows
|
|
79
|
+
if cmd2_commands:
|
|
80
|
+
builtin_commands = {
|
|
81
|
+
"alias": "Create command shortcuts",
|
|
82
|
+
"edit": "Edit a script or configuration",
|
|
83
|
+
"help": "Show help (use 'help -v' for verbose)",
|
|
84
|
+
"history": "Show command history",
|
|
85
|
+
"macro": "Record or play macros",
|
|
86
|
+
"quit": "Exit the program",
|
|
87
|
+
"run_pyscript": "Run a Python script",
|
|
88
|
+
"run_script": "Run a script",
|
|
89
|
+
"set": "Set configuration options",
|
|
90
|
+
"shell": "Run shell commands",
|
|
91
|
+
"shortcuts": "List available keyboard shortcuts",
|
|
92
|
+
"options": "Script options",
|
|
93
|
+
"commands": "Script commands",
|
|
94
|
+
"zset": "Set ZOption",
|
|
95
|
+
"---": "---"
|
|
96
|
+
}
|
|
97
|
+
for name, desc in builtin_commands.items():
|
|
98
|
+
table.add_row(name, desc)
|
|
99
|
+
|
|
100
|
+
# Add user commands with short descriptions
|
|
101
|
+
for name, desc in with_short:
|
|
102
|
+
table.add_row(name, desc)
|
|
103
|
+
|
|
104
|
+
self.scr.print(table)
|
|
105
|
+
|
|
106
|
+
if no_short:
|
|
107
|
+
names = " ".join(name for name, _ in no_short)
|
|
108
|
+
self.scr.print_panel(f"[blue]{names}[/blue]", padding=False)
|
|
109
|
+
|
|
110
|
+
def print_commands_cmd2(self):
|
|
111
|
+
return self._print_commands(True)
|
|
112
|
+
|
|
113
|
+
def print_commands_cli(self):
|
|
114
|
+
return self._print_commands(False)
|
|
115
|
+
|
|
116
|
+
def print_options(self, required_only: bool = False):
|
|
117
|
+
options = self.script.options._options
|
|
118
|
+
|
|
119
|
+
# Filter only required options if present
|
|
120
|
+
if required_only:
|
|
121
|
+
options = {name: opt for name, opt in options.items() if opt.require}
|
|
122
|
+
|
|
123
|
+
if not options:
|
|
124
|
+
return self.scr.print_center(
|
|
125
|
+
f"[italic red]Script has no{' required' if required_only else ''} options[/italic red]"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
table = Table(
|
|
129
|
+
title=f"[blue]Script{' Required' if required_only else ''} Options[/blue]",
|
|
130
|
+
expand=True
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
for col, style in [
|
|
134
|
+
("Name", "cyan"),
|
|
135
|
+
("Value", "green"),
|
|
136
|
+
("Required", "magenta"),
|
|
137
|
+
("Type", "yellow"),
|
|
138
|
+
("Choices", "blue")
|
|
139
|
+
]:
|
|
140
|
+
table.add_column(col, style=style, no_wrap=(col == "Name"))
|
|
141
|
+
|
|
142
|
+
for name, opt in options.items():
|
|
143
|
+
# Safely retrieve value
|
|
144
|
+
try:
|
|
145
|
+
val = opt.value
|
|
146
|
+
except Exception:
|
|
147
|
+
val = "[red]-[/red]"
|
|
148
|
+
|
|
149
|
+
# Choices fallback to "-"
|
|
150
|
+
choices = ", ".join(map(str, getattr(opt, "choices", []))) or "-"
|
|
151
|
+
|
|
152
|
+
table.add_row(
|
|
153
|
+
name,
|
|
154
|
+
str(val),
|
|
155
|
+
"Yes" if getattr(opt, "require", False) else "No",
|
|
156
|
+
getattr(opt._type, "__name__", str(opt._type)),
|
|
157
|
+
choices
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
self.scr.print(table)
|
|
161
|
+
|
|
162
|
+
def __on_exit(self):
|
|
163
|
+
self.script.events.emit("exit")
|
|
164
|
+
|
|
165
|
+
def exception(self, text):
|
|
166
|
+
self.scr.print(f"[red]Error:[/red] luvz: [blue]{text}[/blue]")
|
|
167
|
+
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from .base import RunnerUtils
|
|
2
|
+
|
|
3
|
+
from luvz.core.context import ZScript
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# cli
|
|
7
|
+
class ZScriptRunnerCli:
|
|
8
|
+
def __init__(self, script: ZScript):
|
|
9
|
+
self.script = script
|
|
10
|
+
self.utils = RunnerUtils(self.script)
|
|
11
|
+
|
|
12
|
+
self.script.events.emit("cli:init")
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def scr(self):
|
|
16
|
+
return self.utils.scr
|
|
17
|
+
|
|
18
|
+
def _display_cli_help(self):
|
|
19
|
+
self.utils.print_script_header()
|
|
20
|
+
self.scr.print(f"[red]Usage[/red]: {self.script.script_name} command [ARGS] [-h]")
|
|
21
|
+
self.scr.br()
|
|
22
|
+
|
|
23
|
+
self.utils.print_commands_cli()
|
|
24
|
+
|
|
25
|
+
def run(self, intro: bool = False):
|
|
26
|
+
self.script.events.emit("run")
|
|
27
|
+
args = self.script.args._raw_args
|
|
28
|
+
|
|
29
|
+
if len(args) <= 0:
|
|
30
|
+
args = ["-h"]
|
|
31
|
+
|
|
32
|
+
command = args[0]
|
|
33
|
+
command_args = args[1:]
|
|
34
|
+
|
|
35
|
+
# Show help
|
|
36
|
+
if command in ("-h", "--help"):
|
|
37
|
+
return self._display_cli_help()
|
|
38
|
+
|
|
39
|
+
if intro:
|
|
40
|
+
self.utils.print_intro()
|
|
41
|
+
|
|
42
|
+
func = self.script.commands.get(command)
|
|
43
|
+
if func is None:
|
|
44
|
+
self.exception(f"command '{command}' not found")
|
|
45
|
+
return self.scr.br()
|
|
46
|
+
|
|
47
|
+
func.run_cli(command_args)
|
|
48
|
+
|
|
49
|
+
def exception(self, text):
|
|
50
|
+
self.utils.exception(text)
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import types
|
|
4
|
+
import argparse
|
|
5
|
+
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from cmd2 import Cmd, Cmd2ArgumentParser, with_argparser
|
|
9
|
+
|
|
10
|
+
from rich.text import Text
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
|
|
13
|
+
from luvz.modules.process import sh
|
|
14
|
+
from .base import BANNER, RunnerUtils
|
|
15
|
+
from luvz.core.context import ZScript, ScriptCommand
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def make_options_parser():
|
|
19
|
+
# script argparser
|
|
20
|
+
script_argparse = Cmd2ArgumentParser()
|
|
21
|
+
script_argparse.add_argument(
|
|
22
|
+
'subcommand',
|
|
23
|
+
nargs="?",
|
|
24
|
+
choices=['required'],
|
|
25
|
+
help='Display only required options'
|
|
26
|
+
)
|
|
27
|
+
return script_argparse
|
|
28
|
+
|
|
29
|
+
def make_commands_parser():
|
|
30
|
+
# script argparser
|
|
31
|
+
script_argparse = Cmd2ArgumentParser()
|
|
32
|
+
script_argparse.add_argument(
|
|
33
|
+
'subcommand',
|
|
34
|
+
nargs="?",
|
|
35
|
+
choices=['all'],
|
|
36
|
+
help='Display all commands list'
|
|
37
|
+
)
|
|
38
|
+
return script_argparse
|
|
39
|
+
|
|
40
|
+
def make_cd_parser():
|
|
41
|
+
# script argparser
|
|
42
|
+
cd_argparse = Cmd2ArgumentParser()
|
|
43
|
+
cd_argparse.add_argument('path', nargs="?", default=Path.home(), help='Change current directory')
|
|
44
|
+
return cd_argparse
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# cmd2 - interactive
|
|
48
|
+
class ZScriptRunner(Cmd):
|
|
49
|
+
def __init__(self, script: ZScript):
|
|
50
|
+
# bypassing cmd2 default argparse :)
|
|
51
|
+
original_argv = sys.argv.copy()
|
|
52
|
+
sys.argv = [sys.argv[0]]
|
|
53
|
+
# init of Cmd
|
|
54
|
+
super().__init__()
|
|
55
|
+
# restoring argv
|
|
56
|
+
self.script = script
|
|
57
|
+
sys.argv = original_argv
|
|
58
|
+
|
|
59
|
+
self._register_commands()
|
|
60
|
+
|
|
61
|
+
self.utils = RunnerUtils(self.script)
|
|
62
|
+
self.script.events.emit("it:init")
|
|
63
|
+
|
|
64
|
+
# arg parser
|
|
65
|
+
def _register_commands(self):
|
|
66
|
+
for name, command in self.script.commands.items():
|
|
67
|
+
desired_prog = command.argparser.prog or name
|
|
68
|
+
|
|
69
|
+
@with_argparser(command.argparser)
|
|
70
|
+
def do_func(inner_self, args, cmd=command):
|
|
71
|
+
try:
|
|
72
|
+
cmd.run(args)
|
|
73
|
+
except Exception as e:
|
|
74
|
+
inner_self.exception(e)
|
|
75
|
+
|
|
76
|
+
# cmd2's decorator changes both the argparser.prog and the function name
|
|
77
|
+
# Fix them back:
|
|
78
|
+
do_func.argparser.prog = desired_prog # fixes Usage: text
|
|
79
|
+
do_func.__name__ = f"do_{name}" # fixes command name in help
|
|
80
|
+
do_func.__qualname__ = f"do_{name}" # also for introspection
|
|
81
|
+
|
|
82
|
+
# finally bind the method to your instance
|
|
83
|
+
setattr(self, f"do_{name}", types.MethodType(do_func, self))
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def scr(self):
|
|
87
|
+
return self.utils.scr
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def prompt(self):
|
|
91
|
+
prompt = self.script.events.emit("prompt") or self.script.prompt
|
|
92
|
+
return prompt() if callable(prompt) else prompt
|
|
93
|
+
|
|
94
|
+
# --- commands
|
|
95
|
+
def do_clear(self, _):
|
|
96
|
+
self.scr.clear()
|
|
97
|
+
|
|
98
|
+
def do_ls(self, line):
|
|
99
|
+
sh(f"ls --color {line}").run()
|
|
100
|
+
|
|
101
|
+
def do_pwd(self, _):
|
|
102
|
+
self.scr.print(f"You are in: {os.getcwd()}")
|
|
103
|
+
|
|
104
|
+
def do_exit(self, _):
|
|
105
|
+
return True
|
|
106
|
+
|
|
107
|
+
def complete_cd(self, text, line, begidx, endidx):
|
|
108
|
+
return [
|
|
109
|
+
str(path) for path in self.path_complete(text, line, begidx, endidx)
|
|
110
|
+
if os.path.isdir(path)
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
# Change Directory
|
|
114
|
+
@with_argparser(make_cd_parser())
|
|
115
|
+
def do_cd(self, args):
|
|
116
|
+
try:
|
|
117
|
+
os.chdir(args.path)
|
|
118
|
+
except FileNotFoundError:
|
|
119
|
+
self.exception(f"No such directory: '[green]{args.path}[/green]'")
|
|
120
|
+
except Exception as e:
|
|
121
|
+
self.exception(e)
|
|
122
|
+
|
|
123
|
+
# ---- script
|
|
124
|
+
@with_argparser(make_options_parser())
|
|
125
|
+
def do_options(self, args):
|
|
126
|
+
if args.subcommand == "required":
|
|
127
|
+
self.utils.print_options(True)
|
|
128
|
+
else:
|
|
129
|
+
self.utils.print_options(False)
|
|
130
|
+
|
|
131
|
+
@with_argparser(make_commands_parser())
|
|
132
|
+
def do_commands(self, args):
|
|
133
|
+
if args.subcommand == "all":
|
|
134
|
+
self.utils.print_commands_cmd2()
|
|
135
|
+
else:
|
|
136
|
+
self.utils._print_commands(False)
|
|
137
|
+
|
|
138
|
+
#--- setting option
|
|
139
|
+
def help_zset(self):
|
|
140
|
+
self.scr.print("[red]Usage[/red]: zset <name> <value>\n\nSET ZOption\n")
|
|
141
|
+
|
|
142
|
+
def complete_zset(self, text, line, begidx, endidx):
|
|
143
|
+
tokens = line.split()
|
|
144
|
+
|
|
145
|
+
# Suggest option names when typing the option name
|
|
146
|
+
if len(tokens) <= 1 or (begidx <= len(tokens[0]) + 1):
|
|
147
|
+
return [name for name in self.script.options._options if name.startswith(text)]
|
|
148
|
+
|
|
149
|
+
opt_name = tokens[1]
|
|
150
|
+
|
|
151
|
+
# If typing the value
|
|
152
|
+
if opt_name in self.script.options._options:
|
|
153
|
+
opt = self.script.options._options[opt_name]
|
|
154
|
+
|
|
155
|
+
# If the option has choices, show choices matching text
|
|
156
|
+
if opt.choices:
|
|
157
|
+
return [str(c) for c in opt.choices if str(c).startswith(text)]
|
|
158
|
+
|
|
159
|
+
# Otherwise, use cmd2 built-in path completer
|
|
160
|
+
return self.path_complete(text, line, begidx, endidx)
|
|
161
|
+
|
|
162
|
+
return []
|
|
163
|
+
|
|
164
|
+
def do_zset(self, line):
|
|
165
|
+
if not line:
|
|
166
|
+
self.scr.print("[red]Usage[/red]: zset <name> <value>\n")
|
|
167
|
+
|
|
168
|
+
elif len(line.arg_list) < 2:
|
|
169
|
+
self.scr.print("[red]Missing[/red]: <value>\n")
|
|
170
|
+
else:
|
|
171
|
+
try:
|
|
172
|
+
# set
|
|
173
|
+
name = line.arg_list[0]
|
|
174
|
+
value = " ".join(line.arg_list[1:])
|
|
175
|
+
|
|
176
|
+
self.script.options.set(name, value)
|
|
177
|
+
self.scr.print(f"[red]Updated[/red]: {name}={value}\n")
|
|
178
|
+
except Exception as e:
|
|
179
|
+
self.exception(e.args[0])
|
|
180
|
+
# ---
|
|
181
|
+
|
|
182
|
+
def complete_help(self, text, line, begidx, endidx):
|
|
183
|
+
commands = [name[3:] for name in dir(self) if name.startswith('do_') and not name.startswith("do__")]
|
|
184
|
+
if not text:
|
|
185
|
+
return commands
|
|
186
|
+
return [cmd for cmd in commands if cmd.startswith(text)]
|
|
187
|
+
|
|
188
|
+
def do_help(self, line):
|
|
189
|
+
if line:
|
|
190
|
+
return super().do_help(line)
|
|
191
|
+
|
|
192
|
+
self.scr.print_panel("[red]Usage[/red]: command [ARGS] [-h]", padding=False)
|
|
193
|
+
self.utils.print_commands_cmd2()
|
|
194
|
+
self.utils.print_options()
|
|
195
|
+
|
|
196
|
+
# ---- default
|
|
197
|
+
def default(self, line):
|
|
198
|
+
event = self.script.events.get("it:default")
|
|
199
|
+
if event:
|
|
200
|
+
return event.emit(line)
|
|
201
|
+
self.scr.print(f"[blue]luvz[/blue]: Unknown command: [red]{line.command}[/red]")
|
|
202
|
+
|
|
203
|
+
# ---- starts from here
|
|
204
|
+
def run(self, clear: bool = True, intro: bool = True):
|
|
205
|
+
self.script.events.emit("run")
|
|
206
|
+
if clear:
|
|
207
|
+
self.scr.clear()
|
|
208
|
+
|
|
209
|
+
if intro:
|
|
210
|
+
self.utils.print_intro()
|
|
211
|
+
|
|
212
|
+
self.cmdloop()
|
|
213
|
+
self.script.events.emit("run")
|
|
214
|
+
|
|
215
|
+
# Exception handling
|
|
216
|
+
def exception(self, text):
|
|
217
|
+
self.utils.exception(text)
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import time
|
|
4
|
+
import atexit
|
|
5
|
+
import select
|
|
6
|
+
|
|
7
|
+
from rich.text import Text
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
from rich.align import Align
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.columns import Columns
|
|
13
|
+
|
|
14
|
+
from typing import Literal
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AdvConsole(Console):
|
|
18
|
+
def __init__(self) -> None:
|
|
19
|
+
super().__init__()
|
|
20
|
+
|
|
21
|
+
atexit.register(self.__on_exit)
|
|
22
|
+
|
|
23
|
+
# hide / show cursor
|
|
24
|
+
def hide_cursor(self, hide: bool = True) -> None:
|
|
25
|
+
if hide:
|
|
26
|
+
sys.stdout.write("\033[?25l") # Hide cursor
|
|
27
|
+
else:
|
|
28
|
+
sys.stdout.write("\033[?25h") # Show cursor when stopping
|
|
29
|
+
|
|
30
|
+
# clear screen
|
|
31
|
+
def clear(self) -> None:
|
|
32
|
+
os.system("cls" if os.name == "nt" else "clear") # Windows: cls, Linux/macOS: clear
|
|
33
|
+
|
|
34
|
+
# print empty lines
|
|
35
|
+
def br(self, lines: int = 1) -> None:
|
|
36
|
+
self.print(end="\n"*lines)
|
|
37
|
+
|
|
38
|
+
# print panel
|
|
39
|
+
def print_panel(
|
|
40
|
+
self,
|
|
41
|
+
content,
|
|
42
|
+
title: str = "",
|
|
43
|
+
subtitle: str = "",
|
|
44
|
+
justify: Literal['left', 'right', 'center'] = "left",
|
|
45
|
+
padding: bool = True,
|
|
46
|
+
expand: bool = True,
|
|
47
|
+
markup: bool = True
|
|
48
|
+
) -> None:
|
|
49
|
+
# Add padding if requested
|
|
50
|
+
if padding:
|
|
51
|
+
content = f"\n{content}\n"
|
|
52
|
+
|
|
53
|
+
# Create Text safely depending on markup flag
|
|
54
|
+
if markup:
|
|
55
|
+
text_obj = Text.from_markup(content, justify=justify)
|
|
56
|
+
else:
|
|
57
|
+
text_obj = Text(content, justify=justify)
|
|
58
|
+
|
|
59
|
+
# Print Panel with text object
|
|
60
|
+
self.print(
|
|
61
|
+
Panel(
|
|
62
|
+
text_obj,
|
|
63
|
+
title=title,
|
|
64
|
+
subtitle=subtitle,
|
|
65
|
+
expand=expand
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# print text with align
|
|
70
|
+
def print_text(self, text, markup: bool = True, align: Literal['left', 'right', 'center'] = "left"):
|
|
71
|
+
text = Text.from_markup(text) if markup else Text(text)
|
|
72
|
+
self.print(Align(text, align = align))
|
|
73
|
+
|
|
74
|
+
def print_center(self, text: str, markup: bool = True):
|
|
75
|
+
self.print_text(text, markup, "center")
|
|
76
|
+
|
|
77
|
+
# equal=True, expand=True
|
|
78
|
+
def columns(self, *args, **kwargs):
|
|
79
|
+
return Columns(*args, **kwargs)
|
|
80
|
+
|
|
81
|
+
def panel(self, *args, **kwargs):
|
|
82
|
+
return Panel(*args, **kwargs)
|
|
83
|
+
|
|
84
|
+
def print_list(
|
|
85
|
+
self,
|
|
86
|
+
items,
|
|
87
|
+
border: bool = True,
|
|
88
|
+
multi: bool = False,
|
|
89
|
+
title: str | None = None,
|
|
90
|
+
expand: bool = True,
|
|
91
|
+
equal: bool = True,
|
|
92
|
+
style: str = "white",
|
|
93
|
+
index_color: str = "cyan"
|
|
94
|
+
) -> None:
|
|
95
|
+
if not items:
|
|
96
|
+
raise Exception("Items list cannot be empty")
|
|
97
|
+
|
|
98
|
+
# Numbered items with style
|
|
99
|
+
numbered_items = [
|
|
100
|
+
f"[bold {index_color}]{i}.[/bold {index_color}] [bold {style}]{item}[/bold {style}]"
|
|
101
|
+
for i, item in enumerate(items, start=1)
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
# Build layout
|
|
105
|
+
if multi:
|
|
106
|
+
cpanel = Columns(numbered_items, expand=expand, equal=equal)
|
|
107
|
+
else:
|
|
108
|
+
cpanel = "\n".join(numbered_items)
|
|
109
|
+
|
|
110
|
+
# Render with or without panel
|
|
111
|
+
if border:
|
|
112
|
+
self.print(Panel(cpanel, title=title, expand=expand))
|
|
113
|
+
else:
|
|
114
|
+
self.print(cpanel)
|
|
115
|
+
|
|
116
|
+
def print_table(
|
|
117
|
+
self,
|
|
118
|
+
columns,
|
|
119
|
+
rows,
|
|
120
|
+
title=None,
|
|
121
|
+
header_style="bold cyan",
|
|
122
|
+
border_style="blue",
|
|
123
|
+
col_style="green"
|
|
124
|
+
):
|
|
125
|
+
table = Table(
|
|
126
|
+
title=f"[bold magenta]{title}[/bold magenta]" if title else None,
|
|
127
|
+
header_style=header_style,
|
|
128
|
+
border_style=border_style
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# Add columns
|
|
132
|
+
for col in columns:
|
|
133
|
+
table.add_column(col, style=col_style)
|
|
134
|
+
|
|
135
|
+
# Add rows
|
|
136
|
+
for row in rows:
|
|
137
|
+
table.add_row(*[str(item) for item in row])
|
|
138
|
+
|
|
139
|
+
self.print(table)
|
|
140
|
+
|
|
141
|
+
# True - completed, False - canceled
|
|
142
|
+
def wait_basic(self, seconds: float = 5, message="(Ctrl + C to stop)") -> bool:
|
|
143
|
+
"""Waits with a countdown, allows interruption with Ctrl+C."""
|
|
144
|
+
try:
|
|
145
|
+
self.hide_cursor() # Hide cursor
|
|
146
|
+
for i in reversed(range(0, seconds)):
|
|
147
|
+
print(f"{message} : {i} ", end="\r", flush=True)
|
|
148
|
+
time.sleep(1)
|
|
149
|
+
return True # Completed successfully
|
|
150
|
+
except KeyboardInterrupt:
|
|
151
|
+
# print("\nInterrupted!")
|
|
152
|
+
return False # Interrupted by user
|
|
153
|
+
finally:
|
|
154
|
+
self.hide_cursor(False) # Show cursor
|
|
155
|
+
sys.stdout.flush()
|
|
156
|
+
|
|
157
|
+
def wait(self, timeout: float = 5.0, message: str = "Time remaining") -> bool:
|
|
158
|
+
try:
|
|
159
|
+
self.hide_cursor() # Hide
|
|
160
|
+
start_time = time.time()
|
|
161
|
+
while True:
|
|
162
|
+
elapsed = time.time() - start_time
|
|
163
|
+
remaining = max(0, int(timeout - elapsed))
|
|
164
|
+
# print(f"{message} : {remaining}", end="\r", flush=True)
|
|
165
|
+
sys.stdout.write(f"\r{message} : {remaining}")
|
|
166
|
+
sys.stdout.flush()
|
|
167
|
+
|
|
168
|
+
# Check for input every 0.1s
|
|
169
|
+
if sys.stdin in select.select([sys.stdin], [], [], 0.1)[0]:
|
|
170
|
+
line = sys.stdin.readline()
|
|
171
|
+
print() # Move to next line after user input
|
|
172
|
+
return True if line == '\n' else False
|
|
173
|
+
|
|
174
|
+
if elapsed >= timeout:
|
|
175
|
+
return True
|
|
176
|
+
except KeyboardInterrupt:
|
|
177
|
+
return False
|
|
178
|
+
finally:
|
|
179
|
+
self.hide_cursor(False) # Show cursor
|
|
180
|
+
sys.stdout.write("\r\033[K")
|
|
181
|
+
sys.stdout.flush()
|
|
182
|
+
|
|
183
|
+
# clean up
|
|
184
|
+
def __on_exit(self) -> None:
|
|
185
|
+
self.hide_cursor(False)
|
|
186
|
+
|
|
187
|
+
def convert_markup_to_text(markup_text) -> str:
|
|
188
|
+
return Text.from_markup(markup_text).plain
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
from os import getcwd
|
|
4
|
+
from subprocess import Popen, PIPE
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Process:
|
|
8
|
+
def __init__(self, proc: Popen):
|
|
9
|
+
self._process = proc
|
|
10
|
+
|
|
11
|
+
def wait(self) -> 'Process':
|
|
12
|
+
self._process.wait()
|
|
13
|
+
return self
|
|
14
|
+
|
|
15
|
+
def output(self) -> str:
|
|
16
|
+
if self._process.stdout:
|
|
17
|
+
output = self._process.stdout.read().decode("utf-8").strip()
|
|
18
|
+
return output
|
|
19
|
+
return ""
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def returncode(self) -> int:
|
|
23
|
+
return self._process.returncode
|
|
24
|
+
|
|
25
|
+
class ProcessBuilder:
|
|
26
|
+
def __init__(self, cmd: str) -> None:
|
|
27
|
+
self.cmd: str = cmd
|
|
28
|
+
self.stdin = PIPE
|
|
29
|
+
self.stdout = PIPE
|
|
30
|
+
self.stderr = PIPE
|
|
31
|
+
self.shell: bool = True
|
|
32
|
+
self.cwd: str = getcwd()
|
|
33
|
+
|
|
34
|
+
def pipe(self) -> Process:
|
|
35
|
+
return self._run()
|
|
36
|
+
|
|
37
|
+
def run(self) -> Process:
|
|
38
|
+
self.stdin = sys.stdin
|
|
39
|
+
self.stdout = sys.stdout
|
|
40
|
+
self.stderr = sys.stderr
|
|
41
|
+
return self._run()
|
|
42
|
+
|
|
43
|
+
def _run(self) -> Process:
|
|
44
|
+
proc = Popen(
|
|
45
|
+
self.cmd,
|
|
46
|
+
cwd=self.cwd,
|
|
47
|
+
shell=self.shell,
|
|
48
|
+
stdin=self.stdin,
|
|
49
|
+
stdout=self.stdout,
|
|
50
|
+
stderr=self.stderr,
|
|
51
|
+
)
|
|
52
|
+
return Process(proc).wait()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def sh(cmd: str) -> ProcessBuilder:
|
|
56
|
+
return ProcessBuilder(cmd)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .path import ensure_dir
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import IO, Any, Optional, Type, Union
|
|
4
|
+
from pydantic import BaseModel, ValidationError
|
|
5
|
+
|
|
6
|
+
def parse_file(
|
|
7
|
+
file: IO | dict,
|
|
8
|
+
model: Optional[Type[BaseModel]] = None,
|
|
9
|
+
parse_type: str = "json"
|
|
10
|
+
) -> Any:
|
|
11
|
+
try:
|
|
12
|
+
if parse_type == "dict":
|
|
13
|
+
data = file
|
|
14
|
+
elif parse_type == "json":
|
|
15
|
+
data = json.load(file)
|
|
16
|
+
else:
|
|
17
|
+
raise Exception(f"Unsupported parse type: {parse_type}")
|
|
18
|
+
except Exception as e:
|
|
19
|
+
raise Exception(f"Failed to parse file as {parse_type}: {e}")
|
|
20
|
+
|
|
21
|
+
if model is None:
|
|
22
|
+
return data
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
return model.model_validate(data)
|
|
26
|
+
except ValidationError:
|
|
27
|
+
raise Exception(f"Invalid structure for model {model.__name__}")
|
|
28
|
+
|
|
29
|
+
def parse_config(
|
|
30
|
+
file_path: Union[str, Path, dict],
|
|
31
|
+
model: Optional[Type[BaseModel]] = None,
|
|
32
|
+
parse_type: str = "json"
|
|
33
|
+
) -> Any:
|
|
34
|
+
try:
|
|
35
|
+
if isinstance(file_path, dict):
|
|
36
|
+
return parse_file(file_path, model, "dict")
|
|
37
|
+
with open(file_path, "r") as file:
|
|
38
|
+
return parse_file(file, model, parse_type)
|
|
39
|
+
except FileNotFoundError:
|
|
40
|
+
raise FileNotFoundError(f"Config file not found: {file_path}")
|
|
41
|
+
except json.JSONDecodeError:
|
|
42
|
+
raise json.JSONDecodeError(f"Invalid JSON format: {file_path}")
|
|
43
|
+
except Exception as e:
|
|
44
|
+
raise Exception(f"Unexpected error parsing {file_path}: {e}")
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: luvz
|
|
3
|
+
Version: 0.0.1.dev0
|
|
4
|
+
Summary: Lazy script builder
|
|
5
|
+
Author-email: luvbyte <lovemelong@protonmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.8
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENCE
|
|
10
|
+
Requires-Dist: cmd2
|
|
11
|
+
Requires-Dist: rich
|
|
12
|
+
Requires-Dist: pydantic
|
|
13
|
+
Dynamic: license-file
|
|
14
|
+
|
|
15
|
+
# luvz 💤
|
|
16
|
+
A lightweight framework for building interactive Python scripts
|
|
17
|
+
|
|
18
|
+
`luvz` is a Python package built on top of [`cmd2`](https://cmd2.readthedocs.io) that makes it easy to build interactive, command-driven Python programs.
|
|
19
|
+
It gives you a simple API for defining commands, handling user input, and extending functionality — perfect for creating REPL-like tools, admin shells, or prototypes.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## ✨ Features
|
|
24
|
+
|
|
25
|
+
- Simple API to create interactive command-line applications
|
|
26
|
+
- Built on `cmd2` for history, tab completion, transcripts, and more
|
|
27
|
+
- Minimal setup — just subclass and add commands
|
|
28
|
+
- Great for prototypes, admin utilities, or sharing interactive tools
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 📦 Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install luvz
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
(or clone and install locally:)
|
|
39
|
+
```bash
|
|
40
|
+
git clone https://github.com/luvbyte/luvz.git
|
|
41
|
+
cd luvz
|
|
42
|
+
pip install .
|
|
43
|
+
```
|
|
44
|
+
---
|
|
45
|
+
## ⚡ Why luvz?
|
|
46
|
+
|
|
47
|
+
Save time — skip boilerplate when building interactive shells
|
|
48
|
+
|
|
49
|
+
Enjoy rich features from cmd2 without the setup
|
|
50
|
+
|
|
51
|
+
Create professional-feeling CLI tools quickly
|
|
52
|
+
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
LICENCE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
luvz/__init__.py
|
|
5
|
+
luvz.egg-info/PKG-INFO
|
|
6
|
+
luvz.egg-info/SOURCES.txt
|
|
7
|
+
luvz.egg-info/dependency_links.txt
|
|
8
|
+
luvz.egg-info/requires.txt
|
|
9
|
+
luvz.egg-info/top_level.txt
|
|
10
|
+
luvz/core/__init__.py
|
|
11
|
+
luvz/core/context/__init__.py
|
|
12
|
+
luvz/core/context/command.py
|
|
13
|
+
luvz/core/context/script.py
|
|
14
|
+
luvz/core/context/task.py
|
|
15
|
+
luvz/core/models/script.py
|
|
16
|
+
luvz/core/runner/__init__.py
|
|
17
|
+
luvz/core/runner/base.py
|
|
18
|
+
luvz/core/runner/cli.py
|
|
19
|
+
luvz/core/runner/interactive.py
|
|
20
|
+
luvz/modules/console.py
|
|
21
|
+
luvz/modules/process.py
|
|
22
|
+
luvz/utils/__init__.py
|
|
23
|
+
luvz/utils/cli.py
|
|
24
|
+
luvz/utils/parser.py
|
|
25
|
+
luvz/utils/path.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
luvz
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "luvz"
|
|
7
|
+
version = "0.0.1.dev0"
|
|
8
|
+
description = "Lazy script builder"
|
|
9
|
+
authors = [{name="luvbyte", email="lovemelong@protonmail.com"}]
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
requires-python = ">=3.8"
|
|
13
|
+
dependencies = ["cmd2", "rich", "pydantic"]
|
|
14
|
+
|
|
15
|
+
[tool.setuptools.packages.find]
|
|
16
|
+
where = ["."]
|
|
17
|
+
include = ["luvz*"]
|