Skip to content

Commit

Permalink
feat(CLI): logging verbosity control
Browse files Browse the repository at this point in the history
  • Loading branch information
niall-byrne committed Feb 16, 2022
1 parent e522d12 commit f9c2194
Show file tree
Hide file tree
Showing 18 changed files with 184 additions and 54 deletions.
25 changes: 17 additions & 8 deletions pi_portal/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,54 +9,63 @@
upload_video,
version,
)
from .modules.configuration import state


@click.group()
def cli() -> None:
@click.option('--debug', default=False, is_flag=True, help='Enable debug logs.')
@click.pass_context
def cli(ctx: click.Context, debug: bool) -> None:
"""Door Monitor CLI."""

running_state = state.State()
running_state.load()
ctx.ensure_object(dict)
ctx.obj['DEBUG'] = debug


@cli.command("monitor")
def monitor_command() -> None:
@click.pass_context
def monitor_command(ctx: click.Context) -> None:
"""Begin monitoring the door."""

command = door_monitor.DoorMonitorCommand()
command.load_state(debug=ctx.obj['DEBUG'])
command.invoke()


@cli.command("slack_bot")
def slack_bot_command() -> None:
@click.pass_context
def slack_bot_command(ctx: click.Context) -> None:
"""Connect the interactive Slack bot."""

command = slack_bot.SlackBotCommand()
command.load_state(debug=ctx.obj['DEBUG'])
command.invoke()


@cli.command("upload_snapshot")
@click.argument('filename', type=click.Path(exists=True))
def upload_snapshot_command(filename: str) -> None:
@click.pass_context
def upload_snapshot_command(ctx: click.Context, filename: str) -> None:
"""Upload a snapshot image to Slack.
FILENAME: The path to the image file to upload.
"""

command = upload_snapshot.UploadSnapshotCommand(filename)
command.load_state(debug=ctx.obj['DEBUG'])
command.invoke()


@cli.command("upload_video")
@click.argument('filename', type=click.Path(exists=True))
def upload_video_command(filename: str) -> None:
@click.pass_context
def upload_video_command(ctx: click.Context, filename: str) -> None:
"""Upload a video to Slack and S3.
FILENAME: The path to the video file to upload.
"""

command = upload_video.UploadVideoCommand(filename)
command.load_state(debug=ctx.obj['DEBUG'])
command.invoke()


Expand Down
3 changes: 2 additions & 1 deletion pi_portal/commands/door_monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

from pi_portal.modules.integrations import gpio
from .bases import command
from .mixins import state


class DoorMonitorCommand(command.CommandBase):
class DoorMonitorCommand(command.CommandBase, state.CommandManagedStateMixin):
"""CLI command to start the Door Monitor."""

def invoke(self) -> None:
Expand Down
Empty file.
19 changes: 19 additions & 0 deletions pi_portal/commands/mixins/state.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""CommandBase mixin to provide configured running state."""

import logging

from pi_portal.modules.configuration import state


class CommandManagedStateMixin:
"""Provide configured state to a CLI command."""

def load_state(self, debug: bool) -> None:
"""Load and configure state.
:param debug: Enable or disable debug logs.
"""
running_state = state.State()
running_state.load()
if debug:
running_state.log_level = logging.DEBUG
Empty file.
33 changes: 33 additions & 0 deletions pi_portal/commands/mixins/tests/test_state.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""Test the CommandManagedStateMixin class."""

import logging
from unittest import TestCase, mock

from .. import state


class TestLoadStateCommand(TestCase):
"""Test the CommandManagedStateMixin class."""

def setUp(self) -> None:
self.instance = state.CommandManagedStateMixin()

@mock.patch(state.__name__ + ".state")
def test_load_state(self, m_state: mock.Mock) -> None:
self.instance.load_state(False)
m_state.State.assert_called_once_with()
m_state.State.return_value.load.assert_called_once_with()
self.assertNotEqual(
m_state.State.return_value.log_level,
logging.DEBUG,
)

@mock.patch(state.__name__ + ".state")
def test_load_state_debug_logging(self, m_state: mock.Mock) -> None:
self.instance.load_state(True)
m_state.State.assert_called_once_with()
m_state.State.return_value.load.assert_called_once_with()
self.assertEqual(
m_state.State.return_value.log_level,
logging.DEBUG,
)
3 changes: 2 additions & 1 deletion pi_portal/commands/slack_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

from pi_portal.modules.integrations import slack
from .bases import command
from .mixins import state


class SlackBotCommand(command.CommandBase):
class SlackBotCommand(command.CommandBase, state.CommandManagedStateMixin):
"""CLI command to start the Slack bot."""

