From 49f4a891978bce09602b7f93dcbb4836d44a8667 Mon Sep 17 00:00:00 2001 From: Lydia Date: Wed, 22 Jan 2025 14:49:20 -0600 Subject: [PATCH 01/10] added snake case feature, tests, and updated pre-commit version --- .gitignore | 1 + .pre-commit-config.yaml | 2 +- ferry_cli/__main__.py | 22 ++++++++++++++++++++++ tests/test_main.py | 18 ++++++++++++++++++ 4 files changed, 42 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 77d6dbd..d08843e 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ __pycache__ swagger.json config/swagger.json remove +.vscode diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b78cc84..4777cf3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.3.0 + rev: v5.0.0 hooks: - id: check-toml - id: end-of-file-fixer diff --git a/ferry_cli/__main__.py b/ferry_cli/__main__.py index 6f32a72..2295b27 100755 --- a/ferry_cli/__main__.py +++ b/ferry_cli/__main__.py @@ -545,6 +545,27 @@ def handle_no_args(_config_path: Optional[pathlib.Path]) -> bool: sys.exit(0) +def handle_arg_capitalization( + endpoints: Dict[str, Any], arguments: List[str] +) -> List[str]: + + # check to see if the arguments supplied are for an endpoint. IE a "-e" was supplied + if arguments[0] == "-e" or arguments[0] == "-E": + endpoint_lowercase = arguments[1].lower() + + # check for underscore seperated endpoint and convert it + if "_" in arguments[1]: + endpoint_lowercase = endpoint_lowercase.replace("_", "") + + for entry in endpoints: + if endpoint_lowercase == entry.lower(): + arguments[1] = entry + return arguments + + # otherwise if not endpoint just return back the same set of args we got to be handled by rest of the program + return arguments + + def main() -> None: _config_path = config.get_configfile_path() if len(sys.argv) == 1: @@ -597,6 +618,7 @@ def main() -> None: print("Successfully stored latest swagger file.\n") ferry_cli.endpoints = ferry_cli.generate_endpoints() + other_args = handle_arg_capitalization(ferry_cli.endpoints, other_args) ferry_cli.run( auth_args.debug_level, auth_args.dryrun, diff --git a/tests/test_main.py b/tests/test_main.py index 90cd9d6..d351b8f 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -10,6 +10,7 @@ handle_show_configfile, get_config_info_from_user, help_called, + handle_arg_capitalization, ) import ferry_cli.__main__ as _main import ferry_cli.config.config as _config @@ -302,3 +303,20 @@ def test_handle_no_args_configfile_does_not_exist( assert pytest_wrapped_e.type == SystemExit assert pytest_wrapped_e.value.code == 0 + + +@pytest.mark.unit +def test_snakecase_and_underscore_conversion(): + test_endpoints = ["getUserInfo"] + correct_args = ["-e", "getUserInfo", "--username=johndoe"] + test_args_case_underscore = ["-e", "_Get_USeriNFo", "--username=johndoe"] + result = handle_arg_capitalization(test_endpoints, test_args_case_underscore) + + # test to make sure function does matching irrespective of capitalization + assert result == correct_args + + # test to make sure function never stops working for correct syntax + assert handle_arg_capitalization(test_endpoints, correct_args) == correct_args + + # test that non endpoint arguments are untouched + assert handle_arg_capitalization(test_endpoints, ["-z"]) == ["-z"] From 2d9cbf7fadfd4b15a994667bbb3b1fcd368e00b6 Mon Sep 17 00:00:00 2001 From: ltrestka Date: Tue, 1 Apr 2025 10:59:16 -0500 Subject: [PATCH 02/10] Handled error correction for endpoint argument. - Converts snake case to lower camel case. - Handles edge case where proper endpoint is passed, but uses incorrect capitalization. --- ferry_cli/__main__.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/ferry_cli/__main__.py b/ferry_cli/__main__.py index 2295b27..8baaad5 100755 --- a/ferry_cli/__main__.py +++ b/ferry_cli/__main__.py @@ -548,21 +548,20 @@ def handle_no_args(_config_path: Optional[pathlib.Path]) -> bool: def handle_arg_capitalization( endpoints: Dict[str, Any], arguments: List[str] ) -> List[str]: - # check to see if the arguments supplied are for an endpoint. IE a "-e" was supplied - if arguments[0] == "-e" or arguments[0] == "-E": - endpoint_lowercase = arguments[1].lower() - - # check for underscore seperated endpoint and convert it - if "_" in arguments[1]: - endpoint_lowercase = endpoint_lowercase.replace("_", "") - - for entry in endpoints: - if endpoint_lowercase == entry.lower(): - arguments[1] = entry - return arguments - - # otherwise if not endpoint just return back the same set of args we got to be handled by rest of the program + for i in range(len(arguments)): + if (arguments[i].lower() == "-e" or arguments[i].lower() == "--endpoint") and len(arguments) > i + 1: + # convert snake_case_endpoint arg to lowerCamelCase + arguments[i+1] = "".join([part.capitalize() for part in arguments[i+1].split("_")]) + if len(arguments[i+1]) > 0: + arguments[i+1] = arguments[i+1][:1].lower() + arguments[i+1][1:] + + # handle case where user supplies endpoint with improper capitalization + if arguments[i+1] not in endpoints: + for endpoint in endpoints: + if arguments[i+1].lower() == endpoint.lower(): + arguments[i+1] = endpoint + break return arguments From 0fe6fa67dbd4c669d173be111dcfab29f773d933 Mon Sep 17 00:00:00 2001 From: ltrestka Date: Tue, 1 Apr 2025 11:11:44 -0500 Subject: [PATCH 03/10] Runs same check for endpoint params argument --- ferry_cli/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ferry_cli/__main__.py b/ferry_cli/__main__.py index 8baaad5..c77b845 100755 --- a/ferry_cli/__main__.py +++ b/ferry_cli/__main__.py @@ -550,7 +550,7 @@ def handle_arg_capitalization( ) -> List[str]: # check to see if the arguments supplied are for an endpoint. IE a "-e" was supplied for i in range(len(arguments)): - if (arguments[i].lower() == "-e" or arguments[i].lower() == "--endpoint") and len(arguments) > i + 1: + if arguments[i].lower() in {"-e","--endpoint", "-ep", "--endpoint_params"} and len(arguments) > i + 1: # convert snake_case_endpoint arg to lowerCamelCase arguments[i+1] = "".join([part.capitalize() for part in arguments[i+1].split("_")]) if len(arguments[i+1]) > 0: From 444842c80f6c26b2c055345a3bd51fdc5ac55014 Mon Sep 17 00:00:00 2001 From: ltrestka Date: Tue, 16 Sep 2025 11:08:31 -0500 Subject: [PATCH 04/10] updated _sanitize_base_url to handle edge cases where --server path doesnt end with trailing slash. --- ferry_cli/__main__.py | 84 +++++++++++++++++++++++++++---------------- tests/test_main.py | 21 ++++++++--- 2 files changed, 70 insertions(+), 35 deletions(-) diff --git a/ferry_cli/__main__.py b/ferry_cli/__main__.py index 44d6d40..708745f 100755 --- a/ferry_cli/__main__.py +++ b/ferry_cli/__main__.py @@ -3,6 +3,7 @@ import json import os import pathlib +import re import sys import textwrap from typing import Any, Dict, Optional, List, Type @@ -437,28 +438,51 @@ def error_raised( error_raised(Exception, f"Error: {e}") @staticmethod - def _sanitize_base_url(raw_base_url: str) -> str: - """This function makes sure we have a trailing forward-slash on the base_url before it's passed - to any other functions + def _sanitize_path(raw_path: str) -> str: + """ + Normalizes a URL path: + - Collapses multiple internal slashes + - Ensures exactly one leading slash + - Ensures exactly one trailing slash + + Examples: + "path//to///resource" → "/path/to/resource/" + "/path/to/resource" → "/path/to/resource/" + "///ping" → "/ping/" + "/ping///" → "/ping/" + "/pingus/aa//e/" → "/pingus/aa/e/" + """ + # Collapse multiple slashes + cleaned = re.sub(r"/+", "/", raw_path.strip()) + # Handle special case: empty path + if not cleaned: + return "/" + return "/" + cleaned.strip("/") + "/" - That is, "http://hostname.domain:port" --> "http://hostname.domain:port/" but - "http://hostname.domain:port/" --> "http://hostname.domain:port/" and - "http://hostname.domain:port/path?querykey1=value1&querykey2=value2" --> "http://hostname.domain:port/path?querykey1=value1&querykey2=value2" and + @staticmethod + def _sanitize_base_url(raw_base_url: str) -> str: + """ + Ensures that the base URL: + - Has a normalized path (leading + trailing slash, collapsed slashes) + - Leaves query and fragment unchanged - So if there is a non-empty path, parameters, query, or fragment to our URL as defined by RFC 1808, we leave the URL alone + Will only modify the URL if: + - The query and fragment are empty, OR + - The path is non-empty and needs sanitization """ _parts = urlsplit(raw_base_url) - parts = ( - SplitResult( - scheme=_parts.scheme, - netloc=_parts.netloc, - path="/", - query=_parts.query, - fragment=_parts.fragment, - ) - if (_parts.path == "" and _parts.query == "" and _parts.fragment == "") - else _parts + + # Always sanitize the path + sanitized_path = FerryCLI._sanitize_path(_parts.path) + + parts = SplitResult( + scheme=_parts.scheme, + netloc=_parts.netloc, + path=sanitized_path, + query=_parts.query, + fragment=_parts.fragment, ) + return urlunsplit(parts) def __parse_config_file(self: "FerryCLI") -> configparser.ConfigParser: @@ -589,19 +613,19 @@ def handle_arg_capitalization( endpoints: Dict[str, Any], arguments: List[str] ) -> List[str]: # check to see if the arguments supplied are for an endpoint. IE a "-e" was supplied - for i in range(len(arguments)): - if arguments[i].lower() in {"-e","--endpoint", "-ep", "--endpoint_params"} and len(arguments) > i + 1: - # convert snake_case_endpoint arg to lowerCamelCase - arguments[i+1] = "".join([part.capitalize() for part in arguments[i+1].split("_")]) - if len(arguments[i+1]) > 0: - arguments[i+1] = arguments[i+1][:1].lower() + arguments[i+1][1:] - - # handle case where user supplies endpoint with improper capitalization - if arguments[i+1] not in endpoints: - for endpoint in endpoints: - if arguments[i+1].lower() == endpoint.lower(): - arguments[i+1] = endpoint - break + for idx, arg in enumerate(arguments): + if arg.lower() in {"-e", "--endpoint", "-ep", "--endpoint_params"} and ( + idx + 1 + ) < len(arguments): + # Convert to lowerCamelCase (from snake_case or kebab-case) + raw_arg = arguments[idx + 1] + ep = "".join(part.capitalize() for part in re.split(r"[_-]", raw_arg[1:])) + ep = ep[0].lower() + ep[1:] if ep else "" + + # Match endpoint case-insensitively and replace original argument if found + matched_ep = next((e for e in endpoints if e.lower() == ep.lower()), None) + if matched_ep: + arguments[idx + 1] = matched_ep return arguments diff --git a/tests/test_main.py b/tests/test_main.py index 0dfb8d2..be25047 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -15,6 +15,7 @@ import ferry_cli.__main__ as _main import ferry_cli.config.config as _config + @pytest.fixture def inject_fake_stdin(monkeypatch): def inner(fake_input): @@ -22,6 +23,7 @@ def inner(fake_input): return inner + @pytest.fixture def mock_write_config_file_with_user_values(monkeypatch): def _func(): @@ -33,6 +35,7 @@ def _func(): _func, ) + @pytest.fixture def write_and_set_fake_config_file(monkeypatch, tmp_path): # Fake config file @@ -44,10 +47,12 @@ def write_and_set_fake_config_file(monkeypatch, tmp_path): monkeypatch.setenv("XDG_CONFIG_HOME", str(p.absolute())) return config_file + @pytest.fixture def configfile_doesnt_exist(monkeypatch): monkeypatch.setattr(_config, "get_configfile_path", lambda: None) + @pytest.mark.unit def test_sanitize_base_url(): cases = ["http://hostname.domain:1234/", "http://hostname.domain:1234"] @@ -58,6 +63,7 @@ def test_sanitize_base_url(): complex_case = "http://hostname.domain:1234/apiEndpoint?key1=val1" assert FerryCLI._sanitize_base_url(complex_case) == complex_case + @pytest.mark.unit def test_handle_show_configfile_configfile_exists( capsys, monkeypatch, write_and_set_fake_config_file @@ -79,6 +85,7 @@ def test_handle_show_configfile_configfile_exists( captured = capsys.readouterr() assert captured.out.strip() == case.expected_stdout_substr + @pytest.mark.unit def test_handle_show_configfile_configfile_does_not_exist( capsys, monkeypatch, tmp_path, mock_write_config_file_with_user_values @@ -99,6 +106,7 @@ def test_handle_show_configfile_configfile_does_not_exist( ) assert "Mocked write_config_file" in captured.out + @pytest.mark.unit def test_handle_show_configfile_envs_not_found( capsys, @@ -116,6 +124,7 @@ def test_handle_show_configfile_envs_not_found( ) assert "Mocked write_config_file" in captured.out + @pytest.mark.parametrize( "args, expected_out_substr", [ @@ -137,7 +146,6 @@ def test_handle_show_configfile_envs_not_found( ), # If we pass --show-config-file with other args, --show-config-file should print out the config file ], ) - @pytest.mark.unit def test_show_configfile_flag_with_other_args( tmp_path, monkeypatch, write_and_set_fake_config_file, args, expected_out_substr @@ -155,6 +163,7 @@ def test_show_configfile_flag_with_other_args( pass assert expected_out_substr in str(proc.stdout) + @pytest.mark.unit def test_get_config_info_from_user(monkeypatch, capsys): # test good @@ -175,6 +184,7 @@ def test_get_config_info_from_user(monkeypatch, capsys): ) assert "\nMultiple failures in specifying base URL, exiting..." in captured.out + @pytest.mark.unit def test_help_called(): # Test when "--help" is present in the arguments @@ -189,6 +199,7 @@ def test_help_called(): args = ["command", "arg1", "arg2"] assert help_called(args) == False + @pytest.mark.parametrize( "expected_stdout_before_prompt, user_input, expected_stdout_after_prompt", [ @@ -217,7 +228,6 @@ def test_help_called(): ), ], ) - @pytest.mark.unit def test_handle_no_args_configfile_exists( monkeypatch, @@ -244,6 +254,7 @@ def test_handle_no_args_configfile_exists( assert pytest_wrapped_e.type == SystemExit assert pytest_wrapped_e.value.code == 0 + @pytest.mark.parametrize( "expected_stdout_before_prompt, user_input, expected_stdout_after_prompt", [ @@ -313,7 +324,8 @@ def test_snakecase_and_underscore_conversion(): # test that non endpoint arguments are untouched assert handle_arg_capitalization(test_endpoints, ["-z"]) == ["-z"] - + + @pytest.mark.parametrize( "base_url, expected_base_url", [ @@ -324,7 +336,6 @@ def test_snakecase_and_underscore_conversion(): ), # Get base_url from override ], ) - @pytest.mark.unit def test_override_base_url_FerryCLI(tmp_path, base_url, expected_base_url): # Set up fake config @@ -340,6 +351,7 @@ def test_override_base_url_FerryCLI(tmp_path, base_url, expected_base_url): cli = FerryCLI(config_path=fake_config, base_url=base_url) assert cli.base_url == expected_base_url + @pytest.mark.parametrize( "args, expected_out_url", [ @@ -350,7 +362,6 @@ def test_override_base_url_FerryCLI(tmp_path, base_url, expected_base_url): ), # Get base_url from override ], ) - @pytest.mark.test def test_server_flag_main(tmp_path, monkeypatch, args, expected_out_url): # Run ferry-cli with overridden base_url in dryrun mode to endpoint ping. Then see if we see the correct server in output From 80bb378447c23977a284402a5f734028d003cc97 Mon Sep 17 00:00:00 2001 From: ltrestka Date: Tue, 16 Sep 2025 11:18:22 -0500 Subject: [PATCH 05/10] path needed to strip("/") --- ferry_cli/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ferry_cli/__main__.py b/ferry_cli/__main__.py index 708745f..a063bfc 100755 --- a/ferry_cli/__main__.py +++ b/ferry_cli/__main__.py @@ -453,7 +453,7 @@ def _sanitize_path(raw_path: str) -> str: "/pingus/aa//e/" → "/pingus/aa/e/" """ # Collapse multiple slashes - cleaned = re.sub(r"/+", "/", raw_path.strip()) + cleaned = re.sub(r"/+", "/", raw_path.strip("/")) # Handle special case: empty path if not cleaned: return "/" From 7766a229cbbf24869e1611809e6715f0b8e1d7b4 Mon Sep 17 00:00:00 2001 From: ltrestka Date: Tue, 16 Sep 2025 11:32:17 -0500 Subject: [PATCH 06/10] Added failed call handling --- ferry_cli/__main__.py | 51 +++++++++++++++++----------------------- ferry_cli/helpers/api.py | 4 ++++ 2 files changed, 26 insertions(+), 29 deletions(-) diff --git a/ferry_cli/__main__.py b/ferry_cli/__main__.py index a063bfc..b5edd05 100755 --- a/ferry_cli/__main__.py +++ b/ferry_cli/__main__.py @@ -444,46 +444,39 @@ def _sanitize_path(raw_path: str) -> str: - Collapses multiple internal slashes - Ensures exactly one leading slash - Ensures exactly one trailing slash - - Examples: - "path//to///resource" → "/path/to/resource/" - "/path/to/resource" → "/path/to/resource/" - "///ping" → "/ping/" - "/ping///" → "/ping/" - "/pingus/aa//e/" → "/pingus/aa/e/" """ - # Collapse multiple slashes - cleaned = re.sub(r"/+", "/", raw_path.strip("/")) - # Handle special case: empty path - if not cleaned: - return "/" - return "/" + cleaned.strip("/") + "/" + cleaned = re.sub(r"/+", "/", raw_path.strip()) + return "/" + cleaned.strip("/") + "/" if cleaned else "/" @staticmethod def _sanitize_base_url(raw_base_url: str) -> str: """ - Ensures that the base URL: - - Has a normalized path (leading + trailing slash, collapsed slashes) - - Leaves query and fragment unchanged + Ensures the base URL has a trailing slash **only if**: + - It does not already have one + - It does not include query or fragment parts - Will only modify the URL if: - - The query and fragment are empty, OR - - The path is non-empty and needs sanitization + Leaves URLs with query or fragment untouched. """ - _parts = urlsplit(raw_base_url) + parts = urlsplit(raw_base_url) + + # If query or fragment is present, return as-is + if parts.query or parts.fragment: + return raw_base_url + + # Normalize the path (ensure trailing slash) + path = parts.path or "/" + if not path.endswith("/"): + path += "/" - # Always sanitize the path - sanitized_path = FerryCLI._sanitize_path(_parts.path) + # Collapse multiple slashes in path + path = re.sub(r"/+", "/", path) - parts = SplitResult( - scheme=_parts.scheme, - netloc=_parts.netloc, - path=sanitized_path, - query=_parts.query, - fragment=_parts.fragment, + # Rebuild the URL with sanitized path + sanitized_parts = SplitResult( + scheme=parts.scheme, netloc=parts.netloc, path=path, query="", fragment="" ) - return urlunsplit(parts) + return urlunsplit(sanitized_parts) def __parse_config_file(self: "FerryCLI") -> configparser.ConfigParser: configs = configparser.ConfigParser() diff --git a/ferry_cli/helpers/api.py b/ferry_cli/helpers/api.py index 34e1885..7888256 100755 --- a/ferry_cli/helpers/api.py +++ b/ferry_cli/helpers/api.py @@ -81,6 +81,10 @@ def call_endpoint( raise ValueError("Unsupported HTTP method.") if debug: print(f"Called Endpoint: {response.request.url}") + if not response.ok: + raise RuntimeError( + f" *** API Failure: Status code {response.status_code} returned from endpoint /{endpoint}" + ) output = response.json() output["request_url"] = response.request.url From f06bcd0e0f16183c44ef7d3df7e3e6b658e5c3a3 Mon Sep 17 00:00:00 2001 From: ltrestka Date: Tue, 16 Sep 2025 12:03:26 -0500 Subject: [PATCH 07/10] Keeps leading underscores. Updated Readme with that bit --- README.md | 1 + ferry_cli/__main__.py | 22 ++++++++++++++++------ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 173778a..abc1c06 100644 --- a/README.md +++ b/README.md @@ -242,6 +242,7 @@ Response: { } ``` > Note: All responses are currently stored locally in results.json if the -q flag is not passed, for longer responses - stdout will point to the file, rather than print them in the terminal. +> Endpoints & Workflows that are passed as arguments will be converted to camelCase automatically, however - leading underscores will be preserved, so be sure to use the correct spelling. ## Usage - Custom Workflows Existing workflows are defined in helpers.supported_workflows.* diff --git a/ferry_cli/__main__.py b/ferry_cli/__main__.py index b5edd05..f621938 100755 --- a/ferry_cli/__main__.py +++ b/ferry_cli/__main__.py @@ -605,15 +605,25 @@ def handle_no_args(_config_path: Optional[pathlib.Path]) -> bool: def handle_arg_capitalization( endpoints: Dict[str, Any], arguments: List[str] ) -> List[str]: - # check to see if the arguments supplied are for an endpoint. IE a "-e" was supplied for idx, arg in enumerate(arguments): if arg.lower() in {"-e", "--endpoint", "-ep", "--endpoint_params"} and ( - idx + 1 - ) < len(arguments): - # Convert to lowerCamelCase (from snake_case or kebab-case) + idx + 1 < len(arguments) + ): raw_arg = arguments[idx + 1] - ep = "".join(part.capitalize() for part in re.split(r"[_-]", raw_arg[1:])) - ep = ep[0].lower() + ep[1:] if ep else "" + + # Extract and preserve a single leading underscore, if any + leading_underscore = "_" if raw_arg.startswith("_") else "" + + # Remove all leading underscores before processing + stripped = raw_arg.lstrip("_") + + # Convert to lowerCamelCase from snake_case or kebab-case + parts = re.split(r"[_-]+", stripped) + if not parts: + continue + + camel = parts[0].lower() + "".join(part.capitalize() for part in parts[1:]) + ep = leading_underscore + camel # Match endpoint case-insensitively and replace original argument if found matched_ep = next((e for e in endpoints if e.lower() == ep.lower()), None) From 33fff8a75397f9df54b3c8727b571b390cc600be Mon Sep 17 00:00:00 2001 From: ltrestka Date: Tue, 16 Sep 2025 12:06:44 -0500 Subject: [PATCH 08/10] fixed unit test --- tests/test_main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_main.py b/tests/test_main.py index be25047..98a16b9 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -313,7 +313,7 @@ def test_handle_no_args_configfile_does_not_exist( def test_snakecase_and_underscore_conversion(): test_endpoints = ["getUserInfo"] correct_args = ["-e", "getUserInfo", "--username=johndoe"] - test_args_case_underscore = ["-e", "_Get_USeriNFo", "--username=johndoe"] + test_args_case_underscore = ["-e", "Get_USeriNFo", "--username=johndoe"] result = handle_arg_capitalization(test_endpoints, test_args_case_underscore) # test to make sure function does matching irrespective of capitalization From 3f512ca6fe032839853c5c76f9cb04ee4333712b Mon Sep 17 00:00:00 2001 From: ltrestka Date: Fri, 10 Oct 2025 09:50:41 -0500 Subject: [PATCH 09/10] - addressed comments - renamed function to "normalize_endpoint" - removes args enumeration - called in endpoint/endpoint_params handler before safeguard. --- ferry_cli/__main__.py | 51 +++++++++++++++---------------------------- 1 file changed, 18 insertions(+), 33 deletions(-) diff --git a/ferry_cli/__main__.py b/ferry_cli/__main__.py index f621938..a400fff 100755 --- a/ferry_cli/__main__.py +++ b/ferry_cli/__main__.py @@ -227,6 +227,7 @@ def __call__( # type: ignore def get_endpoint_params_action(self): # type: ignore safeguards = self.safeguards + ferrycli = self ferrycli_get_endpoint_params = self.get_endpoint_params class _GetEndpointParams(argparse.Action): @@ -234,8 +235,9 @@ def __call__( # type: ignore self: "_GetEndpointParams", parser, args, values, option_string=None ) -> None: # Prevent DCS from running this endpoint if necessary, and print proper steps to take instead. - safeguards.verify(values) - ferrycli_get_endpoint_params(values) + ep = normalize_endpoint(ferrycli.endpoints, values) + safeguards.verify(ep) + ferrycli_get_endpoint_params(ep) sys.exit(0) return _GetEndpointParams @@ -358,9 +360,10 @@ def run( if args.endpoint: # Prevent DCS from running this endpoint if necessary, and print proper steps to take instead. - self.safeguards.verify(args.endpoint) + ep = normalize_endpoint(self.endpoints, args.endpoint) + self.safeguards.verify(ep) try: - json_result = self.execute_endpoint(args.endpoint, endpoint_args) + json_result = self.execute_endpoint(ep, endpoint_args) except Exception as e: raise Exception(f"{e}") if not dryrun: @@ -602,34 +605,17 @@ def handle_no_args(_config_path: Optional[pathlib.Path]) -> bool: sys.exit(0) -def handle_arg_capitalization( - endpoints: Dict[str, Any], arguments: List[str] -) -> List[str]: - for idx, arg in enumerate(arguments): - if arg.lower() in {"-e", "--endpoint", "-ep", "--endpoint_params"} and ( - idx + 1 < len(arguments) - ): - raw_arg = arguments[idx + 1] - - # Extract and preserve a single leading underscore, if any - leading_underscore = "_" if raw_arg.startswith("_") else "" - - # Remove all leading underscores before processing - stripped = raw_arg.lstrip("_") - - # Convert to lowerCamelCase from snake_case or kebab-case - parts = re.split(r"[_-]+", stripped) - if not parts: - continue - - camel = parts[0].lower() + "".join(part.capitalize() for part in parts[1:]) - ep = leading_underscore + camel - - # Match endpoint case-insensitively and replace original argument if found - matched_ep = next((e for e in endpoints if e.lower() == ep.lower()), None) - if matched_ep: - arguments[idx + 1] = matched_ep - return arguments +def normalize_endpoint(endpoints: Dict[str, Any], raw: str) -> str: + # Extract and preserve a single leading underscore, if any + leading_underscore = "_" if raw.startswith("_") else "" + # Remove all leading underscores before processing + stripped = raw.lstrip("_") + # Convert to lowerCamelCase from snake_case or kebab-case + parts = re.split(r"[_-]+", stripped) + camel = parts[0].lower() + "".join(part.capitalize() for part in parts[1:]) + normalized = leading_underscore + camel + # Match endpoint case-insensitively and replace original argument if found + return next((ep for ep in endpoints if ep.lower() == normalized.lower()), raw) # pylint: disable=too-many-branches @@ -693,7 +679,6 @@ def main() -> None: sys.exit(1) ferry_cli.endpoints = ferry_cli.generate_endpoints() - other_args = handle_arg_capitalization(ferry_cli.endpoints, other_args) ferry_cli.run( auth_args.debug_level, auth_args.dryrun, From 0bb8706da91d0bc7887cefb6302f0350229405a1 Mon Sep 17 00:00:00 2001 From: ltrestka Date: Fri, 10 Oct 2025 09:58:49 -0500 Subject: [PATCH 10/10] updated unit tests for normalized endpoints --- tests/test_main.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/tests/test_main.py b/tests/test_main.py index 98a16b9..d345fac 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -10,7 +10,7 @@ handle_show_configfile, get_config_info_from_user, help_called, - handle_arg_capitalization, + normalize_endpoint, ) import ferry_cli.__main__ as _main import ferry_cli.config.config as _config @@ -311,19 +311,27 @@ def test_handle_no_args_configfile_does_not_exist( @pytest.mark.unit def test_snakecase_and_underscore_conversion(): - test_endpoints = ["getUserInfo"] - correct_args = ["-e", "getUserInfo", "--username=johndoe"] - test_args_case_underscore = ["-e", "Get_USeriNFo", "--username=johndoe"] - result = handle_arg_capitalization(test_endpoints, test_args_case_underscore) + test_endpoints = {"getUserInfo": object()} # test to make sure function does matching irrespective of capitalization - assert result == correct_args + assert normalize_endpoint(test_endpoints, "Get_USeriNFo") == "getUserInfo" # test to make sure function never stops working for correct syntax - assert handle_arg_capitalization(test_endpoints, correct_args) == correct_args + assert normalize_endpoint(test_endpoints, "getUserInfo") == "getUserInfo" - # test that non endpoint arguments are untouched - assert handle_arg_capitalization(test_endpoints, ["-z"]) == ["-z"] + # test that non-endpoint values are left untouched when no match is found + assert ( + normalize_endpoint(test_endpoints, "SomeOtherEndpoint") == "SomeOtherEndpoint" + ) + + +@pytest.mark.unit +def test_leading_underscore_preserved(): + test_endpoints = {"_internalEndpoint": object()} + + assert ( + normalize_endpoint(test_endpoints, "_Internal_endpoint") == "_internalEndpoint" + ) @pytest.mark.parametrize(