Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .Jules/palette.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
21 changes: 18 additions & 3 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,21 @@

return True

def extract_profile_id(text: str) -> str:
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The type hint for text is str, but the function logic and tests handle None as a valid input. To make the function signature more accurate and prevent potential type checker issues, it's better to use Optional[str]. You'll need to ensure from typing import Optional is present.

Suggested change
def extract_profile_id(text: str) -> str:
def extract_profile_id(text: Optional[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)
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

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

The regex pattern for extracting profile IDs from URLs is case-sensitive and will not match URLs with uppercase letters in the domain (e.g., "https://ControlD.com/..." or "HTTPS://CONTROLD.COM/..."). Consider making the pattern case-insensitive by adding the re.IGNORECASE flag or by explicitly handling both cases in the pattern. Domain names are case-insensitive per RFC standards, so users might paste URLs with different casing.

Suggested change
match = re.search(r"controld\.com/dashboard/profiles/([^/?#\s]+)", text)
match = re.search(r"controld\.com/dashboard/profiles/([^/?#\s]+)", text, re.IGNORECASE)

Copilot uses AI. Check for mistakes.
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)")
Expand Down Expand Up @@ -873,17 +888,17 @@
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}")

Check warning

Code scanning / Pylint (reported by Codacy)

Line too long (146/100) Warning

Line too long (146/100)

Check warning

Code scanning / Pylintpython3 (reported by Codacy)

Line too long (146/100) Warning

Line too long (146/100)
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}")
Expand Down
59 changes: 59 additions & 0 deletions test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,3 +319,62 @@
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():

Check warning

Code scanning / Pylint (reported by Codacy)

Missing function docstring Warning test

Missing function docstring
# Regular ID
assert main.extract_profile_id("12345") == "12345"

Check notice

Code scanning / Bandit

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.

Check notice

Code scanning / Bandit (reported by Codacy)

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
# URL with /filters
assert main.extract_profile_id("https://controld.com/dashboard/profiles/12345/filters") == "12345"

Check notice

Code scanning / Bandit

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.

Check notice

Code scanning / Bandit (reported by Codacy)

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.

Check warning

Code scanning / Pylint (reported by Codacy)

Line too long (102/100) Warning test

Line too long (102/100)

Check warning

Code scanning / Pylintpython3 (reported by Codacy)

Line too long (102/100) Warning test

Line too long (102/100)
# URL without /filters
assert main.extract_profile_id("https://controld.com/dashboard/profiles/12345") == "12345"

Check notice

Code scanning / Bandit

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.

Check notice

Code scanning / Bandit (reported by Codacy)

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
# URL with params
assert main.extract_profile_id("https://controld.com/dashboard/profiles/12345?foo=bar") == "12345"

Check notice

Code scanning / Bandit

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.

Check warning

Code scanning / Pylint (reported by Codacy)

Line too long (102/100) Warning test

Line too long (102/100)

Check warning

Code scanning / Pylintpython3 (reported by Codacy)

Line too long (102/100) Warning test

Line too long (102/100)

Check notice

Code scanning / Bandit (reported by Codacy)

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
# Clean up whitespace
assert main.extract_profile_id(" 12345 ") == "12345"

Check notice

Code scanning / Bandit

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.

Check notice

Code scanning / Bandit (reported by Codacy)

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
# Invalid input returns as is (cleaned)
assert main.extract_profile_id("random-string") == "random-string"

Check notice

Code scanning / Bandit

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.

Check notice

Code scanning / Bandit (reported by Codacy)

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
# Empty input
assert main.extract_profile_id("") == ""

Check notice

Code scanning / Bandit

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.

Check notice

Code scanning / Bandit (reported by Codacy)

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
assert main.extract_profile_id(None) == ""

Check notice

Code scanning / Bandit

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.

Check notice

Code scanning / Bandit (reported by Codacy)

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
Comment on lines +324 to +339
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

This test function contains multiple assert statements for different test cases. To improve readability and make it easier to add new test cases in the future, consider refactoring it to use pytest.mark.parametrize. This will also provide more granular feedback if one of the cases fails.

Suggested change
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) == ""
@pytest.mark.parametrize(
"input_text, expected_id",
[
("12345", "12345"),
("https://controld.com/dashboard/profiles/12345/filters", "12345"),
("https://controld.com/dashboard/profiles/12345", "12345"),
("https://controld.com/dashboard/profiles/12345?foo=bar", "12345"),
(" 12345 ", "12345"),
("random-string", "random-string"),
("", ""),
(None, ""),
],
)
def test_extract_profile_id(input_text, expected_id):
assert main.extract_profile_id(input_text) == expected_id

Comment on lines +324 to +339
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

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

Add a test case for URLs with uppercase letters in the domain (e.g., "HTTPS://CONTROLD.COM/dashboard/profiles/12345/filters" or "https://ControlD.com/dashboard/profiles/12345"). Domain names are case-insensitive per RFC standards, but the current regex pattern is case-sensitive and may not handle these cases correctly.

Copilot uses AI. Check for mistakes.

# Case 9: Interactive input handles URL pasting
def test_interactive_input_extracts_id(monkeypatch, capsys):

Check warning

Code scanning / Pylint (reported by Codacy)

Missing function docstring Warning test

Missing function docstring
# 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)

Check warning

Code scanning / Pylint (reported by Codacy)

Variable name "m" doesn't conform to snake_case naming style Warning test

Variable name "m" doesn't conform to snake_case naming style
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"

Check notice

Code scanning / Bandit

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.

Check notice

Code scanning / Bandit (reported by Codacy)

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.

# Verify prompt text update
captured = capsys.readouterr()
assert "(or just paste the URL)" in captured.out

Check notice

Code scanning / Bandit

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.

Check notice

Code scanning / Bandit (reported by Codacy)

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
Loading