Skip to content

Add OpenAPI-backed Tangle API CLI#1

Open
Volv-G wants to merge 2 commits into
TangleML:mainfrom
Volv-G:piforge/tangle-cli-oss/migrate-typer-to-cyclopts-and-dy-bee035c
Open

Add OpenAPI-backed Tangle API CLI#1
Volv-G wants to merge 2 commits into
TangleML:mainfrom
Volv-G:piforge/tangle-cli-oss/migrate-typer-to-cyclopts-and-dy-bee035c

Conversation

@Volv-G

@Volv-G Volv-G commented Jun 9, 2026

Copy link
Copy Markdown

Summary

  • Migrates the CLI entrypoints from Typer to Cyclopts while preserving the existing components command surface.
  • Adds dynamic OpenAPI/FastAPI-driven tangle api commands generated from the backend schema.
  • Adds schema caching plus tangle api refresh, with cold-cache command/help behavior and OS-specific cache directories via platformdirs.
  • Adds custom auth/header support, including bearer tokens, custom Authorization values for Basic Auth, repeated --header/-H, and env-based headers such as Cloud-Auth.
  • Switches API schema fetch and endpoint dispatch to httpx with an explicit CLI timeout.
  • Adds tests covering command generation, cache behavior, auth/header handling, httpx errors, route collision handling, body/query parsing, and component stub help behavior.

Notes

This approach keeps the OSS Tangle CLI based on the existing TangleML scaffolding and decouples it from tangle-deploy, since the two surfaces have irreconcilable CLI/runtime needs.

Validation

  • uv run pytest -q16 passed
  • uv run tangle --help
  • uv run tangle components --help
  • uv run tangle components annotations get → help + exit 0
  • uv run tangle components annotations set → help + exit 0
  • uv run tangle components annotations get foo → nonzero as expected
  • uv run tangle components annotations set foo key → nonzero as expected
  • cold-cache uv run tangle api -h with unavailable backend → help + exit 0
  • uv run tangle api --help
  • uv run tangle api refresh --help
  • uv run python -m compileall tangle_cli tests
  • git diff --check

Comment thread tangle_cli/components_cli.py Outdated
"""Gets annotation values from component file."""
if component_path is None or keys is None:
_missing_required_args("get", {"component_path": component_path, "keys": keys})
print(locals())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 This is an AI-generated code review comment.

Looks like a leftover debug print: this dumps component_path / keys to stdout right before raising NotImplementedError, and the sibling set command does not have it. Safe to delete.

Comment thread tangle_cli/api_cli.py Outdated
return current if current is not None else value


app = build_app()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 This is an AI-generated code review comment.

build_app() at module-import time reads sys.argv (via _schema_for_current_invocation) and can issue a 30s httpx.get. Any importer — tests, plugins, python -c "from tangle_cli import api_cli" — hits the same code path as the real CLI entrypoint.

Suggest making construction lazy: expose def get_app(): return build_app() and call it from cli.py, so the side effect only fires for the actual invocation.

Comment thread tangle_cli/api_cli.py Outdated

def _argv_requests_api_schema(argv: list[str]) -> bool:
args = list(argv[1:])
if "api" not in args:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 This is an AI-generated code review comment.

"api" in args matches the literal string anywhere in argv, not just the subcommand position. Invocations like tangle components --tag api or tangle components annotations set foo api will be treated as api requests. With a cold cache and no backend reachable, this triggers a 30s httpx.get that hangs every such invocation — the exception is later swallowed because _argv_dispatches_dynamic_command returns False, so the user just sees an unexplained startup hang.

Suggest matching only when api is the first non-option positional:

def _first_positional(argv):
    for arg in argv[1:]:
        if not arg.startswith("-"):
            return arg
    return None

Comment thread tangle_cli/api_cli.py Outdated
try:
schema, path = refresh_schema(normalized_base_url, token, header, auth_header)
except httpx.HTTPStatusError as exc:
message = exc.response.text or exc.response.reason_phrase

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 This is an AI-generated code review comment.

This echoes the full upstream response body into the error message. _schema_fetch_failure_message (used by dynamic dispatch) is careful to only include reason_phrase / exc.__class__.__name__, and test_invalid_header_errors_do_not_echo_secret pins a no-secret-echo posture. A misconfigured proxy/backend can reflect auth headers, request bodies, or internal traces in its error page, so the inconsistency is worth closing.

Suggest matching the other helper: emit HTTP {status} {reason_phrase} and drop response.text (or cap it to a small prefix).

Comment thread tangle_cli/api_cli.py

configured = os.environ.get("TANGLE_CLI_CACHE_DIR")
if configured:
return Path(configured).expanduser()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 This is an AI-generated code review comment.

The two branches return different layouts: the platformdirs path nests under openapi/, while the env override is used as-is. A user who follows the README and sets TANGLE_CLI_CACHE_DIR=~/.tangle-cache will get schema JSON dropped directly in that directory rather than in ~/.tangle-cache/openapi/, which becomes a problem the moment anything else wants to cache there too.

Suggest either appending / "openapi" to both branches, or documenting in the README that the env var is the schema directory rather than the cache root.

Comment thread tangle_cli/api_cli.py Outdated
def _is_simple_schema(schema: Any) -> bool:
"""Return true for scalar/list types safe to expose as CLI options."""

schema = _unwrap_nullable_schema({}, schema)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 This is an AI-generated code review comment.

These call _unwrap_nullable_schema({}, schema) — the empty dict is the OpenAPI document used by _resolve_ref, so any $ref here silently returns unresolved. Most call paths pre-flatten before reaching here, so the top-level is fine, but nested refs (e.g. array.items: {"$ref": "..."}) fall through to str/False and produce wrong-typed CLI options.

Suggest threading the top-level schema through these helpers (same as _flatten_schema), or asserting that callers must fully resolve refs before calling.

Comment thread tangle_cli/api_cli.py Outdated
) -> None:
"""Fetch /openapi.json and update the local schema cache."""

normalized_base_url = _normalize_base_url(base_url or default_base_url())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 This is an AI-generated code review comment.

default_base_url() already returns a _normalize_base_url(...)-ed value, so this double-normalizes when base_url is None. Minor — can be:

normalized_base_url = _normalize_base_url(base_url) if base_url else default_base_url()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants