merge
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
from os.path import expandvars
|
||||
from pathlib import Path
|
||||
from typing import Annotated
|
||||
|
||||
try:
|
||||
import tomllib as tomli # py311+
|
||||
except ImportError: # pragma: no cover
|
||||
import tomli
|
||||
import yaml
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic.functional_validators import AfterValidator
|
||||
|
||||
from .identity import generate
|
||||
from .slugify import slugify
|
||||
|
||||
ExpandVarsStr = Annotated[str, AfterValidator(expandvars)]
|
||||
|
||||
|
||||
class App(BaseModel):
|
||||
"""Defines an application."""
|
||||
|
||||
name: str
|
||||
slug: str = ""
|
||||
path: ExpandVarsStr = "./"
|
||||
color: str = ""
|
||||
command: ExpandVarsStr = ""
|
||||
terminal: bool = False
|
||||
|
||||
|
||||
class Config(BaseModel):
|
||||
"""Root configuration model."""
|
||||
|
||||
apps: list[App] = Field(default_factory=list)
|
||||
landing: list[App] = Field(default_factory=list)
|
||||
|
||||
|
||||
def default_config() -> Config:
|
||||
"""Get a default empty configuration.
|
||||
|
||||
Returns:
|
||||
Configuration object.
|
||||
"""
|
||||
return Config()
|
||||
|
||||
|
||||
def load_config(config_path: Path) -> Config:
|
||||
"""Load config from a path.
|
||||
|
||||
Args:
|
||||
config_path: Path to TOML configuration.
|
||||
|
||||
Returns:
|
||||
Config object.
|
||||
"""
|
||||
with Path(config_path).open("rb") as config_file:
|
||||
config_data = tomli.load(config_file)
|
||||
|
||||
def make_app(name, data: dict[str, object], terminal: bool = False) -> App:
|
||||
data["name"] = name
|
||||
data["terminal"] = terminal
|
||||
if terminal:
|
||||
data["slug"] = generate().lower()
|
||||
elif not data.get("slug", ""):
|
||||
data["slug"] = slugify(name)
|
||||
|
||||
return App(**data)
|
||||
|
||||
apps = [make_app(name, app) for name, app in config_data.get("app", {}).items()]
|
||||
|
||||
apps += [
|
||||
make_app(name, app, terminal=True) for name, app in config_data.get("terminal", {}).items()
|
||||
]
|
||||
|
||||
config = Config(apps=apps)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def load_landing_yaml(manifest_path: Path) -> list[App]:
|
||||
"""Load landing apps from YAML manifest.
|
||||
|
||||
Expected schema: list of {name, slug, command, color?, path?, terminal?}
|
||||
"""
|
||||
with manifest_path.open("r", encoding="utf-8") as f:
|
||||
data = yaml.safe_load(f) or []
|
||||
apps: list[App] = []
|
||||
for entry in data:
|
||||
if not isinstance(entry, dict):
|
||||
continue
|
||||
name = entry.get("name")
|
||||
command = entry.get("command")
|
||||
if not name or not command:
|
||||
continue
|
||||
slug = entry.get("slug") or slugify(name)
|
||||
apps.append(
|
||||
App(
|
||||
name=name,
|
||||
slug=slug,
|
||||
command=command,
|
||||
path=entry.get("path", "./"),
|
||||
color=entry.get("color", ""),
|
||||
terminal=bool(entry.get("terminal", True)),
|
||||
)
|
||||
)
|
||||
return apps
|
||||
|
||||
|
||||
def _extract_label(labels: object, key: str) -> str | None:
|
||||
"""Extract a label value from either dict or list[str] forms."""
|
||||
if isinstance(labels, dict):
|
||||
value = labels.get(key)
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
return None
|
||||
if isinstance(labels, list):
|
||||
for item in labels:
|
||||
if not isinstance(item, str):
|
||||
continue
|
||||
if "=" in item:
|
||||
k, v = item.split("=", 1)
|
||||
if k == key:
|
||||
return v
|
||||
return None
|
||||
|
||||
|
||||
def load_compose_manifest(manifest_path: Path) -> list[App]:
|
||||
"""Load landing apps from a docker-compose YAML file using label `webterm-command`."""
|
||||
with manifest_path.open("r", encoding="utf-8") as f:
|
||||
data = yaml.safe_load(f) or {}
|
||||
services = data.get("services", {}) if isinstance(data, dict) else {}
|
||||
apps: list[App] = []
|
||||
for name, service in services.items():
|
||||
if not isinstance(service, dict):
|
||||
continue
|
||||
labels = service.get("labels", {})
|
||||
command = _extract_label(labels, "webterm-command")
|
||||
if not command:
|
||||
continue
|
||||
slug = slugify(name)
|
||||
apps.append(
|
||||
App(
|
||||
name=name,
|
||||
slug=slug,
|
||||
command=command,
|
||||
path=service.get("working_dir", "./"),
|
||||
color="",
|
||||
terminal=True,
|
||||
)
|
||||
)
|
||||
return apps
|
||||
Reference in New Issue
Block a user