Skip to content

Commit

Permalink
feat(SLACK): clean up snapshots after posting
Browse files Browse the repository at this point in the history
  • Loading branch information
niall-byrne committed Nov 20, 2021
1 parent 224b624 commit 2097344
Show file tree
Hide file tree
Showing 6 changed files with 92 additions and 60 deletions.
2 changes: 1 addition & 1 deletion pi_portal/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def upload_snapshot(filename: str):

modules.state.State().load()
slack_client = modules.slack.Client()
slack_client.send_file(filename)
slack_client.send_snapshot(filename)


@cli.command("upload_video")
Expand Down
34 changes: 13 additions & 21 deletions pi_portal/modules/motion.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,37 +26,26 @@ def __init__(self):
self.data_folder = config.MOTION_FOLDER
self.s3_client = s3.S3Bucket()

def archive_video_to_s3(self, file_name):
def archive_video(self, file_name: str):
"""Copy video to S3 for retention and delete locally.
:param file_name: The path to upload and then remove.
"""
try:
self.s3_client.upload(file_name)
os.remove(file_name)
except s3.S3BucketException as exc:
except (s3.S3BucketException, OSError) as exc:
raise MotionException("Unable to archive video to S3.") from exc

def cleanup_snapshots(self):
"""Remove all videos from the motion data folder."""
def cleanup_snapshot(self, file_name: str):
"""Delete snapshot locally.
for fname in self._list_snapshots():
os.remove(fname)

def _list_snapshots(self):
snapshots = glob.glob(os.path.join(self.data_folder, '/*.mp4'))
if self.snapshot_fname in snapshots:
snapshots.remove(self.snapshot_fname)
return snapshots

def cleanup_videos(self):
"""Remove all videos from the motion data folder."""

for fname in self._list_videos():
os.remove(fname)

def _list_videos(self):
return glob.glob(os.path.join(self.data_folder, '/*.mp4'))
:param file_name: The path to remove.
"""
try:
os.remove(file_name)
except OSError as exc:
raise MotionException("Unable to remove snapshot.") from exc

def get_latest_video_filename(self) -> str:
"""Retrieve the filename of the latest video recording.
Expand All @@ -65,6 +54,9 @@ def get_latest_video_filename(self) -> str:
"""
return max(self._list_videos(), key=os.path.getctime)

def _list_videos(self):
return glob.glob(os.path.join(self.data_folder, '/*.mp4'))

def take_snapshot(self):
"""Take a snapshot with Motion."""

Expand Down
14 changes: 13 additions & 1 deletion pi_portal/modules/slack.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,18 @@ def send_file(self, file_name: str):
except (SlackRequestError, SlackApiError):
pass

def send_snapshot(self, file_name: str):
"""Send a snapshot to Slack, and erase it locally.
:param file_name: The path of the file to process.
"""

try:
self.send_file(file_name)
self.motion_client.cleanup_snapshot(file_name)
except motion.MotionException:
self.send_message("An error occurred cleaning up this snapshot.")

