diff --git a/skills/references/auth.md b/skills/references/auth.md index f0a773aa..4693c556 100644 --- a/skills/references/auth.md +++ b/skills/references/auth.md @@ -77,12 +77,15 @@ kaggle auth print-access-token [options] **Options:** -- `--expiration `: Override token duration. +- `--expiration `: Override token duration. A positive integer + followed by a unit suffix: `s` (seconds), `m` (minutes), `h` (hours), + `d` (days) or `w` (weeks). Example: `6h`, `30m`, `2d`. **Examples:** ```bash kaggle auth print-access-token +kaggle auth print-access-token --expiration 6h ``` **Purpose:** Emit a token that can be placed in `KAGGLE_API_TOKEN` or another @@ -93,9 +96,9 @@ supported token source. - Requires OAuth credentials from `kaggle auth login`. - If no OAuth credentials exist, the command tells the user to run `kaggle auth login`. -- Some CLI help examples use short forms such as `6h`. For automation, omit - `--expiration` unless the accepted duration format has been verified with - the installed CLI. +- `--expiration` accepts a positive integer followed by a single unit suffix + (`s`, `m`, `h`, `d`, `w`), e.g. `6h` or `2d`. Compound (`2h30s`) and colon + (`2:30`) formats are not supported. ## `kaggle auth revoke` diff --git a/src/kaggle/api/kaggle_api_extended.py b/src/kaggle/api/kaggle_api_extended.py index b63a9faa..00138539 100644 --- a/src/kaggle/api/kaggle_api_extended.py +++ b/src/kaggle/api/kaggle_api_extended.py @@ -1617,7 +1617,8 @@ def auth_print_access_token(self, expiration_duration: Optional[str] = None): If an expiration duration is provided, a new token will be generated with the specified expiration duration. Otherwise, the current token will be printed. - The expiration duration should be in the format of a string with a number followed by a unit, + The expiration duration should be a positive integer followed by a single unit suffix: + s (seconds), m (minutes), h (hours), d (days) or w (weeks), e.g. "1h" for one hour, "2d" for two days, etc. Args: @@ -1636,12 +1637,41 @@ def auth_print_access_token(self, expiration_duration: Optional[str] = None): exit(1) print(response.token) + # Maps the single-letter unit suffix accepted on the command line to the + # corresponding ``dateutil.relativedelta`` keyword argument. relativedelta + # only accepts the plural keyword names (e.g. ``hours``), so the abbreviated + # suffixes must be translated before being passed through. + _DURATION_UNITS = { + "s": "seconds", + "m": "minutes", + "h": "hours", + "d": "days", + "w": "weeks", + } + def _parse_duration(self, duration_str: str) -> relativedelta: + """Parses a duration string such as "6h" into a ``relativedelta``. + + The duration must be a positive integer followed by a single unit + suffix: ``s`` (seconds), ``m`` (minutes), ``h`` (hours), ``d`` (days) + or ``w`` (weeks), e.g. "30s", "5m", "6h", "2d", "2w". + """ + invalid = ValueError( + "Invalid duration format. Please provide a positive integer followed by a unit: " + "s (seconds), m (minutes), h (hours), d (days) or w (weeks), e.g. 30s, 5m, 6h, 2d, 2w." + ) + if not duration_str: + raise invalid + unit = self._DURATION_UNITS.get(duration_str[-1]) + if unit is None: + raise invalid try: - delta = relativedelta(**{duration_str[-1]: int(duration_str[:-1])}) # type: ignore[arg-type] - return delta + value = int(duration_str[:-1]) except ValueError: - raise ValueError("Invalid duration format. Please use one of the following formats: 1h, 30s, 2h30s, 2:30") + raise invalid + if value <= 0: + raise invalid + return relativedelta(**{unit: value}) # type: ignore[arg-type] def auth_revoke_token(self, reason: str): """Revokes the current OAuth access token. diff --git a/src/kaggle/cli.py b/src/kaggle/cli.py index cf07a069..bcbb4719 100644 --- a/src/kaggle/cli.py +++ b/src/kaggle/cli.py @@ -2279,7 +2279,8 @@ def parse_auth(subparsers) -> None: "--expiration", dest="expiration_duration", required=False, - help="Override the default expiration duration. Example: 6h for 6 hours, 2:30 for 2 hours and 30 minutes.", + help="Override the default expiration duration. A positive integer followed by a unit: " + "s (seconds), m (minutes), h (hours), d (days) or w (weeks). Example: 6h for 6 hours, 30m for 30 minutes.", ) parser_auth_print_access_token.set_defaults(func=api.auth_print_access_token) diff --git a/tests/unit/test_parse_duration.py b/tests/unit/test_parse_duration.py new file mode 100644 index 00000000..bef2f7c0 --- /dev/null +++ b/tests/unit/test_parse_duration.py @@ -0,0 +1,62 @@ +# coding=utf-8 +"""Regression tests for KaggleApi._parse_duration (auth print-access-token --expiration).""" + +import pytest +from dateutil.relativedelta import relativedelta + + +@pytest.mark.parametrize( + "duration_str, expected", + [ + ("30s", relativedelta(seconds=30)), + ("5m", relativedelta(minutes=5)), + ("6h", relativedelta(hours=6)), + ("2d", relativedelta(days=2)), + ("2w", relativedelta(weeks=2)), + ("1h", relativedelta(hours=1)), + ], +) +def test_parse_duration_valid_formats(api, duration_str, expected): + result = api._parse_duration(duration_str) + assert isinstance(result, relativedelta) + assert result == expected + + +def test_parse_duration_normalizes_weeks_to_days(api): + # relativedelta stores weeks as days; 2w == 14 days. + assert api._parse_duration("2w") == relativedelta(days=14) + + +@pytest.mark.parametrize( + "duration_str", + [ + "", # empty + "6", # missing unit + "h", # missing value + "6x", # unknown unit + "6H", # units are case-sensitive + "2:30", # colon format is not supported + "2h30s", # compound format is not supported + "abc", # not numeric + "-5m", # non-positive value + "0h", # zero is not a valid duration + "1.5h", # non-integer value + ], +) +def test_parse_duration_invalid_formats_raise_value_error(api, duration_str): + with pytest.raises(ValueError) as exc_info: + api._parse_duration(duration_str) + # Users must get the friendly guidance, never a raw TypeError traceback. + assert "Invalid duration format" in str(exc_info.value) + + +def test_parse_duration_does_not_raise_type_error(api): + # The original bug passed single-letter kwargs to relativedelta, raising an + # uncaught TypeError. Ensure malformed input never surfaces a TypeError. + for bad in ["6h", "2d", "30s", "5m", "2w", "6x", ""]: + try: + api._parse_duration(bad) + except ValueError: + pass # acceptable, friendly error + except TypeError as exc: # pragma: no cover - regression guard + pytest.fail(f"_parse_duration({bad!r}) raised TypeError: {exc}")