oaknut-econet-host 13.6.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Robert Smallshire
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,49 @@
1
+ Metadata-Version: 2.4
2
+ Name: oaknut-econet-host
3
+ Version: 13.6.0
4
+ Summary: The econet-host program: a CLI that configures Econet transports and service plug-ins and runs a station.
5
+ Author-email: Robert Smallshire <robert@smallshire.org.uk>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/rob-smallshire/oaknut/tree/master/packages/oaknut-econet-host
8
+ Project-URL: Repository, https://github.com/rob-smallshire/oaknut
9
+ Project-URL: Issues, https://github.com/rob-smallshire/oaknut/issues
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: System Administrators
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: System :: Networking
18
+ Requires-Python: >=3.12
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: oaknut-econet-station>=10.0
22
+ Requires-Dist: oaknut-exception>=10.0
23
+ Requires-Dist: oaknut-extension>=10.0
24
+ Requires-Dist: oaknut-cli>=10.0
25
+ Requires-Dist: click>=8.1.7
26
+ Dynamic: license-file
27
+
28
+ # oaknut-econet-host
29
+
30
+ The **`econet-host`** program — the CLI that turns installed Econet transport
31
+ and service plug-ins into a running station.
32
+
33
+ It depends on the `oaknut-econet-station` library and on Click (a hard
34
+ dependency — this *is* a CLI), and discovers whatever transports
35
+ (`oaknut.econet.transport`) and services (`oaknut.econet.service`) are installed.
36
+
37
+ ```
38
+ econet-host run [--config PATH] # run the station until SIGINT/SIGTERM
39
+ econet-host validate [--config PATH] # build the config, print the plan, exit
40
+ econet-host list-transports # installed transport plug-ins
41
+ econet-host list-services # installed service plug-ins
42
+ econet-host describe <name> # a plug-in's description
43
+ econet-host run --transport aun --station 0.254 --service kvstore # flag-only quick path
44
+ ```
45
+
46
+ Configuration is TOML, discovered (first match wins) from `--config`, then
47
+ `[tool.econet-host]` in a `pyproject.toml`, then `econet-host.toml`, else the
48
+ flags above. See `docs/dev/econet-host.md` for the design and the recommended
49
+ uv-project deployment pattern.
@@ -0,0 +1,22 @@
1
+ # oaknut-econet-host
2
+
3
+ The **`econet-host`** program — the CLI that turns installed Econet transport
4
+ and service plug-ins into a running station.
5
+
6
+ It depends on the `oaknut-econet-station` library and on Click (a hard
7
+ dependency — this *is* a CLI), and discovers whatever transports
8
+ (`oaknut.econet.transport`) and services (`oaknut.econet.service`) are installed.
9
+
10
+ ```
11
+ econet-host run [--config PATH] # run the station until SIGINT/SIGTERM
12
+ econet-host validate [--config PATH] # build the config, print the plan, exit
13
+ econet-host list-transports # installed transport plug-ins
14
+ econet-host list-services # installed service plug-ins
15
+ econet-host describe <name> # a plug-in's description
16
+ econet-host run --transport aun --station 0.254 --service kvstore # flag-only quick path
17
+ ```
18
+
19
+ Configuration is TOML, discovered (first match wins) from `--config`, then
20
+ `[tool.econet-host]` in a `pyproject.toml`, then `econet-host.toml`, else the
21
+ flags above. See `docs/dev/econet-host.md` for the design and the recommended
22
+ uv-project deployment pattern.
@@ -0,0 +1,55 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "oaknut-econet-host"
7
+ dynamic = ["version"]
8
+ authors = [{ name = "Robert Smallshire", email = "robert@smallshire.org.uk" }]
9
+ description = "The econet-host program: a CLI that configures Econet transports and service plug-ins and runs a station."
10
+ readme = "README.md"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ requires-python = ">=3.12"
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: System Administrators",
17
+ "Operating System :: OS Independent",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3 :: Only",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Topic :: System :: Networking",
23
+ ]
24
+ dependencies = [
25
+ "oaknut-econet-station>=10.0",
26
+ "oaknut-exception>=10.0",
27
+ "oaknut-extension>=10.0",
28
+ "oaknut-cli>=10.0",
29
+ "click>=8.1.7",
30
+ ]
31
+
32
+ [project.scripts]
33
+ econet-host = "oaknut.econet.host.cli:cli"
34
+
35
+ [project.urls]
36
+ Homepage = "https://github.com/rob-smallshire/oaknut/tree/master/packages/oaknut-econet-host"
37
+ Repository = "https://github.com/rob-smallshire/oaknut"
38
+ Issues = "https://github.com/rob-smallshire/oaknut/issues"
39
+
40
+ [dependency-groups]
41
+ test = [
42
+ "pytest>=8.0",
43
+ "pytest-asyncio>=0.24",
44
+ ]
45
+ dev = [
46
+ "bump-my-version>=0.28.0",
47
+ "pre-commit>=3.0",
48
+ {include-group = "test"},
49
+ ]
50
+
51
+ [tool.setuptools.dynamic]
52
+ version = { attr = "oaknut.econet.host.__version__" }
53
+
54
+ [tool.setuptools.packages.find]
55
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,12 @@
1
+ """The econet-host host program.
2
+
3
+ A CLI (the ``econet-host`` console script) that configures Econet transports
4
+ and service plug-ins from TOML and runs a :class:`~oaknut.econet.station.Station`.
5
+ The CLI itself lives in :mod:`oaknut.econet.host.cli`.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ __version__ = "13.6.0"
11
+
12
+ __all__: list[str] = []
@@ -0,0 +1,153 @@
1
+ """The ``econet-host`` host CLI: configure transports + services and run."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import contextlib
7
+ import signal
8
+ from pathlib import Path
9
+
10
+ import click
11
+ from oaknut.cli import use_plain_help
12
+ from oaknut.econet.host import __version__
13
+ from oaknut.econet.station import Station, build_station, config_from_flags, load_config
14
+ from oaknut.exception import ConfigurationError, handled_errors
15
+ from oaknut.extension import describe_extension, list_extensions, namespace_for
16
+
17
+ _TRANSPORT_NS = namespace_for("econet.transport")
18
+ _SERVICE_NS = namespace_for("econet.service")
19
+
20
+
21
+ def _print_error(line: str, is_continuation: bool = False) -> None:
22
+ click.secho(line, err=True, fg="yellow" if is_continuation else "red")
23
+
24
+
25
+ class _BoundaryGroup(click.Group):
26
+ """Wraps every command in the oaknut handled_errors boundary, honouring --debug."""
27
+
28
+ def invoke(self, ctx: click.Context):
29
+ debug = bool(ctx.params.get("debug", False))
30
+ with handled_errors(_print_error, debug=debug):
31
+ return super().invoke(ctx)
32
+
33
+
34
+ def _resolve_config(config_path, transport, station, services):
35
+ if config_path is not None:
36
+ return load_config(config_path)
37
+ if transport is not None or station is not None or services:
38
+ if transport is None or station is None:
39
+ raise ConfigurationError("the flag path needs both --transport and --station")
40
+ config = config_from_flags(transport=transport, station=station, services=tuple(services))
41
+ return config, "command-line flags"
42
+ return load_config(None)
43
+
44
+
45
+ @click.group(cls=_BoundaryGroup)
46
+ @click.option("--debug", is_flag=True, help="Show full tracebacks for internal errors.")
47
+ @click.version_option(__version__, prog_name="econet-host")
48
+ def cli(debug: bool) -> None:
49
+ """Host Econet services on a single station."""
50
+
51
+
52
+ _CONFIG = click.option(
53
+ "--config",
54
+ "config_path",
55
+ default=None,
56
+ type=click.Path(exists=True, dir_okay=False, path_type=Path),
57
+ help="Path to a station config TOML file.",
58
+ )
59
+ _TRANSPORT = click.option("--transport", default=None, help="Transport plug-in (flag-only path).")
60
+ _STATION = click.option("--station", default=None, help="Station address, e.g. 0.254 (flag path).")
61
+ _SERVICE = click.option(
62
+ "--service", "services", multiple=True, help="A service plug-in to host (repeatable)."
63
+ )
64
+
65
+
66
+ @cli.command()
67
+ @_CONFIG
68
+ @_TRANSPORT
69
+ @_STATION
70
+ @_SERVICE
71
+ def run(config_path, transport, station, services) -> None:
72
+ """Run the station host until interrupted (SIGINT/SIGTERM)."""
73
+ config, source = _resolve_config(config_path, transport, station, services)
74
+ host = build_station(config)
75
+ click.echo(f"econet-host {host.address} on '{config.transport}' (config: {source})")
76
+ _echo_port_map(host)
77
+ _run_blocking(host)
78
+
79
+
80
+ @cli.command()
81
+ @_CONFIG
82
+ @_TRANSPORT
83
+ @_STATION
84
+ @_SERVICE
85
+ def validate(config_path, transport, station, services) -> None:
86
+ """Parse and build the configuration, print the plan, then exit."""
87
+ config, source = _resolve_config(config_path, transport, station, services)
88
+ host = build_station(config)
89
+ click.echo(f"config source: {source}")
90
+ click.echo(f"station: {host.address}")
91
+ click.echo(f"transport: {config.transport}")
92
+ _echo_port_map(host)
93
+ click.echo("ok")
94
+
95
+
96
+ @cli.command("list-transports")
97
+ def list_transports() -> None:
98
+ """List the installed transport plug-ins."""
99
+ for name in sorted(list_extensions(_TRANSPORT_NS)):
100
+ click.echo(name)
101
+
102
+
103
+ @cli.command("list-services")
104
+ def list_services() -> None:
105
+ """List the installed service plug-ins."""
106
+ for name in sorted(list_extensions(_SERVICE_NS)):
107
+ click.echo(name)
108
+
109
+
110
+ @cli.command()
111
+ @click.argument("name")
112
+ def describe(name: str) -> None:
113
+ """Describe a transport or service plug-in by name."""
114
+ for kind, namespace in (("econet.transport", _TRANSPORT_NS), ("econet.service", _SERVICE_NS)):
115
+ if name in list_extensions(namespace):
116
+ click.echo(describe_extension(kind, namespace, name))
117
+ return
118
+ raise ConfigurationError(f"no transport or service named {name!r}")
119
+
120
+
121
+ def _echo_port_map(host: Station) -> None:
122
+ port_map = host.port_map
123
+ if not port_map:
124
+ click.echo(" (no services)")
125
+ return
126
+ for port in sorted(port_map):
127
+ click.echo(f" &{port:02X} {port_map[port]}")
128
+
129
+
130
+ async def serve_until(host: Station, stop: asyncio.Event) -> None:
131
+ """Open the transport, serve, and shut down gracefully when *stop* is set."""
132
+ await host.transport.open()
133
+ serve_task = asyncio.create_task(host.serve())
134
+ try:
135
+ await stop.wait()
136
+ finally:
137
+ await host.transport.close()
138
+ await serve_task
139
+
140
+
141
+ def _run_blocking(host: Station) -> None:
142
+ async def _main() -> None:
143
+ stop = asyncio.Event()
144
+ loop = asyncio.get_running_loop()
145
+ for sig in (signal.SIGINT, signal.SIGTERM):
146
+ with contextlib.suppress(NotImplementedError):
147
+ loop.add_signal_handler(sig, stop.set)
148
+ await serve_until(host, stop)
149
+
150
+ asyncio.run(_main())
151
+
152
+
153
+ use_plain_help(cli)
@@ -0,0 +1,49 @@
1
+ Metadata-Version: 2.4
2
+ Name: oaknut-econet-host
3
+ Version: 13.6.0
4
+ Summary: The econet-host program: a CLI that configures Econet transports and service plug-ins and runs a station.
5
+ Author-email: Robert Smallshire <robert@smallshire.org.uk>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/rob-smallshire/oaknut/tree/master/packages/oaknut-econet-host
8
+ Project-URL: Repository, https://github.com/rob-smallshire/oaknut
9
+ Project-URL: Issues, https://github.com/rob-smallshire/oaknut/issues
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: System Administrators
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: System :: Networking
18
+ Requires-Python: >=3.12
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: oaknut-econet-station>=10.0
22
+ Requires-Dist: oaknut-exception>=10.0
23
+ Requires-Dist: oaknut-extension>=10.0
24
+ Requires-Dist: oaknut-cli>=10.0
25
+ Requires-Dist: click>=8.1.7
26
+ Dynamic: license-file
27
+
28
+ # oaknut-econet-host
29
+
30
+ The **`econet-host`** program — the CLI that turns installed Econet transport
31
+ and service plug-ins into a running station.
32
+
33
+ It depends on the `oaknut-econet-station` library and on Click (a hard
34
+ dependency — this *is* a CLI), and discovers whatever transports
35
+ (`oaknut.econet.transport`) and services (`oaknut.econet.service`) are installed.
36
+
37
+ ```
38
+ econet-host run [--config PATH] # run the station until SIGINT/SIGTERM
39
+ econet-host validate [--config PATH] # build the config, print the plan, exit
40
+ econet-host list-transports # installed transport plug-ins
41
+ econet-host list-services # installed service plug-ins
42
+ econet-host describe <name> # a plug-in's description
43
+ econet-host run --transport aun --station 0.254 --service kvstore # flag-only quick path
44
+ ```
45
+
46
+ Configuration is TOML, discovered (first match wins) from `--config`, then
47
+ `[tool.econet-host]` in a `pyproject.toml`, then `econet-host.toml`, else the
48
+ flags above. See `docs/dev/econet-host.md` for the design and the recommended
49
+ uv-project deployment pattern.
@@ -0,0 +1,12 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/oaknut/econet/host/__init__.py
5
+ src/oaknut/econet/host/cli.py
6
+ src/oaknut_econet_host.egg-info/PKG-INFO
7
+ src/oaknut_econet_host.egg-info/SOURCES.txt
8
+ src/oaknut_econet_host.egg-info/dependency_links.txt
9
+ src/oaknut_econet_host.egg-info/entry_points.txt
10
+ src/oaknut_econet_host.egg-info/requires.txt
11
+ src/oaknut_econet_host.egg-info/top_level.txt
12
+ tests/test_cli.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ econet-host = oaknut.econet.host.cli:cli
@@ -0,0 +1,5 @@
1
+ oaknut-econet-station>=10.0
2
+ oaknut-exception>=10.0
3
+ oaknut-extension>=10.0
4
+ oaknut-cli>=10.0
5
+ click>=8.1.7
@@ -0,0 +1,66 @@
1
+ """Tests for the econet-host host CLI and the serve lifecycle."""
2
+
3
+ import asyncio
4
+
5
+ from click.testing import CliRunner
6
+ from oaknut.econet.core import Address, TestTransport
7
+ from oaknut.econet.host.cli import cli, serve_until
8
+ from oaknut.econet.station import Station
9
+
10
+
11
+ def test_list_transports_lists_installed_plugins():
12
+ result = CliRunner().invoke(cli, ["list-transports"])
13
+ assert result.exit_code == 0, result.output
14
+ assert "aun" in result.output
15
+ assert "piconet" in result.output
16
+ assert "hat" in result.output
17
+
18
+
19
+ def test_list_services_lists_kvstore():
20
+ result = CliRunner().invoke(cli, ["list-services"])
21
+ assert result.exit_code == 0, result.output
22
+ assert "kvstore" in result.output
23
+
24
+
25
+ def test_describe_a_transport():
26
+ result = CliRunner().invoke(cli, ["describe", "aun"])
27
+ assert result.exit_code == 0, result.output
28
+ assert result.output.strip()
29
+
30
+
31
+ def test_describe_unknown_name_errors():
32
+ result = CliRunner().invoke(cli, ["describe", "no-such-plugin"])
33
+ assert result.exit_code != 0
34
+
35
+
36
+ def test_validate_flag_path():
37
+ result = CliRunner().invoke(
38
+ cli, ["validate", "--transport", "aun", "--station", "0.254", "--service", "kvstore"]
39
+ )
40
+ assert result.exit_code == 0, result.output
41
+ assert "0.254" in result.output
42
+ assert "aun" in result.output
43
+ assert "kvstore" in result.output
44
+ assert "command-line flags" in result.output
45
+
46
+
47
+ def test_validate_unknown_transport_errors():
48
+ result = CliRunner().invoke(cli, ["validate", "--transport", "nope", "--station", "0.254"])
49
+ assert result.exit_code != 0
50
+
51
+
52
+ def test_validate_flag_path_needs_both_transport_and_station():
53
+ result = CliRunner().invoke(cli, ["validate", "--transport", "aun"])
54
+ assert result.exit_code != 0
55
+
56
+
57
+ async def test_serve_until_opens_then_closes_on_stop():
58
+ transport = TestTransport(local_station=Address(0, 254))
59
+ station = Station(transport)
60
+ stop = asyncio.Event()
61
+ task = asyncio.create_task(serve_until(station, stop))
62
+ await asyncio.sleep(0)
63
+ assert transport.is_open
64
+ stop.set()
65
+ await asyncio.wait_for(task, timeout=1.0)
66
+ assert transport.is_closed