shvcli 0.3.0__tar.gz → 0.5.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.
Files changed (31) hide show
  1. {shvcli-0.3.0/shvcli.egg-info → shvcli-0.5.0}/PKG-INFO +7 -12
  2. {shvcli-0.3.0 → shvcli-0.5.0}/README.rst +3 -3
  3. shvcli-0.5.0/pyproject.toml +101 -0
  4. shvcli-0.5.0/shvcli/__init__.py +5 -0
  5. {shvcli-0.3.0 → shvcli-0.5.0}/shvcli/__main__.py +1 -0
  6. shvcli-0.5.0/shvcli/__version__.py +26 -0
  7. {shvcli-0.3.0 → shvcli-0.5.0}/shvcli/builtin.py +8 -2
  8. {shvcli-0.3.0 → shvcli-0.5.0}/shvcli/builtin_impl.py +15 -17
  9. {shvcli-0.3.0 → shvcli-0.5.0}/shvcli/cli.py +5 -4
  10. {shvcli-0.3.0 → shvcli-0.5.0}/shvcli/client.py +36 -28
  11. {shvcli-0.3.0 → shvcli-0.5.0}/shvcli/complet.py +5 -3
  12. {shvcli-0.3.0 → shvcli-0.5.0}/shvcli/complet_tools.py +1 -0
  13. {shvcli-0.3.0 → shvcli-0.5.0}/shvcli/config.py +3 -2
  14. {shvcli-0.3.0 → shvcli-0.5.0}/shvcli/lsdir.py +11 -7
  15. {shvcli-0.3.0 → shvcli-0.5.0}/shvcli/parse.py +14 -22
  16. {shvcli-0.3.0 → shvcli-0.5.0}/shvcli/scan.py +1 -0
  17. {shvcli-0.3.0 → shvcli-0.5.0}/shvcli/tools.py +4 -3
  18. {shvcli-0.3.0 → shvcli-0.5.0}/shvcli/valid.py +3 -2
  19. {shvcli-0.3.0 → shvcli-0.5.0/shvcli.egg-info}/PKG-INFO +7 -12
  20. {shvcli-0.3.0 → shvcli-0.5.0}/shvcli.egg-info/SOURCES.txt +1 -0
  21. shvcli-0.5.0/shvcli.egg-info/requires.txt +4 -0
  22. {shvcli-0.3.0 → shvcli-0.5.0}/shvcli.egg-info/top_level.txt +1 -0
  23. shvcli-0.3.0/pyproject.toml +0 -89
  24. shvcli-0.3.0/shvcli/__init__.py +0 -5
  25. shvcli-0.3.0/shvcli.egg-info/requires.txt +0 -10
  26. {shvcli-0.3.0 → shvcli-0.5.0}/LICENSE +0 -0
  27. {shvcli-0.3.0 → shvcli-0.5.0}/setup.cfg +0 -0
  28. {shvcli-0.3.0 → shvcli-0.5.0}/shvcli/py.typed +0 -0
  29. {shvcli-0.3.0 → shvcli-0.5.0}/shvcli/version +0 -0
  30. {shvcli-0.3.0 → shvcli-0.5.0}/shvcli.egg-info/dependency_links.txt +0 -0
  31. {shvcli-0.3.0 → shvcli-0.5.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.0
3
+ Version: 0.5.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/elektroline-predator/shvcli
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.5.0
41
+ Requires-Dist: pyshv>=0.7.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/elektroline-predator/shvcli/-/raw/master/logo.svg
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/elektroline-predator/shvcli>`__
61
- * `⁉️ Issue tracker <https://gitlab.com/elektroline-predator/shvcli/-/issues>`__
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
 
@@ -1,14 +1,14 @@
1
1
  =====================================
2
2
  Silicon Heaven CLI access application
3
3
  =====================================
4
- .. image:: https://gitlab.com/elektroline-predator/shvcli/-/raw/master/logo.svg
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/elektroline-predator/shvcli>`__
11
- * `⁉️ Issue tracker <https://gitlab.com/elektroline-predator/shvcli/-/issues>`__
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
 
@@ -0,0 +1,101 @@
1
+ [project]
2
+ name = "shvcli"
3
+ version = "0.5.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.7.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", "ASYNC1", "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
@@ -0,0 +1,5 @@
1
+ """Silicon Heaven Command Line Interface."""
2
+
3
+ from .__version__ import VERSION
4
+
5
+ __all__ = ["VERSION"]
@@ -1,4 +1,5 @@
1
1
  """Implementation of command line application."""
2
+
2
3
  import argparse
3
4
  import asyncio
4
5
  import logging
@@ -0,0 +1,26 @@
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
+ if pyproject.exists():
19
+ with pyproject.open("rb") as f:
20
+ res = tomllib.load(f)["project"]["version"]
21
+ if isinstance(res, str):
22
+ return res
23
+ return "unknown"
24
+
25
+
26
+ VERSION = _get_version()
@@ -1,4 +1,5 @@
1
1
  """Builtin methods glue code so they can be defined separatelly."""
2
+
2
3
  import collections.abc
3
4
  import dataclasses
4
5
  import typing
@@ -9,7 +10,9 @@ from .client import SHVClient
9
10
  from .config import CliConfig
10
11
  from .parse import CliItems
11
12
 
12
- _T_METHOD = typing.Callable[[SHVClient, CliConfig, CliItems], typing.Awaitable[None]]
13
+ _T_METHOD = typing.Callable[
14
+ [SHVClient, CliConfig, CliItems], typing.Awaitable[None] | None
15
+ ]
13
16
  _T_XMETHOD = typing.Callable[
14
17
  [SHVClient, CliConfig, CliItems, int | None], typing.Awaitable[None]
15
18
  ]
@@ -114,7 +117,10 @@ async def call_builtin(
114
117
  return
115
118
 
116
119
  if isinstance(m, Method):
117
- return await m.func(shvclient, config, items)
120
+ res = m.func(shvclient, config, items)
121
+ if isinstance(res, collections.abc.Awaitable):
122
+ await res
123
+ return
118
124
 
119
125
  typing.assert_type(m, XMethod)
120
126
  strnum = items.method[len(m.name) + 1 :]
@@ -1,10 +1,10 @@
1
1
  """Implementations of our builtin methods."""
2
+
2
3
  import collections.abc
3
4
  import inspect
4
5
  import itertools
5
6
 
6
7
  from prompt_toolkit.completion import Completion
7
- from shv import RpcSubscription
8
8
 
9
9
  from .builtin import METHODS, Argument, XMethod, builtin, xbuiltin
10
10
  from .client import Node, SHVClient
@@ -21,11 +21,13 @@ def argument_signal_comp(
21
21
  ) -> collections.abc.Iterable[Completion]:
22
22
  """Completion for subscribe argument."""
23
23
  if ":" in items.param_raw:
24
- path, method = items.interpret_param_method(config)
25
- assert method is not None
24
+ path, method, signal = items.interpret_param_ri(config).split(":")
26
25
  node = shvclient.tree.get_path(path)
27
26
  if node is not None:
28
- yield from comp_from(method, node.signals)
27
+ if items.param_raw.count(":") == 1:
28
+ yield from comp_from(method, node.methods)
29
+ elif method in node.methods:
30
+ yield from comp_from(signal, node.methods[method])
29
31
  else:
30
32
  yield from comp_path(shvclient, config, items)
31
33
 
@@ -52,7 +54,7 @@ argument_set = Argument("[OPTION [VALUE]]", argument_set_comp)
52
54
 
53
55
 
54
56
  @builtin("help", {"h"}, hidden=True)
55
- async def _help(_: SHVClient, __: CliConfig, ___: CliItems) -> None:
57
+ def _help(_: SHVClient, __: CliConfig, ___: CliItems) -> None:
56
58
  """Print help for all builtin methods."""
57
59
  print_row("Available internal methods (all prefixed with '!'):")
58
60
  for name, m in METHODS.items():
@@ -69,15 +71,13 @@ async def _help(_: SHVClient, __: CliConfig, ___: CliItems) -> None:
69
71
  @builtin(aliases={"sub"}, argument=argument_signal)
70
72
  async def subscribe(shvclient: SHVClient, config: CliConfig, items: CliItems) -> None:
71
73
  """Add new subscribe."""
72
- path, method = items.interpret_param_method(config)
73
- await shvclient.subscribe(RpcSubscription(path, method or ""))
74
+ await shvclient.subscribe(items.interpret_param_ri(config))
74
75
 
75
76
 
76
77
  @builtin(aliases={"usub"}, argument=argument_signal)
77
78
  async def unsubscribe(shvclient: SHVClient, config: CliConfig, items: CliItems) -> None:
78
79
  """Unsubscribe existing subscription."""
79
- path, method = items.interpret_param_method(config)
80
- await shvclient.unsubscribe(RpcSubscription(path, method or ""))
80
+ await shvclient.unsubscribe(items.interpret_param_ri(config))
81
81
 
82
82
 
83
83
  @builtin(aliases={"subs", "test"})
@@ -87,7 +87,7 @@ async def subscriptions(shvclient: SHVClient, _: CliConfig, __: CliItems) -> Non
87
87
 
88
88
 
89
89
  @builtin(aliases={"t"}, argument=argument_path)
90
- async def tree(shvclient: SHVClient, config: CliConfig, items: CliItems) -> None:
90
+ def tree(shvclient: SHVClient, config: CliConfig, items: CliItems) -> None:
91
91
  """Print tree of nodes discovered in this session."""
92
92
 
93
93
  def print_node(node: Node, cols: list[bool]) -> None:
@@ -95,12 +95,10 @@ async def tree(shvclient: SHVClient, config: CliConfig, items: CliItems) -> None
95
95
  print_ftext(
96
96
  itertools.chain(
97
97
  (("", "│ " if c else " ") for c in cols),
98
- iter(
99
- [
100
- ("", "├─" if hasnext else "└─"),
101
- ls_node_format(name, parent_node=node),
102
- ]
103
- ),
98
+ iter([
99
+ ("", "├─" if hasnext else "└─"),
100
+ ls_node_format(name, parent_node=node),
101
+ ]),
104
102
  )
105
103
  )
106
104
  print_node(node[name], [*cols, hasnext])
@@ -127,7 +125,7 @@ async def scan(
127
125
 
128
126
 
129
127
  @builtin("set", aliases={"s"}, argument=argument_set)
130
- async def _set(_: SHVClient, config: CliConfig, items: CliItems) -> None:
128
+ def _set(_: SHVClient, config: CliConfig, items: CliItems) -> None:
131
129
  """Set configuration options in runtime."""
132
130
  opt, val = items.interpret_param_set()
133
131
  if not opt:
@@ -1,4 +1,5 @@
1
1
  """Command line interface."""
2
+
2
3
  import asyncio
3
4
  import json
4
5
  import pathlib
@@ -8,9 +9,9 @@ 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 import RpcError, RpcLogin, RpcUrl
12
13
 
13
- from . import builtin_impl as _
14
+ from . import builtin_impl as _ # noqa F401
14
15
  from .builtin import call_builtin
15
16
  from .client import Node, SHVClient
16
17
  from .complet import CliCompleter
@@ -51,7 +52,7 @@ async def _app(config: CliConfig, shvclient: SHVClient) -> None:
51
52
  [prompt_path, ("", "> ")], vi_mode=config.vimode
52
53
  )
53
54
  except EOFError:
54
- shvclient.client.disconnect()
55
+ await shvclient.disconnect()
55
56
  return
56
57
  await handle_line(shvclient, config, result)
57
58
  except KeyboardInterrupt:
@@ -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())
@@ -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,
@@ -26,12 +26,11 @@ class Node(collections.abc.Mapping[str, "Node"]):
26
26
  def __init__(self) -> None:
27
27
  """Initialize the node."""
28
28
  self.nodes: dict[str, Node] = {}
29
- self.methods: set[str] = {"ls", "dir"}
30
- self.signals: set[str] = set()
29
+ self.methods: dict[str, set[str]] = {"ls": {"lsmod"}, "dir": set()}
31
30
  self.nodes_probed = False
32
31
  self.methods_probed = False
33
32
 
34
- def __getitem__(self, key: str) -> "Node":
33
+ def __getitem__(self, key: str) -> Node:
35
34
  """Get node from nodes."""
36
35
  return self.nodes[key]
37
36
 
@@ -43,7 +42,7 @@ class Node(collections.abc.Mapping[str, "Node"]):
43
42
  """Get number of child nodes."""
44
43
  return len(self.nodes)
45
44
 
46
- def valid_path(self, path: str | PurePosixPath) -> "Node":
45
+ def valid_path(self, path: str | PurePosixPath) -> Node:
47
46
  """Add valid path relative to this node."""
48
47
  if isinstance(path, str):
49
48
  path = PurePosixPath(path)
@@ -68,7 +67,7 @@ class Node(collections.abc.Mapping[str, "Node"]):
68
67
  if pnode is not None:
69
68
  pnode.nodes.pop(path.name)
70
69
 
71
- def get_path(self, path: str | PurePosixPath) -> typing.Union[None, "Node"]:
70
+ def get_path(self, path: str | PurePosixPath) -> None | Node:
72
71
  """Get node on given path."""
73
72
  if isinstance(path, str):
74
73
  path = PurePosixPath(path)
@@ -79,26 +78,26 @@ class Node(collections.abc.Mapping[str, "Node"]):
79
78
  node = node[n]
80
79
  return node
81
80
 
82
- def dump(self) -> typing.Any:
81
+ def dump(self) -> dict[str, object]:
83
82
  """Dump the data to basic types."""
84
83
  return {
85
84
  "nodes": {n: v.dump() for n, v in self.nodes.items()},
86
- "methods": list(self.methods),
87
- "signals": list(self.signals),
85
+ "methods": {n: list(v) for n, v in self.methods.items()},
88
86
  "nodes_probed": self.nodes_probed,
89
87
  "methods_probed": self.methods_probed,
90
88
  }
91
89
 
92
90
  @classmethod
93
- def load(cls, data: typing.Any) -> Node:
91
+ def load(cls, data: collections.abc.Mapping[str, object]) -> Node:
94
92
  """Load node tree from dump."""
95
93
  assert isinstance(data, collections.abc.Mapping)
96
94
  res = cls()
97
- res.nodes = {n: cls.load(v) for n, v in data["nodes"].items()}
98
- res.methods = set(data["methods"])
99
- res.signals = set(data["signals"])
100
- res.nodes_probed = data["nodes_probed"]
101
- res.methods_probed = data["methods_probed"]
95
+ if isinstance(data["nodes"], collections.abc.Mapping):
96
+ res.nodes = {n: cls.load(v) for n, v in data["nodes"].items()}
97
+ if isinstance(data["methods"], collections.abc.Mapping):
98
+ res.methods = {str(n): set(v) for n, v in data["methods"].items()}
99
+ res.nodes_probed = bool(data["nodes_probed"])
100
+ res.methods_probed = bool(data["methods_probed"])
102
101
  return res
103
102
 
104
103
 
@@ -108,11 +107,11 @@ class SHVClient(SimpleClient):
108
107
  APP_NAME = "shvcli"
109
108
  APP_VERSION = VERSION
110
109
 
111
- def __init__(self, client: RpcClient) -> None:
110
+ def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None: # noqa ANN401
112
111
  """Initialize client and set create reference to tree."""
113
112
  self.tree = Node()
114
113
  self.tree.valid_path(".app")
115
- super().__init__(client)
114
+ super().__init__(*args, **kwargs)
116
115
 
117
116
  async def _loop(self) -> None:
118
117
  await super()._loop()
@@ -121,10 +120,10 @@ class SHVClient(SimpleClient):
121
120
  async def _message(self, msg: RpcMessage) -> None:
122
121
  await super()._message(msg)
123
122
  if msg.is_signal:
124
- method = msg.method
125
- assert method is not None
126
- self.tree.valid_path(msg.path or "").signals.add(method)
127
- print(f"{msg.path}:{method}: {Cpon.pack(msg.param)}")
123
+ self.tree.valid_path(msg.path or "").methods.setdefault(
124
+ msg.source, set()
125
+ ).add(msg.signal_name)
126
+ print(f"{msg.path}:{msg.source}:{msg.signal_name}: {Cpon.pack(msg.param)}")
128
127
 
129
128
  async def ls(self, path: str) -> list[str]:
130
129
  """List same as in ValueClient but with result being preserved in tree."""
@@ -150,25 +149,34 @@ class SHVClient(SimpleClient):
150
149
  self.tree.invalid_path(path)
151
150
  raise exc
152
151
  node = self.tree.valid_path(path)
153
- node.methods = {d.name for d in res if RpcMethodFlags.SIGNAL not in d.flags}
154
- node.signals = {d.name for d in res if RpcMethodFlags.SIGNAL in d.flags}
152
+ node.methods = {
153
+ d.name: set(d.signals)
154
+ for d in res
155
+ if RpcMethodFlags.NOT_CALLABLE not in d.flags
156
+ }
155
157
  node.methods_probed = True
156
158
  return res
157
159
 
158
- async def call(self, path: str, method: str, param: SHVType = None) -> SHVType:
160
+ async def call(
161
+ self,
162
+ path: str,
163
+ method: str,
164
+ *args: typing.Any, # noqa ANN401
165
+ **kwargs: typing.Any, # noqa ANN401
166
+ ) -> SHVType:
159
167
  """Perform call same as ValueClient.
160
168
 
161
169
  This uses every call to get insite into existence of method and node and records
162
170
  that in the tree.
163
171
  """
164
172
  try:
165
- res = await super().call(path, method, param)
173
+ res = await super().call(path, method, *args, **kwargs)
166
174
  except RpcMethodNotFoundError as exc:
167
175
  raise exc
168
176
  except RpcError as exc:
169
- self.tree.valid_path(path).methods.add(method)
177
+ self.tree.valid_path(path).methods.setdefault(method, set())
170
178
  raise exc
171
- self.tree.valid_path(path).methods.add(method)
179
+ self.tree.valid_path(path).methods.setdefault(method, set())
172
180
  return res
173
181
 
174
182
  async def probe(self, path: str) -> Node | None:
@@ -190,7 +198,7 @@ class SHVClient(SimpleClient):
190
198
  async def path_is_valid(self, path: str) -> bool:
191
199
  """Check if given path is valid by using ls command."""
192
200
  path = path.strip("/")
193
- if path == "":
201
+ if not path:
194
202
  return True # top level is always valid
195
203
  if "/" in path:
196
204
  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 ("ls", "dir") and not self.config.raw:
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,9 +59,10 @@ 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 ("ls", "dir")
62
+ or items.method in {"ls", "dir"}
62
63
  or (
63
- items.method[0] == "!"
64
+ items.method
65
+ and items.method[0] == "!"
64
66
  and (bmethod := builtin.get_builtin(items.method[1:])) is not None
65
67
  and bmethod.argument
66
68
  and bmethod.argument.autoprobe
@@ -1,4 +1,5 @@
1
1
  """Tool for completion algorithms."""
2
+
2
3
  import collections.abc
3
4
  import pathlib
4
5
 
@@ -1,4 +1,5 @@
1
1
  """Configuration state of the CLI."""
2
+
2
3
  import collections.abc
3
4
  import configparser
4
5
  import enum
@@ -100,7 +101,7 @@ class CliConfig:
100
101
  self.__rurl = self.__url
101
102
  elif isinstance(self.__url, str):
102
103
  self.__rurl = RpcUrl.parse(
103
- subprocess.run(
104
+ subprocess.run( # noqa S602
104
105
  f"printf '%s' \"{self.__url}\"",
105
106
  shell=True,
106
107
  stdout=subprocess.PIPE,
@@ -128,7 +129,7 @@ class CliConfig:
128
129
  return logging.root.level <= logging.DEBUG
129
130
 
130
131
  @debug.setter
131
- def debug(self, value: bool) -> None:
132
+ def debug(self, value: bool) -> None: # noqa PLR6301
132
133
  logging.root.setLevel(logging.DEBUG if value else logging.WARNING)
133
134
 
134
135
  def shvpath(
@@ -1,4 +1,5 @@
1
1
  """Special handling of ls and dir methods."""
2
+
2
3
  import itertools
3
4
  import pathlib
4
5
  import string
@@ -32,14 +33,17 @@ def ls_node_format(
32
33
  def dir_method_format(method: RpcMethodDesc) -> tuple[str, str]:
33
34
  """Print format for single method info."""
34
35
  methstyle = ""
35
- if RpcMethodFlags.SIGNAL in method.flags:
36
- methstyle = "ansipurple"
36
+ if method.name in {"ls", "dir"}:
37
+ methstyle = "ansibrightblack"
37
38
  elif RpcMethodFlags.SETTER in method.flags:
38
39
  methstyle = "ansiyellow"
39
40
  elif RpcMethodFlags.GETTER in method.flags:
40
- methstyle = "ansigreen"
41
- elif method.name in ("ls", "dir"):
42
- methstyle = "ansibrightblack"
41
+ if method.signals:
42
+ methstyle = "ansimagenta"
43
+ else:
44
+ methstyle = "ansigreen"
45
+ elif method.signals or RpcMethodFlags.NOT_CALLABLE in method.flags:
46
+ methstyle = "ansipurple"
43
47
  name = method.name
44
48
  return (
45
49
  methstyle,
@@ -74,7 +78,7 @@ async def ls_method(shvclient: SHVClient, config: CliConfig, items: CliItems) ->
74
78
  else:
75
79
  print_row(
76
80
  itertools.chain(
77
- iter(n + [("", " ")]), cpon_ftext(Cpon.pack(resp))
81
+ iter([*n, ("", " ")]), cpon_ftext(Cpon.pack(resp))
78
82
  )
79
83
  )
80
84
  continue
@@ -99,7 +103,7 @@ async def dir_method(shvclient: SHVClient, config: CliConfig, items: CliItems) -
99
103
  else:
100
104
  print_row(
101
105
  itertools.chain(
102
- iter(n + [("", " ")]), cpon_ftext(Cpon.pack(resp))
106
+ iter([*n, ("", " ")]), cpon_ftext(Cpon.pack(resp))
103
107
  )
104
108
  )
105
109
  continue
@@ -1,4 +1,5 @@
1
1
  """Implementation of CLI parser."""
2
+
2
3
  import dataclasses
3
4
  import enum
4
5
 
@@ -28,7 +29,7 @@ class CliItems:
28
29
  param_raw: str = ""
29
30
  """Raw parameter passed from CLI."""
30
31
 
31
- flags: CliFlags = CliFlags(0)
32
+ flags: CliFlags = dataclasses.field(default_factory=lambda: CliFlags(0))
32
33
  """Flags signaling the presence of some important dividers."""
33
34
 
34
35
  @property
@@ -38,16 +39,6 @@ class CliItems:
38
39
  return None
39
40
  return Cpon.unpack(self.param_raw)
40
41
 
41
- @property
42
- def param_method(self) -> tuple[str, str | None]:
43
- """Interpret parameter as path and method."""
44
- if ":" in self.param_raw:
45
- path, method = self.param_raw.split(":", maxsplit=1)
46
- return path, method
47
- if "/" in self.param_raw:
48
- return self.param_raw, None
49
- return "", self.param_raw
50
-
51
42
  def interpret_param_path(self, config: CliConfig) -> str:
52
43
  """Interpret parameter as path specification.
53
44
 
@@ -55,16 +46,18 @@ class CliItems:
55
46
  """
56
47
  return config.shvpath([self.path, self.param_raw])
57
48
 
58
- def interpret_param_method(self, config: CliConfig) -> tuple[str, str | None]:
59
- """Interpret parameter as method specification.
49
+ def interpret_param_ri(self, config: CliConfig, expand: bool = True) -> str:
50
+ """Interpret parameter as RI.
60
51
 
61
- Compared to :meth:`param_method` this provides a full combination of
62
- path and method not just parsed :prop:`param_raw`.
52
+ The path in RI is combined with pat in the configuration.
63
53
 
64
- :return: pair of SHV path and method name that is from parameter.
54
+ :return: SHV RPC RI.
65
55
  """
66
- param_path, method = self.param_method
67
- return config.shvpath([self.path, param_path]), method
56
+ parts = self.param_raw.split(":")
57
+ path = parts[0] if parts[0] else "**"
58
+ method = parts[1] if len(parts) == 2 else "*"
59
+ signal = parts[2] if len(parts) == 3 else "*"
60
+ return f"{self.path}{"/" if path else ""}{path}:{method}:{signal}"
68
61
 
69
62
  def interpret_param_set(self) -> tuple[str | None, str | None]:
70
63
  """Interpret parameter as 'set' method specification.
@@ -84,9 +77,8 @@ def parse_line(line: str) -> CliItems:
84
77
  if ":" in line:
85
78
  res.flags |= CliFlags.HAS_COLON
86
79
  res.path, res.method = line.split(":", maxsplit=1)
80
+ elif "/" in line:
81
+ res.path = line
87
82
  else:
88
- if "/" in line:
89
- res.path = line
90
- else:
91
- res.method = line
83
+ res.method = line
92
84
  return res
@@ -3,6 +3,7 @@
3
3
  Implementated sepratelly so we can use it not only from builtin methods but also
4
4
  directly when configured.
5
5
  """
6
+
6
7
  from prompt_toolkit.shortcuts import ProgressBar, ProgressBarCounter
7
8
 
8
9
  from .client import SHVClient
@@ -1,4 +1,5 @@
1
1
  """Various tools used in the code."""
2
+
2
3
  import collections.abc
3
4
  import os
4
5
  import textwrap
@@ -39,7 +40,7 @@ def intersperse(what: T, iterin: typing.Iterable[T]) -> typing.Iterator[T]:
39
40
 
40
41
  def print_ftext(
41
42
  fstrs: collections.abc.Iterable[tuple[str, str]]
42
- | collections.abc.Iterator[tuple[str, str]]
43
+ | collections.abc.Iterator[tuple[str, str]],
43
44
  ) -> None:
44
45
  """Call :meth:`print_formatted_text` with :class:`FormattedText`."""
45
46
  print_formatted_text(FormattedText(fstrs))
@@ -47,7 +48,7 @@ def print_ftext(
47
48
 
48
49
  def print_flist(
49
50
  fstrs: collections.abc.Iterable[tuple[str, str]]
50
- | collections.abc.Iterator[tuple[str, str]]
51
+ | collections.abc.Iterator[tuple[str, str]],
51
52
  ) -> None:
52
53
  """Print with :meth:`print_ftext` but interperse spaces."""
53
54
  cols = os.get_terminal_size().columns
@@ -70,7 +71,7 @@ def print_row(
70
71
  ftext: collections.abc.Iterable[tuple[str, str]]
71
72
  | collections.abc.Iterator[tuple[str, str]]
72
73
  | tuple[str, str]
73
- | str
74
+ | str,
74
75
  ) -> None:
75
76
  """Print one line with hyphens when needed."""
76
77
  if isinstance(ftext, str):
@@ -1,4 +1,5 @@
1
1
  """Validator for CLI."""
2
+
2
3
  from prompt_toolkit.document import Document
3
4
  from prompt_toolkit.validation import ValidationError, Validator
4
5
 
@@ -22,7 +23,7 @@ class CliValidator(Validator):
22
23
 
23
24
  # Parameters
24
25
  if CliFlags.COMPLETE_CALL in items.flags:
25
- if items.method in ("ls", "dir") and not self.config.raw:
26
+ if items.method in {"ls", "dir"} and not self.config.raw:
26
27
  return
27
28
  if builtin.get_builtin(items.method[1:]):
28
29
  # TODO we can add validation to the builtins as well
@@ -30,7 +31,7 @@ class CliValidator(Validator):
30
31
  # Any other command should have CPON as argument and thus validate
31
32
  # it as such.
32
33
  try:
33
- items.param
34
+ _ = items.param
34
35
  except (ValueError, EOFError) as exc:
35
36
  raise ValidationError(message=f"Invalid CPON: {exc}") from exc
36
37
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: shvcli
3
- Version: 0.3.0
3
+ Version: 0.5.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/elektroline-predator/shvcli
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.5.0
41
+ Requires-Dist: pyshv>=0.7.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/elektroline-predator/shvcli/-/raw/master/logo.svg
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/elektroline-predator/shvcli>`__
61
- * `⁉️ Issue tracker <https://gitlab.com/elektroline-predator/shvcli/-/issues>`__
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
 
@@ -3,6 +3,7 @@ README.rst
3
3
  pyproject.toml
4
4
  shvcli/__init__.py
5
5
  shvcli/__main__.py
6
+ shvcli/__version__.py
6
7
  shvcli/builtin.py
7
8
  shvcli/builtin_impl.py
8
9
  shvcli/cli.py
@@ -0,0 +1,4 @@
1
+ pyshv>=0.7.0
2
+ prompt_toolkit
3
+ pygments
4
+ pyxdg
@@ -1 +1,2 @@
1
+ dist
1
2
  shvcli
@@ -1,89 +0,0 @@
1
- [project]
2
- name = "shvcli"
3
- description = "Silicon Heaven CLI access application"
4
- readme = "README.rst"
5
- license = {file = "LICENSE"}
6
- dynamic = ["version"]
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.5.0",
24
- "prompt_toolkit",
25
- "pyxdg",
26
- ]
27
-
28
- [project.urls]
29
- repository = "https://gitlab.com/elektroline-predator/shvcli"
30
-
31
- [project.optional-dependencies]
32
- lint = [
33
- "black",
34
- "isort",
35
- "mypy",
36
- "pydocstyle",
37
- "pylint",
38
- ]
39
-
40
- [project.scripts]
41
- shvcli = "shvcli.__main__:main"
42
-
43
- [build-system]
44
- requires = ["setuptools"]
45
- build-backend = "setuptools.build_meta"
46
-
47
- [tool.setuptools.dynamic]
48
- version = {file = "shvcli/version"}
49
- [tool.setuptools.package-data]
50
- "*" = ["version", "py.typed"]
51
-
52
- [tool.coverage.run]
53
- branch = true
54
- [tool.coverage.report]
55
- exclude_lines = [
56
- "pragma: no cover",
57
- "if typing.TYPE_CHECKING:"
58
- ]
59
-
60
- [tool.isort]
61
- profile = 'black'
62
-
63
- [tool.pylint.main]
64
- recursive = "y"
65
- fail-under = 9.5
66
- [tool.pylint.format]
67
- disable = [
68
- # Exception can be safely caught because there is BaseException for unsafe
69
- # ones (such as SystemExit) and there are reasons to catch all exceptions.
70
- "W0718",
71
- # Prevent usage of module name in documentation (seems unreasonable)
72
- "C0104",
73
- # We have pydocstyle for the following ones
74
- "C0103", "C0114", "C0115", "C0116",
75
- ]
76
- # https://black.readthedocs.io/en/stable/guides/using_black_with_other_tools.html
77
- max-line-length = 88
78
-
79
- [tool.mypy]
80
- python_version = "3.11"
81
- ignore_missing_imports = true
82
- disallow_untyped_defs = true
83
- warn_redundant_casts = true
84
- warn_unused_ignores = true
85
- warn_return_any = true
86
- warn_unreachable = true
87
-
88
- [tool.pydocstyle]
89
- convention = "pep257"
@@ -1,5 +0,0 @@
1
- """Silicon Heaven Command Line Interface."""
2
-
3
- VERSION = (
4
- (__import__("pathlib").Path(__file__).parent / "version").read_text("utf-8").strip()
5
- )
@@ -1,10 +0,0 @@
1
- pyshv>=0.5.0
2
- prompt_toolkit
3
- pyxdg
4
-
5
- [lint]
6
- black
7
- isort
8
- mypy
9
- pydocstyle
10
- pylint
File without changes
File without changes
File without changes
File without changes