Skip to content

Commit

Permalink
appscheduler: support dates when scheduling events
Browse files Browse the repository at this point in the history
Closes #30
  • Loading branch information
eras committed Oct 31, 2023
1 parent b55f541 commit 9c9c170
Show file tree
Hide file tree
Showing 3 changed files with 216 additions and 3 deletions.
6 changes: 3 additions & 3 deletions teslabot/appscheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def valid_schedulable(app_scheduler: "AppScheduler[T]",
ScheduleAtArgs = Tuple[datetime.datetime,
CommandWithArgs]
def valid_schedule_at(app_scheduler: "AppScheduler[T]") -> p.Parser[ScheduleAtArgs]:
return p.Remaining(p.Adjacent(p.Time(), valid_schedulable(app_scheduler, include_every=True, include_until=True)))
return p.Remaining(p.Adjacent(p.TimeOrDateTime(), valid_schedulable(app_scheduler, include_every=True, include_until=True)))

ScheduleEveryArgs = Tuple[Tuple[datetime.timedelta,
Optional[datetime.datetime]],
Expand All @@ -103,14 +103,14 @@ def valid_schedule_every(app_scheduler: "AppScheduler[T]", include_until: bool)
"until",
p.Optional_(
p.Conditional(lambda: include_until,
p.Keyword("until", p.Time()))))),
p.Keyword("until", p.TimeOrDateTime()))))),
valid_schedulable(app_scheduler, include_every=False, include_until=include_until)))

