Skip to content

Commit 1a13c7f

Browse files
authored
Merge pull request #97 from fermitools/lydia_snake_case
added snake case feature and tests
2 parents e1b4f40 + 0bb8706 commit 1a13c7f

File tree

6 files changed

+89
-24
lines changed

6 files changed

+89
-24
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ config/swagger.json
1515
/tmp/*
1616
.make_creds.sh
1717
remove
18+
.vscode

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
repos:
22
- repo: https://github.com/pre-commit/pre-commit-hooks
3-
rev: v2.3.0
3+
rev: v5.0.0
44
hooks:
55
- id: check-toml
66
- id: end-of-file-fixer

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@ Response: {
242242
}
243243
```
244244
> 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.
245+
> 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.
245246
246247
## Usage - Custom Workflows
247248
Existing workflows are defined in helpers.supported_workflows.*

ferry_cli/__main__.py

Lines changed: 56 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import json
44
import os
55
import pathlib
6+
import re
67
import sys
78
import textwrap
89
from typing import Any, Dict, Optional, List, Type
@@ -226,15 +227,17 @@ def __call__( # type: ignore
226227

227228
def get_endpoint_params_action(self): # type: ignore
228229
safeguards = self.safeguards
230+
ferrycli = self
229231
ferrycli_get_endpoint_params = self.get_endpoint_params
230232

231233
class _GetEndpointParams(argparse.Action):
232234
def __call__( # type: ignore
233235
self: "_GetEndpointParams", parser, args, values, option_string=None
234236
) -> None:
235237
# Prevent DCS from running this endpoint if necessary, and print proper steps to take instead.
236-
safeguards.verify(values)
237-
ferrycli_get_endpoint_params(values)
238+
ep = normalize_endpoint(ferrycli.endpoints, values)
239+
safeguards.verify(ep)
240+
ferrycli_get_endpoint_params(ep)
238241
sys.exit(0)
239242

240243
return _GetEndpointParams
@@ -357,9 +360,10 @@ def run(
357360

358361
if args.endpoint:
359362
# Prevent DCS from running this endpoint if necessary, and print proper steps to take instead.
360-
self.safeguards.verify(args.endpoint)
363+
ep = normalize_endpoint(self.endpoints, args.endpoint)
364+
self.safeguards.verify(ep)
361365
try:
362-
json_result = self.execute_endpoint(args.endpoint, endpoint_args)
366+
json_result = self.execute_endpoint(ep, endpoint_args)
363367
except Exception as e:
364368
raise Exception(f"{e}")
365369
if not dryrun:
@@ -437,29 +441,45 @@ def error_raised(
437441
error_raised(Exception, f"Error: {e}")
438442

439443
@staticmethod
440-
def _sanitize_base_url(raw_base_url: str) -> str:
441-
"""This function makes sure we have a trailing forward-slash on the base_url before it's passed
442-
to any other functions
444+
def _sanitize_path(raw_path: str) -> str:
445+
"""
446+
Normalizes a URL path:
447+
- Collapses multiple internal slashes
448+
- Ensures exactly one leading slash
449+
- Ensures exactly one trailing slash
450+
"""
451+
cleaned = re.sub(r"/+", "/", raw_path.strip())
452+
return "/" + cleaned.strip("/") + "/" if cleaned else "/"
443453

444-
That is, "http://hostname.domain:port" --> "http://hostname.domain:port/" but
445-
"http://hostname.domain:port/" --> "http://hostname.domain:port/" and
446-
"http://hostname.domain:port/path?querykey1=value1&querykey2=value2" --> "http://hostname.domain:port/path?querykey1=value1&querykey2=value2" and
454+
@staticmethod
455+
def _sanitize_base_url(raw_base_url: str) -> str:
456+
"""
457+
Ensures the base URL has a trailing slash **only if**:
458+
- It does not already have one
459+
- It does not include query or fragment parts
447460
448-
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
461+
Leaves URLs with query or fragment untouched.
449462
"""
450-
_parts = urlsplit(raw_base_url)
451-
parts = (
452-
SplitResult(
453-
scheme=_parts.scheme,
454-
netloc=_parts.netloc,
455-
path="/",
456-
query=_parts.query,
457-
fragment=_parts.fragment,
458-
)
459-
if (_parts.path == "" and _parts.query == "" and _parts.fragment == "")
460-
else _parts
463+
parts = urlsplit(raw_base_url)
464+
465+
# If query or fragment is present, return as-is
466+
if parts.query or parts.fragment:
467+
return raw_base_url
468+
469+
# Normalize the path (ensure trailing slash)
470+
path = parts.path or "/"
471+
if not path.endswith("/"):
472+
path += "/"
473+
474+
# Collapse multiple slashes in path
475+
path = re.sub(r"/+", "/", path)
476+
477+
# Rebuild the URL with sanitized path
478+
sanitized_parts = SplitResult(
479+
scheme=parts.scheme, netloc=parts.netloc, path=path, query="", fragment=""
461480
)
462-
return urlunsplit(parts)
481+
482+
return urlunsplit(sanitized_parts)
463483

464484
def __parse_config_file(self: "FerryCLI") -> configparser.ConfigParser:
465485
configs = configparser.ConfigParser()
@@ -585,6 +605,19 @@ def handle_no_args(_config_path: Optional[pathlib.Path]) -> bool:
585605
sys.exit(0)
586606

587607

608+
def normalize_endpoint(endpoints: Dict[str, Any], raw: str) -> str:
609+
# Extract and preserve a single leading underscore, if any
610+
leading_underscore = "_" if raw.startswith("_") else ""
611+
# Remove all leading underscores before processing
612+
stripped = raw.lstrip("_")
613+
# Convert to lowerCamelCase from snake_case or kebab-case
614+
parts = re.split(r"[_-]+", stripped)
615+
camel = parts[0].lower() + "".join(part.capitalize() for part in parts[1:])
616+
normalized = leading_underscore + camel
617+
# Match endpoint case-insensitively and replace original argument if found
618+
return next((ep for ep in endpoints if ep.lower() == normalized.lower()), raw)
619+
620+
588621
# pylint: disable=too-many-branches
589622
def main() -> None:
590623
_config_path = config.get_configfile_path()

ferry_cli/helpers/api.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ def call_endpoint(
8181
raise ValueError("Unsupported HTTP method.")
8282
if debug:
8383
print(f"Called Endpoint: {response.request.url}")
84+
if not response.ok:
85+
raise RuntimeError(
86+
f" *** API Failure: Status code {response.status_code} returned from endpoint /{endpoint}"
87+
)
8488
output = response.json()
8589

8690
output["request_url"] = response.request.url

tests/test_main.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
handle_show_configfile,
1111
get_config_info_from_user,
1212
help_called,
13+
normalize_endpoint,
1314
)
1415
import ferry_cli.__main__ as _main
1516
import ferry_cli.config.config as _config
@@ -308,6 +309,31 @@ def test_handle_no_args_configfile_does_not_exist(
308309
assert pytest_wrapped_e.value.code == 0
309310

310311

312+
@pytest.mark.unit
313+
def test_snakecase_and_underscore_conversion():
314+
test_endpoints = {"getUserInfo": object()}
315+
316+
# test to make sure function does matching irrespective of capitalization
317+
assert normalize_endpoint(test_endpoints, "Get_USeriNFo") == "getUserInfo"
318+
319+
# test to make sure function never stops working for correct syntax
320+
assert normalize_endpoint(test_endpoints, "getUserInfo") == "getUserInfo"
321+
322+
# test that non-endpoint values are left untouched when no match is found
323+
assert (
324+
normalize_endpoint(test_endpoints, "SomeOtherEndpoint") == "SomeOtherEndpoint"
325+
)
326+
327+
328+
@pytest.mark.unit
329+
def test_leading_underscore_preserved():
330+
test_endpoints = {"_internalEndpoint": object()}
331+
332+
assert (
333+
normalize_endpoint(test_endpoints, "_Internal_endpoint") == "_internalEndpoint"
334+
)
335+
336+
311337
@pytest.mark.parametrize(
312338
"base_url, expected_base_url",
313339
[

0 commit comments

Comments
 (0)