convert fallback location estimators to be iterators

This commit is contained in:
Sean Breckenridge 2023-02-21 19:37:27 -08:00
parent 1cf9dfe5dd
commit da8f541cdc
4 changed files with 32 additions and 20 deletions

View file

@ -1,17 +1,24 @@
# TODO: add config here which passes kwargs to estimate_from (under_accuracy) # TODO: add config here which passes kwargs to estimate_from (under_accuracy)
# overwritable by passing the kwarg name here to the top-level estimate_location # overwritable by passing the kwarg name here to the top-level estimate_location
from typing import Union from typing import Iterator
from datetime import datetime
from my.location.fallback.common import estimate_from, FallbackLocation from my.core.source import import_source
from my.location.fallback.common import estimate_from, FallbackLocation, DateExact
def estimate_location(dt: Union[datetime, float, int]) -> FallbackLocation: # note: the import_source returns an iterator
@import_source(module_name="my.location.fallback.via_home")
def _home_estimate(dt: DateExact) -> Iterator[FallbackLocation]:
from my.location.fallback.via_home import estimate_location as via_home from my.location.fallback.via_home import estimate_location as via_home
yield from via_home(dt)
def estimate_location(dt: DateExact) -> FallbackLocation:
loc = estimate_from( loc = estimate_from(
dt, dt,
estimators=(via_home,) estimators=(_home_estimate,)
) )
if loc is None: if loc is None:
raise ValueError("Could not estimate location") raise ValueError("Could not estimate location")

View file

@ -1,10 +1,10 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional, Callable, Sequence, Iterator, List, Union from typing import Optional, Callable, Sequence, Iterator, List, Union
from datetime import datetime, timedelta from datetime import datetime, timedelta, date
from ..common import LocationProtocol, Location from ..common import LocationProtocol, Location
DateIsh = Union[datetime, float, int] DateExact = Union[datetime, float, int] # float/int as epoch timestamps
@dataclass @dataclass
class FallbackLocation(LocationProtocol): class FallbackLocation(LocationProtocol):
@ -62,28 +62,31 @@ class FallbackLocation(LocationProtocol):
) )
LocationEstimator = Callable[[DateIsh], Optional[FallbackLocation]] # a location estimator can return multiple fallbacks, incase there are
# differing accuracies/to allow for possible matches to be computed
# iteratively
LocationEstimator = Callable[[DateExact], Iterator[FallbackLocation]]
LocationEstimators = Sequence[LocationEstimator] LocationEstimators = Sequence[LocationEstimator]
# helper function, instead of dealing with datetimes while comparing, just use epoch timestamps # helper function, instead of dealing with datetimes while comparing, just use epoch timestamps
def _datetime_timestamp(dt: DateIsh) -> float: def _datetime_timestamp(dt: DateExact) -> float:
if isinstance(dt, datetime): if isinstance(dt, datetime):
return dt.timestamp() return dt.timestamp()
return float(dt) return float(dt)
def _iter_estimate_from( def _iter_estimate_from(
dt: DateIsh, dt: DateExact,
estimators: LocationEstimators, estimators: LocationEstimators,
) -> Iterator[FallbackLocation]: ) -> Iterator[FallbackLocation]:
for est in estimators: for est in estimators:
loc = est(dt) loc = list(est(dt))
if loc is None: if not loc:
continue continue
yield loc yield from loc
def estimate_from( def estimate_from(
dt: DateIsh, dt: DateExact,
estimators: LocationEstimators, estimators: LocationEstimators,
*, *,
first_match: bool = False, first_match: bool = False,

View file

@ -5,12 +5,12 @@ Simple location provider, serving as a fallback when more detailed data isn't av
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime, time, timezone from datetime import datetime, time, timezone
from functools import lru_cache from functools import lru_cache
from typing import Sequence, Tuple, Union, cast, List from typing import Sequence, Tuple, Union, cast, List, Iterator
from my.config import location as user_config from my.config import location as user_config
from my.location.common import LatLon, DateIsh from my.location.common import LatLon, DateIsh
from my.location.fallback.common import FallbackLocation from my.location.fallback.common import FallbackLocation, DateExact
@dataclass @dataclass
class Config(user_config): class Config(user_config):
@ -85,24 +85,26 @@ def homes_cached() -> List[Tuple[datetime, LatLon]]:
return list(config._history) return list(config._history)
def estimate_location(dt: Union[datetime, int, float]) -> FallbackLocation: def estimate_location(dt: DateExact) -> Iterator[FallbackLocation]:
from my.location.fallback.common import _datetime_timestamp from my.location.fallback.common import _datetime_timestamp
d: float = _datetime_timestamp(dt) d: float = _datetime_timestamp(dt)
hist = list(reversed(homes_cached())) hist = list(reversed(homes_cached()))
for pdt, (lat, lon) in hist: for pdt, (lat, lon) in hist:
if d >= pdt.timestamp(): if d >= pdt.timestamp():
return FallbackLocation( yield FallbackLocation(
lat=lat, lat=lat,
lon=lon, lon=lon,
accuracy=config.home_accuracy, accuracy=config.home_accuracy,
dt=datetime.fromtimestamp(d, timezone.utc), dt=datetime.fromtimestamp(d, timezone.utc),
datasource='via_home') datasource='via_home')
return
else: else:
# I guess the most reasonable is to fallback on the first location # I guess the most reasonable is to fallback on the first location
lat, lon = hist[-1][1] lat, lon = hist[-1][1]
return FallbackLocation( yield FallbackLocation(
lat=lat, lat=lat,
lon=lon, lon=lon,
accuracy=config.home_accuracy, accuracy=config.home_accuracy,
dt=datetime.fromtimestamp(d, timezone.utc), dt=datetime.fromtimestamp(d, timezone.utc),
datasource='via_home') datasource='via_home')
return

View file

@ -199,7 +199,7 @@ def _get_day_tz(d: date) -> Optional[pytz.BaseTzInfo]:
# ok to cache, there are only a few home locations? # ok to cache, there are only a few home locations?
@lru_cache(maxsize=None) @lru_cache(maxsize=None)
def _get_home_tz(loc) -> Optional[pytz.BaseTzInfo]: def _get_home_tz(loc: LatLon) -> Optional[pytz.BaseTzInfo]:
(lat, lng) = loc (lat, lng) = loc
finder = _timezone_finder(fast=False) # ok to use slow here for better precision finder = _timezone_finder(fast=False) # ok to use slow here for better precision
zone = finder.timezone_at(lat=lat, lng=lng) zone = finder.timezone_at(lat=lat, lng=lng)