Coverage for hookee/server.py: 95.00%
60 statements
« prev ^ index » next coverage.py v7.3.4, created at 2023-12-27 17:55 +0000
« prev ^ index » next coverage.py v7.3.4, created at 2023-12-27 17:55 +0000
1import logging
2import threading
3import time
4from http import HTTPStatus
5from urllib.error import URLError
6from urllib.request import urlopen, Request
8from flask import Flask
10from hookee import pluginmanager
12__author__ = "Alex Laird"
13__copyright__ = "Copyright 2023, Alex Laird"
14__version__ = "2.0.0"
16werkzeug_logger = logging.getLogger("werkzeug")
17werkzeug_logger.setLevel(logging.ERROR)
20class Server:
21 """
22 An object that manages a non-blocking Flask server and thread.
24 :var hookee_manager: Reference to the ``hookee`` Manager.
25 :vartype hookee_manager: HookeeManager
26 :var plugin_manager: Reference to the Plugin Manager.
27 :vartype plugin_manager: PluginManager
28 :var print_util: Reference to the PrintUtil.
29 :vartype print_util: PrintUtil
30 :var port: The server's port.
31 :vartype port: int
32 :var app: The Flask app.
33 :vartype app: flask.Flask
34 """
36 def __init__(self, hookee_manager):
37 self.hookee_manager = hookee_manager
38 self.plugin_manager = self.hookee_manager.plugin_manager
39 self.print_util = self.hookee_manager.print_util
40 self.port = self.hookee_manager.config.get("port")
42 self.app = self.create_app()
44 self._thread = None
46 def create_app(self):
47 """
48 Create a Flask app and register all Blueprints found in enabled plugins.
50 :return: The Flask app.
51 :rtype: flask.Flask
52 """
53 app = Flask(__name__)
55 app.config.from_mapping(
56 ENV="development"
57 )
59 for plugin in self.plugin_manager.get_plugins_by_type(pluginmanager.BLUEPRINT_PLUGIN):
60 app.register_blueprint(plugin.blueprint)
62 return app
64 def _loop(self):
65 thread = None
67 try:
68 thread = threading.current_thread()
69 thread.alive = True
71 # This will block until stop() is invoked to shutdown the Werkzeug server
72 self.app.run(host="127.0.0.1", port=self.port, debug=True, use_reloader=False)
73 except OSError as e:
74 self.print_util.print_basic(e)
76 self.stop()
78 if thread:
79 thread.alive = False
81 def start(self):
82 """
83 If one is not already running, start a server in a new thread.
84 """
85 if self._thread is None:
86 self.print_util.print_open_header("Starting Server")
88 self._thread = threading.Thread(target=self._loop)
89 self._thread.start()
91 while self._server_status() != HTTPStatus.OK:
92 time.sleep(1)
94 self.print_close_header()
96 def stop(self):
97 """
98 If running, kill the server and cleanup its thread.
99 """
100 if self._thread:
101 req = Request("http://127.0.0.1:{}/shutdown".format(self.port), method="POST")
102 urlopen(req)
104 self._thread = None
106 def _server_status(self):
107 """
108 Get the response code of the server's ``/status`` endpoint.
110 :return: The status code.
111 :rtype: http.HTTPStatus
112 """
113 try:
114 return urlopen("http://127.0.0.1:{}/status".format(self.port)).getcode()
115 except URLError:
116 return HTTPStatus.INTERNAL_SERVER_ERROR
118 def print_close_header(self):
119 self.print_util.print_basic(" * Port: {}".format(self.port))
120 self.print_util.print_basic(" * Blueprints: registered")
121 self.print_util.print_close_header()