shvcli 0.2.1__tar.gz → 0.4.0__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.
- {shvcli-0.2.1/shvcli.egg-info → shvcli-0.4.0}/PKG-INFO +9 -12
- {shvcli-0.2.1 → shvcli-0.4.0}/README.rst +5 -3
- shvcli-0.4.0/pyproject.toml +101 -0
- shvcli-0.4.0/shvcli/__init__.py +5 -0
- {shvcli-0.2.1 → shvcli-0.4.0}/shvcli/__main__.py +18 -3
- shvcli-0.4.0/shvcli/__version__.py +25 -0
- {shvcli-0.2.1 → shvcli-0.4.0}/shvcli/builtin.py +1 -0
- {shvcli-0.2.1 → shvcli-0.4.0}/shvcli/builtin_impl.py +34 -29
- {shvcli-0.2.1 → shvcli-0.4.0}/shvcli/cli.py +7 -4
- {shvcli-0.2.1 → shvcli-0.4.0}/shvcli/client.py +30 -19
- {shvcli-0.2.1 → shvcli-0.4.0}/shvcli/complet.py +3 -2
- {shvcli-0.2.1 → shvcli-0.4.0}/shvcli/complet_tools.py +1 -0
- shvcli-0.4.0/shvcli/config.py +156 -0
- shvcli-0.4.0/shvcli/lsdir.py +119 -0
- {shvcli-0.2.1 → shvcli-0.4.0}/shvcli/parse.py +5 -5
- shvcli-0.4.0/shvcli/scan.py +30 -0
- shvcli-0.4.0/shvcli/tools.py +133 -0
- {shvcli-0.2.1 → shvcli-0.4.0}/shvcli/valid.py +3 -2
- shvcli-0.4.0/shvcli/version +1 -0
- {shvcli-0.2.1 → shvcli-0.4.0/shvcli.egg-info}/PKG-INFO +9 -12
- {shvcli-0.2.1 → shvcli-0.4.0}/shvcli.egg-info/SOURCES.txt +2 -0
- shvcli-0.4.0/shvcli.egg-info/requires.txt +4 -0
- {shvcli-0.2.1 → shvcli-0.4.0}/shvcli.egg-info/top_level.txt +1 -0
- shvcli-0.2.1/pyproject.toml +0 -89
- shvcli-0.2.1/shvcli/__init__.py +0 -5
- shvcli-0.2.1/shvcli/config.py +0 -109
- shvcli-0.2.1/shvcli/lsdir.py +0 -54
- shvcli-0.2.1/shvcli/tools.py +0 -83
- shvcli-0.2.1/shvcli/version +0 -1
- shvcli-0.2.1/shvcli.egg-info/requires.txt +0 -10
- {shvcli-0.2.1 → shvcli-0.4.0}/LICENSE +0 -0
- {shvcli-0.2.1 → shvcli-0.4.0}/setup.cfg +0 -0
- {shvcli-0.2.1 → shvcli-0.4.0}/shvcli/py.typed +0 -0
- {shvcli-0.2.1 → shvcli-0.4.0}/shvcli.egg-info/dependency_links.txt +0 -0
- {shvcli-0.2.1 → shvcli-0.4.0}/shvcli.egg-info/entry_points.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: shvcli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Silicon Heaven CLI access application
|
|
5
5
|
Author: Elektroline a.s.
|
|
6
6
|
License: MIT License
|
|
@@ -25,7 +25,7 @@ License: MIT License
|
|
|
25
25
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
26
|
SOFTWARE.
|
|
27
27
|
|
|
28
|
-
Project-URL: repository, https://gitlab.com/
|
|
28
|
+
Project-URL: repository, https://gitlab.com/silicon-heaven/shvcli
|
|
29
29
|
Classifier: Development Status :: 4 - Beta
|
|
30
30
|
Classifier: License :: OSI Approved :: MIT License
|
|
31
31
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -38,27 +38,22 @@ Classifier: Topic :: Terminals
|
|
|
38
38
|
Requires-Python: >=3.11
|
|
39
39
|
Description-Content-Type: text/x-rst
|
|
40
40
|
License-File: LICENSE
|
|
41
|
-
Requires-Dist: pyshv>=0.
|
|
41
|
+
Requires-Dist: pyshv>=0.6.0
|
|
42
42
|
Requires-Dist: prompt_toolkit
|
|
43
|
+
Requires-Dist: pygments
|
|
43
44
|
Requires-Dist: pyxdg
|
|
44
|
-
Provides-Extra: lint
|
|
45
|
-
Requires-Dist: black; extra == "lint"
|
|
46
|
-
Requires-Dist: isort; extra == "lint"
|
|
47
|
-
Requires-Dist: mypy; extra == "lint"
|
|
48
|
-
Requires-Dist: pydocstyle; extra == "lint"
|
|
49
|
-
Requires-Dist: pylint; extra == "lint"
|
|
50
45
|
|
|
51
46
|
=====================================
|
|
52
47
|
Silicon Heaven CLI access application
|
|
53
48
|
=====================================
|
|
54
|
-
.. image:: https://gitlab.com/
|
|
49
|
+
.. image:: https://gitlab.com/silicon-heaven/shvcli/-/raw/master/logo.svg
|
|
55
50
|
:align: right
|
|
56
51
|
:height: 128px
|
|
57
52
|
|
|
58
53
|
This provides an easy to use CLI interfase to access the SHV network.
|
|
59
54
|
|
|
60
|
-
* `📃 Sources <https://gitlab.com/
|
|
61
|
-
* `⁉️ Issue tracker <https://gitlab.com/
|
|
55
|
+
* `📃 Sources <https://gitlab.com/silicon-heaven/shvcli>`__
|
|
56
|
+
* `⁉️ Issue tracker <https://gitlab.com/silicon-heaven/shvcli/-/issues>`__
|
|
62
57
|
* `📕 Silicon Heaven protocol documentation <https://silicon-heaven.github.io/shv-doc/>`__
|
|
63
58
|
|
|
64
59
|
|
|
@@ -123,6 +118,8 @@ switched in runtime. The following options are available:
|
|
|
123
118
|
|
|
124
119
|
* **vimode**: If Vi input mode should be used for command line input. The
|
|
125
120
|
default is ``false``.
|
|
121
|
+
* **autoget**: Automatically call getter methods and print received values when
|
|
122
|
+
listing nodes and methods (``ls`` and ``dir`` methods special handling).
|
|
126
123
|
* **autoprobe**: Completion process benefits from probing of the SHV nodes with
|
|
127
124
|
``ls`` and ``dir``, and to provide easier usage this can happen automatically
|
|
128
125
|
in background. This is what this option controls. It is ``true`` by default
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
=====================================
|
|
2
2
|
Silicon Heaven CLI access application
|
|
3
3
|
=====================================
|
|
4
|
-
.. image:: https://gitlab.com/
|
|
4
|
+
.. image:: https://gitlab.com/silicon-heaven/shvcli/-/raw/master/logo.svg
|
|
5
5
|
:align: right
|
|
6
6
|
:height: 128px
|
|
7
7
|
|
|
8
8
|
This provides an easy to use CLI interfase to access the SHV network.
|
|
9
9
|
|
|
10
|
-
* `📃 Sources <https://gitlab.com/
|
|
11
|
-
* `⁉️ Issue tracker <https://gitlab.com/
|
|
10
|
+
* `📃 Sources <https://gitlab.com/silicon-heaven/shvcli>`__
|
|
11
|
+
* `⁉️ Issue tracker <https://gitlab.com/silicon-heaven/shvcli/-/issues>`__
|
|
12
12
|
* `📕 Silicon Heaven protocol documentation <https://silicon-heaven.github.io/shv-doc/>`__
|
|
13
13
|
|
|
14
14
|
|
|
@@ -73,6 +73,8 @@ switched in runtime. The following options are available:
|
|
|
73
73
|
|
|
74
74
|
* **vimode**: If Vi input mode should be used for command line input. The
|
|
75
75
|
default is ``false``.
|
|
76
|
+
* **autoget**: Automatically call getter methods and print received values when
|
|
77
|
+
listing nodes and methods (``ls`` and ``dir`` methods special handling).
|
|
76
78
|
* **autoprobe**: Completion process benefits from probing of the SHV nodes with
|
|
77
79
|
``ls`` and ``dir``, and to provide easier usage this can happen automatically
|
|
78
80
|
in background. This is what this option controls. It is ``true`` by default
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "shvcli"
|
|
3
|
+
version = "0.4.0"
|
|
4
|
+
description = "Silicon Heaven CLI access application"
|
|
5
|
+
readme = "README.rst"
|
|
6
|
+
license = {file = "LICENSE"}
|
|
7
|
+
authors = [
|
|
8
|
+
{ name="Elektroline a.s." },
|
|
9
|
+
]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 4 - Beta",
|
|
12
|
+
"License :: OSI Approved :: MIT License",
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"Operating System :: OS Independent",
|
|
15
|
+
"Environment :: Console",
|
|
16
|
+
"Intended Audience :: End Users/Desktop",
|
|
17
|
+
"Topic :: Home Automation",
|
|
18
|
+
"Topic :: System :: Shells",
|
|
19
|
+
"Topic :: Terminals",
|
|
20
|
+
]
|
|
21
|
+
requires-python = ">=3.11"
|
|
22
|
+
dependencies = [
|
|
23
|
+
"pyshv >= 0.6.0",
|
|
24
|
+
"prompt_toolkit",
|
|
25
|
+
"pygments",
|
|
26
|
+
"pyxdg",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
repository = "https://gitlab.com/silicon-heaven/shvcli"
|
|
31
|
+
|
|
32
|
+
[project.scripts]
|
|
33
|
+
shvcli = "shvcli.__main__:main"
|
|
34
|
+
|
|
35
|
+
[build-system]
|
|
36
|
+
requires = ["setuptools"]
|
|
37
|
+
build-backend = "setuptools.build_meta"
|
|
38
|
+
|
|
39
|
+
[tool.setuptools.packages.find]
|
|
40
|
+
[tool.setuptools.package-data]
|
|
41
|
+
"*" = ["py.typed"]
|
|
42
|
+
|
|
43
|
+
[tool.coverage.run]
|
|
44
|
+
branch = true
|
|
45
|
+
[tool.coverage.report]
|
|
46
|
+
exclude_lines = [
|
|
47
|
+
"pragma: no cover",
|
|
48
|
+
"if typing.TYPE_CHECKING:"
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
[tool.ruff]
|
|
52
|
+
target-version = "py311"
|
|
53
|
+
[tool.ruff.lint]
|
|
54
|
+
select = [
|
|
55
|
+
"F", "E", "W", "I", "N", "D", "UP", "YTT", "ANN", "ASYNC", "TRIO", "S", "BLE",
|
|
56
|
+
"B", "A", "COM", "PL", "RUF"
|
|
57
|
+
]
|
|
58
|
+
ignore = [
|
|
59
|
+
# Magic methods have defined behavior so what is there to document. __init__
|
|
60
|
+
# should be documented in class docstring.
|
|
61
|
+
"D105", "D107",
|
|
62
|
+
# These have conflicting rules.
|
|
63
|
+
"D203", "D213",
|
|
64
|
+
# Ignore too long lines (format tackles that)
|
|
65
|
+
"E501",
|
|
66
|
+
# It is nice that assert should not be used for input checking but it can be
|
|
67
|
+
# also used for internal consistency checking.
|
|
68
|
+
"S101", "B011",
|
|
69
|
+
# subprocess can be insecurely used but not is insecure by design
|
|
70
|
+
"S404", "S603", "S607",
|
|
71
|
+
# Catching Exception is valid and safe in some cases and thus should be allowed
|
|
72
|
+
"BLE001",
|
|
73
|
+
# Conflict with formatter
|
|
74
|
+
"COM812",
|
|
75
|
+
# Remove too-many limit for more complex code.
|
|
76
|
+
"PLR0904", "PLR0911", "PLR0912", "PLR0913", "PLR0917", "PLR2004", "PLR1702",
|
|
77
|
+
"PLR0916",
|
|
78
|
+
# This seems like a wrong rule
|
|
79
|
+
"PLW1641",
|
|
80
|
+
# Deprecated
|
|
81
|
+
"ANN101", "ANN102",
|
|
82
|
+
]
|
|
83
|
+
preview = true
|
|
84
|
+
[tool.ruff.lint.per-file-ignores]
|
|
85
|
+
"tests/**" = [
|
|
86
|
+
"D100", "D101", "D102", "D103", "D104", "S101", "S106", "ANN001", "ANN002",
|
|
87
|
+
"ANN003", "ANN201", "ANN202", "ANN204", "ANN205", "PLR6301",
|
|
88
|
+
]
|
|
89
|
+
"docs/conf.py" = ["D100", "D103", "A001", "ANN201", "ANN001"]
|
|
90
|
+
[tool.ruff.format]
|
|
91
|
+
docstring-code-format = true
|
|
92
|
+
preview = true
|
|
93
|
+
|
|
94
|
+
[tool.mypy]
|
|
95
|
+
python_version = "3.11"
|
|
96
|
+
ignore_missing_imports = true
|
|
97
|
+
disallow_untyped_defs = true
|
|
98
|
+
warn_redundant_casts = true
|
|
99
|
+
warn_unused_ignores = true
|
|
100
|
+
warn_return_any = true
|
|
101
|
+
warn_unreachable = true
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""Implementation of command line application."""
|
|
2
|
+
|
|
2
3
|
import argparse
|
|
3
4
|
import asyncio
|
|
4
5
|
import logging
|
|
@@ -18,11 +19,22 @@ def parse_args() -> argparse.Namespace:
|
|
|
18
19
|
action="store_true",
|
|
19
20
|
help="Enable logging that provides communication debug info.",
|
|
20
21
|
)
|
|
22
|
+
parser.add_argument(
|
|
23
|
+
"-s",
|
|
24
|
+
"--scan",
|
|
25
|
+
action="store_true",
|
|
26
|
+
help="Perform scan as a first action right after connect.",
|
|
27
|
+
)
|
|
28
|
+
parser.add_argument(
|
|
29
|
+
"--scan-depth",
|
|
30
|
+
type=int,
|
|
31
|
+
default=3,
|
|
32
|
+
help="Depth of the scan performed if --scan is used.",
|
|
33
|
+
)
|
|
21
34
|
parser.add_argument(
|
|
22
35
|
"URL",
|
|
23
36
|
nargs="?",
|
|
24
|
-
|
|
25
|
-
help="SHV RPC URL specifying connection to the broker.",
|
|
37
|
+
help="SHV RPC URL specifying connection to the broker or host from configuration.",
|
|
26
38
|
)
|
|
27
39
|
return parser.parse_args()
|
|
28
40
|
|
|
@@ -32,8 +44,11 @@ def main() -> None:
|
|
|
32
44
|
logging.basicConfig(format="[%(asctime)s] [%(levelname)s] - %(message)s")
|
|
33
45
|
args = parse_args()
|
|
34
46
|
config = CliConfig()
|
|
35
|
-
|
|
47
|
+
if args.URL is not None:
|
|
48
|
+
config.url = args.URL
|
|
36
49
|
config.debug = config.debug or args.debug
|
|
50
|
+
config.initial_scan = args.scan
|
|
51
|
+
config.initial_scan_depth = args.scan_depth
|
|
37
52
|
asyncio.run(run(config))
|
|
38
53
|
|
|
39
54
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""The smarter loader of the version.
|
|
2
|
+
|
|
3
|
+
It identifies if this package is installed and returns version from egg info or
|
|
4
|
+
it looks for pyproject.toml file in upper directory.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import importlib.metadata
|
|
8
|
+
import pathlib
|
|
9
|
+
import tomllib
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _get_version() -> str:
|
|
13
|
+
for dist_name in importlib.metadata.packages_distributions().get(__package__, []):
|
|
14
|
+
dist = importlib.metadata.distribution(dist_name)
|
|
15
|
+
if str(dist.locate_file(f"{__package__}/__version__.py")) == __file__:
|
|
16
|
+
return dist.version
|
|
17
|
+
pyproject = pathlib.Path(__file__).parent.parent / "pyproject.toml"
|
|
18
|
+
with pyproject.open("rb") as f:
|
|
19
|
+
res = tomllib.load(f)["project"]["version"]
|
|
20
|
+
if isinstance(res, str):
|
|
21
|
+
return res
|
|
22
|
+
return "unknown"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
VERSION = _get_version()
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"""Implementations of our builtin methods."""
|
|
2
|
+
|
|
2
3
|
import collections.abc
|
|
4
|
+
import inspect
|
|
3
5
|
import itertools
|
|
4
6
|
|
|
5
7
|
from prompt_toolkit.completion import Completion
|
|
6
|
-
from prompt_toolkit.shortcuts import ProgressBar, ProgressBarCounter
|
|
7
8
|
from shv import RpcSubscription
|
|
8
9
|
|
|
9
10
|
from .builtin import METHODS, Argument, XMethod, builtin, xbuiltin
|
|
@@ -12,6 +13,7 @@ from .complet_tools import comp_from, comp_path
|
|
|
12
13
|
from .config import CliConfig
|
|
13
14
|
from .lsdir import ls_node_format
|
|
14
15
|
from .parse import CliItems
|
|
16
|
+
from .scan import scan_nodes
|
|
15
17
|
from .tools import lookahead, print_block, print_ftext, print_row
|
|
16
18
|
|
|
17
19
|
|
|
@@ -35,11 +37,14 @@ def argument_set_comp(
|
|
|
35
37
|
"""Completion for !set method."""
|
|
36
38
|
opt, val = items.interpret_param_set()
|
|
37
39
|
if val is not None:
|
|
38
|
-
if opt
|
|
40
|
+
if config.OPTS.get(opt or "") is config.Type.BOOL:
|
|
39
41
|
yield from comp_from(val, {"true", "false"})
|
|
40
42
|
else:
|
|
41
|
-
yield from comp_from(items.param_raw, config.
|
|
42
|
-
yield from comp_from(
|
|
43
|
+
yield from comp_from(items.param_raw, config.OPTS.keys())
|
|
44
|
+
yield from comp_from(
|
|
45
|
+
items.param_raw,
|
|
46
|
+
(f"no{v}" for v, t in config.OPTS.items() if t is config.Type.BOOL),
|
|
47
|
+
)
|
|
43
48
|
|
|
44
49
|
|
|
45
50
|
argument_signal = Argument("[PATH:SIGNAL]", argument_signal_comp, autoprobe=True)
|
|
@@ -58,8 +63,8 @@ async def _help(_: SHVClient, __: CliConfig, ___: CliItems) -> None:
|
|
|
58
63
|
call = f"{name}[X]"
|
|
59
64
|
else:
|
|
60
65
|
call = name + "".join(f"|{n}" for n in m.aliases)
|
|
61
|
-
print_row(
|
|
62
|
-
print_block(m.description, 4)
|
|
66
|
+
print_row(f"{call} {m.argument.description if m.argument else ''}")
|
|
67
|
+
print_block(inspect.cleandoc(m.description), 4)
|
|
63
68
|
|
|
64
69
|
|
|
65
70
|
@builtin(aliases={"sub"}, argument=argument_signal)
|
|
@@ -91,7 +96,10 @@ async def tree(shvclient: SHVClient, config: CliConfig, items: CliItems) -> None
|
|
|
91
96
|
print_ftext(
|
|
92
97
|
itertools.chain(
|
|
93
98
|
(("", "│ " if c else " ") for c in cols),
|
|
94
|
-
iter([
|
|
99
|
+
iter([
|
|
100
|
+
("", "├─" if hasnext else "└─"),
|
|
101
|
+
ls_node_format(name, parent_node=node),
|
|
102
|
+
]),
|
|
95
103
|
)
|
|
96
104
|
)
|
|
97
105
|
print_node(node[name], [*cols, hasnext])
|
|
@@ -114,22 +122,7 @@ async def scan(
|
|
|
114
122
|
|
|
115
123
|
Scan uses 'ls' and 'dir' to fetch info about all nodes.
|
|
116
124
|
"""
|
|
117
|
-
|
|
118
|
-
path = items.interpret_param_path(config)
|
|
119
|
-
depth += path.count("/") # Extend depth to the depth in path
|
|
120
|
-
pths = [path]
|
|
121
|
-
with ProgressBar() as pb:
|
|
122
|
-
pbcnt: ProgressBarCounter = pb()
|
|
123
|
-
pbcnt.total = 1
|
|
124
|
-
while pths:
|
|
125
|
-
pth = pths.pop()
|
|
126
|
-
pbcnt.label = pth
|
|
127
|
-
node = await shvclient.probe(pth)
|
|
128
|
-
if node is not None and (pth.count("/") + 1 if pth else 0) < depth:
|
|
129
|
-
assert node.nodes is not None
|
|
130
|
-
pths.extend(f"{pth}{'/' if pth else ''}{name}" for name in node.nodes)
|
|
131
|
-
pbcnt.total += len(node.nodes)
|
|
132
|
-
pbcnt.item_completed()
|
|
125
|
+
await scan_nodes(shvclient, items.interpret_param_path(config), depth or 3)
|
|
133
126
|
|
|
134
127
|
|
|
135
128
|
@builtin("set", aliases={"s"}, argument=argument_set)
|
|
@@ -137,17 +130,29 @@ async def _set(_: SHVClient, config: CliConfig, items: CliItems) -> None:
|
|
|
137
130
|
"""Set configuration options in runtime."""
|
|
138
131
|
opt, val = items.interpret_param_set()
|
|
139
132
|
if not opt:
|
|
140
|
-
for n in config.
|
|
141
|
-
|
|
133
|
+
w = max(len(n) for n in config.OPTS.keys())
|
|
134
|
+
for n, t in config.OPTS.items():
|
|
135
|
+
row = [("", (" " * (w - len(n))) + n + " ")]
|
|
136
|
+
match t:
|
|
137
|
+
case config.Type.BOOL:
|
|
138
|
+
v = getattr(config, n)
|
|
139
|
+
assert isinstance(v, bool)
|
|
140
|
+
row.append(("ansigreen" if v else "ansired", str(v).lower()))
|
|
141
|
+
case config.Type.INT:
|
|
142
|
+
row.append(("", str(getattr(config, n))))
|
|
143
|
+
case _:
|
|
144
|
+
raise NotImplementedError(f"Unimplemented {t!r}")
|
|
145
|
+
print_row(row)
|
|
142
146
|
return
|
|
143
|
-
|
|
144
|
-
|
|
147
|
+
|
|
148
|
+
# TODO from here we need add support for integers when we need it.
|
|
149
|
+
if no := opt.startswith("no"):
|
|
145
150
|
opt = opt[2:]
|
|
146
|
-
if opt not in config.
|
|
151
|
+
if opt not in config.OPTS.keys():
|
|
147
152
|
print(f"Invalid option: {opt}")
|
|
148
153
|
return
|
|
149
154
|
if val is None:
|
|
150
|
-
value = not
|
|
155
|
+
value = not no
|
|
151
156
|
else:
|
|
152
157
|
m = {"true": True, "t": True, "false": False, "f": False}
|
|
153
158
|
if val not in m:
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""Command line interface."""
|
|
2
|
+
|
|
2
3
|
import asyncio
|
|
3
4
|
import json
|
|
4
5
|
import pathlib
|
|
@@ -8,16 +9,16 @@ import xdg.BaseDirectory
|
|
|
8
9
|
from prompt_toolkit import PromptSession
|
|
9
10
|
from prompt_toolkit.history import FileHistory
|
|
10
11
|
from prompt_toolkit.patch_stdout import patch_stdout
|
|
11
|
-
from shv import RpcError, RpcUrl
|
|
12
|
-
from shv.cpon import Cpon
|
|
12
|
+
from shv import RpcError, RpcLogin, RpcUrl
|
|
13
13
|
|
|
14
|
-
from . import builtin_impl as _
|
|
14
|
+
from . import builtin_impl as _ # noqa F401
|
|
15
15
|
from .builtin import call_builtin
|
|
16
16
|
from .client import Node, SHVClient
|
|
17
17
|
from .complet import CliCompleter
|
|
18
18
|
from .config import CliConfig
|
|
19
19
|
from .lsdir import dir_method, ls_method
|
|
20
20
|
from .parse import parse_line
|
|
21
|
+
from .scan import scan_nodes
|
|
21
22
|
from .tools import print_cpon
|
|
22
23
|
from .valid import CliValidator
|
|
23
24
|
|
|
@@ -68,7 +69,7 @@ async def run(config: CliConfig) -> None:
|
|
|
68
69
|
location=config.url.location,
|
|
69
70
|
port=config.url.port,
|
|
70
71
|
protocol=config.url.protocol,
|
|
71
|
-
username=config.url.username,
|
|
72
|
+
login=RpcLogin(username=config.url.login.username),
|
|
72
73
|
)
|
|
73
74
|
cpath = xdg.BaseDirectory.save_cache_path("shvcli")
|
|
74
75
|
fname = re.sub(r"[^\w_. -]", "_", cacheurl.to_url())
|
|
@@ -76,6 +77,8 @@ async def run(config: CliConfig) -> None:
|
|
|
76
77
|
if cachepath.exists():
|
|
77
78
|
with cachepath.open("r") as f:
|
|
78
79
|
shvclient.tree = Node.load(json.load(f))
|
|
80
|
+
if config.initial_scan:
|
|
81
|
+
await scan_nodes(shvclient, "", config.initial_scan_depth)
|
|
79
82
|
|
|
80
83
|
clitask = asyncio.create_task(_app(config, shvclient))
|
|
81
84
|
await shvclient.client.wait_disconnect()
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""SHV client for shvcli."""
|
|
2
|
+
|
|
2
3
|
from __future__ import annotations
|
|
3
4
|
|
|
4
5
|
import collections.abc
|
|
@@ -6,7 +7,6 @@ import typing
|
|
|
6
7
|
from pathlib import PurePosixPath
|
|
7
8
|
|
|
8
9
|
from shv import (
|
|
9
|
-
RpcClient,
|
|
10
10
|
RpcError,
|
|
11
11
|
RpcMessage,
|
|
12
12
|
RpcMethodDesc,
|
|
@@ -20,7 +20,7 @@ from shv.cpon import Cpon
|
|
|
20
20
|
from . import VERSION
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
class Node(collections.abc.Mapping):
|
|
23
|
+
class Node(collections.abc.Mapping[str, "Node"]):
|
|
24
24
|
"""Abstraction on the tree node."""
|
|
25
25
|
|
|
26
26
|
def __init__(self) -> None:
|
|
@@ -31,7 +31,7 @@ class Node(collections.abc.Mapping):
|
|
|
31
31
|
self.nodes_probed = False
|
|
32
32
|
self.methods_probed = False
|
|
33
33
|
|
|
34
|
-
def __getitem__(self, key: str) ->
|
|
34
|
+
def __getitem__(self, key: str) -> Node:
|
|
35
35
|
"""Get node from nodes."""
|
|
36
36
|
return self.nodes[key]
|
|
37
37
|
|
|
@@ -43,7 +43,7 @@ class Node(collections.abc.Mapping):
|
|
|
43
43
|
"""Get number of child nodes."""
|
|
44
44
|
return len(self.nodes)
|
|
45
45
|
|
|
46
|
-
def valid_path(self, path: str | PurePosixPath) ->
|
|
46
|
+
def valid_path(self, path: str | PurePosixPath) -> Node:
|
|
47
47
|
"""Add valid path relative to this node."""
|
|
48
48
|
if isinstance(path, str):
|
|
49
49
|
path = PurePosixPath(path)
|
|
@@ -68,7 +68,7 @@ class Node(collections.abc.Mapping):
|
|
|
68
68
|
if pnode is not None:
|
|
69
69
|
pnode.nodes.pop(path.name)
|
|
70
70
|
|
|
71
|
-
def get_path(self, path: str | PurePosixPath) ->
|
|
71
|
+
def get_path(self, path: str | PurePosixPath) -> None | Node:
|
|
72
72
|
"""Get node on given path."""
|
|
73
73
|
if isinstance(path, str):
|
|
74
74
|
path = PurePosixPath(path)
|
|
@@ -79,7 +79,7 @@ class Node(collections.abc.Mapping):
|
|
|
79
79
|
node = node[n]
|
|
80
80
|
return node
|
|
81
81
|
|
|
82
|
-
def dump(self) ->
|
|
82
|
+
def dump(self) -> dict[str, object]:
|
|
83
83
|
"""Dump the data to basic types."""
|
|
84
84
|
return {
|
|
85
85
|
"nodes": {n: v.dump() for n, v in self.nodes.items()},
|
|
@@ -90,15 +90,18 @@ class Node(collections.abc.Mapping):
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
@classmethod
|
|
93
|
-
def load(cls, data:
|
|
93
|
+
def load(cls, data: collections.abc.Mapping[str, object]) -> Node:
|
|
94
94
|
"""Load node tree from dump."""
|
|
95
95
|
assert isinstance(data, collections.abc.Mapping)
|
|
96
96
|
res = cls()
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
97
|
+
if isinstance(data["nodes"], collections.abc.Mapping):
|
|
98
|
+
res.nodes = {n: cls.load(v) for n, v in data["nodes"].items()}
|
|
99
|
+
if isinstance(data["methods"], collections.abc.Sequence):
|
|
100
|
+
res.methods = set(data["methods"])
|
|
101
|
+
if isinstance(data["signals"], collections.abc.Sequence):
|
|
102
|
+
res.signals = set(data["signals"])
|
|
103
|
+
res.nodes_probed = bool(data["nodes_probed"])
|
|
104
|
+
res.methods_probed = bool(data["methods_probed"])
|
|
102
105
|
return res
|
|
103
106
|
|
|
104
107
|
|
|
@@ -108,11 +111,11 @@ class SHVClient(SimpleClient):
|
|
|
108
111
|
APP_NAME = "shvcli"
|
|
109
112
|
APP_VERSION = VERSION
|
|
110
113
|
|
|
111
|
-
def __init__(self,
|
|
114
|
+
def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None: # noqa ANN401
|
|
112
115
|
"""Initialize client and set create reference to tree."""
|
|
113
116
|
self.tree = Node()
|
|
114
117
|
self.tree.valid_path(".app")
|
|
115
|
-
super().__init__(
|
|
118
|
+
super().__init__(*args, **kwargs)
|
|
116
119
|
|
|
117
120
|
async def _loop(self) -> None:
|
|
118
121
|
await super()._loop()
|
|
@@ -150,19 +153,27 @@ class SHVClient(SimpleClient):
|
|
|
150
153
|
self.tree.invalid_path(path)
|
|
151
154
|
raise exc
|
|
152
155
|
node = self.tree.valid_path(path)
|
|
153
|
-
node.methods = {
|
|
154
|
-
|
|
156
|
+
node.methods = {
|
|
157
|
+
d.name for d in res if RpcMethodFlags.NOT_CALLABLE not in d.flags
|
|
158
|
+
}
|
|
159
|
+
node.signals = {d.name for d in res if RpcMethodFlags.NOT_CALLABLE in d.flags}
|
|
155
160
|
node.methods_probed = True
|
|
156
161
|
return res
|
|
157
162
|
|
|
158
|
-
async def call(
|
|
163
|
+
async def call(
|
|
164
|
+
self,
|
|
165
|
+
path: str,
|
|
166
|
+
method: str,
|
|
167
|
+
*args: typing.Any, # noqa ANN401
|
|
168
|
+
**kwargs: typing.Any, # noqa ANN401
|
|
169
|
+
) -> SHVType:
|
|
159
170
|
"""Perform call same as ValueClient.
|
|
160
171
|
|
|
161
172
|
This uses every call to get insite into existence of method and node and records
|
|
162
173
|
that in the tree.
|
|
163
174
|
"""
|
|
164
175
|
try:
|
|
165
|
-
res = await super().call(path, method,
|
|
176
|
+
res = await super().call(path, method, *args, **kwargs)
|
|
166
177
|
except RpcMethodNotFoundError as exc:
|
|
167
178
|
raise exc
|
|
168
179
|
except RpcError as exc:
|
|
@@ -190,7 +201,7 @@ class SHVClient(SimpleClient):
|
|
|
190
201
|
async def path_is_valid(self, path: str) -> bool:
|
|
191
202
|
"""Check if given path is valid by using ls command."""
|
|
192
203
|
path = path.strip("/")
|
|
193
|
-
if path
|
|
204
|
+
if not path:
|
|
194
205
|
return True # top level is always valid
|
|
195
206
|
if "/" in path:
|
|
196
207
|
pth, name = path.rsplit("/", maxsplit=1)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""Completion for CLI."""
|
|
2
|
+
|
|
2
3
|
import collections.abc
|
|
3
4
|
import typing
|
|
4
5
|
|
|
@@ -28,7 +29,7 @@ class CliCompleter(Completer):
|
|
|
28
29
|
|
|
29
30
|
# Parameters
|
|
30
31
|
if CliFlags.COMPLETE_CALL in items.flags:
|
|
31
|
-
if items.method in
|
|
32
|
+
if items.method in {"ls", "dir"} and not self.config.raw:
|
|
32
33
|
yield from comp_path(self.shvclient, self.config, items)
|
|
33
34
|
elif bmethod := builtin.get_builtin(items.method[1:]):
|
|
34
35
|
if bmethod.argument:
|
|
@@ -58,7 +59,7 @@ class CliCompleter(Completer):
|
|
|
58
59
|
items = parse_line(document.text)
|
|
59
60
|
if self.config.autoprobe and (
|
|
60
61
|
CliFlags.COMPLETE_CALL not in items.flags
|
|
61
|
-
or items.method in
|
|
62
|
+
or items.method in {"ls", "dir"}
|
|
62
63
|
or (
|
|
63
64
|
items.method[0] == "!"
|
|
64
65
|
and (bmethod := builtin.get_builtin(items.method[1:])) is not None
|