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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,6 @@
1
+ """NthLayer Bench — Tier 3 operator TUI.
2
+
3
+ Textual-based terminal UI for case management, situation board,
4
+ and system health monitoring. Communicates with nthlayer-core
5
+ exclusively via HTTP API.
6
+ """
@@ -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,2 @@
1
+ [console_scripts]
2
+ nthlayer-bench = nthlayer_bench.cli:main
@@ -0,0 +1,8 @@
1
+ nthlayer-common>=0.1.8
2
+ textual>=1.0
3
+ httpx>=0.27
4
+
5
+ [dev]
6
+ pytest>=8.2
7
+ pytest-asyncio>=0.23
8
+ ruff>=0.8
@@ -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"