job_state: combine events into a single handler

Emit a single event where the first argument contains
a "JobEvent" enumeration that describes the particular
event.  This reduces the number of callbacks registered
by JobState consumers and allows them react to multiple
state changes in the same callback.

The individual events remain for compatibility, however
they are deprecated.  Current modules should be updated
to use the "job_state:state_changed" event and new modules
must use this event.

Signed-off-by:  Eric Callahan <arksine.code@gmail.com>
This commit is contained in:
Eric Callahan 2023-11-03 06:35:13 -04:00
parent 7deb9fac4c
commit 6e8b720d17
No known key found for this signature in database
GPG Key ID: 5A1EB336DFB4C71B
7 changed files with 110 additions and 158 deletions

View File

@ -9,7 +9,7 @@ import sys
import ipaddress import ipaddress
import logging import logging
import copy import copy
from enum import Flag, auto from enum import Enum, Flag, auto
from .utils import ServerError, Sentinel from .utils import ServerError, Sentinel
from .utils import json_wrapper as jsonw from .utils import json_wrapper as jsonw
@ -90,6 +90,39 @@ class TransportType(ExtendedFlag):
MQTT = auto() MQTT = auto()
INTERNAL = auto() INTERNAL = auto()
class ExtendedEnum(Enum):
@classmethod
def from_string(cls, enum_name: str):
str_name = enum_name.upper()
for name, member in cls.__members__.items():
if name == str_name:
return cls(member.value)
raise ValueError(f"No enum member named {enum_name}")
def __str__(self) -> str:
return self._name_.lower() # type: ignore
class JobEvent(ExtendedEnum):
STANDBY = 1
STARTED = 2
PAUSED = 3
RESUMED = 4
COMPLETE = 5
ERROR = 6
CANCELLED = 7
@property
def finished(self) -> bool:
return self.value >= 5
@property
def aborted(self) -> bool:
return self.value >= 6
@property
def is_printing(self) -> bool:
return self.value in [2, 4]
class Subscribable: class Subscribable:
def send_status( def send_status(
self, status: Dict[str, Any], eventtime: float self, status: Dict[str, Any], eventtime: float

View File

@ -6,6 +6,7 @@ from __future__ import annotations
import time import time
import logging import logging
from asyncio import Lock from asyncio import Lock
from ..common import JobEvent
# Annotation imports # Annotation imports
from typing import ( from typing import (
@ -49,15 +50,7 @@ class History:
self.server.register_event_handler( self.server.register_event_handler(
"server:klippy_shutdown", self._handle_shutdown) "server:klippy_shutdown", self._handle_shutdown)
self.server.register_event_handler( self.server.register_event_handler(
"job_state:started", self._on_job_started) "job_state:state_changed", self._on_job_state_changed)
self.server.register_event_handler(
"job_state:complete", self._on_job_complete)
self.server.register_event_handler(
"job_state:cancelled", self._on_job_cancelled)
self.server.register_event_handler(
"job_state:standby", self._on_job_standby)
self.server.register_event_handler(
"job_state:error", self._on_job_error)
self.server.register_notification("history:history_changed") self.server.register_notification("history:history_changed")
self.server.register_endpoint( self.server.register_endpoint(
@ -192,38 +185,23 @@ class History:
"moonraker", "history.job_totals", self.job_totals) "moonraker", "history.job_totals", self.job_totals)
return {'last_totals': last_totals} return {'last_totals': last_totals}
def _on_job_started(self, def _on_job_state_changed(
self,
job_event: JobEvent,
prev_stats: Dict[str, Any], prev_stats: Dict[str, Any],
new_stats: Dict[str, Any] new_stats: Dict[str, Any]
) -> None: ) -> None:
if job_event == JobEvent.STARTED:
if self.current_job is not None: if self.current_job is not None:
# Finish with the previous state # Finish with the previous state
self.finish_job("cancelled", prev_stats) self.finish_job("cancelled", prev_stats)
self.add_job(PrinterJob(new_stats)) self.add_job(PrinterJob(new_stats))
elif job_event == JobEvent.COMPLETE:
def _on_job_complete(self,
prev_stats: Dict[str, Any],
new_stats: Dict[str, Any]
) -> None:
self.finish_job("completed", new_stats) self.finish_job("completed", new_stats)
elif job_event == JobEvent.ERROR:
def _on_job_cancelled(self,
prev_stats: Dict[str, Any],
new_stats: Dict[str, Any]
) -> None:
self.finish_job("cancelled", new_stats)
def _on_job_error(self,
prev_stats: Dict[str, Any],
new_stats: Dict[str, Any]
) -> None:
self.finish_job("error", new_stats) self.finish_job("error", new_stats)
elif job_event in (JobEvent.CANCELLED, JobEvent.STANDBY):
def _on_job_standby(self, # Cancel on "standby" for backward compatibility with
prev_stats: Dict[str, Any],
new_stats: Dict[str, Any]
) -> None:
# Backward compatibility with
# `CLEAR_PAUSE/SDCARD_RESET_FILE` workflow # `CLEAR_PAUSE/SDCARD_RESET_FILE` workflow
self.finish_job("cancelled", prev_stats) self.finish_job("cancelled", prev_stats)

View File

@ -8,6 +8,7 @@ from __future__ import annotations
import asyncio import asyncio
import time import time
import logging import logging
from ..common import JobEvent
# Annotation imports # Annotation imports
from typing import ( from typing import (
@ -46,11 +47,8 @@ class JobQueue:
self.server.register_event_handler( self.server.register_event_handler(
"server:klippy_shutdown", self._handle_shutdown) "server:klippy_shutdown", self._handle_shutdown)
self.server.register_event_handler( self.server.register_event_handler(
"job_state:complete", self._on_job_complete) "job_state:state_changed", self._on_job_state_changed
self.server.register_event_handler( )
"job_state:error", self._on_job_abort)
self.server.register_event_handler(
"job_state:cancelled", self._on_job_abort)
self.server.register_notification("job_queue:job_queue_changed") self.server.register_notification("job_queue:job_queue_changed")
self.server.register_remote_method("pause_job_queue", self.pause_queue) self.server.register_remote_method("pause_job_queue", self.pause_queue)
@ -85,10 +83,13 @@ class JobQueue:
if not self.queued_jobs and self.automatic: if not self.queued_jobs and self.automatic:
self._set_queue_state("ready") self._set_queue_state("ready")
async def _on_job_complete(self, async def _on_job_state_changed(self, job_event: JobEvent, *args) -> None:
prev_stats: Dict[str, Any], if job_event == JobEvent.COMPLETE:
new_stats: Dict[str, Any] await self._on_job_complete()
) -> None: elif job_event.aborted:
await self._on_job_abort()
async def _on_job_complete(self) -> None:
if not self.automatic: if not self.automatic:
return return
async with self.lock: async with self.lock:
@ -99,10 +100,7 @@ class JobQueue:
self.pop_queue_handle = event_loop.delay_callback( self.pop_queue_handle = event_loop.delay_callback(
self.job_delay, self._pop_job) self.job_delay, self._pop_job)
async def _on_job_abort(self, async def _on_job_abort(self) -> None:
prev_stats: Dict[str, Any],
new_stats: Dict[str, Any]
) -> None:
async with self.lock: async with self.lock:
if self.queued_jobs: if self.queued_jobs:
self._set_queue_state("paused") self._set_queue_state("paused")

View File

@ -15,6 +15,7 @@ from typing import (
Dict, Dict,
List, List,
) )
from ..common import JobEvent
if TYPE_CHECKING: if TYPE_CHECKING:
from ..confighelper import ConfigHelper from ..confighelper import ConfigHelper
from .klippy_apis import KlippyAPI from .klippy_apis import KlippyAPI
@ -65,8 +66,16 @@ class JobState:
f"Job State Changed - Prev State: {old_state}, " f"Job State Changed - Prev State: {old_state}, "
f"New State: {new_state}" f"New State: {new_state}"
) )
# NOTE: Individual job_state events are DEPRECATED. New modules
# should register handlers for "job_state: status_changed" and
# match against the JobEvent object provided.
self.server.send_event(f"job_state:{new_state}", prev_ps, new_ps)
self.server.send_event( self.server.send_event(
f"job_state:{new_state}", prev_ps, new_ps) "job_state:state_changed",
JobEvent.from_string(new_state),
prev_ps,
new_ps
)
if "info" in ps: if "info" in ps:
cur_layer: Optional[int] = ps["info"].get("current_layer") cur_layer: Optional[int] = ps["info"].get("current_layer")
if cur_layer is not None: if cur_layer is not None:

View File

@ -10,6 +10,7 @@ import apprise
import logging import logging
import pathlib import pathlib
import re import re
from ..common import JobEvent
# Annotation imports # Annotation imports
from typing import ( from typing import (
@ -29,23 +30,20 @@ class Notifier:
def __init__(self, config: ConfigHelper) -> None: def __init__(self, config: ConfigHelper) -> None:
self.server = config.get_server() self.server = config.get_server()
self.notifiers: Dict[str, NotifierInstance] = {} self.notifiers: Dict[str, NotifierInstance] = {}
self.events: Dict[str, NotifierEvent] = {} self.events: Dict[str, List[NotifierInstance]] = {}
prefix_sections = config.get_prefix_sections("notifier") prefix_sections = config.get_prefix_sections("notifier")
self.register_events(config)
self.register_remote_actions() self.register_remote_actions()
for section in prefix_sections: for section in prefix_sections:
cfg = config[section] cfg = config[section]
try: try:
notifier = NotifierInstance(cfg) notifier = NotifierInstance(cfg)
for job_event in list(JobEvent):
for event in self.events: if job_event == JobEvent.STANDBY:
if event in notifier.events or "*" in notifier.events: continue
self.events[event].register_notifier(notifier) evt_name = str(job_event)
if "*" in notifier.events or evt_name in notifier.events:
self.events.setdefault(evt_name, []).append(notifier)
logging.info(f"Registered notifier: '{notifier.get_name()}'") logging.info(f"Registered notifier: '{notifier.get_name()}'")
except Exception as e: except Exception as e:
msg = f"Failed to load notifier[{cfg.get_name()}]\n{e}" msg = f"Failed to load notifier[{cfg.get_name()}]\n{e}"
self.server.add_warning(msg) self.server.add_warning(msg)
@ -53,6 +51,9 @@ class Notifier:
self.notifiers[notifier.get_name()] = notifier self.notifiers[notifier.get_name()] = notifier
self.register_endpoints(config) self.register_endpoints(config)
self.server.register_event_handler(
"job_state:state_changed", self._on_job_state_changed
)
def register_remote_actions(self): def register_remote_actions(self):
self.server.register_remote_method("notify", self.notify_action) self.server.register_remote_method("notify", self.notify_action)
@ -61,40 +62,17 @@ class Notifier:
if name not in self.notifiers: if name not in self.notifiers:
raise self.server.error(f"Notifier '{name}' not found", 404) raise self.server.error(f"Notifier '{name}' not found", 404)
notifier = self.notifiers[name] notifier = self.notifiers[name]
await notifier.notify("remote_action", [], message) await notifier.notify("remote_action", [], message)
def register_events(self, config: ConfigHelper): async def _on_job_state_changed(
self,
self.events["started"] = NotifierEvent( job_event: JobEvent,
"started", prev_stats: Dict[str, Any],
"job_state:started", new_stats: Dict[str, Any]
config) ) -> None:
evt_name = str(job_event)
self.events["complete"] = NotifierEvent( for notifier in self.events.get(evt_name, []):
"complete", await notifier.notify(evt_name, [prev_stats, new_stats])
"job_state:complete",
config)
self.events["error"] = NotifierEvent(
"error",
"job_state:error",
config)
self.events["cancelled"] = NotifierEvent(
"cancelled",
"job_state:cancelled",
config)
self.events["paused"] = NotifierEvent(
"paused",
"job_state:paused",
config)
self.events["resumed"] = NotifierEvent(
"resumed",
"job_state:resumed",
config)
def register_endpoints(self, config: ConfigHelper): def register_endpoints(self, config: ConfigHelper):
self.server.register_endpoint( self.server.register_endpoint(
@ -134,34 +112,6 @@ class Notifier:
"stats": print_stats "stats": print_stats
} }
class NotifierEvent:
def __init__(self, identifier: str, event_name: str, config: ConfigHelper):
self.identifier = identifier
self.event_name = event_name
self.server = config.get_server()
self.notifiers: Dict[str, NotifierInstance] = {}
self.config = config
self.server.register_event_handler(self.event_name, self._handle)
def register_notifier(self, notifier: NotifierInstance):
self.notifiers[notifier.get_name()] = notifier
async def _handle(self, *args) -> None:
logging.info(f"'{self.identifier}' notifier event triggered'")
await self.invoke_notifiers(args)
async def invoke_notifiers(self, args):
for notifier_name in self.notifiers:
try:
notifier = self.notifiers[notifier_name]
await notifier.notify(self.identifier, args)
except Exception as e:
logging.info(f"Failed to notify [{notifier_name}]\n{e}")
continue
class NotifierInstance: class NotifierInstance:
def __init__(self, config: ConfigHelper) -> None: def __init__(self, config: ConfigHelper) -> None:
self.config = config self.config = config

View File

@ -17,7 +17,7 @@ import logging.handlers
import tempfile import tempfile
from queue import SimpleQueue from queue import SimpleQueue
from ..loghelper import LocalQueueHandler from ..loghelper import LocalQueueHandler
from ..common import Subscribable, WebRequest from ..common import Subscribable, WebRequest, JobEvent
from ..utils import json_wrapper as jsonw from ..utils import json_wrapper as jsonw
from typing import ( from typing import (
@ -28,6 +28,7 @@ from typing import (
List, List,
Union, Union,
Any, Any,
Callable,
) )
if TYPE_CHECKING: if TYPE_CHECKING:
from ..app import InternalTransport from ..app import InternalTransport
@ -157,19 +158,7 @@ class SimplyPrint(Subscribable):
self.server.register_event_handler( self.server.register_event_handler(
"server:klippy_disconnect", self._on_klippy_disconnected) "server:klippy_disconnect", self._on_klippy_disconnected)
self.server.register_event_handler( self.server.register_event_handler(
"job_state:started", self._on_print_start) "job_state:state_changed", self._on_job_state_changed)
self.server.register_event_handler(
"job_state:paused", self._on_print_paused)
self.server.register_event_handler(
"job_state:resumed", self._on_print_resumed)
self.server.register_event_handler(
"job_state:standby", self._on_print_standby)
self.server.register_event_handler(
"job_state:complete", self._on_print_complete)
self.server.register_event_handler(
"job_state:error", self._on_print_error)
self.server.register_event_handler(
"job_state:cancelled", self._on_print_cancelled)
self.server.register_event_handler( self.server.register_event_handler(
"klippy_apis:pause_requested", self._on_pause_requested) "klippy_apis:pause_requested", self._on_pause_requested)
self.server.register_event_handler( self.server.register_event_handler(
@ -542,7 +531,7 @@ class SimplyPrint(Subscribable):
async def _on_klippy_ready(self) -> None: async def _on_klippy_ready(self) -> None:
last_stats: Dict[str, Any] = self.job_state.get_last_stats() last_stats: Dict[str, Any] = self.job_state.get_last_stats()
if last_stats["state"] == "printing": if last_stats["state"] == "printing":
self._on_print_start(last_stats, last_stats, False) self._on_print_started(last_stats, last_stats, False)
else: else:
self._update_state("operational") self._update_state("operational")
query: Optional[Dict[str, Any]] query: Optional[Dict[str, Any]]
@ -674,7 +663,14 @@ class SimplyPrint(Subscribable):
self.cache.reset_print_state() self.cache.reset_print_state()
self.printer_status = {} self.printer_status = {}
def _on_print_start( def _on_job_state_changed(self, job_event: JobEvent, *args) -> None:
callback: Optional[Callable] = getattr(self, f"_on_print_{job_event}", None)
if callback is not None:
callback(*args)
else:
logging.info(f"No defined callback for Job Event: {job_event}")
def _on_print_started(
self, self,
prev_stats: Dict[str, Any], prev_stats: Dict[str, Any],
new_stats: Dict[str, Any], new_stats: Dict[str, Any],

View File

@ -9,7 +9,7 @@ import os
import sys import sys
import copy import copy
import pathlib import pathlib
from enum import Enum from ...common import ExtendedEnum
from ...utils import source_info from ...utils import source_info
from typing import ( from typing import (
TYPE_CHECKING, TYPE_CHECKING,
@ -46,25 +46,13 @@ BASE_CONFIG: Dict[str, Dict[str, str]] = {
} }
} }
class ExtEnum(Enum): class AppType(ExtendedEnum):
@classmethod
def from_string(cls, enum_name: str):
str_name = enum_name.upper()
for name, member in cls.__members__.items():
if name == str_name:
return cls(member.value)
raise ValueError(f"No enum member named {enum_name}")
def __str__(self) -> str:
return self._name_.lower() # type: ignore
class AppType(ExtEnum):
NONE = 1 NONE = 1
WEB = 2 WEB = 2
GIT_REPO = 3 GIT_REPO = 3
ZIP = 4 ZIP = 4
class Channel(ExtEnum): class Channel(ExtendedEnum):
STABLE = 1 STABLE = 1
BETA = 2 BETA = 2
DEV = 3 DEV = 3