shvcli 0.5.0__tar.gz → 0.6.1__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 (28) hide show
  1. {shvcli-0.5.0/shvcli.egg-info → shvcli-0.6.1}/PKG-INFO +8 -2
  2. {shvcli-0.5.0 → shvcli-0.6.1}/README.rst +6 -0
  3. {shvcli-0.5.0 → shvcli-0.6.1}/pyproject.toml +2 -2
  4. {shvcli-0.5.0 → shvcli-0.6.1}/shvcli/__main__.py +7 -1
  5. {shvcli-0.5.0 → shvcli-0.6.1}/shvcli/builtin_impl.py +15 -6
  6. {shvcli-0.5.0 → shvcli-0.6.1}/shvcli/cli.py +10 -2
  7. {shvcli-0.5.0 → shvcli-0.6.1}/shvcli/client.py +2 -2
  8. {shvcli-0.5.0 → shvcli-0.6.1}/shvcli/config.py +25 -5
  9. {shvcli-0.5.0 → shvcli-0.6.1}/shvcli/lsdir.py +31 -5
  10. {shvcli-0.5.0 → shvcli-0.6.1}/shvcli/parse.py +9 -6
  11. {shvcli-0.5.0 → shvcli-0.6.1}/shvcli/tools.py +27 -11
  12. {shvcli-0.5.0 → shvcli-0.6.1/shvcli.egg-info}/PKG-INFO +8 -2
  13. {shvcli-0.5.0 → shvcli-0.6.1}/shvcli.egg-info/requires.txt +1 -1
  14. {shvcli-0.5.0 → shvcli-0.6.1}/LICENSE +0 -0
  15. {shvcli-0.5.0 → shvcli-0.6.1}/setup.cfg +0 -0
  16. {shvcli-0.5.0 → shvcli-0.6.1}/shvcli/__init__.py +0 -0
  17. {shvcli-0.5.0 → shvcli-0.6.1}/shvcli/__version__.py +0 -0
  18. {shvcli-0.5.0 → shvcli-0.6.1}/shvcli/builtin.py +0 -0
  19. {shvcli-0.5.0 → shvcli-0.6.1}/shvcli/complet.py +0 -0
  20. {shvcli-0.5.0 → shvcli-0.6.1}/shvcli/complet_tools.py +0 -0
  21. {shvcli-0.5.0 → shvcli-0.6.1}/shvcli/py.typed +0 -0
  22. {shvcli-0.5.0 → shvcli-0.6.1}/shvcli/scan.py +0 -0
  23. {shvcli-0.5.0 → shvcli-0.6.1}/shvcli/valid.py +0 -0
  24. {shvcli-0.5.0 → shvcli-0.6.1}/shvcli/version +0 -0
  25. {shvcli-0.5.0 → shvcli-0.6.1}/shvcli.egg-info/SOURCES.txt +0 -0
  26. {shvcli-0.5.0 → shvcli-0.6.1}/shvcli.egg-info/dependency_links.txt +0 -0
  27. {shvcli-0.5.0 → shvcli-0.6.1}/shvcli.egg-info/entry_points.txt +0 -0
  28. {shvcli-0.5.0 → shvcli-0.6.1}/shvcli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: shvcli
3
- Version: 0.5.0
3
+ Version: 0.6.1
4
4
  Summary: Silicon Heaven CLI access application
5
5
  Author: Elektroline a.s.
6
6
  License: MIT License
@@ -38,7 +38,7 @@ 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.7.0
41
+ Requires-Dist: pyshv>=0.7.1
42
42
  Requires-Dist: prompt_toolkit
43
43
  Requires-Dist: pygments
44
44
  Requires-Dist: pyxdg
@@ -140,6 +140,12 @@ switched in runtime. The following options are available:
140
140
  * **cache**: Controls if cache is preserved between invocations. Cache is
141
141
  separated based on the connection URL and stored in files in
142
142
  ``$XDG_CACHE_HOME/shvcli``. The default is ``true``.
