Adds int_or_interval format parser

Accepts either int or interval, first tries parsing int then tries
parsing as interval if that fails. Returns a timedelta for easy date
math later. Now allows intervals of length 0 as a 0-length timedelta is
perfectly fine to work with.
This commit is contained in:
Hugo Wallenburg 2025-04-19 20:43:10 +02:00
parent a8f1ed97e0
commit 39bdfaaa2e
No known key found for this signature in database
3 changed files with 65 additions and 11 deletions

View file

@ -27,7 +27,7 @@ from .misc import sysinfo, log_multi, consume
from .misc import ChunkIteratorFileWrapper, open_item, chunkit, iter_separated, ErrorIgnoringTextIOWrapper
from .parseformat import bin_to_hex, hex_to_bin, safe_encode, safe_decode
from .parseformat import text_to_json, binary_to_json, remove_surrogates, join_cmd
from .parseformat import eval_escapes, decode_dict, positive_int_validator, interval
from .parseformat import eval_escapes, decode_dict, positive_int_validator, interval, int_or_interval
from .parseformat import PathSpec, SortBySpec, ChunkerParams, FilesCacheMode, partial_format, DatetimeWrapper
from .parseformat import format_file_size, parse_file_size, FileSize
from .parseformat import sizeof_fmt, sizeof_fmt_iec, sizeof_fmt_decimal, Location, text_validator

View file

@ -13,7 +13,7 @@ import uuid
from pathlib import Path
from typing import ClassVar, Any, TYPE_CHECKING, Literal
from collections import OrderedDict
from datetime import datetime, timezone
from datetime import datetime, timezone, timedelta
from functools import partial
from string import Formatter
@ -155,12 +155,24 @@ def interval(s):
except ValueError:
seconds = -1
if seconds <= 0:
raise argparse.ArgumentTypeError(f'Invalid number "{number}": expected positive integer')
if seconds < 0:
raise argparse.ArgumentTypeError(f'Invalid number "{number}": expected nonnegative integer')
return seconds
def int_or_interval(s):
try:
return int(s)
except ValueError:
pass
try:
return timedelta(seconds=interval(s))
except argparse.ArgumentTypeError as e:
raise argparse.ArgumentTypeError(f"Value is neither an integer nor an interval: {e}")
def ChunkerParams(s):
params = s.strip().split(",")
count = len(params)

View file

@ -1,7 +1,8 @@
import base64
import os
import re
from argparse import ArgumentTypeError
from datetime import datetime, timezone
from datetime import datetime, timedelta, timezone
import pytest
@ -16,6 +17,7 @@ from ...helpers.parseformat import (
format_file_size,
parse_file_size,
interval,
int_or_interval,
partial_format,
clean_lines,
format_line,
@ -376,6 +378,7 @@ def test_format_timedelta():
@pytest.mark.parametrize(
"timeframe, num_secs",
[
("0S", 0),
("5S", 5),
("2M", 2 * 60),
("1H", 60 * 60),
@ -392,9 +395,9 @@ def test_interval(timeframe, num_secs):
@pytest.mark.parametrize(
"invalid_interval, error_tuple",
[
("H", ('Invalid number "": expected positive integer',)),
("-1d", ('Invalid number "-1": expected positive integer',)),
("food", ('Invalid number "foo": expected positive integer',)),
("H", ('Invalid number "": expected nonnegative integer',)),
("-1d", ('Invalid number "-1": expected nonnegative integer',)),
("food", ('Invalid number "foo": expected nonnegative integer',)),
],
)
def test_interval_time_unit(invalid_interval, error_tuple):
@ -403,10 +406,49 @@ def test_interval_time_unit(invalid_interval, error_tuple):
assert exc.value.args == error_tuple
def test_interval_number():
@pytest.mark.parametrize(
"invalid_input, error_regex",
[
("x", r'^Unexpected time unit "x": choose from'),
("-1t", r'^Unexpected time unit "t": choose from'),
("fool", r'^Unexpected time unit "l": choose from'),
("abc", r'^Unexpected time unit "c": choose from'),
(" abc ", r'^Unexpected time unit " ": choose from'),
],
)
def test_interval_invalid_time_format(invalid_input, error_regex):
with pytest.raises(ArgumentTypeError) as exc:
interval("5")
assert exc.value.args == ('Unexpected time unit "5": choose from y, m, w, d, H, M, S',)
interval(invalid_input)
assert re.search(error_regex, exc.value.args[0])
@pytest.mark.parametrize(
"input, result",
[
("0", 0),
("5", 5),
(" 999 ", 999),
("0S", timedelta(seconds=0)),
("5S", timedelta(seconds=5)),
("1m", timedelta(days=31)),
],
)
def test_int_or_interval(input, result):
assert int_or_interval(input) == result
@pytest.mark.parametrize(
"invalid_input, error_regex",
[
("H", r"Value is neither an integer nor an interval:"),
("-1d", r"Value is neither an integer nor an interval:"),
("food", r"Value is neither an integer nor an interval:"),
],
)
def test_int_or_interval_time_unit(invalid_input, error_regex):
with pytest.raises(ArgumentTypeError) as exc:
int_or_interval(invalid_input)
assert re.search(error_regex, exc.value.args[0])
def test_parse_timestamp():