def invoke(self) -> None:
Expand Down
4 changes: 4 additions & 0 deletions pi_portal/commands/tests/test_door_monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from pi_portal.commands.bases.tests.fixtures import command_harness
from .. import door_monitor
from ..mixins import state


class TestDoorMonitorCommand(command_harness.CommandBaseTestHarness):
Expand All @@ -15,6 +16,9 @@ class TestDoorMonitorCommand(command_harness.CommandBaseTestHarness):
def setUpClass(cls) -> None:
cls.test_class = door_monitor.DoorMonitorCommand

def test_mixins(self) -> None:
self.assertIsInstance(self.instance, state.CommandManagedStateMixin)

@mock.patch(door_monitor.__name__ + ".gpio")
def test_invoke(self, m_module: mock.Mock) -> None:

Expand Down
4 changes: 4 additions & 0 deletions pi_portal/commands/tests/test_slack_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from pi_portal.commands.bases.tests.fixtures import command_harness
from .. import slack_bot
from ..mixins import state


class TestSlackBotCommand(command_harness.CommandBaseTestHarness):
Expand All @@ -15,6 +16,9 @@ class TestSlackBotCommand(command_harness.CommandBaseTestHarness):
def setUpClass(cls) -> None:
cls.test_class = slack_bot.SlackBotCommand

def test_mixins(self) -> None:
self.assertIsInstance(self.instance, state.CommandManagedStateMixin)

@mock.patch(slack_bot.__name__ + ".slack")
def test_invoke(self, m_module: mock.Mock) -> None:

Expand Down
4 changes: 4 additions & 0 deletions pi_portal/commands/tests/test_upload_snapshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from pi_portal.commands.bases.tests.fixtures import file_command_harness
from .. import upload_snapshot
from ..mixins import state