143
+ * **call_attempts**: Number of attempts before method call is abandoned and
144
+ timeout is reported. This makes the total time multiple of **call_timeout**.
145
+ * **call_timeout**: Timeout in seconds before call attempt is abandoned. Based
146
+ on the **call_attempts** call is attempted again or timeout is reported.
147
+ * **autoget_timeout**: Timeout in seconds before call that is part of autoget
148
+ functionality.
143
149
 
144
150
 
145
151
  Example configuration file:
@@ -95,6 +95,12 @@ switched in runtime. The following options are available:
95
95
  * **cache**: Controls if cache is preserved between invocations. Cache is
96
96
  separated based on the connection URL and stored in files in
97
97
  ``$XDG_CACHE_HOME/shvcli``. The default is ``true``.
98
+ * **call_attempts**: Number of attempts before method call is abandoned and
99
+ timeout is reported. This makes the total time multiple of **call_timeout**.
100
+ * **call_timeout**: Timeout in seconds before call attempt is abandoned. Based
101
+ on the **call_attempts** call is attempted again or timeout is reported.
102
+ * **autoget_timeout**: Timeout in seconds before call that is part of autoget
103
+ functionality.
98
104
 
99
105
 
100
106
  Example configuration file:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "shvcli"
3
- version = "0.5.0"
3
+ version = "0.6.1"
4
4
  description = "Silicon Heaven CLI access application"
5
5
  readme = "README.rst"
6
6
  license = {file = "LICENSE"}
@@ -20,7 +20,7 @@ classifiers = [
20
20
  ]
21
21
  requires-python = ">=3.11"
22
22
  dependencies = [
23
- "pyshv >= 0.7.0",
23
+ "pyshv >= 0.7.1",
24
24
  "prompt_toolkit",
25
25
  "pygments",
26
26
  "pyxdg",
@@ -21,6 +21,12 @@ def parse_args() -> argparse.Namespace:
21
21
  )
