From 3d046eef0b3fb37e061791ff00f7b6813825ec3e Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 26 Jan 2026 23:03:58 +0000 Subject: [PATCH] feat(cli): Smart Profile ID extraction from URLs - Added `extract_profile_id` helper to `main.py` to handle Control D dashboard URLs. - Updated `main()` to use extraction for both CLI arguments and interactive input. - Updated interactive prompt text to inform users they can paste the full URL. - Added comprehensive tests for extraction logic and interactive flow in `test_main.py`. Co-authored-by: abhimehro <84992105+abhimehro@users.noreply.github.com> --- .Jules/palette.md | 4 ++++ main.py | 21 ++++++++++++++--- test_main.py | 59 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 3 deletions(-) diff --git a/.Jules/palette.md b/.Jules/palette.md index 532c04f4..59fcda32 100644 --- a/.Jules/palette.md +++ b/.Jules/palette.md @@ -9,3 +9,7 @@ ## 2024-05-24 - Fail Fast & Friendly **Learning:** In CLI tools involving APIs, cascade failures (hundreds of "Failed to X") caused by basic auth issues (401/403) are overwhelming and confusing. A dedicated "Pre-flight Check" that validates credentials *before* attempting the main workload allows for specific, actionable error messages (e.g. "Check your token at [URL]") instead of generic HTTP errors. **Action:** Implement a `check_api_access()` step at the start of any CLI workflow to validate permissions and provide human-readable guidance on failure. + +## 2024-05-25 - Smart Input Extraction +**Learning:** Users often copy full URLs instead of specific IDs because it's easier and they lack context on what exactly defines the "ID". Accepting the full URL and extracting the ID programmatically prevents validation errors and reduces user friction. +**Action:** When asking for an ID that is part of a URL, accept the full URL and extract the ID automatically using regex. diff --git a/main.py b/main.py index c7810580..535b1247 100644 --- a/main.py +++ b/main.py @@ -247,6 +247,21 @@ def validate_folder_url(url: str) -> bool: return True +def extract_profile_id(text: str) -> str: + """ + Extracts the Profile ID from a Control D URL if present, + otherwise returns the text as-is (cleaned). + """ + if not text: + return "" + text = text.strip() + # Pattern for Control D Dashboard URLs + # e.g. https://controld.com/dashboard/profiles/12345abc/filters + match = re.search(r"controld\.com/dashboard/profiles/([^/?#\s]+)", text) + if match: + return match.group(1) + return text + def validate_profile_id(profile_id: str) -> bool: if not re.match(r"^[a-zA-Z0-9_-]+$", profile_id): log.error("Invalid profile ID format (contains unsafe characters)") @@ -873,17 +888,17 @@ def main(): global TOKEN args = parse_args() profiles_arg = _clean_env_kv(args.profiles or os.getenv("PROFILE", ""), "PROFILE") or "" - profile_ids = [p.strip() for p in profiles_arg.split(",") if p.strip()] + profile_ids = [extract_profile_id(p) for p in profiles_arg.split(",") if p.strip()] folder_urls = args.folder_url if args.folder_url else DEFAULT_FOLDER_URLS # Interactive prompts for missing config if not args.dry_run and sys.stdin.isatty(): if not profile_ids: print(f"{Colors.CYAN}ℹ Profile ID is missing.{Colors.ENDC}") - print(f"{Colors.CYAN} You can find this in the URL of your profile in the Control D Dashboard.{Colors.ENDC}") + print(f"{Colors.CYAN} You can find this in the URL of your profile in the Control D Dashboard (or just paste the URL).{Colors.ENDC}") p_input = input(f"{Colors.BOLD}Enter Control D Profile ID:{Colors.ENDC} ").strip() if p_input: - profile_ids = [p.strip() for p in p_input.split(",") if p.strip()] + profile_ids = [extract_profile_id(p) for p in p_input.split(",") if p.strip()] if not TOKEN: print(f"{Colors.CYAN}ℹ API Token is missing.{Colors.ENDC}") diff --git a/test_main.py b/test_main.py index 4c4d062a..0f1d7671 100644 --- a/test_main.py +++ b/test_main.py @@ -319,3 +319,62 @@ def test_check_api_access_network_error(monkeypatch): assert m.check_api_access(mock_client, "profile") is False assert mock_log.error.called assert "Network failure" in str(mock_log.error.call_args) + +# Case 8: extract_profile_id correctly extracts ID from URL or returns input +def test_extract_profile_id(): + # Regular ID + assert main.extract_profile_id("12345") == "12345" + # URL with /filters + assert main.extract_profile_id("https://controld.com/dashboard/profiles/12345/filters") == "12345" + # URL without /filters + assert main.extract_profile_id("https://controld.com/dashboard/profiles/12345") == "12345" + # URL with params + assert main.extract_profile_id("https://controld.com/dashboard/profiles/12345?foo=bar") == "12345" + # Clean up whitespace + assert main.extract_profile_id(" 12345 ") == "12345" + # Invalid input returns as is (cleaned) + assert main.extract_profile_id("random-string") == "random-string" + # Empty input + assert main.extract_profile_id("") == "" + assert main.extract_profile_id(None) == "" + +# Case 9: Interactive input handles URL pasting +def test_interactive_input_extracts_id(monkeypatch, capsys): + # Ensure environment is clean + monkeypatch.delenv("PROFILE", raising=False) + monkeypatch.delenv("TOKEN", raising=False) + + # Reload main with isatty=True + m = reload_main_with_env(monkeypatch, isatty=True) + monkeypatch.setattr('sys.stdin.isatty', lambda: True) + + # Provide URL as input + url_input = "https://controld.com/dashboard/profiles/extracted_id/filters" + monkeypatch.setattr('builtins.input', lambda prompt="": url_input) + monkeypatch.setattr('getpass.getpass', lambda prompt="": "test_token") + + # Mock parse_args + mock_args = MagicMock() + mock_args.profiles = None + mock_args.folder_url = None + mock_args.dry_run = False + mock_args.no_delete = False + mock_args.plan_json = None + monkeypatch.setattr(m, "parse_args", lambda: mock_args) + + # Mock sync_profile to catch the call + mock_sync = MagicMock(return_value=True) + monkeypatch.setattr(m, "sync_profile", mock_sync) + monkeypatch.setattr(m, "warm_up_cache", MagicMock()) + + # Run main, expect clean exit + with pytest.raises(SystemExit): + m.main() + + # Verify sync_profile called with extracted ID + args, _ = mock_sync.call_args + assert args[0] == "extracted_id" + + # Verify prompt text update + captured = capsys.readouterr() + assert "(or just paste the URL)" in captured.out