Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 3 additions & 6 deletions findmy/reports/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -785,7 +785,7 @@ async def fetch_location(
| None
):
"""See :meth:`BaseAppleAccount.fetch_location`."""
hist = await self.fetch_location_history(keys)
hist = await self._reports.fetch_location_history(keys, only_latest=True)
if isinstance(hist, list):
return sorted(hist)[-1] if hist else None

Expand Down Expand Up @@ -1188,11 +1188,8 @@ def fetch_location(
| None
):
"""See :meth:`BaseAppleAccount.fetch_location`."""
hist = self.fetch_location_history(keys)
if isinstance(hist, list):
return sorted(hist)[-1] if hist else None

return {dev: sorted(reports)[-1] if reports else None for dev, reports in hist.items()}
coro = self._asyncacc.fetch_location(keys)
return self._evt_loop.run_until_complete(coro)

@override
def get_anisette_headers(
Expand Down
12 changes: 10 additions & 2 deletions findmy/reports/reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,25 +342,33 @@ def __init__(self, account: AsyncAppleAccount) -> None:
async def fetch_location_history(
self,
device: HasHashedPublicKey,
*,
only_latest: bool = False,
) -> list[LocationReport]: ...

@overload
async def fetch_location_history(
self,
device: RollingKeyPairSource,
*,
only_latest: bool = False,
) -> list[LocationReport]: ...

@overload
async def fetch_location_history(
self,
device: Sequence[HasHashedPublicKey | RollingKeyPairSource],
*,
only_latest: bool = False,
) -> dict[HasHashedPublicKey | RollingKeyPairSource, list[LocationReport]]: ...

async def fetch_location_history(
self,
device: HasHashedPublicKey
| RollingKeyPairSource
| Sequence[HasHashedPublicKey | RollingKeyPairSource],
*,
only_latest: bool = False,
) -> (
list[LocationReport] | dict[HasHashedPublicKey | RollingKeyPairSource, list[LocationReport]]
):
Expand All @@ -386,7 +394,7 @@ async def fetch_location_history(

if isinstance(device, RollingKeyPairSource):
# key generator
return await self._fetch_accessory_reports(device, only_latest=True)
return await self._fetch_accessory_reports(device, only_latest=only_latest)

if not isinstance(device, list) or not all(
isinstance(x, HasHashedPublicKey | RollingKeyPairSource) for x in device
Expand All @@ -408,7 +416,7 @@ async def fetch_location_history(
static_keys.append(dev)
elif isinstance(dev, RollingKeyPairSource):
# query immediately
reports[dev] = await self._fetch_accessory_reports(dev, only_latest=True)
reports[dev] = await self._fetch_accessory_reports(dev, only_latest=only_latest)

if static_keys: # batch request for static keys
key_reports = await self._fetch_key_reports(static_keys)
Expand Down
79 changes: 79 additions & 0 deletions tests/test_reports.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""Location report fetcher tests."""

import asyncio
from datetime import datetime

from typing_extensions import override

from findmy import KeyPair
from findmy.accessory import RollingKeyPairSource
from findmy.reports.reports import LocationReport, LocationReportsFetcher


class DummyAccessory(RollingKeyPairSource):
"""Minimal rolling key source for dispatch tests."""

@override
def get_min_index(self, dt: datetime) -> int:
"""Return a dummy minimum key index."""
_ = dt
return 0

@override
def get_max_index(self, dt: datetime) -> int:
"""Return a dummy maximum key index."""
_ = dt
return 0

@override
def update_alignment(self, dt: datetime, index: int) -> None:
"""Ignore alignment updates."""
_ = (dt, index)

@override
def keys_at(self, ind: int) -> set[KeyPair]:
"""Return no keys."""
_ = ind
return set()


class RecordingFetcher(LocationReportsFetcher):
"""Fetcher that records only_latest values passed to accessory fetching."""

def __init__(self, accessory: RollingKeyPairSource) -> None:
"""Initialize the recording fetcher."""
super().__init__(account=None) # type: ignore[arg-type]
self.accessory = accessory
self.calls: list[bool] = []

@override
async def _fetch_accessory_reports(
self,
accessory: RollingKeyPairSource,
only_latest: bool = False,
) -> list[LocationReport]:
assert accessory is self.accessory
self.calls.append(only_latest)
return []


def test_fetch_location_history_fetches_full_rolling_history_by_default() -> None:
"""Rolling-key history should not stop after the latest report."""
accessory = DummyAccessory()
fetcher = RecordingFetcher(accessory)

result = asyncio.run(fetcher.fetch_location_history(accessory))

assert result == []
assert fetcher.calls == [False]


def test_fetch_location_history_can_fetch_latest_rolling_report() -> None:
"""Internal latest-location callers can still stop after the latest report."""
accessory = DummyAccessory()
fetcher = RecordingFetcher(accessory)

result = asyncio.run(fetcher.fetch_location_history([accessory], only_latest=True))

assert result == {accessory: []}
assert fetcher.calls == [True]
Loading