Skip to content

Commit d798873

Browse files
authored
Merge pull request #826 from fkie-cad/dev-logevent-class-implementation
Dev logevent class implementation - implement LogEvent class as subclass of Event with support for original, extra_data, and metadata attributes - ensure consistent state behavior by helper-based wrapping of next_state() to enforce transition validation
2 parents 383a038 + 4c97777 commit d798873

File tree

4 files changed

+214
-0
lines changed

4 files changed

+214
-0
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
### Features
77

88
### Improvements
9+
10+
* add LogEvent class
911
* implement abstract Event class to encapsulate event data, processing state, warnings, and errors
1012
* integrate dotted field handling methods directly into Event, enabling structured field access and manipulation
1113
* support event identity and hashability based on data, allowing usage in sets and as dictionary keys

logprep/ng/events/__init__.py

Whitespace-only changes.

logprep/ng/events/log_event.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
"""Concrete Log Event implementation"""
2+
3+
from types import MethodType
4+
from typing import Any
5+
6+
from logprep.ng.abc.event import Event, EventMetadata
7+
from logprep.ng.event_state import EventState, EventStateType
8+
9+
10+
class LogEvent(Event):
11+
"""Concrete log event class with additional attribute and constraints."""
12+
13+
__slots__: tuple[str, ...] = (
14+
"original",
15+
"extra_data",
16+
"metadata",
17+
"_state",
18+
"_origin_state_next_state_fn",
19+
)
20+
21+
def __init__( # pylint: disable=too-many-arguments
22+
self,
23+
data: dict[str, Any],
24+
*,
25+
original: bytes,
26+
extra_data: list[Event] | None = None,
27+
metadata: EventMetadata | None = None,
28+
state: EventState | None = None,
29+
) -> None:
30+
"""
31+
Parameters
32+
----------
33+
data : dict[str, Any]
34+
The main data payload for the event.
35+
original : bytes
36+
The raw representation of the event (e.g., as received from Kafka).
37+
extra_data : list[Event], optional
38+
Sub-events that were derived or caused by this event
39+
metadata : EventMetadata, optional
40+
Structured metadata for the event.
41+
state : EventState, optional
42+
Optional existing EventState to initialize from (e.g. deserialization).
43+
44+
Examples
45+
--------
46+
>>> e1 = Event({"msg": "hello"})
47+
>>> e2 = Event({"msg": "world"})
48+
>>> le = LogEvent({"msg": "parent"}, original=b'raw', extra_data=[e1, e2])
49+
>>> isinstance(le.state, EventState)
50+
True
51+
"""
52+
53+
self.original = original
54+
self.extra_data = extra_data if extra_data else []
55+
self.metadata = metadata
56+
57+
self._state: EventState = EventState() if state is None else state
58+
59+
super().__init__(data=data, state=self._state)
60+
61+
# Wrap original next_state with validation logic
62+
self._origin_state_next_state_fn = MethodType(EventState.next_state, self._state)
63+
self._state.next_state = self._next_state_validation_helper
64+
65+
@property
66+
def state(self) -> EventState:
67+
"""Return the current EventState instance."""
68+
69+
return self._state
70+
71+
@state.setter
72+
def state(self, value: EventState) -> None:
73+
"""
74+
Prevent assignment of a DELIVERED state if not all extra_data events are DELIVERED.
75+
76+
Parameters
77+
----------
78+
value : EventState
79+
The state to assign.
80+
81+
Raises
82+
------
83+
ValueError
84+
If value.state is DELIVERED but any sub-event is not DELIVERED.
85+
"""
86+
self._validate_state(value.current_state)
87+
self._state = value
88+
89+
# Wrap next_state again if needed
90+
self._origin_state_next_state_fn = self._state.next_state
91+
self._state.next_state = self._next_state_validation_helper
92+
93+
def _next_state_validation_helper(
94+
self, *, success: bool | None = None
95+
) -> EventStateType | None:
96+
new_state: EventStateType = self._origin_state_next_state_fn(success=success)
97+
self._validate_state(new_state)
98+
return new_state
99+
100+
def _validate_state(self, state_type: EventStateType) -> None:
101+
if state_type == EventStateType.DELIVERED:
102+
if not all(e.state.current_state == EventStateType.DELIVERED for e in self.extra_data):
103+
raise ValueError(
104+
"Cannot assign DELIVERED state: not all extra_data events are DELIVERED."
105+
)

