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
11 changes: 7 additions & 4 deletions skills/references/auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,15 @@ kaggle auth print-access-token [options]

**Options:**

- `--expiration <DURATION>`: Override token duration.
- `--expiration <DURATION>`: 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
Expand All @@ -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`

Expand Down
38 changes: 34 additions & 4 deletions src/kaggle/api/kaggle_api_extended.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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.
Expand Down
3 changes: 2 additions & 1 deletion src/kaggle/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
62 changes: 62 additions & 0 deletions tests/unit/test_parse_duration.py
Original file line number Diff line number Diff line change
@@ -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}")
Loading