nthlayer-bench 1.0.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.
- nthlayer_bench-1.0.0/PKG-INFO +13 -0
- nthlayer_bench-1.0.0/pyproject.toml +38 -0
- nthlayer_bench-1.0.0/setup.cfg +4 -0
- nthlayer_bench-1.0.0/src/nthlayer_bench/__init__.py +6 -0
- nthlayer_bench-1.0.0/src/nthlayer_bench/app.py +86 -0
- nthlayer_bench-1.0.0/src/nthlayer_bench/cli.py +21 -0
- nthlayer_bench-1.0.0/src/nthlayer_bench.egg-info/PKG-INFO +13 -0
- nthlayer_bench-1.0.0/src/nthlayer_bench.egg-info/SOURCES.txt +11 -0
- nthlayer_bench-1.0.0/src/nthlayer_bench.egg-info/dependency_links.txt +1 -0
- nthlayer_bench-1.0.0/src/nthlayer_bench.egg-info/entry_points.txt +2 -0
- nthlayer_bench-1.0.0/src/nthlayer_bench.egg-info/requires.txt +8 -0
- nthlayer_bench-1.0.0/src/nthlayer_bench.egg-info/top_level.txt +1 -0
- nthlayer_bench-1.0.0/tests/test_app.py +65 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nthlayer-bench
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: NthLayer Bench — Tier 3 operator TUI for case management and situation awareness
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: nthlayer-common>=0.1.8
|
|
8
|
+
Requires-Dist: textual>=1.0
|
|
9
|
+
Requires-Dist: httpx>=0.27
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: pytest>=8.2; extra == "dev"
|
|
12
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
13
|
+
Requires-Dist: ruff>=0.8; extra == "dev"
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "nthlayer-bench"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "NthLayer Bench — Tier 3 operator TUI for case management and situation awareness"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"nthlayer-common>=0.1.8",
|
|
13
|
+
"textual>=1.0",
|
|
14
|
+
"httpx>=0.27",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.optional-dependencies]
|
|
18
|
+
dev = [
|
|
19
|
+
"pytest>=8.2",
|
|
20
|
+
"pytest-asyncio>=0.23",
|
|
21
|
+
"ruff>=0.8",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.scripts]
|
|
25
|
+
nthlayer-bench = "nthlayer_bench.cli:main"
|
|
26
|
+
|
|
27
|
+
[tool.setuptools.packages.find]
|
|
28
|
+
where = ["src"]
|
|
29
|
+
|
|
30
|
+
[tool.pytest.ini_options]
|
|
31
|
+
asyncio_mode = "auto"
|
|
32
|
+
|
|
33
|
+
[tool.ruff]
|
|
34
|
+
target-version = "py311"
|
|
35
|
+
line-length = 100
|
|
36
|
+
|
|
37
|
+
[tool.uv.sources]
|
|
38
|
+
nthlayer-common = { path = "../nthlayer-common", editable = true }
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""NthLayer Bench Textual application.
|
|
2
|
+
|
|
3
|
+
Single-screen TUI that connects to core's HTTP API and displays
|
|
4
|
+
connection status. Foundation for situation board, case bench,
|
|
5
|
+
and case detail screens (Phase 4).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
from textual.app import App, ComposeResult
|
|
12
|
+
from textual.containers import Container
|
|
13
|
+
from textual.widgets import Footer, Header, Static
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ConnectionStatus(Static):
|
|
17
|
+
"""Displays connection status to nthlayer-core."""
|
|
18
|
+
|
|
19
|
+
CONNECTED = "[green]● Connected[/green]"
|
|
20
|
+
DISCONNECTED = "[red]● Disconnected[/red] — Core unreachable"
|
|
21
|
+
DEGRADED = "[yellow]● Degraded[/yellow] — Data may be stale"
|
|
22
|
+
|
|
23
|
+
def __init__(self, core_url: str) -> None:
|
|
24
|
+
super().__init__(self.DISCONNECTED)
|
|
25
|
+
self.core_url = core_url.rstrip("/")
|
|
26
|
+
self._connected = False
|
|
27
|
+
|
|
28
|
+
def on_mount(self) -> None:
|
|
29
|
+
self.set_interval(5.0, self._check_health)
|
|
30
|
+
# Check immediately on mount
|
|
31
|
+
self.call_later(self._check_health)
|
|
32
|
+
|
|
33
|
+
async def _check_health(self) -> None:
|
|
34
|
+
try:
|
|
35
|
+
async with httpx.AsyncClient(timeout=5.0) as client:
|
|
36
|
+
resp = await client.get(f"{self.core_url}/health")
|
|
37
|
+
if resp.status_code == 200:
|
|
38
|
+
self._connected = True
|
|
39
|
+
self.update(self.CONNECTED)
|
|
40
|
+
else:
|
|
41
|
+
self._connected = False
|
|
42
|
+
self.update(self.DEGRADED)
|
|
43
|
+
except (httpx.RequestError, OSError):
|
|
44
|
+
self._connected = False
|
|
45
|
+
self.update(self.DISCONNECTED)
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def is_connected(self) -> bool:
|
|
49
|
+
return self._connected
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class BenchApp(App):
|
|
53
|
+
"""NthLayer Bench — operator terminal UI."""
|
|
54
|
+
|
|
55
|
+
TITLE = "NthLayer Bench"
|
|
56
|
+
CSS = """
|
|
57
|
+
#status-bar {
|
|
58
|
+
dock: top;
|
|
59
|
+
height: 3;
|
|
60
|
+
padding: 1;
|
|
61
|
+
background: $surface;
|
|
62
|
+
}
|
|
63
|
+
#main {
|
|
64
|
+
padding: 1;
|
|
65
|
+
}
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
BINDINGS = [
|
|
69
|
+
("q", "quit", "Quit"),
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
def __init__(self, core_url: str = "http://localhost:8000") -> None:
|
|
73
|
+
super().__init__()
|
|
74
|
+
self.core_url = core_url
|
|
75
|
+
|
|
76
|
+
def compose(self) -> ComposeResult:
|
|
77
|
+
yield Header()
|
|
78
|
+
yield Container(
|
|
79
|
+
ConnectionStatus(self.core_url),
|
|
80
|
+
id="status-bar",
|
|
81
|
+
)
|
|
82
|
+
yield Container(
|
|
83
|
+
Static("NthLayer Bench v1.5.0a1\n\nPhase 4 screens coming soon: Situation Board, Case Bench, Case Detail"),
|
|
84
|
+
id="main",
|
|
85
|
+
)
|
|
86
|
+
yield Footer()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""NthLayer Bench CLI — start the operator TUI."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def main():
|
|
7
|
+
parser = argparse.ArgumentParser(description="NthLayer Bench — operator TUI")
|
|
8
|
+
parser.add_argument("-V", "--version", action="version", version="%(prog)s 1.5.0a1")
|
|
9
|
+
parser.add_argument("--core-url", default="http://localhost:8000",
|
|
10
|
+
help="Core API URL (default: http://localhost:8000)")
|
|
11
|
+
|
|
12
|
+
args = parser.parse_args()
|
|
13
|
+
|
|
14
|
+
from nthlayer_bench.app import BenchApp
|
|
15
|
+
|
|
16
|
+
app = BenchApp(core_url=args.core_url)
|
|
17
|
+
app.run()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
if __name__ == "__main__":
|
|
21
|
+
main()
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nthlayer-bench
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: NthLayer Bench — Tier 3 operator TUI for case management and situation awareness
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: nthlayer-common>=0.1.8
|
|
8
|
+
Requires-Dist: textual>=1.0
|
|
9
|
+
Requires-Dist: httpx>=0.27
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: pytest>=8.2; extra == "dev"
|
|
12
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
13
|
+
Requires-Dist: ruff>=0.8; extra == "dev"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
pyproject.toml
|
|
2
|
+
src/nthlayer_bench/__init__.py
|
|
3
|
+
src/nthlayer_bench/app.py
|
|
4
|
+
src/nthlayer_bench/cli.py
|
|
5
|
+
src/nthlayer_bench.egg-info/PKG-INFO
|
|
6
|
+
src/nthlayer_bench.egg-info/SOURCES.txt
|
|
7
|
+
src/nthlayer_bench.egg-info/dependency_links.txt
|
|
8
|
+
src/nthlayer_bench.egg-info/entry_points.txt
|
|
9
|
+
src/nthlayer_bench.egg-info/requires.txt
|
|
10
|
+
src/nthlayer_bench.egg-info/top_level.txt
|
|
11
|
+
tests/test_app.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
nthlayer_bench
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Tests for the Bench Textual application."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from unittest.mock import AsyncMock, patch
|
|
5
|
+
|
|
6
|
+
from nthlayer_bench.app import BenchApp, ConnectionStatus
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestConnectionStatus:
|
|
10
|
+
def test_initial_state_disconnected(self):
|
|
11
|
+
cs = ConnectionStatus("http://localhost:8000")
|
|
12
|
+
assert not cs.is_connected
|
|
13
|
+
|
|
14
|
+
async def test_connected_on_healthy_core(self):
|
|
15
|
+
cs = ConnectionStatus("http://test:8000")
|
|
16
|
+
|
|
17
|
+
mock_response = AsyncMock()
|
|
18
|
+
mock_response.status_code = 200
|
|
19
|
+
|
|
20
|
+
mock_client = AsyncMock()
|
|
21
|
+
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
|
|
22
|
+
mock_client.__aexit__ = AsyncMock(return_value=False)
|
|
23
|
+
mock_client.get = AsyncMock(return_value=mock_response)
|
|
24
|
+
|
|
25
|
+
with patch("nthlayer_bench.app.httpx.AsyncClient", return_value=mock_client):
|
|
26
|
+
await cs._check_health()
|
|
27
|
+
|
|
28
|
+
assert cs.is_connected
|
|
29
|
+
|
|
30
|
+
async def test_degraded_on_non_200_response(self):
|
|
31
|
+
cs = ConnectionStatus("http://test:8000")
|
|
32
|
+
|
|
33
|
+
mock_response = AsyncMock()
|
|
34
|
+
mock_response.status_code = 503
|
|
35
|
+
|
|
36
|
+
mock_client = AsyncMock()
|
|
37
|
+
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
|
|
38
|
+
mock_client.__aexit__ = AsyncMock(return_value=False)
|
|
39
|
+
mock_client.get = AsyncMock(return_value=mock_response)
|
|
40
|
+
|
|
41
|
+
with patch("nthlayer_bench.app.httpx.AsyncClient", return_value=mock_client):
|
|
42
|
+
await cs._check_health()
|
|
43
|
+
|
|
44
|
+
assert not cs.is_connected
|
|
45
|
+
|
|
46
|
+
async def test_disconnected_on_connection_error(self):
|
|
47
|
+
cs = ConnectionStatus("http://test:8000")
|
|
48
|
+
|
|
49
|
+
import httpx
|
|
50
|
+
mock_client = AsyncMock()
|
|
51
|
+
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
|
|
52
|
+
mock_client.__aexit__ = AsyncMock(return_value=False)
|
|
53
|
+
mock_client.get = AsyncMock(side_effect=httpx.ConnectError("refused"))
|
|
54
|
+
|
|
55
|
+
with patch("nthlayer_bench.app.httpx.AsyncClient", return_value=mock_client):
|
|
56
|
+
await cs._check_health()
|
|
57
|
+
|
|
58
|
+
assert not cs.is_connected
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class TestBenchApp:
|
|
62
|
+
def test_app_creates(self):
|
|
63
|
+
app = BenchApp(core_url="http://test:8000")
|
|
64
|
+
assert app.core_url == "http://test:8000"
|
|
65
|
+
assert app.TITLE == "NthLayer Bench"
|