shvcli 0.7.0__tar.gz → 0.8.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.7.0/shvcli.egg-info → shvcli-0.8.0}/PKG-INFO +19 -3
- {shvcli-0.7.0 → shvcli-0.8.0}/README.rst +17 -0
- {shvcli-0.7.0 → shvcli-0.8.0}/pyproject.toml +4 -3
- {shvcli-0.7.0 → shvcli-0.8.0}/shvcli/__main__.py +1 -2
- shvcli-0.8.0/shvcli/__setuptools__.py +18 -0
- shvcli-0.8.0/shvcli/__version__.py +24 -0
- {shvcli-0.7.0 → shvcli-0.8.0}/shvcli/builtins/__init__.py +2 -0
- shvcli-0.8.0/shvcli/builtins/history.py +146 -0
- {shvcli-0.7.0 → shvcli-0.8.0}/shvcli/cli.py +1 -1
- {shvcli-0.7.0 → shvcli-0.8.0}/shvcli/client.py +5 -9
- {shvcli-0.7.0 → shvcli-0.8.0}/shvcli/cliitems.py +5 -2
- {shvcli-0.7.0 → shvcli-0.8.0}/shvcli/file.py +12 -9
- {shvcli-0.7.0 → shvcli-0.8.0}/shvcli/lsdir.py +2 -1
- shvcli-0.8.0/shvcli/oauth.py +75 -0
- {shvcli-0.7.0 → shvcli-0.8.0}/shvcli/options.py +1 -1
- {shvcli-0.7.0 → shvcli-0.8.0}/shvcli/state.py +5 -3
- {shvcli-0.7.0 → shvcli-0.8.0}/shvcli/tools/complet.py +3 -3
- {shvcli-0.7.0 → shvcli-0.8.0}/shvcli/tools/print.py +8 -3
- {shvcli-0.7.0 → shvcli-0.8.0}/shvcli/tree.py +15 -11
- {shvcli-0.7.0 → shvcli-0.8.0}/shvcli/url.py +80 -6
- {shvcli-0.7.0 → shvcli-0.8.0/shvcli.egg-info}/PKG-INFO +19 -3
- {shvcli-0.7.0 → shvcli-0.8.0}/shvcli.egg-info/SOURCES.txt +3 -0
- {shvcli-0.7.0 → shvcli-0.8.0}/shvcli.egg-info/requires.txt +1 -1
- shvcli-0.7.0/shvcli/__version__.py +0 -26
- {shvcli-0.7.0 → shvcli-0.8.0}/LICENSE +0 -0
- {shvcli-0.7.0 → shvcli-0.8.0}/setup.cfg +0 -0
- {shvcli-0.7.0 → shvcli-0.8.0}/shvcli/__init__.py +0 -0
- {shvcli-0.7.0 → shvcli-0.8.0}/shvcli/afterconn.py +0 -0
- {shvcli-0.7.0 → shvcli-0.8.0}/shvcli/args.py +0 -0
- {shvcli-0.7.0 → shvcli-0.8.0}/shvcli/builtin.py +0 -0
- {shvcli-0.7.0 → shvcli-0.8.0}/shvcli/builtins/broker.py +0 -0
- {shvcli-0.7.0 → shvcli-0.8.0}/shvcli/builtins/cd.py +0 -0
- {shvcli-0.7.0 → shvcli-0.8.0}/shvcli/builtins/file.py +0 -0
- {shvcli-0.7.0 → shvcli-0.8.0}/shvcli/builtins/help.py +0 -0
- {shvcli-0.7.0 → shvcli-0.8.0}/shvcli/builtins/scan.py +0 -0
- {shvcli-0.7.0 → shvcli-0.8.0}/shvcli/builtins/set.py +0 -0
- {shvcli-0.7.0 → shvcli-0.8.0}/shvcli/builtins/tree.py +0 -0
- {shvcli-0.7.0 → shvcli-0.8.0}/shvcli/complet.py +0 -0
- {shvcli-0.7.0 → shvcli-0.8.0}/shvcli/config.py +0 -0
- {shvcli-0.7.0 → shvcli-0.8.0}/shvcli/option.py +0 -0
- {shvcli-0.7.0 → shvcli-0.8.0}/shvcli/plugins.py +0 -0
- {shvcli-0.7.0 → shvcli-0.8.0}/shvcli/py.typed +0 -0
- {shvcli-0.7.0 → shvcli-0.8.0}/shvcli/scan.py +0 -0
- {shvcli-0.7.0 → shvcli-0.8.0}/shvcli/tools/__init__.py +0 -0
- {shvcli-0.7.0 → shvcli-0.8.0}/shvcli/valid.py +0 -0
- {shvcli-0.7.0 → shvcli-0.8.0}/shvcli.egg-info/dependency_links.txt +0 -0
- {shvcli-0.7.0 → shvcli-0.8.0}/shvcli.egg-info/entry_points.txt +0 -0
- {shvcli-0.7.0 → shvcli-0.8.0}/shvcli.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: shvcli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: Silicon Heaven CLI access application
|
|
5
5
|
Author: Elektroline a.s.
|
|
6
6
|
License: MIT License
|
|
@@ -27,7 +27,6 @@ License: MIT License
|
|
|
27
27
|
|
|
28
28
|
Project-URL: repository, https://gitlab.com/silicon-heaven/shvcli
|
|
29
29
|
Classifier: Development Status :: 4 - Beta
|
|
30
|
-
Classifier: Programming Language :: Python :: 3
|
|
31
30
|
Classifier: Operating System :: OS Independent
|
|
32
31
|
Classifier: Environment :: Console
|
|
33
32
|
Classifier: Intended Audience :: End Users/Desktop
|
|
@@ -37,7 +36,7 @@ Classifier: Topic :: Terminals
|
|
|
37
36
|
Requires-Python: >=3.11
|
|
38
37
|
Description-Content-Type: text/x-rst
|
|
39
38
|
License-File: LICENSE
|
|
40
|
-
Requires-Dist: pyshv>=0.
|
|
39
|
+
Requires-Dist: pyshv>=0.10.0
|
|
41
40
|
Requires-Dist: prompt_toolkit
|
|
42
41
|
Requires-Dist: pygments
|
|
43
42
|
Requires-Dist: pyxdg
|
|
@@ -151,6 +150,8 @@ following options are available without any plugins:
|
|
|
151
150
|
is used only in case when method doesn't support delayed responses.
|
|
152
151
|
* **autoget_timeout**: Timeout in seconds for call that is part of autoget
|
|
153
152
|
functionality.
|
|
153
|
+
* **history_count**: Integer number used for number of records requested at time
|
|
154
|
+
for ``!history`` builtin.
|
|
154
155
|
|
|
155
156
|
Example configuration file:
|
|
156
157
|
|
|
@@ -213,6 +214,10 @@ parameter must be path to the local file where data will be stored.
|
|
|
213
214
|
**verify**: provides a way to verify RPC File node against local file. The
|
|
214
215
|
parameter must be path to the local file used for the verification.
|
|
215
216
|
|
|
217
|
+
**history**: is helper to list recorded history of signals by ``.history`` API.
|
|
218
|
+
The listing is incremental in ``history_count`` configured number of records and
|
|
219
|
+
in the order from the newest ones.
|
|
220
|
+
|
|
216
221
|
|
|
217
222
|
Special methods ``ls`` and ``dir``
|
|
218
223
|
----------------------------------
|
|
@@ -225,6 +230,17 @@ Their parameter is also handled in a special way. It is considered to be
|
|
|
225
230
|
additional path suffix unless it is a valid CPON. This is allowed to match the
|
|
226
231
|
common shells.
|
|
227
232
|
|
|
233
|
+
You can see ``raw`` option to disable this behavior.
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
OAuth2 login
|
|
237
|
+
------------
|
|
238
|
+
|
|
239
|
+
SHVCLI supports non-standard login method OAuth2. This is not defined by SHV
|
|
240
|
+
standard. The non-standard URL query ``oauth2=y`` is accepted to perform the
|
|
241
|
+
OAuth2 login. The login is facilitated by web browser. SHVCLI will start server
|
|
242
|
+
on ``tcp://localhost:37719/`` to be able to receive token.
|
|
243
|
+
|
|
228
244
|
|
|
229
245
|
Plugins
|
|
230
246
|
-------
|
|
@@ -105,6 +105,8 @@ following options are available without any plugins:
|
|
|
105
105
|
is used only in case when method doesn't support delayed responses.
|
|
106
106
|
* **autoget_timeout**: Timeout in seconds for call that is part of autoget
|
|
107
107
|
functionality.
|
|
108
|
+
* **history_count**: Integer number used for number of records requested at time
|
|
109
|
+
for ``!history`` builtin.
|
|
108
110
|
|
|
109
111
|
Example configuration file:
|
|
110
112
|
|
|
@@ -167,6 +169,10 @@ parameter must be path to the local file where data will be stored.
|
|
|
167
169
|
**verify**: provides a way to verify RPC File node against local file. The
|
|
168
170
|
parameter must be path to the local file used for the verification.
|
|
169
171
|
|
|
172
|
+
**history**: is helper to list recorded history of signals by ``.history`` API.
|
|
173
|
+
The listing is incremental in ``history_count`` configured number of records and
|
|
174
|
+
in the order from the newest ones.
|
|
175
|
+
|
|
170
176
|
|
|
171
177
|
Special methods ``ls`` and ``dir``
|
|
172
178
|
----------------------------------
|
|
@@ -179,6 +185,17 @@ Their parameter is also handled in a special way. It is considered to be
|
|
|
179
185
|
additional path suffix unless it is a valid CPON. This is allowed to match the
|
|
180
186
|
common shells.
|
|
181
187
|
|
|
188
|
+
You can see ``raw`` option to disable this behavior.
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
OAuth2 login
|
|
192
|
+
------------
|
|
193
|
+
|
|
194
|
+
SHVCLI supports non-standard login method OAuth2. This is not defined by SHV
|
|
195
|
+
standard. The non-standard URL query ``oauth2=y`` is accepted to perform the
|
|
196
|
+
OAuth2 login. The login is facilitated by web browser. SHVCLI will start server
|
|
197
|
+
on ``tcp://localhost:37719/`` to be able to receive token.
|
|
198
|
+
|
|
182
199
|
|
|
183
200
|
Plugins
|
|
184
201
|
-------
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "shvcli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.8.0"
|
|
4
4
|
description = "Silicon Heaven CLI access application"
|
|
5
5
|
readme = "README.rst"
|
|
6
6
|
license = {file = "LICENSE"}
|
|
@@ -9,7 +9,6 @@ authors = [
|
|
|
9
9
|
]
|
|
10
10
|
classifiers = [
|
|
11
11
|
"Development Status :: 4 - Beta",
|
|
12
|
-
"Programming Language :: Python :: 3",
|
|
13
12
|
"Operating System :: OS Independent",
|
|
14
13
|
"Environment :: Console",
|
|
15
14
|
"Intended Audience :: End Users/Desktop",
|
|
@@ -19,7 +18,7 @@ classifiers = [
|
|
|
19
18
|
]
|
|
20
19
|
requires-python = ">=3.11"
|
|
21
20
|
dependencies = [
|
|
22
|
-
"pyshv >= 0.
|
|
21
|
+
"pyshv >= 0.10.0",
|
|
23
22
|
"prompt_toolkit",
|
|
24
23
|
"pygments",
|
|
25
24
|
"pyxdg",
|
|
@@ -39,6 +38,8 @@ build-backend = "setuptools.build_meta"
|
|
|
39
38
|
[tool.setuptools.packages.find]
|
|
40
39
|
[tool.setuptools.package-data]
|
|
41
40
|
"*" = ["py.typed"]
|
|
41
|
+
[tool.setuptools.cmdclass]
|
|
42
|
+
build_py = "shvcli.__setuptools__.BuildPy"
|
|
42
43
|
|
|
43
44
|
[tool.coverage.run]
|
|
44
45
|
branch = true
|
|
@@ -5,7 +5,6 @@ import asyncio
|
|
|
5
5
|
from .afterconn import Afterconn
|
|
6
6
|
from .args import args_apply, args_parse
|
|
7
7
|
from .cli import run
|
|
8
|
-
from .client import Client
|
|
9
8
|
from .config import load_config
|
|
10
9
|
from .plugins import load_plugins
|
|
11
10
|
from .state import State
|
|
@@ -22,7 +21,7 @@ def main() -> None:
|
|
|
22
21
|
state.cache_load(Url(state).cache_path())
|
|
23
22
|
|
|
24
23
|
async def async_main() -> None:
|
|
25
|
-
client = await
|
|
24
|
+
client = await Url(state).connect()
|
|
26
25
|
await Afterconn(state).run(client)
|
|
27
26
|
await run(client)
|
|
28
27
|
if client.client.connected:
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Setuptools hook used to replace __version__.py file."""
|
|
2
|
+
|
|
3
|
+
import pathlib
|
|
4
|
+
|
|
5
|
+
from setuptools.command.build_py import build_py as _build_py
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BuildPy(_build_py):
|
|
9
|
+
"""Custom package build step."""
|
|
10
|
+
|
|
11
|
+
def run(self) -> None: # noqa: D102
|
|
12
|
+
package = pathlib.Path(__file__).parent.name
|
|
13
|
+
buildlib = pathlib.Path(self.build_lib) / package
|
|
14
|
+
buildlib.mkdir(parents=True, exist_ok=True)
|
|
15
|
+
version = self.distribution.metadata.version
|
|
16
|
+
(buildlib / "__version__.py").write_text(f'VERSION = "{version}"\n')
|
|
17
|
+
super().run()
|
|
18
|
+
(buildlib / "__setuptools__.py").unlink()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""The loader of the version for source file only installation.
|
|
2
|
+
|
|
3
|
+
It looks for pyproject.toml file in upper directory and loads version from
|
|
4
|
+
there.
|
|
5
|
+
|
|
6
|
+
This file is overwritten during distribution build to contain only constant
|
|
7
|
+
VERSION. Please see __setuptools__.py for details.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import pathlib
|
|
11
|
+
import tomllib
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _get_version() -> str:
|
|
15
|
+
pyproject = pathlib.Path(__file__).parent.parent / "pyproject.toml"
|
|
16
|
+
if pyproject.exists():
|
|
17
|
+
with pyproject.open("rb") as f:
|
|
18
|
+
res = tomllib.load(f)["project"]["version"]
|
|
19
|
+
if isinstance(res, str):
|
|
20
|
+
return res
|
|
21
|
+
return "unknown"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
VERSION = _get_version()
|
|
@@ -8,6 +8,7 @@ from .broker import BuiltinSubscribe, BuiltinSubscriptions, BuiltinUnsubscribe
|
|
|
8
8
|
from .cd import BuiltinCD
|
|
9
9
|
from .file import BuiltinDownload, BuiltinUpload, BuiltinVerify
|
|
10
10
|
from .help import BuiltinHelp
|
|
11
|
+
from .history import BuiltinHistory
|
|
11
12
|
from .scan import BuiltinScan
|
|
12
13
|
from .set import BuiltinSet
|
|
13
14
|
from .tree import BuiltinTree
|
|
@@ -16,6 +17,7 @@ __all__ = [
|
|
|
16
17
|
"BuiltinCD",
|
|
17
18
|
"BuiltinDownload",
|
|
18
19
|
"BuiltinHelp",
|
|
20
|
+
"BuiltinHistory",
|
|
19
21
|
"BuiltinScan",
|
|
20
22
|
"BuiltinSet",
|
|
21
23
|
"BuiltinSubscribe",
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""The getLog builtin."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import collections.abc
|
|
6
|
+
import contextlib
|
|
7
|
+
import datetime
|
|
8
|
+
import itertools
|
|
9
|
+
|
|
10
|
+
import shv
|
|
11
|
+
import shv.path
|
|
12
|
+
import shv.rpcdef
|
|
13
|
+
from prompt_toolkit import Application
|
|
14
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
15
|
+
from prompt_toolkit.key_binding.key_processor import KeyPressEvent
|
|
16
|
+
from prompt_toolkit.layout import FormattedTextControl, Layout, Window
|
|
17
|
+
from prompt_toolkit.patch_stdout import patch_stdout
|
|
18
|
+
|
|
19
|
+
from ..builtin import Builtin, Builtins
|
|
20
|
+
from ..client import Client
|
|
21
|
+
from ..cliitems import CliItems
|
|
22
|
+
from ..option import IntOption
|
|
23
|
+
from ..state import State
|
|
24
|
+
from ..tools.print import print_cpon, print_ftext
|
|
25
|
+
|
|
26
|
+
_kb = KeyBindings()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@_kb.add("<any>")
|
|
30
|
+
def _(event: KeyPressEvent) -> None:
|
|
31
|
+
event.app.exit(result=event.key_sequence[0].key in {"c-m", "y"})
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class BuiltinHistory(Builtin):
|
|
35
|
+
"""The implementation of ``history`` builtin that fetches logs."""
|
|
36
|
+
|
|
37
|
+
def __init__(self, builtins: Builtins, state: State) -> None:
|
|
38
|
+
super().__init__(builtins, state)
|
|
39
|
+
builtins["history"] = self
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def description(self) -> tuple[str, str]: # noqa: D102
|
|
43
|
+
return "[SINCE]", "Get the node's history."
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
async def _locate_getlog(
|
|
47
|
+
path: shv.path.SHVPath, client: Client
|
|
48
|
+
) -> shv.path.SHVPath | None:
|
|
49
|
+
for pth in itertools.chain(reversed(path.parents), iter((path,))):
|
|
50
|
+
hpth = pth / ".history" / (path.relative_to(pth))
|
|
51
|
+
with contextlib.suppress(shv.rpcdef.RpcMethodNotFoundError):
|
|
52
|
+
if await client.dir_exists(hpth, "getLog"):
|
|
53
|
+
return hpth
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
async def run(self, items: CliItems, client: Client) -> None: # noqa: D102
|
|
57
|
+
if items.param:
|
|
58
|
+
since = datetime.datetime.fromisoformat(items.param)
|
|
59
|
+
since = since.replace(tzinfo=datetime.UTC)
|
|
60
|
+
else:
|
|
61
|
+
since = datetime.datetime.now()
|
|
62
|
+
until = datetime.datetime.fromtimestamp(0)
|
|
63
|
+
count = HistoryCount(self.state)
|
|
64
|
+
|
|
65
|
+
# First we need to locate the appropriate .history
|
|
66
|
+
history_path = await self._locate_getlog(items.path, client)
|
|
67
|
+
if history_path is None:
|
|
68
|
+
print(f"There are no logs for path: {items.path}")
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
app: Application[bool] = Application(
|
|
72
|
+
Layout(Window(FormattedTextControl("Fetch more logs? (Y/n)"))),
|
|
73
|
+
key_bindings=_kb,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
cont = True
|
|
77
|
+
while cont:
|
|
78
|
+
logs = await client.call(
|
|
79
|
+
history_path, "getLog", {1: since, 2: until, 3: int(count)}
|
|
80
|
+
)
|
|
81
|
+
if not shv.is_shvlist(logs) or not logs:
|
|
82
|
+
break # No more logs
|
|
83
|
+
|
|
84
|
+
refmap: dict[int, str] = {}
|
|
85
|
+
for i, log in enumerate(logs):
|
|
86
|
+
if shv.is_shvimap(log):
|
|
87
|
+
timestamp = log.get(1, None)
|
|
88
|
+
loc = None
|
|
89
|
+
if isinstance(ref := log.get(2), int):
|
|
90
|
+
loc = refmap.pop(i - ref - 1, None)
|
|
91
|
+
if loc is not None:
|
|
92
|
+
refmap[i] = loc
|
|
93
|
+
else:
|
|
94
|
+
spath = log.get(3, "")
|
|
95
|
+
method = log.get(5, "get")
|
|
96
|
+
signal = log.get(4, "chng")
|
|
97
|
+
if all(isinstance(s, str) for s in (spath, method, signal)):
|
|
98
|
+
loc = f"{spath}:{method}:{signal}"
|
|
99
|
+
refmap[i] = loc
|
|
100
|
+
if isinstance(timestamp, datetime.datetime) and loc is not None:
|
|
101
|
+
print_ftext(
|
|
102
|
+
(
|
|
103
|
+
(
|
|
104
|
+
"ansibrightblack",
|
|
105
|
+
timestamp.replace(tzinfo=None).isoformat(
|
|
106
|
+
timespec="milliseconds"
|
|
107
|
+
)
|
|
108
|
+
+ "Z",
|
|
109
|
+
),
|
|
110
|
+
("", " "),
|
|
111
|
+
("ansiblue", loc),
|
|
112
|
+
),
|
|
113
|
+
end=" ",
|
|
114
|
+
)
|
|
115
|
+
print_cpon(log[6], short=True)
|
|
116
|
+
since = timestamp
|
|
117
|
+
cont = True
|
|
118
|
+
continue
|
|
119
|
+
print_cpon(log, short=True)
|
|
120
|
+
cont = False
|
|
121
|
+
|
|
122
|
+
if cont:
|
|
123
|
+
with patch_stdout():
|
|
124
|
+
cont = await app.run_async()
|
|
125
|
+
app.output.cursor_up(1)
|
|
126
|
+
app.output.write("\r")
|
|
127
|
+
app.output.erase_end_of_line()
|
|
128
|
+
app.output.flush()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class HistoryCount(IntOption):
|
|
132
|
+
"""Number of items fetched at one ``!history`` builtin invocation."""
|
|
133
|
+
|
|
134
|
+
def __init__(self, state: State) -> None:
|
|
135
|
+
super().__init__(state)
|
|
136
|
+
self._value = 20
|
|
137
|
+
|
|
138
|
+
@classmethod
|
|
139
|
+
def aliases(cls) -> collections.abc.Sequence[str]: # noqa: D102
|
|
140
|
+
return ("history_count",)
|
|
141
|
+
|
|
142
|
+
def __int__(self) -> int:
|
|
143
|
+
return self._value
|
|
144
|
+
|
|
145
|
+
def rset(self, value: int) -> None: # noqa: D102
|
|
146
|
+
self._value = value
|
|
@@ -14,7 +14,7 @@ from prompt_toolkit.key_binding.key_processor import KeyPressEvent
|
|
|
14
14
|
from prompt_toolkit.patch_stdout import patch_stdout
|
|
15
15
|
from prompt_toolkit.shortcuts import CompleteStyle, ProgressBar, ProgressBarCounter
|
|
16
16
|
from prompt_toolkit.shortcuts.progress_bar import formatters
|
|
17
|
-
from shv import RpcError
|
|
17
|
+
from shv.rpcdef import RpcError
|
|
18
18
|
|
|
19
19
|
from .builtin import Builtins
|
|
20
20
|
from .client import Client
|
|
@@ -4,15 +4,11 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import typing
|
|
6
6
|
|
|
7
|
-
from shv import
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
SHVClient,
|
|
13
|
-
SHVPath,
|
|
14
|
-
SHVType,
|
|
15
|
-
)
|
|
7
|
+
from shv import SHVType
|
|
8
|
+
from shv.path import SHVPath
|
|
9
|
+
from shv.rpcapi.client import SHVClient
|
|
10
|
+
from shv.rpcdef import RpcDir, RpcError, RpcMethodNotFoundError
|
|
11
|
+
from shv.rpcmessage import RpcMessage
|
|
16
12
|
|
|
17
13
|
from . import VERSION
|
|
18
14
|
from .options import CallQueryTimeout, CallRetryTimeout
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
import contextlib
|
|
4
4
|
import string
|
|
5
5
|
|
|
6
|
-
from shv import
|
|
6
|
+
from shv import SHVType
|
|
7
7
|
from shv.cpon import Cpon
|
|
8
|
+
from shv.path import SHVPath
|
|
8
9
|
from shv.rpctypes import RpcType, RpcTypeParseError, rpctype_parse
|
|
9
10
|
|
|
10
11
|
|
|
@@ -29,7 +30,9 @@ class CliItems:
|
|
|
29
30
|
path = "" if ":" not in ri and "/" not in ri else ri.partition(":")[0]
|
|
30
31
|
if any(c in path for c in string.whitespace):
|
|
31
32
|
raise ValueError("Path can't have white character in it.")
|
|
32
|
-
|
|
33
|
+
if path == "/":
|
|
34
|
+
return SHVPath("")
|
|
35
|
+
return self.path_prefix / path.rstrip("/")
|
|
33
36
|
|
|
34
37
|
@property
|
|
35
38
|
def method(self) -> str:
|
|
@@ -7,15 +7,18 @@ import io
|
|
|
7
7
|
import pathlib
|
|
8
8
|
|
|
9
9
|
import shv
|
|
10
|
+
import shv.path
|
|
11
|
+
import shv.rpcdef.file
|
|
10
12
|
from prompt_toolkit.shortcuts import ProgressBar, ProgressBarCounter
|
|
13
|
+
from shv.rpcdef.file import RpcFile
|
|
11
14
|
|
|
12
15
|
from .client import Client
|
|
13
16
|
|
|
14
17
|
|
|
15
18
|
async def copy_file(
|
|
16
19
|
client: Client,
|
|
17
|
-
src: pathlib.Path | shv.SHVPath,
|
|
18
|
-
dest: pathlib.Path | shv.SHVPath,
|
|
20
|
+
src: pathlib.Path | shv.path.SHVPath,
|
|
21
|
+
dest: pathlib.Path | shv.path.SHVPath,
|
|
19
22
|
label: str = "",
|
|
20
23
|
) -> None:
|
|
21
24
|
"""Copy file with support for local and SHV RPC files.
|
|
@@ -23,17 +26,17 @@ async def copy_file(
|
|
|
23
26
|
:raises RpcError: For RPC errors.
|
|
24
27
|
:raises FileNotFoundError: In case remote or local file is not located.
|
|
25
28
|
"""
|
|
26
|
-
srcfile:
|
|
27
|
-
destfile:
|
|
29
|
+
srcfile: RpcFile | io.IOBase | None = None
|
|
30
|
+
destfile: RpcFile | io.IOBase | None = None
|
|
28
31
|
|
|
29
32
|
try:
|
|
30
|
-
if isinstance(src, shv.SHVPath):
|
|
31
|
-
srcfile =
|
|
33
|
+
if isinstance(src, shv.path.SHVPath):
|
|
34
|
+
srcfile = RpcFile(client, str(src), flags=RpcFile.Flag(0))
|
|
32
35
|
if not await srcfile.readable():
|
|
33
36
|
raise FileNotFoundError(f"No such readable SHV RPC file: {src}")
|
|
34
37
|
srcsiz = await srcfile.size()
|
|
35
|
-
if isinstance(dest, shv.SHVPath):
|
|
36
|
-
destfile =
|
|
38
|
+
if isinstance(dest, shv.path.SHVPath):
|
|
39
|
+
destfile = RpcFile(client, str(dest), flags=RpcFile.Flag(0))
|
|
37
40
|
if not await destfile.writable():
|
|
38
41
|
raise FileNotFoundError(f"No such writable SHV RPC file: {dest}")
|
|
39
42
|
|
|
@@ -44,7 +47,7 @@ async def copy_file(
|
|
|
44
47
|
destfile = dest.open("wb")
|
|
45
48
|
|
|
46
49
|
assert srcfile is not None and destfile is not None
|
|
47
|
-
if isinstance(destfile,
|
|
50
|
+
if isinstance(destfile, RpcFile):
|
|
48
51
|
await destfile.truncate(0)
|
|
49
52
|
|
|
50
53
|
with ProgressBar() as pb:
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Utility to receive token from OAuth2 provider."""
|
|
2
|
+
|
|
3
|
+
import http.server
|
|
4
|
+
import json
|
|
5
|
+
import socketserver
|
|
6
|
+
import urllib.parse
|
|
7
|
+
import urllib.request
|
|
8
|
+
import webbrowser
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def oauth_login_token(
|
|
12
|
+
tp: str, client_id: str, authorize_url: str, token_url: str, scopes: list[str]
|
|
13
|
+
) -> str:
|
|
14
|
+
"""Use OAuth2 to get login token.
|
|
15
|
+
|
|
16
|
+
This is blocking call and thus :func:`asyncio.run_in_executor` should be
|
|
17
|
+
used if called from asyncio.
|
|
18
|
+
"""
|
|
19
|
+
redirect_uri = "http://localhost:37719/"
|
|
20
|
+
|
|
21
|
+
auth_code = None
|
|
22
|
+
|
|
23
|
+
class _HTTPHandler(http.server.BaseHTTPRequestHandler):
|
|
24
|
+
def do_GET(self) -> None: # noqa: N802
|
|
25
|
+
nonlocal auth_code
|
|
26
|
+
parsed_path = urllib.parse.urlparse(self.path)
|
|
27
|
+
if parsed_path.path == "/":
|
|
28
|
+
params = urllib.parse.parse_qs(parsed_path.query)
|
|
29
|
+
auth_code = params.get("code", [None])[0]
|
|
30
|
+
self.send_response(200)
|
|
31
|
+
self.send_header("Content-Type", "text/html")
|
|
32
|
+
self.end_headers()
|
|
33
|
+
self.wfile.write(b"""
|
|
34
|
+
<html><body>
|
|
35
|
+
<p>Authorization complete. You may close this window.</p>
|
|
36
|
+
<script>window.close();</script>
|
|
37
|
+
</body></html>
|
|
38
|
+
""")
|
|
39
|
+
else:
|
|
40
|
+
self.send_error(404)
|
|
41
|
+
|
|
42
|
+
def log_message(self, fmt: str, *args: str) -> None:
|
|
43
|
+
pass # suppress default logging
|
|
44
|
+
|
|
45
|
+
socketserver.TCPServer.allow_reuse_address = True
|
|
46
|
+
with socketserver.TCPServer(("", 37719), _HTTPHandler) as httpd:
|
|
47
|
+
webbrowser.open(
|
|
48
|
+
authorize_url
|
|
49
|
+
+ "?"
|
|
50
|
+
+ urllib.parse.urlencode({
|
|
51
|
+
"client_id": client_id,
|
|
52
|
+
"response_type": "code",
|
|
53
|
+
"redirect_uri": redirect_uri,
|
|
54
|
+
"scope": " ".join(scopes),
|
|
55
|
+
})
|
|
56
|
+
)
|
|
57
|
+
print("Waiting for authorization...")
|
|
58
|
+
while auth_code is None:
|
|
59
|
+
httpd.handle_request()
|
|
60
|
+
|
|
61
|
+
data = {
|
|
62
|
+
"client_id": client_id,
|
|
63
|
+
"grant_type": "authorization_code",
|
|
64
|
+
"code": auth_code,
|
|
65
|
+
"redirect_uri": redirect_uri,
|
|
66
|
+
"scope": " ".join(scopes),
|
|
67
|
+
}
|
|
68
|
+
req = urllib.request.Request( # noqa: S310
|
|
69
|
+
token_url, data=urllib.parse.urlencode(data).encode("UTF-8"), method="POST"
|
|
70
|
+
)
|
|
71
|
+
req.add_header("Content-Type", "application/x-www-form-urlencoded")
|
|
72
|
+
with urllib.request.urlopen(req) as resp: # noqa: S310
|
|
73
|
+
res = json.loads(resp.read())["access_token"]
|
|
74
|
+
assert isinstance(res, str)
|
|
75
|
+
return f"{tp}:{res}"
|
|
@@ -78,7 +78,7 @@ class DebugOption(BoolOption):
|
|
|
78
78
|
logging.root.setLevel(logging.DEBUG if value else logging.WARNING)
|
|
79
79
|
|
|
80
80
|
def __bool__(self) -> bool:
|
|
81
|
-
return logging.root.level
|
|
81
|
+
return logging.root.level <= logging.DEBUG
|
|
82
82
|
|
|
83
83
|
|
|
84
84
|
class CallDuration(BoolOption):
|
|
@@ -10,6 +10,8 @@ import pathlib
|
|
|
10
10
|
import typing
|
|
11
11
|
|
|
12
12
|
import shv
|
|
13
|
+
import shv.chainpack
|
|
14
|
+
import shv.path
|
|
13
15
|
|
|
14
16
|
|
|
15
17
|
class StateVarMeta(abc.ABCMeta):
|
|
@@ -67,7 +69,7 @@ class State(dict[type[StateVar], StateVar]):
|
|
|
67
69
|
|
|
68
70
|
def __init__(self) -> None:
|
|
69
71
|
super().__init__()
|
|
70
|
-
self.path
|
|
72
|
+
self.path = shv.path.SHVPath()
|
|
71
73
|
"""Current path we are working relative to."""
|
|
72
74
|
|
|
73
75
|
for cls in StateVar.variables:
|
|
@@ -79,7 +81,7 @@ class State(dict[type[StateVar], StateVar]):
|
|
|
79
81
|
with contextlib.suppress(FileNotFoundError):
|
|
80
82
|
with cachefile.open("rb") as f:
|
|
81
83
|
try:
|
|
82
|
-
data = shv.ChainPack.unpack(f.read())
|
|
84
|
+
data = shv.chainpack.ChainPack.unpack(f.read())
|
|
83
85
|
except (ValueError, EOFError):
|
|
84
86
|
return
|
|
85
87
|
if shv.is_shvmap(data):
|
|
@@ -93,4 +95,4 @@ class State(dict[type[StateVar], StateVar]):
|
|
|
93
95
|
var.cache_dump(data)
|
|
94
96
|
cachefile.parent.mkdir(parents=True, exist_ok=True)
|
|
95
97
|
with cachefile.open("wb") as f:
|
|
96
|
-
f.write(shv.ChainPack.pack(data))
|
|
98
|
+
f.write(shv.chainpack.ChainPack.pack(data))
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import collections.abc
|
|
4
4
|
import itertools
|
|
5
5
|
|
|
6
|
-
import shv
|
|
6
|
+
import shv.path
|
|
7
7
|
from prompt_toolkit.completion import Completion
|
|
8
8
|
|
|
9
9
|
from ..tree import Tree
|
|
@@ -21,7 +21,7 @@ def comp_from(
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
def comp_path(
|
|
24
|
-
path: str, path_prefix: shv.SHVPath, tree: Tree, tail: str = ":"
|
|
24
|
+
path: str, path_prefix: shv.path.SHVPath, tree: Tree, tail: str = ":"
|
|
25
25
|
) -> collections.abc.Iterable[Completion]:
|
|
26
26
|
"""Perform completion for the given current path."""
|
|
27
27
|
pth, _, word = path.rpartition("/")
|
|
@@ -36,7 +36,7 @@ def comp_path(
|
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
def comp_signal_ri(
|
|
39
|
-
ri: str, path_prefix: shv.SHVPath, tree: Tree
|
|
39
|
+
ri: str, path_prefix: shv.path.SHVPath, tree: Tree
|
|
40
40
|
) -> collections.abc.Iterable[Completion]:
|
|
41
41
|
"""Completion for RPC RI."""
|
|
42
42
|
match ri.count(":"):
|
|
@@ -8,6 +8,7 @@ import typing
|
|
|
8
8
|
|
|
9
9
|
import more_itertools
|
|
10
10
|
import shv
|
|
11
|
+
import shv.cpon
|
|
11
12
|
from prompt_toolkit import print_formatted_text
|
|
12
13
|
from prompt_toolkit.document import Document
|
|
13
14
|
from prompt_toolkit.formatted_text import FormattedText
|
|
@@ -18,9 +19,11 @@ from pygments.lexers.data import JsonLexer
|
|
|
18
19
|
def print_ftext(
|
|
19
20
|
fstrs: collections.abc.Iterable[tuple[str, str]]
|
|
20
21
|
| collections.abc.Iterator[tuple[str, str]],
|
|
22
|
+
*args: typing.Any, # noqa: ANN401
|
|
23
|
+
**kwargs: typing.Any, # noqa: ANN401
|
|
21
24
|
) -> None:
|
|
22
25
|
"""Call :meth:`print_formatted_text` with :class:`FormattedText`."""
|
|
23
|
-
print_formatted_text(FormattedText(fstrs))
|
|
26
|
+
print_formatted_text(FormattedText(fstrs), *args, **kwargs)
|
|
24
27
|
|
|
25
28
|
|
|
26
29
|
def print_flist(
|
|
@@ -125,12 +128,14 @@ def _wrap_cpon(cpon: str) -> collections.abc.Iterator[str]:
|
|
|
125
128
|
def print_cpon(data: shv.SHVType, prefix: str = "", short: bool = False) -> None:
|
|
126
129
|
"""Print given data in CPON format."""
|
|
127
130
|
if short:
|
|
128
|
-
cpon = "\n".join(_wrap_cpon(prefix + shv.Cpon.pack(data)))[len(prefix) :]
|
|
131
|
+
cpon = "\n".join(_wrap_cpon(prefix + shv.cpon.Cpon.pack(data)))[len(prefix) :]
|
|
129
132
|
print_ftext(itertools.chain(iter((("", prefix),)), cpon_ftext(cpon)))
|
|
130
133
|
else:
|
|
131
134
|
print_ftext(
|
|
132
135
|
itertools.chain(
|
|
133
136
|
iter((("", prefix),)),
|
|
134
|
-
cpon_ftext(
|
|
137
|
+
cpon_ftext(
|
|
138
|
+
shv.cpon.Cpon.pack(data, shv.cpon.CponWriter.Options(indent=b" "))
|
|
139
|
+
),
|
|
135
140
|
)
|
|
136
141
|
)
|
|
@@ -7,6 +7,8 @@ import contextlib
|
|
|
7
7
|
import typing
|
|
8
8
|
|
|
9
9
|
import shv
|
|
10
|
+
import shv.path
|
|
11
|
+
import shv.rpcdef
|
|
10
12
|
|
|
11
13
|
from .state import State, StateVar
|
|
12
14
|
|
|
@@ -20,9 +22,9 @@ class Node(collections.abc.Mapping[str, "Node"]):
|
|
|
20
22
|
|
|
21
23
|
self.nodes: dict[str, Node] = {}
|
|
22
24
|
"""The child nodes of this node."""
|
|
23
|
-
self.methods: dict[str, shv.RpcDir | None] = {
|
|
24
|
-
"ls": shv.RpcDir.stdls(),
|
|
25
|
-
"dir": shv.RpcDir.stddir(),
|
|
25
|
+
self.methods: dict[str, shv.rpcdef.RpcDir | None] = {
|
|
26
|
+
"ls": shv.rpcdef.RpcDir.stdls(),
|
|
27
|
+
"dir": shv.rpcdef.RpcDir.stddir(),
|
|
26
28
|
}
|
|
27
29
|
"""The methods discovered to be associated with this method."""
|
|
28
30
|
self.nodes_probed = False
|
|
@@ -42,10 +44,10 @@ class Node(collections.abc.Mapping[str, "Node"]):
|
|
|
42
44
|
"""Get number of child nodes."""
|
|
43
45
|
return len(self.nodes)
|
|
44
46
|
|
|
45
|
-
def valid_path(self, path: shv.SHVPath | str) -> Node:
|
|
47
|
+
def valid_path(self, path: shv.path.SHVPath | str) -> Node:
|
|
46
48
|
"""Add valid path relative to this node."""
|
|
47
49
|
if isinstance(path, str):
|
|
48
|
-
path = shv.SHVPath(path)
|
|
50
|
+
path = shv.path.SHVPath(path)
|
|
49
51
|
node = self
|
|
50
52
|
for n in path.parts:
|
|
51
53
|
if n not in node:
|
|
@@ -53,10 +55,10 @@ class Node(collections.abc.Mapping[str, "Node"]):
|
|
|
53
55
|
node = node[n]
|
|
54
56
|
return node
|
|
55
57
|
|
|
56
|
-
def invalid_path(self, path: shv.SHVPath | str) -> None:
|
|
58
|
+
def invalid_path(self, path: shv.path.SHVPath | str) -> None:
|
|
57
59
|
"""Invalidate path as not existent."""
|
|
58
60
|
if isinstance(path, str):
|
|
59
|
-
path = shv.SHVPath(path)
|
|
61
|
+
path = shv.path.SHVPath(path)
|
|
60
62
|
pnode = None
|
|
61
63
|
node = self
|
|
62
64
|
for n in path.parts:
|
|
@@ -67,7 +69,7 @@ class Node(collections.abc.Mapping[str, "Node"]):
|
|
|
67
69
|
if pnode is not None:
|
|
68
70
|
pnode.nodes.pop(path.name)
|
|
69
71
|
|
|
70
|
-
def get_node(self, path: shv.SHVPath) -> Node | None:
|
|
72
|
+
def get_node(self, path: shv.path.SHVPath) -> Node | None:
|
|
71
73
|
"""Get node on given path."""
|
|
72
74
|
node = self
|
|
73
75
|
for n in path.parts:
|
|
@@ -76,7 +78,9 @@ class Node(collections.abc.Mapping[str, "Node"]):
|
|
|
76
78
|
node = node[n]
|
|
77
79
|
return node
|
|
78
80
|
|
|
79
|
-
def get_method(
|
|
81
|
+
def get_method(
|
|
82
|
+
self, path: shv.path.SHVPath, method: str
|
|
83
|
+
) -> shv.rpcdef.RpcDir | None:
|
|
80
84
|
"""Get method from given path."""
|
|
81
85
|
if (node := self.get_node(path)) is not None:
|
|
82
86
|
return node.methods.get(method, None)
|
|
@@ -104,8 +108,8 @@ class Node(collections.abc.Mapping[str, "Node"]):
|
|
|
104
108
|
if shv.is_shvmap(data["methods"]):
|
|
105
109
|
for n, v in data["methods"].items():
|
|
106
110
|
desc = None
|
|
107
|
-
with contextlib.suppress(shv.RpcInvalidParamError, ValueError):
|
|
108
|
-
desc = shv.RpcDir.from_shv(v)
|
|
111
|
+
with contextlib.suppress(shv.rpcdef.RpcInvalidParamError, ValueError):
|
|
112
|
+
desc = shv.rpcdef.RpcDir.from_shv(v)
|
|
109
113
|
self.methods[n] = desc
|
|
110
114
|
self.nodes_probed = bool(data["nodes_probed"])
|
|
111
115
|
self.methods_probed = bool(data["methods_probed"])
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import argparse
|
|
6
|
+
import asyncio
|
|
6
7
|
import contextlib
|
|
8
|
+
import dataclasses
|
|
7
9
|
import os
|
|
8
10
|
import pathlib
|
|
9
11
|
import re
|
|
@@ -12,13 +14,39 @@ import subprocess
|
|
|
12
14
|
import typing
|
|
13
15
|
|
|
14
16
|
import shv
|
|
17
|
+
import shv.rpcapi
|
|
18
|
+
import shv.rpclogin
|
|
19
|
+
import shv.rpctransport
|
|
20
|
+
import shv.rpcurl
|
|
15
21
|
import xdg.BaseDirectory
|
|
16
22
|
|
|
17
23
|
from .args import ArgsParseFuncGenT, register_argparser
|
|
24
|
+
from .client import Client
|
|
18
25
|
from .config import Config, ConfigError, register_config
|
|
26
|
+
from .oauth import oauth_login_token
|
|
19
27
|
from .state import State, StateVar
|
|
20
28
|
|
|
21
29
|
|
|
30
|
+
class RpcUrlEx(shv.rpcurl.RpcUrl):
|
|
31
|
+
"""Extended :class:`shv.rpcurl.RpcUrl`.
|
|
32
|
+
|
|
33
|
+
This defines additional attributes custom for the SHVCLI.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
oauth2: bool = False
|
|
37
|
+
"""If OAuth2 login method should be used or not."""
|
|
38
|
+
|
|
39
|
+
def _parse_query(self, pqs: dict[str, list[str]]) -> None:
|
|
40
|
+
super()._parse_query(pqs)
|
|
41
|
+
if opts := pqs.pop("oauth2", []):
|
|
42
|
+
if opts[0] in {"true", "t", "y"}:
|
|
43
|
+
self.oauth2 = True
|
|
44
|
+
elif opts[0] in {"false", "f", "n"}:
|
|
45
|
+
self.oauth2 = False
|
|
46
|
+
else:
|
|
47
|
+
raise ValueError(f"Invalid for verify: {opts[0]}")
|
|
48
|
+
|
|
49
|
+
|
|
22
50
|
class Url(StateVar):
|
|
23
51
|
"""The SHV RPC URL deduction and store."""
|
|
24
52
|
|
|
@@ -28,8 +56,9 @@ class Url(StateVar):
|
|
|
28
56
|
"""The mapping of hosts aliases to the full URLs."""
|
|
29
57
|
self.hosts_shell: dict[str, str] = {}
|
|
30
58
|
"""The mapping of aliases to the shell expanded URLs."""
|
|
31
|
-
self.url:
|
|
59
|
+
self.url: RpcUrlEx = RpcUrlEx("localhost")
|
|
32
60
|
"""SHV RPC URL currectly set as the one to be used."""
|
|
61
|
+
self._state = state
|
|
33
62
|
|
|
34
63
|
def set(self, value: str) -> None:
|
|
35
64
|
"""Set given value as URL by interpreting it in various ways.
|
|
@@ -55,27 +84,72 @@ class Url(StateVar):
|
|
|
55
84
|
check=True,
|
|
56
85
|
).stdout.decode()
|
|
57
86
|
value = self.hosts.get(value, value)
|
|
58
|
-
self.url =
|
|
87
|
+
self.url = RpcUrlEx.parse(value)
|
|
59
88
|
# TODO this modifies it even if unix is explicitly specified
|
|
60
|
-
if self.url.protocol is shv.RpcProtocol.UNIX:
|
|
89
|
+
if self.url.protocol is shv.rpcurl.RpcProtocol.UNIX:
|
|
61
90
|
with contextlib.suppress(FileNotFoundError):
|
|
62
91
|
st = os.stat(self.url.location)
|
|
63
92
|
if stat.S_ISCHR(st.st_mode):
|
|
64
|
-
self.url.protocol = shv.RpcProtocol.TTY
|
|
93
|
+
self.url.protocol = shv.rpcurl.RpcProtocol.TTY
|
|
65
94
|
|
|
66
95
|
def cache_path(self) -> pathlib.Path:
|
|
67
96
|
"""Cache path based on the current URL."""
|
|
68
97
|
return pathlib.Path(xdg.BaseDirectory.save_cache_path("shvcli")) / re.sub(
|
|
69
98
|
r"[^\w_. -]",
|
|
70
99
|
"_",
|
|
71
|
-
|
|
100
|
+
RpcUrlEx(
|
|
72
101
|
location=self.url.location,
|
|
73
102
|
port=self.url.port,
|
|
74
103
|
protocol=self.url.protocol,
|
|
75
|
-
login=shv.RpcLogin(username=self.url.login.username),
|
|
104
|
+
login=shv.rpclogin.RpcLogin(username=self.url.login.username),
|
|
76
105
|
).to_url(),
|
|
77
106
|
)
|
|
78
107
|
|
|
108
|
+
async def connect(self) -> Client:
|
|
109
|
+
"""Create new client based on this URL."""
|
|
110
|
+
url = self.url
|
|
111
|
+
if self.url.oauth2:
|
|
112
|
+
base = shv.rpcapi.SHVBase(
|
|
113
|
+
await shv.rpctransport.connect_rpc_client(self.url)
|
|
114
|
+
)
|
|
115
|
+
workflows = await base.call("", "workflows")
|
|
116
|
+
await base.disconnect()
|
|
117
|
+
if shv.is_shvlist(workflows):
|
|
118
|
+
oauths = [
|
|
119
|
+
v
|
|
120
|
+
for v in workflows
|
|
121
|
+
if shv.is_shvmap(v)
|
|
122
|
+
and isinstance(tp := v.get("type"), str)
|
|
123
|
+
and tp.startswith("oauth2")
|
|
124
|
+
]
|
|
125
|
+
if len(oauths) >= 1:
|
|
126
|
+
oauth = oauths[0]
|
|
127
|
+
tp = oauth.get("type")
|
|
128
|
+
client_id = oauth.get("clientId")
|
|
129
|
+
authorize_url = oauth.get("authorizeUrl")
|
|
130
|
+
token_url = oauth.get("tokenUrl")
|
|
131
|
+
scopes = oauth.get("scopes")
|
|
132
|
+
assert shv.is_shvlist(scopes)
|
|
133
|
+
token = await asyncio.get_running_loop().run_in_executor(
|
|
134
|
+
None,
|
|
135
|
+
lambda: oauth_login_token(
|
|
136
|
+
str(tp),
|
|
137
|
+
str(client_id),
|
|
138
|
+
str(authorize_url),
|
|
139
|
+
str(token_url),
|
|
140
|
+
list(str(v) for v in scopes),
|
|
141
|
+
),
|
|
142
|
+
)
|
|
143
|
+
url = dataclasses.replace(
|
|
144
|
+
url,
|
|
145
|
+
login=dataclasses.replace(
|
|
146
|
+
url.login,
|
|
147
|
+
token=token,
|
|
148
|
+
login_type=shv.rpclogin.RpcLoginType.TOKEN,
|
|
149
|
+
),
|
|
150
|
+
)
|
|
151
|
+
return await Client.connect(url, state=self._state)
|
|
152
|
+
|
|
79
153
|
|
|
80
154
|
@register_config
|
|
81
155
|
def _config(config: Config, state: State) -> None:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: shvcli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: Silicon Heaven CLI access application
|
|
5
5
|
Author: Elektroline a.s.
|
|
6
6
|
License: MIT License
|
|
@@ -27,7 +27,6 @@ License: MIT License
|
|
|
27
27
|
|
|
28
28
|
Project-URL: repository, https://gitlab.com/silicon-heaven/shvcli
|
|
29
29
|
Classifier: Development Status :: 4 - Beta
|
|
30
|
-
Classifier: Programming Language :: Python :: 3
|
|
31
30
|
Classifier: Operating System :: OS Independent
|
|
32
31
|
Classifier: Environment :: Console
|
|
33
32
|
Classifier: Intended Audience :: End Users/Desktop
|
|
@@ -37,7 +36,7 @@ Classifier: Topic :: Terminals
|
|
|
37
36
|
Requires-Python: >=3.11
|
|
38
37
|
Description-Content-Type: text/x-rst
|
|
39
38
|
License-File: LICENSE
|
|
40
|
-
Requires-Dist: pyshv>=0.
|
|
39
|
+
Requires-Dist: pyshv>=0.10.0
|
|
41
40
|
Requires-Dist: prompt_toolkit
|
|
42
41
|
Requires-Dist: pygments
|
|
43
42
|
Requires-Dist: pyxdg
|
|
@@ -151,6 +150,8 @@ following options are available without any plugins:
|
|
|
151
150
|
is used only in case when method doesn't support delayed responses.
|
|
152
151
|
* **autoget_timeout**: Timeout in seconds for call that is part of autoget
|
|
153
152
|
functionality.
|
|
153
|
+
* **history_count**: Integer number used for number of records requested at time
|
|
154
|
+
for ``!history`` builtin.
|
|
154
155
|
|
|
155
156
|
Example configuration file:
|
|
156
157
|
|
|
@@ -213,6 +214,10 @@ parameter must be path to the local file where data will be stored.
|
|
|
213
214
|
**verify**: provides a way to verify RPC File node against local file. The
|
|
214
215
|
parameter must be path to the local file used for the verification.
|
|
215
216
|
|
|
217
|
+
**history**: is helper to list recorded history of signals by ``.history`` API.
|
|
218
|
+
The listing is incremental in ``history_count`` configured number of records and
|
|
219
|
+
in the order from the newest ones.
|
|
220
|
+
|
|
216
221
|
|
|
217
222
|
Special methods ``ls`` and ``dir``
|
|
218
223
|
----------------------------------
|
|
@@ -225,6 +230,17 @@ Their parameter is also handled in a special way. It is considered to be
|
|
|
225
230
|
additional path suffix unless it is a valid CPON. This is allowed to match the
|
|
226
231
|
common shells.
|
|
227
232
|
|
|
233
|
+
You can see ``raw`` option to disable this behavior.
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
OAuth2 login
|
|
237
|
+
------------
|
|
238
|
+
|
|
239
|
+
SHVCLI supports non-standard login method OAuth2. This is not defined by SHV
|
|
240
|
+
standard. The non-standard URL query ``oauth2=y`` is accepted to perform the
|
|
241
|
+
OAuth2 login. The login is facilitated by web browser. SHVCLI will start server
|
|
242
|
+
on ``tcp://localhost:37719/`` to be able to receive token.
|
|
243
|
+
|
|
228
244
|
|
|
229
245
|
Plugins
|
|
230
246
|
-------
|
|
@@ -3,6 +3,7 @@ README.rst
|
|
|
3
3
|
pyproject.toml
|
|
4
4
|
shvcli/__init__.py
|
|
5
5
|
shvcli/__main__.py
|
|
6
|
+
shvcli/__setuptools__.py
|
|
6
7
|
shvcli/__version__.py
|
|
7
8
|
shvcli/afterconn.py
|
|
8
9
|
shvcli/args.py
|
|
@@ -14,6 +15,7 @@ shvcli/complet.py
|
|
|
14
15
|
shvcli/config.py
|
|
15
16
|
shvcli/file.py
|
|
16
17
|
shvcli/lsdir.py
|
|
18
|
+
shvcli/oauth.py
|
|
17
19
|
shvcli/option.py
|
|
18
20
|
shvcli/options.py
|
|
19
21
|
shvcli/plugins.py
|
|
@@ -34,6 +36,7 @@ shvcli/builtins/broker.py
|
|
|
34
36
|
shvcli/builtins/cd.py
|
|
35
37
|
shvcli/builtins/file.py
|
|
36
38
|
shvcli/builtins/help.py
|
|
39
|
+
shvcli/builtins/history.py
|
|
37
40
|
shvcli/builtins/scan.py
|
|
38
41
|
shvcli/builtins/set.py
|
|
39
42
|
shvcli/builtins/tree.py
|
|
@@ -1,26 +0,0 @@
|
|
|
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()
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|