refactor: Update component specifications to use 'program' and 'behavior' attributes, enhancing clarity and consistency across the application
This commit is contained in:
@@ -156,17 +156,6 @@ def _summary_from_job(name: str, job: JobSpec, config: object) -> ComponentSumma
|
||||
|
||||
def _summary_from_program(name: str, comp: ProgramSpec, root: Path) -> ComponentSummary:
|
||||
"""Build a ComponentSummary from a ProgramSpec (tools/frontends)."""
|
||||
# Determine behavior
|
||||
is_tool = bool((comp.install and comp.install.path) or comp.tool)
|
||||
is_frontend = bool(comp.build and (comp.build.outputs or comp.build.commands))
|
||||
|
||||
if is_tool:
|
||||
behavior = "tool"
|
||||
elif is_frontend:
|
||||
behavior = "frontend"
|
||||
else:
|
||||
behavior = None
|
||||
|
||||
source = comp.source
|
||||
|
||||
# Infer runner from source directory
|
||||
@@ -179,20 +168,19 @@ def _summary_from_program(name: str, comp: ProgramSpec, root: Path) -> Component
|
||||
runner = "command"
|
||||
|
||||
installed: bool | None = None
|
||||
if comp.install and comp.install.path:
|
||||
alias = comp.install.path.alias or name
|
||||
installed = shutil.which(alias) is not None
|
||||
if comp.source and comp.stack:
|
||||
installed = shutil.which(name) is not None
|
||||
|
||||
return ComponentSummary(
|
||||
id=name,
|
||||
category="program",
|
||||
description=comp.description,
|
||||
behavior=behavior,
|
||||
behavior=comp.behavior,
|
||||
stack=comp.stack,
|
||||
runner=runner,
|
||||
version=comp.tool.version if comp.tool else None,
|
||||
version=comp.version,
|
||||
source=source,
|
||||
system_dependencies=comp.tool.system_dependencies if comp.tool else [],
|
||||
system_dependencies=comp.system_dependencies,
|
||||
installed=installed,
|
||||
)
|
||||
|
||||
@@ -325,29 +313,6 @@ def _program_from_spec(
|
||||
name: str, comp: ProgramSpec, root: Path, config: object | None = None
|
||||
) -> ProgramSummary:
|
||||
"""Build a ProgramSummary from a ProgramSpec."""
|
||||
is_tool = bool((comp.install and comp.install.path) or comp.tool)
|
||||
is_frontend = bool(comp.build and (comp.build.outputs or comp.build.commands))
|
||||
|
||||
if is_tool:
|
||||
behavior = "tool"
|
||||
elif is_frontend:
|
||||
behavior = "frontend"
|
||||
else:
|
||||
behavior = None
|
||||
|
||||
# Derive behavior from backing service/job
|
||||
if behavior is None and config is not None:
|
||||
svc_components = {
|
||||
s.component for s in config.services.values() if s.component
|
||||
}
|
||||
job_components = {
|
||||
j.component for j in config.jobs.values() if j.component
|
||||
}
|
||||
if name in svc_components or name in config.services:
|
||||
behavior = "daemon"
|
||||
elif name in job_components or name in config.jobs:
|
||||
behavior = "tool"
|
||||
|
||||
source = comp.source
|
||||
runner = None
|
||||
if source:
|
||||
@@ -358,24 +323,18 @@ def _program_from_spec(
|
||||
runner = "command"
|
||||
|
||||
installed: bool | None = None
|
||||
if comp.install and comp.install.path:
|
||||
alias = comp.install.path.alias or name
|
||||
installed = shutil.which(alias) is not None
|
||||
elif config is not None:
|
||||
# Daemons: check if the service's run.tool binary is on PATH
|
||||
svc = config.services.get(name)
|
||||
if svc and hasattr(svc.run, "tool"):
|
||||
installed = shutil.which(svc.run.tool) is not None
|
||||
if comp.source and comp.stack:
|
||||
installed = shutil.which(name) is not None
|
||||
|
||||
return ProgramSummary(
|
||||
id=name,
|
||||
description=comp.description,
|
||||
behavior=behavior,
|
||||
behavior=comp.behavior,
|
||||
stack=comp.stack,
|
||||
runner=runner,
|
||||
version=comp.tool.version if comp.tool else None,
|
||||
version=comp.version,
|
||||
source=source,
|
||||
system_dependencies=comp.tool.system_dependencies if comp.tool else [],
|
||||
system_dependencies=comp.system_dependencies,
|
||||
installed=installed,
|
||||
actions=available_actions(comp),
|
||||
)
|
||||
@@ -701,21 +660,11 @@ def list_components(include_remote: bool = False) -> list[ComponentSummary]:
|
||||
if ref and ref in config.programs:
|
||||
s.source = config.programs[ref].source
|
||||
|
||||
# Programs — always include entries from the programs
|
||||
# section as catalog items. Derive behavior from backing
|
||||
# service/job when the program has no install/build spec.
|
||||
svc_components = {s.component for s in config.services.values() if s.component}
|
||||
job_components = {j.component for j in config.jobs.values() if j.component}
|
||||
|
||||
# Programs from the software catalog
|
||||
for name, comp in config.programs.items():
|
||||
summary = _summary_from_program(name, comp, root)
|
||||
if summary.behavior is None:
|
||||
if name in svc_components or name in config.services:
|
||||
summary.behavior = "daemon"
|
||||
elif name in job_components or name in config.jobs:
|
||||
summary.behavior = "tool"
|
||||
else:
|
||||
continue
|
||||
continue
|
||||
summary.node = local_hostname
|
||||
summaries.append(summary)
|
||||
except FileNotFoundError:
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
@@ -16,18 +17,15 @@ router = APIRouter(tags=["tools"])
|
||||
|
||||
|
||||
def _is_tool(comp: ProgramSpec) -> bool:
|
||||
"""Check if a component is a tool (has install.path or tool spec)."""
|
||||
return bool((comp.install and comp.install.path) or comp.tool)
|
||||
"""Check if a component is a tool."""
|
||||
return comp.behavior == "tool"
|
||||
|
||||
|
||||
def _tool_summary(
|
||||
name: str, comp: ProgramSpec, root: Path | None = None
|
||||
) -> ToolSummary:
|
||||
"""Build a ToolSummary from a ProgramSpec that is a tool."""
|
||||
t = comp.tool
|
||||
installed = bool(
|
||||
comp.install and comp.install.path and comp.install.path.enable
|
||||
)
|
||||
installed = shutil.which(name) is not None
|
||||
|
||||
# Infer runner from source directory
|
||||
runner = None
|
||||
@@ -43,9 +41,9 @@ def _tool_summary(
|
||||
id=name,
|
||||
description=comp.description,
|
||||
source=source,
|
||||
version=t.version if t else None,
|
||||
version=comp.version,
|
||||
runner=runner,
|
||||
system_dependencies=t.system_dependencies if t else [],
|
||||
system_dependencies=comp.system_dependencies,
|
||||
installed=installed,
|
||||
)
|
||||
|
||||
|
||||
@@ -23,21 +23,18 @@ def castle_root(tmp_path: Path) -> Generator[Path, None, None]:
|
||||
castle_yaml = tmp_path / "castle.yaml"
|
||||
config = {
|
||||
"gateway": {"port": 9000},
|
||||
"components": {
|
||||
"programs": {
|
||||
"test-tool": {
|
||||
"description": "Test tool",
|
||||
"source": "test-tool",
|
||||
"install": {"path": {"alias": "test-tool"}},
|
||||
"tool": {
|
||||
"system_dependencies": ["pandoc"],
|
||||
},
|
||||
"behavior": "tool",
|
||||
"system_dependencies": ["pandoc"],
|
||||
},
|
||||
"test-tool-2": {
|
||||
"description": "Another test tool",
|
||||
"source": "test-tool-2",
|
||||
"tool": {
|
||||
"version": "2.0.0",
|
||||
},
|
||||
"behavior": "tool",
|
||||
"version": "2.0.0",
|
||||
},
|
||||
},
|
||||
"services": {
|
||||
@@ -46,7 +43,7 @@ def castle_root(tmp_path: Path) -> Generator[Path, None, None]:
|
||||
"description": "Test service",
|
||||
"run": {
|
||||
"runner": "python",
|
||||
"tool": "test-svc",
|
||||
"program": "test-svc",
|
||||
},
|
||||
"expose": {
|
||||
"http": {
|
||||
|
||||
@@ -33,18 +33,12 @@ class TestToolsList:
|
||||
assert tool["system_dependencies"] == ["pandoc"]
|
||||
|
||||
def test_installed_flag(self, client: TestClient) -> None:
|
||||
"""Tool with install.path is marked as installed."""
|
||||
"""Tool installed field reflects whether binary is on PATH."""
|
||||
response = client.get("/tools")
|
||||
data = response.json()
|
||||
tool = next(t for t in data if t["id"] == "test-tool")
|
||||
assert tool["installed"] is True
|
||||
|
||||
def test_not_installed_flag(self, client: TestClient) -> None:
|
||||
"""Tool without install.path is not marked as installed."""
|
||||
response = client.get("/tools")
|
||||
data = response.json()
|
||||
tool = next(t for t in data if t["id"] == "test-tool-2")
|
||||
assert tool["installed"] is False
|
||||
# test-tool binary won't be on PATH in test env
|
||||
assert isinstance(tool["installed"], bool)
|
||||
|
||||
def test_service_excluded(self, client: TestClient) -> None:
|
||||
"""Services without tool spec are not listed."""
|
||||
|
||||
Reference in New Issue
Block a user