exosphere-cli 3.0.0__tar.gz → 3.0.1__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 (62) hide show
  1. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/PKG-INFO +9 -5
  2. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/README.md +8 -4
  3. exosphere_cli-3.0.1/pyproject.toml +131 -0
  4. exosphere_cli-3.0.0/pyproject.toml → exosphere_cli-3.0.1/pyproject.toml.orig +2 -2
  5. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/src/exosphere/commands/config.py +4 -4
  6. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/src/exosphere/commands/host.py +11 -10
  7. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/src/exosphere/commands/inventory.py +31 -28
  8. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/src/exosphere/commands/report.py +7 -7
  9. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/src/exosphere/commands/utils.py +4 -3
  10. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/src/exosphere/commands/version.py +4 -1
  11. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/src/exosphere/config.py +33 -10
  12. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/src/exosphere/data.py +4 -4
  13. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/src/exosphere/editing.py +4 -1
  14. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/src/exosphere/fspaths.py +1 -1
  15. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/src/exosphere/inventory.py +12 -6
  16. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/src/exosphere/main.py +14 -7
  17. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/src/exosphere/migrations.py +12 -9
  18. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/src/exosphere/objects.py +97 -21
  19. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/src/exosphere/pipelining.py +3 -3
  20. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/src/exosphere/providers/api.py +2 -2
  21. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/src/exosphere/providers/debian.py +2 -1
  22. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/src/exosphere/providers/factory.py +5 -3
  23. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/src/exosphere/providers/freebsd.py +9 -9
  24. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/src/exosphere/providers/redhat.py +11 -12
  25. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/src/exosphere/repl.py +14 -3
  26. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/src/exosphere/reporting.py +2 -2
  27. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/src/exosphere/ui/app.py +18 -11
  28. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/src/exosphere/ui/dashboard.py +15 -10
  29. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/src/exosphere/ui/elements.py +5 -3
  30. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/src/exosphere/ui/inventory.py +15 -10
  31. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/src/exosphere/ui/logs.py +11 -9
  32. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/COPYRIGHT +0 -0
  33. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/LICENSE +0 -0
  34. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/LICENSES/Apache-2.0.txt +0 -0
  35. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/LICENSES/CC-BY-4.0.txt +0 -0
  36. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/LICENSES/MIT.txt +0 -0
  37. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/LICENSES/OFL-1.1.txt +0 -0
  38. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/src/exosphere/__init__.py +0 -0
  39. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/src/exosphere/cli.py +0 -0
  40. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/src/exosphere/commands/__init__.py +0 -0
  41. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/src/exosphere/commands/connections.py +0 -0
  42. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/src/exosphere/commands/sudo.py +0 -0
  43. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/src/exosphere/commands/ui.py +0 -0
  44. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/src/exosphere/context.py +0 -0
  45. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/src/exosphere/database.py +0 -0
  46. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/src/exosphere/errors.py +0 -0
  47. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/src/exosphere/providers/__init__.py +0 -0
  48. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/src/exosphere/providers/openbsd.py +0 -0
  49. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/src/exosphere/runners.py +0 -0
  50. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/src/exosphere/schema/__init__.py +0 -0
  51. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/src/exosphere/schema/host-report.schema.json +0 -0
  52. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/src/exosphere/security.py +0 -0
  53. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/src/exosphere/setup/__init__.py +0 -0
  54. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/src/exosphere/setup/detect.py +0 -0
  55. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/src/exosphere/templates/report.html.j2 +0 -0
  56. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/src/exosphere/templates/report.md.j2 +0 -0
  57. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/src/exosphere/templates/report.txt.j2 +0 -0
  58. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/src/exosphere/ui/__init__.py +0 -0
  59. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/src/exosphere/ui/context.py +0 -0
  60. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/src/exosphere/ui/messages.py +0 -0
  61. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/src/exosphere/ui/palette.py +0 -0
  62. {exosphere_cli-3.0.0 → exosphere_cli-3.0.1}/src/exosphere/ui/style.tcss +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: exosphere-cli