def send_video(self, file_name: str):
"""Send a video to Slack, and have motion archive it in S3.
Expand All @@ -91,7 +103,7 @@ def send_video(self, file_name: str):

try:
self.send_file(file_name)
self.motion_client.archive_video_to_s3(file_name)
self.motion_client.archive_video(file_name)
except motion.MotionException:
self.send_message("An error occurred archiving this video.")

Expand Down
48 changes: 44 additions & 4 deletions pi_portal/modules/tests/slack/test_slack.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ def test_handle_event_invalid_command(self, m_slack_cli):
test_event = {
"text": "invalid_command"
}

self.slack_client.handle_event(test_event)
m_slack_cli.return_value.command_id.assert_not_called()

Expand All @@ -42,12 +43,14 @@ def test_handle_event_valid_command(self, m_slack_cli):
test_event = {
"text": "id"
}

self.slack_client.handle_event(test_event)
m_slack_cli.return_value.command_id.assert_called_once_with()

def test_handle_rtm_message_no_channel(self):
self.slack_client.handle_event = mock.MagicMock()
test_event = {}

self.slack_client.handle_rtm_message(test_event)
self.slack_client.handle_event.assert_not_called()

Expand All @@ -56,6 +59,7 @@ def test_handle_rtm_message_no_text(self):
test_event = {
'channel': 'mockChannel'
}

self.slack_client.handle_rtm_message(test_event)
self.slack_client.handle_event.assert_not_called()

Expand All @@ -65,6 +69,7 @@ def test_handle_rtm_message_wrong_channel(self):
'channel': 'mockChannel',
'text': "hello"
}

self.slack_client.handle_rtm_message(test_event)
self.slack_client.handle_event.assert_not_called()

Expand All @@ -74,12 +79,14 @@ def test_handle_rtm_message_valid_event(self):
'channel': self.slack_client.channel_id,
'text': "hello"
}

self.slack_client.handle_rtm_message(test_event)
self.slack_client.handle_event.assert_called_once_with(test_event)

def test_send_message(self):
test_message = "test message"
self.slack_client.web = mock.MagicMock()

self.slack_client.send_message(test_message)
self.slack_client.web.chat_postMessage.assert_called_once_with(
channel=self.slack_client.channel, text=test_message
Expand All @@ -91,6 +98,7 @@ def test_test_send_message_exception(self):
self.slack_client.web.chat_postMessage.side_effect = (
SlackRequestError("Boom!")
)

self.slack_client.send_message(test_message)
self.assertListEqual(
self.slack_client.web.chat_postMessage.mock_calls,
Expand All @@ -103,6 +111,7 @@ def test_test_send_message_exception(self):
def test_send_file(self):
test_file = "/path/to/mock/file.txt"
self.slack_client.web = mock.MagicMock()

self.slack_client.send_file(test_file)
self.slack_client.web.files_upload.assert_called_once_with(
channels=self.slack_client.channel,
Expand All @@ -116,6 +125,7 @@ def test_send_file_exception(self):
self.slack_client.web.files_upload.side_effect = (
SlackRequestError("Boom!")
)

self.slack_client.send_file(test_file)
self.assertListEqual(
self.slack_client.web.files_upload.mock_calls, [
Expand All @@ -127,25 +137,55 @@ def test_send_file_exception(self):
] * self.slack_client.retries
)

def test_send_snapshot(self):
test_snapshot = "/path/to/mock/snapshot.jpg"
self.slack_client.send_file = mock.MagicMock()

self.slack_client.send_snapshot(test_snapshot)
self.slack_client.send_file.assert_called_once_with(test_snapshot)
self.slack_client.motion_client.cleanup_snapshot.assert_called_once_with(
test_snapshot
)

def test_send_snapshot_exception(self):
test_snapshot = "/path/to/mock/snapshot.jpg"
self.slack_client.send_file = mock.MagicMock()
self.slack_client.web = mock.MagicMock()
self.slack_client.motion_client.cleanup_snapshot.side_effect = (
motion.MotionException("Boom!")
)

self.slack_client.send_snapshot(test_snapshot)
self.slack_client.send_file.assert_called_once_with(test_snapshot)
self.slack_client.motion_client.cleanup_snapshot.assert_called_once_with(
test_snapshot
)
self.slack_client.web.chat_postMessage.assert_called_once_with(
channel=self.slack_client.channel,
text="An error occurred cleaning up this snapshot.",
)

def test_send_video(self):
test_video = "/path/to/mock/video.mp4"
self.slack_client.send_file = mock.MagicMock()

self.slack_client.send_video(test_video)
self.slack_client.send_file.assert_called_once_with(test_video)
self.slack_client.motion_client.archive_video_to_s3.assert_called_once_with(
self.slack_client.motion_client.archive_video.assert_called_once_with(
test_video
)

def test_send_video_exception(self):
test_video = "/path/to/mock/video.mp4"
self.slack_client.send_file = mock.MagicMock()
self.slack_client.web = mock.MagicMock()
self.slack_client.motion_client.archive_video_to_s3.side_effect = (
self.slack_client.motion_client.archive_video.side_effect = (
motion.MotionException("Boom!")
)
self.slack_client.send_file = mock.MagicMock()

self.slack_client.send_video(test_video)
self.slack_client.send_file.assert_called_once_with(test_video)
self.slack_client.motion_client.archive_video_to_s3.assert_called_once_with(
self.slack_client.motion_client.archive_video.assert_called_once_with(
test_video
)
self.slack_client.web.chat_postMessage.assert_called_once_with(
Expand Down
52 changes: 20 additions & 32 deletions pi_portal/modules/tests/test_motion.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""Test Motion Integration."""