ScheduleUntilArgs = Tuple[Tuple[datetime.datetime,
Optional[datetime.timedelta]],
CommandWithArgs]
def valid_schedule_until(app_scheduler: "AppScheduler[T]", include_every: bool) -> p.Parser[ScheduleUntilArgs]:
return p.Remaining(p.Adjacent(p.Adjacent(p.Time(),
return p.Remaining(p.Adjacent(p.Adjacent(p.TimeOrDateTime(),
p.Labeled(
"every",
p.Optional_(
Expand Down
66 changes: 66 additions & 0 deletions teslabot/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -838,6 +838,36 @@ def parse(self, args: List[str]) -> ParseResult[float]:
meters = value.value * multiplier
return ParseOK(meters, processed=result.processed)

@dataclass
class _DateWrapped:
yyyymmdd: Optional[Tuple[Optional[str], ...]] = None

class Date(Parser[datetime.date]):
regex: Parser[_DateWrapped]
today: Optional[datetime.date] # future use to support relative dates

def __init__(self, today: Optional[datetime.date] = None) -> None:
self.regex = Map(map=lambda x: _DateWrapped(yyyymmdd=x),
parser=Regex(r"^([0-9]{4})-(0*[0-9]{1,2})-(0*[0-9]{1,2})$"))
self.today = today

def parse(self, args: List[str]) -> ParseResult[datetime.date]:
if len(args) == 0:
return ParseFail("No argument provided", processed=0)
result = self.regex.parse(args)
if isinstance(result, ParseFail):
return ParseFail("Failed to parse date", processed=0)
else:
assert isinstance(result, ParseOK)
yyyymmdd = assert_some(result.value.yyyymmdd)
try:
return ParseOK(datetime.date(year=int(assert_some(yyyymmdd[0])),
month=int(assert_some(yyyymmdd[1])),
day=int(assert_some(yyyymmdd[2]))),
processed=result.processed)
except ValueError as exc:
return ParseFail(exc.args[0], processed=0)

@dataclass
class _TimeWrapped:
hhmm: Optional[Tuple[Optional[str], ...]] = None
Expand Down Expand Up @@ -902,3 +932,39 @@ def get(x: SuffixInfo[int]) -> int:
else:
assert False
return ParseOK(time, processed=result.processed)

class DateTime(Parser[datetime.datetime]):
today: Optional[datetime.date]
parser: Adjacent[datetime.date, Tuple[int, int]]

def __init__(self, today: Optional[datetime.date] = None) -> None:
self.today = today
self.parser = Adjacent(Date(today=self.today), HhMm())

def parse(self, args: List[str]) -> ParseResult[datetime.datetime]:
result = self.parser.parse(args)
if isinstance(result, ParseOK):
now = datetime.datetime.combine(result.value[0],
datetime.time(hour=result.value[1][0],
minute=result.value[1][1]))
return ParseOK(now, processed=result.processed)
else:
assert isinstance(result, ParseFail)
return ParseFail(message=result.message,
processed=result.processed)

class TimeOrDateTime(Parser[datetime.datetime]):
parser: Parser[datetime.datetime]

def __init__(self, now: Optional[datetime.datetime] = None) -> None:
today = map_optional(now, lambda x: x.date())
self.parser = OneOf(Labeled("datetime", DateTime(today=today)),
Labeled("time", Time(now=now)))

def parse(self, args: List[str]) -> ParseResult[datetime.datetime]:
result = self.parser.parse(args)
if isinstance(result, ParseOK):
return ParseOK(result.value, processed=result.processed)
else:
assert isinstance(result, ParseFail)
return result.forward(processed=0)
147 changes: 147 additions & 0 deletions tests/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,66 @@ def test_hhmm(self) -> None:
self.assertEqual(p.HhMm().parse(["10:10"]),
p.ParseOK((10, 10), processed=1))

def test_date(self) -> None:
now = datetime.datetime.fromisoformat("2022-02-22 01:00")
today = now.date()
with self.subTest():
self.assertEqual(p.Date(today=today).parse([]),
p.ParseFail("No argument provided", processed=0))

with self.subTest():
self.assertEqual(p.Date(today=today).parse(["2022-03-03"]),
p.ParseOK(datetime.date(year=2022,
month=3,
day=3),
processed=1))

with self.subTest():
self.assertEqual(p.Date(today=today).parse(["2022-3-0003"]),
p.ParseOK(datetime.date(year=2022,
month=3,
day=3),
processed=1))

with self.subTest():
self.assertEqual(p.Date(today=today).parse(["2022-3-99"]),
p.ParseFail("day is out of range for month",
processed=0))

with self.subTest():
self.assertEqual(p.Date(today=today).parse(["2022-99-3"]),
p.ParseFail("month must be in 1..12",
processed=0))

with self.subTest():
self.assertEqual(p.Date(today=today).parse(["0000-9-3"]),
p.ParseFail("year 0 is out of range",
processed=0))

with self.subTest():
self.assertEqual(p.Date(today=today).parse(["2022-03-"]),
p.ParseFail("Failed to parse date",
processed=0))

with self.subTest():
self.assertEqual(p.Date(today=today).parse(["-03-03"]),
p.ParseFail("Failed to parse date",
processed=0))
with self.subTest():
self.assertEqual(p.Date(today=today).parse(["2022"]),
p.ParseFail("Failed to parse date",
processed=0))

with self.subTest():
self.assertEqual(p.Date(today=today).parse([""]),
p.ParseFail("Failed to parse date",
processed=0))

with self.subTest():
self.assertEqual(p.Date(today=today).parse([" "]),
p.ParseFail("Failed to parse date",
processed=0))

def test_time(self) -> None:
now = datetime.datetime.fromisoformat("2022-02-22 01:00")
today = now.date()
Expand Down Expand Up @@ -591,6 +651,93 @@ def test_time(self) -> None:
datetime.time(11, 10)),
processed=3))

def test_datetime(self) -> None:
now = datetime.datetime.fromisoformat("2022-02-22 01:00")
today = now.date()
with self.subTest():
self.assertEqual(p.DateTime(today=today).parse([]),
p.ParseFail("No adjacent arguments parsed completely", processed=0))

with self.subTest():
self.assertEqual(p.DateTime(today=today).parse(["2022-03-03"]),
p.ParseFail("No argument provided while parsing right argument", processed=1))

with self.subTest():
self.assertEqual(p.DateTime(today=today).parse(["2022-03-03", "01:00"]),
p.ParseOK(datetime.datetime(year=2022,
month=3,
day=3,
hour=1,
minute=00),
processed=2))

def test_time_or_datetime(self) -> None:
now = datetime.datetime.fromisoformat("2022-02-22 01:00")
today = now.date()
with self.subTest():
self.assertEqual(p.TimeOrDateTime(now=now).parse([]),
p.ParseFail("No argument provided", processed=0))

with self.subTest():
self.assertEqual(p.TimeOrDateTime(now=now).parse(["2022-03-03"]),
p.ParseFail("No argument provided while parsing right argument", processed=1))

with self.subTest():
self.assertEqual(p.TimeOrDateTime(now=now).parse(["2022-03-03", "01:00"]),
p.ParseOK(datetime.datetime(year=2022,
month=3,
day=3,
hour=1,
minute=00),
processed=2))

with self.subTest():
self.assertEqual(p.TimeOrDateTime(now=now).parse(["00:00"]),
p.ParseOK(datetime.datetime.combine(today,
datetime.time(0, 0)) +
datetime.timedelta(days=1),
processed=1))
with self.subTest():
self.assertEqual(p.TimeOrDateTime(now=now).parse(["01:00"]),
p.ParseOK(datetime.datetime.combine(today,
datetime.time(1, 0)),
processed=1))
with self.subTest():
self.assertEqual(p.TimeOrDateTime(now=now).parse(["02:00"]),
p.ParseOK(datetime.datetime.combine(today,
datetime.time(2, 0)),
processed=1))
with self.subTest():
self.assertEqual(p.TimeOrDateTime(now=now).parse(["0m"]),
p.ParseOK(datetime.datetime.combine(today,
datetime.time(1, 0)),
processed=1))
with self.subTest():
self.assertEqual(p.TimeOrDateTime(now=now).parse(["10m"]),
p.ParseOK(datetime.datetime.combine(today,
datetime.time(1, 10)),
processed=1))
with self.subTest():
self.assertEqual(p.TimeOrDateTime(now=now).parse(["60m"]),
p.ParseOK(datetime.datetime.combine(today,
datetime.time(2, 0)),
processed=1))
with self.subTest():
self.assertEqual(p.TimeOrDateTime(now=now).parse(["0h"]),
p.ParseOK(datetime.datetime.combine(today,
datetime.time(1, 0)),
processed=1))
with self.subTest():
self.assertEqual(p.TimeOrDateTime(now=now).parse(["12h"]),
p.ParseOK(datetime.datetime.combine(today,
datetime.time(13, 0)),
processed=1))
with self.subTest():
self.assertEqual(p.TimeOrDateTime(now=now).parse(["12h12m"]),
p.ParseOK(datetime.datetime.combine(today,
datetime.time(13, 12)),
processed=1))

def test_rest_as_list(self) -> None:
with self.subTest():
self.assertEqual(p.List_(p.AnyStr()).parse([]),
Expand Down

0 comments on commit 9c9c170

Please sign in to comment.