tests/unit/ng/test_log_event.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# pylint: disable=missing-docstring
2+
# pylint: disable=protected-access
3+
# pylint: disable=too-few-public-methods
4+
# pylint: disable=redefined-slots-in-subclass
5+
6+
from unittest.mock import Mock
7+
8+
import pytest
9+
10+
from logprep.ng.abc.event import Event
11+
from logprep.ng.event_state import EventState, EventStateType
12+
from logprep.ng.events.log_event import LogEvent
13+
14+
15+
class DummyEvent(Event):
16+
__slots__ = Event.__slots__
17+
18+
19+
def test_log_event_initializes_correctly() -> None:
20+
log_event = LogEvent(data={"foo": "bar"}, original=b"raw")
21+
22+
assert isinstance(log_event.state, EventState)
23+
assert log_event.original == b"raw"
24+
assert not log_event.extra_data
25+
assert log_event.metadata is None
26+
27+
assert callable(log_event._origin_state_next_state_fn)
28+
assert log_event.state.next_state != log_event._origin_state_next_state_fn
29+
30+
31+
def test_log_event_preserves_state_on_init() -> None:
32+
state = EventState()
33+
state.current_state = EventStateType.STORED_IN_OUTPUT
34+
log_event = LogEvent(data={"msg": "payload"}, original=b"event", state=state)
35+
36+
assert log_event.state.current_state is EventStateType.STORED_IN_OUTPUT
37+
38+
39+
def test_log_event_transition_to_delivered_succeeds_if_all_extra_data_delivered() -> None:
40+
child1 = DummyEvent({"c1": 1})
41+
child2 = DummyEvent({"c2": 2})
42+
child1.state.current_state = EventStateType.DELIVERED
43+
child2.state.current_state = EventStateType.DELIVERED
44+
45+
log_event = LogEvent(
46+
data={"parent": "yes"},
47+
original=b"...",
48+
extra_data=[child1, child2],
49+
)
50+
log_event.state.current_state = EventStateType.STORED_IN_OUTPUT
51+
52+
log_event.state.next_state(success=True)
53+
assert log_event.state.current_state == EventStateType.DELIVERED
54+
55+
56+
def test_log_event_transition_to_next_state_excluding_delivered() -> None:
57+
58+
log_event = LogEvent(
59+
data={"parent": "yes"},
60+
original=b"...",
61+
extra_data=[],
62+
)
63+
log_event.state.current_state = EventStateType.PROCESSING
64+
65+
log_event.state.next_state(success=True)
66+
assert log_event.state.current_state == EventStateType.PROCESSED
67+
68+
69+
def test_log_event_transition_to_delivered_fails_if_extra_data_not_delivered() -> None:
70+
child1 = DummyEvent({"c1": 1})
71+
child2 = DummyEvent({"c2": 2})
72+
child1.state.current_state = EventStateType.DELIVERED
73+
child2.state.current_state = EventStateType.FAILED
74+
75+
log_event = LogEvent(data={"parent": "e"}, original=b"...", extra_data=[child1, child2])
76+
log_event.state.current_state = EventStateType.STORED_IN_OUTPUT
77+
78+
with pytest.raises(ValueError, match="not all extra_data events are DELIVERED"):
79+
log_event.state.next_state(success=True)
80+
81+
82+
def test_log_event_direct_state_assignment_succeeds_if_all_extra_data_delivered() -> None:
83+
child = DummyEvent({"child": "ok"})
84+
child.state.current_state = EventStateType.DELIVERED
85+
log_event = LogEvent(data={"parent": "x"}, original=b"...", extra_data=[child])
86+
new_state = EventState()
87+
new_state.current_state = EventStateType.DELIVERED
88+
log_event.state = new_state
89+
90+
assert log_event.state.current_state is EventStateType.DELIVERED
91+
92+
93+
def test_next_state_validation_helper_returns_new_state_with_mock():
94+
sub_event = Mock(spec=Event)
95+
sub_event.state.current_state = EventStateType.DELIVERED
96+
97+
log_event = LogEvent(
98+
data={"msg": "parent"},
99+
original=b"raw",
100+
extra_data=[sub_event],
101+
)
102+
103+
log_event._origin_state_next_state_fn = Mock(return_value=EventStateType.DELIVERED)
104+
105+
result = log_event._next_state_validation_helper(success=True)
106+
107+
assert result == EventStateType.DELIVERED

0 commit comments

Comments
 (0)