otto-sh 0.1.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.
Files changed (88) hide show
  1. otto_sh-0.1.0/LICENSE +21 -0
  2. otto_sh-0.1.0/PKG-INFO +229 -0
  3. otto_sh-0.1.0/README.md +189 -0
  4. otto_sh-0.1.0/pyproject.toml +164 -0
  5. otto_sh-0.1.0/src/otto/__init__.py +6 -0
  6. otto_sh-0.1.0/src/otto/__main__.py +3 -0
  7. otto_sh-0.1.0/src/otto/cli/__init__.py +4 -0
  8. otto_sh-0.1.0/src/otto/cli/banner.py +10 -0
  9. otto_sh-0.1.0/src/otto/cli/callbacks.py +14 -0
  10. otto_sh-0.1.0/src/otto/cli/cov.py +174 -0
  11. otto_sh-0.1.0/src/otto/cli/host.py +189 -0
  12. otto_sh-0.1.0/src/otto/cli/login.py +0 -0
  13. otto_sh-0.1.0/src/otto/cli/main.py +440 -0
  14. otto_sh-0.1.0/src/otto/cli/monitor.py +169 -0
  15. otto_sh-0.1.0/src/otto/cli/reservation.py +78 -0
  16. otto_sh-0.1.0/src/otto/cli/run.py +166 -0
  17. otto_sh-0.1.0/src/otto/cli/test.py +583 -0
  18. otto_sh-0.1.0/src/otto/configmodule/__init__.py +111 -0
  19. otto_sh-0.1.0/src/otto/configmodule/completion_cache.py +519 -0
  20. otto_sh-0.1.0/src/otto/configmodule/completion_stubs.py +95 -0
  21. otto_sh-0.1.0/src/otto/configmodule/configmodule.py +248 -0
  22. otto_sh-0.1.0/src/otto/configmodule/env.py +115 -0
  23. otto_sh-0.1.0/src/otto/configmodule/lab.py +114 -0
  24. otto_sh-0.1.0/src/otto/configmodule/repo.py +503 -0
  25. otto_sh-0.1.0/src/otto/configmodule/version.py +43 -0
  26. otto_sh-0.1.0/src/otto/console.py +5 -0
  27. otto_sh-0.1.0/src/otto/coverage/__init__.py +18 -0
  28. otto_sh-0.1.0/src/otto/coverage/correlator/__init__.py +5 -0
  29. otto_sh-0.1.0/src/otto/coverage/correlator/lcov_loader.py +115 -0
  30. otto_sh-0.1.0/src/otto/coverage/correlator/merger.py +177 -0
  31. otto_sh-0.1.0/src/otto/coverage/correlator/paths.py +187 -0
  32. otto_sh-0.1.0/src/otto/coverage/fetcher/__init__.py +3 -0
  33. otto_sh-0.1.0/src/otto/coverage/fetcher/remote.py +152 -0
  34. otto_sh-0.1.0/src/otto/coverage/renderer/__init__.py +3 -0
  35. otto_sh-0.1.0/src/otto/coverage/renderer/html_renderer.py +375 -0
  36. otto_sh-0.1.0/src/otto/coverage/renderer/static/report.css +291 -0
  37. otto_sh-0.1.0/src/otto/coverage/renderer/static/report.js +53 -0
  38. otto_sh-0.1.0/src/otto/coverage/renderer/templates/file.html +95 -0
  39. otto_sh-0.1.0/src/otto/coverage/renderer/templates/index.html +85 -0
  40. otto_sh-0.1.0/src/otto/coverage/reporter.py +373 -0
  41. otto_sh-0.1.0/src/otto/coverage/store/__init__.py +3 -0
  42. otto_sh-0.1.0/src/otto/coverage/store/model.py +363 -0
  43. otto_sh-0.1.0/src/otto/host/__init__.py +23 -0
  44. otto_sh-0.1.0/src/otto/host/connections.py +301 -0
  45. otto_sh-0.1.0/src/otto/host/host.py +502 -0
  46. otto_sh-0.1.0/src/otto/host/interact.py +544 -0
  47. otto_sh-0.1.0/src/otto/host/localHost.py +207 -0
  48. otto_sh-0.1.0/src/otto/host/options.py +414 -0
  49. otto_sh-0.1.0/src/otto/host/remoteHost.py +678 -0
  50. otto_sh-0.1.0/src/otto/host/repeat.py +161 -0
  51. otto_sh-0.1.0/src/otto/host/session.py +849 -0
  52. otto_sh-0.1.0/src/otto/host/telnet.py +206 -0
  53. otto_sh-0.1.0/src/otto/host/toolchain.py +74 -0
  54. otto_sh-0.1.0/src/otto/host/toolchain_discovery.py +182 -0
  55. otto_sh-0.1.0/src/otto/host/transfer.py +1225 -0
  56. otto_sh-0.1.0/src/otto/host/transport.py +87 -0
  57. otto_sh-0.1.0/src/otto/logger/__init__.py +9 -0
  58. otto_sh-0.1.0/src/otto/logger/formatters.py +102 -0
  59. otto_sh-0.1.0/src/otto/logger/levels.py +9 -0
  60. otto_sh-0.1.0/src/otto/logger/logger.py +240 -0
  61. otto_sh-0.1.0/src/otto/monitor/__init__.py +32 -0
  62. otto_sh-0.1.0/src/otto/monitor/collector.py +686 -0
  63. otto_sh-0.1.0/src/otto/monitor/events.py +61 -0
  64. otto_sh-0.1.0/src/otto/monitor/parsers.py +347 -0
  65. otto_sh-0.1.0/src/otto/monitor/server.py +284 -0
  66. otto_sh-0.1.0/src/otto/monitor/static/dashboard.css +373 -0
  67. otto_sh-0.1.0/src/otto/monitor/static/dashboard.html +80 -0
  68. otto_sh-0.1.0/src/otto/monitor/static/dashboard.js +786 -0
  69. otto_sh-0.1.0/src/otto/monitor/static/plotly.min.js +8 -0
  70. otto_sh-0.1.0/src/otto/params.py +37 -0
  71. otto_sh-0.1.0/src/otto/reservations/__init__.py +133 -0
  72. otto_sh-0.1.0/src/otto/reservations/check.py +154 -0
  73. otto_sh-0.1.0/src/otto/reservations/identity.py +56 -0
  74. otto_sh-0.1.0/src/otto/reservations/json_backend.py +167 -0
  75. otto_sh-0.1.0/src/otto/reservations/null_backend.py +24 -0
  76. otto_sh-0.1.0/src/otto/reservations/protocol.py +110 -0
  77. otto_sh-0.1.0/src/otto/storage/__init__.py +14 -0
  78. otto_sh-0.1.0/src/otto/storage/factory.py +173 -0
  79. otto_sh-0.1.0/src/otto/storage/json_repository.py +201 -0
  80. otto_sh-0.1.0/src/otto/storage/protocol.py +74 -0
  81. otto_sh-0.1.0/src/otto/suite/__init__.py +7 -0
  82. otto_sh-0.1.0/src/otto/suite/expect.py +149 -0
  83. otto_sh-0.1.0/src/otto/suite/plugin.py +251 -0
  84. otto_sh-0.1.0/src/otto/suite/register.py +135 -0
  85. otto_sh-0.1.0/src/otto/suite/suite.py +441 -0
  86. otto_sh-0.1.0/src/otto/suite/timeout.py +55 -0
  87. otto_sh-0.1.0/src/otto/utils.py +131 -0
  88. otto_sh-0.1.0/src/otto/version.py +7 -0