class TestUploadSnapshotCommand(
Expand All @@ -20,6 +21,9 @@ class TestUploadSnapshotCommand(
def setUpClass(cls) -> None:
cls.test_class = upload_snapshot.UploadSnapshotCommand

def test_mixins(self) -> None:
self.assertIsInstance(self.instance, state.CommandManagedStateMixin)

@mock.patch(upload_snapshot.__name__ + ".slack")
def test_invoke(self, m_module: mock.Mock) -> None:
self.instance.invoke()
Expand Down
4 changes: 4 additions & 0 deletions pi_portal/commands/tests/test_upload_video.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from pi_portal.commands.bases.tests.fixtures import file_command_harness
from .. import upload_video
from ..mixins import state


class TestUploadVideoCommand(file_command_harness.FileCommandBaseTestHarness):
Expand All @@ -18,6 +19,9 @@ class TestUploadVideoCommand(file_command_harness.FileCommandBaseTestHarness):
def setUpClass(cls) -> None:
cls.test_class = upload_video.UploadVideoCommand

def test_mixins(self) -> None:
self.assertIsInstance(self.instance, state.CommandManagedStateMixin)

@mock.patch(upload_video.__name__ + ".slack")
def test_invoke(self, m_module: mock.Mock) -> None:

Expand Down
5 changes: 4 additions & 1 deletion pi_portal/commands/upload_snapshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

from pi_portal.modules.integrations import slack
from .bases import file_command
from .mixins import state


class UploadSnapshotCommand(file_command.FileCommandBase):
class UploadSnapshotCommand(
file_command.FileCommandBase, state.CommandManagedStateMixin
):
"""CLI command to send a Motion snapshot to Slack."""

def invoke(self) -> None:
Expand Down
5 changes: 4 additions & 1 deletion pi_portal/commands/upload_video.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

from pi_portal.modules.integrations import slack
from .bases import file_command
from .mixins import state


class UploadVideoCommand(file_command.FileCommandBase):
class UploadVideoCommand(
file_command.FileCommandBase, state.CommandManagedStateMixin
):
"""CLI command to send a Motion video to Slack and S3."""

def invoke(self) -> None:
Expand Down
3 changes: 1 addition & 2 deletions pi_portal/modules/configuration/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,9 @@
class LoggingConfiguration:
"""Pi Portal logging configuration."""

level = logging.DEBUG

def __init__(self) -> None:
running_state = state.State()
self.level = running_state.log_level
self.formatter = logging.Formatter(
"%(asctime)s [ " + running_state.log_uuid +
" ] [ %(levelname)s ] %(message)s"
Expand Down
20 changes: 20 additions & 0 deletions pi_portal/modules/configuration/state.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Borg monostate of the current running configuration."""

import logging
import uuid
from typing import Any, Dict, cast

Expand All @@ -19,6 +20,25 @@ def __init__(self) -> None:
if not self.__shared_state:
self.user_config: TypeUserConfig = cast(TypeUserConfig, {})
self.log_uuid = str(uuid.uuid4())
self._log_level = logging.INFO

@property
def log_level(self) -> int:
"""Return the currently configured logging level.
:returns: The currently configured logging level.
"""

return self._log_level

@log_level.setter
def log_level(self, level: int) -> None:
"""Configure the logging level.
:param level: The desired logging level.
"""

self._log_level = level

def load(self) -> None:
"""Load the end user configuration."""
Expand Down
9 changes: 6 additions & 3 deletions pi_portal/modules/configuration/tests/fixtures/mock_state.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""Fixtures for mocking required environment variables."""

import logging
from typing import Any, Callable, TypeVar
from unittest import mock

Expand All @@ -13,6 +13,7 @@
MOCK_SLACK_TOKEN = "secretValue"
MOCK_S3_BUCKET_NAME = 'MOCK_S3_BUCKET_NAME'
MOCK_LOG_UUID = "MOCK_UUID_VALUE"
MOCK_LOG_LEVEL = logging.DEBUG

TypeReturn = TypeVar("TypeReturn")

Expand All @@ -23,7 +24,8 @@ def patched_function(*args: Any, **kwargs: Any) -> TypeReturn:

with mock.patch(state.__name__ + ".State") as mock_state:

mock_state.return_value.user_config = {
mock_state_instance = mock_state.return_value
mock_state_instance.user_config = {
"AWS_ACCESS_KEY_ID": MOCK_AWS_ACCESS_KEY_ID,
"AWS_SECRET_ACCESS_KEY": MOCK_AWS_SECRET_ACCESS_KEY,
"LOGZ_IO_CODE": MOCK_LOGZ_IO_CODE,
Expand All @@ -36,7 +38,8 @@ def patched_function(*args: Any, **kwargs: Any) -> TypeReturn:
"GPIO": 5,
}]
}
mock_state.return_value.log_uuid = MOCK_LOG_UUID
mock_state_instance.log_uuid = MOCK_LOG_UUID
mock_state_instance.log_level = MOCK_LOG_LEVEL

return func(*args, **kwargs)

Expand Down
30 changes: 22 additions & 8 deletions pi_portal/modules/configuration/tests/test_state.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Test RunningConfig monostate."""

import logging
from typing import cast
from unittest import TestCase, mock

Expand All @@ -10,21 +11,34 @@ class TestRunningConfig(TestCase):
"""Test the RunningConfig monostate."""

def setUp(self) -> None:
self.state = state.State()
self.instance = state.State()

def test_instantiate(self) -> None:
instance = state.State()
self.assertEqual(instance.user_config, {})
self.assertEqual(instance.log_level, logging.INFO)
self.assertIsInstance(instance.log_uuid, str)

def test_debug_enabled(self) -> None:
self.instance.log_level = logging.DEBUG
self.assertEqual(self.instance.log_level, logging.DEBUG)
self.instance.log_level = logging.INFO

def test_mono_state_user_config(self) -> None:
self.state.user_config = cast(
self.instance.user_config = cast(
user_config.TypeUserConfig, {'test': 'value'}
)

instance2 = state.State()
self.assertEqual(self.state.user_config, getattr(instance2, 'user_config'))
self.assertEqual(
self.instance.user_config, getattr(instance2, 'user_config')
)

def test_mono_state_log_uuid(self) -> None:
self.state.log_uuid = "test id"
self.instance.log_uuid = "test id"

instance2 = state.State()
self.assertEqual(self.state.log_uuid, getattr(instance2, 'log_uuid'))
self.assertEqual(self.instance.log_uuid, getattr(instance2, 'log_uuid'))

@mock.patch(state.__name__ + ".UserConfiguration")
def test_load_config(self, m_user_config: mock.Mock) -> None:
Expand All @@ -33,10 +47,10 @@ def test_load_config(self, m_user_config: mock.Mock) -> None:
}
m_user_config.return_value.user_config = mock_config

self.state.load()
self.instance.load()

m_user_config.return_value.load.assert_called_once_with()
self.assertEqual(self.state.user_config, mock_config)
self.assertEqual(self.instance.user_config, mock_config)

instance2 = state.State()
self.assertEqual(self.state.user_config, instance2.user_config)
self.assertEqual(self.instance.user_config, instance2.user_config)
Loading

0 comments on commit f9c2194

Please sign in to comment.