Skip to content

Commit

Permalink
rework: config init and app setup
Browse files Browse the repository at this point in the history
  • Loading branch information
u8slvn committed Nov 4, 2021
1 parent d338e8b commit 5159129
Show file tree
Hide file tree
Showing 14 changed files with 256 additions and 143 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ help: ## List all the command helps.
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)

tests: ## Run tests.
@poetry run pytest tests/ -x -vv
@poetry run pytest tests/ -x -vv --cache-clear

quality: ## Check quality.
@poetry run flake8
Expand Down
128 changes: 61 additions & 67 deletions poetry.lock

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ black = "^21.10b0"
[tool.poetry.extras]
coverage = ["coveralls"]

[tool.poetry.scripts]
transilienwatcher = "transilienwatcher.cli:run"
transilienwatcher-init = "transilienwatcher.cli:init"

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
File renamed without changes.
11 changes: 11 additions & 0 deletions tests/configs/invalid.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
transilien:
stations:
test: '00000000'
credentials:
username: 898323
refresh_time: 10
display:
type: 'oled'
lcd:
columns: 'hello'
rows: 2
21 changes: 20 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import builtins
import sys
from pathlib import Path
from unittest.mock import Mock

from loguru import logger
import pytest
import requests
from loguru import logger

# Monkeypatch for non Raspberry pi environment.
sys.modules['board'] = Mock()
Expand Down Expand Up @@ -56,3 +58,20 @@ def get():
return requests_fixture

monkeypatch.setattr(requests, 'get', get)


@pytest.fixture
def cleanup_files(monkeypatch):
def open_wrapper(open_func, files):
def open_patched(path, mode='r', *args, **kwargs):
if mode in ['w', 'x'] and not Path(path).is_file():
files.append(path)
return open_func(path, mode=mode, *args, **kwargs)

return open_patched

files = []
monkeypatch.setattr(builtins, 'open', open_wrapper(builtins.open, files))
yield
for file in files:
Path(file).unlink()
15 changes: 3 additions & 12 deletions tests/test_app.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,9 @@
import pytest

from transilienwatcher.app import TransilienWatcher
from transilienwatcher.configuration import ConfigLoader
from transilienwatcher import TransilienWatcher


@pytest.fixture(scope='function')
def config_loader(monkeypatch, config):
def load(file: str):
return config

monkeypatch.setattr(ConfigLoader, 'load', load)


def test_rerwatcher_workflow(mocker, config_loader, capsys):
def test_rerwatcher_workflow(mocker, config, capsys):
messages = [
['TEST: 1min', 'TEST: 1h'],
['TEST: 2min', 'TEST: 2h'],
Expand All @@ -26,7 +17,7 @@ def test_rerwatcher_workflow(mocker, config_loader, capsys):
side_effect=messages
)

app = TransilienWatcher(None, config_file="config.py")
app = TransilienWatcher(log_file='', config=config)
with pytest.raises(KeyboardInterrupt):
app.run()

Expand Down
37 changes: 30 additions & 7 deletions tests/test_configuration.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,45 @@
import os
from pathlib import Path

import pytest

from transilienwatcher.configuration import ConfigLoader
from transilienwatcher.exceptions import ConfigError
from transilienwatcher.configuration import ConfigManager
from transilienwatcher.exceptions import ConfigError, ConfigNotFoundError, InvalidConfigError

HERE = Path(__file__).parent.absolute()


def test_load_config_success():
config = ConfigLoader.load(file="config.yml")
config = ConfigManager.load(file=f"{HERE}/configs/config.yml")

assert 'transilien' in config
assert 'refresh_time' in config
assert 'display' in config


def test_load_config_fails():
def test_load_no_config_fails():
with pytest.raises(ConfigNotFoundError):
ConfigManager.load(file='no-config.yml')


def test_load_invalid_config_fails():
with pytest.raises(InvalidConfigError):
ConfigManager.load(file=f"{HERE}/configs/invalid.yml")


def test_create_config(cleanup_files):
config = f"{HERE}/test_config.yml"
ConfigManager.create(file=config)

assert Path(config).is_file()


def test_create_config_fails_if_already_exists(cleanup_files):
config = f"{HERE}/test_config.yml"
ConfigManager.create(file=config)

with pytest.raises(ConfigError):
ConfigLoader.load(file='no-config.yml')
ConfigManager.create(file=config)


def test_overwrite_config_with_env(mocker):
Expand All @@ -29,7 +52,7 @@ def test_overwrite_config_with_env(mocker):
}
}

result = ConfigLoader.overwrite_config_with_env(config)
result = ConfigManager.overwrite_config_with_env(config)

assert result['foo']['bar'] == 'foobar'

Expand All @@ -38,6 +61,6 @@ def test_update_config():
source_config = {'foo': {'bar': 'barfoo', 'test': True}}
update_config = {'foo': {'bar': 'foobar'}}

ConfigLoader.update_config(source=source_config, update=update_config)
ConfigManager.update_config(source=source_config, update=update_config)

assert source_config == {'foo': {'bar': 'foobar', 'test': True}}
15 changes: 14 additions & 1 deletion transilienwatcher/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
from pathlib import Path

from transilienwatcher.app import TransilienWatcher
from transilienwatcher.configuration import ConfigManager

WORKSPACE = f"{Path.home()}/transilenwatcher"
CONFIG_FILE = f"{WORKSPACE}/config.yml"
LOG_FILE = f"{WORKSPACE}/transilenwatcher.log"

