Coverage for hookee/server.py: 95.00%

60 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-12-04 15:06 +0000

1import logging 

2import threading 

3import time 

4from http import HTTPStatus 

5from urllib.error import URLError 

6from urllib.request import urlopen, Request 

7 

8from flask import Flask 

9 

10from hookee import pluginmanager 

11 

12__author__ = "Alex Laird" 

13__copyright__ = "Copyright 2023, Alex Laird" 

14__version__ = "2.0.0" 

15 

16werkzeug_logger = logging.getLogger("werkzeug") 

17werkzeug_logger.setLevel(logging.ERROR) 

18 

19 

20class Server: 

21 """ 

22 An object that manages a non-blocking Flask server and thread. 

23 

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 """ 

35 

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") 

41 

42 self.app = self.create_app() 

43 

44 self._thread = None 

45 

46 def create_app(self): 

47 """ 

48 Create a Flask app and register all Blueprints found in enabled plugins. 

49 

50 :return: The Flask app. 

51 :rtype: flask.Flask 

52 """ 

53 app = Flask(__name__) 

54 

55 app.config.from_mapping( 

56 ENV="development" 

57 ) 

58 

59 for plugin in self.plugin_manager.get_plugins_by_type(pluginmanager.BLUEPRINT_PLUGIN): 

60 app.register_blueprint(plugin.blueprint) 

61 

62 return app 

63 

64 def _loop(self): 

65 thread = None 

66 

67 try: 

68 thread = threading.current_thread() 

69 thread.alive = True 

70 

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) 

75 

76 self.stop() 

77 

78 if thread: 

79 thread.alive = False 

80 

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") 

87 

88 self._thread = threading.Thread(target=self._loop) 

89 self._thread.start() 

90 

91 while self._server_status() != HTTPStatus.OK: 

92 time.sleep(1) 

93 

94 self.print_close_header() 

95 

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) 

103 

104 self._thread = None 

105 

106 def _server_status(self): 

107 """ 

108 Get the response code of the server's ``/status`` endpoint. 

109 

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 

117 

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()