Skip to content
Snippets Groups Projects
Commit 5125bb65 authored by Robert Goldmann's avatar Robert Goldmann
Browse files

#3 - split pages in tiles + introduce tileservice + scheduling

parent 24524a41
No related branches found
No related tags found
No related merge requests found
......@@ -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"
......
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)
......
......@@ -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]
from abc import ABC, abstractmethod
from typing import List, Dict
from typing import Dict, Tuple
from TheCodeLabs_BaseUtils import CachedService
class Page(ABC):
class Tile(ABC):
"""
Abstract page class. Custom implementations must inherit from this class in order to work.
Page implementations are dynamically scanned via the PageRegistry.
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, name: str, settings: Dict):
self._name = name
def __init__(self, uniqueName: str, settings: Dict, intervalInSeconds: int):
self._uniqueName = uniqueName
self._settings = settings
self._intervalInSeconds = intervalInSeconds
# user can choose from dropdown, list, whatever in editor
@abstractmethod
def register_services(self) -> List[CachedService]:
pass
def get_uniqueName(self) -> str:
return self._uniqueName
def get_intervalInSeconds(self) -> int:
return self._intervalInSeconds
# user must implement this methods in website textarea in editor
@abstractmethod
def fetch(self, services: Dict) -> Dict:
pass
......@@ -27,6 +27,10 @@ class Page(ABC):
def render(self, data: Dict) -> str:
pass
def update(self) -> str:
def update(self) -> Tuple[str, str]:
data = self.fetch({})
return self.render(data)
return self._uniqueName, self.render(data)
@abstractmethod
def ConstructBlueprint(self, *args, **kwargs):
pass
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())
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]
File moved
from datetime import datetime
from typing import Dict, List
from typing import Dict
from TheCodeLabs_BaseUtils import CachedService
from flask import Blueprint
from page.Page import Page
from tile.Tile import Tile
class ClockPage(Page):
def __init__(self, uniqueName: str, settings: Dict):
super().__init__(uniqueName, settings)
def register_services(self) -> List[CachedService]:
pass
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__)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment