sercomm-printserver 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.
- sercomm_printserver/__init__.py +101 -0
- sercomm_printserver/__init__conf__.py +90 -0
- sercomm_printserver/__main__.py +9 -0
- sercomm_printserver/adapters/__init__.py +14 -0
- sercomm_printserver/adapters/cli/__init__.py +80 -0
- sercomm_printserver/adapters/cli/commands/__init__.py +49 -0
- sercomm_printserver/adapters/cli/commands/_device.py +166 -0
- sercomm_printserver/adapters/cli/commands/arp.py +232 -0
- sercomm_printserver/adapters/cli/commands/config.py +527 -0
- sercomm_printserver/adapters/cli/commands/device.py +391 -0
- sercomm_printserver/adapters/cli/commands/device_config.py +251 -0
- sercomm_printserver/adapters/cli/commands/discover.py +81 -0
- sercomm_printserver/adapters/cli/commands/info.py +38 -0
- sercomm_printserver/adapters/cli/commands/printing.py +137 -0
- sercomm_printserver/adapters/cli/config_load.py +242 -0
- sercomm_printserver/adapters/cli/constants.py +68 -0
- sercomm_printserver/adapters/cli/context.py +188 -0
- sercomm_printserver/adapters/cli/envelope.py +93 -0
- sercomm_printserver/adapters/cli/exit_codes.py +59 -0
- sercomm_printserver/adapters/cli/main.py +123 -0
- sercomm_printserver/adapters/cli/py.typed +0 -0
- sercomm_printserver/adapters/cli/root.py +166 -0
- sercomm_printserver/adapters/cli/safe_console.py +315 -0
- sercomm_printserver/adapters/cli/typed_click.py +77 -0
- sercomm_printserver/adapters/config/__init__.py +25 -0
- sercomm_printserver/adapters/config/defaultconfig.d/40-layered-config.toml +78 -0
- sercomm_printserver/adapters/config/defaultconfig.d/50-device.toml +64 -0
- sercomm_printserver/adapters/config/defaultconfig.d/90-logging.toml +448 -0
- sercomm_printserver/adapters/config/defaultconfig.toml +56 -0
- sercomm_printserver/adapters/config/deploy.py +135 -0
- sercomm_printserver/adapters/config/device_settings.py +78 -0
- sercomm_printserver/adapters/config/display.py +60 -0
- sercomm_printserver/adapters/config/loader.py +210 -0
- sercomm_printserver/adapters/config/overrides.py +326 -0
- sercomm_printserver/adapters/config/py.typed +0 -0
- sercomm_printserver/adapters/discovery/__init__.py +7 -0
- sercomm_printserver/adapters/discovery/py.typed +0 -0
- sercomm_printserver/adapters/discovery/scanner.py +98 -0
- sercomm_printserver/adapters/ftp/__init__.py +11 -0
- sercomm_printserver/adapters/ftp/client.py +459 -0
- sercomm_printserver/adapters/ftp/py.typed +0 -0
- sercomm_printserver/adapters/logging/__init__.py +13 -0
- sercomm_printserver/adapters/logging/py.typed +0 -0
- sercomm_printserver/adapters/logging/setup.py +448 -0
- sercomm_printserver/adapters/lpd/__init__.py +11 -0
- sercomm_printserver/adapters/lpd/client.py +252 -0
- sercomm_printserver/adapters/lpd/py.typed +0 -0
- sercomm_printserver/adapters/memory/__init__.py +45 -0
- sercomm_printserver/adapters/memory/config.py +66 -0
- sercomm_printserver/adapters/memory/device.py +190 -0
- sercomm_printserver/adapters/memory/discovery.py +67 -0
- sercomm_printserver/adapters/memory/logging.py +80 -0
- sercomm_printserver/adapters/memory/neighbors.py +58 -0
- sercomm_printserver/adapters/neighbors/__init__.py +7 -0
- sercomm_printserver/adapters/neighbors/py.typed +0 -0
- sercomm_printserver/adapters/neighbors/table.py +102 -0
- sercomm_printserver/adapters/py.typed +0 -0
- sercomm_printserver/adapters/raw/__init__.py +7 -0
- sercomm_printserver/adapters/raw/printer.py +130 -0
- sercomm_printserver/adapters/raw/py.typed +0 -0
- sercomm_printserver/adapters/snmp/__init__.py +7 -0
- sercomm_printserver/adapters/snmp/py.typed +0 -0
- sercomm_printserver/adapters/snmp/uptime.py +118 -0
- sercomm_printserver/adapters/telnet/__init__.py +7 -0
- sercomm_printserver/adapters/telnet/client.py +179 -0
- sercomm_printserver/adapters/telnet/iac.py +114 -0
- sercomm_printserver/adapters/telnet/py.typed +0 -0
- sercomm_printserver/adapters/tftp/__init__.py +11 -0
- sercomm_printserver/adapters/tftp/client.py +264 -0
- sercomm_printserver/adapters/tftp/py.typed +0 -0
- sercomm_printserver/application/__init__.py +28 -0
- sercomm_printserver/application/arp_trick.py +199 -0
- sercomm_printserver/application/discovery.py +92 -0
- sercomm_printserver/application/ports.py +244 -0
- sercomm_printserver/application/print_server.py +339 -0
- sercomm_printserver/application/py.typed +0 -0
- sercomm_printserver/composition/__init__.py +185 -0
- sercomm_printserver/composition/py.typed +0 -0
- sercomm_printserver/domain/__init__.py +20 -0
- sercomm_printserver/domain/config_catalogue.py +351 -0
- sercomm_printserver/domain/config_codec.py +146 -0
- sercomm_printserver/domain/config_lines.py +89 -0
- sercomm_printserver/domain/config_model.py +233 -0
- sercomm_printserver/domain/config_reader.py +326 -0
- sercomm_printserver/domain/config_types.py +176 -0
- sercomm_printserver/domain/config_writer.py +206 -0
- sercomm_printserver/domain/device_files.py +53 -0
- sercomm_printserver/domain/device_info.py +229 -0
- sercomm_printserver/domain/endpoint.py +48 -0
- sercomm_printserver/domain/enums.py +55 -0
- sercomm_printserver/domain/errors.py +102 -0
- sercomm_printserver/domain/port_status.py +174 -0
- sercomm_printserver/domain/py.typed +0 -0
- sercomm_printserver/entry.py +26 -0
- sercomm_printserver/py.typed +0 -0
- sercomm_printserver-0.1.0.dist-info/METADATA +355 -0
- sercomm_printserver-0.1.0.dist-info/RECORD +100 -0
- sercomm_printserver-0.1.0.dist-info/WHEEL +4 -0
- sercomm_printserver-0.1.0.dist-info/entry_points.txt +3 -0
- sercomm_printserver-0.1.0.dist-info/licenses/LICENSE +22 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Public package surface: talk to a SerComm print server from Python.
|
|
2
|
+
|
|
3
|
+
Typical use::
|
|
4
|
+
|
|
5
|
+
from sercomm_printserver import DeviceEndpoint, PrintJob, open_print_server
|
|
6
|
+
|
|
7
|
+
server = open_print_server(DeviceEndpoint(host="192.0.2.52"))
|
|
8
|
+
server.print_lpd("L1", PrintJob(data=b"hello\\r\\n\\f", name="greeting"))
|
|
9
|
+
print(server.info().firmware_version)
|
|
10
|
+
|
|
11
|
+
Finding devices: :func:`discover_print_servers`. Reaching a device by its MAC through a borrowed
|
|
12
|
+
address (the ARP trick, needs admin rights): :mod:`arp_trick` with :class:`IpscoutNeighborTable`.
|
|
13
|
+
Every failure is one of the exceptions exported here; see each method's ``Raises``.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from ipaddress import IPv4Network
|
|
19
|
+
|
|
20
|
+
# Metadata
|
|
21
|
+
from .__init__conf__ import print_info
|
|
22
|
+
from .adapters.discovery import FtpBannerScanner
|
|
23
|
+
from .adapters.neighbors import IpscoutNeighborTable
|
|
24
|
+
from .application import arp_trick
|
|
25
|
+
from .application.discovery import DiscoveredDevice, DiscoveryReport, discover
|
|
26
|
+
from .application.print_server import ConfigWriteResult, PrintServer
|
|
27
|
+
|
|
28
|
+
# Composition exports (wired adapters)
|
|
29
|
+
from .composition import get_config, open_print_server
|
|
30
|
+
from .domain.config_codec import ConfigChange, parse_config, render_config, with_value
|
|
31
|
+
from .domain.config_model import DeviceConfig
|
|
32
|
+
from .domain.device_info import DeviceInfo, MacAddress, normalise_mac
|
|
33
|
+
from .domain.endpoint import DeviceEndpoint, PrintJob
|
|
34
|
+
from .domain.errors import (
|
|
35
|
+
AuthenticationRequiredError,
|
|
36
|
+
ConfigFormatError,
|
|
37
|
+
ConfigurationError,
|
|
38
|
+
ConfirmationRequiredError,
|
|
39
|
+
DeviceResponseError,
|
|
40
|
+
DeviceUnavailableError,
|
|
41
|
+
HostPrivilegeError,
|
|
42
|
+
NeighborTableError,
|
|
43
|
+
)
|
|
44
|
+
from .domain.port_status import PortStatus
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def discover_print_servers(*, networks: list[str] | None = None, timeout: float = 3.0) -> DiscoveryReport:
|
|
48
|
+
"""Find the family's print servers by their FTP greeting; never logs in, never prints.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
networks: CIDR networks to search (``"192.0.2.0/24"``); None searches this host's own.
|
|
52
|
+
timeout: Seconds to wait for each host's greeting.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
The devices found, with the MAC this host's neighbour cache holds for each.
|
|
56
|
+
|
|
57
|
+
Raises:
|
|
58
|
+
ValueError: A network is not an IPv4 CIDR.
|
|
59
|
+
"""
|
|
60
|
+
chosen = [_network(network) for network in networks] if networks else None
|
|
61
|
+
return discover(FtpBannerScanner(), networks=chosen, timeout=timeout)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _network(text: str) -> IPv4Network:
|
|
65
|
+
try:
|
|
66
|
+
return IPv4Network(text.strip(), strict=False)
|
|
67
|
+
except ValueError as error:
|
|
68
|
+
raise ValueError(f"{text!r} is not an IPv4 network: {error}") from error
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
__all__ = [
|
|
72
|
+
"AuthenticationRequiredError",
|
|
73
|
+
"ConfigChange",
|
|
74
|
+
"ConfigFormatError",
|
|
75
|
+
"ConfigWriteResult",
|
|
76
|
+
"ConfigurationError",
|
|
77
|
+
"ConfirmationRequiredError",
|
|
78
|
+
"DeviceConfig",
|
|
79
|
+
"DeviceEndpoint",
|
|
80
|
+
"DeviceInfo",
|
|
81
|
+
"DeviceResponseError",
|
|
82
|
+
"DeviceUnavailableError",
|
|
83
|
+
"DiscoveredDevice",
|
|
84
|
+
"DiscoveryReport",
|
|
85
|
+
"HostPrivilegeError",
|
|
86
|
+
"IpscoutNeighborTable",
|
|
87
|
+
"MacAddress",
|
|
88
|
+
"NeighborTableError",
|
|
89
|
+
"PortStatus",
|
|
90
|
+
"PrintJob",
|
|
91
|
+
"PrintServer",
|
|
92
|
+
"arp_trick",
|
|
93
|
+
"discover_print_servers",
|
|
94
|
+
"get_config",
|
|
95
|
+
"normalise_mac",
|
|
96
|
+
"open_print_server",
|
|
97
|
+
"parse_config",
|
|
98
|
+
"print_info",
|
|
99
|
+
"render_config",
|
|
100
|
+
"with_value",
|
|
101
|
+
]
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Static package metadata surfaced to CLI commands and documentation.
|
|
2
|
+
|
|
3
|
+
Purpose
|
|
4
|
+
-------
|
|
5
|
+
Expose the current project metadata as simple constants. These values are kept
|
|
6
|
+
in sync with ``pyproject.toml`` by development automation (tests, push
|
|
7
|
+
pipelines), so runtime code does not query packaging metadata.
|
|
8
|
+
|
|
9
|
+
Contents
|
|
10
|
+
--------
|
|
11
|
+
* Module-level constants describing the published package.
|
|
12
|
+
* :func:`print_info` rendering the constants for the CLI ``info`` command.
|
|
13
|
+
|
|
14
|
+
System Role
|
|
15
|
+
-----------
|
|
16
|
+
Lives in the adapters/platform layer; CLI transports import these constants to
|
|
17
|
+
present authoritative project information without invoking packaging APIs.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import sys
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"LAYEREDCONF_APP",
|
|
26
|
+
"LAYEREDCONF_SLUG",
|
|
27
|
+
"LAYEREDCONF_VENDOR",
|
|
28
|
+
"author",
|
|
29
|
+
"author_email",
|
|
30
|
+
"homepage",
|
|
31
|
+
"name",
|
|
32
|
+
"print_info",
|
|
33
|
+
"shell_command",
|
|
34
|
+
"title",
|
|
35
|
+
"version",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
#: Distribution name declared in ``pyproject.toml``.
|
|
39
|
+
name = "sercomm-printserver"
|
|
40
|
+
#: Human-readable summary shown in CLI help output.
|
|
41
|
+
title = "CLI and Python library for SerComm-based network print servers (PS301 and OEM variants)"
|
|
42
|
+
#: Current release version pulled from ``pyproject.toml`` by automation.
|
|
43
|
+
version = "0.1.0"
|
|
44
|
+
#: Repository homepage presented to users.
|
|
45
|
+
homepage = "https://github.com/bitranox/sercomm-printserver"
|
|
46
|
+
#: Author attribution surfaced in CLI output.
|
|
47
|
+
author = "bitranox"
|
|
48
|
+
#: Contact email surfaced in CLI output.
|
|
49
|
+
author_email = "bitranox@gmail.com"
|
|
50
|
+
#: Console-script name published by the package.
|
|
51
|
+
shell_command = "sercomm-printserver"
|
|
52
|
+
|
|
53
|
+
#: Vendor identifier for lib_layered_config paths (macOS/Windows)
|
|
54
|
+
LAYEREDCONF_VENDOR: str = "bitranox"
|
|
55
|
+
#: Application display name for lib_layered_config paths (macOS/Windows)
|
|
56
|
+
LAYEREDCONF_APP: str = "sercomm_printserver"
|
|
57
|
+
#: Configuration slug for lib_layered_config Linux paths and environment variables
|
|
58
|
+
LAYEREDCONF_SLUG: str = "sercomm-printserver"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def print_info() -> None:
|
|
62
|
+
"""Print the summarised metadata block used by the CLI ``info`` command.
|
|
63
|
+
|
|
64
|
+
Why
|
|
65
|
+
Provides a single, auditable rendering function so documentation and
|
|
66
|
+
CLI output always match the system design reference.
|
|
67
|
+
|
|
68
|
+
Side Effects
|
|
69
|
+
Writes to ``stdout``.
|
|
70
|
+
|
|
71
|
+
Examples
|
|
72
|
+
--------
|
|
73
|
+
>>> print_info() # doctest: +ELLIPSIS
|
|
74
|
+
Info for sercomm-printserver:
|
|
75
|
+
...
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
fields = [
|
|
79
|
+
("name", name),
|
|
80
|
+
("title", title),
|
|
81
|
+
("version", version),
|
|
82
|
+
("homepage", homepage),
|
|
83
|
+
("author", author),
|
|
84
|
+
("author_email", author_email),
|
|
85
|
+
("shell_command", shell_command),
|
|
86
|
+
]
|
|
87
|
+
pad = max(len(label) for label, _ in fields)
|
|
88
|
+
lines = [f"Info for {name}:", ""]
|
|
89
|
+
lines.extend(f" {label.ljust(pad)} = {value}" for label, value in fields)
|
|
90
|
+
sys.stdout.write("\n".join(lines) + "\n")
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Module entry point for ``python -m sercomm_printserver``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .adapters.cli.main import main
|
|
6
|
+
from .composition import build_production
|
|
7
|
+
|
|
8
|
+
if __name__ == "__main__":
|
|
9
|
+
raise SystemExit(main(services_factory=build_production))
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Adapters layer - infrastructure and framework integrations.
|
|
2
|
+
|
|
3
|
+
Contains adapter implementations that connect the application to external
|
|
4
|
+
systems and frameworks (CLI, configuration, logging).
|
|
5
|
+
|
|
6
|
+
Contents:
|
|
7
|
+
* :mod:`.config` - Configuration loading, deployment, and display
|
|
8
|
+
* :mod:`.logging` - Logging setup with lib_log_rich
|
|
9
|
+
* :mod:`.cli` - Click CLI framework integration
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
__all__: list[str] = []
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""CLI package providing the command-line interface.
|
|
2
|
+
|
|
3
|
+
Re-exports all public symbols from submodules for convenient access.
|
|
4
|
+
|
|
5
|
+
Contents:
|
|
6
|
+
* Click context helpers and traceback state management from :mod:`.context`
|
|
7
|
+
* Root command group from :mod:`.root`
|
|
8
|
+
* Entry point from :mod:`.main`
|
|
9
|
+
* All command functions from :mod:`.commands`
|
|
10
|
+
|
|
11
|
+
System Role:
|
|
12
|
+
Acts as the public facade for the CLI subsystem. Consumers import from here
|
|
13
|
+
and remain insulated from internal module boundaries.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from .commands import (
|
|
19
|
+
cli_arp,
|
|
20
|
+
cli_config,
|
|
21
|
+
cli_config_deploy,
|
|
22
|
+
cli_config_generate_examples,
|
|
23
|
+
cli_device_config,
|
|
24
|
+
cli_device_info,
|
|
25
|
+
cli_discover,
|
|
26
|
+
cli_factory_default,
|
|
27
|
+
cli_firmware,
|
|
28
|
+
cli_info,
|
|
29
|
+
cli_password,
|
|
30
|
+
cli_print,
|
|
31
|
+
cli_queue,
|
|
32
|
+
cli_reset,
|
|
33
|
+
cli_status,
|
|
34
|
+
cli_store_ip,
|
|
35
|
+
)
|
|
36
|
+
from .constants import CLICK_CONTEXT_SETTINGS, TRACEBACK_SUMMARY_LIMIT, TRACEBACK_VERBOSE_LIMIT
|
|
37
|
+
from .context import (
|
|
38
|
+
TracebackState,
|
|
39
|
+
apply_traceback_preferences,
|
|
40
|
+
restore_traceback_state,
|
|
41
|
+
snapshot_traceback_state,
|
|
42
|
+
store_cli_context,
|
|
43
|
+
)
|
|
44
|
+
from .main import main
|
|
45
|
+
from .root import cli
|
|
46
|
+
|
|
47
|
+
__all__ = [
|
|
48
|
+
# Constants
|
|
49
|
+
"CLICK_CONTEXT_SETTINGS",
|
|
50
|
+
"TRACEBACK_SUMMARY_LIMIT",
|
|
51
|
+
"TRACEBACK_VERBOSE_LIMIT",
|
|
52
|
+
# Traceback management
|
|
53
|
+
"TracebackState",
|
|
54
|
+
"apply_traceback_preferences",
|
|
55
|
+
# Root command
|
|
56
|
+
"cli",
|
|
57
|
+
# Commands
|
|
58
|
+
"cli_arp",
|
|
59
|
+
"cli_config",
|
|
60
|
+
"cli_config_deploy",
|
|
61
|
+
"cli_config_generate_examples",
|
|
62
|
+
"cli_device_config",
|
|
63
|
+
"cli_device_info",
|
|
64
|
+
"cli_discover",
|
|
65
|
+
"cli_factory_default",
|
|
66
|
+
"cli_firmware",
|
|
67
|
+
"cli_info",
|
|
68
|
+
"cli_password",
|
|
69
|
+
"cli_print",
|
|
70
|
+
"cli_queue",
|
|
71
|
+
"cli_reset",
|
|
72
|
+
"cli_status",
|
|
73
|
+
"cli_store_ip",
|
|
74
|
+
# Entry point
|
|
75
|
+
"main",
|
|
76
|
+
"restore_traceback_state",
|
|
77
|
+
"snapshot_traceback_state",
|
|
78
|
+
# Context helpers
|
|
79
|
+
"store_cli_context",
|
|
80
|
+
]
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""CLI command implementations.
|
|
2
|
+
|
|
3
|
+
Collects all subcommand functions and re-exports them for registration
|
|
4
|
+
with the root CLI group.
|
|
5
|
+
|
|
6
|
+
Contents:
|
|
7
|
+
* Info commands from :mod:`.info`
|
|
8
|
+
* App configuration commands from :mod:`.config`
|
|
9
|
+
* Device commands from :mod:`.device`, :mod:`.device_config` and :mod:`.printing`
|
|
10
|
+
* ARP-trick commands from :mod:`.arp`
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from .arp import cli_arp
|
|
16
|
+
from .config import cli_config, cli_config_deploy, cli_config_generate_examples
|
|
17
|
+
from .device import (
|
|
18
|
+
cli_device_info,
|
|
19
|
+
cli_factory_default,
|
|
20
|
+
cli_firmware,
|
|
21
|
+
cli_password,
|
|
22
|
+
cli_queue,
|
|
23
|
+
cli_reset,
|
|
24
|
+
cli_status,
|
|
25
|
+
cli_store_ip,
|
|
26
|
+
)
|
|
27
|
+
from .device_config import cli_device_config
|
|
28
|
+
from .discover import cli_discover
|
|
29
|
+
from .info import cli_info
|
|
30
|
+
from .printing import cli_print
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"cli_arp",
|
|
34
|
+
"cli_config",
|
|
35
|
+
"cli_config_deploy",
|
|
36
|
+
"cli_config_generate_examples",
|
|
37
|
+
"cli_device_config",
|
|
38
|
+
"cli_device_info",
|
|
39
|
+
"cli_discover",
|
|
40
|
+
"cli_factory_default",
|
|
41
|
+
"cli_firmware",
|
|
42
|
+
"cli_info",
|
|
43
|
+
"cli_password",
|
|
44
|
+
"cli_print",
|
|
45
|
+
"cli_queue",
|
|
46
|
+
"cli_reset",
|
|
47
|
+
"cli_status",
|
|
48
|
+
"cli_store_ip",
|
|
49
|
+
]
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""What every device command shares: its options, how it reaches the device, how it answers.
|
|
2
|
+
|
|
3
|
+
A command supplies only the work (a function of a
|
|
4
|
+
:class:`~sercomm_printserver.application.print_server.PrintServer`) and how to say the result
|
|
5
|
+
to a person. :func:`run_device_command` resolves the device from ``[device]`` and the flags,
|
|
6
|
+
runs the work, prints the envelope or the human text, and turns the known failures into their
|
|
7
|
+
exit codes, so all commands report the same failure the same way.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import TYPE_CHECKING, TypeVar
|
|
13
|
+
|
|
14
|
+
import lib_log_rich.runtime
|
|
15
|
+
import rich_click as click
|
|
16
|
+
from pydantic import BaseModel
|
|
17
|
+
|
|
18
|
+
from sercomm_printserver.adapters.config.device_settings import load_device_settings, resolve_endpoint
|
|
19
|
+
from sercomm_printserver.domain.errors import (
|
|
20
|
+
AuthenticationRequiredError,
|
|
21
|
+
ConfigurationError,
|
|
22
|
+
ConfirmationRequiredError,
|
|
23
|
+
DeviceResponseError,
|
|
24
|
+
DeviceUnavailableError,
|
|
25
|
+
HostPrivilegeError,
|
|
26
|
+
NeighborTableError,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
from .. import safe_console
|
|
30
|
+
from ..config_load import require_config
|
|
31
|
+
from ..context import get_cli_context
|
|
32
|
+
from ..envelope import Envelope, OutputMode, report_failure, write_envelope
|
|
33
|
+
from ..exit_codes import ExitCode
|
|
34
|
+
from ..typed_click import option
|
|
35
|
+
|
|
36
|
+
if TYPE_CHECKING:
|
|
37
|
+
from collections.abc import Callable
|
|
38
|
+
from typing import Any
|
|
39
|
+
|
|
40
|
+
from sercomm_printserver.adapters.config.device_settings import DeviceSettings
|
|
41
|
+
from sercomm_printserver.application.print_server import PrintServer
|
|
42
|
+
from sercomm_printserver.composition import AppServices
|
|
43
|
+
from sercomm_printserver.domain.endpoint import DeviceEndpoint
|
|
44
|
+
|
|
45
|
+
from ..constants import CommandName
|
|
46
|
+
|
|
47
|
+
_P = TypeVar("_P", bound="BaseModel")
|
|
48
|
+
_F = TypeVar("_F", bound="Callable[..., Any]")
|
|
49
|
+
|
|
50
|
+
#: Known failures and their exit codes, most specific first: DeviceResponseError and
|
|
51
|
+
#: ConfigFormatError are both ValueErrors, and the refusals and HostPrivilegeError are all
|
|
52
|
+
#: PermissionErrors - only the last says THIS host lacks a right, so it alone exits 13.
|
|
53
|
+
_EXIT_CODES: tuple[tuple[type[Exception], ExitCode], ...] = (
|
|
54
|
+
(ConfigurationError, ExitCode.CONFIG_ERROR),
|
|
55
|
+
(DeviceUnavailableError, ExitCode.DEVICE_UNAVAILABLE),
|
|
56
|
+
(DeviceResponseError, ExitCode.PROTOCOL_ERROR),
|
|
57
|
+
(HostPrivilegeError, ExitCode.PERMISSION_DENIED),
|
|
58
|
+
(NeighborTableError, ExitCode.GENERAL_ERROR),
|
|
59
|
+
(AuthenticationRequiredError, ExitCode.REFUSED),
|
|
60
|
+
(ConfirmationRequiredError, ExitCode.REFUSED),
|
|
61
|
+
(ValueError, ExitCode.INVALID_ARGUMENT),
|
|
62
|
+
)
|
|
63
|
+
_HANDLED: tuple[type[Exception], ...] = tuple(error_type for error_type, _ in _EXIT_CODES)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def exit_code_for(error: Exception) -> ExitCode:
|
|
67
|
+
"""Return the exit code a known failure maps to.
|
|
68
|
+
|
|
69
|
+
Example:
|
|
70
|
+
>>> exit_code_for(DeviceUnavailableError("down")).value
|
|
71
|
+
69
|
|
72
|
+
"""
|
|
73
|
+
for error_type, code in _EXIT_CODES:
|
|
74
|
+
if isinstance(error, error_type):
|
|
75
|
+
return code
|
|
76
|
+
return ExitCode.GENERAL_ERROR
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def device_options(command: _F) -> _F:
|
|
80
|
+
"""Add ``--host``, ``--timeout``, ``--json`` and ``--json-bare`` to a device command."""
|
|
81
|
+
host = option("--host", type=str, default=None, help="Print server address (overrides [device] host).")
|
|
82
|
+
wrapped: Any = host(session_options(command))
|
|
83
|
+
return wrapped
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def session_options(command: _F) -> _F:
|
|
87
|
+
"""Add ``--timeout``, ``--json`` and ``--json-bare``: what a command that names its own address needs."""
|
|
88
|
+
decorators = (
|
|
89
|
+
option(
|
|
90
|
+
"--timeout",
|
|
91
|
+
type=click.FloatRange(min=0.1, max=600),
|
|
92
|
+
default=None,
|
|
93
|
+
help="Network timeout per operation in seconds (overrides [device] timeout).",
|
|
94
|
+
),
|
|
95
|
+
option("--json", "as_json", is_flag=True, default=False, help="Answer as an indented JSON envelope."),
|
|
96
|
+
option("--json-bare", "as_json_bare", is_flag=True, default=False, help="Answer as one-line JSON."),
|
|
97
|
+
)
|
|
98
|
+
wrapped: Any = command
|
|
99
|
+
for decorator in reversed(decorators):
|
|
100
|
+
wrapped = decorator(wrapped)
|
|
101
|
+
return wrapped
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def run_device_command(
|
|
105
|
+
ctx: click.Context,
|
|
106
|
+
*,
|
|
107
|
+
command: CommandName,
|
|
108
|
+
flags: DeviceFlags,
|
|
109
|
+
work: Callable[[PrintServer], _P],
|
|
110
|
+
human: Callable[[_P], str],
|
|
111
|
+
) -> None:
|
|
112
|
+
"""Run ``work`` against the configured device and answer in the requested shape."""
|
|
113
|
+
|
|
114
|
+
def on_endpoint(endpoint: DeviceEndpoint, services: AppServices, settings: DeviceSettings) -> _P:
|
|
115
|
+
return work(services.open_print_server(endpoint, raw_port_base=settings.raw_port_base))
|
|
116
|
+
|
|
117
|
+
run_endpoint_command(ctx, command=command, flags=flags, work=on_endpoint, human=human)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def run_endpoint_command(
|
|
121
|
+
ctx: click.Context,
|
|
122
|
+
*,
|
|
123
|
+
command: CommandName,
|
|
124
|
+
flags: DeviceFlags,
|
|
125
|
+
work: Callable[[DeviceEndpoint, AppServices, DeviceSettings], _P],
|
|
126
|
+
human: Callable[[_P], str],
|
|
127
|
+
) -> None:
|
|
128
|
+
"""Like :func:`run_device_command`, for work that needs the endpoint and services themselves.
|
|
129
|
+
|
|
130
|
+
The ARP trick opens the print server only after this host maps the borrowed address, so it
|
|
131
|
+
takes the endpoint (``flags.host`` is that address) instead of a ready server.
|
|
132
|
+
"""
|
|
133
|
+
mode = OutputMode.of(as_json=flags.as_json, as_json_bare=flags.as_json_bare)
|
|
134
|
+
cli_ctx = get_cli_context(ctx)
|
|
135
|
+
config = require_config(ctx, cli_ctx)
|
|
136
|
+
with lib_log_rich.runtime.bind(job_id=f"cli-{command.value}", extra={"command": command.value}):
|
|
137
|
+
try:
|
|
138
|
+
settings = load_device_settings(config)
|
|
139
|
+
endpoint = resolve_endpoint(settings, host=flags.host, timeout=flags.timeout)
|
|
140
|
+
payload = work(endpoint, cli_ctx.services, settings)
|
|
141
|
+
except _HANDLED as error:
|
|
142
|
+
report_failure(error, command=command, mode=mode)
|
|
143
|
+
ctx.exit(exit_code_for(error))
|
|
144
|
+
if mode.machine:
|
|
145
|
+
write_envelope(Envelope.of(command=command, payload=payload), mode=mode)
|
|
146
|
+
else:
|
|
147
|
+
safe_console.echo(human(payload))
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class DeviceFlags(BaseModel, frozen=True, extra="forbid"):
|
|
151
|
+
"""The four options :func:`device_options` adds, as one value."""
|
|
152
|
+
|
|
153
|
+
host: str | None
|
|
154
|
+
timeout: float | None
|
|
155
|
+
as_json: bool
|
|
156
|
+
as_json_bare: bool
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
__all__ = [
|
|
160
|
+
"DeviceFlags",
|
|
161
|
+
"device_options",
|
|
162
|
+
"exit_code_for",
|
|
163
|
+
"run_device_command",
|
|
164
|
+
"run_endpoint_command",
|
|
165
|
+
"session_options",
|
|
166
|
+
]
|