diff --git a/pi_portal/cli.py b/pi_portal/cli.py index fa7935a1..6a309fba 100644 --- a/pi_portal/cli.py +++ b/pi_portal/cli.py @@ -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") diff --git a/pi_portal/modules/motion.py b/pi_portal/modules/motion.py index b44789b3..6d1657d0 100644 --- a/pi_portal/modules/motion.py +++ b/pi_portal/modules/motion.py @@ -26,7 +26,7 @@ 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. @@ -34,29 +34,18 @@ def archive_video_to_s3(self, file_name): 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. @@ -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.""" diff --git a/pi_portal/modules/slack.py b/pi_portal/modules/slack.py index 9e29d0ca..e54e2fa6 100644 --- a/pi_portal/modules/slack.py +++ b/pi_portal/modules/slack.py @@ -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. @@ -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.") diff --git a/pi_portal/modules/tests/slack/test_slack.py b/pi_portal/modules/tests/slack/test_slack.py index 3cfb8f35..b0fb3c91 100644 --- a/pi_portal/modules/tests/slack/test_slack.py +++ b/pi_portal/modules/tests/slack/test_slack.py @@ -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() @@ -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() @@ -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() @@ -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() @@ -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 @@ -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, @@ -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, @@ -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, [ @@ -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( diff --git a/pi_portal/modules/tests/test_motion.py b/pi_portal/modules/tests/test_motion.py index 64d224df..39caa958 100644 --- a/pi_portal/modules/tests/test_motion.py +++ b/pi_portal/modules/tests/test_motion.py @@ -1,6 +1,5 @@ """Test Motion Integration.""" -import os from unittest import TestCase, mock from pi_portal import config @@ -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) diff --git a/pi_portal/tests/test_cli.py b/pi_portal/tests/test_cli.py index a80f8004..c0929751 100644 --- a/pi_portal/tests/test_cli.py +++ b/pi_portal/tests/test_cli.py @@ -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 )