Add OpenAPI-backed Tangle API CLI#1
Conversation
| """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()) |
There was a problem hiding this comment.
🤖 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.
| return current if current is not None else value | ||
|
|
||
|
|
||
| app = build_app() |
There was a problem hiding this comment.
🤖 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.
|
|
||
| def _argv_requests_api_schema(argv: list[str]) -> bool: | ||
| args = list(argv[1:]) | ||
| if "api" not in args: |
There was a problem hiding this comment.
🤖 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| 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 |
There was a problem hiding this comment.
🤖 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).
|
|
||
| configured = os.environ.get("TANGLE_CLI_CACHE_DIR") | ||
| if configured: | ||
| return Path(configured).expanduser() |
There was a problem hiding this comment.
🤖 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.
| def _is_simple_schema(schema: Any) -> bool: | ||
| """Return true for scalar/list types safe to expose as CLI options.""" | ||
|
|
||
| schema = _unwrap_nullable_schema({}, schema) |
There was a problem hiding this comment.
🤖 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.
| ) -> None: | ||
| """Fetch /openapi.json and update the local schema cache.""" | ||
|
|
||
| normalized_base_url = _normalize_base_url(base_url or default_base_url()) |
There was a problem hiding this comment.
🤖 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()
Summary
componentscommand surface.tangle apicommands generated from the backend schema.tangle api refresh, with cold-cache command/help behavior and OS-specific cache directories viaplatformdirs.Authorizationvalues for Basic Auth, repeated--header/-H, and env-based headers such asCloud-Auth.httpxwith an explicit CLI timeout.httpxerrors, 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 -q→16 passeduv run tangle --helpuv run tangle components --helpuv run tangle components annotations get→ help + exit 0uv run tangle components annotations set→ help + exit 0uv run tangle components annotations get foo→ nonzero as expecteduv run tangle components annotations set foo key→ nonzero as expecteduv run tangle api -hwith unavailable backend → help + exit 0uv run tangle api --helpuv run tangle api refresh --helpuv run python -m compileall tangle_cli testsgit diff --check