3
- Version: 3.0.0
3
+ Version: 3.0.1
4
4
  Summary: CLI/TUI driven patch reporting for remote Unix-like systems.
5
5
  Author: Alexandre Gauthier
6
6
  Author-email: Alexandre Gauthier <alex@underwares.org>
@@ -149,7 +149,6 @@ uv run textual run --dev -c exosphere ui
149
149
  Congratulations! Editing any of the `.tcss` files in the `ui/` directory will
150
150
  reflect changes immediately.
151
151
 
152
-
153
152
  ### Documentation Editing Quick Start
154
153
 
155
154
  To edit the documentation, you can use the following commands:
@@ -188,8 +187,10 @@ tasks are defined in the `pyproject.toml` file under the `[tool.poe.tasks]` tabl
188
187
 
189
188
  | path | description |
190
189
  | ---- | ----------- |
190
+ | `LICENSES/` | Full text of every license used by the project and its assets |
191
191
  | `docs/` | Sphinx documentation source tree |
192
192
  | `docs/source/_ext/` | Custom Sphinx extensions for the project |
193
+ | `docs/source/changelog/` | Per-release changelog entries (markdown) and their assets |
193
194
  | `examples/` | Example configuration files and reports |
194
195
  | `scripts/` | Utilitarian scripts for dev and maintenance |
195
196
  | `src/` | Main source code for the application |
@@ -224,13 +225,16 @@ Paths below are relative to `src/exosphere/` unless otherwise noted.
224
225
  | `context.py` | Context management for shared state across commands and UI |
225
226
  | `data.py` | Data models and structures for serialization and exchange |
226
227
  | `database.py` | Cache system for serialization |
228
+ | `editing.py` | External editor helper module, shared between CLI and UI |
227
229
  | `errors.py` | Exception classes and general error messages |
230
+ | `fspaths.py` | Platform-appropriate filesystem paths module, for state |
228
231
  | `inventory.py` | Inventory management subsystem |
229
232
  | `migrations.py` | Cache format migration processes |
230
233
  | `objects.py` | Main objects for representing Hosts, and most of the relevant logic |
231
234
  | `pipelining.py` | SSH pipelining implementation, including reaper thread |
232
235
  | `repl.py` | REPL module for interactive CLI usage |
233
236
  | `reporting.py` | Reporting subsystem, including templates and formatters |
237
+ | `runners.py` | SSH runners module, handles setting up remote POSIX environment |
234
238
  | `security.py` | Sudo management subsystem, including policy and utilities |
235
239
 
236
240
  Generally, most of the things Exosphere does to hosts (including connection management
@@ -245,9 +249,9 @@ Paths below are relative to `src/exosphere/` unless otherwise noted.
245
249
  | `ui/app.py` | Main Textual application class and entry point for the UI |
246
250
  | `ui/context.py` | UI Context management for shared state across UI components |
247
251
  | `ui/elements.py` | Shared UI elements, including task runners |
248
- | `ui/dashboard.py` | Dashboard view implementation |
249
- | `ui/inventory.py` | Inventory view implementation |
250
- | `ui/logs.py` | Logs view implementation |
252
+ | `ui/dashboard.py` | Dashboard screen implementation |
253
+ | `ui/inventory.py` | Inventory screen implementation |
254
+ | `ui/logs.py` | Logs screen implementation |
251
255
  | `ui/messages.py` | Screen refresh and message passing system |
252
256
  | `ui/palette.py` | Command palette providers and implementations |
253
257
 
@@ -108,7 +108,6 @@ uv run textual run --dev -c exosphere ui
108
108
  Congratulations! Editing any of the `.tcss` files in the `ui/` directory will
109
109
  reflect changes immediately.
110
110
 
111
-
112
111
  ### Documentation Editing Quick Start
113
112
 
114
113
  To edit the documentation, you can use the following commands:
@@ -147,8 +146,10 @@ tasks are defined in the `pyproject.toml` file under the `[tool.poe.tasks]` tabl
147
146
 
148
147
  | path | description |
149
148
  | ---- | ----------- |
149
+ | `LICENSES/` | Full text of every license used by the project and its assets |
150
150
  | `docs/` | Sphinx documentation source tree |
151
151
  | `docs/source/_ext/` | Custom Sphinx extensions for the project |
152
+ | `docs/source/changelog/` | Per-release changelog entries (markdown) and their assets |
152
153
  | `examples/` | Example configuration files and reports |
153
154
  | `scripts/` | Utilitarian scripts for dev and maintenance |
154
155
  | `src/` | Main source code for the application |
@@ -183,13 +184,16 @@ Paths below are relative to `src/exosphere/` unless otherwise noted.
183
184
  | `context.py` | Context management for shared state across commands and UI |
184
185
  | `data.py` | Data models and structures for serialization and exchange |
185
186
  | `database.py` | Cache system for serialization |
187
+ | `editing.py` | External editor helper module, shared between CLI and UI |
186
188
  | `errors.py` | Exception classes and general error messages |
189
+ | `fspaths.py` | Platform-appropriate filesystem paths module, for state |
187
190
  | `inventory.py` | Inventory management subsystem |
188
191
  | `migrations.py` | Cache format migration processes |
189
192
  | `objects.py` | Main objects for representing Hosts, and most of the relevant logic |
190
193
  | `pipelining.py` | SSH pipelining implementation, including reaper thread |
191
194
  | `repl.py` | REPL module for interactive CLI usage |
192
195
  | `reporting.py` | Reporting subsystem, including templates and formatters |
196
+ | `runners.py` | SSH runners module, handles setting up remote POSIX environment |
193
197
  | `security.py` | Sudo management subsystem, including policy and utilities |
194
198
 
195
199
  Generally, most of the things Exosphere does to hosts (including connection management
@@ -204,9 +208,9 @@ Paths below are relative to `src/exosphere/` unless otherwise noted.
204
208
  | `ui/app.py` | Main Textual application class and entry point for the UI |
205
209
  | `ui/context.py` | UI Context management for shared state across UI components |
206
210
  | `ui/elements.py` | Shared UI elements, including task runners |
207
- | `ui/dashboard.py` | Dashboard view implementation |
208
- | `ui/inventory.py` | Inventory view implementation |
209
- | `ui/logs.py` | Logs view implementation |
211
+ | `ui/dashboard.py` | Dashboard screen implementation |
212
+ | `ui/inventory.py` | Inventory screen implementation |
213
+ | `ui/logs.py` | Logs screen implementation |
210
214
  | `ui/messages.py` | Screen refresh and message passing system |
211
215
  | `ui/palette.py` | Command palette providers and implementations |
212
216
 
@@ -0,0 +1,131 @@
1
+ [project]
2
+ name = "exosphere-cli"
3
+ version = "3.0.1"
4
+ description = "CLI/TUI driven patch reporting for remote Unix-like systems."
5
+ readme = "README.md"
6
+ requires-python = ">=3.13"
7
+ classifiers = [
8
+ "Development Status :: 5 - Production/Stable",
9
+ "Environment :: Console",
10
+ "Intended Audience :: System Administrators",
11
+ "Natural Language :: English",
12
+ "Operating System :: OS Independent",
13
+ "Programming Language :: Python :: 3.13",
14
+ "Programming Language :: Python :: 3.14",
15
+ "Topic :: System :: Monitoring",
16
+ "Topic :: System :: Systems Administration",
17
+ "Topic :: Utilities",
18
+ "Typing :: Typed",
19
+ ]
20
+ license = "MIT"
21
+ license-files = [
22
+ "LICENSE",
23
+ "LICENSES/*",
24
+ "COPYRIGHT",
25
+ ]
26
+ dependencies = [
27
+ "fabric>=3.2.2",
28
+ "textual>=6.7.0",
29
+ "pyyaml>=6.0.3",
30
+ "platformdirs>=4.3.8",
31
+ "prompt-toolkit>=3.0.51",
32
+ "jinja2>=3.1.6",
33
+ "packaging>=24.0",
34
+ "rich>=14.1.0",
35
+ "cyclopts>=4.16.1",
36
+ "filelock>=3.29.4",
37
+ "pydantic>=2.12",
38
+ ]
39
+
40
+ [[project.authors]]
41
+ name = "Alexandre Gauthier"
42
+ email = "alex@underwares.org"
43
+
44
+ [project.urls]
45
+ homepage = "https://exosphere.readthedocs.io"
46
+ repository = "https://github.com/mrdaemon/exosphere"
47
+ issues = "https://github.com/mrdaemon/exosphere/issues"
48
+
49
+ [project.scripts]
50
+ exosphere = "exosphere.main:main"
51
+
52
+ [dependency-groups]
53
+ dev = [
54
+ "fabric[pytest]>=3.2.2",
55
+ "jsonschema>=4.25.1",
56
+ "poethepoet>=0.33.1",
57
+ "pyright>=1.1.400",
58
+ "pytest>=8.4.2",
59
+ "pytest-asyncio>=1.2.0",
60
+ "pytest-cov>=6.1.1",
61
+ "pytest-json-ctrf>=0.3.5",
62
+ "pytest-mock>=3.14.1",
63
+ "sphinx-rtd-theme>=3.0.2",
64
+ "ruff>=0.15.0",
65
+ "sphinx>=8.2.3",
66
+ "sphinx-autobuild>=2024.10.3",
67
+ "sphinx-lint>=1.0.0",
68
+ "sphinx-tabs>=3.4.7",
69
+ "sphinxcontrib-spelling>=8.0.2",
70
+ "textual-dev>=1.7.0",
71
+ "codespell>=2.4.0",
72
+ "myst-parser[linkify]>=4.0.0",
73
+ ]
74
+
75
+ [build-system]
76
+ requires = ["uv_build>=0.7.19,<0.13.0"]
77
+ build-backend = "uv_build"
78
+
79
+ [tool.uv.build-backend]
80
+ module-name = "exosphere"
81
+
82
+ [tool.pytest.ini_options]
83
+ testpaths = ["tests"]
84
+
85
+ [tool.codespell]
86
+ skip = "*.lock,*.woff,*.woff2,*.ttf,*.ttx,*.svg,*.png,*.gif,*.html,*.htm,docs/source/_static"
87
+
88
+ [tool.poe.tasks]
89
+ lint = "ruff check src tests scripts"
90
+ isort = "ruff check --select I --fix src tests scripts"
91
+ isort-check = "ruff check --select I src tests scripts --diff"
92
+ typecheck = "pyright src tests scripts"
93
+ ruff-format = "ruff format src tests scripts"
94
+ ruff-format-check = "ruff format --check src tests scripts --diff"
95
+ spellcheck = "codespell src tests scripts docs/source README.md"
96
+ spellcheck-fix = "codespell --write-changes src tests scripts docs/source README.md"
97
+ format = [
98
+ "ruff-format",
99
+ "isort",
100
+ ]
101
+ format-check = [
102
+ "ruff-format-check",
103
+ "isort-check",
104
+ ]
105
+ check = [
106
+ "format-check",
107
+ "lint",
108
+ "typecheck",
109
+ "spellcheck",
110
+ ]
111
+ test = "pytest -v --ctrf .tests_report.json"
112
+ coverage = "pytest --cov-report term-missing --cov-report html --cov=src tests/"
113
+ docs-build = "sphinx-build -b html -W --keep-going docs/source docs/build/html"
114
+ docs-lint = "sphinx-lint docs/source"
115
+ docs-spellcheck = "sphinx-build -b spelling -W --keep-going docs/source docs/build"
116
+ docs-linkcheck = "sphinx-build -b linkcheck docs/source docs/build/linkcheck"
117
+ docs = [
118
+ "docs-lint",
119
+ "docs-spellcheck",
120
+ "docs-build",
121
+ ]
122
+ docs-serve = "sphinx-autobuild -b html docs/source docs/build/html --port 8000 --open-browser"
123
+ docs-mkreports = "python scripts/generate_example_reports.py"
124
+ demo = "vhs scripts/demo.tape"
125
+ release-preflight = "python scripts/release_preflight.py"
126
+ release-check = [
127
+ "check",
128
+ "test",
129
+ "docs",
130
+ "release-preflight",
131
+ ]
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "exosphere-cli"
3
- version = "3.0.0"
3
+ version = "3.0.1"
4
4
  description = "CLI/TUI driven patch reporting for remote Unix-like systems."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -73,7 +73,7 @@ issues = "https://github.com/mrdaemon/exosphere/issues"
73
73
  exosphere = "exosphere.main:main"
74
74
 
75
75
  [build-system]
76
- requires = ["uv_build>=0.7.19,<0.12.0"]
76
+ requires = ["uv_build>=0.7.19,<0.13.0"]
77
77
  build-backend = "uv_build"
78
78
 
79
79
  [tool.uv.build-backend]
@@ -40,7 +40,7 @@ def show(
40
40
  Displays the current configuration options, or the value of a specific option
41
41
  if specified.
42
42
 
43
- If `--full` is specified, it will show the entire configuration structure,
43
+ If ``--full`` is specified, it will show the entire configuration structure,
44
44
  including the inventory, beyond just the "options" section.
45
45
 
46
46
  Parameters
@@ -150,9 +150,9 @@ def diff(
150
150
  file can exclusively contain the options you want to change.
151
151
 
152
152
  This command allows you to see exactly what has been changed, optionally
153
- in its context, using the `--full` option.
153
+ in its context, using the ``--full`` option.
154
154
 
155
- For a full config dump, use the `show` command instead.
155
+ For a full config dump, use the ``show`` command instead.
156
156
 
157
157
  Parameters
158
158
  ----------
@@ -264,7 +264,7 @@ def edit(
264
264
 
265
265
  try:
266
266
  validate_config(target)
267
- except Exception as e:
267
+ except (ValueError, OSError) as e:
268
268
  err_console.print(f"[red]Configuration is invalid:[/red]\n{e}")
269
269
  if Confirm.ask("Re-open editor to fix?", default=True):
270
270
  continue
@@ -22,6 +22,7 @@ from exosphere.commands.utils import (
22
22
  err_console,
23
23
  save_inventory_state,
24
24
  )
25
+ from exosphere.errors import DataRefreshError
25
26
  from exosphere.objects import Host
26
27
 
27
28
  # Simple spinner layout for long or pre-tasks
@@ -232,10 +233,10 @@ def discover(host: HostArg, /) -> int:
232
233
  progress.add_task(f"Discovering platform for '{host.name}'", total=None)
233
234
  try:
234
235
  host.discover()
235
- except Exception as e:
236
+ except DataRefreshError as e:
236
237
  progress.console.print(
237
238
  Panel.fit(
238
- f"{str(e)}",
239
+ f"{e!s}",
239
240
  title="[red]Error[/red]",
240
241
  style="red",
241
242
  title_align="left",
@@ -265,7 +266,7 @@ def refresh(
265
266
  This command retrieves the host by name from the inventory
266
267
  and refreshes its state and available updates.
267
268
 
268
- If `--sync` is specified, the package repositories will also be
269
+ If ``--sync`` is specified, the package repositories will also be
269
270
  synchronized remotely.
270
271
 
271
272
  Parameters
@@ -277,17 +278,17 @@ def refresh(
277
278
  discover
278
279
  Also refresh platform information
279
280
  """
280
- with Progress(transient=True, *SPINNER_ARGS) as progress:
281
+ with Progress(*SPINNER_ARGS, transient=True) as progress:
281
282
  if discover:
282
283
  task = progress.add_task(
283
284
  f"Refreshing platform information for '{host.name}'", total=None
284
285
  )
285
286
  try:
286
287
  host.discover()
287
- except Exception as e:
288
+ except DataRefreshError as e:
288
289
  progress.console.print(
289
290
  Panel.fit(
290
- f"{str(e)}",
291
+ f"{e!s}",
291
292
  title="[red]Error[/red]",
292
293
  style="red",
293
294
  title_align="left",
@@ -304,10 +305,10 @@ def refresh(
304
305
  )
305
306
  try:
306
307
  host.sync_repos()
307
- except Exception as e:
308
+ except DataRefreshError as e:
308
309
  progress.console.print(
309
310
  Panel.fit(
310
- f"{str(e)}",
311
+ f"{e!s}",
311
312
  title="[red]Error[/red]",
312
313
  style="red",
313
314
  title_align="left",
@@ -321,10 +322,10 @@ def refresh(
321
322
  task = progress.add_task(f"Refreshing updates for '{host.name}'", total=None)
322
323
  try:
323
324
  host.refresh_updates()
324
- except Exception as e:
325
+ except DataRefreshError as e:
325
326
  progress.console.print(
326
327
  Panel.fit(
327
- f"{str(e)}",
328
+ f"{e!s}",
328
329
  title="[red]Error[/red]",
329
330
  style="red",
330
331
  title_align="left",
@@ -27,7 +27,7 @@ from exosphere.commands.utils import (
27
27
  save_inventory_state,
28
28
  )
29
29
  from exosphere.inventory import FilterMode, Inventory, SortField
30
- from exosphere.objects import HostOperation
30
+ from exosphere.objects import Host, HostOperation
31
31
 
32
32
  # Constants for display
33
33
  ERROR_STYLE = {
@@ -122,11 +122,11 @@ def refresh(
122
122
  Connects to hosts in the inventory and retrieves pending package
123
123
  updates.
124
124
 
125
- If `--discover` is specified, the platform information (Operating
125
+ If ``--discover`` is specified, the platform information (Operating
126
126
  System flavor, version, package manager) will also be refreshed.
127
127
  Also refreshes the online status in the process.
128
128
 
129
- If `--sync` is specified, the package repositories will also be
129
+ If ``--sync`` is specified, the package repositories will also be
130
130
  synchronized remotely.
131
131
 
132
132
  Synchronizing the package repositories involves invoking whatever mechanism
@@ -135,7 +135,7 @@ def refresh(
135
135
  with a handful of slow hosts.
136
136
 
137
137
  By default, only the progress bar is shown during the operation.
138
- If `--verbose` is specified, the name and completion status of each host
138
+ If ``--verbose`` is specified, the name and completion status of each host
139
139
  will be shown in real time.
140
140
 
141
141
  Parameters
@@ -274,7 +274,7 @@ def ping(*names: HostArg) -> int:
274
274
  error_count += 1
275
275
  if exc:
276
276
  progress.console.print(
277
- f" Host [bold]{host.name}[/bold]: [bold red]ERROR[/bold red] - {str(exc)}",
277
+ f" Host [bold]{host.name}[/bold]: [bold red]ERROR[/bold red] - {exc!s}",
278
278
  )
279
279
  else:
280
280
  progress.console.print(
@@ -342,18 +342,18 @@ def status(
342
342
  online status and whether or not the data is stale.
343
343
 
344
344
  Output can be filtered to show only hosts with pending updates
345
- (`--updates-only`) or only those with pending security updates
346
- (`--security-only`). These two filters are mutually exclusive.
345
+ (``--updates-only``) or only those with pending security updates
346
+ (``--security-only``). These two filters are mutually exclusive.
347
347
 
348
- Output can also be sorted by any column with `--sort`, optionally
349
- reversed with `--reverse`. Sorting by 'version' groups hosts by
348
+ Output can also be sorted by any column with ``--sort``, optionally
349
+ reversed with ``--reverse``. Sorting by 'version' groups hosts by
350
350
  flavor first, since versions are not comparable across flavors.
351
351
 
352
352
  When sorting by any column other than name, hosts with unknown
353
353
  or unsupported values for that column will be grouped together at the
354
354
  end.
355
355
 
356
- Use `--full` to include extra columns, such as the host description.
356
+ Use ``--full`` to include extra columns, such as the host description.
357
357
 
358
358
  No matches when filtering will exit with code 3.
359
359
 
@@ -368,7 +368,7 @@ def status(
368
368
  sort
369
369
  Sort the table by the given column
370
370
  reverse
371
- Reverse the sort order (requires `--sort`)
371
+ Reverse the sort order (requires ``--sort``)
372
372
  full
373
373
  Show additional columns, including host descriptions
374
374
  """
@@ -420,12 +420,22 @@ def status(
420
420
  caption_justify="right",
421
421
  )
422
422
 
423
+ def get_platform_value(host: Host, value: str | None) -> str:
424
+ """
425
+ Retrieve display value for host platform.
426
+ Will return appropriate placeholders as fallback.
427
+ """
428
+ if value:
429
+ return value
430
+ elif not host.supported:
431
+ return "[dim](unsupported)[/dim]"
432
+ else:
433
+ return "[dim](undiscovered)[/dim]"
434
+
423
435
  for host in hosts:
424
- # Prepare some rendering data for suffixes and placeholders
436
+ # Prepare some rendering data for suffixes
425
437
  stale_suffix = " [dim]*[/dim]" if host.is_stale else ""
426
438
  reboot_suffix = " [red]![/red]" if host.needs_reboot else ""
427
- undiscovered_status = "[dim](undiscovered)[/dim]"
428
- unsupported_status = "[dim](unsupported)[/dim]"
429
439
  empty_placeholder = "[dim]—[/dim]"
430
440
 
431
441
  # Prepare table row data
@@ -446,22 +456,12 @@ def status(
446
456
  "[bold green]Online[/bold green]" if host.online else "[red]Offline[/red]"
447
457
  ) + reboot_suffix
448
458
 
449
- # Helper function to get platform info with appropriate
450
- # handling for unsupported and undiscovered hosts
451
- def get_platform_info(value):
452
- if value:
453
- return value
454
- elif not host.supported:
455
- return unsupported_status
456
- else:
457
- return undiscovered_status
458
-
459
459
  # Construct table row for host
460
460
  row = [
461
461
  host.name,
462
- get_platform_info(host.os),
463
- get_platform_info(host.flavor),
464
- get_platform_info(host.version),
462
+ get_platform_value(host, host.os),
463
+ get_platform_value(host, host.flavor),
464
+ get_platform_value(host, host.version),
465
465
  updates,
466
466
  security_updates,
467
467
  online_status,
@@ -537,7 +537,10 @@ def clear(
537
537
 
538
538
  try:
539
539
  inventory.clear_state()
540
- except Exception as e:
540
+ except Exception as e: # noqa: BLE001
541
+ # clear_state wraps cache errors as RuntimeError, but also
542
+ # re-runs init_all() afterwards, which reopens the full config
543
+ # and host construction surfaces, so this is broad on purpose.
541
544
  err_console.print(
542
545
  Panel.fit(
543
546
  f"[bold red]Error clearing inventory state:[/bold red] {e}",
@@ -102,8 +102,8 @@ def generate(
102
102
  the report will include all hosts in the inventory.
103
103
 
104
104
  It can further be narrowed to hosts with pending updates
105
- (`--updates-only`) or pending security updates
106
- (`--security-updates-only`), which are mutually exclusive.
105
+ (``--updates-only``) or pending security updates
106
+ (``--security-updates-only``), which are mutually exclusive.
107
107
 
108
108
  Note: Undiscovered or unsupported hosts are excluded from the report.
109
109
 
@@ -120,7 +120,7 @@ def generate(
120
120
  security_only
121
121
  Only report security updates
122
122
  tee
123
- Also print report to stdout (requires `--output`)
123
+ Also print report to stdout (requires ``--output``)
124
124
  quiet
125
125
  Suppress informational messages
126
126
  navigation
@@ -200,7 +200,7 @@ def generate(
200
200
  if output:
201
201
  try:
202
202
  output.write_text(content, encoding="utf-8")
203
- except Exception as e:
203
+ except (OSError, UnicodeEncodeError) as e:
204
204
  err_console.print(f"[red]Failed to write to {output}: {e}[/red]")
205
205
  return 2 # Application error
206
206
 
@@ -241,14 +241,14 @@ def schema(
241
241
  Show or write the JSON Schema for the current version of Exosphere
242
242
 
243
243
  Emits the JSON Schema (draft-07) describing the structure produced
244
- by `report generate --format json`, for the currently running
244
+ by ``report generate --format json``, for the currently running
245
245
  version of Exosphere.
246
246
 
247
247
  This allows anyone to easily get an overview of the structure,
248
248
  validate and integrate, offline, without a local source tree or
249
249
  access to the online documentation.
250
250
 
251
- By default the schema is printed to stdout. Use `--output` to
251
+ By default the schema is printed to stdout. Use ``--output`` to
252
252
  write it to a file instead.
253
253
 
254
254
  Parameters
@@ -267,7 +267,7 @@ def schema(
267
267
  if output:
268
268
  try:
269
269
  output.write_text(content, encoding="utf-8")
270
- except Exception as e:
270
+ except (OSError, UnicodeEncodeError) as e:
271
271
  err_console.print(f"[red]Failed to write to {output}: {e}[/red]")
272
272
  return 2 # Application error
273
273
 
@@ -358,7 +358,7 @@ def run_task_with_progress(
358
358
  else:
359
359
  skipped = []
360
360
 
361
- with Progress(transient=transient, *progress_args) as progress:
361
+ with Progress(*progress_args, transient=transient) as progress:
362
362
  task = progress.add_task(task_description, total=len(hosts))
363
363
 
364
364
  # Surface skipped hosts up front, if any. It is easier to do
@@ -381,7 +381,7 @@ def run_task_with_progress(
381
381
  if exc:
382
382
  if immediate_error_display:
383
383
  progress.console.print(
384
- f"{operation.label}: [red]{str(exc)}[/red]",
384
+ f"{operation.label}: [red]{exc!s}[/red]",
385
385
  )
386
386
 
387
387
  if collect_errors:
@@ -418,7 +418,8 @@ def save_inventory_state() -> None:
418
418
  try:
419
419
  inventory.save_state()
420
420
  progress.stop_task(task)
421
- except Exception as e:
421
+ except Exception as e: # noqa: BLE001
422
+ # Persistence failure is terminal for the caller either way
422
423
  logger.error("Error saving inventory: %s", e)
423
424
  progress.stop_task(task)
424
425
  progress.console.print(
@@ -131,6 +131,9 @@ def check(
131
131
  f"[red]Error:[/red] Unexpected response from PyPI API (missing key: {e})"
132
132
  )
133
133
  return 2 # Application error
134
- except Exception as e:
134
+ except Exception as e: # noqa: BLE001
135
+ # Tail catch for any unexpected failures, covers json garbage,
136
+ # network issues, or anything else under the sun.
137
+ # "Is there an update" shouldn't just raise at runtime.
135
138
  err_console.print(f"[red]Error:[/red] Failed to check for updates: {e}")
136
139
  return 2 # Application error