moonraker: add annotations

Annotate function definitions, class attributes, and local variables as necessary for type hinting.  This is useful for IDEs with linters that support type hints and also can be used by GitHub Actions to detect erroneous code.

Signed-off-by:  Eric Callahan <arksine.code@gmail.com>
This commit is contained in:
Arksine 2021-05-11 18:10:47 -04:00
parent 9c76dbef7a
commit ca69a10838
1 changed files with 146 additions and 88 deletions

View File

@ -4,6 +4,8 @@
# Copyright (C) 2020 Eric Callahan <arksine.code@gmail.com> # Copyright (C) 2020 Eric Callahan <arksine.code@gmail.com>
# #
# This file may be distributed under the terms of the GNU GPLv3 license # This file may be distributed under the terms of the GNU GPLv3 license
from __future__ import annotations
import argparse import argparse
import sys import sys
import importlib import importlib
@ -22,7 +24,28 @@ from tornado.ioloop import IOLoop
from tornado.util import TimeoutError from tornado.util import TimeoutError
from tornado.locks import Event from tornado.locks import Event
from app import MoonrakerApp from app import MoonrakerApp
from utils import ServerError from utils import ServerError, SentinelClass
# Annotation imports
from typing import (
TYPE_CHECKING,
Any,
Optional,
Callable,
Coroutine,
Tuple,
Dict,
List,
Union,
TypeVar,
)
if TYPE_CHECKING:
from websockets import WebRequest, WebSocket, Subscribable
from components.data_store import DataStore
from components.klippy_apis import KlippyAPI
from components.file_manager import FileManager
FlexCallback = Callable[..., Optional[Coroutine]]
_T = TypeVar("_T")
INIT_TIME = .25 INIT_TIME = .25
LOG_ATTEMPT_INTERVAL = int(2. / INIT_TIME + .5) LOG_ATTEMPT_INTERVAL = int(2. / INIT_TIME + .5)
@ -32,12 +55,14 @@ CORE_COMPONENTS = [
'database', 'file_manager', 'klippy_apis', 'machine', 'database', 'file_manager', 'klippy_apis', 'machine',
'data_store', 'shell_command', 'proc_stats'] 'data_store', 'shell_command', 'proc_stats']
class Sentinel: SENTINEL = SentinelClass.get_instance()
pass
class Server: class Server:
error = ServerError error = ServerError
def __init__(self, args, file_logger): def __init__(self,
args: argparse.Namespace,
file_logger: Optional[utils.MoonrakerLoggingHandler]
) -> None:
self.file_logger = file_logger self.file_logger = file_logger
self.config = config = confighelper.get_configuration(self, args) self.config = config = confighelper.get_configuration(self, args)
# log config file # log config file
@ -48,29 +73,29 @@ class Server:
cfg_item += "#"*65 cfg_item += "#"*65
strio.close() strio.close()
self.add_log_rollover_item('config', cfg_item) self.add_log_rollover_item('config', cfg_item)
self.host = config.get('host', "0.0.0.0") self.host: str = config.get('host', "0.0.0.0")
self.port = config.getint('port', 7125) self.port: int = config.getint('port', 7125)
self.exit_reason = "" self.exit_reason: str = ""
# Event initialization # Event initialization
self.events = {} self.events: Dict[str, List[FlexCallback]] = {}
# Klippy Connection Handling # Klippy Connection Handling
self.klippy_address = config.get( self.klippy_address: str = config.get(
'klippy_uds_address', "/tmp/klippy_uds") 'klippy_uds_address', "/tmp/klippy_uds")
self.klippy_connection = KlippyConnection( self.klippy_connection = KlippyConnection(
self.process_command, self.on_connection_closed) self.process_command, self.on_connection_closed)
self.klippy_info = {} self.klippy_info: Dict[str, Any] = {}
self.init_list = [] self.init_list: List[str] = []
self.init_handle = None self.init_handle: Optional[object] = None
self.init_attempts = 0 self.init_attempts: int = 0
self.klippy_state = "disconnected" self.klippy_state: str = "disconnected"
self.klippy_disconnect_evt = None self.klippy_disconnect_evt: Optional[Event] = None
self.subscriptions = {} self.subscriptions: Dict[Subscribable, Dict[str, Any]] = {}
self.failed_components = [] self.failed_components: List[str] = []
# Server/IOLoop # Server/IOLoop
self.server_running = False self.server_running: bool = False
self.moonraker_app = app = MoonrakerApp(config) self.moonraker_app = app = MoonrakerApp(config)
self.register_endpoint = app.register_local_handler self.register_endpoint = app.register_local_handler
self.register_static_file_handler = app.register_static_file_handler self.register_static_file_handler = app.register_static_file_handler
@ -93,9 +118,9 @@ class Server:
# Setup remote methods accessable to Klippy. Note that all # Setup remote methods accessable to Klippy. Note that all
# registered remote methods should be of the notification type, # registered remote methods should be of the notification type,
# they do not return a response to Klippy after execution # they do not return a response to Klippy after execution
self.pending_requests = {} self.pending_requests: Dict[int, BaseRequest] = {}
self.remote_methods = {} self.remote_methods: Dict[str, FlexCallback] = {}
self.klippy_reg_methods = [] self.klippy_reg_methods: List[str] = []
self.register_remote_method( self.register_remote_method(
'process_gcode_response', self._process_gcode_response, 'process_gcode_response', self._process_gcode_response,
need_klippy_reg=False) need_klippy_reg=False)
@ -104,12 +129,12 @@ class Server:
need_klippy_reg=False) need_klippy_reg=False)
# Component initialization # Component initialization
self.components = {} self.components: Dict[str, Any] = {}
self._load_components(config) self._load_components(config)
self.klippy_apis = self.lookup_component('klippy_apis') self.klippy_apis: KlippyAPI = self.lookup_component('klippy_apis')
config.validate_config() config.validate_config()
def start(self): def start(self) -> None:
hostname, hostport = self.get_host_info() hostname, hostport = self.get_host_info()
logging.info( logging.info(
f"Starting Moonraker on ({self.host}, {hostport}), " f"Starting Moonraker on ({self.host}, {hostport}), "
@ -119,19 +144,20 @@ class Server:
self.ioloop.spawn_callback(self._init_signals) self.ioloop.spawn_callback(self._init_signals)
self.ioloop.spawn_callback(self._connect_klippy) self.ioloop.spawn_callback(self._connect_klippy)
def _init_signals(self): def _init_signals(self) -> None:
aioloop = asyncio.get_event_loop() aioloop = asyncio.get_event_loop()
aioloop.add_signal_handler( aioloop.add_signal_handler(
signal.SIGTERM, self._handle_term_signal) signal.SIGTERM, self._handle_term_signal)
def add_log_rollover_item(self, name, item, log=True): def add_log_rollover_item(self, name: str, item: str,
log: bool = True) -> None:
if self.file_logger is not None: if self.file_logger is not None:
self.file_logger.set_rollover_info(name, item) self.file_logger.set_rollover_info(name, item)
if log and item is not None: if log and item is not None:
logging.info(item) logging.info(item)
# ***** Component Management ***** # ***** Component Management *****
def _load_components(self, config): def _load_components(self, config: confighelper.ConfigHelper) -> None:
# load core components # load core components
for component in CORE_COMPONENTS: for component in CORE_COMPONENTS:
self.load_component(config, component) self.load_component(config, component)
@ -142,7 +168,11 @@ class Server:
for section in opt_sections: for section in opt_sections:
self.load_component(config, section, None) self.load_component(config, section, None)
def load_component(self, config, component_name, default=Sentinel): def load_component(self,
config: confighelper.ConfigHelper,
component_name: str,
default: Union[SentinelClass, _T] = SENTINEL
) -> Union[_T, Any]:
if component_name in self.components: if component_name in self.components:
return self.components[component_name] return self.components[component_name]
# Make sure component exists # Make sure component exists
@ -152,7 +182,7 @@ class Server:
msg = f"Component ({component_name}) does not exist" msg = f"Component ({component_name}) does not exist"
logging.info(msg) logging.info(msg)
self.failed_components.append(component_name) self.failed_components.append(component_name)
if default == Sentinel: if isinstance(default, SentinelClass):
raise ServerError(msg) raise ServerError(msg)
return default return default
try: try:
@ -169,32 +199,45 @@ class Server:
msg = f"Unable to load component: ({component_name})" msg = f"Unable to load component: ({component_name})"
logging.exception(msg) logging.exception(msg)
self.failed_components.append(component_name) self.failed_components.append(component_name)
if default == Sentinel: if isinstance(default, SentinelClass):
raise ServerError(msg) raise ServerError(msg)
return default return default
self.components[component_name] = component self.components[component_name] = component
logging.info(f"Component ({component_name}) loaded") logging.info(f"Component ({component_name}) loaded")
return component return component
def lookup_component(self, component_name, default=Sentinel): def lookup_component(self,
component_name: str,
default: Union[SentinelClass, _T] = SENTINEL
) -> Union[_T, Any]:
component = self.components.get(component_name, default) component = self.components.get(component_name, default)
if component == Sentinel: if isinstance(component, SentinelClass):
raise ServerError(f"Component ({component_name}) not found") raise ServerError(f"Component ({component_name}) not found")
return component return component
def register_notification(self, event_name, notify_name=None): def register_notification(self,
event_name: str,
notify_name: Optional[str] = None
) -> None:
wsm = self.moonraker_app.get_websocket_manager() wsm = self.moonraker_app.get_websocket_manager()
wsm.register_notification(event_name, notify_name) wsm.register_notification(event_name, notify_name)
def register_event_handler(self, event, callback): def register_event_handler(self,
event: str,
callback: FlexCallback
) -> None:
self.events.setdefault(event, []).append(callback) self.events.setdefault(event, []).append(callback)
def send_event(self, event, *args): def send_event(self, event: str, *args) -> None:
events = self.events.get(event, []) events = self.events.get(event, [])
for evt in events: for evt in events:
self.ioloop.spawn_callback(evt, *args) self.ioloop.spawn_callback(evt, *args)
def register_remote_method(self, method_name, cb, need_klippy_reg=True): def register_remote_method(self,
method_name: str,
cb: FlexCallback,
need_klippy_reg: bool = True
) -> None:
if method_name in self.remote_methods: if method_name in self.remote_methods:
# XXX - may want to raise an exception here # XXX - may want to raise an exception here
logging.info(f"Remote method ({method_name}) already registered") logging.info(f"Remote method ({method_name}) already registered")
@ -204,28 +247,28 @@ class Server:
# These methods need to be registered with Klippy # These methods need to be registered with Klippy
self.klippy_reg_methods.append(method_name) self.klippy_reg_methods.append(method_name)
def get_host_info(self): def get_host_info(self) -> Tuple[str, int]:
hostname = socket.gethostname() hostname = socket.gethostname()
return hostname, self.port return hostname, self.port
def get_klippy_info(self): def get_klippy_info(self) -> Dict[str, Any]:
return dict(self.klippy_info) return dict(self.klippy_info)
def get_klippy_state(self): def get_klippy_state(self) -> str:
return self.klippy_state return self.klippy_state
# ***** Klippy Connection ***** # ***** Klippy Connection *****
async def _connect_klippy(self): async def _connect_klippy(self) -> None:
if not self.server_running: if not self.server_running:
return return
ret = await self.klippy_connection.connect(self.klippy_address) ret = await self.klippy_connection.connect(self.klippy_address)
if not ret: if not ret:
self.ioloop.call_later(.25, self._connect_klippy) self.ioloop.call_later(.25, self._connect_klippy) # type: ignore
return return
# begin server iniialization # begin server iniialization
self.ioloop.spawn_callback(self._initialize) self.ioloop.spawn_callback(self._initialize)
def process_command(self, cmd): def process_command(self, cmd: Dict[str, Any]) -> None:
method = cmd.get('method', None) method = cmd.get('method', None)
if method is not None: if method is not None:
# This is a remote method called from klippy # This is a remote method called from klippy
@ -253,15 +296,15 @@ class Server:
result = ServerError(err, 400) result = ServerError(err, 400)
request.notify(result) request.notify(result)
async def _execute_method(self, method_name, **kwargs): async def _execute_method(self, method_name: str, **kwargs) -> None:
try: try:
ret = self.remote_methods[method_name](**kwargs) ret = self.remote_methods[method_name](**kwargs)
if asyncio.iscoroutine(ret): if ret is not None:
await ret await ret
except Exception: except Exception:
logging.exception(f"Error running remote method: {method_name}") logging.exception(f"Error running remote method: {method_name}")
def on_connection_closed(self): def on_connection_closed(self) -> None:
self.init_list = [] self.init_list = []
self.klippy_state = "disconnected" self.klippy_state = "disconnected"
for request in self.pending_requests.values(): for request in self.pending_requests.values():
@ -273,11 +316,11 @@ class Server:
if self.init_handle is not None: if self.init_handle is not None:
self.ioloop.remove_timeout(self.init_handle) self.ioloop.remove_timeout(self.init_handle)
if self.server_running: if self.server_running:
self.ioloop.call_later(.25, self._connect_klippy) self.ioloop.call_later(.25, self._connect_klippy) # type: ignore
if self.klippy_disconnect_evt is not None: if self.klippy_disconnect_evt is not None:
self.klippy_disconnect_evt.set() self.klippy_disconnect_evt.set()
async def _initialize(self): async def _initialize(self) -> None:
if not self.server_running: if not self.server_running:
return return
await self._check_ready() await self._check_ready()
@ -311,18 +354,19 @@ class Server:
else: else:
self.init_attempts += 1 self.init_attempts += 1
self.init_handle = self.ioloop.call_later( self.init_handle = self.ioloop.call_later(
INIT_TIME, self._initialize) INIT_TIME, self._initialize) # type: ignore
async def _request_endpoints(self): async def _request_endpoints(self) -> None:
result = await self.klippy_apis.list_endpoints(default=None) result = await self.klippy_apis.list_endpoints(default=None)
if result is None: if result is None:
return return
endpoints = result.get('endpoints', {}) endpoints = result.get('endpoints', [])
for ep in endpoints: for ep in endpoints:
self.moonraker_app.register_remote_handler(ep) self.moonraker_app.register_remote_handler(ep)
async def _check_ready(self): async def _check_ready(self) -> None:
send_id = "identified" not in self.init_list send_id = "identified" not in self.init_list
result: Dict[str, Any]
try: try:
result = await self.klippy_apis.get_klippy_info(send_id) result = await self.klippy_apis.get_klippy_info(send_id)
except ServerError as e: except ServerError as e:
@ -354,7 +398,7 @@ class Server:
msg = result.get('state_message', "Klippy Not Ready") msg = result.get('state_message', "Klippy Not Ready")
logging.info("\n" + msg) logging.info("\n" + msg)
async def _verify_klippy_requirements(self): async def _verify_klippy_requirements(self) -> None:
result = await self.klippy_apis.get_object_list(default=None) result = await self.klippy_apis.get_object_list(default=None)
if result is None: if result is None:
logging.info( logging.info(
@ -370,39 +414,43 @@ class Server:
f"to printer.cfg for full Moonraker functionality.") f"to printer.cfg for full Moonraker functionality.")
if "virtual_sdcard" not in missing_objs: if "virtual_sdcard" not in missing_objs:
# Update the gcode path # Update the gcode path
result = await self.klippy_apis.query_objects( query_res = await self.klippy_apis.query_objects(
{'configfile': None}, default=None) {'configfile': None}, default=None)
if result is None: if query_res is None:
logging.info(f"Unable to set SD Card path") logging.info(f"Unable to set SD Card path")
else: else:
config = result.get('configfile', {}).get('config', {}) config = query_res.get('configfile', {}).get('config', {})
vsd_config = config.get('virtual_sdcard', {}) vsd_config = config.get('virtual_sdcard', {})
vsd_path = vsd_config.get('path', None) vsd_path = vsd_config.get('path', None)
if vsd_path is not None: if vsd_path is not None:
file_manager = self.lookup_component('file_manager') file_manager: FileManager = self.lookup_component(
'file_manager')
file_manager.register_directory('gcodes', vsd_path) file_manager.register_directory('gcodes', vsd_path)
else: else:
logging.info( logging.info(
"Configuration for [virtual_sdcard] not found," "Configuration for [virtual_sdcard] not found,"
" unable to set SD Card path") " unable to set SD Card path")
def _process_gcode_response(self, response): def _process_gcode_response(self, response: str) -> None:
self.send_event("server:gcode_response", response) self.send_event("server:gcode_response", response)
def _process_status_update(self, eventtime, status): def _process_status_update(self,
eventtime: float,
status: Dict[str, Any]
) -> None:
if 'webhooks' in status: if 'webhooks' in status:
# XXX - process other states (startup, ready, error, etc)? # XXX - process other states (startup, ready, error, etc)?
state = status['webhooks'].get('state', None) state: Optional[str] = status['webhooks'].get('state', None)
if state is not None: if state is not None:
if state == "shutdown": if state == "shutdown":
logging.info("Klippy has shutdown") logging.info("Klippy has shutdown")
self.send_event("server:klippy_shutdown") self.send_event("server:klippy_shutdown")
self.klippy_state = state self.klippy_state = state
for conn, sub in self.subscriptions.items(): for conn, sub in self.subscriptions.items():
conn_status = {} conn_status: Dict[str, Any] = {}
for name, fields in sub.items(): for name, fields in sub.items():
if name in status: if name in status:
val = status[name] val: Dict[str, Any] = status[name]
if fields is None: if fields is None:
conn_status[name] = dict(val) conn_status[name] = dict(val)
else: else:
@ -410,18 +458,20 @@ class Server:
k: v for k, v in val.items() if k in fields} k: v for k, v in val.items() if k in fields}
conn.send_status(conn_status) conn.send_status(conn_status)
async def make_request(self, web_request): async def make_request(self, web_request: WebRequest) -> Any:
rpc_method = web_request.get_endpoint() rpc_method = web_request.get_endpoint()
if rpc_method == "objects/subscribe": if rpc_method == "objects/subscribe":
return await self._request_subscripton(web_request) return await self._request_subscripton(web_request)
else: else:
if rpc_method == "gcode/script": if rpc_method == "gcode/script":
script = web_request.get_str('script', "") script = web_request.get_str('script', "")
data_store = self.lookup_component('data_store') data_store: DataStore = self.lookup_component('data_store')
data_store.store_gcode_command(script) data_store.store_gcode_command(script)
return await self._request_standard(web_request) return await self._request_standard(web_request)
async def _request_subscripton(self, web_request): async def _request_subscripton(self,
web_request: WebRequest
) -> Dict[str, Any]:
args = web_request.get_args() args = web_request.get_args()
conn = web_request.get_connection() conn = web_request.get_connection()
@ -432,7 +482,7 @@ class Server:
raise self.error( raise self.error(
"No connection associated with subscription request") "No connection associated with subscription request")
self.subscriptions[conn] = sub self.subscriptions[conn] = sub
all_subs = {} all_subs: Dict[str, Any] = {}
# request superset of all client subscriptions # request superset of all client subscriptions
for sub in self.subscriptions.values(): for sub in self.subscriptions.values():
for obj, items in sub.items(): for obj, items in sub.items():
@ -465,7 +515,7 @@ class Server:
result['status'] = pruned_status result['status'] = pruned_status
return result return result
async def _request_standard(self, web_request): async def _request_standard(self, web_request: WebRequest) -> Any:
rpc_method = web_request.get_endpoint() rpc_method = web_request.get_endpoint()
args = web_request.get_args() args = web_request.get_args()
# Create a base klippy request # Create a base klippy request
@ -475,23 +525,23 @@ class Server:
self.klippy_connection.send_request, base_request) self.klippy_connection.send_request, base_request)
return await base_request.wait() return await base_request.wait()
def remove_subscription(self, conn): def remove_subscription(self, conn: Subscribable) -> None:
self.subscriptions.pop(conn, None) self.subscriptions.pop(conn, None)
def _handle_term_signal(self): def _handle_term_signal(self) -> None:
logging.info(f"Exiting with signal SIGTERM") logging.info(f"Exiting with signal SIGTERM")
self.ioloop.spawn_callback(self._stop_server, "terminate") self.ioloop.spawn_callback(self._stop_server, "terminate")
async def _stop_server(self, exit_reason="restart"): async def _stop_server(self, exit_reason: str = "restart") -> None:
self.server_running = False self.server_running = False
for method in ["on_exit", "close"]: for method in ["on_exit", "close"]:
for name, component in self.components.items(): for name, component in self.components.items():
if not hasattr(component, method): if not hasattr(component, method):
continue continue
func = getattr(component, method) func: FlexCallback = getattr(component, method)
try: try:
ret = func() ret = func()
if asyncio.iscoroutine(ret): if ret is not None:
await ret await ret
except Exception: except Exception:
logging.exception( logging.exception(
@ -517,12 +567,15 @@ class Server:
aioloop.remove_signal_handler(signal.SIGTERM) aioloop.remove_signal_handler(signal.SIGTERM)
self.ioloop.stop() self.ioloop.stop()
async def _handle_server_restart(self, web_request): async def _handle_server_restart(self, web_request: WebRequest) -> str:
self.ioloop.spawn_callback(self._stop_server) self.ioloop.spawn_callback(self._stop_server)
return "ok" return "ok"
async def _handle_info_request(self, web_request): async def _handle_info_request(self,
file_manager = self.lookup_component('file_manager', None) web_request: WebRequest
) -> Dict[str, Any]:
file_manager: Optional[FileManager] = self.lookup_component(
'file_manager', None)
reg_dirs = [] reg_dirs = []
if file_manager is not None: if file_manager is not None:
reg_dirs = file_manager.get_registered_dirs() reg_dirs = file_manager.get_registered_dirs()
@ -535,19 +588,24 @@ class Server:
'failed_plugins': self.failed_components, 'failed_plugins': self.failed_components,
'registered_directories': reg_dirs} 'registered_directories': reg_dirs}
async def _handle_config_request(self, web_request): async def _handle_config_request(self,
web_request: WebRequest
) -> Dict[str, Any]:
return { return {
'config': self.config.get_parsed_config() 'config': self.config.get_parsed_config()
} }
class KlippyConnection: class KlippyConnection:
def __init__(self, on_recd, on_close): def __init__(self,
on_recd: Callable[[dict], None],
on_close: Callable[[], None]
) -> None:
self.ioloop = IOLoop.current() self.ioloop = IOLoop.current()
self.iostream = None self.iostream: Optional[iostream.IOStream] = None
self.on_recd = on_recd self.on_recd = on_recd
self.on_close = on_close self.on_close = on_close
async def connect(self, address): async def connect(self, address: str) -> bool:
ksock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) ksock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
kstream = iostream.IOStream(ksock) kstream = iostream.IOStream(ksock)
try: try:
@ -560,7 +618,7 @@ class KlippyConnection:
self.ioloop.spawn_callback(self._read_stream, self.iostream) self.ioloop.spawn_callback(self._read_stream, self.iostream)
return True return True
async def _read_stream(self, stream): async def _read_stream(self, stream: iostream.IOStream) -> None:
while not stream.closed(): while not stream.closed():
try: try:
data = await stream.read_until(b'\x03') data = await stream.read_until(b'\x03')
@ -576,7 +634,7 @@ class KlippyConnection:
logging.exception( logging.exception(
f"Error processing Klippy Host Response: {data.decode()}") f"Error processing Klippy Host Response: {data.decode()}")
async def send_request(self, request): async def send_request(self, request: BaseRequest) -> None:
if self.iostream is None: if self.iostream is None:
request.notify(ServerError("Klippy Host not connected", 503)) request.notify(ServerError("Klippy Host not connected", 503))
return return
@ -586,24 +644,24 @@ class KlippyConnection:
except iostream.StreamClosedError: except iostream.StreamClosedError:
request.notify(ServerError("Klippy Host not connected", 503)) request.notify(ServerError("Klippy Host not connected", 503))
def is_connected(self): def is_connected(self) -> bool:
return self.iostream is not None and not self.iostream.closed() return self.iostream is not None and not self.iostream.closed()
def close(self): def close(self) -> None:
if self.iostream is not None and \ if self.iostream is not None and \
not self.iostream.closed(): not self.iostream.closed():
self.iostream.close() self.iostream.close()
# Basic WebRequest class, easily converted to dict for json encoding # Basic WebRequest class, easily converted to dict for json encoding
class BaseRequest: class BaseRequest:
def __init__(self, rpc_method, params): def __init__(self, rpc_method: str, params: Dict[str, Any]) -> None:
self.id = id(self) self.id = id(self)
self.rpc_method = rpc_method self.rpc_method = rpc_method
self.params = params self.params = params
self._event = Event() self._event = Event()
self.response = None self.response: Any = None
async def wait(self): async def wait(self) -> Any:
# Log pending requests every 60 seconds # Log pending requests every 60 seconds
start_time = time.time() start_time = time.time()
while True: while True:
@ -622,15 +680,15 @@ class BaseRequest:
raise self.response raise self.response
return self.response return self.response
def notify(self, response): def notify(self, response: Any) -> None:
self.response = response self.response = response
self._event.set() self._event.set()
def to_dict(self): def to_dict(self) -> Dict[str, Any]:
return {'id': self.id, 'method': self.rpc_method, return {'id': self.id, 'method': self.rpc_method,
'params': self.params} 'params': self.params}
def main(): def main() -> None:
# Parse start arguments # Parse start arguments
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="Moonraker - Klipper API Server") description="Moonraker - Klipper API Server")