Skip to content

Commit

Permalink
Add an UpdateHandler
Browse files Browse the repository at this point in the history
Run the ``poll()``-method in a while-loop or similar for real-time
changes.
  • Loading branch information
hmpf committed Oct 17, 2023
1 parent b99d9ac commit 74bc6ca
Show file tree
Hide file tree
Showing 3 changed files with 201 additions and 6 deletions.
13 changes: 13 additions & 0 deletions src/zinolib/controllers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class ManagerException(Exception):
def __init__(self, session=None):
self.session = session
self.events = {}
self.removed_ids = set()

def _get_event(self, event_or_id: EventOrId) -> Event:
if isinstance(event_or_id, Event):
Expand All @@ -29,9 +30,21 @@ def _get_event(self, event_or_id: EventOrId) -> Event:
return self.events[event_or_id]
raise ValueError("Unknown type")

def _get_event_id(self, event_or_id: EventOrId) -> int:
if isinstance(event_or_id, int):
return event_or_id
if isinstance(event_or_id, Event):
return event_or_id.id
raise ValueError("Unknown type")

Check warning on line 38 in src/zinolib/controllers/base.py

View check run for this annotation

Codecov / codecov/patch

src/zinolib/controllers/base.py#L36-L38

Added lines #L36 - L38 were not covered by tests

def _set_event(self, event: Event):
self.events[event.id] = event

def remove_event(self, event_or_id: EventOrId):
event_id = self._get_event_id(event_or_id)
self.events.pop(event_id)
self.removed_ids.add(event_id)

def check_session(self):
if not self.session:
raise ValueError # raise correct error
Expand Down
86 changes: 84 additions & 2 deletions src/zinolib/controllers/zino1.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
> from zinolib.ritz import parse_tcl_config
> from zinolib.zino1 import Zino1EventManager
> conf = parse_tcl_config("~/.ritz.tcl")['default']
> event_manager = Zino1EventManager().configure(conf)
> event_manager = Zino1EventManager.configure(conf)
> event_manager.connect()
To get a list of currently available events::
Expand All @@ -35,6 +35,18 @@
This is a dictionary of event_id, event object pairs.
To get a set of removed event ids::
> event_manager.removed_ids
For updates, either regularly use ``get_events()`` or utilize the UpdateHandler::
> updater = UpdateHandler(event_manager)
> updated = updater.poll()
This updates ``event_manager.events`` and ``event_manager.removed_ids`` and
returns ``True`` on any change, falsey otherwise.
To get history for a specific event::
> history_list = event_manager.get_history_for_id(INT)
Expand All @@ -59,8 +71,10 @@

from datetime import datetime, timezone
from typing import Iterable, List, TypedDict, Optional, Set
import logging

from .base import EventManager
from ..compat import StrEnum
from ..event_types import EventType, Event, HistoryEntry, LogEntry, AdmState
from ..ritz import ProtocolError, ritz, notifier

Expand All @@ -78,12 +92,75 @@


DEFAULT_TIMEOUT = 30
LOG = logging.getLogger(__name__)


def convert_timestamp(timestamp: int) -> datetime:
return datetime.fromtimestamp(timestamp, timezone.utc)


class UpdateHandler:
class UpdateType(StrEnum):
STATE = "state"
ATTR = "attr"
HISTORY = "history"
LOG = "log"
SCAVENGED = "scavenged"

def __init__(self, manager, autoremove=False):
self.manager = manager
self.events = manager.events
self.autoremove = autoremove

def poll(self):
update = self.manager.session.push.poll()
if not update:
return False
return self.handle(update)

Check warning on line 119 in src/zinolib/controllers/zino1.py

View check run for this annotation

Codecov / codecov/patch

src/zinolib/controllers/zino1.py#L116-L119

Added lines #L116 - L119 were not covered by tests

def update(self, event_id: int):
event = self.manager.get_updated_event_for_id(event_id)
self.manager._set_event(event)
LOG.debug("Updated event #%i", event_id)

def remove(self, event_id: int):
self.manager.remove_event(event_id)
LOG.debug("Removed event #%i", event_id)