otto_sh-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Chris Collins
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.
otto_sh-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,229 @@
1
+ Metadata-Version: 2.4
2
+ Name: otto-sh
3
+ Version: 0.1.0
4
+ Summary: Our Trusty Testing Orchestrator - framework and CLI for deploying, testing, and monitoring software across remote hosts.
5
+ Keywords: testing,automation,orchestration,ssh,telnet,asyncio,cli,remote-execution
6
+ Author: Chris Collins
7
+ Author-email: Chris Collins <chriscoll93@gmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Console
12
+ Classifier: Framework :: AsyncIO
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: POSIX :: Linux
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Topic :: Software Development :: Testing
19
+ Classifier: Topic :: System :: Systems Administration
20
+ Requires-Dist: aioftp>=0.27.2
21
+ Requires-Dist: aiosqlite>=0.21.0
22
+ Requires-Dist: asyncssh>=2.22.0
23
+ Requires-Dist: fastapi>=0.135.1
24
+ Requires-Dist: jinja2>=3.1.0
25
+ Requires-Dist: pytest>=9.0.1
26
+ Requires-Dist: pytest-asyncio>=1.3.0
27
+ Requires-Dist: rich>=14.3.3
28
+ Requires-Dist: sse-starlette>=3.3.3
29
+ Requires-Dist: telnetlib3>=4.0.1
30
+ Requires-Dist: tomli>=2.4.0
31
+ Requires-Dist: typer>=0.24.0
32
+ Requires-Dist: uvicorn>=0.42.0
33
+ Requires-Python: >=3.10
34
+ Project-URL: Homepage, https://github.com/ludachrish3/otto-sh
35
+ Project-URL: Source, https://github.com/ludachrish3/otto-sh
36
+ Project-URL: Issues, https://github.com/ludachrish3/otto-sh/issues
37
+ Project-URL: Documentation, https://otto-sh.readthedocs.io
38
+ Project-URL: Changelog, https://github.com/ludachrish3/otto-sh/blob/main/CHANGELOG.md
39
+ Description-Content-Type: text/markdown
40
+
41
+ # otto
42
+
43
+ **otto** — Our Trusty Testing Orchestrator — is a framework for deploying
44
+ products to remote hosts for testing and validation. It provides a CLI and a
45
+ Python API for running commands on remote systems, transferring files,
46
+ executing test suites, and monitoring host metrics in real time.
47
+
48
+ ## Who is otto for?
49
+
50
+ Otto is a general-purpose tool for developers and testers who need to interact
51
+ with one or more remote machines as part of their workflow — deploying builds,
52
+ validating firmware, running integration tests, or collecting performance
53
+ data.
54
+
55
+ ## Two ways to use otto
56
+
57
+ - **CLI users** — interact with otto through the `otto run`, `otto test`, and
58
+ `otto monitor` commands.
59
+ - **API builders** — import otto's Python packages to build higher-level
60
+ automation on top of hosts, suites, and the monitor.
61
+
62
+ ## Key concepts
63
+
64
+ ### Hosts
65
+
66
+ A **Host** represents a machine otto can talk to. `RemoteHost` connects over
67
+ SSH or Telnet; `LocalHost` runs commands on the local machine without any
68
+ network connection.
69
+
70
+ Both expose the same core interface — `run`, `oneshot`, `send` / `expect`, and
71
+ file-transfer methods (`put`, `get`).
72
+
73
+ `run` executes a command on a host's persistent shell session (state like the
74
+ working directory and environment variables are preserved between calls).
75
+ `oneshot` runs each call independently of the persistent shell and of other
76
+ concurrent `oneshot` calls, making it safe to fan out via `asyncio.gather()`.
77
+
78
+ ### Labs
79
+
80
+ Hosts can be reached through intermediate **hops** — SSH jump hosts that otto
81
+ tunnels through automatically. Hops can be chained for multi-hop paths
82
+ (`otto -> hop1 -> hop2 -> target`). All file transfer protocols (SCP, SFTP,
83
+ FTP, netcat) work through hops. Set the `hop` field in a host's JSON
84
+ definition or use `--hop` on the CLI.
85
+
86
+ A **Lab** is a JSON file that describes a set of hosts and their topology.
87
+ Otto loads labs at startup (via `--lab` or the `OTTO_LAB` environment
88
+ variable) and makes every host available to instructions, test suites, and
89
+ the monitor. Multiple labs can be merged by passing several names.
90
+
91
+ ### Repos and settings
92
+
93
+ Otto discovers your project through a `.otto/settings.toml` file at the
94
+ repository root. This file tells otto where to find your Python libraries,
95
+ test suites, run instructions, and lab data:
96
+
97
+ ```toml
98
+ name = "my_project"
99
+ version = "1.0.0"
100
+
101
+ labs = ["${sutDir}/../lab_data"]
102
+ libs = ["${sutDir}/pylib"]
103
+ tests = ["${sutDir}/tests"]
104
+ init = ["my_instructions"]
105
+ ```
106
+
107
+ `${sutDir}` is replaced with the repository root at load time. The `init` list
108
+ names Python modules that otto imports at startup — this is where you
109
+ register your instructions and shared options.
110
+
111
+ ### Instructions (`otto run`)
112
+
113
+ An **instruction** is an async function decorated with `@instruction()` that
114
+ becomes a subcommand of `otto run`. Instructions have full access to the
115
+ lab's hosts and can accept their own CLI options via Typer annotations:
116
+
117
+ ```python
118
+ from otto.cli.run import instruction
119
+ from otto.configmodule.configmodule import all_hosts
120
+ from otto.logger import getOttoLogger
121
+
122
+ logger = getOttoLogger()
123
+
124
+ @instruction()
125
+ async def deploy(
126
+ debug: Annotated[bool, typer.Option("--field/--debug")] = False,
127
+ ):
128
+ for host in all_hosts():
129
+ await host.run(["echo deploying", "make install"])
130
+ logger.info("Done")
131
+ ```
132
+
133
+ ```bash
134
+ otto -l my_lab run deploy --debug
135
+ ```
136
+
137
+ ### Test suites (`otto test`)
138
+
139
+ A **suite** is a class that extends `OttoSuite` and is registered with the
140
+ `@register_suite()` decorator. Each suite becomes a subcommand of `otto test`.
141
+ Suites can define their own `Options` dataclass whose fields appear as CLI
142
+ flags:
143
+
144
+ ```python
145
+ from dataclasses import dataclass
146
+ from typing import Annotated
147
+
148
+ import typer
149
+ from otto.suite import OttoSuite, register_suite
150
+
151
+ @dataclass
152
+ class _Options:
153
+ firmware: Annotated[str, typer.Option(help="Firmware version.")] = "latest"
154
+
155
+ @register_suite()
156
+ class TestDevice(OttoSuite[_Options]):
157
+ Options = _Options
158
+
159
+ async def test_device_reachable(self, suite_options: _Options) -> None:
160
+ self.logger.info(f"firmware={suite_options.firmware}")
161
+ assert True
162
+ ```
163
+
164
+ ```bash
165
+ otto -l my_lab test TestDevice --firmware 2.1
166
+ otto test --iterations 10 --threshold 95 TestDevice
167
+ ```
168
+
169
+ Suites support pytest markers (`timeout`, `retry`, `parametrize`,
170
+ `integration`), non-fatal assertions via `self.expect()`, per-test artifact
171
+ directories, and built-in monitoring.
172
+
173
+ Both suites and instructions accept an options dataclass. For flags that are
174
+ repo-wide (device type, lab environment, etc.), define a single `RepoOptions`
175
+ dataclass in your pylib and inherit it from both sides.
176
+
177
+ ### Monitor (`otto monitor`)
178
+
179
+ The monitor collects live performance metrics (CPU, memory, disk, network)
180
+ from one or more hosts and serves an interactive web dashboard:
181
+
182
+ ```bash
183
+ otto -l my_lab monitor # all hosts, default 5 s interval
184
+ otto monitor host1,host2 --interval 2.0 # specific hosts, faster polling
185
+ otto monitor --db metrics.db # persist data for later viewing
186
+ otto monitor --file metrics.db # replay saved data
187
+ ```
188
+
189
+ Monitoring can also be started from within a test suite using
190
+ `await self.startMonitor(hosts=...)` and `await self.stopMonitor()`.
191
+
192
+ ## Quick-start example
193
+
194
+ 1. **Set the environment** — point otto at your repo and lab:
195
+
196
+ ```bash
197
+ export OTTO_SUT_DIRS=/path/to/my_project
198
+ otto --lab my_lab --list-hosts # verify hosts are visible
199
+ ```
200
+
201
+ 2. **Run an instruction:**
202
+
203
+ ```bash
204
+ otto -l my_lab run deploy --debug
205
+ ```
206
+
207
+ 3. **Run a test suite:**
208
+
209
+ ```bash
210
+ otto -l my_lab test TestDevice --firmware 2.1
211
+ ```
212
+
213
+ 4. **Monitor hosts:**
214
+
215
+ ```bash
216
+ otto -l my_lab monitor
217
+ ```
218
+
219
+ ## Documentation
220
+
221
+ Hosted documentation: [otto-sh.readthedocs.io](https://otto-sh.readthedocs.io).
222
+
223
+ The same content lives under `docs/` and can be built locally with `make docs`
224
+ — the generated HTML is written to `docs/_build/html/`. Key entry points:
225
+
226
+ - `docs/getting-started.md` — installation and first steps
227
+ - `docs/guide/` — detailed guides for each CLI command
228
+ - `docs/cookbook/` — recipes for common asyncio patterns
229
+ - `docs/api/` — full API reference for all otto packages
@@ -0,0 +1,189 @@
1
+ # otto
2
+
3
+ **otto** — Our Trusty Testing Orchestrator — is a framework for deploying
4
+ products to remote hosts for testing and validation. It provides a CLI and a
5
+ Python API for running commands on remote systems, transferring files,
6
+ executing test suites, and monitoring host metrics in real time.
7
+
8
+ ## Who is otto for?
9
+
10
+ Otto is a general-purpose tool for developers and testers who need to interact
11
+ with one or more remote machines as part of their workflow — deploying builds,
12
+ validating firmware, running integration tests, or collecting performance
13
+ data.
14
+
15
+ ## Two ways to use otto
16
+
17
+ - **CLI users** — interact with otto through the `otto run`, `otto test`, and
18
+ `otto monitor` commands.
19
+ - **API builders** — import otto's Python packages to build higher-level
20
+ automation on top of hosts, suites, and the monitor.
21
+
22
+ ## Key concepts
23
+
24
+ ### Hosts
25
+
26
+ A **Host** represents a machine otto can talk to. `RemoteHost` connects over
27
+ SSH or Telnet; `LocalHost` runs commands on the local machine without any
28
+ network connection.
29
+
30
+ Both expose the same core interface — `run`, `oneshot`, `send` / `expect`, and
31
+ file-transfer methods (`put`, `get`).
32
+
33
+ `run` executes a command on a host's persistent shell session (state like the
34
+ working directory and environment variables are preserved between calls).
35
+ `oneshot` runs each call independently of the persistent shell and of other
36
+ concurrent `oneshot` calls, making it safe to fan out via `asyncio.gather()`.
37
+
38
+ ### Labs
39
+
40
+ Hosts can be reached through intermediate **hops** — SSH jump hosts that otto
41
+ tunnels through automatically. Hops can be chained for multi-hop paths
42
+ (`otto -> hop1 -> hop2 -> target`). All file transfer protocols (SCP, SFTP,
43
+ FTP, netcat) work through hops. Set the `hop` field in a host's JSON
44
+ definition or use `--hop` on the CLI.
45
+
46
+ A **Lab** is a JSON file that describes a set of hosts and their topology.
47
+ Otto loads labs at startup (via `--lab` or the `OTTO_LAB` environment
48
+ variable) and makes every host available to instructions, test suites, and
49
+ the monitor. Multiple labs can be merged by passing several names.
50
+
51
+ ### Repos and settings
52
+
53
+ Otto discovers your project through a `.otto/settings.toml` file at the
54
+ repository root. This file tells otto where to find your Python libraries,
55
+ test suites, run instructions, and lab data:
56
+
57
+ ```toml
58
+ name = "my_project"
59
+ version = "1.0.0"
60
+
61
+ labs = ["${sutDir}/../lab_data"]
62
+ libs = ["${sutDir}/pylib"]
63
+ tests = ["${sutDir}/tests"]
64
+ init = ["my_instructions"]
65
+ ```
66
+
67
+ `${sutDir}` is replaced with the repository root at load time. The `init` list
68
+ names Python modules that otto imports at startup — this is where you
69
+ register your instructions and shared options.
70
+
71
+ ### Instructions (`otto run`)
72
+
73
+ An **instruction** is an async function decorated with `@instruction()` that
74
+ becomes a subcommand of `otto run`. Instructions have full access to the
75
+ lab's hosts and can accept their own CLI options via Typer annotations:
76
+
77
+ ```python
78
+ from otto.cli.run import instruction
79
+ from otto.configmodule.configmodule import all_hosts
80
+ from otto.logger import getOttoLogger
81
+
82
+ logger = getOttoLogger()
83
+
84
+ @instruction()
85
+ async def deploy(
86
+ debug: Annotated[bool, typer.Option("--field/--debug")] = False,
87
+ ):
88
+ for host in all_hosts():
89
+ await host.run(["echo deploying", "make install"])
90
+ logger.info("Done")
91
+ ```
92
+
93
+ ```bash
94
+ otto -l my_lab run deploy --debug
95
+ ```
96
+
97
+ ### Test suites (`otto test`)
98
+
99
+ A **suite** is a class that extends `OttoSuite` and is registered with the
100
+ `@register_suite()` decorator. Each suite becomes a subcommand of `otto test`.
101
+ Suites can define their own `Options` dataclass whose fields appear as CLI
102
+ flags:
103
+
104
+ ```python
105
+ from dataclasses import dataclass
106
+ from typing import Annotated
107
+
108
+ import typer
109
+ from otto.suite import OttoSuite, register_suite
110
+
111
+ @dataclass
112
+ class _Options:
113
+ firmware: Annotated[str, typer.Option(help="Firmware version.")] = "latest"
114
+
115
+ @register_suite()
116
+ class TestDevice(OttoSuite[_Options]):
117
+ Options = _Options
118
+
119
+ async def test_device_reachable(self, suite_options: _Options) -> None:
120
+ self.logger.info(f"firmware={suite_options.firmware}")
121
+ assert True
122
+ ```
123
+
124
+ ```bash
125
+ otto -l my_lab test TestDevice --firmware 2.1
126
+ otto test --iterations 10 --threshold 95 TestDevice
127
+ ```
128
+
129
+ Suites support pytest markers (`timeout`, `retry`, `parametrize`,
130
+ `integration`), non-fatal assertions via `self.expect()`, per-test artifact
131
+ directories, and built-in monitoring.
132
+
133
+ Both suites and instructions accept an options dataclass. For flags that are
134
+ repo-wide (device type, lab environment, etc.), define a single `RepoOptions`
135
+ dataclass in your pylib and inherit it from both sides.
136
+
137
+ ### Monitor (`otto monitor`)
138
+
139
+ The monitor collects live performance metrics (CPU, memory, disk, network)
140
+ from one or more hosts and serves an interactive web dashboard:
141
+
142
+ ```bash
143
+ otto -l my_lab monitor # all hosts, default 5 s interval
144
+ otto monitor host1,host2 --interval 2.0 # specific hosts, faster polling
145
+ otto monitor --db metrics.db # persist data for later viewing
146
+ otto monitor --file metrics.db # replay saved data
147
+ ```
148
+
149
+ Monitoring can also be started from within a test suite using
150
+ `await self.startMonitor(hosts=...)` and `await self.stopMonitor()`.
151
+
152
+ ## Quick-start example
153
+
154
+ 1. **Set the environment** — point otto at your repo and lab:
155
+
156
+ ```bash
157
+ export OTTO_SUT_DIRS=/path/to/my_project
158
+ otto --lab my_lab --list-hosts # verify hosts are visible
159
+ ```
160
+
161
+ 2. **Run an instruction:**
162
+
163
+ ```bash
164
+ otto -l my_lab run deploy --debug
165
+ ```
166
+
167
+ 3. **Run a test suite:**
168
+
169
+ ```bash
170
+ otto -l my_lab test TestDevice --firmware 2.1
171
+ ```
172
+
173
+ 4. **Monitor hosts:**
174
+
175
+ ```bash
176
+ otto -l my_lab monitor
177
+ ```
178
+
179
+ ## Documentation
180
+
181
+ Hosted documentation: [otto-sh.readthedocs.io](https://otto-sh.readthedocs.io).
182
+
183
+ The same content lives under `docs/` and can be built locally with `make docs`
184
+ — the generated HTML is written to `docs/_build/html/`. Key entry points:
185
+
186
+ - `docs/getting-started.md` — installation and first steps
187
+ - `docs/guide/` — detailed guides for each CLI command
188
+ - `docs/cookbook/` — recipes for common asyncio patterns
189
+ - `docs/api/` — full API reference for all otto packages
@@ -0,0 +1,164 @@
1
+ [project]
2
+ name = "otto-sh"
3
+ version = "0.1.0"
4
+ description = "Our Trusty Testing Orchestrator - framework and CLI for deploying, testing, and monitoring software across remote hosts."
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = "MIT"
8
+ license-files = ["LICENSE"]
9
+ authors = [
10
+ { name = "Chris Collins", email = "chriscoll93@gmail.com" },
11
+ ]
12
+ keywords = [
13
+ "testing",
14
+ "automation",
15
+ "orchestration",
16
+ "ssh",
17
+ "telnet",
18
+ "asyncio",
19
+ "cli",
20
+ "remote-execution",
21
+ ]
22
+ classifiers = [
23
+ "Development Status :: 3 - Alpha",
24
+ "Environment :: Console",
25
+ "Framework :: AsyncIO",
26
+ "Intended Audience :: Developers",
27
+ "License :: OSI Approved :: MIT License",
28
+ "Operating System :: POSIX :: Linux",
29
+ "Programming Language :: Python :: 3",
30
+ "Programming Language :: Python :: 3.10",
31
+ "Topic :: Software Development :: Testing",
32
+ "Topic :: System :: Systems Administration",
33
+ ]
34
+ dependencies = [
35
+ "aioftp>=0.27.2",
36
+ "aiosqlite>=0.21.0",
37
+ "asyncssh>=2.22.0",
38
+ "fastapi>=0.135.1",
39
+ "jinja2>=3.1.0",
40
+ # pytest + pytest-asyncio are runtime deps because otto imports user
41
+ # test files at runtime (orchestration introspection), and those files
42
+ # commonly `import pytest` / use `pytest_asyncio` markers.
43
+ "pytest>=9.0.1",
44
+ "pytest-asyncio>=1.3.0",
45
+ "rich>=14.3.3",
46
+ "sse-starlette>=3.3.3",
47
+ "telnetlib3>=4.0.1",
48
+ "tomli>=2.4.0",
49
+ "typer>=0.24.0",
50
+ "uvicorn>=0.42.0",
51
+ ]
52
+
53
+ [project.scripts]
54
+ otto = "otto:app"
55
+
56
+ [project.urls]
57
+ Homepage = "https://github.com/ludachrish3/otto-sh"
58
+ Source = "https://github.com/ludachrish3/otto-sh"
59
+ Issues = "https://github.com/ludachrish3/otto-sh/issues"
60
+ Documentation = "https://otto-sh.readthedocs.io"
61
+ Changelog = "https://github.com/ludachrish3/otto-sh/blob/main/CHANGELOG.md"
62
+
63
+ [build-system]
64
+ requires = [
65
+ "uv_build>=0.10.0",
66
+ "uv_build<0.11.0",
67
+ ]
68
+ build-backend = "uv_build"
69
+
70
+ # Dist name is `otto-sh` (for PyPI); the import package stays `otto`, so the
71
+ # build backend needs to be pointed at src/otto/ explicitly.
72
+ [tool.uv.build-backend]
73
+ module-name = "otto"
74
+
75
+ [dependency-groups]
76
+ dev = [
77
+ "bump-my-version>=1.3.0",
78
+ "sphinx-immaterial",
79
+ "myst-parser>=4.0.1",
80
+ "py-spy>=0.4.1",
81
+ "pyinstrument>=5.1.2",
82
+ "pytest-cov>=7.0.0",
83
+ "pytest-xdist>=3.8.0",
84
+ "ruff>=0.14.7",
85
+ "sphinx>=8.0",
86
+ "sphinx-autodoc-typehints>=2.0",
87
+ "ty==0.0.31",
88
+ ]
89
+
90
+ [tool.pyright]
91
+ typeCheckingMode = "strict"
92
+ ignore = [
93
+ #"tests", # pytest does not play nice with type checking
94
+ ]
95
+
96
+ # ty is being trialled alongside pyright; see todo/ty_vs_pylance_eval.md.
97
+ # Pinned hard because ty uses 0.0.x versioning with breaking changes between
98
+ # any two releases.
99
+ [tool.ty.environment]
100
+ python-version = "3.10"
101
+ root = ["./src"]
102
+
103
+ [tool.ty.src]
104
+ include = ["src"]
105
+ exclude = ["tests", "typings", "docs", "build", "dist"]
106
+
107
+ # All rules at error — there is no pyright-style --strict preset, so we
108
+ # escalate everything uniformly. Rules that turn out to be too noisy should
109
+ # be demoted individually here (not silenced globally) so the decision is
110
+ # visible in diff review.
111
+ [tool.ty.rules]
112
+ all = "error"
113
+
114
+ [tool.pytest.ini_options]
115
+ testpaths = [
116
+ "tests/unit",
117
+ "tests/integration",
118
+ ]
119
+ pythonpath = ["src", "."]
120
+ log_cli = true
121
+ log_level = "INFO"
122
+ log_format = "%(asctime)s [%(levelname)s] %(message)s"
123
+ log_cli_date_format = "%Y-%m-%d %H:%M:%S.%f"
124
+ addopts = "--doctest-modules --cov=otto --cov-context=test --cov-report term-missing --cov-report html -n auto --dist loadgroup"
125
+ doctest_optionflags = ["NORMALIZE_WHITESPACE", "ELLIPSIS"]
126
+ asyncio_default_fixture_loop_scope = "function"
127
+ asyncio_mode = "strict"
128
+ markers = [
129
+ "integration: requires running Vagrant VMs (deselect with -m 'not integration')",
130
+ "hops: multi-hop integration tests requiring 3 Vagrant VMs (deselect with -m 'not hops')",
131
+ "timeout(seconds): fail the test if it exceeds the given number of seconds",
132
+ "retry(n): retry the test up to n times on failure before reporting it as failed",
133
+ ]
134
+
135
+ [tool.bumpversion]
136
+ current_version = "0.1.0"
137
+ commit = true
138
+ tag = true
139
+ tag_name = "v{new_version}"
140
+ tag_message = "Release v{new_version}"
141
+ message = "chore(release): bump version {current_version} -> {new_version}"
142
+
143
+ [[tool.bumpversion.files]]
144
+ filename = "pyproject.toml"
145
+ search = 'version = "{current_version}"'
146
+ replace = 'version = "{new_version}"'
147
+
148
+ [[tool.bumpversion.files]]
149
+ filename = "CHANGELOG.md"
150
+ search = "## [Unreleased]"
151
+ replace = "## [Unreleased]\n\n## [{new_version}] - {now:%Y-%m-%d}"
152
+
153
+ [[tool.bumpversion.files]]
154
+ filename = "CHANGELOG.md"
155
+ search = "[Unreleased]: https://github.com/ludachrish3/otto-sh/compare/v{current_version}...HEAD"
156
+ replace = "[Unreleased]: https://github.com/ludachrish3/otto-sh/compare/v{new_version}...HEAD\n[{new_version}]: https://github.com/ludachrish3/otto-sh/compare/v{current_version}...v{new_version}"
157
+
158
+ # Keep the lock entry for otto-sh in sync. Without this, every `uv run`
159
+ # after a bump detects pyproject ↔ lock drift and re-syncs the lock,
160
+ # dirtying the tree and blocking the next `make release`.
161
+ [[tool.bumpversion.files]]
162
+ filename = "uv.lock"
163
+ search = "name = \"otto-sh\"\nversion = \"{current_version}\""
164
+ replace = "name = \"otto-sh\"\nversion = \"{new_version}\""
@@ -0,0 +1,6 @@
1
+ # Ensure that the OttoLogger is initialized correctly before anything else happens
2
+ from otto.logger import getOttoLogger as getOttoLogger
3
+
4
+ getOttoLogger()
5
+
6
+ from otto.cli import app
@@ -0,0 +1,3 @@
1
+ from otto import app
2
+
3
+ app()
@@ -0,0 +1,4 @@
1
+ """otto is a test suite and instruction framework."""
2
+ from .main import (
3
+ app as app,
4
+ )
@@ -0,0 +1,10 @@
1
+ from rich.text import (
2
+ Text,
3
+ )
4
+
5
+ banner = Text(style="bold green", no_wrap=True)
6
+ banner.append(r' __ __ ' '\n')
7
+ banner.append(r' ____ / /_/ /_____ ' '\n')
8
+ banner.append(r' / __ \/ __/ __/ __ \ ' '\n')
9
+ banner.append(r' / /_/ / /_/ /_/ /_/ / ' '\n')
10
+ banner.append(r' \____/\__/\__/\____/ ' '\n')
@@ -0,0 +1,14 @@
1
+ """Shared Typer option callbacks used across multiple CLI subapps."""
2
+
3
+ from ..configmodule import getConfigModule
4
+
5
+
6
+ def list_hosts_callback(value: bool) -> None:
7
+ """Print all host IDs from the current lab and exit."""
8
+ if not value:
9
+ return
10
+ lab = getConfigModule().lab
11
+ print()
12
+ for host in lab.hosts:
13
+ print(f'\u2022 {host}')
14
+ print()