From 5125bb65f378730a9581d9d3cfd61f96ff545db4 Mon Sep 17 00:00:00 2001 From: Robert Goldmann <deadlocker@gmx.de> Date: Fri, 9 Oct 2020 19:56:13 +0200 Subject: [PATCH] #3 - split pages in tiles + introduce tileservice + scheduling --- Pipfile | 4 +- src/DashboardLeaf.py | 57 ++++++++++++- src/page/Page.py | 32 ------- src/page/PageManager.py | 34 ++++---- src/page/pages/ClockPage.py | 20 ----- src/tile/Tile.py | 36 ++++++++ .../PageRegistry.py => tile/TileRegistry.py} | 29 ++++--- src/tile/TileService.py | 85 +++++++++++++++++++ src/{page/pages => tile}/__init__.py | 0 src/tile/tiles/ClockTile.py | 20 +++++ src/tile/tiles/__init__.py | 0 11 files changed, 229 insertions(+), 88 deletions(-) delete mode 100644 src/page/Page.py delete mode 100644 src/page/pages/ClockPage.py create mode 100644 src/tile/Tile.py rename src/{page/PageRegistry.py => tile/TileRegistry.py} (51%) create mode 100644 src/tile/TileService.py rename src/{page/pages => tile}/__init__.py (100%) create mode 100644 src/tile/tiles/ClockTile.py create mode 100644 src/tile/tiles/__init__.py diff --git a/Pipfile b/Pipfile index 693b74c..68ba84a 100644 --- a/Pipfile +++ b/Pipfile @@ -13,10 +13,12 @@ python_version = "3" [packages] flask = "==1.1.2" -gevent = "==20.6.1" +gevent = "==20.9.0" TheCodeLabs-BaseUtils = "*" TheCodeLabs-FlaskUtils = "*" Pillow = "==7.2.0" +flask-socketio= "==4.3.1" +apscheduler = "==3.6.3" # services python-jenkins = "==1.5.0" diff --git a/src/DashboardLeaf.py b/src/DashboardLeaf.py index f46b401..2bd35f4 100644 --- a/src/DashboardLeaf.py +++ b/src/DashboardLeaf.py @@ -1,13 +1,18 @@ +import json +import logging import os from TheCodeLabs_BaseUtils.DefaultLogger import DefaultLogger from TheCodeLabs_FlaskUtils.FlaskBaseApp import FlaskBaseApp +from flask import Flask +from flask_socketio import SocketIO from blueprints import Routes from logic import Constants -from page.PageManager import PageManager -from page.PageRegistry import PageRegistry from logic.services.JenkinsSingleJobService import JenkinsSingleJobService +from page.PageManager import PageManager +from tile.TileRegistry import TileRegistry +from tile.TileService import TileService LOGGER = DefaultLogger().create_logger_if_not_exists(Constants.APP_NAME) @@ -19,13 +24,57 @@ class DashboardLeaf(FlaskBaseApp): def __init__(self, appName: str): super().__init__(appName, os.path.dirname(__file__), LOGGER, serveRobotsTxt=True) - self._pageRegistry = PageRegistry('page.pages') - self._pageManager = PageManager(Constants.ROOT_DIR, self._pageRegistry) + self._pageManager = PageManager(Constants.ROOT_DIR) + self._tileRegistry = TileRegistry('tile.tiles') + self._socketio = None + self._tileService = None + + def _create_flask_app(self): + app = Flask(self._rootDir) + self._socketio = SocketIO(app) + logging.getLogger('flask_socketio').setLevel(logging.ERROR) + + @self._socketio.on('refresh', namespace='/update') + def Refresh(tileName): + raise NotImplementedError + # tileService.ForceRefresh(tileName) + + @self._socketio.on('connect', namespace='/update') + def Connect(): + raise NotImplementedError + # LOGGER.debug('Client connected') + # tileService.EmitFromCache() + + self._tileService = self.__register_tiles(app) + + return app def _register_blueprints(self, app): app.register_blueprint(Routes.construct_blueprint(self._settings, self._pageManager)) return app + def __register_tiles(self, app) -> TileService: + tileService = TileService(self._socketio) + + with open(os.path.join(Constants.ROOT_DIR, 'pageSettings.json'), 'r') as f: + config = json.load(f) + + # TODO + for tileConfig in config[0]['tiles']: + tileType = tileConfig['tileType'] + if tileType not in self._tileRegistry.get_all_available_tile_types(): + LOGGER.error(f'Skipping unknown tile with type "{tileType}"') + continue + + tile = self._tileRegistry.get_tile_by_type(tileType)(uniqueName=tileConfig['uniqueName'], + settings=tileConfig['settings'], + intervalInSeconds=tileConfig['intervalInSeconds']) + tileService.RegisterTile(tile) + app.register_blueprint(tile.ConstructBlueprint(tileService=tileService)) + + tileService.Run() + return tileService + if __name__ == '__main__': website = DashboardLeaf(Constants.APP_NAME) diff --git a/src/page/Page.py b/src/page/Page.py deleted file mode 100644 index 7603a9b..0000000 --- a/src/page/Page.py +++ /dev/null @@ -1,32 +0,0 @@ -from abc import ABC, abstractmethod -from typing import List, Dict - -from TheCodeLabs_BaseUtils import CachedService - - -class Page(ABC): - """ - Abstract page class. Custom implementations must inherit from this class in order to work. - Page implementations are dynamically scanned via the PageRegistry. - """ - def __init__(self, name: str, settings: Dict): - self._name = name - self._settings = settings - - # user can choose from dropdown, list, whatever in editor - @abstractmethod - def register_services(self) -> List[CachedService]: - pass - - # user must implement this methods in website textarea in editor - @abstractmethod - def fetch(self, services: Dict) -> Dict: - pass - - @abstractmethod - def render(self, data: Dict) -> str: - pass - - def update(self) -> str: - data = self.fetch({}) - return self.render(data) diff --git a/src/page/PageManager.py b/src/page/PageManager.py index 4445708..7c38746 100644 --- a/src/page/PageManager.py +++ b/src/page/PageManager.py @@ -2,21 +2,17 @@ import json import os from typing import List, Dict -from page.Page import Page -from page.PageRegistry import PageRegistry - class PageManager: """ Handles the page settings (order, additional settings per pages) and provides access to the corresponding page instances. """ - def __init__(self, settingsFolder: str, pageRegistry: PageRegistry): + def __init__(self, settingsFolder: str): self._settingsFolder = settingsFolder - self._pageRegistry = pageRegistry self._pageSettingsPath = os.path.join(self._settingsFolder, 'pageSettings.json') self._pageSettings = self.__load_settings() - self._pageInstances = self.__create_page_instances() + # self._pageInstances = self.__create_page_instances() def __load_settings(self) -> List[Dict]: if not os.path.exists(self._pageSettingsPath): @@ -29,20 +25,20 @@ class PageManager: with open(self._pageSettingsPath, 'w', encoding='UTF-8') as f: json.dump(self._pageSettings, f) - def __create_page_instances(self) -> Dict[str, Page]: - pageInstances = {} - for pageSetting in self._pageSettings: - pageType = pageSetting['pageType'] - uniqueName = pageSetting['uniqueName'] - settings = pageSetting['settings'] - pageInstance = self._pageRegistry.get_page_by_type(pageType) - pageInstances[uniqueName] = pageInstance(uniqueName, settings) - return pageInstances + # def __create_page_instances(self) -> Dict: + # pageInstances = {} + # for pageSetting in self._pageSettings: + # pageType = pageSetting['pageType'] + # uniqueName = pageSetting['uniqueName'] + # settings = pageSetting['settings'] + # pageInstance = self._pageRegistry.get_tile_by_type(pageType) + # pageInstances[uniqueName] = pageInstance(uniqueName, settings) + # return pageInstances def save_and_load(self): self.__save_settings() self._pageSettings = self.__load_settings() - self._pageInstances = self.__create_page_instances() + # self._pageInstances = self.__create_page_instances() def add_page(self, index: int, pageName: str, uniqueName: str, settings: Dict): self._pageSettings.insert(index, {'pageName': pageName, 'uniqueName': uniqueName, 'settings': settings}) @@ -53,7 +49,7 @@ class PageManager: self.save_and_load() def get_all_available_page_names(self): - return list(self._pageInstances.keys()) + return [page['uniqueName'] for page in self._pageSettings] - def get_page_instance_by_name(self, name: str): - return self._pageInstances[name] + # def get_page_instance_by_name(self, name: str): + # return self._pageInstances[name] diff --git a/src/page/pages/ClockPage.py b/src/page/pages/ClockPage.py deleted file mode 100644 index da25f1d..0000000 --- a/src/page/pages/ClockPage.py +++ /dev/null @@ -1,20 +0,0 @@ -from datetime import datetime -from typing import Dict, List - -from TheCodeLabs_BaseUtils import CachedService - -from page.Page import Page - - -class ClockPage(Page): - def __init__(self, uniqueName: str, settings: Dict): - super().__init__(uniqueName, settings) - - def register_services(self) -> List[CachedService]: - pass - - def fetch(self, services: Dict) -> Dict: - return {'time': datetime.now()} - - def render(self, data: Dict) -> str: - return f'Current time: {data["time"]}' diff --git a/src/tile/Tile.py b/src/tile/Tile.py new file mode 100644 index 0000000..f7144b3 --- /dev/null +++ b/src/tile/Tile.py @@ -0,0 +1,36 @@ +from abc import ABC, abstractmethod +from typing import Dict, Tuple + + +class Tile(ABC): + """ + Abstract tile class. Custom implementations must inherit from this class in order to work. + Tile implementations are dynamically scanned via the TileRegistry. + """ + + def __init__(self, uniqueName: str, settings: Dict, intervalInSeconds: int): + self._uniqueName = uniqueName + self._settings = settings + self._intervalInSeconds = intervalInSeconds + + def get_uniqueName(self) -> str: + return self._uniqueName + + def get_intervalInSeconds(self) -> int: + return self._intervalInSeconds + + @abstractmethod + def fetch(self, services: Dict) -> Dict: + pass + + @abstractmethod + def render(self, data: Dict) -> str: + pass + + def update(self) -> Tuple[str, str]: + data = self.fetch({}) + return self._uniqueName, self.render(data) + + @abstractmethod + def ConstructBlueprint(self, *args, **kwargs): + pass diff --git a/src/page/PageRegistry.py b/src/tile/TileRegistry.py similarity index 51% rename from src/page/PageRegistry.py rename to src/tile/TileRegistry.py index 4c33baf..01e3e33 100644 --- a/src/page/PageRegistry.py +++ b/src/tile/TileRegistry.py @@ -1,28 +1,30 @@ import inspect import logging import pkgutil +from typing import List from logic import Constants -from page.Page import Page +from tile.Tile import Tile LOGGER = logging.getLogger(Constants.APP_NAME) -class PageRegistry: +class TileRegistry: """ - Scans for available page implementations and provides access to them via class name + Scans for available tile implementations and provides access to them via class name """ + def __init__(self, package: str): self._package = package - self._availablePages = {} + self._availableTiles = {} self.reload() def reload(self): - self._availablePages = self.__scan_package(self._package) + self._availableTiles = self.__scan_package(self._package) @staticmethod def __scan_package(package: str): - availablePages = {} + availableTiles = {} imported_package = __import__(package, fromlist=['blah']) for _, pluginName, isPkg in pkgutil.iter_modules(imported_package.__path__, imported_package.__name__ + '.'): @@ -30,10 +32,13 @@ class PageRegistry: pluginModule = __import__(pluginName, fromlist=['blah']) clsMembers = inspect.getmembers(pluginModule, inspect.isclass) for (_, c) in clsMembers: - if issubclass(c, Page) and c is not Page: - availablePages[c.__name__] = c - LOGGER.debug(f'Found {len(availablePages)} pages {list(availablePages.keys())}') - return availablePages + if issubclass(c, Tile) and c is not Tile: + availableTiles[c.__name__] = c + LOGGER.debug(f'Found {len(availableTiles)} tiles {list(availableTiles.keys())}') + return availableTiles + + def get_tile_by_type(self, tileType: str) -> Tile: + return self._availableTiles[tileType] - def get_page_by_type(self, pageType: str) -> Page: - return self._availablePages[pageType] + def get_all_available_tile_types(self) -> List[str]: + return list(self._availableTiles.keys()) diff --git a/src/tile/TileService.py b/src/tile/TileService.py new file mode 100644 index 0000000..b8ee759 --- /dev/null +++ b/src/tile/TileService.py @@ -0,0 +1,85 @@ +import json +import logging +from datetime import datetime +from typing import Dict + +from apscheduler.events import EVENT_JOB_EXECUTED, EVENT_JOB_ERROR +from apscheduler.job import Job +from apscheduler.schedulers.gevent import GeventScheduler + +from logic import Constants +from tile.Tile import Tile + +LOGGER = logging.getLogger(Constants.APP_NAME) + + +class TileService: + def __init__(self, socketio): + self.__socketio = socketio + self.__jobs = {} + self.__tiles = {} + self.__cache = {} + self.__scheduler = GeventScheduler() + + def RegisterTile(self, tile: Tile): + name = tile.get_uniqueName() + if name in self.__jobs: + LOGGER.warning(f'Tile "{name}" already registered') + + job = self.__scheduler.add_job(tile.update, 'interval', + seconds=tile.get_intervalInSeconds(), + next_run_time=datetime.now()) + + self.__jobs[name] = job + self.__cache[name] = None + self.__tiles[name] = tile + LOGGER.debug(f'Registered "{name}" (scheduled every {tile.get_intervalInSeconds()} seconds)') + + def UnregisterTile(self, tile: Tile): + name = tile.get_uniqueName() + if name not in self.__jobs: + LOGGER.warning(f'Tile "{name}" is not registered') + + self.__jobs[name].remove() + del self.__jobs[name] + del self.__cache[name] + del self.__tiles[name] + LOGGER.debug(f'Unregistered "{name}"') + + def EmitFromCache(self): + for name, value in self.__cache.items(): + self.__EmitUpdate(name, value) + + def Run(self): + def JobListener(event): + if event.exception: + LOGGER.error(event.exception) + else: + name, value = event.retval + self.__cache[name] = value + self.__EmitUpdate(name, value) + + self.__scheduler.add_listener(JobListener, EVENT_JOB_EXECUTED | EVENT_JOB_ERROR) + self.__scheduler.start() + + def __EmitUpdate(self, uniqueName: str, content: str): + data = {'uniqueName': uniqueName, 'content': content} + self.__socketio.emit('tileUpdate', json.dumps(data), namespace='/update') + + def GetTiles(self) -> Dict[str, Tile]: + return self.__tiles + + def GetJobs(self) -> Dict[str, Job]: + return self.__jobs + + def ForceRefresh(self, tileName): + job = self.__GetJobByName(tileName) + if job is not None: + LOGGER.debug(f'Manual refresh for tile "{tileName}"') + job.modify(next_run_time=datetime.now()) + + def __GetJobByName(self, tileName) -> Job or None: + if tileName not in self.__jobs: + LOGGER.warning(f'Ignoring request to refresh non-existing tile "{tileName}"') + return None + return self.__jobs[tileName] diff --git a/src/page/pages/__init__.py b/src/tile/__init__.py similarity index 100% rename from src/page/pages/__init__.py rename to src/tile/__init__.py diff --git a/src/tile/tiles/ClockTile.py b/src/tile/tiles/ClockTile.py new file mode 100644 index 0000000..70c3337 --- /dev/null +++ b/src/tile/tiles/ClockTile.py @@ -0,0 +1,20 @@ +from datetime import datetime +from typing import Dict + +from flask import Blueprint + +from tile.Tile import Tile + + +class ClockTile(Tile): + def __init__(self, uniqueName: str, settings: Dict, intervalInSeconds: int): + super().__init__(uniqueName, settings, intervalInSeconds) + + def fetch(self, services: Dict) -> Dict: + return {'time': datetime.now()} + + def render(self, data: Dict) -> str: + return f'Current time: {data["time"]}' + + def ConstructBlueprint(self, *args, **kwargs): + return Blueprint('clock_{}'.format(self.get_uniqueName()), __name__) diff --git a/src/tile/tiles/__init__.py b/src/tile/tiles/__init__.py new file mode 100644 index 0000000..e69de29 -- GitLab