def handle(self, update):
if update.id not in self.events and update.type != self.UpdateType.STATE:
# new event that still don't have a state
return None
if update.type in tuple(self.UpdateType):
method = getattr(self, f"cmd_{update.type}")
return method(update)
return self.fallback(update)

def cmd_state(self, update):
states = update.info.split(" ")
if states[1] == "closed" and self.autoremove:
LOG.debug('Autoremoving "%s"', update.id)
self.remove(update.id)
else:
self.update(update.id)
return True

def cmd_attr(self, update):
self.update(update.id)
return True

cmd_history = cmd_attr
cmd_log = cmd_attr

def cmd_scavenged(self, update):
self.remove(update.id)
return True

def fallback(self, update):
LOG.warning('Unknown update type: "%s" for id %s' % (update.type, update.id))
return False


class SessionAdapter:

class _Session:
Expand Down Expand Up @@ -137,6 +214,7 @@ def connect_session(session):
def close_session(session):
session.push._sock.close()
session.request.close()
return None

Check warning on line 217 in src/zinolib/controllers/zino1.py

View check run for this annotation

Codecov / codecov/patch

src/zinolib/controllers/zino1.py#L215-L217

Added lines #L215 - L217 were not covered by tests


class EventAdapter:
Expand Down Expand Up @@ -323,12 +401,16 @@ def connect(self):
self.check_session()
self.session = self._session_adapter.connect_session(self.session)

Check warning on line 402 in src/zinolib/controllers/zino1.py

View check run for this annotation

Codecov / codecov/patch

src/zinolib/controllers/zino1.py#L401-L402

Added lines #L401 - L402 were not covered by tests

def disconnect(self):
self.check_session()
self.session = self._session_adapter.close_session(self.session)

Check warning on line 406 in src/zinolib/controllers/zino1.py

View check run for this annotation

Codecov / codecov/patch

src/zinolib/controllers/zino1.py#L405-L406

Added lines #L405 - L406 were not covered by tests

def clear_flapping(self, event: EventType):
"""Clear flapping state of a PortStateEvent
Usage:
c = ritz_session.case(123)
c.clear_clapping()
c.clear_flapping()
"""
if event.type == Event.Type.PortState:
return self.session.request.clear_flapping(event.router, event.ifindex)

Check warning on line 416 in src/zinolib/controllers/zino1.py

View check run for this annotation

Codecov / codecov/patch

src/zinolib/controllers/zino1.py#L416

Added line #L416 was not covered by tests
Expand Down
108 changes: 104 additions & 4 deletions tests/test_zinolib_controllers_zino1.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
from datetime import datetime, timedelta, timezone

from zinolib.event_types import AdmState, Event, HistoryEntry, LogEntry
from zinolib.controllers.zino1 import EventAdapter, HistoryAdapter, LogAdapter, SessionAdapter, Zino1EventManager
from zinolib.controllers.zino1 import EventAdapter, HistoryAdapter, LogAdapter, SessionAdapter, Zino1EventManager, UpdateHandler
from zinolib.ritz import NotifierResponse

raw_event_id = 139110
raw_attrlist = [
Expand Down Expand Up @@ -42,7 +43,7 @@
class FakeEventAdapter:
@staticmethod
def get_attrlist(request, event_id: int):
return raw_attrlist
return raw_attrlist.copy()

@classmethod
def attrlist_to_attrdict(cls, attrlist):
Expand All @@ -60,13 +61,13 @@ def get_event_ids(request):
class FakeHistoryAdapter(HistoryAdapter):
@staticmethod
def get_history(request, event_id: int):
return raw_history
return raw_history.copy()


class FakeLogAdapter(LogAdapter):
@staticmethod
def get_log(request, event_id: int):
return raw_log
return raw_log.copy()


class FakeSessionAdapter(SessionAdapter):
Expand Down Expand Up @@ -144,3 +145,102 @@ def test_get_log_for_id(self):
log='some other log message')
]
self.assertEqual(log_list, expected_log_list)


class UpdateHandlerTest(unittest.TestCase):

def init_manager(self):
zino1 = FakeZino1EventManager.configure(None)
return zino1

def test_cmd_scavenged(self):
zino1 = self.init_manager()
zino1.get_events()
self.assertIn(raw_event_id, zino1.events)
self.assertNotIn(raw_event_id, zino1.removed_ids)
updates = UpdateHandler(zino1)
update = NotifierResponse(raw_event_id, "","")
ok = updates.cmd_scavenged(update)
self.assertTrue(ok)
self.assertNotIn(raw_event_id, zino1.events)
self.assertIn(raw_event_id, zino1.removed_ids)

def test_cmd_attr(self):
zino1 = self.init_manager()
zino1.get_events()
old_events = zino1.events.copy()
old_events[raw_event_id].priority = 500
updates = UpdateHandler(zino1)
update = NotifierResponse(raw_event_id, "","")
ok = updates.cmd_attr(update)
self.assertTrue(ok)
self.assertNotEqual(zino1.events[raw_event_id].priority, old_events[raw_event_id].priority)

def test_cmd_state_is_closed_and_autoremove_is_on(self):
zino1 = self.init_manager()
zino1.get_events()
self.assertNotIn(raw_event_id, zino1.removed_ids)
self.assertIn(raw_event_id, zino1.events)
updates = UpdateHandler(zino1, autoremove=True)
update = NotifierResponse(raw_event_id, "", "X closed")
ok = updates.cmd_state(update)
self.assertTrue(ok)
self.assertIn(raw_event_id, zino1.removed_ids)
self.assertNotIn(raw_event_id, zino1.events)

def test_cmd_state_is_closed_and_autoremove_is_off(self):
zino1 = self.init_manager()
zino1.get_events()
old_events = zino1.events.copy()
old_events[raw_event_id].priority = 500
updates = UpdateHandler(zino1, autoremove=False)
update = NotifierResponse(raw_event_id, "","X closed")
ok = updates.cmd_state(update)
self.assertTrue(ok)
self.assertNotEqual(zino1.events[raw_event_id].priority, old_events[raw_event_id].priority)

def test_cmd_state_is_not_closed(self):
zino1 = self.init_manager()
zino1.get_events()
old_events = zino1.events.copy()
old_events[raw_event_id].priority = 500
updates = UpdateHandler(zino1, autoremove=False)
update = NotifierResponse(raw_event_id, "","x butterfly")
ok = updates.cmd_state(update)
self.assertTrue(ok)
self.assertNotEqual(zino1.events[raw_event_id].priority, old_events[raw_event_id].priority)

def test_fallback(self):
zino1 = self.init_manager()
updates = UpdateHandler(zino1)
update = NotifierResponse(raw_event_id, "", "")
with self.assertLogs('zinolib.controllers.zino1', level='WARNING') as cm:
self.assertFalse(updates.fallback(update))
self.assertEqual(cm.output, ['WARNING:zinolib.controllers.zino1:Unknown update type: "" for id 139110'])

def test_handle_new_stateless_event_is_very_special(self):
zino1 = self.init_manager()
updates = UpdateHandler(zino1)
update = NotifierResponse(1337, "", "")
result = updates.handle(update)
self.assertEqual(result, None)

def test_handle_known_type(self):
zino1 = self.init_manager()
zino1.get_events()
old_events = zino1.events.copy()
old_events[raw_event_id].priority = 500
updates = UpdateHandler(zino1)
update = NotifierResponse(raw_event_id, updates.UpdateType.LOG, "")
ok = updates.handle(update) # will refetch events
self.assertTrue(ok)
self.assertNotEqual(zino1.events[raw_event_id].priority, old_events[raw_event_id].priority)

def test_handle_unknown_type(self):
zino1 = self.init_manager()
zino1.get_events()
updates = UpdateHandler(zino1)
update = NotifierResponse(raw_event_id, "trout", "")
with self.assertLogs('zinolib.controllers.zino1', level='WARNING'):
ok = updates.handle(update) # will run fallback
self.assertFalse(ok)

0 comments on commit 74bc6ca

Please sign in to comment.