devicectl-core 0.1.0__py3-none-any.whl

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 (47) hide show
  1. devicectl/__init__.py +18 -0
  2. devicectl/cli/__init__.py +1 -0
  3. devicectl/cli/command.py +95 -0
  4. devicectl/cli/exits.py +32 -0
  5. devicectl/cli/fanout.py +142 -0
  6. devicectl/cli/main.py +69 -0
  7. devicectl/cli/output.py +299 -0
  8. devicectl/cli/parser.py +80 -0
  9. devicectl/cli/report.py +86 -0
  10. devicectl/cli/target.py +26 -0
  11. devicectl/clock.py +57 -0
  12. devicectl/devtools/__init__.py +6 -0
  13. devicectl/devtools/frontlint.py +935 -0
  14. devicectl/devtools/htmcheck.py +396 -0
  15. devicectl/devtools/rendercheck.py +384 -0
  16. devicectl/doctor.py +112 -0
  17. devicectl/errors.py +68 -0
  18. devicectl/fields.py +564 -0
  19. devicectl/meta.py +64 -0
  20. devicectl/paths.py +40 -0
  21. devicectl/progress.py +77 -0
  22. devicectl/report.py +67 -0
  23. devicectl/testing.py +199 -0
  24. devicectl/trace.py +333 -0
  25. devicectl/web/__init__.py +1 -0
  26. devicectl/web/agents.py +94 -0
  27. devicectl/web/events.py +171 -0
  28. devicectl/web/http.py +243 -0
  29. devicectl/web/progress.py +101 -0
  30. devicectl/web/server.py +1013 -0
  31. devicectl/web/static/core.css +3034 -0
  32. devicectl/web/static/js/api.js +198 -0
  33. devicectl/web/static/js/band.js +640 -0
  34. devicectl/web/static/js/chart.js +400 -0
  35. devicectl/web/static/js/drafts.js +312 -0
  36. devicectl/web/static/js/notify.js +272 -0
  37. devicectl/web/static/js/panels.js +432 -0
  38. devicectl/web/static/js/shell.js +672 -0
  39. devicectl/web/static/js/trace.js +133 -0
  40. devicectl/web/static/js/ui.js +1139 -0
  41. devicectl/web/static/vendor/preact-htm.module.js +27 -0
  42. devicectl/web/worker.py +697 -0
  43. devicectl_core-0.1.0.dist-info/METADATA +131 -0
  44. devicectl_core-0.1.0.dist-info/RECORD +47 -0
  45. devicectl_core-0.1.0.dist-info/WHEEL +4 -0
  46. devicectl_core-0.1.0.dist-info/licenses/LICENSE +287 -0
  47. devicectl_core-0.1.0.dist-info/licenses/NOTICE +13 -0
@@ -0,0 +1,80 @@
1
+ """Filling in what the command line left out.
2
+
3
+ Two small rewrites of ``argv`` before argparse ever sees it. Both exist for
4
+ the same reason: the options a command needs live on its *action* parsers,
5
+ so a word left out is not merely a missing word -- it takes the options with
6
+ it, and argparse's complaint names the wrong thing.
7
+
8
+ Both leave a leading word alone, so a mistyped command or action still gets
9
+ argparse's "invalid choice" rather than being quietly handed somewhere else,
10
+ and both leave ``-h`` alone, because it has to reach the parser that lists
11
+ the choices.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from collections.abc import Mapping, Sequence
17
+ from typing import Any
18
+
19
+ # Options that must reach the root parser rather than a subcommand.
20
+ ROOT_OPTIONS = ("-h", "--help", "--version")
21
+
22
+ HELP_OPTIONS = ("-h", "--help")
23
+
24
+
25
+ def insert_default_action(
26
+ argv: Sequence[str], defaults: Mapping[str, str]
27
+ ) -> list[str]:
28
+ """Return ``argv`` with a command's default ACTION filled in, if missing.
29
+
30
+ ``<prog> tags`` becomes ``<prog> tags list``, and options meant for that
31
+ default action are handed through to it (``<prog> scn --peers`` becomes
32
+ ``<prog> scn status --peers``).
33
+ """
34
+ argv = list(argv)
35
+ if not argv or argv[0] not in defaults:
36
+ return argv
37
+ rest = argv[1:]
38
+ if rest and (not rest[0].startswith("-") or rest[0] in HELP_OPTIONS):
39
+ return argv
40
+ return [argv[0], defaults[argv[0]], *rest]
41
+
42
+
43
+ def insert_default_command(argv: Sequence[str], command: str) -> list[str]:
44
+ """Return ``argv`` with ``command`` filled in when no command was typed.
45
+
46
+ For a program with an obvious thing to do when run bare -- serving its
47
+ web interface, usually -- because a page of usage text is not what
48
+ somebody who typed the bare name came for. Options meant for it are
49
+ handed through, so ``<prog> --port 8080`` reaches that command rather
50
+ than being told ``--port`` belongs to a subcommand.
51
+ """
52
+ argv = list(argv)
53
+ if not argv:
54
+ return [command]
55
+ first = argv[0]
56
+ if first in ROOT_OPTIONS or not first.startswith("-"):
57
+ return argv
58
+ return [command, *argv]
59
+
60
+
61
+ def default_actions(commands: Mapping[str, Any]) -> dict[str, str]:
62
+ """Read every command's default ACTION out of the command table.
63
+
64
+ The alternative is a second table listing them, which has to be kept in
65
+ step with the first by hand and silently does nothing when it is not --
66
+ a default named for a command that no longer exists never fires, and a
67
+ command that grew actions never gets one.
68
+ """
69
+ return {
70
+ name: command.default_action
71
+ for name, command in commands.items()
72
+ if getattr(command, "default_action", "")
73
+ }
74
+
75
+
76
+ __all__ = [
77
+ "default_actions",
78
+ "insert_default_action",
79
+ "insert_default_command",
80
+ ]
@@ -0,0 +1,86 @@
1
+ """Drawing a slow operation's progress on a terminal.
2
+
3
+ The one live stderr line, redrawn as things move and ended with a newline
4
+ when the phase is over. Two things turn it off: a pipe, where nothing would
5
+ ever erase the escape codes, and a debug or trace mode, whose log lines
6
+ would shred it. Both fall back to plain lines, which is also what a log
7
+ file wants.
8
+
9
+ What a transfer *counts* is the program's own -- bytes for one, 128-byte
10
+ blocks for another -- so :meth:`Reporter.sending` is left to the subclass.
11
+ Everything around it, including closing off a half-drawn line when a failure
12
+ lands on top of it, is here.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import sys
18
+ from types import TracebackType
19
+
20
+ from devicectl.progress import end_live, write_live
21
+ from devicectl.report import Reporter
22
+
23
+
24
+ class TerminalReporter(Reporter):
25
+ """Report a slow operation's progress to the terminal.
26
+
27
+ Use it as a context manager: leaving the block closes off a live line
28
+ that a failure would otherwise have left half-drawn, with the error
29
+ message landing on top of it.
30
+ """
31
+
32
+ def __init__(self, *, quiet: bool = False) -> None:
33
+ """Report to stderr, drawing a live line unless ``quiet`` or a pipe.
34
+
35
+ ``quiet`` is what a program's ``--debug`` or ``--trace`` passes: its
36
+ own logging is going to the same stream, and the two cannot share a
37
+ line that is redrawn in place.
38
+ """
39
+ self.quiet = quiet
40
+ self.live = not quiet and sys.stderr.isatty()
41
+ self._drawn = False # a live line is on screen, awaiting its newline
42
+ self._last_draw = 0.0
43
+
44
+ def __enter__(self) -> "TerminalReporter":
45
+ """Return the reporter; nothing is drawn until something happens."""
46
+ return self
47
+
48
+ def __exit__(
49
+ self,
50
+ exc_type: type[BaseException] | None,
51
+ exc: BaseException | None,
52
+ tb: TracebackType | None,
53
+ ) -> None:
54
+ """End any live line, so the next thing printed starts on its own."""
55
+ self._close()
56
+
57
+ def _close(self) -> None:
58
+ """Finish the live line, if one is on screen."""
59
+ if self._drawn:
60
+ end_live()
61
+ self._drawn = False
62
+
63
+ def _draw(self, text: str) -> None:
64
+ """Put ``text`` on the live line (no-op when there is no live line)."""
65
+ if not self.live:
66
+ return
67
+ write_live(text)
68
+ self._drawn = True
69
+
70
+ def step(self, message: str) -> None:
71
+ """Print the new phase on a line of its own."""
72
+ self._close()
73
+ print(f"{message}...")
74
+
75
+ def detail(self, message: str) -> None:
76
+ """Print a detail, indented under the phase it belongs to."""
77
+ self._close()
78
+ print(f" {message}")
79
+
80
+ def warn(self, message: str) -> None:
81
+ """Print a warning to stderr."""
82
+ self._close()
83
+ print(f"warning: {message}", file=sys.stderr)
84
+
85
+
86
+ __all__ = ["TerminalReporter"]
@@ -0,0 +1,26 @@
1
+ """Choosing between sources of the same setting.
2
+
3
+ Which device a command talks to can be answered by the command line, by a
4
+ named table in the config file, by that file's own defaults, or by whatever
5
+ the program can discover for itself. Each program resolves that its own way,
6
+ but they all resolve it by asking the same question of one value after
7
+ another, and this is that question.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import TypeVar
13
+
14
+ T = TypeVar("T")
15
+
16
+
17
+ def first_set(*values: T | None, default: T) -> T:
18
+ """Return the first value that was actually given, else ``default``.
19
+
20
+ Sources are passed most specific first, so a precedence rule reads as one
21
+ line instead of a stack of conditionals.
22
+ """
23
+ return next((value for value in values if value is not None), default)
24
+
25
+
26
+ __all__ = ["first_set"]
devicectl/clock.py ADDED
@@ -0,0 +1,57 @@
1
+ """How far a device's clock is from this computer's, in words.
2
+
3
+ Both programs show it, on the same card of the same dashboard, and each had
4
+ its own sentence for it: one said "3.5 hours ahead of this computer", the
5
+ other printed a bare "03h 30m" beside a label and left the direction out
6
+ altogether. One phrase now, with the direction in it and the computer left
7
+ out of it -- the page is often read on a different machine from the one the
8
+ program runs on, and "this computer" is then the wrong one.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from devicectl.progress import SECONDS_PER_DAY, SECONDS_PER_HOUR, SECONDS_PER_MINUTE
14
+
15
+ # Closer than this and the two clocks are the same clock. A reading takes a
16
+ # round trip, and a device that answers in a second and a half is not a
17
+ # device a second and a half out.
18
+ DRIFT_NOISE_S = 2.0
19
+
20
+ SECONDS_PER_YEAR = 365 * SECONDS_PER_DAY
21
+
22
+ IN_SYNC = "in sync"
23
+
24
+ # The scale matters: a device that has never been set is not seconds out but
25
+ # years, since it boots with its clock at whatever its firmware was built on.
26
+ _UNITS = (
27
+ (SECONDS_PER_MINUTE, 1, "second", 0),
28
+ (SECONDS_PER_HOUR, SECONDS_PER_MINUTE, "minute", 0),
29
+ (SECONDS_PER_DAY, SECONDS_PER_HOUR, "hour", 1),
30
+ (SECONDS_PER_YEAR, SECONDS_PER_DAY, "day", 1),
31
+ (None, SECONDS_PER_YEAR, "year", 1),
32
+ )
33
+
34
+
35
+ def format_drift(seconds: float | None) -> str | None:
36
+ """Say ``12 seconds ahead``, ``2.5 years behind`` or ``in sync``.
37
+
38
+ Positive is a device clock ahead of this computer's. None -- a device
39
+ that reports no time -- is handed back as None, because what to say
40
+ instead is the caller's: a charger and a battery do not fail to have a
41
+ clock for the same reason.
42
+ """
43
+ if seconds is None:
44
+ return None
45
+ size = abs(seconds)
46
+ if size < DRIFT_NOISE_S:
47
+ return IN_SYNC
48
+ for below, per, word, digits in _UNITS:
49
+ if below is None or size < below:
50
+ amount = round(size / per, digits)
51
+ said = f"{amount:.{digits}f}"
52
+ plural = "" if amount == 1 else "s"
53
+ return f"{said} {word}{plural} {'ahead' if seconds > 0 else 'behind'}"
54
+ raise AssertionError("unreachable: the last unit has no upper bound")
55
+
56
+
57
+ __all__ = ["DRIFT_NOISE_S", "IN_SYNC", "format_drift"]
@@ -0,0 +1,6 @@
1
+ """Checks on the browser half that no off-the-shelf linter performs.
2
+
3
+ Each is a module with a ``main`` and runs as ``python -m
4
+ devicectl.devtools.<name> <static-root>``, so a program's test suite is a
5
+ few lines pointing it at its own ``web/static``.
6
+ """