feat: add source registry mutations

This commit is contained in:
lda
2026-06-04 15:14:31 +07:00 Unverified
parent c0a885f050
commit 9ca81fedf2
15 changed files with 1689 additions and 80 deletions
+129 -17
View File
@@ -1,21 +1,32 @@
from __future__ import annotations
import asyncio
from typing import Annotated
import json
from pathlib import Path
from typing import Annotated, Any
import typer
from wf_cli.context import load_cli_context_from_typer
from wf_cli.context import CliContext, load_cli_context_from_typer
from wf_cli.formats import ListOutputFormat, emit_list_payload
from wf_cli.io import emit_json
app = typer.Typer(
name="registry",
help="List and inspect desired persisted source registry entries.",
help="List, inspect, and mutate desired persisted source registry entries.",
no_args_is_help=True,
)
def _require_registry_admin(context: CliContext) -> Any:
"""Return the source registry admin surface or raise if unavailable."""
if context.source_registry_admin is None:
raise typer.BadParameter(
"source registry admin operations are not available for this server"
)
return context.source_registry_admin
@app.command("list")
def list_registry_entries(
ctx: typer.Context,
@@ -31,13 +42,8 @@ def list_registry_entries(
) -> None:
"""List desired persisted source registry entries."""
context = load_cli_context_from_typer(ctx)
if context.source_registry_admin is None:
raise typer.BadParameter(
"source registry admin reads are not available for this server"
)
payload = asyncio.run(
context.source_registry_admin.list_registry_entries(cursor=cursor, limit=limit)
)
admin = _require_registry_admin(context)
payload = asyncio.run(admin.list_registry_entries(cursor=cursor, limit=limit))
emit_list_payload(
payload,
collection_key="entries",
@@ -54,11 +60,117 @@ def inspect_registry_entry(
) -> None:
"""Inspect one desired persisted source registry entry."""
context = load_cli_context_from_typer(ctx)
if context.source_registry_admin is None:
raise typer.BadParameter(
"source registry admin reads are not available for this server"
)
payload = asyncio.run(
context.source_registry_admin.inspect_registry_entry(source_id=source_id)
)
admin = _require_registry_admin(context)
payload = asyncio.run(admin.inspect_registry_entry(source_id=source_id))
emit_json(payload)
@app.command("add")
def add_registry_entry(
ctx: typer.Context,
input_json: Annotated[
str | None, typer.Option("--input", help="JSON payload for the new entry.")
] = None,
input_file: Annotated[
str | None,
typer.Option("--input-file", help="Path to JSON file for the new entry."),
] = None,
) -> None:
"""Add a new desired source registry entry."""
context = load_cli_context_from_typer(ctx)
admin = _require_registry_admin(context)
entry = _read_json_arg(input_json, input_file, "--input/--input-file")
payload = asyncio.run(admin.add_registry_entry(entry=entry))
emit_json(payload)
@app.command("update")
def update_registry_entry(
ctx: typer.Context,
source_id: Annotated[str, typer.Argument(help="Source ID to update.")],
patch_json: Annotated[
str | None, typer.Option("--patch", help="JSON patch to apply.")
] = None,
patch_file: Annotated[
str | None, typer.Option("--patch-file", help="Path to JSON patch file.")
] = None,
) -> None:
"""Update an existing desired source registry entry."""
context = load_cli_context_from_typer(ctx)
admin = _require_registry_admin(context)
patch = _read_json_arg(patch_json, patch_file, "--patch/--patch-file")
payload = asyncio.run(admin.update_registry_entry(source_id=source_id, patch=patch))
emit_json(payload)
@app.command("enable")
def enable_registry_entry(
ctx: typer.Context,
source_id: Annotated[str, typer.Argument(help="Source ID to enable.")],
) -> None:
"""Enable a desired source registry entry."""
context = load_cli_context_from_typer(ctx)
admin = _require_registry_admin(context)
payload = asyncio.run(admin.enable_registry_entry(source_id=source_id))
emit_json(payload)
@app.command("disable")
def disable_registry_entry(
ctx: typer.Context,
source_id: Annotated[str, typer.Argument(help="Source ID to disable.")],
) -> None:
"""Disable a desired source registry entry."""
context = load_cli_context_from_typer(ctx)
admin = _require_registry_admin(context)
payload = asyncio.run(admin.disable_registry_entry(source_id=source_id))
emit_json(payload)
@app.command("remove")
def remove_registry_entry(
ctx: typer.Context,
source_id: Annotated[str, typer.Argument(help="Source ID to remove.")],
confirm: Annotated[
bool, typer.Option("--confirm", help="Confirm removal.")
] = False,
) -> None:
"""Remove a desired source registry entry (requires --confirm)."""
context = load_cli_context_from_typer(ctx)
admin = _require_registry_admin(context)
if not confirm:
raise typer.BadParameter("removal requires --confirm flag")
payload = asyncio.run(admin.remove_registry_entry(source_id=source_id))
emit_json(payload)
def _read_json_arg(
inline: str | None,
file_path: str | None,
flag_names: str,
) -> dict[str, Any]:
"""Resolve JSON from either inline text or a file path."""
if inline and file_path:
raise typer.BadParameter(f"provide exactly one of {flag_names}")
if inline:
try:
value = json.loads(inline)
except json.JSONDecodeError as exc:
raise typer.BadParameter(f"invalid JSON: {exc}") from exc
return _require_json_object(value, flag_names)
if file_path:
try:
value = json.loads(Path(file_path).read_text(encoding="utf-8"))
except FileNotFoundError:
raise typer.BadParameter(f"file not found: {file_path}")
except json.JSONDecodeError as exc:
raise typer.BadParameter(f"invalid JSON in file: {exc}") from exc
return _require_json_object(value, flag_names)
raise typer.BadParameter(f"{flag_names} is required")
def _require_json_object(value: Any, flag_names: str) -> dict[str, Any]:
"""Registry add/update payloads must be JSON objects, not arrays/scalars."""
if not isinstance(value, dict):
raise typer.BadParameter(f"{flag_names} must be a JSON object")
return value