__all__ = ["TransilienWatcher"]
__all__ = [
"TransilienWatcher",
"ConfigManager",
"WORKSPACE",
"CONFIG_FILE",
"LOG_FILE",
]
43 changes: 0 additions & 43 deletions transilienwatcher/__main__.py

This file was deleted.

4 changes: 1 addition & 3 deletions transilienwatcher/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@

from loguru import logger

from transilienwatcher.configuration import ConfigLoader
from transilienwatcher.daemon import Daemon
from transilienwatcher.display import DisplayBuilder
from transilienwatcher.transilien import Transilien


class TransilienWatcher(Daemon):
def __init__(self, log_file: str, config_file: str):
def __init__(self, log_file: str, config: dict):
pidfile = os.path.join(tempfile.gettempdir(), "transilienwatcher.pid")
app_name = self.__class__.__name__
super().__init__(
Expand All @@ -21,7 +20,6 @@ def __init__(self, log_file: str, config_file: str):
stdout=log_file,
)

config = ConfigLoader.load(file=config_file)
display = DisplayBuilder.build(config["display"])
transilien = Transilien(config["transilien"])

Expand Down
80 changes: 80 additions & 0 deletions transilienwatcher/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#!/usr/bin/env python3
import argparse
import sys
from enum import Enum, auto
from pathlib import Path

from loguru import logger

from transilienwatcher import ConfigManager, LOG_FILE, CONFIG_FILE, WORKSPACE
from transilienwatcher import TransilienWatcher


def _app_already_init():
return Path(WORKSPACE).is_dir() and Path(CONFIG_FILE).is_file()


def run():
if not _app_already_init():
print(
"It appears that it may be the first time you run TransilienWatcher."
"\nYou need to run 'transilienwatcher-init' first."
)
sys.exit()

parser = argparse.ArgumentParser(description="TransilienWatcher")
parser.add_argument(
dest="operation",
type=str,
choices=["start", "stop", "status"],
help="TransilienWatcher commands.",
)
args = parser.parse_args()

logger.remove() # Reset default loguru logger.
logger.add(LOG_FILE, rotation="00:00", retention="2 days", level="DEBUG")
config = ConfigManager.load(file=CONFIG_FILE)

daemon = TransilienWatcher(log_file=LOG_FILE, config=config)
operation = getattr(daemon, args.operation)
operation()


def init():
if _app_already_init():
print(f"TranslienWatcher config already exists in '{WORKSPACE}'.")
return

class Status(Enum):
ASK_SETUP = auto()
ASK_QUIT = auto()
SETUP = auto()
QUIT = auto()

setup_status = Status.ASK_SETUP
while setup_status != Status.QUIT:
if setup_status == Status.ASK_SETUP:
resp = input("Setup TransilienWatcher configuration file? [Y/n]: ") or "y"
if resp.lower() in ["y", "yes"]:
setup_status = Status.SETUP
if resp.lower() in ["n", "no"]:
setup_status = Status.ASK_QUIT
if setup_status == Status.ASK_QUIT:
resp = (
input("Setup is mandatory to run the app, are you sure? [Y/n]: ") or "y"
)
if resp.lower() in ["y", "yes"]:
setup_status = Status.QUIT
if resp.lower() in ["n", "no"]:
setup_status = Status.ASK_SETUP
if setup_status == Status.SETUP:
print("Setting up TransilienWatcher...")
Path(WORKSPACE).mkdir(exist_ok=True)
print("Creating TransilienWatcher config...")
ConfigManager.create(CONFIG_FILE)
print(
f"All done!\n\nYou can now edit TransilienWatcher config in "
f"'{CONFIG_FILE}' and then start the app."
)
setup_status = Status.QUIT
sys.exit()
31 changes: 23 additions & 8 deletions transilienwatcher/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,21 @@
import yaml
from cerberus import Validator

from transilienwatcher.exceptions import ConfigError
from transilienwatcher.exceptions import (
ConfigError,
InvalidConfigError,
ConfigNotFoundError,
)

default_config = {
DEFAULT_CONFIG = {
"transilien": {
"stations": {
"departure": None,
"arrival": None,
},
"credentials": {
"username": None,
"password": None,
},
},
"refresh_time": 30,
Expand Down Expand Up @@ -93,27 +98,37 @@
config_validator = Validator(config_schema)


class ConfigLoader:
class ConfigManager:
@classmethod
def load(cls, file: str):
config = dict(default_config)
def load(cls, file: str) -> dict:
config = dict(DEFAULT_CONFIG)
cls.update_config(config, cls._load_file(file=file))
config.update(cls.overwrite_config_with_env(config=config))
if not config_validator.validate(config):
raise ConfigError(
raise InvalidConfigError(
f"Invalid configuration provided.\n {config_validator.errors}"
)
return config

@staticmethod
def _load_file(file: str):
def _load_file(file: str) -> dict:
try:
with open(file, "r") as f:
config = yaml.safe_load(f)
except FileNotFoundError as error:
raise ConfigError(f"Configuration file {file} not found.") from error
raise ConfigNotFoundError(
f"Configuration file {file} not found."
) from error
return config

@classmethod
def create(cls, file: str):
try:
with open(file, "x") as f:
f.write(yaml.dump(DEFAULT_CONFIG))
except FileExistsError as error:
raise ConfigError(f"Configuration file {file} already exists.") from error

@classmethod
def overwrite_config_with_env(cls, config: dict, _path=None):
"""Iterate over config dict and upload value with env vars."""
Expand Down

0 comments on commit 5159129

Please sign in to comment.