22
22
  parser.add_argument(
23
23
  "-s",
24
+ "--subscribe",
25
+ action="append",
26
+ default=[],
27
+ help="Automatic subscription right after connection.",
28
+ )
29
+ parser.add_argument(
24
30
  "--scan",
25
31
  action="store_true",
26
32
  help="Perform scan as a first action right after connect.",
@@ -49,7 +55,7 @@ def main() -> None:
49
55
  config.debug = config.debug or args.debug
50
56
  config.initial_scan = args.scan
51
57
  config.initial_scan_depth = args.scan_depth
52
- asyncio.run(run(config))
58
+ asyncio.run(run(config, args.subscribe))
53
59
 
54
60
 
55
61
  if __name__ == "__main__":
@@ -13,7 +13,7 @@ from .config import CliConfig
13
13
  from .lsdir import ls_node_format
14
14
  from .parse import CliItems
15
15
  from .scan import scan_nodes
16
- from .tools import lookahead, print_block, print_ftext, print_row
16
+ from .tools import lookahead, print_block, print_flist, print_ftext, print_row
17
17
 
18
18
 
19
19
  def argument_signal_comp(
@@ -21,10 +21,10 @@ 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, signal = items.interpret_param_ri(config).split(":")
24
+ path, method, signal = items.interpret_param_ri(config)[-1].split(":")
25
25
  node = shvclient.tree.get_path(path)
26
26
  if node is not None:
27
- if items.param_raw.count(":") == 1:
27
+ if items.param_raw.rsplit(maxsplit=1)[-1].count(":") == 1:
28
28
  yield from comp_from(method, node.methods)
29
29
  elif method in node.methods:
30
30
  yield from comp_from(signal, node.methods[method])
@@ -71,19 +71,28 @@ def _help(_: SHVClient, __: CliConfig, ___: CliItems) -> None:
71
71
  @builtin(aliases={"sub"}, argument=argument_signal)
72
72
  async def subscribe(shvclient: SHVClient, config: CliConfig, items: CliItems) -> None:
73
73
  """Add new subscribe."""
74
- await shvclient.subscribe(items.interpret_param_ri(config))
74
+ for ri in items.interpret_param_ri(config):
75
+ await shvclient.subscribe(ri)
75
76
 
76
77
 
77
78
  @builtin(aliases={"usub"}, argument=argument_signal)
78
79
  async def unsubscribe(shvclient: SHVClient, config: CliConfig, items: CliItems) -> None:
79
80
  """Unsubscribe existing subscription."""
80
- await shvclient.unsubscribe(items.interpret_param_ri(config))
81
+ for ri in items.interpret_param_ri(config):
82
+ await shvclient.unsubscribe(ri)
81
83
 
82
84
 
83
85
  @builtin(aliases={"subs", "test"})
84
86
  async def subscriptions(shvclient: SHVClient, _: CliConfig, __: CliItems) -> None:
85
87
  """List current subscriptions."""
86
- print(await shvclient.call(".app/broker/currentClient", "subscriptions"))
88
+ subs = await shvclient.call(".broker/currentClient", "subscriptions")
89
+ if isinstance(subs, dict):
90
+ print_flist(
91
+ ("", sub + (f"?{timeout}secs" if timeout is not None else ""))
92
+ for sub, timeout in subs.items()
93
+ )
94
+ else:
95
+ print(subs)
87
96
 
88
97
 
89
98
  @builtin(aliases={"t"}, argument=argument_path)
@@ -59,10 +59,12 @@ async def _app(config: CliConfig, shvclient: SHVClient) -> None:
59
59
  continue
60
60
 
61
61
 
62
- async def run(config: CliConfig) -> None:
62
+ async def run(config: CliConfig, subscriptions: list[str]) -> None:
63
63
  """Loop to run interactive CLI session."""
64
64
  shvclient = await SHVClient.connect(config.url)
65
65
  assert isinstance(shvclient, SHVClient)
66
+ for ri in subscriptions:
67
+ await shvclient.subscribe(ri)
66
68
 
67
69
  if config.cache:
68
70
  cacheurl = RpcUrl(
@@ -115,9 +117,15 @@ async def handle_line(shvclient: SHVClient, config: CliConfig, cmdline: str) ->
115
117
  else:
116
118
  print_cpon(
117
119
  await shvclient.call(
118
- config.shvpath(items.path), items.method, param
120
+ config.shvpath(items.path),
121
+ items.method,
122
+ param,
123
+ call_attempts=config.call_attempts,
124
+ call_timeout=config.call_timeout,
119
125
  )
120
126
  )
127
+ except TimeoutError:
128
+ print("Call timed out.")
121
129
  except RpcError as exc:
122
130
  print(f"{type(exc).__name__}: {exc.message}")
123
131
  else:
@@ -15,9 +15,9 @@ from shv import (
15
15
  SHVType,
16
16
  SimpleClient,
17
17
  )
18
- from shv.cpon import Cpon
19
18
 
20
19
  from . import VERSION
20
+ from .tools import print_cpon
21
21
 
22
22
 
23
23
  class Node(collections.abc.Mapping[str, "Node"]):
@@ -123,7 +123,7 @@ class SHVClient(SimpleClient):
123
123
  self.tree.valid_path(msg.path or "").methods.setdefault(
124
124
  msg.source, set()
125
125
  ).add(msg.signal_name)
126
- print(f"{msg.path}:{msg.source}:{msg.signal_name}: {Cpon.pack(msg.param)}")
126
+ print_cpon(msg.param, f"{msg.path}:{msg.source}:{msg.signal_name}: ", True)
127
127
 
128
128
  async def ls(self, path: str) -> list[str]:
129
129
  """List same as in ValueClient but with result being preserved in tree."""
@@ -4,6 +4,7 @@ import collections.abc
4
4
  import configparser
5
5
  import enum
6
6
  import functools
7
+ import itertools
7
8
  import logging
8
9
  import pathlib
9
10
  import subprocess
@@ -20,6 +21,7 @@ class CliConfig:
20
21
 
21
22
  BOOL = enum.auto()
22
23
  INT = enum.auto()
24
+ FLOAT = enum.auto()
23
25
 
24
26
  OPTS: collections.abc.Mapping[str, Type] = {
25
27
  "vimode": Type.BOOL,
@@ -27,12 +29,21 @@ class CliConfig:
27
29
  "autoprobe": Type.BOOL,
28
30
  "raw": Type.BOOL,
29
31
  "debug": Type.BOOL,
32
+ "call_attempts": Type.INT,
33
+ "call_timeout": Type.FLOAT,
34
+ "autoget_timeout": Type.FLOAT,
30
35
  }
31
- """All options allowed to be set in runtime.
36
+ """Options allowed to be set in runtime and from configuration file.
37
+
32
38
  You can use :func:`setattr` and :func:`getattr`.
33
39
  """
34
40
 
35
- def __init__(self) -> None:
41
+ COPTS: collections.abc.Mapping[str, Type] = {
42
+ "cache": Type.BOOL,
43
+ }
44
+ """Options allowed to be set only from configuration file."""
45
+
46
+ def __init__(self) -> None: # noqa: PLR0915
36
47
  """Initialize the configuration to the default and load config files."""
37
48
  self.hosts: dict[str, RpcUrl] = {}
38
49
  """Hosts that can be used instead of URL."""
@@ -53,6 +64,13 @@ class CliConfig:
53
64
  """Interpret ls and dir method calls internally or not."""
54
65
  self.cache: bool = True
55
66
  """Preserve cache between executions. Not modifiable in runtime!"""
67
+ self.call_attempts: int = 1
68
+ """Number of call attempts before call is abandoned."""
69
+ self.call_timeout: float = 5.0
70
+ """Timeout in seconds before call is abandoned."""
71
+ self.autoget_timeout: float = 1.0
72
+ """Timeout in seconds for call that is part of autoget feature."""
73
+
56
74
  self.initial_scan: bool = False
57
75
  """Perform scan right after connection."""
58
76
  self.initial_scan_depth: int = 3
@@ -74,17 +92,19 @@ class CliConfig:
74
92
  if self.hosts_shell and self.__url is None:
75
93
  self.__url = self.hosts_shell[next(iter(self.hosts_shell))]
76
94
  case "config":
77
- for n, t in self.OPTS.items():
95
+ for n, t in itertools.chain(self.OPTS.items(), self.COPTS.items()):
78
96
  value = getattr(self, n)
79
97
  match t:
80
98
  case self.Type.BOOL:
81
99
  value = sec.getboolean(n, fallback=value)
82
100
  case self.Type.INT:
83
- value = sec.getinteger(n, fallback=value)
101
+ value = sec.getint(n, fallback=value)
102
+ case self.Type.FLOAT:
103
+ value = sec.getfloat(n, fallback=value)
84
104
  case _:
85
105
  raise NotImplementedError(f"Unhandled type: {t!r}")
86
106
  setattr(self, n, value)
87
- if opts := set(sec.keys()) - set(self.OPTS.keys()):
107
+ if opts := set(sec.keys()) - set(self.OPTS) - set(self.COPTS):
88
108
  raise ValueError(
89
109
  f"Invalid configuration option: {', '.join(opts)}"
90
110
  )
@@ -42,7 +42,7 @@ def dir_method_format(method: RpcMethodDesc) -> tuple[str, str]:
42
42
  methstyle = "ansimagenta"
43
43
  else:
44
44
  methstyle = "ansigreen"
45
- elif method.signals or RpcMethodFlags.NOT_CALLABLE in method.flags:
45
+ elif method.signals:
46
46
  methstyle = "ansipurple"
47
47
  name = method.name
48
48
  return (
@@ -51,6 +51,20 @@ def dir_method_format(method: RpcMethodDesc) -> tuple[str, str]:
51
51
  )
52
52
 
53
53
 
54
+ def dir_signal_format(method: RpcMethodDesc, signal: str) -> tuple[str, str]:
55
+ """Print format for signals."""
56
+ sigstyle = "ansipurple"
57
+ if method.name in {"ls", "dir"}:
58
+ sigstyle = "ansibrightblack"
59
+ elif RpcMethodFlags.GETTER in method.flags:
60
+ sigstyle = "ansimagenta"
61
+ label = f"{method.name}:{signal}"
62
+ return (
63
+ sigstyle,
64
+ f'"{label}"' if any(c in label for c in string.whitespace) else label,
65
+ )
66
+
67
+
54
68
  async def ls_method(shvclient: SHVClient, config: CliConfig, items: CliItems) -> None:
55
69
  """SHV ls method that is just smarter than regular call."""
56
70
  shvpath = items.interpret_param_path(config)
@@ -71,9 +85,12 @@ async def ls_method(shvclient: SHVClient, config: CliConfig, items: CliItems) ->
71
85
  if "get" in nv.methods:
72
86
  try:
73
87
  resp = await shvclient.call(
74
- str(pathlib.PurePosixPath(shvpath) / nn), "get"
88
+ str(pathlib.PurePosixPath(shvpath) / nn),
89
+ "get",
90
+ call_attempts=1,
91
+ call_timeout=config.autoget_timeout,
75
92
  )
76
- except RpcError:
93
+ except (RpcError, TimeoutError):
77
94
  pass
78
95
  else:
79
96
  print_row(
@@ -94,11 +111,18 @@ async def dir_method(shvclient: SHVClient, config: CliConfig, items: CliItems) -
94
111
  if config.autoget and any(_use_autoget(d) for d in dirr):
95
112
  w = max(len(d.name) for d in dirr)
96
113
  for d in dirr:
114
+ if RpcMethodFlags.NOT_CALLABLE in d.flags:
115
+ continue # Ignore not callable
97
116
  n = [("", " " * (w - len(d.name))), dir_method_format(d)]
98
117
  if _use_autoget(d):
99
118
  try:
100
- resp = await shvclient.call(shvpath, d.name)
101
- except RpcError:
119
+ resp = await shvclient.call(
120
+ shvpath,
121
+ d.name,
122
+ call_attempts=1,
123
+ call_timeout=config.autoget_timeout,
124
+ )
125
+ except (RpcError, TimeoutError):
102
126
  pass
103
127
  else:
104
128
  print_row(
@@ -110,6 +134,8 @@ async def dir_method(shvclient: SHVClient, config: CliConfig, items: CliItems) -
110
134
  print_row(n)
111
135
  else:
112
136
  print_flist(dir_method_format(d) for d in dirr)
137
+ if any(d.signals for d in dirr):
138
+ print_flist(dir_signal_format(d, s) for d in dirr for s in d.signals)
113
139
 
114
140
 
115
141
  def _use_autoget(method: RpcMethodDesc) -> bool:
@@ -46,18 +46,21 @@ class CliItems:
46
46
  """
47
47
  return config.shvpath([self.path, self.param_raw])
48
48
 
49
- def interpret_param_ri(self, config: CliConfig, expand: bool = True) -> str:
49
+ def interpret_param_ri(self, config: CliConfig, expand: bool = True) -> list[str]:
50
50
  """Interpret parameter as RI.
51
51
 
52
52
  The path in RI is combined with pat in the configuration.
53
53
 
54
54
  :return: SHV RPC RI.
55
55
  """
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}"
56
+ res = []
57
+ for item in self.param_raw.split():
58
+ parts = item.split(":")
59
+ path = parts[0] if parts[0] else "**"
60
+ method = parts[1] if len(parts) == 2 else "*"
61
+ signal = parts[2] if len(parts) == 3 else "*"
62
+ res.append(f"{self.path}{'/' if self.path else ''}{path}:{method}:{signal}")
63
+ return res
61
64
 
62
65
  def interpret_param_set(self) -> tuple[str | None, str | None]:
63
66
  """Interpret parameter as 'set' method specification.
@@ -1,6 +1,7 @@
1
1
  """Various tools used in the code."""
2
2
 
3
3
  import collections.abc
4
+ import itertools
4
5
  import os
5
6
  import textwrap
6
7
  import typing
@@ -118,16 +119,31 @@ def cpon_ftext(cpon: str) -> collections.abc.Iterator[tuple[str, str]]:
118
119
  yield "", "\n"
119
120
 
120
121
 
121
- def print_cpon(data: shv.SHVType, short: bool = False) -> None:
122
+ def _wrap_cpon(cpon: str) -> collections.abc.Iterator[str]:
123
+ """Wrap CPON.
124
+
125
+ It is better to wrap CPON on division characters rather than white spaces
126
+ because those are part of the data.
127
+ """
128
+ cols = os.get_terminal_size().columns
129
+ while len(cpon) > cols:
130
+ i = max(cpon.rfind(sep, 0, cols) for sep in (",", "]", "}"))
131
+ if i == -1:
132
+ i = cols
133
+ yield cpon[: i + 1]
134
+ cpon = " " + cpon[i + 1 :]
135
+ yield cpon
136
+
137
+
138
+ def print_cpon(data: shv.SHVType, prefix: str = "", short: bool = False) -> None:
122
139
  """Print given data in CPON format."""
123
- simple = shv.Cpon.pack(data)
124
- if len(simple) > os.get_terminal_size().columns:
125
- if short:
126
- print_row(cpon_ftext(simple))
127
- else:
128
- for line in shv.Cpon.pack(
129
- data, shv.CponWriter.Options(indent=b" ")
130
- ).splitlines():
131
- print_row(cpon_ftext(line))
140
+ if short:
141
+ cpon = "\n".join(_wrap_cpon(prefix + shv.Cpon.pack(data)))[len(prefix) :]
142
+ print_ftext(itertools.chain(iter((("", prefix),)), cpon_ftext(cpon)))
132
143
  else:
133
- print_ftext(cpon_ftext(simple))
144
+ print_ftext(
145
+ itertools.chain(
146
+ iter((("", prefix),)),
147
+ cpon_ftext(shv.Cpon.pack(data, shv.CponWriter.Options(indent=b" "))),
148
+ )
149
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: shvcli
3
- Version: 0.5.0
3
+ Version: 0.6.1
4
4
  Summary: Silicon Heaven CLI access application
5
5
  Author: Elektroline a.s.
6
6
  License: MIT License
@@ -38,7 +38,7 @@ 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.7.0
41
+ Requires-Dist: pyshv>=0.7.1
42
42
  Requires-Dist: prompt_toolkit
43
43
  Requires-Dist: pygments
44
44
  Requires-Dist: pyxdg
@@ -140,6 +140,12 @@ switched in runtime. The following options are available:
140
140
  * **cache**: Controls if cache is preserved between invocations. Cache is
141
141
  separated based on the connection URL and stored in files in
142
142
  ``$XDG_CACHE_HOME/shvcli``. The default is ``true``.
143
+ * **call_attempts**: Number of attempts before method call is abandoned and
144
+ timeout is reported. This makes the total time multiple of **call_timeout**.
145
+ * **call_timeout**: Timeout in seconds before call attempt is abandoned. Based
146
+ on the **call_attempts** call is attempted again or timeout is reported.
147
+ * **autoget_timeout**: Timeout in seconds before call that is part of autoget
148
+ functionality.
143
149
 
144
150
 
145
151
  Example configuration file:
@@ -1,4 +1,4 @@
1
- pyshv>=0.7.0
1
+ pyshv>=0.7.1
2
2
  prompt_toolkit
3
3
  pygments
4
4
  pyxdg
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes