Implement E1 setup-education and capability report (fixtures).
Add owner-safe lesson catalog, fixture-backed capability statuses, CLI, and unit tests without live OAuth or connect scripts.
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""Setup education providers — fixture loaders for capability state."""
|
||||
@@ -0,0 +1,125 @@
|
||||
"""Fixture provider for setup/capability data.
|
||||
|
||||
Loads capability state fixtures from JSON files under data/fixtures/setup/.
|
||||
All output is labeled `is_fixture: True` so the owner never sees
|
||||
silent fake live data.
|
||||
|
||||
This is the *only* data source for the capability report until live
|
||||
connection state is available.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import pathlib
|
||||
from typing import Any
|
||||
|
||||
from lumina_skills.setup.capability_report import (
|
||||
CapabilityEntry,
|
||||
CapabilityReport,
|
||||
ConnectionStatus,
|
||||
build_capability_report,
|
||||
)
|
||||
|
||||
# Mapping from fixture status strings to domain enum.
|
||||
_STATUS_MAP: dict[str, ConnectionStatus] = {
|
||||
"connected": ConnectionStatus.CONNECTED,
|
||||
"skipped": ConnectionStatus.SKIPPED,
|
||||
"later": ConnectionStatus.LATER,
|
||||
"error": ConnectionStatus.ERROR,
|
||||
"offline": ConnectionStatus.OFFLINE,
|
||||
}
|
||||
|
||||
|
||||
def _parse_status(raw: str) -> ConnectionStatus:
|
||||
"""Convert a fixture status string to ConnectionStatus.
|
||||
|
||||
Raises:
|
||||
ValueError: If the status string is not recognized.
|
||||
"""
|
||||
key = raw.lower().strip()
|
||||
if key not in _STATUS_MAP:
|
||||
raise ValueError(
|
||||
f"Unknown capability status {raw!r}. "
|
||||
f"Expected one of: {', '.join(sorted(_STATUS_MAP))}"
|
||||
)
|
||||
return _STATUS_MAP[key]
|
||||
|
||||
|
||||
def load_capability_fixture(fixture_path: str | pathlib.Path) -> CapabilityReport:
|
||||
"""Load a capability report from a fixture JSON file.
|
||||
|
||||
Expected top-level shape:
|
||||
```json
|
||||
{
|
||||
"salon_name": "Lumina Hair Studio & Spa",
|
||||
"is_fixture": true,
|
||||
"capabilities": [
|
||||
{
|
||||
"area": "channels",
|
||||
"provider": "whatsapp",
|
||||
"status": "connected",
|
||||
"details": "WhatsApp channel active"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Args:
|
||||
fixture_path: Path to a JSON fixture file.
|
||||
|
||||
Returns:
|
||||
A fully populated CapabilityReport.
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If the fixture file does not exist.
|
||||
ValueError: If the fixture JSON is malformed or has unknown status.
|
||||
"""
|
||||
path = pathlib.Path(fixture_path)
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(f"Fixture not found: {path}")
|
||||
|
||||
raw = json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
salon_name = raw.get("salon_name", "Unknown Salon")
|
||||
is_fixture = raw.get("is_fixture", True)
|
||||
|
||||
capabilities: list[CapabilityEntry] = []
|
||||
for i, cap_raw in enumerate(raw.get("capabilities", [])):
|
||||
# Validate required fields with clear error messages.
|
||||
for required_field in ("area", "provider", "status"):
|
||||
if required_field not in cap_raw:
|
||||
raise ValueError(
|
||||
f"Capability entry {i} missing required field {required_field!r}. "
|
||||
f"Each entry must have 'area', 'provider', and 'status'."
|
||||
)
|
||||
capabilities.append(CapabilityEntry(
|
||||
area=cap_raw["area"],
|
||||
provider=cap_raw["provider"],
|
||||
status=_parse_status(cap_raw["status"]),
|
||||
details=cap_raw.get("details", ""),
|
||||
))
|
||||
|
||||
return build_capability_report(
|
||||
capabilities=capabilities,
|
||||
salon_name=salon_name,
|
||||
is_fixture=is_fixture,
|
||||
)
|
||||
|
||||
|
||||
def load_fixture_metadata(fixture_path: str | pathlib.Path) -> dict[str, Any]:
|
||||
"""Load non-capability metadata from a fixture file.
|
||||
|
||||
Returns salon_name, is_fixture, generated_at, note, etc.
|
||||
"""
|
||||
path = pathlib.Path(fixture_path)
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(f"Fixture not found: {path}")
|
||||
|
||||
raw = json.loads(path.read_text(encoding="utf-8"))
|
||||
return {
|
||||
"salon_name": raw.get("salon_name", "Unknown Salon"),
|
||||
"is_fixture": raw.get("is_fixture", True),
|
||||
"generated_at": raw.get("generated_at", ""),
|
||||
"note": raw.get("note", ""),
|
||||
}
|
||||
Reference in New Issue
Block a user