Skip to content

Commit

Permalink
Remove events from pickle cache (#37)
Browse files Browse the repository at this point in the history
  • Loading branch information
tkdrob authored Dec 7, 2023
1 parent 1a6eb39 commit 89a555a
Show file tree
Hide file tree
Showing 6 changed files with 48 additions and 54 deletions.
33 changes: 6 additions & 27 deletions aioskybell/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,14 @@
from typing import Any, Collection, cast

from aiohttp.client import ClientSession, ClientTimeout
from aiohttp.client_exceptions import (
ClientConnectorError,
ClientError,
ClientResponseError,
)
from aiohttp.client_exceptions import ClientConnectorError, ClientError

from . import utils as UTILS
from .device import SkybellDevice
from .exceptions import SkybellAuthenticationException, SkybellException
from .helpers import const as CONST
from .helpers import errors as ERROR
from .helpers.models import DeviceDict, DeviceTypeDict, EventTypeDict
from .helpers.models import DeviceTypeDict, EventTypeDict

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -120,7 +116,7 @@ async def async_login(
}

response = await self.async_send_request(
CONST.LOGIN_URL, json=login_data, retry=False, method=CONST.HTTPMethod.POST
CONST.LOGIN_URL, json=login_data, method=CONST.HTTPMethod.POST, retry=False
)

_LOGGER.debug("Login Response: %s", response)
Expand Down Expand Up @@ -237,11 +233,12 @@ async def async_send_request( # pylint:disable=too-many-arguments
)
if response.status == 401:
raise SkybellAuthenticationException(await response.text())
if response.status == 404:
if response.status in (403, 404):
# 403/404 for expired request/device key no longer present in S3
_LOGGER.exception(await response.text())
return None
response.raise_for_status()
except (ClientConnectorError, ClientError, ClientResponseError) as ex:
except ClientError as ex:
if retry:
await self.async_login()

Expand All @@ -264,24 +261,6 @@ async def async_update_cache(
UTILS.update(self._cache, data)
await self._async_save_cache()

def dev_cache(
self, device: SkybellDevice, key: str | None = None
) -> dict[str, EventTypeDict] | EventTypeDict | None:
"""Get a cached value for a device."""
cache = cast(dict[str, DeviceDict], self._cache.get(CONST.DEVICES, {}))
device_cache = cache.get(device.device_id)

if device_cache and key:
return device_cache.get(key)

return device_cache

async def async_update_dev_cache(
self, device: SkybellDevice, data: dict[str, EventTypeDict]
) -> None:
"""Update cached values for a device."""
await self.async_update_cache({CONST.DEVICES: {device.device_id: data}})

async def _async_load_cache(self) -> None:
"""Load existing cache and merge for updating if required."""
if not self._disable_cache:
Expand Down
21 changes: 9 additions & 12 deletions aioskybell/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ def __init__(self, device_json: DeviceDict, skybell: Skybell) -> None:
self._skybell = skybell
self._type = device_json.get(CONST.TYPE, "")
self.images: dict[str, bytes | None] = {CONST.ACTIVITY: None}
self._events: EventTypeDict = {}

async def _async_device_request(self) -> DeviceDict:
url = str.replace(CONST.DEVICE_URL, "$DEVID$", self.device_id)
Expand Down Expand Up @@ -124,17 +125,13 @@ async def _async_update_events(
self, activities: list[EventDict] | None = None
) -> None:
"""Update our cached list of latest activity events."""
events = cast(EventTypeDict, self._skybell.dev_cache(self, CONST.EVENT)) or {}

activities = activities or self._activities
for activity in activities:
event = activity[CONST.EVENT]
created_at = activity[CONST.CREATED_AT]

if not (old := events.get(event)) or created_at >= old[CONST.CREATED_AT]:
events[event] = activity
created = activity[CONST.CREATED_AT]

await self._skybell.async_update_dev_cache(self, {CONST.EVENT: events})
if not (old := self._events.get(event)) or created >= old[CONST.CREATED_AT]:
self._events[event] = activity

def activities(self, limit: int = 1, event: str | None = None) -> list[EventDict]:
"""Return device activity information."""
Expand All @@ -149,19 +146,19 @@ def activities(self, limit: int = 1, event: str | None = None) -> list[EventDict

def latest(self, event: str | None = None) -> EventDict:
"""Return the latest event activity (motion or button)."""
events = cast(EventTypeDict, self._skybell.dev_cache(self, CONST.EVENT)) or {}
_LOGGER.debug(events)
_LOGGER.debug(self._events)

if event:
if (_evt := cast(EventDict, events.get(f"device:sensor:{event}"))) is None:
_evt: dict[str, str]
if not (_evt := self._events.get(f"device:sensor:{event}", {})):
_default = {CONST.CREATED_AT: "1970-01-01T00:00:00.000Z"}
_evt = events.get(f"application:on-{event}", _default)
_evt = self._events.get(f"application:on-{event}", _default)
_entry = {CONST.CREATED_AT: parse_datetime(_evt[CONST.CREATED_AT])}
return cast(EventDict, _evt | _entry)

latest: EventDict = EventDict()
latest_date = None
for evt in events.values():
for evt in self._events.values():
date = parse_datetime(evt[CONST.CREATED_AT])
if len(latest) == 0 or latest_date is None or latest_date < date:
latest = evt
Expand Down
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ ciso8601>=1.0.1
isort>=5.10.1
flake8>=4.0.1
flake8-docstrings>=1.6.0
freezegun==1.3.1
mypy>=0.910
pylint>=2.12.1
pytest-cov>=3.0.0
pytest-asyncio>=0.16.0
pytest-freezer>=0.4.8
pytest>=6.2.4
types-aiofiles>=0.8.3
8 changes: 4 additions & 4 deletions tests/fixtures/activities.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
"createdAt": "2020-03-30T12:35:02.204Z",
"updatedAt": "2020-03-30T12:35:02.566Z",
"id": "1234567890ab1234567890ab",
"media": "https://skybell-thumbnails-stage.s3.amazonaws.com/012345670123456789abcdef/1646859244793-951012345670123456789abcdef_012345670123456789abcdef.jpeg",
"mediaSmall": "https://skybell-thumbnails-stage.s3.amazonaws.com/012345670123456789abcdef/1646859244793-951012345670123456789abcdef_012345670123456789abcdef_small.jpeg"
"media": "https://skybell-thumbnails-stage.s3.amazonaws.com/012345670123456789abcdef/1646859244793-951012345670123456789abcdef_012345670123456789abcdef.jpeg?Expires=1585575303",
"mediaSmall": "https://skybell-thumbnails-stage.s3.amazonaws.com/012345670123456789abcdef/1646859244793-951012345670123456789abcdef_012345670123456789abcdef_small.jpeg?Expires=1585575303"
},
{
"videoState": "download:ready",
Expand All @@ -24,7 +24,7 @@
"createdAt": "2020-03-30T11:35:02.204Z",
"updatedAt": "2020-03-30T11:35:02.566Z",
"id": "1234567890ab1234567890a9",
"media": "https://skybell-thumbnails-stage.s3.amazonaws.com/012345670123456789abcdef/1646859244793-951012345670123456789abcdef_012345670123456789abcde9.jpeg",
"mediaSmall": "https://skybell-thumbnails-stage.s3.amazonaws.com/012345670123456789abcdef/1646859244793-951012345670123456789abcdef_012345670123456789abcde9_small.jpeg"
"media": "https://skybell-thumbnails-stage.s3.amazonaws.com/012345670123456789abcdef/1646859244793-951012345670123456789abcdef_012345670123456789abcde9.jpeg?Expires=1585575303",
"mediaSmall": "https://skybell-thumbnails-stage.s3.amazonaws.com/012345670123456789abcdef/1646859244793-951012345670123456789abcdef_012345670123456789abcde9_small.jpeg?Expires=1585575303"
}
]
4 changes: 2 additions & 2 deletions tests/fixtures/new-activity.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"createdAt": "2020-03-30T13:30:02.204Z",
"updatedAt": "2020-03-30T13:30:02.566Z",
"id": "1234567890ab1234567890ac",
"media": "https://skybell-thumbnails-stage.s3.amazonaws.com/012345670123456789abcdef/1646859244794-951012345670123456789abcdef_012345670123456789abcdef.jpeg",
"mediaSmall": "https://skybell-thumbnails-stage.s3.amazonaws.com/012345670123456789abcdef/1646859244794-951012345670123456789abcdef_012345670123456789abcdef_small.jpeg"
"media": "https://skybell-thumbnails-stage.s3.amazonaws.com/012345670123456789abcdef/1646859244794-951012345670123456789abcdef_012345670123456789abcdef.jpeg?Expires=1585575303",
"mediaSmall": "https://skybell-thumbnails-stage.s3.amazonaws.com/012345670123456789abcdef/1646859244794-951012345670123456789abcdef_012345670123456789abcdef_small.jpeg?Expires=1585575303"
}
]
34 changes: 25 additions & 9 deletions tests/test_skybell.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import pytest
from aiohttp import ClientConnectorError
from aresponses import ResponsesMockServer
from freezegun.api import FrozenDateTimeFactory