import os
from unittest import TestCase, mock

from pi_portal import config
Expand Down Expand Up @@ -50,49 +49,38 @@ def test_get_latest_video_filename(self, m_ctime, m_glob):
fname = self.motion_client.get_latest_video_filename()
self.assertEqual(fname, m_glob.return_value[1])

@mock.patch(motion.__name__ + ".glob.glob")
@mock.patch(motion.__name__ + ".os.remove")
def test_cleanup_videos(self, m_remove, m_glob):
m_glob.return_value = ["1.mp4", "2.mp4"]
self.motion_client.cleanup_videos()

m_glob.assert_called_once_with(
os.path.join(self.motion_client.data_folder, '/*.mp4')
)
for fname in m_glob.return_value:
m_remove.assert_any_call(fname)
self.assertEqual(m_remove.call_count, len(m_glob.return_value))

@mock.patch(motion.__name__ + ".glob.glob")
@mock.patch(motion.__name__ + ".os.remove")
def test_cleanup_snapshots(self, m_remove, m_glob):
m_glob.return_value = ["1.mp4", "2.mp4", self.motion_client.snapshot_fname]
self.motion_client.cleanup_snapshots()

m_glob.assert_called_once_with(
os.path.join(self.motion_client.data_folder, '/*.mp4')
)
for fname in m_glob.return_value:
if fname != self.motion_client.snapshot_fname:
m_remove.assert_any_call(fname)
self.assertEqual(m_remove.call_count, len(m_glob.return_value))

@mock.patch(motion.__name__ + ".os.remove")
def test_archive_video_to_s3(self, m_remove):
def test_archive_video(self, m_remove):
mock_video_name = "mock_video.mp4"
self.motion_client.archive_video_to_s3(mock_video_name)
self.motion_client.archive_video(mock_video_name)
self.motion_client.s3_client.upload.assert_called_once_with(mock_video_name)
m_remove.assert_called_once_with(mock_video_name)

@mock.patch(motion.__name__ + ".os.remove")
def test_archive_video_to_s3_failure(self, m_remove):
def test_archive_video_exception(self, m_remove):
mock_video_name = "mock_video.mp4"
self.motion_client.s3_client.upload.side_effect = s3.S3BucketException(
"Boom!"
)

with self.assertRaises(motion.MotionException):
self.motion_client.archive_video_to_s3(mock_video_name)
self.motion_client.archive_video(mock_video_name)

self.motion_client.s3_client.upload.assert_called_once_with(mock_video_name)
m_remove.assert_not_called()

@mock.patch(motion.__name__ + ".os.remove")
def test_cleanup_snapshot(self, m_remove):
mock_snapshot_name = "mock_snapshot.jpg"
self.motion_client.cleanup_snapshot(mock_snapshot_name)
m_remove.assert_called_once_with(mock_snapshot_name)

@mock.patch(motion.__name__ + ".os.remove")
def test_cleanup_snapshot_exception(self, m_remove):
mock_snapshot_name = "mock_snapshot.jpg"
m_remove.side_effect = OSError("Boom!")

with self.assertRaises(motion.MotionException):
self.motion_client.cleanup_snapshot(mock_snapshot_name)

m_remove.assert_called_once_with(mock_snapshot_name)
2 changes: 1 addition & 1 deletion pi_portal/tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def test_upload_snapshot(self, m_state, m_slack):
self.runner.invoke(cli.cli, command)
m_state.State.return_value.load.assert_called_once_with()
m_slack.Client.assert_called_once_with()
m_slack.Client.return_value.send_file.assert_called_once_with(
m_slack.Client.return_value.send_snapshot.assert_called_once_with(
mock_snapshot_name
)

Expand Down

0 comments on commit 2097344

Please sign in to comment.