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.
Files changed (100) hide show
  1. sercomm_printserver/__init__.py +101 -0
  2. sercomm_printserver/__init__conf__.py +90 -0
  3. sercomm_printserver/__main__.py +9 -0
  4. sercomm_printserver/adapters/__init__.py +14 -0
  5. sercomm_printserver/adapters/cli/__init__.py +80 -0
  6. sercomm_printserver/adapters/cli/commands/__init__.py +49 -0
  7. sercomm_printserver/adapters/cli/commands/_device.py +166 -0
  8. sercomm_printserver/adapters/cli/commands/arp.py +232 -0
  9. sercomm_printserver/adapters/cli/commands/config.py +527 -0
  10. sercomm_printserver/adapters/cli/commands/device.py +391 -0
  11. sercomm_printserver/adapters/cli/commands/device_config.py +251 -0
  12. sercomm_printserver/adapters/cli/commands/discover.py +81 -0
  13. sercomm_printserver/adapters/cli/commands/info.py +38 -0
  14. sercomm_printserver/adapters/cli/commands/printing.py +137 -0
  15. sercomm_printserver/adapters/cli/config_load.py +242 -0
  16. sercomm_printserver/adapters/cli/constants.py +68 -0
  17. sercomm_printserver/adapters/cli/context.py +188 -0
  18. sercomm_printserver/adapters/cli/envelope.py +93 -0
  19. sercomm_printserver/adapters/cli/exit_codes.py +59 -0
  20. sercomm_printserver/adapters/cli/main.py +123 -0
  21. sercomm_printserver/adapters/cli/py.typed +0 -0
  22. sercomm_printserver/adapters/cli/root.py +166 -0
  23. sercomm_printserver/adapters/cli/safe_console.py +315 -0
  24. sercomm_printserver/adapters/cli/typed_click.py +77 -0
  25. sercomm_printserver/adapters/config/__init__.py +25 -0
  26. sercomm_printserver/adapters/config/defaultconfig.d/40-layered-config.toml +78 -0
  27. sercomm_printserver/adapters/config/defaultconfig.d/50-device.toml +64 -0
  28. sercomm_printserver/adapters/config/defaultconfig.d/90-logging.toml +448 -0
  29. sercomm_printserver/adapters/config/defaultconfig.toml +56 -0
  30. sercomm_printserver/adapters/config/deploy.py +135 -0
  31. sercomm_printserver/adapters/config/device_settings.py +78 -0
  32. sercomm_printserver/adapters/config/display.py +60 -0
  33. sercomm_printserver/adapters/config/loader.py +210 -0
  34. sercomm_printserver/adapters/config/overrides.py +326 -0
  35. sercomm_printserver/adapters/config/py.typed +0 -0
  36. sercomm_printserver/adapters/discovery/__init__.py +7 -0
  37. sercomm_printserver/adapters/discovery/py.typed +0 -0
  38. sercomm_printserver/adapters/discovery/scanner.py +98 -0
  39. sercomm_printserver/adapters/ftp/__init__.py +11 -0
  40. sercomm_printserver/adapters/ftp/client.py +459 -0
  41. sercomm_printserver/adapters/ftp/py.typed +0 -0
  42. sercomm_printserver/adapters/logging/__init__.py +13 -0
  43. sercomm_printserver/adapters/logging/py.typed +0 -0
  44. sercomm_printserver/adapters/logging/setup.py +448 -0
  45. sercomm_printserver/adapters/lpd/__init__.py +11 -0
  46. sercomm_printserver/adapters/lpd/client.py +252 -0
  47. sercomm_printserver/adapters/lpd/py.typed +0 -0
  48. sercomm_printserver/adapters/memory/__init__.py +45 -0
  49. sercomm_printserver/adapters/memory/config.py +66 -0
  50. sercomm_printserver/adapters/memory/device.py +190 -0
  51. sercomm_printserver/adapters/memory/discovery.py +67 -0
  52. sercomm_printserver/adapters/memory/logging.py +80 -0
  53. sercomm_printserver/adapters/memory/neighbors.py +58 -0
  54. sercomm_printserver/adapters/neighbors/__init__.py +7 -0
  55. sercomm_printserver/adapters/neighbors/py.typed +0 -0
  56. sercomm_printserver/adapters/neighbors/table.py +102 -0
  57. sercomm_printserver/adapters/py.typed +0 -0
  58. sercomm_printserver/adapters/raw/__init__.py +7 -0
  59. sercomm_printserver/adapters/raw/printer.py +130 -0
  60. sercomm_printserver/adapters/raw/py.typed +0 -0
  61. sercomm_printserver/adapters/snmp/__init__.py +7 -0
  62. sercomm_printserver/adapters/snmp/py.typed +0 -0
  63. sercomm_printserver/adapters/snmp/uptime.py +118 -0
  64. sercomm_printserver/adapters/telnet/__init__.py +7 -0
  65. sercomm_printserver/adapters/telnet/client.py +179 -0
  66. sercomm_printserver/adapters/telnet/iac.py +114 -0
  67. sercomm_printserver/adapters/telnet/py.typed +0 -0
  68. sercomm_printserver/adapters/tftp/__init__.py +11 -0
  69. sercomm_printserver/adapters/tftp/client.py +264 -0
  70. sercomm_printserver/adapters/tftp/py.typed +0 -0
  71. sercomm_printserver/application/__init__.py +28 -0
  72. sercomm_printserver/application/arp_trick.py +199 -0
  73. sercomm_printserver/application/discovery.py +92 -0
  74. sercomm_printserver/application/ports.py +244 -0
  75. sercomm_printserver/application/print_server.py +339 -0
  76. sercomm_printserver/application/py.typed +0 -0
  77. sercomm_printserver/composition/__init__.py +185 -0
  78. sercomm_printserver/composition/py.typed +0 -0
  79. sercomm_printserver/domain/__init__.py +20 -0
  80. sercomm_printserver/domain/config_catalogue.py +351 -0
  81. sercomm_printserver/domain/config_codec.py +146 -0
  82. sercomm_printserver/domain/config_lines.py +89 -0
  83. sercomm_printserver/domain/config_model.py +233 -0
  84. sercomm_printserver/domain/config_reader.py +326 -0
  85. sercomm_printserver/domain/config_types.py +176 -0
  86. sercomm_printserver/domain/config_writer.py +206 -0
  87. sercomm_printserver/domain/device_files.py +53 -0
  88. sercomm_printserver/domain/device_info.py +229 -0
  89. sercomm_printserver/domain/endpoint.py +48 -0
  90. sercomm_printserver/domain/enums.py +55 -0
  91. sercomm_printserver/domain/errors.py +102 -0
  92. sercomm_printserver/domain/port_status.py +174 -0
  93. sercomm_printserver/domain/py.typed +0 -0
  94. sercomm_printserver/entry.py +26 -0
  95. sercomm_printserver/py.typed +0 -0
  96. sercomm_printserver-0.1.0.dist-info/METADATA +355 -0
  97. sercomm_printserver-0.1.0.dist-info/RECORD +100 -0
  98. sercomm_printserver-0.1.0.dist-info/WHEEL +4 -0
  99. sercomm_printserver-0.1.0.dist-info/entry_points.txt +3 -0
  100. 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
+ ]