from aioskybell import Skybell, exceptions
from aioskybell import utils as UTILS
Expand Down Expand Up @@ -186,20 +187,22 @@ def avatar_camera_image(aresponses: ResponsesMockServer, device: str) -> None:
headers={"Content-Type": "image/jpeg"},
body=bytes(1),
),
match_querystring=True,
)


def activity_camera_image(aresponses: ResponsesMockServer, device: str) -> None:
"""Generate activity camera image response."""
aresponses.add(
"skybell-thumbnails-stage.s3.amazonaws.com",
f"/{device}/1646859244793-951{device}_{device}.jpeg",
f"/{device}/1646859244793-951{device}_{device}.jpeg?Expires=1585575303",
"get",
aresponses.Response(
status=200,
headers={"Content-Type": "image/jpeg"},
body=bytes(2),
),
match_querystring=True,
)


Expand All @@ -209,20 +212,21 @@ def activity_camera_image_not_found(
"""Generate activity camera image not found response."""
aresponses.add(
"skybell-thumbnails-stage.s3.amazonaws.com",
f"/{device}/1646859244793-951{device}_{device}.jpeg",
f"/{device}/1646859244793-951{device}_{device}.jpeg?Expires=1585575303",
"get",
aresponses.Response(
status=404,
headers={"Content-Type": "image/jpeg"},
),
match_querystring=True,
)


def new_activity_camera_image(aresponses: ResponsesMockServer, device: str) -> None:
"""Generate activity camera image response."""
aresponses.add(
"skybell-thumbnails-stage.s3.amazonaws.com",
f"/{device}/1646859244794-951{device}_{device}.jpeg",
f"/{device}/1646859244794-951{device}_{device}.jpeg?Expires=1585575303",
"get",
aresponses.Response(
status=200,
Expand Down Expand Up @@ -321,8 +325,11 @@ async def test_async_initialize_and_logout(aresponses: ResponsesMockServer) -> N


@pytest.mark.asyncio
async def test_get_devices(aresponses: ResponsesMockServer, client: Skybell) -> None:
async def test_get_devices(
aresponses: ResponsesMockServer, client: Skybell, freezer: FrozenDateTimeFactory
) -> None:
"""Test getting devices."""
freezer.move_to("2023-03-30 13:33:00+00:00")
login_response(aresponses)
devices_response(aresponses)
users_me(aresponses)
Expand Down Expand Up @@ -385,7 +392,7 @@ async def test_get_devices(aresponses: ResponsesMockServer, client: Skybell) ->
assert device._activities[0][CONST.ID] == "1234567890ab1234567890ab"
assert (
device._activities[0][CONST.MEDIA_URL]
== "https://skybell-thumbnails-stage.s3.amazonaws.com/012345670123456789abcdef/1646859244793-951012345670123456789abcdef_012345670123456789abcdef.jpeg"
== "https://skybell-thumbnails-stage.s3.amazonaws.com/012345670123456789abcdef/1646859244793-951012345670123456789abcdef_012345670123456789abcdef.jpeg?Expires=1585575303"
)
assert device.images == {"activity": b"\x00\x00", "avatar": b"\x00"}
assert (
Expand All @@ -398,7 +405,7 @@ async def test_get_devices(aresponses: ResponsesMockServer, client: Skybell) ->
assert device._activities[0][CONST.ID] == "1234567890ab1234567890ac"
assert (
device._activities[0][CONST.MEDIA_URL]
== "https://skybell-thumbnails-stage.s3.amazonaws.com/012345670123456789abcdef/1646859244794-951012345670123456789abcdef_012345670123456789abcdef.jpeg"
== "https://skybell-thumbnails-stage.s3.amazonaws.com/012345670123456789abcdef/1646859244794-951012345670123456789abcdef_012345670123456789abcdef.jpeg?Expires=1585575303"
)

login_response(aresponses)
Expand Down Expand Up @@ -440,11 +447,17 @@ async def test_errors(aresponses: ResponsesMockServer, client: Skybell) -> None:
"/api/v3/login/",
"post",
aresponses.Response(
status=403,
status=401,
headers={"Content-Type": "application/json"},
),
)
with pytest.raises(exceptions.SkybellException):
with pytest.raises(exceptions.SkybellAuthenticationException):
await client.async_login()

with patch(
"aioskybell.ClientSession.request",
side_effect=ClientConnectorError("", OSError),
), pytest.raises(exceptions.SkybellException):
await client.async_login()

with patch("aioskybell.asyncio.sleep"), patch(
Expand Down Expand Up @@ -485,9 +498,12 @@ async def test_errors(aresponses: ResponsesMockServer, client: Skybell) -> None:

@pytest.mark.asyncio
async def test_async_refresh_device(
aresponses: ResponsesMockServer, client: Skybell
aresponses: ResponsesMockServer,
client: Skybell,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test refreshing device."""
freezer.move_to("2020-03-30 13:33:00+00:00")
login_response(aresponses)
devices_response(aresponses)
_device(aresponses)
Expand Down

0 comments on commit 89a555a

Please sign in to comment.