Skip to content

Commit

Permalink
0.17.0
Browse files Browse the repository at this point in the history
- Ping handling should be more robust
- Fixed an issue where HABApp would not reconnect after an error
- Doc fixes by Rosi2143
- Added DictParameter which can be accessed like a dict
- Reworked file handling:
  It is now possible to specify files as dependencies which will be loaded before the specifying file and it is possible to automatically reload a file if another file changed. The topics for file handling changed, too.
  • Loading branch information
spacemanspiff2007 authored Dec 1, 2020
1 parent 983d08b commit 1f3fb2c
Show file tree
Hide file tree
Showing 52 changed files with 1,400 additions and 541 deletions.
2 changes: 1 addition & 1 deletion HABApp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

import HABApp.util
from HABApp.rule import Rule
from HABApp.parameters import Parameter
from HABApp.parameters import Parameter, DictParameter

#from HABApp.runtime import Runtime
from HABApp.config import CONFIG
2 changes: 1 addition & 1 deletion HABApp/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '0.16.2'
__version__ = '0.17.0'
1 change: 1 addition & 0 deletions HABApp/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from .event_bus_listener import EventBusListener

import HABApp.core.events
import HABApp.core.files
import HABApp.core.items

import HABApp.core.EventBus
Expand Down
1 change: 1 addition & 0 deletions HABApp/core/const/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
from . import topics
from .const import MISSING
from .loop import loop
from .yml import yml
9 changes: 5 additions & 4 deletions HABApp/core/const/topics.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@
except ImportError:
Final = str


INFOS: Final = 'HABApp.Infos'
WARNINGS: Final = 'HABApp.Warnings'
ERRORS: Final = 'HABApp.Errors'
INFOS: Final = 'HABApp.Infos'

RULES: Final = 'HABApp.Rules'
PARAM: Final = 'HABApp.Parameters'
FILES: Final = 'HABApp.Files'


ALL: typing.List[str] = [
WARNINGS, ERRORS, INFOS,

RULES, PARAM,
FILES
]
8 changes: 8 additions & 0 deletions HABApp/core/const/yml.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import ruamel.yaml

yml = ruamel.yaml.YAML()
yml.default_flow_style = False
yml.default_style = False # type: ignore
yml.width = 1000000 # type: ignore
yml.allow_unicode = True
yml.sort_base_mapping_type_on_output = False # type: ignore
38 changes: 14 additions & 24 deletions HABApp/core/events/habapp_events.py
Original file line number Diff line number Diff line change
@@ -1,45 +1,35 @@
import HABApp
from pathlib import Path


class RequestFileLoadEvent:
"""Request (re-) loading of the specified file
:ivar str filename: relative filename
"""
class __FileEventBase:

@classmethod
def from_path(cls, folder: Path, file: Path) -> 'RequestFileLoadEvent':
return cls(str(file.relative_to(folder)))
def from_path(cls, path: Path) -> '__FileEventBase':
return cls(HABApp.core.files.name_from_path(path))

def __init__(self, name: str):
self.filename: str = name
self.name: str = name

def get_path(self, parent_folder: Path) -> Path:
return parent_folder / self.filename
def get_path(self) -> Path:
return HABApp.core.files.path_from_name(self.name)

def __repr__(self):
return f'<{self.__class__.__name__} filename: {self.filename}>'
return f'<{self.__class__.__name__} filename: {self.name}>'


