diff --git a/src/pomodouroboros/model/schema.py b/src/pomodouroboros/model/schema.py index 33c0bec..7a0f3d3 100644 --- a/src/pomodouroboros/model/schema.py +++ b/src/pomodouroboros/model/schema.py @@ -37,6 +37,17 @@ "intervalType": Literal["Break"], }, ) +SavedTime = TypedDict("SavedTime",{"time": str, "zone": str}) +SavedRule = TypedDict( + "SavedRule", + { + # TODO: these are obviously specifically-formatted strings, i.e. ISO + # formats for dailySart / dailyEnd, and weekday enums for days. + "dailyStart": SavedTime, + "dailyEnd": SavedTime, + "days": list[int], + }, +) SavedEvaluationResult = Literal[ "distracted", "interrupted", "focused", "achieved" ] @@ -102,5 +113,6 @@ # scalability "previousStreaks": list[SavedStreak], "sessions": list[SavedSession], + "sessionRules": list[SavedRule], }, ) diff --git a/src/pomodouroboros/model/storage.py b/src/pomodouroboros/model/storage.py index d30f65f..38179da 100644 --- a/src/pomodouroboros/model/storage.py +++ b/src/pomodouroboros/model/storage.py @@ -2,18 +2,23 @@ from __future__ import annotations -from math import inf +from datetime import time from functools import singledispatch from json import dump, load +from math import inf from os import makedirs, replace from os.path import basename, dirname, exists, expanduser, join from typing import Callable, TypeAlias, cast +from zoneinfo import ZoneInfo +from datetype import Time, aware from fritter.boundaries import Scheduler from fritter.drivers.memory import MemoryDriver from fritter.scheduler import schedulerFromDriver from pomodouroboros.model.intervals import Idle +from pomodouroboros.model.schema import SavedRule, SavedTime +from pomodouroboros.model.sessions import DailySessionRule, Weekday from .boundaries import EvaluationResult, IntervalType, UserInterfaceFactory from .intention import Estimate, Intention @@ -114,6 +119,22 @@ def loadInterval(savedInterval: SavedInterval) -> AnyStreakInterval: loadInterval(interval) for interval in saved["currentStreak"] ] + def loadRule(savedRule: SavedRule) -> DailySessionRule: + + def loadOneTime(savedTime: SavedTime) -> Time[ZoneInfo]: + return aware( + time.fromisoformat(savedTime["time"]).replace( + tzinfo=ZoneInfo(savedTime["zone"]) + ), + ZoneInfo, + ) + + return DailySessionRule( + dailyStart=loadOneTime(savedRule["dailyStart"]), + dailyEnd=loadOneTime(savedRule["dailyEnd"]), + days={Weekday(each) for each in savedRule["days"]}, + ) + lastUpdateTime = saved["lastUpdateTime"] scheduler: Scheduler[float, Callable[[], None], int] = schedulerFromDriver( driver := MemoryDriver() @@ -148,6 +169,7 @@ def loadInterval(savedInterval: SavedInterval) -> AnyStreakInterval: _interfaceFactory=userInterfaceFactory, _lastUpdateTime=lastUpdateTime, _liveInterval=Idle(0, inf), + _sessionRules=[loadRule(rule) for rule in saved["sessionRules"]], ) return nexus @@ -250,6 +272,20 @@ def saveStartPrompt(interval: StartPrompt) -> SavedStartPrompt: } for session in nexus._sessions ], + "sessionRules": [ + { + "dailyStart": { + "time": rule.dailyStart.isoformat(), + "zone": rule.dailyStart.tzinfo.key, + }, + "dailyEnd": { + "time": rule.dailyEnd.isoformat(), + "zone": rule.dailyEnd.tzinfo.key, + }, + "days": [day.value for day in rule.days], + } + for rule in nexus._sessionRules + ], } diff --git a/src/pomodouroboros/model/test/test_model.py b/src/pomodouroboros/model/test/test_model.py index 1104b80..2f58f29 100644 --- a/src/pomodouroboros/model/test/test_model.py +++ b/src/pomodouroboros/model/test/test_model.py @@ -25,11 +25,13 @@ Pomodoro, StartPrompt, ) -from ..nexus import Nexus +from ..nexus import Nexus, _noUIFactory from ..observables import Changes, IgnoreChanges, SequenceObserver from ..sessions import DailySessionRule, Session, Weekday from ..storage import nexusFromJSON, nexusToJSON +TZ = ZoneInfo("America/Los_Angeles") + @dataclass class TestInterval: @@ -186,6 +188,7 @@ def setUp(self) -> None: self.clock = Clock() self.testUI = TestUserInterface(self.clock) from math import inf + self.nexus = Nexus( schedulerFromDriver(driver := MemoryDriver()), driver, @@ -401,7 +404,6 @@ def test_advanceToNewSession(self) -> None: A nexus should start a new session automatically when its rules say it's time to do that. """ - TZ = ZoneInfo("America/Los_Angeles") dailyStart = aware( time(hour=9, minute=30, tzinfo=TZ), ZoneInfo, @@ -763,6 +765,25 @@ def test_story(self) -> None: self.assertEqual(self.nexus._currentStreak, roundTrip._currentStreak) self.assertEqual(self.nexus._sessions, roundTrip._sessions) + def test_saveSessionRules(self) -> None: + """ + Auto-starting session rules are persisted. + """ + dailyStart = aware(time(9, tzinfo=TZ), ZoneInfo) + dailyEnd = aware(time(5, tzinfo=TZ), ZoneInfo) + # TODO: replace this with L{ActiveSessionManager.rules} + self.nexus._sessionRules.append( + DailySessionRule( + dailyStart=dailyStart, + dailyEnd=dailyEnd, + days={Weekday.monday}, + ) + ) + self.assertEqual( + self.nexus._sessionRules, + nexusFromJSON(nexusToJSON(self.nexus), _noUIFactory)._sessionRules, + ) + def test_achievedEarly(self) -> None: """ If I achieve the desired intent of a pomodoro while it is still