diff --git a/doc/example_config/my/config/__init__.py b/doc/example_config/my/config/__init__.py index 4f7393a..ec70750 100644 --- a/doc/example_config/my/config/__init__.py +++ b/doc/example_config/my/config/__init__.py @@ -32,3 +32,7 @@ class bluemaestro: class google: takeout_path: Paths = '' + +class location: + class home: + current = (1.0, -1.0) diff --git a/my/location/home.py b/my/location/home.py new file mode 100644 index 0000000..4c392f1 --- /dev/null +++ b/my/location/home.py @@ -0,0 +1,61 @@ +''' +Simple location provider, serving as a fallback when more detailed data isn't available +''' +from dataclasses import dataclass +from datetime import datetime, date +from functools import lru_cache +from typing import Optional, Sequence, Tuple, Union + +from ..core.common import fromisoformat + +from my.config import location as L +user_config = L.home + + +DateIsh = Union[datetime, str] + +# todo hopefully reasonable? might be nice to add name or something too +LatLon = Tuple[float, float] + +@dataclass +class home(user_config): + # TODO could make current Optional and somehow determine from system settings? + # todo possibly also could be core config.. but not sure + current: LatLon + + ''' + First element is location, the second is the date when you left it (datetime/ISO string) + ''' + past: Sequence[Tuple[LatLon, DateIsh]] = () + # todo test for proper localized/not localized handling as well + # todo make sure they are increasing + + + @property + def _past(self) -> Sequence[Tuple[LatLon, datetime]]: + # todo cache? + res = [] + for loc, x in self.past: + dt: datetime + if isinstance(x, str): + dt = fromisoformat(x) + else: + dt = x + res.append((loc, dt)) + return res + + + +from ..core.cfg import make_config +config = make_config(home) + + +@lru_cache(maxsize=None) +def get_location(dt: datetime) -> LatLon: + ''' + Interpolates the location at dt + ''' + for loc, pdt in config._past: + if dt <= pdt: + return loc + return config.current diff --git a/my/time/tz/via_location.py b/my/time/tz/via_location.py index 15ffe5f..09c9528 100644 --- a/my/time/tz/via_location.py +++ b/my/time/tz/via_location.py @@ -25,15 +25,18 @@ logger = LazyLogger(__name__, level='debug') # todo should move to config? not sure -_FASTER: bool = False -@lru_cache(1) -def _timezone_finder(): - from timezonefinder import TimezoneFinder as Finder # type: ignore - if _FASTER: - from timezonefinder import TimezoneFinderL as Finder # type: ignore +_FASTER: bool = True +@lru_cache(2) +def _timezone_finder(fast: bool): + if fast: + # less precise, but faster + from timezonefinder import TimezoneFinderL as Finder # type: ignore + else: + from timezonefinder import TimezoneFinder as Finder # type: ignore return Finder(in_memory=True) +# todo move to common? Zone = str @@ -55,6 +58,7 @@ def _iter_local_dates(start=0, stop=None) -> Iterator[DayWithZone]: warnings.append(f"Couldn't figure out tz for {l}") continue tz = pytz.timezone(zone) + # TODO this is probably a bit expensive... test & benchmark ldt = l.dt.astimezone(tz) ndate = ldt.date() if pdt is not None and ndate < pdt.date(): @@ -100,9 +104,27 @@ def _get_day_tz(d: date) -> Optional[pytz.BaseTzInfo]: break return None if zone is None else pytz.timezone(zone) +# ok to cache, there are only a few home locations? +@lru_cache(maxsize=None) +def _get_home_tz(loc) -> Optional[pytz.BaseTzInfo]: + (lat, lng) = loc + finder = _timezone_finder(fast=False) # ok to use slow here for better precision + zone = finder.timezone_at(lat=lat, lng=lng) + if zone is None: + # TODO shouldn't really happen, warn? + return None + else: + return pytz.timezone(zone) + def _get_tz(dt: datetime) -> Optional[pytz.BaseTzInfo]: - return _get_day_tz(d=dt.date()) + res = _get_day_tz(d=dt.date()) + if res is not None: + return res + # fallback to home tz + from ...location import home + loc = home.get_location(dt) + return _get_home_tz(loc=loc) def localize(dt: datetime) -> datetime: diff --git a/tests/tz.py b/tests/tz.py index b117a63..43e2ed8 100644 --- a/tests/tz.py +++ b/tests/tz.py @@ -13,6 +13,15 @@ def test_iter_tzs() -> None: assert len(ll) > 3 +def test_past() -> None: + # should fallback to the home location provider + dt = D('20000101 12:34:45') + dt = TZ.localize(dt) + tz = dt.tzinfo + assert tz is not None + assert getattr(tz, 'zone') == 'America/New_York' + + def test_future() -> None: fut = datetime.now() + timedelta(days=100) # shouldn't crash at least @@ -55,8 +64,16 @@ def prepare(tmp_path: Path): # FIXME ugh. early import/inheritance of user_confg in my.google.takeout.paths messes things up.. from my.cfg import config - class user_config: + class google: takeout_path = tmp_path - config.google = user_config # type: ignore + config.google = google # type: ignore + + class location: + class home: + current = (1.0, 1.0) + past = [ + ((40.7128, -74.0060), '2005-12-04'), # NY + ] + config.location = location # type: ignore yield