class RequestFileUnloadEvent:
"""Request unloading of the specified file
class RequestFileLoadEvent(__FileEventBase):
"""Request (re-) loading of the specified file
:ivar str filename: relative filename
"""

@classmethod
def from_path(cls, folder: Path, file: Path) -> 'RequestFileUnloadEvent':
return cls(str(file.relative_to(folder)))

def __init__(self, name: str):
self.filename: str = name

def get_path(self, parent_folder: Path) -> Path:
return parent_folder / self.filename

class RequestFileUnloadEvent(__FileEventBase):
"""Request unloading of the specified file
def __repr__(self):
return f'<{self.__class__.__name__} filename: {self.filename}>'
:ivar str filename: relative filename
"""


class HABAppError:
Expand Down
5 changes: 5 additions & 0 deletions HABApp/core/files/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from . import watcher
from .file_name import path_from_name, name_from_path
from .file import HABAppFile
from .all import watch_folder, file_load_failed, file_load_ok
from .event_listener import add_event_bus_listener
107 changes: 107 additions & 0 deletions HABApp/core/files/all.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import logging
import typing
from itertools import chain
from pathlib import Path
from threading import Lock

import HABApp
from HABApp.core.wrapper import ignore_exception
from . import name_from_path
from .file import CircularReferenceError, HABAppFile
from .watcher import AggregatingAsyncEventHandler

log = logging.getLogger('HABApp.files')

LOCK = Lock()

ALL: typing.Dict[str, HABAppFile] = {}
LOAD_RUNNING = False


def process(files: typing.List[Path], load_next: bool = True):
global LOAD_RUNNING

for file in files:
name = name_from_path(file)

# unload
if not file.is_file():
with LOCK:
existing = ALL.pop(name, None)
if existing is not None:
existing.unload()
continue

# reload/initial load
obj = HABAppFile.from_path(name, file)
with LOCK:
ALL[name] = obj

if not load_next:
return None

# Start loading only once
with LOCK:
if LOAD_RUNNING:
return None
LOAD_RUNNING = True

_load_next()


@ignore_exception
def file_load_ok(name: str):
with LOCK:
file = ALL.get(name)
file.load_ok()

# reload files with the property "reloads_on"
reload = [k.path for k in filter(lambda f: f.is_loaded and name in f.properties.reloads_on, ALL.values())]
if reload:
process(reload, load_next=False)

_load_next()


@ignore_exception
def file_load_failed(name: str):
with LOCK:
f = ALL.get(name)
f.load_failed()

_load_next()


def _load_next():
global LOAD_RUNNING

# check files for dependencies etc.
for file in list(filter(lambda x: not x.is_checked, ALL.values())):
try:
file.check_properties()
except Exception as e:
if not isinstance(e, (CircularReferenceError, FileNotFoundError)):
HABApp.core.wrapper.process_exception(file.check_properties, e, logger=log)

# Load order is parameters -> openhab config files-> rules
f_n = HABApp.core.files.file_name
_all = sorted(ALL.keys())
files = chain(filter(f_n.is_param, _all), filter(f_n.is_config, _all), filter(f_n.is_rule, _all))

for name in files:
file = ALL[name]
if file.is_loaded or file.is_failed:
continue

if file.can_be_loaded():
file.load()
return None

with LOCK:
LOAD_RUNNING = False


def watch_folder(folder: Path, file_ending: str, watch_subfolders: bool = False) -> AggregatingAsyncEventHandler:
handler = AggregatingAsyncEventHandler(folder, process, file_ending, watch_subfolders)
HABApp.core.files.watcher.add_folder_watch(handler)
return handler
58 changes: 58 additions & 0 deletions HABApp/core/files/event_listener.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from typing import Any, Callable, Optional
import logging
from pathlib import Path
from HABApp.core.logger import log_error

import HABApp


def add_event_bus_listener(
file_type: str,
func_load: Optional[Callable[[str, Path], Any]],
func_unload: Optional[Callable[[str, Path], Any]],
logger: logging.Logger):

func = {
'config': HABApp.core.files.file_name.is_config,
'rule': HABApp.core.files.file_name.is_rule,
'param': HABApp.core.files.file_name.is_param,
}[file_type]

def filter_func_load(event: HABApp.core.events.habapp_events.RequestFileLoadEvent):
if not func(event.name):
return None

name = event.name
path = event.get_path()

# Only load existing files
if not path.is_file():
log_error(logger, f'{file_type} file "{path}" does not exist and can not be loaded!')
return None

func_load(name, path)

def filter_func_unload(event: HABApp.core.events.habapp_events.RequestFileUnloadEvent):
if not func(event.name):
return None
name = event.name
path = event.get_path()
func_unload(name, path)

if filter_func_unload is not None:
HABApp.core.EventBus.add_listener(
HABApp.core.EventBusListener(
HABApp.core.const.topics.FILES,
HABApp.core.WrappedFunction(filter_func_unload),
HABApp.core.events.habapp_events.RequestFileUnloadEvent
)
)

if filter_func_load is not None:
HABApp.core.EventBus.add_listener(
HABApp.core.EventBusListener(
HABApp.core.const.topics.FILES,
HABApp.core.WrappedFunction(filter_func_load),
HABApp.core.events.habapp_events.RequestFileLoadEvent
)
)
109 changes: 109 additions & 0 deletions HABApp/core/files/file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
from __future__ import annotations

import logging
import typing
from pathlib import Path

import HABApp
from HABApp.core.const.topics import FILES as T_FILES
from HABApp.core.events.habapp_events import RequestFileLoadEvent, RequestFileUnloadEvent
from .file_props import FileProperties, get_props

log = logging.getLogger('HABApp.files')


class CircularReferenceError(Exception):
pass


class HABAppFile:

@classmethod
def from_path(cls, name: str, path: Path) -> HABAppFile:
with path.open('r', encoding='utf-8') as f:
txt = f.read(10 * 1024)
return cls(name, path, get_props(txt))

def __init__(self, name: str, path: Path, properties: FileProperties):
self.name: str = name
self.path: Path = path
self.properties: FileProperties = properties

# file checks
self.is_checked = False
self.is_valid = False

# file loaded
self.is_loaded = False
self.is_failed = False

def _check_refs(self, stack, prop: str):
c: typing.List[str] = getattr(self.properties, prop)
for f in c:
_stack = stack + (f, )
if f in stack:
log.error(f'Circular reference: {" -> ".join(_stack)}')
raise CircularReferenceError(" -> ".join(_stack))

next_file = ALL.get(f)
if next_file is not None:
next_file._check_refs(_stack, prop)

def check_properties(self):
self.is_checked = True

# check dependencies
mis = set(filter(lambda x: x not in ALL, self.properties.depends_on))
if mis:
one = len(mis) == 1
msg = f'File {self.path} depends on file{"" if one else "s"} that ' \
f'do{"es" if one else ""}n\'t exist: {", ".join(sorted(mis))}'
log.error(msg)
raise FileNotFoundError(msg)

# check reload
mis = set(filter(lambda x: x not in ALL, self.properties.reloads_on))
if mis:
one = len(mis) == 1
log.warning(f'File {self.path} reloads on file{"" if one else "s"} that '
f'do{"es" if one else ""}n\'t exist: {", ".join(sorted(mis))}')

# check for circular references
self._check_refs((self.name, ), 'depends_on')
self._check_refs((self.name, ), 'reloads_on')

self.is_valid = True

def can_be_loaded(self) -> bool:
if not self.is_valid:
return False

for name in self.properties.depends_on:
f = ALL.get(name, None)
if f is None:
return False

if not f.is_loaded:
return False
return True

def load(self):
self.is_loaded = False

HABApp.core.EventBus.post_event(
T_FILES, RequestFileLoadEvent(self.name)
)

def unload(self):
HABApp.core.EventBus.post_event(
T_FILES, RequestFileUnloadEvent(self.name)
)

def load_ok(self):
self.is_loaded = True

def load_failed(self):
self.is_failed = True


from .all import ALL # noqa F401
Loading

0 comments on commit 1f3fb2c

Please sign in to comment.