Skip to content

Commit 383a038

Browse files
authored
Merge pull request #825 from fkie-cad/dev-event-class-implementation
Dev event class implementation
2 parents 51b5154 + e4cfc4c commit 383a038

File tree

7 files changed

+464
-3
lines changed

7 files changed

+464
-3
lines changed

CHANGELOG.md

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

88
### Improvements
9+
* implement abstract Event class to encapsulate event data, processing state, warnings, and errors
10+
* integrate dotted field handling methods directly into Event, enabling structured field access and manipulation
11+
* support event identity and hashability based on data, allowing usage in sets and as dictionary keys
12+
913
* implement EventState class to manage the lifecycle of log events
1014
* integrate a finite state machine to control valid state transitions
1115

logprep/ng/abc/event.py

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
# pylint: disable=too-few-public-methods
2+
3+
"""abstract module for event"""
4+
5+
from abc import ABC
6+
from typing import TYPE_CHECKING, Any, Optional, Union
7+
8+
from logprep.ng.event_state import EventState
9+
from logprep.util.helper import (
10+
add_fields_to,
11+
get_dotted_field_value,
12+
pop_dotted_field_value,
13+
)
14+
15+
if TYPE_CHECKING: # pragma: no cover
16+
from logprep.processor.base.rule import Rule
17+
18+
19+
class EventMetadata(ABC):
20+
"""Abstract EventMetadata Class to define the Interface"""
21+
22+
23+
class Event(ABC):
24+
"""
25+
Abstract base class representing an event in the processing pipeline.
26+
27+
Encapsulates data, warnings, errors, and processing state.
28+
"""
29+
30+
__slots__: tuple[str, ...] = ("data", "state", "errors", "warnings")
31+
32+
def __init__(
33+
self,
34+
data: dict[str, Any],
35+
*,
36+
state: EventState | None = None,
37+
) -> None:
38+
"""
39+
Initialize an Event instance.
40+
41+
Parameters
42+
----------
43+
data : dict[str, Any]
44+
The raw or processed data associated with the event.
45+
state : EventState, optional
46+
An optional initial EventState. Defaults to a new EventState() if not provided.
47+
48+
Examples
49+
--------
50+
Basic usage with automatic state:
51+
52+
>>> event = Event({"source": "syslog"})
53+
>>> event.data
54+
{'source': 'syslog'}
55+
>>> event.state.current_state.name
56+
'RECEIVING'
57+
58+
Providing a custom state:
59+
60+
>>> custom_state = EventState()
61+
>>> event = Event({"source": "api"}, state=custom_state)
62+
>>> event.state is custom_state
63+
True
64+
65+
Handling warnings and errors:
66+
67+
>>> event = Event({"id": 123})
68+
>>> event.warnings.append("Missing timestamp")
69+
>>> event.errors.append(ValueError("Invalid format"))
70+
>>> event.warnings
71+
['Missing timestamp']
72+
>>> isinstance(event.errors[0], ValueError)
73+
True
74+
"""
75+
76+
self.state: EventState = EventState() if state is None else state
77+
self.data: dict[str, Any] = data
78+
self.warnings: list[str] = []
79+
self.errors: list[Exception] = []
80+
super().__init__()
81+
82+
def __eq__(self, other: object) -> bool:
83+
"""
84+
Determines whether two Event instances are considered equal.
85+
Equality is defined by the equality of their `data` content.
86+
87+
Parameters
88+
----------
89+
other : object
90+
The object to compare against.
91+
92+
Returns
93+
-------
94+
bool
95+
True if the other object is an Event and its `data` is equal to this instance's `data`.
96+
"""
97+
98+
if not isinstance(other, Event):
99+
return NotImplemented
100+
101+
return self.data == other.data
102+
103+
def __hash__(self) -> int:
104+
"""
105+
Returns a hash based on the immutable representation of the `data` field.
106+
This enables Event instances to be used as keys in dictionaries or as members of sets.
107+
108+
Returns
109+
-------
110+
int
111+
A hash value derived from the event's `data`.
112+
"""
113+
114+
return hash(self._deep_freeze(self.data))
115+
116+
def _deep_freeze(self, obj: Any) -> Any:
117+
"""
118+
Recursively converts a data structure into a
119+
hashable (immutable) representation. Used internally for generating
120+
consistent hash values from nested dictionaries/lists.
121+
122+
Parameters
123+
----------
124+
obj : Any
125+
The object (usually dict or list) to be frozen.
126+
127+
Returns
128+
-------
129+
Any
130+
A hashable, immutable version of the input object.
131+
"""
132+
133+
if isinstance(obj, dict):
134+
return frozenset((k, self._deep_freeze(v)) for k, v in obj.items())
135+
136+
if isinstance(obj, list):
137+
return tuple(self._deep_freeze(x) for x in obj)
138+
139+
if isinstance(obj, set):
140+
return frozenset(self._deep_freeze(x) for x in obj)
141+
142+
return obj
143+
144+
def add_fields_to(
145+
self,
146+
fields: dict[str, Any],
147+
rule: "Rule" = None,
148+
merge_with_target: bool = False,
149+
overwrite_target: bool = False,
150+
) -> None:
151+
"""
152+
Add one or more fields to the target dictionary.
153+
154+
This method wraps the global `add_fields_to` utility function from
155+
`logprep.util.helper`, allowing convenient field injection, merging,
156+
and optional overwriting.
157+
158+
Args:
159+
fields (dict): A dictionary of fields to add.
160+
rule (Rule, optional): The rule context under which fields are added.
161+
merge_with_target (bool): Whether to merge dictionaries recursively instead of overwriting.
162+
overwrite_target (bool): Whether to overwrite existing values in the target.
163+
164+
Returns:
165+
None
166+
"""
167+
return add_fields_to(self.data, fields, rule, merge_with_target, overwrite_target)
168+
169+
def get_dotted_field_value(self, dotted_field: str) -> Any:
170+
"""
171+
Shortcut method that delegates to the global `get_dotted_field_value` helper.
172+
173+
Parameters
174+
----------
175+
dotted_field : str
176+
The dotted path of the field to retrieve.
177+
178+
Returns
179+
-------
180+
Any
181+
The value at the specified dotted path, or None if not found.
182+
"""
183+
return get_dotted_field_value(self.data, dotted_field)
184+
185+
def pop_dotted_field_value(self, dotted_field: str) -> Optional[Union[dict, list, str]]:
186+
"""
187+
Shortcut method that delegates to the global `pop_dotted_field_value` helper.
188+
189+
Parameters
190+
----------
191+
dotted_field : str
192+
The dotted path of the field to remove.
193+
194+
Returns
195+
-------
196+
Any
197+
The removed value, or None if the path did not exist.
198+
"""
199+
return pop_dotted_field_value(self.data, dotted_field)

logprep/ng/abc/event_metadata.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
# pylint: disable=too-few-public-methods
2-
# -> deactivate temporarily pylint issue here
32

43
"""abstract module for event related datatypes"""
54

logprep/ng/inputs/kafka_input.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import attrs
44

5-
from logprep.ng.abc.event_metadata import EventMetadata
5+
from logprep.ng.abc.event import EventMetadata
66

77

88
@attrs.define(slots=True, kw_only=True)

tests/unit/ng/inputs/__init__.py

Whitespace-only changes.

tests/unit/ng/inputs/test_kafka_input.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import pytest
55

6-
from logprep.ng.abc.event_metadata import EventMetadata
6+
from logprep.ng.abc.event import EventMetadata
77
from logprep.ng.inputs.kafka_input import KafkaInputMetadata
88

99

0 commit comments

Comments
 (0)