From d07e91a9eb9e47436c7f3d060b440add72f39c6e Mon Sep 17 00:00:00 2001 From: Alban Date: Thu, 24 Dec 2020 14:00:02 +0100 Subject: [PATCH 1/3] handlers: rework the export handler class for the web service Currently the export classes expose an `run` method that directly write to a file given in the config. To allow using the exporter in a web service rework the classes to also provide an `export` method that only produce the data. The `run` method can then be provided by the base class using the new `export` method. --- handlers/base.py | 11 ++++++++++- handlers/export_handlers/basic_xml.py | 7 ++----- handlers/export_handlers/extended_xml.py | 7 ++----- handlers/export_handlers/frab_json.py | 8 ++------ 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/handlers/base.py b/handlers/base.py index c478c2c..47b4d1c 100644 --- a/handlers/base.py +++ b/handlers/base.py @@ -1,8 +1,13 @@ +import logging + from abc import ABCMeta, abstractmethod from configparser import ConfigParser from fahrplan.model.schedule import Schedule +from hacks import noexcept +from util import write_output +log = logging.getLogger(__name__) class HandlerBase(metaclass=ABCMeta): def __init__(self, name: str, config: ConfigParser, global_config: ConfigParser): @@ -18,6 +23,10 @@ def run(self) -> Schedule: class ExportHandler(HandlerBase, metaclass=ABCMeta): - @abstractmethod + @noexcept(log) def run(self, schedule: Schedule) -> bool: + return write_output(self.config["path"], self.export(schedule)) + + @abstractmethod + def export(self, schedule: Schedule) -> str: pass diff --git a/handlers/export_handlers/basic_xml.py b/handlers/export_handlers/basic_xml.py index 32cf5ba..360a2bf 100644 --- a/handlers/export_handlers/basic_xml.py +++ b/handlers/export_handlers/basic_xml.py @@ -10,8 +10,5 @@ class BasicXMLExportHandler(ExportHandler): - @noexcept(log) - def run(self, schedule: Schedule) -> bool: - path = self.config["path"] - content = schedule.to_xml(extended=False) - return write_output(path, content) + def export(self, schedule: Schedule) -> str: + return schedule.to_xml(extended=False) diff --git a/handlers/export_handlers/extended_xml.py b/handlers/export_handlers/extended_xml.py index e55dbea..93fb777 100644 --- a/handlers/export_handlers/extended_xml.py +++ b/handlers/export_handlers/extended_xml.py @@ -10,8 +10,5 @@ class ExtendedXMLExportHandler(ExportHandler): - @noexcept(log) - def run(self, schedule: Schedule) -> bool: - path = self.config["path"] - content = schedule.to_xml(extended=True) - return write_output(path, content) + def export(self, schedule: Schedule) -> str: + return schedule.to_xml(extended=True) diff --git a/handlers/export_handlers/frab_json.py b/handlers/export_handlers/frab_json.py index fb54ef2..e484d31 100644 --- a/handlers/export_handlers/frab_json.py +++ b/handlers/export_handlers/frab_json.py @@ -4,19 +4,15 @@ from ..base import ExportHandler from fahrplan.model.schedule import Schedule from fahrplan.datetime import format_duration, format_date, format_datetime, format_time -from hacks import noexcept -from util import write_output log = logging.getLogger(__name__) class FrabJsonExportHandler(ExportHandler): - @noexcept(log) - def run(self, schedule: Schedule) -> bool: - path = self.config["path"] + def export(self, schedule: Schedule) -> str: content = self.get_data(schedule) - return write_output(path, json.dumps({"schedule": content}, ensure_ascii=False, sort_keys=True, indent=2)) + return json.dumps({"schedule": content}, ensure_ascii=False, sort_keys=True, indent=2) def get_data(self, schedule): """ From 6fd26ba6449b4950df92a8537d23cd141c7b2ba0 Mon Sep 17 00:00:00 2001 From: Alban Date: Thu, 24 Dec 2020 15:18:20 +0100 Subject: [PATCH 2/3] handlers: let the export handlers report the content type they produce The content type of the export is needed for the web service, let the exporters report it. --- handlers/base.py | 2 ++ handlers/export_handlers/basic_xml.py | 2 ++ handlers/export_handlers/extended_xml.py | 2 ++ handlers/export_handlers/frab_json.py | 2 ++ 4 files changed, 8 insertions(+) diff --git a/handlers/base.py b/handlers/base.py index 47b4d1c..3fe53d9 100644 --- a/handlers/base.py +++ b/handlers/base.py @@ -23,6 +23,8 @@ def run(self) -> Schedule: class ExportHandler(HandlerBase, metaclass=ABCMeta): + content_type = None + @noexcept(log) def run(self, schedule: Schedule) -> bool: return write_output(self.config["path"], self.export(schedule)) diff --git a/handlers/export_handlers/basic_xml.py b/handlers/export_handlers/basic_xml.py index 360a2bf..d977d69 100644 --- a/handlers/export_handlers/basic_xml.py +++ b/handlers/export_handlers/basic_xml.py @@ -10,5 +10,7 @@ class BasicXMLExportHandler(ExportHandler): + content_type = "application/xml" + def export(self, schedule: Schedule) -> str: return schedule.to_xml(extended=False) diff --git a/handlers/export_handlers/extended_xml.py b/handlers/export_handlers/extended_xml.py index 93fb777..ea793be 100644 --- a/handlers/export_handlers/extended_xml.py +++ b/handlers/export_handlers/extended_xml.py @@ -10,5 +10,7 @@ class ExtendedXMLExportHandler(ExportHandler): + content_type = "application/xml" + def export(self, schedule: Schedule) -> str: return schedule.to_xml(extended=True) diff --git a/handlers/export_handlers/frab_json.py b/handlers/export_handlers/frab_json.py index e484d31..f7d2d71 100644 --- a/handlers/export_handlers/frab_json.py +++ b/handlers/export_handlers/frab_json.py @@ -10,6 +10,8 @@ class FrabJsonExportHandler(ExportHandler): + content_type = "application/json" + def export(self, schedule: Schedule) -> str: content = self.get_data(schedule) return json.dumps({"schedule": content}, ensure_ascii=False, sort_keys=True, indent=2) From 815428b6a6f500ae4010b2b1dfe7687df7057993 Mon Sep 17 00:00:00 2001 From: Alban Bedel Date: Tue, 22 Dec 2020 05:44:21 +0100 Subject: [PATCH 3/3] add a simple web service to serve the converted schedules Add a simple web service that can serve the schedules dynamicaly. It use the same config as the file exporter but instead of writing to files it serve the configured paths. Using the demo data you can then get the json schedule at: http://localhost:8080/demo/gpn11.json and the extended xml at http://localhost:8080/demo/gpn11.xml. --- webschedule.py | 107 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100755 webschedule.py diff --git a/webschedule.py b/webschedule.py new file mode 100755 index 0000000..f07ff34 --- /dev/null +++ b/webschedule.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 + +import logging + +from argparse import ArgumentParser, FileType +from configparser import ConfigParser +from functools import reduce + +from fahrplan.model.schedule import Schedule + +import schedule +from functools import partial +from http.server import HTTPServer, BaseHTTPRequestHandler +from urllib.parse import urlparse + +log = logging.getLogger(__name__) + +class HTTPRequestHandler(BaseHTTPRequestHandler): + def __init__(self, config, import_handlers, export_handlers, *args, **kwargs): + self.config = config + self.import_handlers = import_handlers + self.export_handlers = { h.config["path"]: h for h in export_handlers } + super().__init__(*args, **kwargs) + + def do_GET(self): + log.info(f'Get {self.path}') + url = urlparse(self.path) + path = url.path[1:] + + if path not in self.export_handlers: + self.send_error(404, 'Path not found') + return + + try: + schedule = self.make_schedule() + if schedule is None: + self.send_error(204) + return + handler = self.export_handlers[path] + body = handler.export(schedule) + if body is not None and not isinstance(body, bytes): + body = body.encode() + content_type = handler.content_type + except Exception as err: + self.send_error(500, str(err)) + raise + + self.send_response(200) + self.send_header('Content-Type', content_type) + self.send_header('Content-Length', len(body)) + self.end_headers() + self.wfile.write(body) + + def make_schedule(self): + imported_schedules = [] + log.info('Running import handlers') + for handler in self.import_handlers: + log.info(f'Running import handler "{handler.name}".') + imported_schedules.append(handler.run()) + log.debug('Finished running import handlers') + + if not any(imported_schedules): + return None + + log.info('Merging schedules.') + final_schedule = reduce(Schedule.merge, imported_schedules) + log.debug('Finished merging schedules.') + + return final_schedule + +def main(): + ap = ArgumentParser() + ap.add_argument('--verbose', '-v', action='count') + ap.add_argument('--quiet', '-q', action='count') + ap.add_argument('--config', '-c', nargs='?', type=FileType('r'), default='./config.ini') + ap.add_argument('--logfile', '-l') + ap.add_argument('--debug', '-d', action='store_true') + ap.add_argument('--interface', '-i', default='localhost') + ap.add_argument('--port', '-p', type=int, default=8080) + args = ap.parse_args() + + schedule.configure_logging(args) + + log.info(f'Using config file "{args.config.name}".') + config = ConfigParser() + config.read_file(args.config) + log.debug('Basic initialization done.') + + import_handlers = schedule.initialize_import_handlers(config) + if not import_handlers: + log.critical("No import handlers to run, aborting.") + sys.exit(1) + + export_handlers = schedule.initialize_export_handlers(config) + if not export_handlers: + log.critical("No export handlers to run, aborting.") + sys.exit(1) + + req_handler = partial(HTTPRequestHandler, config, import_handlers, export_handlers) + httpd = HTTPServer((args.interface, args.port), req_handler) + try: + httpd.serve_forever() + except KeyboardInterrupt: + pass + +if __name__ == '__main__': + main()