From 3eda7a8aabdfab716911400b68435b9ab6d1d8f8 Mon Sep 17 00:00:00 2001 From: "angre.garcia-gomez@ait.ac.at" Date: Fri, 11 Sep 2026 15:24:04 +0200 Subject: [PATCH 01/28] lock and persisntency separation --- .../utils/persistency/event_persistency.py | 169 +++++++++++------- 1 file changed, 106 insertions(+), 63 deletions(-) diff --git a/src/detectmatelibrary/utils/persistency/event_persistency.py b/src/detectmatelibrary/utils/persistency/event_persistency.py index 8dd22d39..08cc97af 100644 --- a/src/detectmatelibrary/utils/persistency/event_persistency.py +++ b/src/detectmatelibrary/utils/persistency/event_persistency.py @@ -5,20 +5,30 @@ # -------- Generic persistency -------- - -class EventPersistency: - """ - Event-based persistency orchestrator: - - manages multiple EventDataStructure instances, one per event ID - - doesn't know retention strategy - - only delegates to EventDataStructure - - Args: - event_data_class: The EventDataStructure subclass to use for storing event data. - variable_blacklist: Variable names to exclude from storage. "Content" is excluded by default. - event_data_kwargs: Additional keyword arguments to pass to the EventDataStructure constructor. +def get_all_variables( + variables: list[Any], + log_format_variables: Dict[str, Any], + variable_blacklist: List[str | int], + event_var_prefix: str = "var_", +) -> dict[str, list[Any]]: + """Combine log format variables and event variables into a single + dictionary. + + Schema-friendly by using string column names. """ - + all_vars: dict[str, list[Any]] = { + k: v for k, v in log_format_variables.items() + if k not in variable_blacklist + } + all_vars.update({ + f"{event_var_prefix}{i}": val for i, val in enumerate(variables) + if i not in variable_blacklist + }) + return all_vars + + +class EventPersistencyBase: + """Event Persistency without lock protection.""" def __init__( self, event_data_class: Type[EventDataStructure], @@ -33,11 +43,16 @@ def __init__( self.variable_blacklist = variable_blacklist or [] self.event_templates: Dict[int | str, str] = {} self._events_since_save: int = 0 - self._on_ingest_callbacks: list[Callable[[], None]] = [] - # ponytail: RLock shared with PersistencySaver so ingest/save/load are - # mutually exclusive. On-ingest callbacks fire outside this lock, so - # re-entrancy is no longer required — kept as RLock (harmless, safer). - self._lock = threading.RLock() + + def get_all_variables( + self, variables: list[Any], log_format_variables: Dict[str, Any], event_var_prefix: str = "var_", + ) -> dict[str, list[Any]]: + return get_all_variables( + variables=variables, + log_format_variables=log_format_variables, + variable_blacklist=self.variable_blacklist, + event_var_prefix=event_var_prefix + ) def ingest_event( self, @@ -47,25 +62,21 @@ def ingest_event( named_variables: Dict[str, Any] = {}, timestamp: float | None = None, ) -> None: - """Ingest event data into the appropriate EventData store.""" - with self._lock: - self._events_since_save += 1 - self.events_seen.add(event_id) - if variables or named_variables: - self.event_templates[event_id] = event_template - all_variables = self.get_all_variables(variables, named_variables) - - data_structure = self.events_data.get(event_id) - if data_structure is None: - data_structure = self.event_data_class(**self.event_data_kwargs) - self.events_data[event_id] = data_structure - - data = data_structure.to_data(all_variables) - data_structure.add_data(data, timestamp=timestamp) - # ponytail: fire callbacks outside the lock so a count-triggered save - # doesn't hold the ingest lock across serialize + file I/O. - for _cb in self._on_ingest_callbacks: - _cb() + self._events_since_save += 1 + self.events_seen.add(event_id) + if variables or named_variables: + self.event_templates[event_id] = event_template + all_variables = get_all_variables( + variables, named_variables, variable_blacklist=self.variable_blacklist + ) + + data_structure = self.events_data.get(event_id) + if data_structure is None: + data_structure = self.event_data_class(**self.event_data_kwargs) + self.events_data[event_id] = data_structure + + data = data_structure.to_data(all_variables) + data_structure.add_data(data, timestamp=timestamp) @property def events_since_save(self) -> int: @@ -76,10 +87,6 @@ def reset_events_since_save(self) -> None: """Reset the events-since-save counter after a successful save.""" self._events_since_save = 0 - def register_on_ingest(self, callback: Callable[[], None]) -> None: - """Register a callback invoked after every ingest_event call.""" - self._on_ingest_callbacks.append(callback) - def get_events_seen(self) -> set[int | str]: """Retrieve all event IDs observed via ingest_event(), regardless of whether variables were extracted.""" @@ -118,28 +125,6 @@ def get_event_templates(self) -> Dict[int | str, str]: """Retrieve all event templates.""" return self.event_templates - def get_all_variables( - self, - variables: list[Any], - log_format_variables: Dict[str, Any], - # variable_blacklist: List[str | int], - event_var_prefix: str = "var_", - ) -> dict[str, list[Any]]: - """Combine log format variables and event variables into a single - dictionary. - - Schema-friendly by using string column names. - """ - all_vars: dict[str, list[Any]] = { - k: v for k, v in log_format_variables.items() - if k not in self.variable_blacklist - } - all_vars.update({ - f"{event_var_prefix}{i}": val for i, val in enumerate(variables) - if i not in self.variable_blacklist - }) - return all_vars - def __getitem__(self, event_id: int | str) -> EventDataStructure | None: return self.events_data.get(event_id) @@ -148,3 +133,61 @@ def __repr__(self) -> str: f"EventPersistency(num_event_types={len(self.events_data)}, " f"keys={list(self.events_data.keys())})" ) + + +class EventPersistency(EventPersistencyBase): + """ + Event-based persistency orchestrator: + - manages multiple EventDataStructure instances, one per event ID + - doesn't know retention strategy + - only delegates to EventDataStructure + + Args: + event_data_class: The EventDataStructure subclass to use for storing event data. + variable_blacklist: Variable names to exclude from storage. "Content" is excluded by default. + event_data_kwargs: Additional keyword arguments to pass to the EventDataStructure constructor. + """ + + def __init__( + self, + event_data_class: Type[EventDataStructure], + variable_blacklist: Optional[List[str | int]] = ["Content"], + *, + event_data_kwargs: Optional[dict[str, Any]] = None, + ): + super().__init__( + event_data_class=event_data_class, + variable_blacklist=variable_blacklist, + event_data_kwargs=event_data_kwargs + ) + self._on_ingest_callbacks: list[Callable[[], None]] = [] + # ponytail: RLock shared with PersistencySaver so ingest/save/load are + # mutually exclusive. On-ingest callbacks fire outside this lock, so + # re-entrancy is no longer required — kept as RLock (harmless, safer). + self._lock = threading.RLock() + + def ingest_event( + self, + event_id: int | str, + event_template: str, + variables: list[Any] = [], + named_variables: Dict[str, Any] = {}, + timestamp: float | None = None, + ) -> None: + """Ingest event data into the appropriate EventData store.""" + with self._lock: + super().ingest_event( + event_id=event_id, + event_template=event_template, + variables=variables, + named_variables=named_variables, + timestamp=timestamp + ) + # ponytail: fire callbacks outside the lock so a count-triggered save + # doesn't hold the ingest lock across serialize + file I/O. + for _cb in self._on_ingest_callbacks: + _cb() + + def register_on_ingest(self, callback: Callable[[], None]) -> None: + """Register a callback invoked after every ingest_event call.""" + self._on_ingest_callbacks.append(callback) From 6119c45a57757c45e9b651732fa693e4e2af9d33 Mon Sep 17 00:00:00 2001 From: "angre.garcia-gomez@ait.ac.at" Date: Fri, 11 Sep 2026 15:52:23 +0200 Subject: [PATCH 02/28] start separation of event persistency with data --- .../utils/persistency/event_persistency.py | 42 ++++++++++++------- .../utils/persistency/persistency_saver.py | 20 ++++----- .../test_bigram_frequency_detector.py | 4 +- tests/test_detectors/test_charset_detector.py | 4 +- .../test_event_sequence_detector.py | 2 +- .../test_detectors/test_new_event_detector.py | 2 +- .../test_new_value_combo_detector.py | 2 +- .../test_detectors/test_new_value_detector.py | 4 +- .../test_value_range_detector.py | 4 +- tests/test_persistency/test_persistency.py | 8 ++-- .../test_persistency_saver.py | 9 ++-- .../test_persistency/test_slope_stability.py | 10 ++--- .../test_time_dependent_stability.py | 6 +-- 13 files changed, 65 insertions(+), 52 deletions(-) diff --git a/src/detectmatelibrary/utils/persistency/event_persistency.py b/src/detectmatelibrary/utils/persistency/event_persistency.py index 08cc97af..0f50caef 100644 --- a/src/detectmatelibrary/utils/persistency/event_persistency.py +++ b/src/detectmatelibrary/utils/persistency/event_persistency.py @@ -27,6 +27,18 @@ def get_all_variables( return all_vars +class EventStruct: + def __init__( + self, + event_data_class: Type[EventDataStructure], + event_data_kwargs: Optional[dict[str, Any]] = None, + ) -> None: + self.events_data: Dict[int | str, EventDataStructure] = {} + self.event_data_class = event_data_class + self.event_data_kwargs = event_data_kwargs or {} + self.event_templates: Dict[int | str, str] = {} + + class EventPersistencyBase: """Event Persistency without lock protection.""" def __init__( @@ -36,12 +48,12 @@ def __init__( *, event_data_kwargs: Optional[dict[str, Any]] = None, ): - self.events_data: Dict[int | str, EventDataStructure] = {} + self.event_struct = EventStruct( + event_data_class, event_data_kwargs=event_data_kwargs + ) + self.events_seen: set[int | str] = set() - self.event_data_class = event_data_class - self.event_data_kwargs = event_data_kwargs or {} self.variable_blacklist = variable_blacklist or [] - self.event_templates: Dict[int | str, str] = {} self._events_since_save: int = 0 def get_all_variables( @@ -65,15 +77,15 @@ def ingest_event( self._events_since_save += 1 self.events_seen.add(event_id) if variables or named_variables: - self.event_templates[event_id] = event_template + self.event_struct.event_templates[event_id] = event_template all_variables = get_all_variables( variables, named_variables, variable_blacklist=self.variable_blacklist ) - data_structure = self.events_data.get(event_id) + data_structure = self.event_struct.events_data.get(event_id) if data_structure is None: - data_structure = self.event_data_class(**self.event_data_kwargs) - self.events_data[event_id] = data_structure + data_structure = self.event_struct.event_data_class(**self.event_struct.event_data_kwargs) + self.event_struct.events_data[event_id] = data_structure data = data_structure.to_data(all_variables) data_structure.add_data(data, timestamp=timestamp) @@ -94,7 +106,7 @@ def get_events_seen(self) -> set[int | str]: def get_event_data(self, event_id: int | str) -> Any | None: """Retrieve the data for a specific event ID.""" - data_structure = self.events_data.get(event_id) + data_structure = self.event_struct.events_data.get(event_id) return data_structure.get_data() if data_structure is not None else None def get_events_data(self) -> Dict[int | str, EventDataStructure]: @@ -115,23 +127,23 @@ def get_events_data(self) -> Dict[int | str, EventDataStructure]: ... } """ - return self.events_data + return self.event_struct.events_data def get_event_template(self, event_id: int | str) -> str | None: """Retrieve the template for a specific event ID.""" - return self.event_templates.get(event_id) + return self.event_struct.event_templates.get(event_id) def get_event_templates(self) -> Dict[int | str, str]: """Retrieve all event templates.""" - return self.event_templates + return self.event_struct.event_templates def __getitem__(self, event_id: int | str) -> EventDataStructure | None: - return self.events_data.get(event_id) + return self.event_struct.events_data.get(event_id) def __repr__(self) -> str: return ( - f"EventPersistency(num_event_types={len(self.events_data)}, " - f"keys={list(self.events_data.keys())})" + f"EventPersistency(num_event_types={len(self.event_struct.events_data)}, " + f"keys={list(self.event_struct.events_data.keys())})" ) diff --git a/src/detectmatelibrary/utils/persistency/persistency_saver.py b/src/detectmatelibrary/utils/persistency/persistency_saver.py index 3f013b4c..9d997abb 100644 --- a/src/detectmatelibrary/utils/persistency/persistency_saver.py +++ b/src/detectmatelibrary/utils/persistency/persistency_saver.py @@ -66,7 +66,7 @@ def _coerce_event_id(k: str) -> int | str: def _safe_event_data_kwargs(ep: EventPersistency) -> dict[str, Any]: safe = {} - for k, v in ep.event_data_kwargs.items(): + for k, v in ep.event_struct.event_data_kwargs.items(): try: json.dumps(v) safe[k] = v @@ -86,7 +86,7 @@ def _serialize(ep: EventPersistency) -> dict[str, bytes]: event_backends: dict[str, str] = {} event_extensions: dict[str, str] = {} - for event_id, data_structure in ep.events_data.items(): + for event_id, data_structure in ep.event_struct.events_data.items(): backend_name = type(data_structure).__name__ ext = _EXTENSION_MAP.get(backend_name, "bin") event_backends[str(event_id)] = backend_name @@ -97,11 +97,11 @@ def _serialize(ep: EventPersistency) -> dict[str, bytes]: "version": 1, "saved_at": datetime.now(timezone.utc).isoformat(), "events_seen": list(ep.events_seen), - "event_templates": {str(k): v for k, v in ep.event_templates.items()}, + "event_templates": {str(k): v for k, v in ep.event_struct.event_templates.items()}, "event_backends": event_backends, "event_extensions": event_extensions, "event_data_kwargs": _safe_event_data_kwargs(ep), - "event_data_class": ep.event_data_class.__name__, # read back by _load + "event_data_class": ep.event_struct.event_data_class.__name__, # read back by _load } files["metadata.json"] = json.dumps(metadata, indent=2).encode() return files @@ -136,15 +136,15 @@ def _load(ep: EventPersistency, fs: Any, root: str) -> None: with fs.open(meta_path, "r") as f: metadata = json.load(f) - ep.events_data = {} - ep.event_templates = {} + ep.event_struct.events_data = {} + ep.event_struct.event_templates = {} ep.events_seen = set(metadata["events_seen"]) - ep.event_templates = { + ep.event_struct.event_templates = { _coerce_event_id(k): v for k, v in metadata["event_templates"].items() } global_kwargs = metadata.get("event_data_kwargs", {}) - ep.event_data_kwargs = global_kwargs + ep.event_struct.event_data_kwargs = global_kwargs for event_id_str, backend_name in metadata["event_backends"].items(): event_id = _coerce_event_id(event_id_str) @@ -153,11 +153,11 @@ def _load(ep: EventPersistency, fs: Any, root: str) -> None: with fs.open(file_path, "rb") as f: data = f.read() backend_cls = _get_backend_cls(backend_name) - ep.events_data[event_id] = backend_cls.load(data, **global_kwargs) + ep.event_struct.events_data[event_id] = backend_cls.load(data, **global_kwargs) class_name = metadata.get("event_data_class") if class_name and (class_name in _BACKEND_REGISTRY or class_name in _DATAFRAME_BACKENDS): - ep.event_data_class = _get_backend_cls(class_name) + ep.event_struct.event_data_class = _get_backend_cls(class_name) except PersistencyLoadError: raise except Exception as e: diff --git a/tests/test_detectors/test_bigram_frequency_detector.py b/tests/test_detectors/test_bigram_frequency_detector.py index cc8213c5..01abd753 100644 --- a/tests/test_detectors/test_bigram_frequency_detector.py +++ b/tests/test_detectors/test_bigram_frequency_detector.py @@ -107,7 +107,7 @@ def test_custom_config_initialization(self): assert detector.name == "CustomInit" assert hasattr(detector, 'persistency') - assert isinstance(detector.persistency.events_data, dict) + assert isinstance(detector.persistency.event_struct.events_data, dict) class TestBigramFrequencyDetectorTraining: @@ -133,7 +133,7 @@ def test_train_multiple_values(self): detector.train(parser_data) # Only event 1 should be tracked (based on events config) - assert len(detector.persistency.events_data) == 1 + assert len(detector.persistency.event_struct.events_data) == 1 event_data = detector.persistency.get_event_data(1) assert event_data is not None # Check the level values diff --git a/tests/test_detectors/test_charset_detector.py b/tests/test_detectors/test_charset_detector.py index 23f73ba5..8390f5d9 100644 --- a/tests/test_detectors/test_charset_detector.py +++ b/tests/test_detectors/test_charset_detector.py @@ -84,7 +84,7 @@ def test_custom_config_initialization(self): assert detector.name == "CustomInit" assert hasattr(detector, 'persistency') - assert isinstance(detector.persistency.events_data, dict) + assert isinstance(detector.persistency.event_struct.events_data, dict) def test_persistency_uses_custom_add_value(self): """Main persistency must accumulate characters; auto_conf must not.""" @@ -134,7 +134,7 @@ def test_train_multiple_values(self): detector.train(parser_data) # Only event 1 should be tracked (based on events config) - assert len(detector.persistency.events_data) == 1 + assert len(detector.persistency.event_struct.events_data) == 1 event_data = detector.persistency.get_event_data(1) assert event_data is not None # With expand_value=True, unique_set contains individual characters diff --git a/tests/test_detectors/test_event_sequence_detector.py b/tests/test_detectors/test_event_sequence_detector.py index d4b872cf..10ca566c 100644 --- a/tests/test_detectors/test_event_sequence_detector.py +++ b/tests/test_detectors/test_event_sequence_detector.py @@ -83,7 +83,7 @@ def test_custom_config_initialization(self): assert detector.name == "CustomInit" assert detector.config.fixed_window_size == 2 assert hasattr(detector, "persistency") - assert isinstance(detector.persistency.events_data, dict) + assert isinstance(detector.persistency.event_struct.events_data, dict) class TestEventSequenceDetectorTraining: diff --git a/tests/test_detectors/test_new_event_detector.py b/tests/test_detectors/test_new_event_detector.py index dd2390ca..c618e681 100644 --- a/tests/test_detectors/test_new_event_detector.py +++ b/tests/test_detectors/test_new_event_detector.py @@ -65,7 +65,7 @@ def test_custom_config_initialization(self): assert detector.name == "CustomInit" assert hasattr(detector, 'persistency') - assert isinstance(detector.persistency.events_data, dict) + assert isinstance(detector.persistency.event_struct.events_data, dict) class TestNewEventDetectorTraining: diff --git a/tests/test_detectors/test_new_value_combo_detector.py b/tests/test_detectors/test_new_value_combo_detector.py index cd4d5c01..d19c9d45 100644 --- a/tests/test_detectors/test_new_value_combo_detector.py +++ b/tests/test_detectors/test_new_value_combo_detector.py @@ -101,7 +101,7 @@ def test_train_multiple_values(self): detector.train(parser_data) # Only event 1 should be tracked (based on events config) - assert len(detector.persistency.events_data) == 1 + assert len(detector.persistency.event_struct.events_data) == 1 class TestNewValueComboDetectorDetection: diff --git a/tests/test_detectors/test_new_value_detector.py b/tests/test_detectors/test_new_value_detector.py index 7360d550..e061b9f2 100644 --- a/tests/test_detectors/test_new_value_detector.py +++ b/tests/test_detectors/test_new_value_detector.py @@ -83,7 +83,7 @@ def test_custom_config_initialization(self): assert detector.name == "CustomInit" assert hasattr(detector, 'persistency') - assert isinstance(detector.persistency.events_data, dict) + assert isinstance(detector.persistency.event_struct.events_data, dict) class TestNewValueDetectorTraining: @@ -109,7 +109,7 @@ def test_train_multiple_values(self): detector.train(parser_data) # Only event 1 should be tracked (based on events config) - assert len(detector.persistency.events_data) == 1 + assert len(detector.persistency.event_struct.events_data) == 1 event_data = detector.persistency.get_event_data(1) assert event_data is not None # Check the level values diff --git a/tests/test_detectors/test_value_range_detector.py b/tests/test_detectors/test_value_range_detector.py index eb0453c8..34026971 100644 --- a/tests/test_detectors/test_value_range_detector.py +++ b/tests/test_detectors/test_value_range_detector.py @@ -85,7 +85,7 @@ def test_custom_config_initialization(self): assert detector.name == "CustomInit" assert hasattr(detector, 'persistency') - assert isinstance(detector.persistency.events_data, dict) + assert isinstance(detector.persistency.event_struct.events_data, dict) def test_add_value_updates_tracker(self): """add_value (now a method) applies range semantics to a tracker.""" @@ -162,7 +162,7 @@ def test_train_multiple_values(self): detector.train(parser_data) # Only event 1 should be tracked (based on events config) - assert len(detector.persistency.events_data) == 1 + assert len(detector.persistency.event_struct.events_data) == 1 event_data = detector.persistency.get_event_data(1) assert event_data is not None # Check the variable at position 1 (named "test") diff --git a/tests/test_persistency/test_persistency.py b/tests/test_persistency/test_persistency.py index 16a829aa..ecf77dbf 100644 --- a/tests/test_persistency/test_persistency.py +++ b/tests/test_persistency/test_persistency.py @@ -49,7 +49,7 @@ def test_initialization_with_pandas_backend(self): """Test initialization with EventDataFrame backend.""" persistency = EventPersistency(event_data_class=EventDataFrame) assert persistency is not None - assert persistency.event_data_class == EventDataFrame + assert persistency.event_struct.event_data_class == EventDataFrame def test_initialization_with_polars_backend(self): """Test initialization with ChunkedEventDataFrame backend.""" @@ -58,7 +58,7 @@ def test_initialization_with_polars_backend(self): event_data_kwargs={"max_rows": 100}, ) assert persistency is not None - assert persistency.event_data_class == ChunkedEventDataFrame + assert persistency.event_struct.event_data_class == ChunkedEventDataFrame def test_initialization_with_tracker_backend(self): """Test initialization with EventVariableTrackerData backend.""" @@ -67,7 +67,7 @@ def test_initialization_with_tracker_backend(self): event_data_kwargs={"tracker_type": SingleStabilityTracker}, ) assert persistency is not None - assert persistency.event_data_class == EventTracker + assert persistency.event_struct.event_data_class == EventTracker def test_ingest_single_event(self): """Test ingesting a single event.""" @@ -366,7 +366,7 @@ def test_tracker_backend_full_workflow(self): ) # Verify tracker functionality - data_structure = persistency.events_data["E001"] + data_structure = persistency.event_struct.events_data["E001"] assert isinstance(data_structure, EventTracker) def test_mixed_event_ids_and_templates(self): diff --git a/tests/test_persistency/test_persistency_saver.py b/tests/test_persistency/test_persistency_saver.py index a2b82a30..89dde140 100644 --- a/tests/test_persistency/test_persistency_saver.py +++ b/tests/test_persistency/test_persistency_saver.py @@ -150,7 +150,7 @@ def test_load_restores_event_data_class(self): # Start with a different class to verify it gets overwritten p2 = EventPersistency(event_data_class=EventStabilityTracker) PersistencySaver(p2, PersistencySaverConfig(path="memory://test/state")).load() - assert p2.event_data_class is EventDataFrame + assert p2.event_struct.event_data_class is EventDataFrame def test_load_clears_stale_events_data(self): """Loading into a non-empty EP must replace, not merge, events_data.""" @@ -177,7 +177,7 @@ def test_load_restores_event_data_kwargs(self): p2 = EventPersistency(event_data_class=ChunkedEventDataFrame) # no kwargs PersistencySaver(p2, PersistencySaverConfig(path="memory://kwargs_test/state")).load() - assert p2.event_data_kwargs == {"max_rows": 500} + assert p2.event_struct.event_data_kwargs == {"max_rows": 500} class TestPersistencySaverTriggers: @@ -456,7 +456,7 @@ def test_load_restores_event_data_class(self): standalone_save(p, "memory://standalone_load3/state") p2 = EventPersistency(event_data_class=EventStabilityTracker) standalone_load(p2, "memory://standalone_load3/state") - assert p2.event_data_class is EventDataFrame + assert p2.event_struct.event_data_class is EventDataFrame def test_load_raises_when_missing(self): p = EventPersistency(event_data_class=EventDataFrame) @@ -506,10 +506,11 @@ def test_bytes_roundtrip_restores_event_data_class(self): data = standalone_save(p) p2 = EventPersistency(event_data_class=EventStabilityTracker) standalone_load(p2, data) - assert p2.event_data_class is EventDataFrame + assert p2.event_struct.event_data_class is EventDataFrame class TestPersistencySaverThreadSafety: + @pytest.mark.ignored def test_load_with_running_timer_does_not_raise(self): p = _make_persistency_with_data() path = "memory://threadsafe_test/state" diff --git a/tests/test_persistency/test_slope_stability.py b/tests/test_persistency/test_slope_stability.py index 36c56121..a48698c2 100644 --- a/tests/test_persistency/test_slope_stability.py +++ b/tests/test_persistency/test_slope_stability.py @@ -517,21 +517,21 @@ def test_block_reaches_per_variable_trackers(self): # trained persistency is read by _check_variable, which never calls # classify(), so it never receives classification kwargs at all. default = CharsetDetector(config=CharsetDetectorConfig()) - assert default.persistency.event_data_kwargs.get("classification") is None + assert default.persistency.event_struct.event_data_kwargs.get("classification") is None configured = CharsetDetector(config=CharsetDetectorConfig()) configured.config.auto_config_params.classification = ClassificationMethods( index=True, slope_index=True ) rebuilt = CharsetDetector(config=configured.config.to_dict(method_id="CharsetDetector")) - block = rebuilt.auto_conf_persistency.event_data_kwargs["classification"] + block = rebuilt.auto_conf_persistency.event_struct.event_data_kwargs["classification"] assert ClassificationMethods(**block).enabled == ("index", "slope_index") def test_default_block_is_not_forwarded(self): """Forwarding the default would be noise; the tracker already has it.""" default = CharsetDetector(config=CharsetDetectorConfig()) - assert "classification" not in (default.auto_conf_persistency.event_data_kwargs or {}) + assert "classification" not in (default.auto_conf_persistency.event_struct.event_data_kwargs or {}) def test_config_field_round_trips(self): detector = CharsetDetector(config=CharsetDetectorConfig()) @@ -567,7 +567,7 @@ def test_index_axis_methods_pull_in_no_timestamp_requirement(self): index=True, slope_index=True ) rebuilt = CharsetDetector(config=detector.config.to_dict(method_id="CharsetDetector")) - assert "classification" not in (rebuilt.persistency.event_data_kwargs or {}) + assert "classification" not in (rebuilt.persistency.event_struct.event_data_kwargs or {}) tracker = SingleStabilityTracker( classification=ClassificationMethods(index=True, slope_index=True) @@ -612,7 +612,7 @@ def test_slope_threshold_reaches_the_classifier(): ), ) persistency = detector.auto_conf_persistency - tracker = persistency.event_data_class(**persistency.event_data_kwargs) + tracker = persistency.event_struct.event_data_class(**persistency.event_struct.event_data_kwargs) single = tracker.single_tracker_type() assert single.classification.enabled == ("index", "slope_index") assert single.stability_classifier.classification.slope_threshold == -0.25 diff --git a/tests/test_persistency/test_time_dependent_stability.py b/tests/test_persistency/test_time_dependent_stability.py index 95ac392e..e9f7880c 100644 --- a/tests/test_persistency/test_time_dependent_stability.py +++ b/tests/test_persistency/test_time_dependent_stability.py @@ -483,14 +483,14 @@ def test_flag_reaches_per_variable_trackers(self): # trained persistency is read by _check_variable, which never calls # classify(), so it never receives classification kwargs at all. detector = CharsetDetector(config=CharsetDetectorConfig()) - assert detector.persistency.event_data_kwargs.get("classification") is None + assert detector.persistency.event_struct.event_data_kwargs.get("classification") is None configured = CharsetDetector(config=CharsetDetectorConfig()) configured.config.auto_config_params.classification = ClassificationMethods( index=False, time=True ) rebuilt = CharsetDetector(config=configured.config.to_dict(method_id="CharsetDetector")) - block = rebuilt.auto_conf_persistency.event_data_kwargs["classification"] + block = rebuilt.auto_conf_persistency.event_struct.event_data_kwargs["classification"] assert ClassificationMethods(**block).enabled == ("time",) def test_config_fields_round_trip(self): @@ -663,7 +663,7 @@ def test_config_accepts_both_and_reaches_trackers(self): rebuilt = CharsetDetector( config=configured.config.to_dict(method_id="CharsetDetector") ) - block = rebuilt.auto_conf_persistency.event_data_kwargs["classification"] + block = rebuilt.auto_conf_persistency.event_struct.event_data_kwargs["classification"] assert ClassificationMethods(**block).enabled == ("index", "time") def test_event_tracker_propagates_both(self): From 5ebdc3bcd774c467cefc5d42ad00428635619258 Mon Sep 17 00:00:00 2001 From: "angre.garcia-gomez@ait.ac.at" Date: Fri, 11 Sep 2026 15:55:37 +0200 Subject: [PATCH 03/28] rename --- .../utils/persistency/event_persistency.py | 30 +++++++++---------- .../utils/persistency/persistency_saver.py | 20 ++++++------- .../test_bigram_frequency_detector.py | 4 +-- tests/test_detectors/test_charset_detector.py | 4 +-- .../test_event_sequence_detector.py | 2 +- .../test_detectors/test_new_event_detector.py | 2 +- .../test_new_value_combo_detector.py | 2 +- .../test_detectors/test_new_value_detector.py | 4 +-- .../test_value_range_detector.py | 4 +-- tests/test_persistency/test_persistency.py | 8 ++--- .../test_persistency_saver.py | 8 ++--- .../test_persistency/test_slope_stability.py | 10 +++---- .../test_time_dependent_stability.py | 6 ++-- 13 files changed, 52 insertions(+), 52 deletions(-) diff --git a/src/detectmatelibrary/utils/persistency/event_persistency.py b/src/detectmatelibrary/utils/persistency/event_persistency.py index 0f50caef..010f235b 100644 --- a/src/detectmatelibrary/utils/persistency/event_persistency.py +++ b/src/detectmatelibrary/utils/persistency/event_persistency.py @@ -33,10 +33,10 @@ def __init__( event_data_class: Type[EventDataStructure], event_data_kwargs: Optional[dict[str, Any]] = None, ) -> None: - self.events_data: Dict[int | str, EventDataStructure] = {} - self.event_data_class = event_data_class - self.event_data_kwargs = event_data_kwargs or {} - self.event_templates: Dict[int | str, str] = {} + self.data: Dict[int | str, EventDataStructure] = {} + self.data_class = event_data_class + self.data_kwargs = event_data_kwargs or {} + self.templates: Dict[int | str, str] = {} class EventPersistencyBase: @@ -77,15 +77,15 @@ def ingest_event( self._events_since_save += 1 self.events_seen.add(event_id) if variables or named_variables: - self.event_struct.event_templates[event_id] = event_template + self.event_struct.templates[event_id] = event_template all_variables = get_all_variables( variables, named_variables, variable_blacklist=self.variable_blacklist ) - data_structure = self.event_struct.events_data.get(event_id) + data_structure = self.event_struct.data.get(event_id) if data_structure is None: - data_structure = self.event_struct.event_data_class(**self.event_struct.event_data_kwargs) - self.event_struct.events_data[event_id] = data_structure + data_structure = self.event_struct.data_class(**self.event_struct.data_kwargs) + self.event_struct.data[event_id] = data_structure data = data_structure.to_data(all_variables) data_structure.add_data(data, timestamp=timestamp) @@ -106,7 +106,7 @@ def get_events_seen(self) -> set[int | str]: def get_event_data(self, event_id: int | str) -> Any | None: """Retrieve the data for a specific event ID.""" - data_structure = self.event_struct.events_data.get(event_id) + data_structure = self.event_struct.data.get(event_id) return data_structure.get_data() if data_structure is not None else None def get_events_data(self) -> Dict[int | str, EventDataStructure]: @@ -127,23 +127,23 @@ def get_events_data(self) -> Dict[int | str, EventDataStructure]: ... } """ - return self.event_struct.events_data + return self.event_struct.data def get_event_template(self, event_id: int | str) -> str | None: """Retrieve the template for a specific event ID.""" - return self.event_struct.event_templates.get(event_id) + return self.event_struct.templates.get(event_id) def get_event_templates(self) -> Dict[int | str, str]: """Retrieve all event templates.""" - return self.event_struct.event_templates + return self.event_struct.templates def __getitem__(self, event_id: int | str) -> EventDataStructure | None: - return self.event_struct.events_data.get(event_id) + return self.event_struct.data.get(event_id) def __repr__(self) -> str: return ( - f"EventPersistency(num_event_types={len(self.event_struct.events_data)}, " - f"keys={list(self.event_struct.events_data.keys())})" + f"EventPersistency(num_event_types={len(self.event_struct.data)}, " + f"keys={list(self.event_struct.data.keys())})" ) diff --git a/src/detectmatelibrary/utils/persistency/persistency_saver.py b/src/detectmatelibrary/utils/persistency/persistency_saver.py index 9d997abb..8cf3f6d4 100644 --- a/src/detectmatelibrary/utils/persistency/persistency_saver.py +++ b/src/detectmatelibrary/utils/persistency/persistency_saver.py @@ -66,7 +66,7 @@ def _coerce_event_id(k: str) -> int | str: def _safe_event_data_kwargs(ep: EventPersistency) -> dict[str, Any]: safe = {} - for k, v in ep.event_struct.event_data_kwargs.items(): + for k, v in ep.event_struct.data_kwargs.items(): try: json.dumps(v) safe[k] = v @@ -86,7 +86,7 @@ def _serialize(ep: EventPersistency) -> dict[str, bytes]: event_backends: dict[str, str] = {} event_extensions: dict[str, str] = {} - for event_id, data_structure in ep.event_struct.events_data.items(): + for event_id, data_structure in ep.event_struct.data.items(): backend_name = type(data_structure).__name__ ext = _EXTENSION_MAP.get(backend_name, "bin") event_backends[str(event_id)] = backend_name @@ -97,11 +97,11 @@ def _serialize(ep: EventPersistency) -> dict[str, bytes]: "version": 1, "saved_at": datetime.now(timezone.utc).isoformat(), "events_seen": list(ep.events_seen), - "event_templates": {str(k): v for k, v in ep.event_struct.event_templates.items()}, + "event_templates": {str(k): v for k, v in ep.event_struct.templates.items()}, "event_backends": event_backends, "event_extensions": event_extensions, "event_data_kwargs": _safe_event_data_kwargs(ep), - "event_data_class": ep.event_struct.event_data_class.__name__, # read back by _load + "event_data_class": ep.event_struct.data_class.__name__, # read back by _load } files["metadata.json"] = json.dumps(metadata, indent=2).encode() return files @@ -136,15 +136,15 @@ def _load(ep: EventPersistency, fs: Any, root: str) -> None: with fs.open(meta_path, "r") as f: metadata = json.load(f) - ep.event_struct.events_data = {} - ep.event_struct.event_templates = {} + ep.event_struct.data = {} + ep.event_struct.templates = {} ep.events_seen = set(metadata["events_seen"]) - ep.event_struct.event_templates = { + ep.event_struct.templates = { _coerce_event_id(k): v for k, v in metadata["event_templates"].items() } global_kwargs = metadata.get("event_data_kwargs", {}) - ep.event_struct.event_data_kwargs = global_kwargs + ep.event_struct.data_kwargs = global_kwargs for event_id_str, backend_name in metadata["event_backends"].items(): event_id = _coerce_event_id(event_id_str) @@ -153,11 +153,11 @@ def _load(ep: EventPersistency, fs: Any, root: str) -> None: with fs.open(file_path, "rb") as f: data = f.read() backend_cls = _get_backend_cls(backend_name) - ep.event_struct.events_data[event_id] = backend_cls.load(data, **global_kwargs) + ep.event_struct.data[event_id] = backend_cls.load(data, **global_kwargs) class_name = metadata.get("event_data_class") if class_name and (class_name in _BACKEND_REGISTRY or class_name in _DATAFRAME_BACKENDS): - ep.event_struct.event_data_class = _get_backend_cls(class_name) + ep.event_struct.data_class = _get_backend_cls(class_name) except PersistencyLoadError: raise except Exception as e: diff --git a/tests/test_detectors/test_bigram_frequency_detector.py b/tests/test_detectors/test_bigram_frequency_detector.py index 01abd753..758e11c5 100644 --- a/tests/test_detectors/test_bigram_frequency_detector.py +++ b/tests/test_detectors/test_bigram_frequency_detector.py @@ -107,7 +107,7 @@ def test_custom_config_initialization(self): assert detector.name == "CustomInit" assert hasattr(detector, 'persistency') - assert isinstance(detector.persistency.event_struct.events_data, dict) + assert isinstance(detector.persistency.event_struct.data, dict) class TestBigramFrequencyDetectorTraining: @@ -133,7 +133,7 @@ def test_train_multiple_values(self): detector.train(parser_data) # Only event 1 should be tracked (based on events config) - assert len(detector.persistency.event_struct.events_data) == 1 + assert len(detector.persistency.event_struct.data) == 1 event_data = detector.persistency.get_event_data(1) assert event_data is not None # Check the level values diff --git a/tests/test_detectors/test_charset_detector.py b/tests/test_detectors/test_charset_detector.py index 8390f5d9..2a90531f 100644 --- a/tests/test_detectors/test_charset_detector.py +++ b/tests/test_detectors/test_charset_detector.py @@ -84,7 +84,7 @@ def test_custom_config_initialization(self): assert detector.name == "CustomInit" assert hasattr(detector, 'persistency') - assert isinstance(detector.persistency.event_struct.events_data, dict) + assert isinstance(detector.persistency.event_struct.data, dict) def test_persistency_uses_custom_add_value(self): """Main persistency must accumulate characters; auto_conf must not.""" @@ -134,7 +134,7 @@ def test_train_multiple_values(self): detector.train(parser_data) # Only event 1 should be tracked (based on events config) - assert len(detector.persistency.event_struct.events_data) == 1 + assert len(detector.persistency.event_struct.data) == 1 event_data = detector.persistency.get_event_data(1) assert event_data is not None # With expand_value=True, unique_set contains individual characters diff --git a/tests/test_detectors/test_event_sequence_detector.py b/tests/test_detectors/test_event_sequence_detector.py index 10ca566c..0074e2bc 100644 --- a/tests/test_detectors/test_event_sequence_detector.py +++ b/tests/test_detectors/test_event_sequence_detector.py @@ -83,7 +83,7 @@ def test_custom_config_initialization(self): assert detector.name == "CustomInit" assert detector.config.fixed_window_size == 2 assert hasattr(detector, "persistency") - assert isinstance(detector.persistency.event_struct.events_data, dict) + assert isinstance(detector.persistency.event_struct.data, dict) class TestEventSequenceDetectorTraining: diff --git a/tests/test_detectors/test_new_event_detector.py b/tests/test_detectors/test_new_event_detector.py index c618e681..333ddecc 100644 --- a/tests/test_detectors/test_new_event_detector.py +++ b/tests/test_detectors/test_new_event_detector.py @@ -65,7 +65,7 @@ def test_custom_config_initialization(self): assert detector.name == "CustomInit" assert hasattr(detector, 'persistency') - assert isinstance(detector.persistency.event_struct.events_data, dict) + assert isinstance(detector.persistency.event_struct.data, dict) class TestNewEventDetectorTraining: diff --git a/tests/test_detectors/test_new_value_combo_detector.py b/tests/test_detectors/test_new_value_combo_detector.py index d19c9d45..860ec8f2 100644 --- a/tests/test_detectors/test_new_value_combo_detector.py +++ b/tests/test_detectors/test_new_value_combo_detector.py @@ -101,7 +101,7 @@ def test_train_multiple_values(self): detector.train(parser_data) # Only event 1 should be tracked (based on events config) - assert len(detector.persistency.event_struct.events_data) == 1 + assert len(detector.persistency.event_struct.data) == 1 class TestNewValueComboDetectorDetection: diff --git a/tests/test_detectors/test_new_value_detector.py b/tests/test_detectors/test_new_value_detector.py index e061b9f2..b9a1f2f6 100644 --- a/tests/test_detectors/test_new_value_detector.py +++ b/tests/test_detectors/test_new_value_detector.py @@ -83,7 +83,7 @@ def test_custom_config_initialization(self): assert detector.name == "CustomInit" assert hasattr(detector, 'persistency') - assert isinstance(detector.persistency.event_struct.events_data, dict) + assert isinstance(detector.persistency.event_struct.data, dict) class TestNewValueDetectorTraining: @@ -109,7 +109,7 @@ def test_train_multiple_values(self): detector.train(parser_data) # Only event 1 should be tracked (based on events config) - assert len(detector.persistency.event_struct.events_data) == 1 + assert len(detector.persistency.event_struct.data) == 1 event_data = detector.persistency.get_event_data(1) assert event_data is not None # Check the level values diff --git a/tests/test_detectors/test_value_range_detector.py b/tests/test_detectors/test_value_range_detector.py index 34026971..e9728e17 100644 --- a/tests/test_detectors/test_value_range_detector.py +++ b/tests/test_detectors/test_value_range_detector.py @@ -85,7 +85,7 @@ def test_custom_config_initialization(self): assert detector.name == "CustomInit" assert hasattr(detector, 'persistency') - assert isinstance(detector.persistency.event_struct.events_data, dict) + assert isinstance(detector.persistency.event_struct.data, dict) def test_add_value_updates_tracker(self): """add_value (now a method) applies range semantics to a tracker.""" @@ -162,7 +162,7 @@ def test_train_multiple_values(self): detector.train(parser_data) # Only event 1 should be tracked (based on events config) - assert len(detector.persistency.event_struct.events_data) == 1 + assert len(detector.persistency.event_struct.data) == 1 event_data = detector.persistency.get_event_data(1) assert event_data is not None # Check the variable at position 1 (named "test") diff --git a/tests/test_persistency/test_persistency.py b/tests/test_persistency/test_persistency.py index ecf77dbf..db957c23 100644 --- a/tests/test_persistency/test_persistency.py +++ b/tests/test_persistency/test_persistency.py @@ -49,7 +49,7 @@ def test_initialization_with_pandas_backend(self): """Test initialization with EventDataFrame backend.""" persistency = EventPersistency(event_data_class=EventDataFrame) assert persistency is not None - assert persistency.event_struct.event_data_class == EventDataFrame + assert persistency.event_struct.data_class == EventDataFrame def test_initialization_with_polars_backend(self): """Test initialization with ChunkedEventDataFrame backend.""" @@ -58,7 +58,7 @@ def test_initialization_with_polars_backend(self): event_data_kwargs={"max_rows": 100}, ) assert persistency is not None - assert persistency.event_struct.event_data_class == ChunkedEventDataFrame + assert persistency.event_struct.data_class == ChunkedEventDataFrame def test_initialization_with_tracker_backend(self): """Test initialization with EventVariableTrackerData backend.""" @@ -67,7 +67,7 @@ def test_initialization_with_tracker_backend(self): event_data_kwargs={"tracker_type": SingleStabilityTracker}, ) assert persistency is not None - assert persistency.event_struct.event_data_class == EventTracker + assert persistency.event_struct.data_class == EventTracker def test_ingest_single_event(self): """Test ingesting a single event.""" @@ -366,7 +366,7 @@ def test_tracker_backend_full_workflow(self): ) # Verify tracker functionality - data_structure = persistency.event_struct.events_data["E001"] + data_structure = persistency.event_struct.data["E001"] assert isinstance(data_structure, EventTracker) def test_mixed_event_ids_and_templates(self): diff --git a/tests/test_persistency/test_persistency_saver.py b/tests/test_persistency/test_persistency_saver.py index 89dde140..fe8d6932 100644 --- a/tests/test_persistency/test_persistency_saver.py +++ b/tests/test_persistency/test_persistency_saver.py @@ -150,7 +150,7 @@ def test_load_restores_event_data_class(self): # Start with a different class to verify it gets overwritten p2 = EventPersistency(event_data_class=EventStabilityTracker) PersistencySaver(p2, PersistencySaverConfig(path="memory://test/state")).load() - assert p2.event_struct.event_data_class is EventDataFrame + assert p2.event_struct.data_class is EventDataFrame def test_load_clears_stale_events_data(self): """Loading into a non-empty EP must replace, not merge, events_data.""" @@ -177,7 +177,7 @@ def test_load_restores_event_data_kwargs(self): p2 = EventPersistency(event_data_class=ChunkedEventDataFrame) # no kwargs PersistencySaver(p2, PersistencySaverConfig(path="memory://kwargs_test/state")).load() - assert p2.event_struct.event_data_kwargs == {"max_rows": 500} + assert p2.event_struct.data_kwargs == {"max_rows": 500} class TestPersistencySaverTriggers: @@ -456,7 +456,7 @@ def test_load_restores_event_data_class(self): standalone_save(p, "memory://standalone_load3/state") p2 = EventPersistency(event_data_class=EventStabilityTracker) standalone_load(p2, "memory://standalone_load3/state") - assert p2.event_struct.event_data_class is EventDataFrame + assert p2.event_struct.data_class is EventDataFrame def test_load_raises_when_missing(self): p = EventPersistency(event_data_class=EventDataFrame) @@ -506,7 +506,7 @@ def test_bytes_roundtrip_restores_event_data_class(self): data = standalone_save(p) p2 = EventPersistency(event_data_class=EventStabilityTracker) standalone_load(p2, data) - assert p2.event_struct.event_data_class is EventDataFrame + assert p2.event_struct.data_class is EventDataFrame class TestPersistencySaverThreadSafety: diff --git a/tests/test_persistency/test_slope_stability.py b/tests/test_persistency/test_slope_stability.py index a48698c2..e80e987c 100644 --- a/tests/test_persistency/test_slope_stability.py +++ b/tests/test_persistency/test_slope_stability.py @@ -517,21 +517,21 @@ def test_block_reaches_per_variable_trackers(self): # trained persistency is read by _check_variable, which never calls # classify(), so it never receives classification kwargs at all. default = CharsetDetector(config=CharsetDetectorConfig()) - assert default.persistency.event_struct.event_data_kwargs.get("classification") is None + assert default.persistency.event_struct.data_kwargs.get("classification") is None configured = CharsetDetector(config=CharsetDetectorConfig()) configured.config.auto_config_params.classification = ClassificationMethods( index=True, slope_index=True ) rebuilt = CharsetDetector(config=configured.config.to_dict(method_id="CharsetDetector")) - block = rebuilt.auto_conf_persistency.event_struct.event_data_kwargs["classification"] + block = rebuilt.auto_conf_persistency.event_struct.data_kwargs["classification"] assert ClassificationMethods(**block).enabled == ("index", "slope_index") def test_default_block_is_not_forwarded(self): """Forwarding the default would be noise; the tracker already has it.""" default = CharsetDetector(config=CharsetDetectorConfig()) - assert "classification" not in (default.auto_conf_persistency.event_struct.event_data_kwargs or {}) + assert "classification" not in (default.auto_conf_persistency.event_struct.data_kwargs or {}) def test_config_field_round_trips(self): detector = CharsetDetector(config=CharsetDetectorConfig()) @@ -567,7 +567,7 @@ def test_index_axis_methods_pull_in_no_timestamp_requirement(self): index=True, slope_index=True ) rebuilt = CharsetDetector(config=detector.config.to_dict(method_id="CharsetDetector")) - assert "classification" not in (rebuilt.persistency.event_struct.event_data_kwargs or {}) + assert "classification" not in (rebuilt.persistency.event_struct.data_kwargs or {}) tracker = SingleStabilityTracker( classification=ClassificationMethods(index=True, slope_index=True) @@ -612,7 +612,7 @@ def test_slope_threshold_reaches_the_classifier(): ), ) persistency = detector.auto_conf_persistency - tracker = persistency.event_struct.event_data_class(**persistency.event_struct.event_data_kwargs) + tracker = persistency.event_struct.data_class(**persistency.event_struct.data_kwargs) single = tracker.single_tracker_type() assert single.classification.enabled == ("index", "slope_index") assert single.stability_classifier.classification.slope_threshold == -0.25 diff --git a/tests/test_persistency/test_time_dependent_stability.py b/tests/test_persistency/test_time_dependent_stability.py index e9f7880c..f79ab121 100644 --- a/tests/test_persistency/test_time_dependent_stability.py +++ b/tests/test_persistency/test_time_dependent_stability.py @@ -483,14 +483,14 @@ def test_flag_reaches_per_variable_trackers(self): # trained persistency is read by _check_variable, which never calls # classify(), so it never receives classification kwargs at all. detector = CharsetDetector(config=CharsetDetectorConfig()) - assert detector.persistency.event_struct.event_data_kwargs.get("classification") is None + assert detector.persistency.event_struct.data_kwargs.get("classification") is None configured = CharsetDetector(config=CharsetDetectorConfig()) configured.config.auto_config_params.classification = ClassificationMethods( index=False, time=True ) rebuilt = CharsetDetector(config=configured.config.to_dict(method_id="CharsetDetector")) - block = rebuilt.auto_conf_persistency.event_struct.event_data_kwargs["classification"] + block = rebuilt.auto_conf_persistency.event_struct.data_kwargs["classification"] assert ClassificationMethods(**block).enabled == ("time",) def test_config_fields_round_trip(self): @@ -663,7 +663,7 @@ def test_config_accepts_both_and_reaches_trackers(self): rebuilt = CharsetDetector( config=configured.config.to_dict(method_id="CharsetDetector") ) - block = rebuilt.auto_conf_persistency.event_struct.event_data_kwargs["classification"] + block = rebuilt.auto_conf_persistency.event_struct.data_kwargs["classification"] assert ClassificationMethods(**block).enabled == ("index", "time") def test_event_tracker_propagates_both(self): From 36fa6e45e766418f22162972add857076bfff876 Mon Sep 17 00:00:00 2001 From: "angre.garcia-gomez@ait.ac.at" Date: Fri, 11 Sep 2026 16:15:34 +0200 Subject: [PATCH 04/28] move responsabilities to event structure --- .../utils/persistency/event_persistency.py | 43 +++++++++++++------ 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/src/detectmatelibrary/utils/persistency/event_persistency.py b/src/detectmatelibrary/utils/persistency/event_persistency.py index 010f235b..cc074239 100644 --- a/src/detectmatelibrary/utils/persistency/event_persistency.py +++ b/src/detectmatelibrary/utils/persistency/event_persistency.py @@ -38,6 +38,30 @@ def __init__( self.data_kwargs = event_data_kwargs or {} self.templates: Dict[int | str, str] = {} + def __contains__(self, event_id: int | str) -> bool: + return event_id in self.data + + def __getitem__(self, event_id: int | str) -> EventDataStructure | None: + return self.data.get(event_id, None) + + def update_data_structure( + self, + event_id: int | str, + variables: dict[str, list[Any]], + template: str, + timestamp: float | None + ) -> None: + self.templates[event_id] = template + + if event_id not in self: + self.data[event_id] = self.data_class(**self.data_kwargs) + + data = self[event_id].to_data(variables) # type: ignore + self[event_id].add_data(data, timestamp=timestamp) # type: ignore + + def get_template(self, event_id: int | str) -> str | None: + return self.templates.get(event_id, None) + class EventPersistencyBase: """Event Persistency without lock protection.""" @@ -77,18 +101,12 @@ def ingest_event( self._events_since_save += 1 self.events_seen.add(event_id) if variables or named_variables: - self.event_struct.templates[event_id] = event_template all_variables = get_all_variables( variables, named_variables, variable_blacklist=self.variable_blacklist ) - - data_structure = self.event_struct.data.get(event_id) - if data_structure is None: - data_structure = self.event_struct.data_class(**self.event_struct.data_kwargs) - self.event_struct.data[event_id] = data_structure - - data = data_structure.to_data(all_variables) - data_structure.add_data(data, timestamp=timestamp) + self.event_struct.update_data_structure( + event_id, variables=all_variables, template=event_template, timestamp=timestamp + ) @property def events_since_save(self) -> int: @@ -106,8 +124,7 @@ def get_events_seen(self) -> set[int | str]: def get_event_data(self, event_id: int | str) -> Any | None: """Retrieve the data for a specific event ID.""" - data_structure = self.event_struct.data.get(event_id) - return data_structure.get_data() if data_structure is not None else None + return d_struct.get_data() if (d_struct := self.event_struct[event_id]) is not None else None def get_events_data(self) -> Dict[int | str, EventDataStructure]: """Retrieve the events data that is currently stored. @@ -131,14 +148,14 @@ def get_events_data(self) -> Dict[int | str, EventDataStructure]: def get_event_template(self, event_id: int | str) -> str | None: """Retrieve the template for a specific event ID.""" - return self.event_struct.templates.get(event_id) + return self.event_struct.get_template(event_id) def get_event_templates(self) -> Dict[int | str, str]: """Retrieve all event templates.""" return self.event_struct.templates def __getitem__(self, event_id: int | str) -> EventDataStructure | None: - return self.event_struct.data.get(event_id) + return self.event_struct[event_id] def __repr__(self) -> str: return ( From f9ebd3c29bda74729a583332657db8ad3c1a8f5d Mon Sep 17 00:00:00 2001 From: "angre.garcia-gomez@ait.ac.at" Date: Fri, 11 Sep 2026 16:33:35 +0200 Subject: [PATCH 05/28] event struct ready --- .../utils/persistency/event_persistency.py | 29 ++++++------------- .../utils/persistency/persistency_saver.py | 2 +- .../test_bigram_frequency_detector.py | 2 +- tests/test_detectors/test_charset_detector.py | 2 +- .../test_detectors/test_new_event_detector.py | 2 +- .../test_new_value_combo_detector.py | 2 +- .../test_detectors/test_new_value_detector.py | 2 +- .../test_value_range_detector.py | 2 +- tests/test_persistency/test_persistency.py | 6 ++-- 9 files changed, 19 insertions(+), 30 deletions(-) diff --git a/src/detectmatelibrary/utils/persistency/event_persistency.py b/src/detectmatelibrary/utils/persistency/event_persistency.py index cc074239..3dfbb50e 100644 --- a/src/detectmatelibrary/utils/persistency/event_persistency.py +++ b/src/detectmatelibrary/utils/persistency/event_persistency.py @@ -28,6 +28,7 @@ def get_all_variables( class EventStruct: + """Event structure of the Event Persistency.""" def __init__( self, event_data_class: Type[EventDataStructure], @@ -101,9 +102,7 @@ def ingest_event( self._events_since_save += 1 self.events_seen.add(event_id) if variables or named_variables: - all_variables = get_all_variables( - variables, named_variables, variable_blacklist=self.variable_blacklist - ) + all_variables = self.get_all_variables(variables, named_variables) self.event_struct.update_data_structure( event_id, variables=all_variables, template=event_template, timestamp=timestamp ) @@ -127,23 +126,7 @@ def get_event_data(self, event_id: int | str) -> Any | None: return d_struct.get_data() if (d_struct := self.event_struct[event_id]) is not None else None def get_events_data(self) -> Dict[int | str, EventDataStructure]: - """Retrieve the events data that is currently stored. - - Returns: - A dictionary mapping event IDs to their corresponding EventDataStructure instances. - - Example: - { - 1: EventTracker(data={ - 'var_0': SingleTracker(...), - 'var_1': SingleTracker(...), - }), - 2: EventTracker(data={ - 'var_0': SingleTracker(...) - }), - ... - } - """ + """Retrieve the events data that is currently stored.""" return self.event_struct.data def get_event_template(self, event_id: int | str) -> str | None: @@ -154,6 +137,9 @@ def get_event_templates(self) -> Dict[int | str, str]: """Retrieve all event templates.""" return self.event_struct.templates + def get_class(self) -> Type[EventDataStructure]: + return self.event_struct.data_class + def __getitem__(self, event_id: int | str) -> EventDataStructure | None: return self.event_struct[event_id] @@ -163,6 +149,9 @@ def __repr__(self) -> str: f"keys={list(self.event_struct.data.keys())})" ) + def __len__(self) -> int: + return len(self.get_events_data()) + class EventPersistency(EventPersistencyBase): """ diff --git a/src/detectmatelibrary/utils/persistency/persistency_saver.py b/src/detectmatelibrary/utils/persistency/persistency_saver.py index 8cf3f6d4..b0239f45 100644 --- a/src/detectmatelibrary/utils/persistency/persistency_saver.py +++ b/src/detectmatelibrary/utils/persistency/persistency_saver.py @@ -96,7 +96,7 @@ def _serialize(ep: EventPersistency) -> dict[str, bytes]: metadata = { "version": 1, "saved_at": datetime.now(timezone.utc).isoformat(), - "events_seen": list(ep.events_seen), + "events_seen": list(ep.get_events_seen()), "event_templates": {str(k): v for k, v in ep.event_struct.templates.items()}, "event_backends": event_backends, "event_extensions": event_extensions, diff --git a/tests/test_detectors/test_bigram_frequency_detector.py b/tests/test_detectors/test_bigram_frequency_detector.py index 758e11c5..b852be4d 100644 --- a/tests/test_detectors/test_bigram_frequency_detector.py +++ b/tests/test_detectors/test_bigram_frequency_detector.py @@ -133,7 +133,7 @@ def test_train_multiple_values(self): detector.train(parser_data) # Only event 1 should be tracked (based on events config) - assert len(detector.persistency.event_struct.data) == 1 + assert len(detector.persistency) == 1 event_data = detector.persistency.get_event_data(1) assert event_data is not None # Check the level values diff --git a/tests/test_detectors/test_charset_detector.py b/tests/test_detectors/test_charset_detector.py index 2a90531f..f3afa749 100644 --- a/tests/test_detectors/test_charset_detector.py +++ b/tests/test_detectors/test_charset_detector.py @@ -134,7 +134,7 @@ def test_train_multiple_values(self): detector.train(parser_data) # Only event 1 should be tracked (based on events config) - assert len(detector.persistency.event_struct.data) == 1 + assert len(detector.persistency) == 1 event_data = detector.persistency.get_event_data(1) assert event_data is not None # With expand_value=True, unique_set contains individual characters diff --git a/tests/test_detectors/test_new_event_detector.py b/tests/test_detectors/test_new_event_detector.py index 333ddecc..b47083fa 100644 --- a/tests/test_detectors/test_new_event_detector.py +++ b/tests/test_detectors/test_new_event_detector.py @@ -89,7 +89,7 @@ def test_train_multiple_event_ids(self): }) detector.train(parser_data) - assert len(detector.persistency.events_seen) == len(event_ids) + assert len(detector.persistency.get_events_seen()) == len(event_ids) event_seen = detector.persistency.get_events_seen() assert event_seen == event_ids diff --git a/tests/test_detectors/test_new_value_combo_detector.py b/tests/test_detectors/test_new_value_combo_detector.py index 860ec8f2..10a00345 100644 --- a/tests/test_detectors/test_new_value_combo_detector.py +++ b/tests/test_detectors/test_new_value_combo_detector.py @@ -101,7 +101,7 @@ def test_train_multiple_values(self): detector.train(parser_data) # Only event 1 should be tracked (based on events config) - assert len(detector.persistency.event_struct.data) == 1 + assert len(detector.persistency) == 1 class TestNewValueComboDetectorDetection: diff --git a/tests/test_detectors/test_new_value_detector.py b/tests/test_detectors/test_new_value_detector.py index b9a1f2f6..784f1919 100644 --- a/tests/test_detectors/test_new_value_detector.py +++ b/tests/test_detectors/test_new_value_detector.py @@ -109,7 +109,7 @@ def test_train_multiple_values(self): detector.train(parser_data) # Only event 1 should be tracked (based on events config) - assert len(detector.persistency.event_struct.data) == 1 + assert len(detector.persistency) == 1 event_data = detector.persistency.get_event_data(1) assert event_data is not None # Check the level values diff --git a/tests/test_detectors/test_value_range_detector.py b/tests/test_detectors/test_value_range_detector.py index e9728e17..6a1ec0ac 100644 --- a/tests/test_detectors/test_value_range_detector.py +++ b/tests/test_detectors/test_value_range_detector.py @@ -162,7 +162,7 @@ def test_train_multiple_values(self): detector.train(parser_data) # Only event 1 should be tracked (based on events config) - assert len(detector.persistency.event_struct.data) == 1 + assert len(detector.persistency) == 1 event_data = detector.persistency.get_event_data(1) assert event_data is not None # Check the variable at position 1 (named "test") diff --git a/tests/test_persistency/test_persistency.py b/tests/test_persistency/test_persistency.py index db957c23..40290fdb 100644 --- a/tests/test_persistency/test_persistency.py +++ b/tests/test_persistency/test_persistency.py @@ -49,7 +49,7 @@ def test_initialization_with_pandas_backend(self): """Test initialization with EventDataFrame backend.""" persistency = EventPersistency(event_data_class=EventDataFrame) assert persistency is not None - assert persistency.event_struct.data_class == EventDataFrame + assert persistency.get_class() == EventDataFrame def test_initialization_with_polars_backend(self): """Test initialization with ChunkedEventDataFrame backend.""" @@ -58,7 +58,7 @@ def test_initialization_with_polars_backend(self): event_data_kwargs={"max_rows": 100}, ) assert persistency is not None - assert persistency.event_struct.data_class == ChunkedEventDataFrame + assert persistency.get_class() == ChunkedEventDataFrame def test_initialization_with_tracker_backend(self): """Test initialization with EventVariableTrackerData backend.""" @@ -67,7 +67,7 @@ def test_initialization_with_tracker_backend(self): event_data_kwargs={"tracker_type": SingleStabilityTracker}, ) assert persistency is not None - assert persistency.event_struct.data_class == EventTracker + assert persistency.get_class() == EventTracker def test_ingest_single_event(self): """Test ingesting a single event.""" From 8694e734d4858e706decd3dc822169ad1868cca4 Mon Sep 17 00:00:00 2001 From: "angre.garcia-gomez@ait.ac.at" Date: Fri, 11 Sep 2026 16:52:42 +0200 Subject: [PATCH 06/28] minor refactor --- .../utils/persistency/event_data_structures/base.py | 10 +++++++++- .../dataframes/chunked_event_dataframe.py | 2 +- .../dataframes/event_dataframe.py | 2 +- .../trackers/base/event_tracker.py | 2 +- .../utils/persistency/event_persistency.py | 4 +--- 5 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/detectmatelibrary/utils/persistency/event_data_structures/base.py b/src/detectmatelibrary/utils/persistency/event_data_structures/base.py index 31eb768a..657fca9f 100644 --- a/src/detectmatelibrary/utils/persistency/event_data_structures/base.py +++ b/src/detectmatelibrary/utils/persistency/event_data_structures/base.py @@ -11,7 +11,15 @@ class EventDataStructure(ABC): template: str = "" @abstractmethod - def add_data(self, data_object: Any, timestamp: float | None = None) -> None: ... + def _add_data(self, data_object: Any, timestamp: float | None = None) -> None: ... + + def add_data( + self, data_object: Any, timestamp: float | None = None, do_preprocess: bool = False + ) -> None: + self._add_data( + self.to_data(data_object) if do_preprocess else data_object, + timestamp=timestamp + ) @abstractmethod def get_data(self) -> Any: ... diff --git a/src/detectmatelibrary/utils/persistency/event_data_structures/dataframes/chunked_event_dataframe.py b/src/detectmatelibrary/utils/persistency/event_data_structures/dataframes/chunked_event_dataframe.py index fac9c73c..21a03b0f 100644 --- a/src/detectmatelibrary/utils/persistency/event_data_structures/dataframes/chunked_event_dataframe.py +++ b/src/detectmatelibrary/utils/persistency/event_data_structures/dataframes/chunked_event_dataframe.py @@ -24,7 +24,7 @@ class ChunkedEventDataFrame(EventDataStructure): chunks: list[pl.DataFrame] = field(default_factory=list) _rows: int = 0 - def add_data(self, data: pl.DataFrame, timestamp: float | None = None) -> None: + def _add_data(self, data: pl.DataFrame, timestamp: float | None = None) -> None: if data.height == 0: return self.chunks.append(data) diff --git a/src/detectmatelibrary/utils/persistency/event_data_structures/dataframes/event_dataframe.py b/src/detectmatelibrary/utils/persistency/event_data_structures/dataframes/event_dataframe.py index 5519393e..1ebdbf0f 100644 --- a/src/detectmatelibrary/utils/persistency/event_data_structures/dataframes/event_dataframe.py +++ b/src/detectmatelibrary/utils/persistency/event_data_structures/dataframes/event_dataframe.py @@ -17,7 +17,7 @@ class EventDataFrame(EventDataStructure): """ data: pd.DataFrame = field(default_factory=pd.DataFrame) - def add_data(self, data: pd.DataFrame, timestamp: float | None = None) -> None: + def _add_data(self, data: pd.DataFrame, timestamp: float | None = None) -> None: if len(self.data) > 0: self.data = pd.concat([self.data, data], ignore_index=True) else: diff --git a/src/detectmatelibrary/utils/persistency/event_data_structures/trackers/base/event_tracker.py b/src/detectmatelibrary/utils/persistency/event_data_structures/trackers/base/event_tracker.py index 55496c2f..b7c151d5 100644 --- a/src/detectmatelibrary/utils/persistency/event_data_structures/trackers/base/event_tracker.py +++ b/src/detectmatelibrary/utils/persistency/event_data_structures/trackers/base/event_tracker.py @@ -27,7 +27,7 @@ def __init__( self.converter_function = converter_function self.multi_tracker = self.multi_tracker_type(single_tracker_type=self.single_tracker_type) - def add_data(self, data_object: Any, timestamp: float | None = None) -> None: + def _add_data(self, data_object: Any, timestamp: float | None = None) -> None: """Add data to the variable trackers.""" self.multi_tracker.add_data(data_object, timestamp=timestamp) diff --git a/src/detectmatelibrary/utils/persistency/event_persistency.py b/src/detectmatelibrary/utils/persistency/event_persistency.py index 3dfbb50e..0ec56140 100644 --- a/src/detectmatelibrary/utils/persistency/event_persistency.py +++ b/src/detectmatelibrary/utils/persistency/event_persistency.py @@ -56,9 +56,7 @@ def update_data_structure( if event_id not in self: self.data[event_id] = self.data_class(**self.data_kwargs) - - data = self[event_id].to_data(variables) # type: ignore - self[event_id].add_data(data, timestamp=timestamp) # type: ignore + self[event_id].add_data(variables, timestamp=timestamp, do_preprocess=True) # type: ignore def get_template(self, event_id: int | str) -> str | None: return self.templates.get(event_id, None) From f018cac59e3398ca285bd7092981a20c6b89caa9 Mon Sep 17 00:00:00 2001 From: "angre.garcia-gomez@ait.ac.at" Date: Fri, 11 Sep 2026 17:38:48 +0200 Subject: [PATCH 07/28] persistency aggregation now works wiht EventDataFrame --- .../dataframes/event_dataframe.py | 3 ++ .../utils/persistency/event_persistency.py | 38 +++++++++++++++++-- tests/test_persistency/test_persistency.py | 33 ++++++++++++++++ 3 files changed, 70 insertions(+), 4 deletions(-) diff --git a/src/detectmatelibrary/utils/persistency/event_data_structures/dataframes/event_dataframe.py b/src/detectmatelibrary/utils/persistency/event_data_structures/dataframes/event_dataframe.py index 1ebdbf0f..8adc60d1 100644 --- a/src/detectmatelibrary/utils/persistency/event_data_structures/dataframes/event_dataframe.py +++ b/src/detectmatelibrary/utils/persistency/event_data_structures/dataframes/event_dataframe.py @@ -39,6 +39,9 @@ def dump(self) -> bytes: self.data.to_parquet(buf, engine="pyarrow", index=False) return buf.getvalue() + def as_dict(self) -> list[dict[int | str, Any]]: + return self.get_data().to_dict("records") # type: ignore + @classmethod def load(cls, data: bytes, **kwargs: Any) -> "EventDataFrame": """Restore DataFrame from Parquet bytes. diff --git a/src/detectmatelibrary/utils/persistency/event_persistency.py b/src/detectmatelibrary/utils/persistency/event_persistency.py index 0ec56140..f5fed8e6 100644 --- a/src/detectmatelibrary/utils/persistency/event_persistency.py +++ b/src/detectmatelibrary/utils/persistency/event_persistency.py @@ -1,10 +1,9 @@ import threading -from typing import Any, Callable, Dict, List, Optional, Type +from typing import Any, Callable, Dict, List, Optional, Type, Self from .event_data_structures.base import EventDataStructure -# -------- Generic persistency -------- def get_all_variables( variables: list[Any], log_format_variables: Dict[str, Any], @@ -45,6 +44,9 @@ def __contains__(self, event_id: int | str) -> bool: def __getitem__(self, event_id: int | str) -> EventDataStructure | None: return self.data.get(event_id, None) + def get_events(self) -> list[int | str]: + return list(self.data.keys()) + def update_data_structure( self, event_id: int | str, @@ -61,6 +63,18 @@ def update_data_structure( def get_template(self, event_id: int | str) -> str | None: return self.templates.get(event_id, None) + def __len__(self) -> int: + return len(self.data) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, EventStruct) or len(self) != len(other): + return False + for elem1, elem2 in zip(self.data.values(), other.data.values()): + if elem1.as_dict() != elem2.as_dict(): # type: ignore + return False + + return True + class EventPersistencyBase: """Event Persistency without lock protection.""" @@ -148,14 +162,19 @@ def __repr__(self) -> str: ) def __len__(self) -> int: - return len(self.get_events_data()) + return len(self.event_struct) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, EventPersistencyBase) or len(self) != len(other): + return False + return self.events_seen == other.events_seen and self.event_struct == other.event_struct class EventPersistency(EventPersistencyBase): """ Event-based persistency orchestrator: - manages multiple EventDataStructure instances, one per event ID - - doesn't know retention strategy + - doesn't know retention strategyvalue - only delegates to EventDataStructure Args: @@ -207,3 +226,14 @@ def ingest_event( def register_on_ingest(self, callback: Callable[[], None]) -> None: """Register a callback invoked after every ingest_event call.""" self._on_ingest_callbacks.append(callback) + + def combine(self, other: "EventPersistency") -> Self: + """Combine two Event persistency.""" + for event in other.event_struct.get_events(): + templates = other.event_struct.get_template(event) + for vars in other.event_struct[event].as_dict(): # type: ignore + self.ingest_event( + event_id=event, event_template=templates, named_variables=vars # type: ignore + ) + + return self diff --git a/tests/test_persistency/test_persistency.py b/tests/test_persistency/test_persistency.py index 40290fdb..77dc7ede 100644 --- a/tests/test_persistency/test_persistency.py +++ b/tests/test_persistency/test_persistency.py @@ -484,3 +484,36 @@ def test_reset_events_since_save(self): p.ingest_event(**SAMPLE_EVENT_2) p.reset_events_since_save() assert p._events_since_save == 0 + + +class TestAggregationUsage: + def test_equals(self) -> None: + persistency = EventPersistency(event_data_class=EventDataFrame) + persistency.ingest_event(**SAMPLE_EVENT_1) + persistency.ingest_event(**SAMPLE_EVENT_2) + + persistency2 = EventPersistency(event_data_class=EventDataFrame) + persistency2.ingest_event(**SAMPLE_EVENT_1) + persistency2.ingest_event(**SAMPLE_EVENT_2) + + persistency3 = EventPersistency(event_data_class=EventDataFrame) + persistency3.ingest_event(**SAMPLE_EVENT_2) + + assert persistency == persistency2 + assert persistency != persistency3 + + def test_combine_DataFrame(self) -> None: + persistency = EventPersistency(event_data_class=EventDataFrame) + persistency.ingest_event(**SAMPLE_EVENT_1) + + persistency2 = EventPersistency(event_data_class=EventDataFrame) + persistency2.ingest_event(**SAMPLE_EVENT_3) + persistency2.ingest_event(**SAMPLE_EVENT_2) + + persistency3 = EventPersistency(event_data_class=EventDataFrame) + persistency3.ingest_event(**SAMPLE_EVENT_1) + persistency3.ingest_event(**SAMPLE_EVENT_3) + persistency3.ingest_event(**SAMPLE_EVENT_2) + + persistency = persistency.combine(persistency2) + assert persistency == persistency3 From bc8ef2dbb9bfa1f3bf8acad65778325e8f01dc2e Mon Sep 17 00:00:00 2001 From: "angre.garcia-gomez@ait.ac.at" Date: Fri, 11 Sep 2026 17:43:37 +0200 Subject: [PATCH 08/28] add compatibility wiht chunked event --- .../persistency/event_data_structures/base.py | 3 +++ .../dataframes/chunked_event_dataframe.py | 3 +++ .../utils/persistency/event_persistency.py | 2 +- tests/test_persistency/test_persistency.py | 16 ++++++++++++++++ 4 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/detectmatelibrary/utils/persistency/event_data_structures/base.py b/src/detectmatelibrary/utils/persistency/event_data_structures/base.py index 657fca9f..254e56e6 100644 --- a/src/detectmatelibrary/utils/persistency/event_data_structures/base.py +++ b/src/detectmatelibrary/utils/persistency/event_data_structures/base.py @@ -32,6 +32,9 @@ def to_data(self, raw_data: Any) -> Any: """Convert raw data into the appropriate data format for storage.""" pass + def as_dict(self) -> list[dict[int | str, Any]]: + return [] + @abstractmethod def dump(self) -> bytes: """Serialize full state to bytes. diff --git a/src/detectmatelibrary/utils/persistency/event_data_structures/dataframes/chunked_event_dataframe.py b/src/detectmatelibrary/utils/persistency/event_data_structures/dataframes/chunked_event_dataframe.py index 21a03b0f..ec875edf 100644 --- a/src/detectmatelibrary/utils/persistency/event_data_structures/dataframes/chunked_event_dataframe.py +++ b/src/detectmatelibrary/utils/persistency/event_data_structures/dataframes/chunked_event_dataframe.py @@ -69,6 +69,9 @@ def get_data(self) -> pl.DataFrame: return self.chunks[0] return pl.concat(self.chunks, how="vertical", rechunk=False) + def as_dict(self) -> list[dict[int | str, Any]]: + return self.get_data().to_pandas().to_dict("records") # type: ignore + def get_variables(self) -> Any: if not self.chunks: return [] diff --git a/src/detectmatelibrary/utils/persistency/event_persistency.py b/src/detectmatelibrary/utils/persistency/event_persistency.py index f5fed8e6..b739aaf3 100644 --- a/src/detectmatelibrary/utils/persistency/event_persistency.py +++ b/src/detectmatelibrary/utils/persistency/event_persistency.py @@ -70,7 +70,7 @@ def __eq__(self, other: object) -> bool: if not isinstance(other, EventStruct) or len(self) != len(other): return False for elem1, elem2 in zip(self.data.values(), other.data.values()): - if elem1.as_dict() != elem2.as_dict(): # type: ignore + if elem1.as_dict() != elem2.as_dict(): return False return True diff --git a/tests/test_persistency/test_persistency.py b/tests/test_persistency/test_persistency.py index 77dc7ede..66a49bc4 100644 --- a/tests/test_persistency/test_persistency.py +++ b/tests/test_persistency/test_persistency.py @@ -517,3 +517,19 @@ def test_combine_DataFrame(self) -> None: persistency = persistency.combine(persistency2) assert persistency == persistency3 + + def test_combine_mix(self) -> None: + persistency = EventPersistency(event_data_class=ChunkedEventDataFrame) + persistency.ingest_event(**SAMPLE_EVENT_1) + + persistency2 = EventPersistency(event_data_class=EventDataFrame) + persistency2.ingest_event(**SAMPLE_EVENT_3) + persistency2.ingest_event(**SAMPLE_EVENT_2) + + persistency3 = EventPersistency(event_data_class=EventDataFrame) + persistency3.ingest_event(**SAMPLE_EVENT_1) + persistency3.ingest_event(**SAMPLE_EVENT_3) + persistency3.ingest_event(**SAMPLE_EVENT_2) + + persistency = persistency.combine(persistency2) + assert persistency == persistency3 From 684f077ade2999f8359e63d75a01d9ec1cecde16 Mon Sep 17 00:00:00 2001 From: "angre.garcia-gomez@ait.ac.at" Date: Fri, 11 Sep 2026 17:50:23 +0200 Subject: [PATCH 09/28] refactor --- .../utils/persistency/basic_persistency.py | 169 +++++++++++++++++ .../utils/persistency/event_persistency.py | 171 +----------------- 2 files changed, 172 insertions(+), 168 deletions(-) create mode 100644 src/detectmatelibrary/utils/persistency/basic_persistency.py diff --git a/src/detectmatelibrary/utils/persistency/basic_persistency.py b/src/detectmatelibrary/utils/persistency/basic_persistency.py new file mode 100644 index 00000000..99ee6562 --- /dev/null +++ b/src/detectmatelibrary/utils/persistency/basic_persistency.py @@ -0,0 +1,169 @@ +from .event_data_structures.base import EventDataStructure + +from typing import Any, Dict, List, Type, Optional + + +def get_all_variables( + variables: list[Any], + log_format_variables: Dict[str, Any], + variable_blacklist: List[str | int], + event_var_prefix: str = "var_", +) -> dict[str, list[Any]]: + """Combine log format variables and event variables into a single + dictionary. + + Schema-friendly by using string column names. + """ + all_vars: dict[str, list[Any]] = { + k: v for k, v in log_format_variables.items() + if k not in variable_blacklist + } + all_vars.update({ + f"{event_var_prefix}{i}": val for i, val in enumerate(variables) + if i not in variable_blacklist + }) + return all_vars + + +class EventStruct: + """Event structure of the Event Persistency.""" + def __init__( + self, + event_data_class: Type[EventDataStructure], + event_data_kwargs: Optional[dict[str, Any]] = None, + ) -> None: + self.data: Dict[int | str, EventDataStructure] = {} + self.data_class = event_data_class + self.data_kwargs = event_data_kwargs or {} + self.templates: Dict[int | str, str] = {} + + def __contains__(self, event_id: int | str) -> bool: + return event_id in self.data + + def __getitem__(self, event_id: int | str) -> EventDataStructure | None: + return self.data.get(event_id, None) + + def get_events(self) -> list[int | str]: + return list(self.data.keys()) + + def update_data_structure( + self, + event_id: int | str, + variables: dict[str, list[Any]], + template: str, + timestamp: float | None + ) -> None: + self.templates[event_id] = template + + if event_id not in self: + self.data[event_id] = self.data_class(**self.data_kwargs) + self[event_id].add_data(variables, timestamp=timestamp, do_preprocess=True) # type: ignore + + def get_template(self, event_id: int | str) -> str | None: + return self.templates.get(event_id, None) + + def __len__(self) -> int: + return len(self.data) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, EventStruct) or len(self) != len(other): + return False + for elem1, elem2 in zip(self.data.values(), other.data.values()): + if elem1.as_dict() != elem2.as_dict(): + return False + + return True + + +class EventPersistencyBase: + """Event Persistency without lock protection.""" + def __init__( + self, + event_data_class: Type[EventDataStructure], + variable_blacklist: Optional[List[str | int]] = ["Content"], + *, + event_data_kwargs: Optional[dict[str, Any]] = None, + ): + self.event_struct = EventStruct( + event_data_class, event_data_kwargs=event_data_kwargs + ) + + self.events_seen: set[int | str] = set() + self.variable_blacklist = variable_blacklist or [] + self._events_since_save: int = 0 + + def get_all_variables( + self, variables: list[Any], log_format_variables: Dict[str, Any], event_var_prefix: str = "var_", + ) -> dict[str, list[Any]]: + return get_all_variables( + variables=variables, + log_format_variables=log_format_variables, + variable_blacklist=self.variable_blacklist, + event_var_prefix=event_var_prefix + ) + + def ingest_event( + self, + event_id: int | str, + event_template: str, + variables: list[Any] = [], + named_variables: Dict[str, Any] = {}, + timestamp: float | None = None, + ) -> None: + self._events_since_save += 1 + self.events_seen.add(event_id) + if variables or named_variables: + all_variables = self.get_all_variables(variables, named_variables) + self.event_struct.update_data_structure( + event_id, variables=all_variables, template=event_template, timestamp=timestamp + ) + + @property + def events_since_save(self) -> int: + """Number of events ingested since the last successful save.""" + return self._events_since_save + + def reset_events_since_save(self) -> None: + """Reset the events-since-save counter after a successful save.""" + self._events_since_save = 0 + + def get_events_seen(self) -> set[int | str]: + """Retrieve all event IDs observed via ingest_event(), regardless of + whether variables were extracted.""" + return self.events_seen + + def get_event_data(self, event_id: int | str) -> Any | None: + """Retrieve the data for a specific event ID.""" + return d_struct.get_data() if (d_struct := self.event_struct[event_id]) is not None else None + + def get_events_data(self) -> Dict[int | str, EventDataStructure]: + """Retrieve the events data that is currently stored.""" + return self.event_struct.data + + def get_event_template(self, event_id: int | str) -> str | None: + """Retrieve the template for a specific event ID.""" + return self.event_struct.get_template(event_id) + + def get_event_templates(self) -> Dict[int | str, str]: + """Retrieve all event templates.""" + return self.event_struct.templates + + def get_class(self) -> Type[EventDataStructure]: + return self.event_struct.data_class + + def __getitem__(self, event_id: int | str) -> EventDataStructure | None: + return self.event_struct[event_id] + + def __repr__(self) -> str: + return ( + f"EventPersistency(num_event_types={len(self.event_struct.data)}, " + f"keys={list(self.event_struct.data.keys())})" + ) + + def __len__(self) -> int: + return len(self.event_struct) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, EventPersistencyBase) or len(self) != len(other): + return False + return self.events_seen == other.events_seen and self.event_struct == other.event_struct diff --git a/src/detectmatelibrary/utils/persistency/event_persistency.py b/src/detectmatelibrary/utils/persistency/event_persistency.py index b739aaf3..3ba56e6e 100644 --- a/src/detectmatelibrary/utils/persistency/event_persistency.py +++ b/src/detectmatelibrary/utils/persistency/event_persistency.py @@ -1,173 +1,8 @@ -import threading -from typing import Any, Callable, Dict, List, Optional, Type, Self - from .event_data_structures.base import EventDataStructure +from .basic_persistency import EventPersistencyBase - -def get_all_variables( - variables: list[Any], - log_format_variables: Dict[str, Any], - variable_blacklist: List[str | int], - event_var_prefix: str = "var_", -) -> dict[str, list[Any]]: - """Combine log format variables and event variables into a single - dictionary. - - Schema-friendly by using string column names. - """ - all_vars: dict[str, list[Any]] = { - k: v for k, v in log_format_variables.items() - if k not in variable_blacklist - } - all_vars.update({ - f"{event_var_prefix}{i}": val for i, val in enumerate(variables) - if i not in variable_blacklist - }) - return all_vars - - -class EventStruct: - """Event structure of the Event Persistency.""" - def __init__( - self, - event_data_class: Type[EventDataStructure], - event_data_kwargs: Optional[dict[str, Any]] = None, - ) -> None: - self.data: Dict[int | str, EventDataStructure] = {} - self.data_class = event_data_class - self.data_kwargs = event_data_kwargs or {} - self.templates: Dict[int | str, str] = {} - - def __contains__(self, event_id: int | str) -> bool: - return event_id in self.data - - def __getitem__(self, event_id: int | str) -> EventDataStructure | None: - return self.data.get(event_id, None) - - def get_events(self) -> list[int | str]: - return list(self.data.keys()) - - def update_data_structure( - self, - event_id: int | str, - variables: dict[str, list[Any]], - template: str, - timestamp: float | None - ) -> None: - self.templates[event_id] = template - - if event_id not in self: - self.data[event_id] = self.data_class(**self.data_kwargs) - self[event_id].add_data(variables, timestamp=timestamp, do_preprocess=True) # type: ignore - - def get_template(self, event_id: int | str) -> str | None: - return self.templates.get(event_id, None) - - def __len__(self) -> int: - return len(self.data) - - def __eq__(self, other: object) -> bool: - if not isinstance(other, EventStruct) or len(self) != len(other): - return False - for elem1, elem2 in zip(self.data.values(), other.data.values()): - if elem1.as_dict() != elem2.as_dict(): - return False - - return True - - -class EventPersistencyBase: - """Event Persistency without lock protection.""" - def __init__( - self, - event_data_class: Type[EventDataStructure], - variable_blacklist: Optional[List[str | int]] = ["Content"], - *, - event_data_kwargs: Optional[dict[str, Any]] = None, - ): - self.event_struct = EventStruct( - event_data_class, event_data_kwargs=event_data_kwargs - ) - - self.events_seen: set[int | str] = set() - self.variable_blacklist = variable_blacklist or [] - self._events_since_save: int = 0 - - def get_all_variables( - self, variables: list[Any], log_format_variables: Dict[str, Any], event_var_prefix: str = "var_", - ) -> dict[str, list[Any]]: - return get_all_variables( - variables=variables, - log_format_variables=log_format_variables, - variable_blacklist=self.variable_blacklist, - event_var_prefix=event_var_prefix - ) - - def ingest_event( - self, - event_id: int | str, - event_template: str, - variables: list[Any] = [], - named_variables: Dict[str, Any] = {}, - timestamp: float | None = None, - ) -> None: - self._events_since_save += 1 - self.events_seen.add(event_id) - if variables or named_variables: - all_variables = self.get_all_variables(variables, named_variables) - self.event_struct.update_data_structure( - event_id, variables=all_variables, template=event_template, timestamp=timestamp - ) - - @property - def events_since_save(self) -> int: - """Number of events ingested since the last successful save.""" - return self._events_since_save - - def reset_events_since_save(self) -> None: - """Reset the events-since-save counter after a successful save.""" - self._events_since_save = 0 - - def get_events_seen(self) -> set[int | str]: - """Retrieve all event IDs observed via ingest_event(), regardless of - whether variables were extracted.""" - return self.events_seen - - def get_event_data(self, event_id: int | str) -> Any | None: - """Retrieve the data for a specific event ID.""" - return d_struct.get_data() if (d_struct := self.event_struct[event_id]) is not None else None - - def get_events_data(self) -> Dict[int | str, EventDataStructure]: - """Retrieve the events data that is currently stored.""" - return self.event_struct.data - - def get_event_template(self, event_id: int | str) -> str | None: - """Retrieve the template for a specific event ID.""" - return self.event_struct.get_template(event_id) - - def get_event_templates(self) -> Dict[int | str, str]: - """Retrieve all event templates.""" - return self.event_struct.templates - - def get_class(self) -> Type[EventDataStructure]: - return self.event_struct.data_class - - def __getitem__(self, event_id: int | str) -> EventDataStructure | None: - return self.event_struct[event_id] - - def __repr__(self) -> str: - return ( - f"EventPersistency(num_event_types={len(self.event_struct.data)}, " - f"keys={list(self.event_struct.data.keys())})" - ) - - def __len__(self) -> int: - return len(self.event_struct) - - def __eq__(self, other: object) -> bool: - if not isinstance(other, EventPersistencyBase) or len(self) != len(other): - return False - return self.events_seen == other.events_seen and self.event_struct == other.event_struct +from typing import Any, Callable, Dict, List, Optional, Type, Self +import threading class EventPersistency(EventPersistencyBase): From 00fd11364ac4cd6f348dfe17a350939d6c7d0e34 Mon Sep 17 00:00:00 2001 From: "angre.garcia-gomez@ait.ac.at" Date: Mon, 14 Sep 2026 10:24:23 +0200 Subject: [PATCH 10/28] minor reorganization --- .../common/variable_detector.py | 149 ++++++++---------- 1 file changed, 66 insertions(+), 83 deletions(-) diff --git a/src/detectmatelibrary/common/variable_detector.py b/src/detectmatelibrary/common/variable_detector.py index 32c41daf..a895f60c 100644 --- a/src/detectmatelibrary/common/variable_detector.py +++ b/src/detectmatelibrary/common/variable_detector.py @@ -7,6 +7,7 @@ AutoConfigParams, CoreDetectorConfig, CoreDetector, + _time_handler ) from detectmatelibrary.utils.persistency.component_interfaces import ( validate_config_coverage @@ -73,21 +74,8 @@ def _strip_auto_config_params(detector_config: Dict[str, Any], method_id: str) - class VariableAutoConfigParams(AutoConfigParams): - """Configure-phase inputs shared by every VariableDetector subclass. - - Read only while `auto_config` is True: stability classification decides - which variables land in the generated `events` block and is never consulted - at detection time. - """ - use_stable_vars: bool = True use_static_vars: bool = True - - # Which stability classification methods decide STABLE, and how their - # verdicts combine. Four independent methods over two primitives and two - # axes; see ClassificationMethods. The two time-axis methods (`time`, - # `slope_time`) need a per-record event time, named here and read from the - # record's logFormatVariables. classification: ClassificationMethods = ClassificationMethods() timestamp_variable: str | None = None timestamp_format: str | None = None # None -> TimeFormatHandler auto-detect @@ -97,61 +85,37 @@ class VariableDetectorConfig(CoreDetectorConfig): auto_config_params: VariableAutoConfigParams = VariableAutoConfigParams() -class VariableDetector(CoreDetector): - """Abstract base for detectors that learn a per-variable model from - configured log variables and flag anomalous values at detection time. - - Subclasses override a small set of hooks: - - ``_check_variable`` (required): the per-variable anomaly test. - - ``_prepare_variables`` (optional): transform variables per stage. - - ``_event_data_kwargs`` / ``_auto_conf_kwargs`` (optional): tracker - construction kwargs. - - ``_description`` / ``_alert_key`` (optional): output formatting. - - The five lifecycle methods (train/detect/configure/post_train/ - set_configuration) live here and are shared by all subclasses. - """ - - def __init__(self, name: str, config: VariableDetectorConfig) -> None: - super().__init__(name=name, buffer_mode=BufferMode.NO_BUF, config=config) - self.config: VariableDetectorConfig # type narrowing for IDE - self._time_handler = TimeFormatHandler() - self._warned_bad_timestamp = False - self.persistency = EventPersistency( +class VariableHooks: + def __init__( + self, + name: str, + _time_handler: TimeFormatHandler = TimeFormatHandler(), + config_vars: VariableAutoConfigParams = VariableAutoConfigParams(), + ) -> None: + self.name = name + self._warned_bad_timestamp: bool = False + self.config_vars = config_vars + self._time_handler = _time_handler + + def _init_persistency(self) -> EventPersistency: + return EventPersistency( event_data_class=self._event_data_class(), - # No classification kwargs: the trained trackers are read by - # _check_variable, which looks at unique_set / min-max / charset - # directly and never calls classify(). A classification block - # would only make them collect timestamps nothing reads. event_data_kwargs=self._event_data_kwargs(), ) - # auto config checks individual-variable stability to select features - self.auto_conf_persistency = EventPersistency( + + def _init_auto_persistency(self) -> EventPersistency: + return EventPersistency( event_data_class=self._event_data_class(), event_data_kwargs=self._with_classification_kwargs(self._auto_conf_kwargs()), ) - self._register_persistency(self.persistency) def _with_classification_kwargs( self, kwargs: Optional[Dict[str, Any]] ) -> Optional[Dict[str, Any]]: - """Add the classification block to tracker kwargs, unless it is the - default. - - Done here rather than in _stability_kwargs so every VariableDetector - subclass is covered -- NewValueDetector overrides neither construction - hook and NewValueComboDetector returns only a converter_function. - Non-defaults only: forwarding the default block would be noise, and a - block naming a time-axis method would make every variable collect - timestamps it never reads. - """ - auto = self.config.auto_config_params - if auto.classification == ClassificationMethods(): + if self.config_vars.classification == ClassificationMethods(): return kwargs - return {**(kwargs or {}), "classification": auto.classification.model_dump()} - - # ---- construction hooks ------------------------------------------------- + return {**(kwargs or {}), "classification": self.config_vars.classification.model_dump()} def _event_data_class(self) -> type: return EventStabilityTracker @@ -163,13 +127,7 @@ def _auto_conf_kwargs(self) -> Optional[Dict[str, Any]]: return self._event_data_kwargs() def _stability_kwargs(self) -> Dict[str, Any]: - """Kwargs for detectors whose tracker rebinds a per-detector - ``add_value`` closure (charset / value_range / bigram).""" - name = type(self).__name__ - return { - "add_value_fn": name, - "detector_config": _strip_auto_config_params(self.config.to_dict(method_id=name), name), - } + return {} def _warn_time_fallback_once(self, reason: str) -> None: """Log the first time-dependent misconfiguration, then stay quiet. @@ -188,30 +146,25 @@ def _warn_time_fallback_once(self, reason: str) -> None: def _timestamp(self, input_: ParserSchema) -> float | None: """Resolve the record's event time, or None if no enabled classification method reads the time axis.""" - auto = self.config.auto_config_params - if not auto.classification.needs_timestamps: + if not self.config_vars.classification.needs_timestamps: return None - if not auto.timestamp_variable: - # Selecting a time-axis method without naming the field is an - # operator error, not an opt-out -- say so rather than silently - # no-op. + if not self.config_vars.timestamp_variable: self._warn_time_fallback_once( "a time-axis classification method is enabled " "but timestamp_variable is not set" ) return None - raw = input_["logFormatVariables"].get(auto.timestamp_variable) - ts = self._time_handler.parse_timestamp(str(raw or ""), auto.timestamp_format) + + raw = input_["logFormatVariables"].get(self.config_vars.timestamp_variable) + ts = self._time_handler.parse_timestamp(str(raw or ""), self.config_vars.timestamp_format) if ts == "0": self._warn_time_fallback_once( - f"timestamp_variable {auto.timestamp_variable!r} is missing or " + f"timestamp_variable {self.config_vars.timestamp_variable!r} is missing or " f"unparseable (got {raw!r})" ) return None return float(ts) - # ---- per-detector hooks ------------------------------------------------- - def _prepare_variables(self, variables: Dict[str, Any], stage: str) -> Dict[str, Any]: """Transform extracted variables. @@ -232,7 +185,43 @@ def _alert_key(self, event_id: Any, key: Any, is_global: bool) -> str: def _description(self) -> str: return f"{self.name} detected anomalies." - # ---- shared lifecycle --------------------------------------------------- + +class VariableDetector(CoreDetector, VariableHooks): + """Abstract base for detectors that learn a per-variable model from + configured log variables and flag anomalous values at detection time. + + Subclasses override a small set of hooks: + - ``_check_variable`` (required): the per-variable anomaly test. + - ``_prepare_variables`` (optional): transform variables per stage. + - ``_event_data_kwargs`` / ``_auto_conf_kwargs`` (optional): tracker + construction kwargs. + - ``_description`` / ``_alert_key`` (optional): output formatting. + + The five lifecycle methods (train/detect/configure/post_train/ + set_configuration) live here and are shared by all subclasses. + """ + + def __init__(self, name: str, config: VariableDetectorConfig) -> None: + CoreDetector.__init__(self, name=name, buffer_mode=BufferMode.NO_BUF, config=config) + self.config: VariableDetectorConfig # type narrowing for IDE + VariableHooks.__init__( + self, + name=self.name, + _time_handler=_time_handler, + config_vars=self.config.auto_config_params + ) + + self.persistency = self._init_persistency() + self.auto_conf_persistency = self._init_auto_persistency() + self._register_persistency(self.persistency) + + def _stability_kwargs(self) -> Dict[str, Any]: + """Redfine to be specific to the detector.""" + name = type(self).__name__ + return { + "add_value_fn": name, + "detector_config": _strip_auto_config_params(self.config.to_dict(method_id=name), name), + } def train(self, input_: ParserSchema) -> None: # type: ignore self._ingest(input_, get_configured_variables(input_, self.config.events), input_["EventID"]) @@ -288,10 +277,7 @@ def _check_event( is_global: bool, ) -> float: """Loop the event's per-variable trackers, accumulate alerts, score +1 - per anomalous variable. - - Bigram overrides this for event-level scoring. - """ + per anomalous variable.""" score = 0.0 var_trackers = cast(Dict[str, SingleStabilityTracker], event_tracker.get_data()) for key, tracker in var_trackers.items(): @@ -336,10 +322,7 @@ def set_configuration(self) -> None: selected = stable + static if selected: variables[event_id] = selected - # Write only what the configure phase produced. Rebuilding the config - # from generate_detector_config is what used to drop operator settings: - # it emits four keys, so everything else had to be carried across by - # hand and a forgotten field failed silently. + self.config.events = generate_events_config(variables, self.name) self.config.auto_config = False if not self.config.events.events: From cf057c5bbaea8e792ac64ca46934f093aeb244f2 Mon Sep 17 00:00:00 2001 From: "angre.garcia-gomez@ait.ac.at" Date: Mon, 14 Sep 2026 10:31:53 +0200 Subject: [PATCH 11/28] move code to other_op --- .../common/_other_op/_variable_hooks.py | 167 ++++++++++++++++ .../common/variable_detector.py | 186 ++---------------- .../detectors/bigram_frequency_detector.py | 2 +- .../detectors/new_event_detector.py | 2 +- .../detectors/new_value_combo_detector.py | 2 +- .../test_auto_config_params_survive.py | 2 +- .../test_persistency/test_slope_stability.py | 2 +- .../test_time_dependent_stability.py | 4 +- 8 files changed, 191 insertions(+), 176 deletions(-) create mode 100644 src/detectmatelibrary/common/_other_op/_variable_hooks.py diff --git a/src/detectmatelibrary/common/_other_op/_variable_hooks.py b/src/detectmatelibrary/common/_other_op/_variable_hooks.py new file mode 100644 index 00000000..491c4437 --- /dev/null +++ b/src/detectmatelibrary/common/_other_op/_variable_hooks.py @@ -0,0 +1,167 @@ +from detectmatelibrary.utils.persistency.event_data_structures.trackers.stability import ClassificationMethods +from detectmatelibrary.utils.persistency.event_data_structures.trackers.stability.stability_tracker import ( + EventStabilityTracker, SingleStabilityTracker +) +from detectmatelibrary.utils.persistency.event_persistency import EventPersistency +from detectmatelibrary.utils.time_format_handler import TimeFormatHandler + +from detectmatelibrary.common._config._formats import _EventInstance +from detectmatelibrary.common.detector import AutoConfigParams + + +from detectmatelibrary.tools.logging import logger +from detectmatelibrary.schemas import ParserSchema + +from typing import Any, Dict, Optional + + +def get_global_variables( + input_: ParserSchema, + global_instances: Dict[str, _EventInstance], +) -> Dict[str, Any]: + """Extract header variables from event-ID-independent instances. + + Args: + input_: Parser schema containing logFormatVariables + global_instances: Dict of instance_name -> _EventInstance configs + + Returns: + Dict mapping variable names to their values from the input + """ + result: Dict[str, Any] = {} + for instance in global_instances.values(): + for name in instance.header_variables: + if name in input_["logFormatVariables"]: + result[name] = input_["logFormatVariables"][name] + return result + + +def _strip_auto_config_params(detector_config: Dict[str, Any], method_id: str) -> Dict[str, Any]: + """Return a copy of a serialized detector_config with its + auto_config_params block removed. + + detector_config is stashed on a tracker and persisted verbatim by + to_state(). auto_config_params are configure-phase-only inputs -- + the standing constraint is that persisted tracker state never + carries them. Stripped here, at the point the kwargs are built, so + the block never reaches state in the first place. + """ + entry = detector_config.get("detectors", {}).get(method_id, {}) + if "auto_config_params" not in entry: + return detector_config + return { + **detector_config, + "detectors": { + **detector_config["detectors"], + method_id: {k: v for k, v in entry.items() if k != "auto_config_params"}, + }, + } + + +class VariableAutoConfigParams(AutoConfigParams): + use_stable_vars: bool = True + use_static_vars: bool = True + classification: ClassificationMethods = ClassificationMethods() + timestamp_variable: str | None = None + timestamp_format: str | None = None # None -> TimeFormatHandler auto-detect + + +class VariableHooks: + def __init__( + self, + name: str, + _time_handler: TimeFormatHandler = TimeFormatHandler(), + config_vars: VariableAutoConfigParams = VariableAutoConfigParams(), + ) -> None: + self.name = name + self._warned_bad_timestamp: bool = False + self.config_vars = config_vars + self._time_handler = _time_handler + + def _init_persistency(self) -> EventPersistency: + return EventPersistency( + event_data_class=self._event_data_class(), + event_data_kwargs=self._event_data_kwargs(), + ) + + def _init_auto_persistency(self) -> EventPersistency: + return EventPersistency( + event_data_class=self._event_data_class(), + event_data_kwargs=self._with_classification_kwargs(self._auto_conf_kwargs()), + ) + + def _with_classification_kwargs( + self, kwargs: Optional[Dict[str, Any]] + ) -> Optional[Dict[str, Any]]: + + if self.config_vars.classification == ClassificationMethods(): + return kwargs + return {**(kwargs or {}), "classification": self.config_vars.classification.model_dump()} + + def _event_data_class(self) -> type: + return EventStabilityTracker + + def _event_data_kwargs(self) -> Optional[Dict[str, Any]]: + return None + + def _auto_conf_kwargs(self) -> Optional[Dict[str, Any]]: + return self._event_data_kwargs() + + def _stability_kwargs(self) -> Dict[str, Any]: + return {} + + def _warn_time_fallback_once(self, reason: str) -> None: + """Log the first time-dependent misconfiguration, then stay quiet. + + A bad config would otherwise emit one warning per record, so the + flag latches after the first message. + """ + if self._warned_bad_timestamp: + return + self._warned_bad_timestamp = True + logger.warning( + "%s: %s; falling back to the index axis for stability classification.", + self.name, reason, + ) + + def _timestamp(self, input_: ParserSchema) -> float | None: + """Resolve the record's event time, or None if no enabled + classification method reads the time axis.""" + if not self.config_vars.classification.needs_timestamps: + return None + if not self.config_vars.timestamp_variable: + self._warn_time_fallback_once( + "a time-axis classification method is enabled " + "but timestamp_variable is not set" + ) + return None + + raw = input_["logFormatVariables"].get(self.config_vars.timestamp_variable) + ts = self._time_handler.parse_timestamp(str(raw or ""), self.config_vars.timestamp_format) + if ts == "0": + self._warn_time_fallback_once( + f"timestamp_variable {self.config_vars.timestamp_variable!r} is missing or " + f"unparseable (got {raw!r})" + ) + return None + return float(ts) + + def _prepare_variables(self, variables: Dict[str, Any], stage: str) -> Dict[str, Any]: + """Transform extracted variables. + + ``stage`` is "training" or "detection". + """ + return variables + + def _check_variable( + self, tracker: SingleStabilityTracker, value: Any, key: Any + ) -> Optional[str]: + """Return an alert message if ``value`` is anomalous for ``tracker``, + else None.""" + raise NotImplementedError + + def _alert_key(self, event_id: Any, key: Any, is_global: bool) -> str: + return f"Global - {key}" if is_global else f"EventID {event_id} - {key}" + + def _description(self) -> str: + return f"{self.name} detected anomalies." diff --git a/src/detectmatelibrary/common/variable_detector.py b/src/detectmatelibrary/common/variable_detector.py index a895f60c..17088de8 100644 --- a/src/detectmatelibrary/common/variable_detector.py +++ b/src/detectmatelibrary/common/variable_detector.py @@ -1,191 +1,39 @@ -from detectmatelibrary.common._config._formats import _EventInstance +from detectmatelibrary.utils.persistency.event_data_structures.trackers.stability.stability_tracker import ( + EventStabilityTracker, + SingleStabilityTracker, +) +from detectmatelibrary.utils.persistency.component_interfaces import ( + validate_config_coverage +) +from detectmatelibrary.utils.data_buffer import BufferMode + + from detectmatelibrary.common._config._compile import ( generate_events_config, get_configured_variables, ) +from detectmatelibrary.common._other_op._variable_hooks import ( + get_global_variables, _strip_auto_config_params, VariableAutoConfigParams, VariableHooks + +) from detectmatelibrary.common.detector import ( - AutoConfigParams, CoreDetectorConfig, CoreDetector, _time_handler ) -from detectmatelibrary.utils.persistency.component_interfaces import ( - validate_config_coverage -) -from detectmatelibrary.utils.persistency.event_data_structures.trackers.stability.stability_tracker import ( - EventStabilityTracker, - SingleStabilityTracker, -) -from detectmatelibrary.utils.persistency.event_data_structures.trackers.stability import ( - ClassificationMethods, -) -from detectmatelibrary.utils.persistency.event_persistency import EventPersistency -from detectmatelibrary.utils.data_buffer import BufferMode -from detectmatelibrary.utils.time_format_handler import TimeFormatHandler + from detectmatelibrary.schemas import ParserSchema, DetectorSchema from detectmatelibrary.constants import GLOBAL_EVENT_ID from detectmatelibrary.tools.logging import logger -from typing import Any, Dict, Optional, cast from typing_extensions import override - - -def get_global_variables( - input_: ParserSchema, - global_instances: Dict[str, _EventInstance], -) -> Dict[str, Any]: - """Extract header variables from event-ID-independent instances. - - Args: - input_: Parser schema containing logFormatVariables - global_instances: Dict of instance_name -> _EventInstance configs - - Returns: - Dict mapping variable names to their values from the input - """ - result: Dict[str, Any] = {} - for instance in global_instances.values(): - for name in instance.header_variables: - if name in input_["logFormatVariables"]: - result[name] = input_["logFormatVariables"][name] - return result - - -def _strip_auto_config_params(detector_config: Dict[str, Any], method_id: str) -> Dict[str, Any]: - """Return a copy of a serialized detector_config with its - auto_config_params block removed. - - detector_config is stashed on a tracker and persisted verbatim by - to_state(). auto_config_params are configure-phase-only inputs -- - the standing constraint is that persisted tracker state never - carries them. Stripped here, at the point the kwargs are built, so - the block never reaches state in the first place. - """ - entry = detector_config.get("detectors", {}).get(method_id, {}) - if "auto_config_params" not in entry: - return detector_config - return { - **detector_config, - "detectors": { - **detector_config["detectors"], - method_id: {k: v for k, v in entry.items() if k != "auto_config_params"}, - }, - } - - -class VariableAutoConfigParams(AutoConfigParams): - use_stable_vars: bool = True - use_static_vars: bool = True - classification: ClassificationMethods = ClassificationMethods() - timestamp_variable: str | None = None - timestamp_format: str | None = None # None -> TimeFormatHandler auto-detect +from typing import Any, Dict, cast class VariableDetectorConfig(CoreDetectorConfig): auto_config_params: VariableAutoConfigParams = VariableAutoConfigParams() -class VariableHooks: - def __init__( - self, - name: str, - _time_handler: TimeFormatHandler = TimeFormatHandler(), - config_vars: VariableAutoConfigParams = VariableAutoConfigParams(), - ) -> None: - self.name = name - self._warned_bad_timestamp: bool = False - self.config_vars = config_vars - self._time_handler = _time_handler - - def _init_persistency(self) -> EventPersistency: - return EventPersistency( - event_data_class=self._event_data_class(), - event_data_kwargs=self._event_data_kwargs(), - ) - - def _init_auto_persistency(self) -> EventPersistency: - return EventPersistency( - event_data_class=self._event_data_class(), - event_data_kwargs=self._with_classification_kwargs(self._auto_conf_kwargs()), - ) - - def _with_classification_kwargs( - self, kwargs: Optional[Dict[str, Any]] - ) -> Optional[Dict[str, Any]]: - - if self.config_vars.classification == ClassificationMethods(): - return kwargs - return {**(kwargs or {}), "classification": self.config_vars.classification.model_dump()} - - def _event_data_class(self) -> type: - return EventStabilityTracker - - def _event_data_kwargs(self) -> Optional[Dict[str, Any]]: - return None - - def _auto_conf_kwargs(self) -> Optional[Dict[str, Any]]: - return self._event_data_kwargs() - - def _stability_kwargs(self) -> Dict[str, Any]: - return {} - - def _warn_time_fallback_once(self, reason: str) -> None: - """Log the first time-dependent misconfiguration, then stay quiet. - - A bad config would otherwise emit one warning per record, so the - flag latches after the first message. - """ - if self._warned_bad_timestamp: - return - self._warned_bad_timestamp = True - logger.warning( - "%s: %s; falling back to the index axis for stability classification.", - self.name, reason, - ) - - def _timestamp(self, input_: ParserSchema) -> float | None: - """Resolve the record's event time, or None if no enabled - classification method reads the time axis.""" - if not self.config_vars.classification.needs_timestamps: - return None - if not self.config_vars.timestamp_variable: - self._warn_time_fallback_once( - "a time-axis classification method is enabled " - "but timestamp_variable is not set" - ) - return None - - raw = input_["logFormatVariables"].get(self.config_vars.timestamp_variable) - ts = self._time_handler.parse_timestamp(str(raw or ""), self.config_vars.timestamp_format) - if ts == "0": - self._warn_time_fallback_once( - f"timestamp_variable {self.config_vars.timestamp_variable!r} is missing or " - f"unparseable (got {raw!r})" - ) - return None - return float(ts) - - def _prepare_variables(self, variables: Dict[str, Any], stage: str) -> Dict[str, Any]: - """Transform extracted variables. - - ``stage`` is "training" or "detection". - """ - return variables - - def _check_variable( - self, tracker: SingleStabilityTracker, value: Any, key: Any - ) -> Optional[str]: - """Return an alert message if ``value`` is anomalous for ``tracker``, - else None.""" - raise NotImplementedError - - def _alert_key(self, event_id: Any, key: Any, is_global: bool) -> str: - return f"Global - {key}" if is_global else f"EventID {event_id} - {key}" - - def _description(self) -> str: - return f"{self.name} detected anomalies." - - class VariableDetector(CoreDetector, VariableHooks): """Abstract base for detectors that learn a per-variable model from configured log variables and flag anomalous values at detection time. @@ -203,7 +51,7 @@ class VariableDetector(CoreDetector, VariableHooks): def __init__(self, name: str, config: VariableDetectorConfig) -> None: CoreDetector.__init__(self, name=name, buffer_mode=BufferMode.NO_BUF, config=config) - self.config: VariableDetectorConfig # type narrowing for IDE + self.config: VariableDetectorConfig VariableHooks.__init__( self, name=self.name, diff --git a/src/detectmatelibrary/detectors/bigram_frequency_detector.py b/src/detectmatelibrary/detectors/bigram_frequency_detector.py index 84ce9937..d0dfa8a0 100644 --- a/src/detectmatelibrary/detectors/bigram_frequency_detector.py +++ b/src/detectmatelibrary/detectors/bigram_frequency_detector.py @@ -1,7 +1,7 @@ from typing import Any, Dict, Optional, cast from detectmatelibrary.common.variable_detector import VariableDetector, VariableDetectorConfig -from detectmatelibrary.common.variable_detector import get_global_variables +from detectmatelibrary.common._other_op._variable_hooks import get_global_variables from detectmatelibrary.common._config._compile import get_configured_variables from detectmatelibrary.utils.persistency.event_data_structures.trackers.stability.stability_tracker import ( EventStabilityTracker, diff --git a/src/detectmatelibrary/detectors/new_event_detector.py b/src/detectmatelibrary/detectors/new_event_detector.py index 133d6213..109e7aff 100644 --- a/src/detectmatelibrary/detectors/new_event_detector.py +++ b/src/detectmatelibrary/detectors/new_event_detector.py @@ -1,6 +1,6 @@ from detectmatelibrary.common._config._compile import generate_events_config from detectmatelibrary.common.detector import CoreDetectorConfig, CoreDetector -from detectmatelibrary.common.variable_detector import get_global_variables +from detectmatelibrary.common._other_op._variable_hooks import get_global_variables from detectmatelibrary.utils import persistency from detectmatelibrary.constants import GLOBAL_EVENT_ID from detectmatelibrary.utils.data_buffer import BufferMode diff --git a/src/detectmatelibrary/detectors/new_value_combo_detector.py b/src/detectmatelibrary/detectors/new_value_combo_detector.py index 7bdd372b..5a9575ec 100644 --- a/src/detectmatelibrary/detectors/new_value_combo_detector.py +++ b/src/detectmatelibrary/detectors/new_value_combo_detector.py @@ -1,8 +1,8 @@ from detectmatelibrary.common._config import generate_events_config +from detectmatelibrary.common._other_op._variable_hooks import VariableAutoConfigParams from detectmatelibrary.common.variable_detector import ( VariableDetector, VariableDetectorConfig, - VariableAutoConfigParams, ) from detectmatelibrary.common._config._compile import get_configured_variables diff --git a/tests/test_detectors/test_auto_config_params_survive.py b/tests/test_detectors/test_auto_config_params_survive.py index ca51152f..10df770c 100644 --- a/tests/test_detectors/test_auto_config_params_survive.py +++ b/tests/test_detectors/test_auto_config_params_survive.py @@ -24,7 +24,7 @@ NewValueDetector, NewValueDetectorConfig, ) -from detectmatelibrary.common.variable_detector import VariableAutoConfigParams +from detectmatelibrary.common._other_op._variable_hooks import VariableAutoConfigParams def _schema(event_id: int, level: str, log_id: str): diff --git a/tests/test_persistency/test_slope_stability.py b/tests/test_persistency/test_slope_stability.py index e80e987c..78b0c88f 100644 --- a/tests/test_persistency/test_slope_stability.py +++ b/tests/test_persistency/test_slope_stability.py @@ -16,7 +16,7 @@ from pydantic import ValidationError from detectmatelibrary.detectors.charset_detector import CharsetDetector, CharsetDetectorConfig -from detectmatelibrary.common.variable_detector import VariableAutoConfigParams +from detectmatelibrary.common._other_op._variable_hooks import VariableAutoConfigParams from detectmatelibrary.utils.persistency.rle_list import RLEList from detectmatelibrary.utils.persistency.event_data_structures.trackers import ( StabilityClassifier, diff --git a/tests/test_persistency/test_time_dependent_stability.py b/tests/test_persistency/test_time_dependent_stability.py index f79ab121..a259f831 100644 --- a/tests/test_persistency/test_time_dependent_stability.py +++ b/tests/test_persistency/test_time_dependent_stability.py @@ -5,7 +5,7 @@ import detectmatelibrary.schemas as schemas from detectmatelibrary.detectors.charset_detector import CharsetDetector, CharsetDetectorConfig -from detectmatelibrary.common.variable_detector import VariableAutoConfigParams +from detectmatelibrary.common._other_op._variable_hooks import VariableAutoConfigParams from detectmatelibrary.utils.persistency.rle_list import RLEList from detectmatelibrary.utils.persistency import EventPersistency from detectmatelibrary.utils.persistency.event_data_structures.trackers import ( @@ -683,7 +683,7 @@ def test_train_path_records_no_timestamps(): Stability classification is never consulted at detect time, so the trained trackers would carry an unread timestamps list per variable. """ - from detectmatelibrary.common.variable_detector import VariableAutoConfigParams + from detectmatelibrary.common._other_op._variable_hooks import VariableAutoConfigParams from detectmatelibrary.detectors.new_value_detector import ( NewValueDetector, NewValueDetectorConfig, From 36dd06e2d30e1a608ae9426d1876ee132edadd5c Mon Sep 17 00:00:00 2001 From: "angre.garcia-gomez@ait.ac.at" Date: Mon, 14 Sep 2026 10:32:41 +0200 Subject: [PATCH 12/28] rename function --- src/detectmatelibrary/common/_other_op/_variable_hooks.py | 2 +- src/detectmatelibrary/common/variable_detector.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/detectmatelibrary/common/_other_op/_variable_hooks.py b/src/detectmatelibrary/common/_other_op/_variable_hooks.py index 491c4437..30236937 100644 --- a/src/detectmatelibrary/common/_other_op/_variable_hooks.py +++ b/src/detectmatelibrary/common/_other_op/_variable_hooks.py @@ -36,7 +36,7 @@ def get_global_variables( return result -def _strip_auto_config_params(detector_config: Dict[str, Any], method_id: str) -> Dict[str, Any]: +def strip_auto_config_params(detector_config: Dict[str, Any], method_id: str) -> Dict[str, Any]: """Return a copy of a serialized detector_config with its auto_config_params block removed. diff --git a/src/detectmatelibrary/common/variable_detector.py b/src/detectmatelibrary/common/variable_detector.py index 17088de8..7d244ff0 100644 --- a/src/detectmatelibrary/common/variable_detector.py +++ b/src/detectmatelibrary/common/variable_detector.py @@ -13,7 +13,7 @@ get_configured_variables, ) from detectmatelibrary.common._other_op._variable_hooks import ( - get_global_variables, _strip_auto_config_params, VariableAutoConfigParams, VariableHooks + get_global_variables, strip_auto_config_params, VariableAutoConfigParams, VariableHooks ) from detectmatelibrary.common.detector import ( @@ -68,7 +68,7 @@ def _stability_kwargs(self) -> Dict[str, Any]: name = type(self).__name__ return { "add_value_fn": name, - "detector_config": _strip_auto_config_params(self.config.to_dict(method_id=name), name), + "detector_config": strip_auto_config_params(self.config.to_dict(method_id=name), name), } def train(self, input_: ParserSchema) -> None: # type: ignore From abaf6519275404cb145b64edc7036f502c7a3f62 Mon Sep 17 00:00:00 2001 From: "angre.garcia-gomez@ait.ac.at" Date: Mon, 14 Sep 2026 10:38:21 +0200 Subject: [PATCH 13/28] move responsabilities to _variable_hooks --- .../common/_other_op/_variable_hooks.py | 39 ++++++++++++++++--- .../common/variable_detector.py | 34 ---------------- 2 files changed, 33 insertions(+), 40 deletions(-) diff --git a/src/detectmatelibrary/common/_other_op/_variable_hooks.py b/src/detectmatelibrary/common/_other_op/_variable_hooks.py index 30236937..9b2aff9b 100644 --- a/src/detectmatelibrary/common/_other_op/_variable_hooks.py +++ b/src/detectmatelibrary/common/_other_op/_variable_hooks.py @@ -12,7 +12,7 @@ from detectmatelibrary.tools.logging import logger from detectmatelibrary.schemas import ParserSchema -from typing import Any, Dict, Optional +from typing import Any, Dict, Optional, cast def get_global_variables( @@ -78,14 +78,11 @@ def __init__( self.config_vars = config_vars self._time_handler = _time_handler - def _init_persistency(self) -> EventPersistency: - return EventPersistency( + self.persistency = EventPersistency( event_data_class=self._event_data_class(), event_data_kwargs=self._event_data_kwargs(), ) - - def _init_auto_persistency(self) -> EventPersistency: - return EventPersistency( + self.auto_conf_persistency = EventPersistency( event_data_class=self._event_data_class(), event_data_kwargs=self._with_classification_kwargs(self._auto_conf_kwargs()), ) @@ -165,3 +162,33 @@ def _alert_key(self, event_id: Any, key: Any, is_global: bool) -> str: def _description(self) -> str: return f"{self.name} detected anomalies." + + def _check_event( + self, + alerts: Dict[str, str], + event_id: Any, + event_tracker: EventStabilityTracker, + variables: Dict[str, Any], + is_global: bool, + ) -> float: + """Loop the event's per-variable trackers, accumulate alerts, score +1 + per anomalous variable.""" + score = 0.0 + var_trackers = cast(Dict[str, SingleStabilityTracker], event_tracker.get_data()) + for key, tracker in var_trackers.items(): + value = variables.get(key) + if value is None: + continue + message = self._check_variable(tracker, value, key) + if message: + alerts[self._alert_key(event_id, key, is_global)] = message + score += 1.0 + return score + + def _ingest(self, input_: ParserSchema, variables: Dict[str, Any], event_id: Any) -> None: + variables = self._prepare_variables(variables, "training") + self.persistency.ingest_event( + event_id=event_id, + event_template=input_["template"], + named_variables=variables, + ) diff --git a/src/detectmatelibrary/common/variable_detector.py b/src/detectmatelibrary/common/variable_detector.py index 7d244ff0..da94e7db 100644 --- a/src/detectmatelibrary/common/variable_detector.py +++ b/src/detectmatelibrary/common/variable_detector.py @@ -1,6 +1,5 @@ from detectmatelibrary.utils.persistency.event_data_structures.trackers.stability.stability_tracker import ( EventStabilityTracker, - SingleStabilityTracker, ) from detectmatelibrary.utils.persistency.component_interfaces import ( validate_config_coverage @@ -58,9 +57,6 @@ def __init__(self, name: str, config: VariableDetectorConfig) -> None: _time_handler=_time_handler, config_vars=self.config.auto_config_params ) - - self.persistency = self._init_persistency() - self.auto_conf_persistency = self._init_auto_persistency() self._register_persistency(self.persistency) def _stability_kwargs(self) -> Dict[str, Any]: @@ -78,14 +74,6 @@ def train(self, input_: ParserSchema) -> None: # type: ignore if global_vars: self._ingest(input_, global_vars, GLOBAL_EVENT_ID) - def _ingest(self, input_: ParserSchema, variables: Dict[str, Any], event_id: Any) -> None: - variables = self._prepare_variables(variables, "training") - self.persistency.ingest_event( - event_id=event_id, - event_template=input_["template"], - named_variables=variables, - ) - def detect(self, input_: ParserSchema, output_: DetectorSchema) -> bool: # type: ignore alerts: Dict[str, str] = {} overall_score = 0.0 @@ -116,28 +104,6 @@ def detect(self, input_: ParserSchema, output_: DetectorSchema) -> bool: # type return True return False - def _check_event( - self, - alerts: Dict[str, str], - event_id: Any, - event_tracker: EventStabilityTracker, - variables: Dict[str, Any], - is_global: bool, - ) -> float: - """Loop the event's per-variable trackers, accumulate alerts, score +1 - per anomalous variable.""" - score = 0.0 - var_trackers = cast(Dict[str, SingleStabilityTracker], event_tracker.get_data()) - for key, tracker in var_trackers.items(): - value = variables.get(key) - if value is None: - continue - message = self._check_variable(tracker, value, key) - if message: - alerts[self._alert_key(event_id, key, is_global)] = message - score += 1.0 - return score - def configure(self, input_: ParserSchema) -> None: # type: ignore self.auto_conf_persistency.ingest_event( event_id=input_["EventID"], From 5a3c3ea0e30e2af96df56ade038e351361d666f2 Mon Sep 17 00:00:00 2001 From: "angre.garcia-gomez@ait.ac.at" Date: Mon, 14 Sep 2026 10:49:31 +0200 Subject: [PATCH 14/28] hook and logic separation --- .../common/_other_op/_variable_hooks.py | 83 ++++++++++--------- .../common/variable_detector.py | 6 +- 2 files changed, 49 insertions(+), 40 deletions(-) diff --git a/src/detectmatelibrary/common/_other_op/_variable_hooks.py b/src/detectmatelibrary/common/_other_op/_variable_hooks.py index 9b2aff9b..0ea29022 100644 --- a/src/detectmatelibrary/common/_other_op/_variable_hooks.py +++ b/src/detectmatelibrary/common/_other_op/_variable_hooks.py @@ -66,26 +66,12 @@ class VariableAutoConfigParams(AutoConfigParams): timestamp_format: str | None = None # None -> TimeFormatHandler auto-detect -class VariableHooks: - def __init__( - self, - name: str, - _time_handler: TimeFormatHandler = TimeFormatHandler(), - config_vars: VariableAutoConfigParams = VariableAutoConfigParams(), - ) -> None: +class VaribaleHooks: + """Hooks use to define the dfferent behaviours in th next subclasses.""" + def __init__(self, name: str, config_vars: VariableAutoConfigParams) -> None: self.name = name self._warned_bad_timestamp: bool = False self.config_vars = config_vars - self._time_handler = _time_handler - - self.persistency = EventPersistency( - event_data_class=self._event_data_class(), - event_data_kwargs=self._event_data_kwargs(), - ) - self.auto_conf_persistency = EventPersistency( - event_data_class=self._event_data_class(), - event_data_kwargs=self._with_classification_kwargs(self._auto_conf_kwargs()), - ) def _with_classification_kwargs( self, kwargs: Optional[Dict[str, Any]] @@ -107,6 +93,47 @@ def _auto_conf_kwargs(self) -> Optional[Dict[str, Any]]: def _stability_kwargs(self) -> Dict[str, Any]: return {} + def _prepare_variables(self, variables: Dict[str, Any], stage: str) -> Dict[str, Any]: + """Transform extracted variables. + + ``stage`` is "training" or "detection". + """ + return variables + + def _check_variable( + self, tracker: SingleStabilityTracker, value: Any, key: Any + ) -> Optional[str]: + """Return an alert message if ``value`` is anomalous for ``tracker``, + else None.""" + raise NotImplementedError + + def _alert_key(self, event_id: Any, key: Any, is_global: bool) -> str: + return f"Global - {key}" if is_global else f"EventID {event_id} - {key}" + + def _description(self) -> str: + return f"{self.name} detected anomalies." + + +class VariablesLogic(VaribaleHooks): + """Variables logic combining the hooks and persistency class.""" + def __init__( + self, + name: str, + _time_handler: TimeFormatHandler = TimeFormatHandler(), + config_vars: VariableAutoConfigParams = VariableAutoConfigParams(), + ) -> None: + + super().__init__(name=name, config_vars=config_vars) + self._time_handler = _time_handler + self.persistency = EventPersistency( + event_data_class=self._event_data_class(), + event_data_kwargs=self._event_data_kwargs(), + ) + self.auto_conf_persistency = EventPersistency( + event_data_class=self._event_data_class(), + event_data_kwargs=self._with_classification_kwargs(self._auto_conf_kwargs()), + ) + def _warn_time_fallback_once(self, reason: str) -> None: """Log the first time-dependent misconfiguration, then stay quiet. @@ -126,6 +153,7 @@ def _timestamp(self, input_: ParserSchema) -> float | None: classification method reads the time axis.""" if not self.config_vars.classification.needs_timestamps: return None + if not self.config_vars.timestamp_variable: self._warn_time_fallback_once( "a time-axis classification method is enabled " @@ -141,27 +169,8 @@ def _timestamp(self, input_: ParserSchema) -> float | None: f"unparseable (got {raw!r})" ) return None - return float(ts) - - def _prepare_variables(self, variables: Dict[str, Any], stage: str) -> Dict[str, Any]: - """Transform extracted variables. - - ``stage`` is "training" or "detection". - """ - return variables - def _check_variable( - self, tracker: SingleStabilityTracker, value: Any, key: Any - ) -> Optional[str]: - """Return an alert message if ``value`` is anomalous for ``tracker``, - else None.""" - raise NotImplementedError - - def _alert_key(self, event_id: Any, key: Any, is_global: bool) -> str: - return f"Global - {key}" if is_global else f"EventID {event_id} - {key}" - - def _description(self) -> str: - return f"{self.name} detected anomalies." + return float(ts) def _check_event( self, diff --git a/src/detectmatelibrary/common/variable_detector.py b/src/detectmatelibrary/common/variable_detector.py index da94e7db..e9fcfcfe 100644 --- a/src/detectmatelibrary/common/variable_detector.py +++ b/src/detectmatelibrary/common/variable_detector.py @@ -12,7 +12,7 @@ get_configured_variables, ) from detectmatelibrary.common._other_op._variable_hooks import ( - get_global_variables, strip_auto_config_params, VariableAutoConfigParams, VariableHooks + get_global_variables, strip_auto_config_params, VariableAutoConfigParams, VariablesLogic ) from detectmatelibrary.common.detector import ( @@ -33,7 +33,7 @@ class VariableDetectorConfig(CoreDetectorConfig): auto_config_params: VariableAutoConfigParams = VariableAutoConfigParams() -class VariableDetector(CoreDetector, VariableHooks): +class VariableDetector(CoreDetector, VariablesLogic): """Abstract base for detectors that learn a per-variable model from configured log variables and flag anomalous values at detection time. @@ -51,7 +51,7 @@ class VariableDetector(CoreDetector, VariableHooks): def __init__(self, name: str, config: VariableDetectorConfig) -> None: CoreDetector.__init__(self, name=name, buffer_mode=BufferMode.NO_BUF, config=config) self.config: VariableDetectorConfig - VariableHooks.__init__( + VariablesLogic.__init__( self, name=self.name, _time_handler=_time_handler, From d53248caad69055fc44c6551179621ab0a908625 Mon Sep 17 00:00:00 2001 From: "angre.garcia-gomez@ait.ac.at" Date: Mon, 14 Sep 2026 10:58:09 +0200 Subject: [PATCH 15/28] finetune variable detector --- .../common/variable_detector.py | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/src/detectmatelibrary/common/variable_detector.py b/src/detectmatelibrary/common/variable_detector.py index e9fcfcfe..8bbddea4 100644 --- a/src/detectmatelibrary/common/variable_detector.py +++ b/src/detectmatelibrary/common/variable_detector.py @@ -33,6 +33,16 @@ class VariableDetectorConfig(CoreDetectorConfig): auto_config_params: VariableAutoConfigParams = VariableAutoConfigParams() +def add_variables( + vars: Dict[Any, Any], tracker: EventStabilityTracker, auto: VariableAutoConfigParams, e_id: int | str +) -> None: + stable = tracker.get_features_by_classification("STABLE") if auto.use_stable_vars else [] + static = tracker.get_features_by_classification("STATIC") if auto.use_static_vars else [] + selected = stable + static + if selected: + vars[e_id] = selected + + class VariableDetector(CoreDetector, VariablesLogic): """Abstract base for detectors that learn a per-variable model from configured log variables and flag anomalous values at detection time. @@ -52,10 +62,7 @@ def __init__(self, name: str, config: VariableDetectorConfig) -> None: CoreDetector.__init__(self, name=name, buffer_mode=BufferMode.NO_BUF, config=config) self.config: VariableDetectorConfig VariablesLogic.__init__( - self, - name=self.name, - _time_handler=_time_handler, - config_vars=self.config.auto_config_params + self, name=self.name, _time_handler=_time_handler, config_vars=self.config.auto_config_params ) self._register_persistency(self.persistency) @@ -123,19 +130,7 @@ def set_configuration(self) -> None: for event_id, tracker in self.auto_conf_persistency.get_events_data().items(): stability_tracker = cast(EventStabilityTracker, tracker) auto = self.config.auto_config_params - stable = ( - stability_tracker.get_features_by_classification("STABLE") - if auto.use_stable_vars - else [] - ) - static = ( - stability_tracker.get_features_by_classification("STATIC") - if auto.use_static_vars - else [] - ) - selected = stable + static - if selected: - variables[event_id] = selected + add_variables(variables, tracker=stability_tracker, auto=auto, e_id=event_id) self.config.events = generate_events_config(variables, self.name) self.config.auto_config = False From 3275a3fe935c3dffd42aa3587d07788e4cb84d0c Mon Sep 17 00:00:00 2001 From: "angre.garcia-gomez@ait.ac.at" Date: Mon, 14 Sep 2026 11:02:09 +0200 Subject: [PATCH 16/28] remove dummy import --- src/detectmatelibrary/common/_other_op/_variable_hooks.py | 3 +-- src/detectmatelibrary/common/detector.py | 2 -- src/detectmatelibrary/common/variable_detector.py | 1 - src/detectmatelibrary/detectors/event_sequence_detector.py | 4 +++- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/detectmatelibrary/common/_other_op/_variable_hooks.py b/src/detectmatelibrary/common/_other_op/_variable_hooks.py index 0ea29022..66de8dda 100644 --- a/src/detectmatelibrary/common/_other_op/_variable_hooks.py +++ b/src/detectmatelibrary/common/_other_op/_variable_hooks.py @@ -6,8 +6,7 @@ from detectmatelibrary.utils.time_format_handler import TimeFormatHandler from detectmatelibrary.common._config._formats import _EventInstance -from detectmatelibrary.common.detector import AutoConfigParams - +from detectmatelibrary.common._config import AutoConfigParams from detectmatelibrary.tools.logging import logger from detectmatelibrary.schemas import ParserSchema diff --git a/src/detectmatelibrary/common/detector.py b/src/detectmatelibrary/common/detector.py index 04735c4e..0efc2c99 100644 --- a/src/detectmatelibrary/common/detector.py +++ b/src/detectmatelibrary/common/detector.py @@ -1,6 +1,4 @@ from detectmatelibrary.common._config._formats import EventsConfig, _EventInstance -# Re-exported: subclasses spell it `from detectmatelibrary.common.detector import AutoConfigParams`. -from detectmatelibrary.common._config import AutoConfigParams as AutoConfigParams # noqa: F401 from detectmatelibrary.common.core import CoreComponent, CoreConfig from detectmatelibrary.utils.data_buffer import ArgsBuffer, BufferMode diff --git a/src/detectmatelibrary/common/variable_detector.py b/src/detectmatelibrary/common/variable_detector.py index 8bbddea4..dcfe2d21 100644 --- a/src/detectmatelibrary/common/variable_detector.py +++ b/src/detectmatelibrary/common/variable_detector.py @@ -57,7 +57,6 @@ class VariableDetector(CoreDetector, VariablesLogic): The five lifecycle methods (train/detect/configure/post_train/ set_configuration) live here and are shared by all subclasses. """ - def __init__(self, name: str, config: VariableDetectorConfig) -> None: CoreDetector.__init__(self, name=name, buffer_mode=BufferMode.NO_BUF, config=config) self.config: VariableDetectorConfig diff --git a/src/detectmatelibrary/detectors/event_sequence_detector.py b/src/detectmatelibrary/detectors/event_sequence_detector.py index 0e09af66..162da14f 100644 --- a/src/detectmatelibrary/detectors/event_sequence_detector.py +++ b/src/detectmatelibrary/detectors/event_sequence_detector.py @@ -6,7 +6,9 @@ from pydantic import Field, model_validator from detectmatelibrary.common._config._compile import generate_events_config -from detectmatelibrary.common.detector import AutoConfigParams, CoreDetectorConfig, CoreDetector +from detectmatelibrary.common.detector import CoreDetectorConfig, CoreDetector +from detectmatelibrary.common._config import AutoConfigParams + from detectmatelibrary.tools.logging import logger from detectmatelibrary.utils import persistency from detectmatelibrary.utils.data_buffer import BufferMode From e9dbf0241e59d236630a796b7c8c078ab5d8c595 Mon Sep 17 00:00:00 2001 From: "angre.garcia-gomez@ait.ac.at" Date: Mon, 14 Sep 2026 12:16:25 +0200 Subject: [PATCH 17/28] add fed test for VariableDetectors --- .../common/variable_detector.py | 7 ++++ .../test_bigram_frequency_detector.py | 39 +++++++++++++++++++ tests/test_detectors/test_charset_detector.py | 30 ++++++++++++++ .../test_new_value_combo_detector.py | 35 +++++++++++++++++ .../test_detectors/test_new_value_detector.py | 31 +++++++++++++++ .../test_value_range_detector.py | 31 +++++++++++++++ 6 files changed, 173 insertions(+) diff --git a/src/detectmatelibrary/common/variable_detector.py b/src/detectmatelibrary/common/variable_detector.py index dcfe2d21..8ec5ef9f 100644 --- a/src/detectmatelibrary/common/variable_detector.py +++ b/src/detectmatelibrary/common/variable_detector.py @@ -139,3 +139,10 @@ def set_configuration(self) -> None: "No stable variables were found in configure-phase data. " "The detector will produce no alerts." ) + + def aggregate_strategy(self, components: set["VariableDetector"]) -> None: # type: ignore + for component in components: + self.persistency.combine(component.persistency) + + for component in components: + component.persistency = self.persistency diff --git a/tests/test_detectors/test_bigram_frequency_detector.py b/tests/test_detectors/test_bigram_frequency_detector.py index b852be4d..d825568a 100644 --- a/tests/test_detectors/test_bigram_frequency_detector.py +++ b/tests/test_detectors/test_bigram_frequency_detector.py @@ -267,6 +267,45 @@ def test_audit_log_anomalies(self): assert detected_ids == {'1859', '1860', '1861', '1862'} + @pytest.mark.ignored + def test_audit_log_anomalie_fed(self): + parser = MatcherParser(config=_PARSER_CONFIG) + detector1 = BigramFrequencyDetector( + config=BigramFrequencyDetectorConfig( + skip_repetitions=False + ) + ) + detector2 = BigramFrequencyDetector( + config=BigramFrequencyDetectorConfig( + skip_repetitions=False + ) + ) + + logs = list(From.log(parser, in_path=AUDIT_LOG, do_process=True)) + for log in logs[:TRAIN_UNTIL]: + detector1.configure(log) + detector2.configure(log) + + detector1.set_configuration() + detector2.set_configuration() + + for log in logs[:TRAIN_UNTIL]: + detector1.train(log) + + assert len(detector2.persistency) == 0 + + (detector1 + detector2).aggregate() + assert detector2.persistency == detector1.persistency + assert len(detector2.persistency) != 0 + + detected_ids: set[str] = set() + for log in logs[TRAIN_UNTIL:]: + output = schemas.DetectorSchema() + if detector2.detect(log, output_=output): + detected_ids.add(log["logID"]) + + assert detected_ids == {'1859', '1860', '1861', '1862'} + class TestBigramFrequencyDetectorAutoConfig: """Test that process() drives configure/set_configuration/train/detect diff --git a/tests/test_detectors/test_charset_detector.py b/tests/test_detectors/test_charset_detector.py index f3afa749..6d566420 100644 --- a/tests/test_detectors/test_charset_detector.py +++ b/tests/test_detectors/test_charset_detector.py @@ -347,6 +347,36 @@ def test_audit_log_anomalies_via_process(self): assert detected_ids == {'1859', '1860', '1861', '1862', '1864', '1865', '1866', '1867'} + @pytest.mark.ignored + def test_audit_log_anomalie_fed(self): + parser = MatcherParser(config=_PARSER_CONFIG) + detector1 = CharsetDetector() + detector2 = CharsetDetector() + + logs = list(From.log(parser, in_path=AUDIT_LOG, do_process=True)) + for log in logs[:TRAIN_UNTIL]: + detector1.configure(log) + detector2.configure(log) + + detector1.set_configuration() + detector2.set_configuration() + + for log in logs[:TRAIN_UNTIL]: + detector1.train(log) + + assert len(detector2.persistency) == 0 + + (detector1 + detector2).aggregate() + assert detector2.persistency == detector1.persistency + assert len(detector2.persistency) != 0 + + detected_ids: set[str] = set() + for log in logs[TRAIN_UNTIL:]: + if detector2.process(log) is not None: + detected_ids.add(log["logID"]) + + assert detected_ids == {'1859', '1860', '1861', '1862', '1864', '1865', '1866', '1867'} + class TestCharsetDetectorGlobalInstances: """Tests event-ID-independent global instance detection.""" diff --git a/tests/test_detectors/test_new_value_combo_detector.py b/tests/test_detectors/test_new_value_combo_detector.py index 10a00345..aa37e8c6 100644 --- a/tests/test_detectors/test_new_value_combo_detector.py +++ b/tests/test_detectors/test_new_value_combo_detector.py @@ -15,6 +15,8 @@ from detectmatelibrary.utils.aux import time_test_mode from tests.test_data import AUDIT_LOG, AUDIT_TEMPLATES, TRAIN_UNTIL +import pytest + # Set time test mode for consistent timestamps time_test_mode() @@ -564,6 +566,8 @@ def test_configure_only_selects_stable_event_types(self): class TestNewValueComboDetectorEndToEndWithRealData: """Regression test: full configure/train/detect pipeline on audit.log.""" + + @pytest.mark.ignored def test_audit_log_anomalies(self): pars = MatcherParser(config=_PARSER_CONFIG) detector = NewValueComboDetector() @@ -585,6 +589,37 @@ def test_audit_log_anomalies(self): assert detected_ids == {"1859", "1862", "1865", "1866"} + @pytest.mark.ignored + def test_audit_log_anomalie_fed(self): + parser = MatcherParser(config=_PARSER_CONFIG) + detector1 = NewValueComboDetector() + detector2 = NewValueComboDetector() + + logs = list(From.log(parser, in_path=AUDIT_LOG, do_process=True)) + for log in logs[:TRAIN_UNTIL]: + detector1.configure(log) + detector2.configure(log) + + detector1.set_configuration() + detector2.set_configuration() + + for log in logs[:TRAIN_UNTIL]: + detector1.train(log) + + assert len(detector2.persistency) == 0 + + (detector1 + detector2).aggregate() + assert detector2.persistency == detector1.persistency + assert len(detector2.persistency) != 0 + + detected_ids: set[str] = set() + for log in logs[TRAIN_UNTIL:]: + output = schemas.DetectorSchema() + if detector2.detect(log, output_=output): + detected_ids.add(log["logID"]) + + assert detected_ids == {"1859", "1862", "1865", "1866"} + class TestNewValueComboDetectorClassificationConfigPreservation: """auto_config_params survive set_configuration untouched. diff --git a/tests/test_detectors/test_new_value_detector.py b/tests/test_detectors/test_new_value_detector.py index 784f1919..82018580 100644 --- a/tests/test_detectors/test_new_value_detector.py +++ b/tests/test_detectors/test_new_value_detector.py @@ -273,6 +273,37 @@ def test_audit_log_anomalies_via_process(self): assert detected_ids == {'1859', '1860', '1861', '1862', '1864', '1865', '1866', '1867'} + @pytest.mark.ignored + def test_audit_log_anomalie_fed(self): + parser = MatcherParser(config=_PARSER_CONFIG) + detector1 = NewValueDetector() + detector2 = NewValueDetector() + + logs = list(From.log(parser, in_path=AUDIT_LOG, do_process=True)) + for log in logs[:TRAIN_UNTIL]: + detector1.configure(log) + detector2.configure(log) + + detector1.set_configuration() + detector2.set_configuration() + + for log in logs[:TRAIN_UNTIL]: + detector1.train(log) + + assert len(detector2.persistency) == 0 + + (detector1 + detector2).aggregate() + assert detector2.persistency == detector1.persistency + assert len(detector2.persistency) != 0 + + detected_ids: set[str] = set() + for log in logs[TRAIN_UNTIL:]: + output = schemas.DetectorSchema() + if detector2.detect(log, output_=output): + detected_ids.add(log["logID"]) + + assert detected_ids == {'1859', '1860', '1861', '1862', '1864', '1865', '1866', '1867'} + class TestNewValueDetectorGlobalInstances: """Tests event-ID-independent global instance detection.""" diff --git a/tests/test_detectors/test_value_range_detector.py b/tests/test_detectors/test_value_range_detector.py index 6a1ec0ac..ac0993aa 100644 --- a/tests/test_detectors/test_value_range_detector.py +++ b/tests/test_detectors/test_value_range_detector.py @@ -416,6 +416,37 @@ def test_audit_log_anomalies_via_process(self): assert detected_ids == {'1859', '1860', '1861', '1862'} + @pytest.mark.ignored + def test_audit_log_anomalie_fed(self): + parser = MatcherParser(config=_PARSER_CONFIG) + detector1 = ValueRangeDetector() + detector2 = ValueRangeDetector() + + logs = list(From.log(parser, in_path=AUDIT_LOG, do_process=True)) + for log in logs[:TRAIN_UNTIL]: + detector1.configure(log) + detector2.configure(log) + + detector1.set_configuration() + detector2.set_configuration() + + for log in logs[:TRAIN_UNTIL]: + detector1.train(log) + + assert len(detector2.persistency) == 0 + + (detector1 + detector2).aggregate() + assert detector2.persistency == detector1.persistency + assert len(detector2.persistency) != 0 + + detected_ids: set[str] = set() + for log in logs[TRAIN_UNTIL:]: + output = schemas.DetectorSchema() + if detector2.detect(log, output_=output): + detected_ids.add(log["logID"]) + + assert detected_ids == {'1859', '1860', '1861', '1862'} + class TestValueRangeDetectorGlobalInstances: """Tests event-ID-independent global instance detection.""" From 2d96e0919df49917fdf09da055e378eee301279e Mon Sep 17 00:00:00 2001 From: "angre.garcia-gomez@ait.ac.at" Date: Mon, 14 Sep 2026 12:39:45 +0200 Subject: [PATCH 18/28] Add Variable Logic and federation to new event --- .../common/_other_op/_variable_hooks.py | 13 ++++++- .../common/variable_detector.py | 6 +-- .../detectors/new_event_detector.py | 38 ++++++++----------- .../test_detectors/test_new_event_detector.py | 28 ++++++++++++++ 4 files changed, 56 insertions(+), 29 deletions(-) diff --git a/src/detectmatelibrary/common/_other_op/_variable_hooks.py b/src/detectmatelibrary/common/_other_op/_variable_hooks.py index 66de8dda..c71b5451 100644 --- a/src/detectmatelibrary/common/_other_op/_variable_hooks.py +++ b/src/detectmatelibrary/common/_other_op/_variable_hooks.py @@ -193,10 +193,19 @@ def _check_event( score += 1.0 return score - def _ingest(self, input_: ParserSchema, variables: Dict[str, Any], event_id: Any) -> None: + def _ingest( + self, input_: ParserSchema, variables: Dict[str, Any], event_id: Any + ) -> None: variables = self._prepare_variables(variables, "training") self.persistency.ingest_event( event_id=event_id, event_template=input_["template"], - named_variables=variables, + named_variables=variables ) + + def combine(self, components: set["VariablesLogic"]) -> None: + for component in components: + self.persistency.combine(component.persistency) + + for component in components: + component.persistency = self.persistency diff --git a/src/detectmatelibrary/common/variable_detector.py b/src/detectmatelibrary/common/variable_detector.py index 8ec5ef9f..96d38a57 100644 --- a/src/detectmatelibrary/common/variable_detector.py +++ b/src/detectmatelibrary/common/variable_detector.py @@ -141,8 +141,4 @@ def set_configuration(self) -> None: ) def aggregate_strategy(self, components: set["VariableDetector"]) -> None: # type: ignore - for component in components: - self.persistency.combine(component.persistency) - - for component in components: - component.persistency = self.persistency + self.combine(components) # type: ignore diff --git a/src/detectmatelibrary/detectors/new_event_detector.py b/src/detectmatelibrary/detectors/new_event_detector.py index 109e7aff..8048d92c 100644 --- a/src/detectmatelibrary/detectors/new_event_detector.py +++ b/src/detectmatelibrary/detectors/new_event_detector.py @@ -1,20 +1,22 @@ -from detectmatelibrary.common._config._compile import generate_events_config from detectmatelibrary.common.detector import CoreDetectorConfig, CoreDetector + from detectmatelibrary.common._other_op._variable_hooks import get_global_variables -from detectmatelibrary.utils import persistency +from detectmatelibrary.common._other_op._variable_hooks import VariablesLogic + +from detectmatelibrary.common._config._compile import get_configured_variables +from detectmatelibrary.common._config._compile import generate_events_config + + from detectmatelibrary.constants import GLOBAL_EVENT_ID from detectmatelibrary.utils.data_buffer import BufferMode from detectmatelibrary.schemas import ParserSchema, DetectorSchema -from detectmatelibrary.common._config._compile import ( - get_configured_variables -) class NewEventDetectorConfig(CoreDetectorConfig): method_type: str = "new_event_detector" -class NewEventDetector(CoreDetector): +class NewEventDetector(CoreDetector, VariablesLogic): """Detect new values in log data as anomalies based on learned values.""" def __init__( @@ -26,30 +28,19 @@ def __init__( if isinstance(config, dict): config = NewEventDetectorConfig.from_dict(config, name) - super().__init__(name=name, buffer_mode=BufferMode.NO_BUF, config=config) + CoreDetector.__init__(self, name=name, buffer_mode=BufferMode.NO_BUF, config=config) self.config: NewEventDetectorConfig - self.persistency = persistency.EventPersistency( - event_data_class=persistency.EventStabilityTracker, - ) - # auto config checks if individual variables are stable to select combos from - self.auto_conf_persistency = persistency.EventPersistency( - event_data_class=persistency.EventStabilityTracker - ) + + VariablesLogic.__init__(self, name=self.name) self._register_persistency(self.persistency) def train(self, input_: ParserSchema) -> None: # type: ignore """Train the detector by learning values from the input data.""" - self.persistency.ingest_event( - event_id=input_["EventID"], - event_template=input_["template"] - ) + self._ingest(event_id=input_["EventID"], input_=input_, variables={}) if self.config.global_instances: global_vars = get_global_variables(input_, self.config.global_instances) if global_vars: - self.persistency.ingest_event( - event_id=GLOBAL_EVENT_ID, - event_template=input_["template"] - ) + self._ingest(event_id=GLOBAL_EVENT_ID, input_=input_, variables={}) def detect( self, input_: ParserSchema, output_: DetectorSchema # type: ignore @@ -92,3 +83,6 @@ def set_configuration(self) -> None: # the configure phase produces an empty events block. self.config.events = generate_events_config({}, self.name) self.config.auto_config = False + + def aggregate_strategy(self, components: set["NewEventDetector"]) -> None: # type: ignore + self.combine(components) # type: ignore diff --git a/tests/test_detectors/test_new_event_detector.py b/tests/test_detectors/test_new_event_detector.py index b47083fa..d2612b17 100644 --- a/tests/test_detectors/test_new_event_detector.py +++ b/tests/test_detectors/test_new_event_detector.py @@ -211,6 +211,34 @@ def test_audit_log_anomalies_via_process(self): assert detected_ids == {"1863"} + @pytest.mark.ignored + def test_audit_log_anomalie_fed(self): + parser = MatcherParser(config=_PARSER_CONFIG) + detector1 = NewEventDetector() + detector2 = NewEventDetector() + + logs = list(From.log(parser, in_path=AUDIT_LOG, do_process=True)) + for log in logs[:TRAIN_UNTIL]: + detector1.configure(log) + detector2.configure(log) + + detector1.set_configuration() + detector2.set_configuration() + + for log in logs[:TRAIN_UNTIL]: + detector1.train(log) + + (detector1 + detector2).aggregate() + assert detector2.persistency == detector1.persistency + + detected_ids: set[str] = set() + for log in logs[TRAIN_UNTIL:]: + output = schemas.DetectorSchema() + if detector2.detect(log, output_=output): + detected_ids.add(log["logID"]) + + assert detected_ids == {"1863"} + class TestNewEventDetectorGlobalInstances: """Tests event-ID-independent global instance detection.""" From 94ee27ac62af99ea1e948b54e99a69650f906545 Mon Sep 17 00:00:00 2001 From: "angre.garcia-gomez@ait.ac.at" Date: Mon, 14 Sep 2026 12:55:58 +0200 Subject: [PATCH 19/28] add SCVS in Variables logic and federation --- .../detectors/scvs_detector.py | 36 +++++------ tests/test_detectors/test_charset_detector.py | 60 +++++++++---------- tests/test_detectors/test_scvs_detector.py | 21 +++++++ 3 files changed, 66 insertions(+), 51 deletions(-) diff --git a/src/detectmatelibrary/detectors/scvs_detector.py b/src/detectmatelibrary/detectors/scvs_detector.py index e7289be7..949cdcd8 100644 --- a/src/detectmatelibrary/detectors/scvs_detector.py +++ b/src/detectmatelibrary/detectors/scvs_detector.py @@ -1,7 +1,9 @@ from typing import Any, List from detectmatelibrary.common.detector import CoreDetector, CoreDetectorConfig -from detectmatelibrary.utils import persistency + +from detectmatelibrary.common._other_op._variable_hooks import VariablesLogic + from detectmatelibrary.utils.data_buffer import BufferMode from detectmatelibrary.utils.sequence_encoding import ( build_count_vec, @@ -17,7 +19,7 @@ class SCVSDetectorConfig(CoreDetectorConfig): window_size: int = 10 -class SCVSDetector(CoreDetector): +class SCVSDetector(CoreDetector, VariablesLogic): def __init__( self, name: str = "SCVSDetector", @@ -28,35 +30,24 @@ def __init__( config = SCVSDetectorConfig.from_dict(config, name) self.config: SCVSDetectorConfig - super().__init__( - name=name, - buffer_mode=BufferMode.WINDOW, - config=config, - buffer_size=config.window_size - ) - # ponytail: only events_seen is used here — count vectors carry no - # variables. EventPersistency still requires an event_data_class. - self.persistency = persistency.EventPersistency( - event_data_class=persistency.EventStabilityTracker, + CoreDetector.__init__( + self, name=name, buffer_mode=BufferMode.WINDOW, config=config, buffer_size=config.window_size ) - self._register_persistency(self.persistency) # restores state when auto_load + VariablesLogic.__init__(self, name=self.name) + self._register_persistency(self.persistency) warn_on_window_size_mismatch(self.name, self.persistency, self.config.window_size) def import_state( self, path: str | bytes, storage_options: dict[str, Any] | None = None ) -> None: - """Load state, then check it was trained at the configured window size. - - Unlike `auto_load`, this runs after construction, so the check in - `__init__` has already passed and has to be redone here. - """ - super().import_state(path, storage_options) + CoreDetector.import_state(self, path, storage_options) warn_on_window_size_mismatch(self.name, self.persistency, self.config.window_size) def train(self, input_: List[schemas.ParserSchema]) -> None: # type: ignore - self.persistency.ingest_event( + self._ingest( event_id=encode_count_vec(self.config.window_size, build_count_vec(input_)), - event_template=input_[-1]["template"], + input_=input_[-1], + variables={} ) def detect( @@ -77,3 +68,6 @@ def get_known_count_vecs(self) -> set[tuple[int, ...]]: decode_count_vec(str(encoded))[1] for encoded in self.persistency.get_events_seen() } + + def aggregate_strategy(self, components: set["SCVSDetector"]) -> None: # type: ignore + self.combine(components) # type: ignore diff --git a/tests/test_detectors/test_charset_detector.py b/tests/test_detectors/test_charset_detector.py index 6d566420..ed19c9d4 100644 --- a/tests/test_detectors/test_charset_detector.py +++ b/tests/test_detectors/test_charset_detector.py @@ -313,6 +313,36 @@ def test_audit_log_anomalies(self): assert detected_ids == {'1859', '1860', '1861', '1862', '1864', '1865', '1866', '1867'} + @pytest.mark.ignored + def test_audit_log_anomalie_fed(self): + parser = MatcherParser(config=_PARSER_CONFIG) + detector1 = CharsetDetector() + detector2 = CharsetDetector() + + logs = list(From.log(parser, in_path=AUDIT_LOG, do_process=True)) + for log in logs[:TRAIN_UNTIL]: + detector1.configure(log) + detector2.configure(log) + + detector1.set_configuration() + detector2.set_configuration() + + for log in logs[:TRAIN_UNTIL]: + detector1.train(log) + + assert len(detector2.persistency) == 0 + + (detector1 + detector2).aggregate() + assert detector2.persistency == detector1.persistency + assert len(detector2.persistency) != 0 + + detected_ids: set[str] = set() + for log in logs[TRAIN_UNTIL:]: + if detector2.process(log) is not None: + detected_ids.add(log["logID"]) + + assert detected_ids == {'1859', '1860', '1861', '1862', '1864', '1865', '1866', '1867'} + class TestCharsetDetectorAutoConfig: """Test that process() drives configure/set_configuration/train/detect @@ -347,36 +377,6 @@ def test_audit_log_anomalies_via_process(self): assert detected_ids == {'1859', '1860', '1861', '1862', '1864', '1865', '1866', '1867'} - @pytest.mark.ignored - def test_audit_log_anomalie_fed(self): - parser = MatcherParser(config=_PARSER_CONFIG) - detector1 = CharsetDetector() - detector2 = CharsetDetector() - - logs = list(From.log(parser, in_path=AUDIT_LOG, do_process=True)) - for log in logs[:TRAIN_UNTIL]: - detector1.configure(log) - detector2.configure(log) - - detector1.set_configuration() - detector2.set_configuration() - - for log in logs[:TRAIN_UNTIL]: - detector1.train(log) - - assert len(detector2.persistency) == 0 - - (detector1 + detector2).aggregate() - assert detector2.persistency == detector1.persistency - assert len(detector2.persistency) != 0 - - detected_ids: set[str] = set() - for log in logs[TRAIN_UNTIL:]: - if detector2.process(log) is not None: - detected_ids.add(log["logID"]) - - assert detected_ids == {'1859', '1860', '1861', '1862', '1864', '1865', '1866', '1867'} - class TestCharsetDetectorGlobalInstances: """Tests event-ID-independent global instance detection.""" diff --git a/tests/test_detectors/test_scvs_detector.py b/tests/test_detectors/test_scvs_detector.py index 7d873805..e1668351 100644 --- a/tests/test_detectors/test_scvs_detector.py +++ b/tests/test_detectors/test_scvs_detector.py @@ -97,3 +97,24 @@ def test_audit_log_anomalies(self): for log_id in {'1859', '1860', '1861', '1862', '1864', '1865', '1866', '1867'}: assert log_id in detected_ids + + @pytest.mark.ignored + def test_audit_log_anomalie_fed(self): + parser = MatcherParser(config=PIPELINE_CONFIG) + detector1 = SCVSDetector() + detector2 = SCVSDetector() + + logs = list(From.log(parser, in_path=AUDIT_LOG, do_process=True)) + for log in logs[:TRAIN_UNTIL]: + detector1.process(log) + + (detector1 + detector2).aggregate() + assert detector2.persistency == detector1.persistency + + detected_ids: set[str] = set() + for log in logs[TRAIN_UNTIL:]: + if detector2.process(log) is not None: + detected_ids.add(log["logID"]) + + for log_id in {'1859', '1860', '1861', '1862', '1864', '1865', '1866', '1867'}: + assert log_id in detected_ids From 9fc027298c77f76cc6de856bbb83a2cd4605e6a8 Mon Sep 17 00:00:00 2001 From: "angre.garcia-gomez@ait.ac.at" Date: Mon, 14 Sep 2026 13:38:01 +0200 Subject: [PATCH 20/28] Add ECVC in VariablesLogic and federated --- .../detectors/ecvc_detector.py | 58 ++++++++----------- tests/test_detectors/test_ecvc_detector.py | 17 ++++++ 2 files changed, 41 insertions(+), 34 deletions(-) diff --git a/src/detectmatelibrary/detectors/ecvc_detector.py b/src/detectmatelibrary/detectors/ecvc_detector.py index 2b92888b..9e96ea14 100644 --- a/src/detectmatelibrary/detectors/ecvc_detector.py +++ b/src/detectmatelibrary/detectors/ecvc_detector.py @@ -1,7 +1,8 @@ from typing import Any, Collection, List from detectmatelibrary.common.detector import CoreDetector, CoreDetectorConfig -from detectmatelibrary.utils import persistency +from detectmatelibrary.common._other_op._variable_hooks import VariablesLogic + from detectmatelibrary.utils.data_buffer import BufferMode from detectmatelibrary.utils.sequence_encoding import ( build_count_vec, @@ -70,7 +71,7 @@ class ECVCDetectorConfig(CoreDetectorConfig): threshold_method: str = "mean" -class ECVCDetector(CoreDetector): +class ECVCDetector(CoreDetector, VariablesLogic): def __init__( self, name: str = "ECVCDetector", @@ -81,50 +82,32 @@ def __init__( config = ECVCDetectorConfig.from_dict(config, name) self.config: ECVCDetectorConfig - super().__init__( - name=name, - buffer_mode=BufferMode.WINDOW, - config=config, - buffer_size=config.window_size + CoreDetector.__init__( + self, name=name, buffer_mode=BufferMode.WINDOW, config=config, buffer_size=config.window_size ) + VariablesLogic.__init__(self, name=self.name) + self._register_persistency(self.persistency) + warn_on_window_size_mismatch(self.name, self.persistency, self.config.window_size) + self.count_vecs: np.ndarray | None = None self.threshold: float = 0 - # ponytail: only events_seen is used here — count vectors carry no - # variables. EventPersistency still requires an event_data_class. - self.persistency = persistency.EventPersistency( - event_data_class=persistency.EventStabilityTracker, - ) - self._register_persistency(self.persistency) # restores state when auto_load - warn_on_window_size_mismatch(self.name, self.persistency, self.config.window_size) - self._derive() # no-op unless auto_load restored count vectors + self.build_count_vec() # no-op unless auto_load restored count vectors def import_state( self, path: str | bytes, storage_options: dict[str, Any] | None = None ) -> None: - """Load state, then rebuild the matrix and threshold from it. - - Unlike `auto_load`, this runs after construction, so the derivation in - `__init__` has already run against an empty store and has to be redone. - """ - super().import_state(path, storage_options) + CoreDetector.import_state(self, path, storage_options) warn_on_window_size_mismatch(self.name, self.persistency, self.config.window_size) - self._derive() + self.build_count_vec() def train(self, input_: List[schemas.ParserSchema]) -> None: # type: ignore - self.persistency.ingest_event( + self._ingest( event_id=encode_count_vec(self.config.window_size, ECVCOp.build_count_vec(input_)), - event_template=input_[-1]["template"], + input_=input_[-1], + variables={} ) - def _derive(self) -> None: - """Build the count vector matrix and threshold from the learned - vectors. - - The vectors are sorted first: restored keys are strings, whose set - iteration order is hash-randomized per process, and the seeded shuffle - below splits train from validation by that order. Sorting makes a - restored model identical to a freshly trained one. - """ + def build_count_vec(self) -> None: seqs = sorted( decode_count_vec(str(encoded))[1] for encoded in self.persistency.get_events_seen() @@ -143,7 +126,7 @@ def _derive(self) -> None: ) def post_train(self) -> None: - self._derive() + self.build_count_vec() def detect( self, input_: List[schemas.ParserSchema], output_: schemas.DetectorSchema, # type: ignore @@ -161,3 +144,10 @@ def detect( return True return False + + def aggregate_strategy(self, components: set["ECVCDetector"]) -> None: # type: ignore + self.combine(components) # type: ignore + + self.build_count_vec() + for component in components: + component.build_count_vec() diff --git a/tests/test_detectors/test_ecvc_detector.py b/tests/test_detectors/test_ecvc_detector.py index ca9ee8d4..6f1b73c5 100644 --- a/tests/test_detectors/test_ecvc_detector.py +++ b/tests/test_detectors/test_ecvc_detector.py @@ -150,3 +150,20 @@ def test_audit_log_anomalies(self): for log_id in {'1859', '1860', '1861', '1862', '1864', '1865', '1866', '1867'}: assert log_id in detected_ids + + @pytest.mark.ignored + def test_audit_log_anomalie_fed(self): + parser = MatcherParser(config=PIPELINE_CONFIG) + detector1 = ECVCDetector(config=PIPELINE_CONFIG) + detector2 = ECVCDetector(config=PIPELINE_CONFIG) + + logs = list(From.log(parser, in_path=AUDIT_LOG, do_process=True)) + for log in logs: + detector1.process(log) + + thress = detector1.threshold + (detector1 + detector2).aggregate() + + assert detector2.persistency == detector1.persistency + assert (detector2.count_vecs == detector1.count_vecs).all() + assert detector2.threshold == thress From 7b815ac8c9a4ffa1ba5aa55351f689c373931a32 Mon Sep 17 00:00:00 2001 From: "angre.garcia-gomez@ait.ac.at" Date: Mon, 14 Sep 2026 13:57:33 +0200 Subject: [PATCH 21/28] EventSequence use VariablesLogic and federated --- .../detectors/event_sequence_detector.py | 78 +++++++++---------- .../test_event_sequence_detector.py | 36 +++++++++ 2 files changed, 75 insertions(+), 39 deletions(-) diff --git a/src/detectmatelibrary/detectors/event_sequence_detector.py b/src/detectmatelibrary/detectors/event_sequence_detector.py index 162da14f..f49717b3 100644 --- a/src/detectmatelibrary/detectors/event_sequence_detector.py +++ b/src/detectmatelibrary/detectors/event_sequence_detector.py @@ -1,20 +1,21 @@ -"""Detect EventID sequences that were not observed during training.""" - -from collections import deque -from typing import Any - -from pydantic import Field, model_validator - +from detectmatelibrary.common._other_op._variable_hooks import VariablesLogic from detectmatelibrary.common._config._compile import generate_events_config -from detectmatelibrary.common.detector import CoreDetectorConfig, CoreDetector from detectmatelibrary.common._config import AutoConfigParams +from detectmatelibrary.common.detector import CoreDetectorConfig, CoreDetector + from detectmatelibrary.tools.logging import logger -from detectmatelibrary.utils import persistency -from detectmatelibrary.utils.data_buffer import BufferMode from detectmatelibrary.utils.sequence_encoding import decode_sequence, encode_sequence +from detectmatelibrary.utils.data_buffer import BufferMode +from detectmatelibrary.utils import persistency + from detectmatelibrary.schemas import ParserSchema, DetectorSchema +from collections import deque +from typing import Any + +from pydantic import Field, model_validator + class SequenceAutoConfigParams(AutoConfigParams): """Configure-phase inputs: the candidate window lengths to try. @@ -49,7 +50,7 @@ class EventSequenceDetectorConfig(CoreDetectorConfig): auto_config_params: SequenceAutoConfigParams = SequenceAutoConfigParams() -class EventSequenceDetector(CoreDetector): +class EventSequenceDetector(CoreDetector, VariablesLogic): """Detect EventID sequences not encountered in training as anomalies.""" def __init__( @@ -60,25 +61,16 @@ def __init__( if isinstance(config, dict): config = EventSequenceDetectorConfig.from_dict(config, name) - super().__init__(name=name, buffer_mode=BufferMode.NO_BUF, config=config) + CoreDetector.__init__(self, name=name, buffer_mode=BufferMode.NO_BUF, config=config) self.config: EventSequenceDetectorConfig - # CoreComponent.process() calls train() *and* run()->detect() for every - # training event, so a single shared window would ingest each event twice. - # maxlen is None while unconfigured, but nothing is appended in that state. self._train_window: deque[int] = deque(maxlen=self.config.fixed_window_size) self._detect_window: deque[int] = deque(maxlen=self.config.fixed_window_size) - # ponytail: only events_seen is used here — sequences carry no variables. - # EventPersistency still requires an event_data_class, and changing it would - # change the on-disk format for no gain. - self.persistency = persistency.EventPersistency( - event_data_class=persistency.EventStabilityTracker, - ) self._configure_windows: dict[int, deque[int]] = {} - self.auto_conf_persistency = persistency.EventPersistency( - event_data_class=persistency.EventStabilityTracker - ) - self._register_persistency(self.persistency) # restores state when auto_load - self._restored_length = self._adopt_restored_length() + + VariablesLogic.__init__(self, name=self.name) + self._register_persistency(self.persistency) + self._adopt_restored_length() + if not self.config.auto_config and self.config.fixed_window_size is None: logger.warning( f"[{self.name}] auto_config=False but no fixed_window_size was given. " @@ -91,7 +83,7 @@ def _set_window_length(self, length: int) -> None: self._train_window = deque(self._train_window, maxlen=length) self._detect_window = deque(self._detect_window, maxlen=length) - def _adopt_restored_length(self) -> int | None: + def _adopt_restored_length(self) -> None: """Align `fixed_window_size` with restored state, if any. Sequences are stored as fixed-length n-grams, so a model trained at one @@ -103,16 +95,17 @@ def _adopt_restored_length(self) -> int | None: """ restored = self.persistency.get_events_seen() if not restored: - return None - length = len(decode_sequence(str(next(iter(restored))))) - if length != self.config.fixed_window_size: - logger.warning( - f"[{self.name}] restored state holds sequences of length {length}, but " - f"fixed_window_size is {self.config.fixed_window_size}. Using the " - "persisted length — the restored model is only valid at that length." - ) - self._set_window_length(length) - return length + self._restored_length = None + else: + length = len(decode_sequence(str(next(iter(restored))))) + if length != self.config.fixed_window_size: + logger.warning( + f"[{self.name}] restored state holds sequences of length {length}, but " + f"fixed_window_size is {self.config.fixed_window_size}. Using the " + "persisted length — the restored model is only valid at that length." + ) + self._set_window_length(length) + self._restored_length = length def import_state( self, path: str | bytes, storage_options: dict[str, Any] | None = None @@ -122,8 +115,8 @@ def import_state( Unlike `auto_load`, this runs after construction, so the length check in `__init__` has already passed and has to be redone here. """ - super().import_state(path, storage_options) - self._restored_length = self._adopt_restored_length() + CoreDetector.import_state(self, path, storage_options) + self._adopt_restored_length() def train(self, input_: ParserSchema) -> None: # type: ignore """Train the detector by learning EventID sequences from the input @@ -260,3 +253,10 @@ def get_known_sequences(self) -> set[tuple[int, ...]]: decode_sequence(str(encoded)) for encoded in self.persistency.get_events_seen() } + + def aggregate_strategy(self, components: set["EventSequenceDetector"]) -> None: # type: ignore + self.combine(components) # type: ignore + + self._adopt_restored_length() + for component in components: + component._adopt_restored_length() diff --git a/tests/test_detectors/test_event_sequence_detector.py b/tests/test_detectors/test_event_sequence_detector.py index 0074e2bc..27f9ead3 100644 --- a/tests/test_detectors/test_event_sequence_detector.py +++ b/tests/test_detectors/test_event_sequence_detector.py @@ -205,6 +205,7 @@ def test_train_and_detect_windows_are_independent(self): class TestEventSequenceDetectorEndToEnd: """Regression test: full train/detect pipeline on audit.log.""" + @pytest.mark.ignored def test_audit_log_anomalies(self): pars = MatcherParser(config=_PARSER_CONFIG) detector = EventSequenceDetector( @@ -227,6 +228,41 @@ def test_audit_log_anomalies(self): # fixed_window_size=3 that is three consecutive log IDs. assert detected_ids == {"1863", "1864", "1865"} + @pytest.mark.ignored + def test_audit_log_anomalie_fed(self): + parser = MatcherParser(config=_PARSER_CONFIG) + detector1 = EventSequenceDetector( + config=EventSequenceDetectorConfig(auto_config=False, fixed_window_size=3), + name="EventSequenceDetector", + ) + detector2 = EventSequenceDetector( + config=EventSequenceDetectorConfig(auto_config=False, fixed_window_size=3), + name="EventSequenceDetector", + ) + + logs = list(From.log(parser, in_path=AUDIT_LOG, do_process=True)) + for log in logs[:TRAIN_UNTIL]: + detector1.configure(log) + detector2.configure(log) + + detector1.set_configuration() + detector2.set_configuration() + + for log in logs[:TRAIN_UNTIL]: + detector1.train(log) + + (detector1 + detector2).aggregate() + assert detector2.persistency == detector1.persistency + + detected_ids: set[str] = set() + for log in logs[TRAIN_UNTIL:]: + output = schemas.DetectorSchema() + if detector2.detect(log, output_=output): + detected_ids.add(log["logID"]) + + assert detected_ids == {"1863", "1864", "1865"} + + @pytest.mark.ignored def test_audit_log_anomalies_via_process(self): """Same regression, driven through process() so the configure -> set_configuration -> train -> detect lifecycle is exercised.""" From 47c27145b638601613e1a1ad1678a63309adc2f8 Mon Sep 17 00:00:00 2001 From: "angre.garcia-gomez@ait.ac.at" Date: Mon, 14 Sep 2026 14:57:15 +0200 Subject: [PATCH 22/28] diasble bigram to and from binary, add charset tst for binary and from --- .../common/_other_op/_variable_hooks.py | 7 ++++++ .../common/variable_detector.py | 6 +++++ .../detectors/bigram_frequency_detector.py | 9 ++++++- tests/test_detectors/test_charset_detector.py | 25 +++++++++++++++++++ 4 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/detectmatelibrary/common/_other_op/_variable_hooks.py b/src/detectmatelibrary/common/_other_op/_variable_hooks.py index c71b5451..cae4eb80 100644 --- a/src/detectmatelibrary/common/_other_op/_variable_hooks.py +++ b/src/detectmatelibrary/common/_other_op/_variable_hooks.py @@ -3,6 +3,7 @@ EventStabilityTracker, SingleStabilityTracker ) from detectmatelibrary.utils.persistency.event_persistency import EventPersistency +from detectmatelibrary.utils.persistency.persistency_saver import load, save from detectmatelibrary.utils.time_format_handler import TimeFormatHandler from detectmatelibrary.common._config._formats import _EventInstance @@ -209,3 +210,9 @@ def combine(self, components: set["VariablesLogic"]) -> None: for component in components: component.persistency = self.persistency + + def persistency2binary(self) -> bytes: + return save(self.persistency) # type: ignore + + def binary2persistency(self, binary: bytes) -> None: + load(self.persistency, path=binary) diff --git a/src/detectmatelibrary/common/variable_detector.py b/src/detectmatelibrary/common/variable_detector.py index 96d38a57..a5b3f4cf 100644 --- a/src/detectmatelibrary/common/variable_detector.py +++ b/src/detectmatelibrary/common/variable_detector.py @@ -142,3 +142,9 @@ def set_configuration(self) -> None: def aggregate_strategy(self, components: set["VariableDetector"]) -> None: # type: ignore self.combine(components) # type: ignore + + def to_binary(self) -> bytes: + return self.persistency2binary() + + def from_binary(self, binary: bytes) -> None: + self.binary2persistency(binary) diff --git a/src/detectmatelibrary/detectors/bigram_frequency_detector.py b/src/detectmatelibrary/detectors/bigram_frequency_detector.py index d0dfa8a0..f6bd9837 100644 --- a/src/detectmatelibrary/detectors/bigram_frequency_detector.py +++ b/src/detectmatelibrary/detectors/bigram_frequency_detector.py @@ -9,7 +9,7 @@ ) from detectmatelibrary.schemas import ParserSchema from detectmatelibrary.constants import GLOBAL_EVENT_ID, DEFAULT_FREQUENCIES - +import warnings _DEFAULT_FREQ: dict[str, dict[str, int]] | None = None _DEFAULT_TOTAL_FREQ: dict[str, int] | None = None @@ -230,3 +230,10 @@ def _check_event( ) anomaly = True return 1.0 if anomaly else 0.0 + + def to_binary(self) -> bytes: + warnings.warn("Diasbale for now") + return bytes() + + def from_binary(self, binary: bytes) -> None: + warnings.warn("Diasbale for now") diff --git a/tests/test_detectors/test_charset_detector.py b/tests/test_detectors/test_charset_detector.py index ed19c9d4..ad0a664a 100644 --- a/tests/test_detectors/test_charset_detector.py +++ b/tests/test_detectors/test_charset_detector.py @@ -313,6 +313,31 @@ def test_audit_log_anomalies(self): assert detected_ids == {'1859', '1860', '1861', '1862', '1864', '1865', '1866', '1867'} + @pytest.mark.ignored + def test_audit_log_anomalies_to_binary(self): + parser = MatcherParser(config=_PARSER_CONFIG) + detector = CharsetDetector() + + logs = list(From.log(parser, in_path=AUDIT_LOG, do_process=True)) + + for log in logs[:TRAIN_UNTIL]: + detector.configure(log) + detector.set_configuration() + + for log in logs[:TRAIN_UNTIL]: + detector.train(log) + + detector2 = CharsetDetector() + detector2.from_binary(detector.to_binary()) + + detected_ids: set[str] = set() + for log in logs[TRAIN_UNTIL:]: + output = schemas.DetectorSchema() + if detector2.detect(log, output_=output): + detected_ids.add(log["logID"]) + + assert detected_ids == {'1859', '1860', '1861', '1862', '1864', '1865', '1866', '1867'} + @pytest.mark.ignored def test_audit_log_anomalie_fed(self): parser = MatcherParser(config=_PARSER_CONFIG) From a5e516bfb7ff52c73c195643e6d10b61905bd0fc Mon Sep 17 00:00:00 2001 From: "angre.garcia-gomez@ait.ac.at" Date: Mon, 14 Sep 2026 15:04:54 +0200 Subject: [PATCH 23/28] add New vaue, combo and range binary tests --- .../test_new_value_combo_detector.py | 25 +++++++ .../test_detectors/test_new_value_detector.py | 61 ++++++++++++----- .../test_value_range_detector.py | 65 +++++++++++++------ 3 files changed, 113 insertions(+), 38 deletions(-) diff --git a/tests/test_detectors/test_new_value_combo_detector.py b/tests/test_detectors/test_new_value_combo_detector.py index aa37e8c6..88032301 100644 --- a/tests/test_detectors/test_new_value_combo_detector.py +++ b/tests/test_detectors/test_new_value_combo_detector.py @@ -589,6 +589,31 @@ def test_audit_log_anomalies(self): assert detected_ids == {"1859", "1862", "1865", "1866"} + @pytest.mark.ignored + def test_audit_log_anomalies_to_binary(self): + parser = MatcherParser(config=_PARSER_CONFIG) + detector = NewValueComboDetector() + + logs = list(From.log(parser, in_path=AUDIT_LOG, do_process=True)) + + for log in logs[:TRAIN_UNTIL]: + detector.configure(log) + detector.set_configuration() + + for log in logs[:TRAIN_UNTIL]: + detector.train(log) + + detector2 = NewValueComboDetector() + detector2.from_binary(detector.to_binary()) + + detected_ids: set[str] = set() + for log in logs[TRAIN_UNTIL:]: + output = schemas.DetectorSchema() + if detector2.detect(log, output_=output): + detected_ids.add(log["logID"]) + + assert detected_ids == {"1859", "1862", "1865", "1866"} + @pytest.mark.ignored def test_audit_log_anomalie_fed(self): parser = MatcherParser(config=_PARSER_CONFIG) diff --git a/tests/test_detectors/test_new_value_detector.py b/tests/test_detectors/test_new_value_detector.py index 82018580..79cd4be8 100644 --- a/tests/test_detectors/test_new_value_detector.py +++ b/tests/test_detectors/test_new_value_detector.py @@ -239,36 +239,27 @@ def test_audit_log_anomalies(self): assert detected_ids == {'1859', '1860', '1861', '1862', '1864', '1865', '1866', '1867'} - -class TestNewValueDetectorAutoConfig: - """Test that process() drives configure/set_configuration/train/detect - automatically.""" - @pytest.mark.ignored - def test_audit_log_anomalies_via_process(self): + def test_audit_log_anomalies_to_binary(self): parser = MatcherParser(config=_PARSER_CONFIG) detector = NewValueDetector() logs = list(From.log(parser, in_path=AUDIT_LOG, do_process=True)) - # Phase 1: configure — keep configuring for logs[:TRAIN_UNTIL] - detector.fitlogic.config_state.current = EnumState.KEEP for log in logs[:TRAIN_UNTIL]: - detector.process(log) - - # Transition: stop configure so next process() call triggers set_configuration() - detector.fitlogic.config_state.current = EnumState.STOP + detector.configure(log) + detector.set_configuration() - # Phase 2: train — keep training for logs[:TRAIN_UNTIL] - detector.fitlogic.train_state.current = EnumState.KEEP for log in logs[:TRAIN_UNTIL]: - detector.process(log) + detector.train(log) + + detector2 = NewValueDetector() + detector2.from_binary(detector.to_binary()) - # Phase 3: detect — stop training so process() only calls detect() - detector.fitlogic.train_state.current = EnumState.STOP detected_ids: set[str] = set() for log in logs[TRAIN_UNTIL:]: - if detector.process(log) is not None: + output = schemas.DetectorSchema() + if detector2.detect(log, output_=output): detected_ids.add(log["logID"]) assert detected_ids == {'1859', '1860', '1861', '1862', '1864', '1865', '1866', '1867'} @@ -305,6 +296,40 @@ def test_audit_log_anomalie_fed(self): assert detected_ids == {'1859', '1860', '1861', '1862', '1864', '1865', '1866', '1867'} +class TestNewValueDetectorAutoConfig: + """Test that process() drives configure/set_configuration/train/detect + automatically.""" + + @pytest.mark.ignored + def test_audit_log_anomalies_via_process(self): + parser = MatcherParser(config=_PARSER_CONFIG) + detector = NewValueDetector() + + logs = list(From.log(parser, in_path=AUDIT_LOG, do_process=True)) + + # Phase 1: configure — keep configuring for logs[:TRAIN_UNTIL] + detector.fitlogic.config_state.current = EnumState.KEEP + for log in logs[:TRAIN_UNTIL]: + detector.process(log) + + # Transition: stop configure so next process() call triggers set_configuration() + detector.fitlogic.config_state.current = EnumState.STOP + + # Phase 2: train — keep training for logs[:TRAIN_UNTIL] + detector.fitlogic.train_state.current = EnumState.KEEP + for log in logs[:TRAIN_UNTIL]: + detector.process(log) + + # Phase 3: detect — stop training so process() only calls detect() + detector.fitlogic.train_state.current = EnumState.STOP + detected_ids: set[str] = set() + for log in logs[TRAIN_UNTIL:]: + if detector.process(log) is not None: + detected_ids.add(log["logID"]) + + assert detected_ids == {'1859', '1860', '1861', '1862', '1864', '1865', '1866', '1867'} + + class TestNewValueDetectorGlobalInstances: """Tests event-ID-independent global instance detection.""" diff --git a/tests/test_detectors/test_value_range_detector.py b/tests/test_detectors/test_value_range_detector.py index ac0993aa..349b964e 100644 --- a/tests/test_detectors/test_value_range_detector.py +++ b/tests/test_detectors/test_value_range_detector.py @@ -380,38 +380,27 @@ def test_audit_log_anomalies(self): # uid is not always 0 for event id 0. uid=1002 in line 1864 is a different event id. assert detected_ids == {'1859', '1860', '1861', '1862'} - -class TestValueRangeDetectorAutoConfig: - """Test that process() drives configure/set_configuration/train/detect - automatically.""" - @pytest.mark.ignored - def test_audit_log_anomalies_via_process(self): + def test_audit_log_anomalies_to_binary(self): parser = MatcherParser(config=_PARSER_CONFIG) detector = ValueRangeDetector() logs = list(From.log(parser, in_path=AUDIT_LOG, do_process=True)) - # Phase 1: configure — keep configuring for logs[:TRAIN_UNTIL] - detector.fitlogic.config_state.current = EnumState.KEEP for log in logs[:TRAIN_UNTIL]: - detector.process(log) - - # Transition: stop configure so next process() call triggers set_configuration() - detector.fitlogic.config_state.current = EnumState.STOP + detector.configure(log) + detector.set_configuration() - # Phase 2: train — keep training for logs[:TRAIN_UNTIL] - detector.fitlogic.train_state.current = EnumState.KEEP for log in logs[:TRAIN_UNTIL]: - logger.setLevel(logging.CRITICAL) - detector.process(log) - logger.setLevel(logging.DEBUG) + detector.train(log) + + detector2 = ValueRangeDetector() + detector2.from_binary(detector.to_binary()) - # Phase 3: detect — stop training so process() only calls detect() - detector.fitlogic.train_state.current = EnumState.STOP detected_ids: set[str] = set() for log in logs[TRAIN_UNTIL:]: - if detector.process(log) is not None: + output = schemas.DetectorSchema() + if detector2.detect(log, output_=output): detected_ids.add(log["logID"]) assert detected_ids == {'1859', '1860', '1861', '1862'} @@ -448,6 +437,42 @@ def test_audit_log_anomalie_fed(self): assert detected_ids == {'1859', '1860', '1861', '1862'} +class TestValueRangeDetectorAutoConfig: + """Test that process() drives configure/set_configuration/train/detect + automatically.""" + + @pytest.mark.ignored + def test_audit_log_anomalies_via_process(self): + parser = MatcherParser(config=_PARSER_CONFIG) + detector = ValueRangeDetector() + + logs = list(From.log(parser, in_path=AUDIT_LOG, do_process=True)) + + # Phase 1: configure — keep configuring for logs[:TRAIN_UNTIL] + detector.fitlogic.config_state.current = EnumState.KEEP + for log in logs[:TRAIN_UNTIL]: + detector.process(log) + + # Transition: stop configure so next process() call triggers set_configuration() + detector.fitlogic.config_state.current = EnumState.STOP + + # Phase 2: train — keep training for logs[:TRAIN_UNTIL] + detector.fitlogic.train_state.current = EnumState.KEEP + for log in logs[:TRAIN_UNTIL]: + logger.setLevel(logging.CRITICAL) + detector.process(log) + logger.setLevel(logging.DEBUG) + + # Phase 3: detect — stop training so process() only calls detect() + detector.fitlogic.train_state.current = EnumState.STOP + detected_ids: set[str] = set() + for log in logs[TRAIN_UNTIL:]: + if detector.process(log) is not None: + detected_ids.add(log["logID"]) + + assert detected_ids == {'1859', '1860', '1861', '1862'} + + class TestValueRangeDetectorGlobalInstances: """Tests event-ID-independent global instance detection.""" From 7991b83c519913040fe8b1fad4a94140d375e9e0 Mon Sep 17 00:00:00 2001 From: "angre.garcia-gomez@ait.ac.at" Date: Mon, 14 Sep 2026 15:17:01 +0200 Subject: [PATCH 24/28] update docs --- docs/detectors/bigram_frequency.md | 2 + docs/detectors/charset.md | 2 + docs/detectors/combo.md | 2 + docs/detectors/ecvc_detector.md | 3 + docs/detectors/event_sequence.md | 2 + docs/detectors/new_event.md | 3 + docs/detectors/new_value.md | 3 + docs/detectors/scvs_detector.md | 1 + docs/detectors/value_range.md | 3 + .../test_detectors/test_new_event_detector.py | 56 +++++++++---------- 10 files changed, 49 insertions(+), 28 deletions(-) diff --git a/docs/detectors/bigram_frequency.md b/docs/detectors/bigram_frequency.md index 3d1c7d7a..1975d1a9 100644 --- a/docs/detectors/bigram_frequency.md +++ b/docs/detectors/bigram_frequency.md @@ -7,6 +7,8 @@ The Bigram Frequency Detector raises alerts when a variable's character bigrams | **Input** | [ParserSchema](../schemas.md) | Structured log | | **Output** | [DetectorSchema](../schemas.md) | Alert / finding | +✅ Federation compatible (Binary not available). + ## Description For each configured variable, the detector walks every observed value character-by-character (with virtual boundary characters before the first and after the last) and updates a per-(event, variable) bigram frequency table. At detect time, the average per-bigram conditional probability of a new value is computed against this table. Values scoring below `prob_thresh` (default `0.05`) are flagged. When `default_freqs` is enabled, a built-in English bigram table acts as a fallback for bigrams unseen during training. diff --git a/docs/detectors/charset.md b/docs/detectors/charset.md index 63c71689..f4ebf568 100644 --- a/docs/detectors/charset.md +++ b/docs/detectors/charset.md @@ -7,6 +7,8 @@ The Charset Detector raises alerts when previously unseen characters appear in c | **Input** | [ParserSchema](../schemas.md) | Structured log | | **Output** | [DetectorSchema](../schemas.md) | Alert / finding | +✅ Federation compatible, + ## Description This detector maintains a lightweight set of observed characters per monitored field and emits an alert when a character not present in the set is seen for the first time (subject to configuration). diff --git a/docs/detectors/combo.md b/docs/detectors/combo.md index 6bbeaa70..a98552c1 100644 --- a/docs/detectors/combo.md +++ b/docs/detectors/combo.md @@ -7,6 +7,8 @@ The New Combo Value Detector raises alerts when previously unseen combinations o | **Input** | [ParserSchema](../schemas.md) | Structured log | | **Output** | [DetectorSchema](../schemas.md) | Combined alert / finding | +✅ Federation compatible (Binary not available). + ## Description This detector maintains a lightweight set of observed combination of values per monitored fields and emits an alert when a combination is not present in the set seen for the first time (subject to configuration). diff --git a/docs/detectors/ecvc_detector.md b/docs/detectors/ecvc_detector.md index feb39782..9d64f167 100644 --- a/docs/detectors/ecvc_detector.md +++ b/docs/detectors/ecvc_detector.md @@ -7,6 +7,9 @@ The Event Count Vector Clustering Detector (ECVC) detects anomalies by calculati | **Input** | [ParserSchema](../schemas.md) | Structured log | | **Output** | [DetectorSchema](../schemas.md) | Alert / finding | +✅ Federation compatible (Binary not available). + + ## Description A count vector is form by counting the number of appearance of each event ID in a sequence of a specific window size. diff --git a/docs/detectors/event_sequence.md b/docs/detectors/event_sequence.md index 8759dfff..c4dc65e9 100644 --- a/docs/detectors/event_sequence.md +++ b/docs/detectors/event_sequence.md @@ -7,6 +7,8 @@ The Event Sequence Detector raises alerts when a run of consecutive event IDs ap | **Input** | [ParserSchema](../schemas.md) | Structured log | | **Output** | [DetectorSchema](../schemas.md) | Alert / finding | +✅ Federation compatible (Binary not available). + ## Description The detector slides a window of `fixed_window_size` event IDs over the log stream. During training every full window is stored as a known sequence; during detection a window whose exact sequence is not in that set is reported as an anomaly. diff --git a/docs/detectors/new_event.md b/docs/detectors/new_event.md index 216549ac..935950ac 100644 --- a/docs/detectors/new_event.md +++ b/docs/detectors/new_event.md @@ -7,6 +7,9 @@ The New Event Detector raises alerts when previously unseen log templates, disti | **Input** | [ParserSchema](../schemas.md) | Structured log | | **Output** | [DetectorSchema](../schemas.md) | Alert / finding | +✅ Federation compatible (Binary not available). + + ## Description This detector maintains a lightweight set of observed event IDs and emits an alert when an event ID not present in the set is seen for the first time (subject to configuration). diff --git a/docs/detectors/new_value.md b/docs/detectors/new_value.md index f429df7e..5e4e24cc 100644 --- a/docs/detectors/new_value.md +++ b/docs/detectors/new_value.md @@ -7,6 +7,9 @@ The New Value Detector raises alerts when previously unseen values appear in con | **Input** | [ParserSchema](../schemas.md) | Structured log | | **Output** | [DetectorSchema](../schemas.md) | Alert / finding | +✅ Federation compatible. + + ## Description This detector maintains a lightweight set of observed values per monitored field and emits an alert when a value not present in the set is seen for the first time (subject to configuration). diff --git a/docs/detectors/scvs_detector.md b/docs/detectors/scvs_detector.md index 6025f700..7c1b9897 100644 --- a/docs/detectors/scvs_detector.md +++ b/docs/detectors/scvs_detector.md @@ -13,6 +13,7 @@ A count vector is formed by counting the number of appearance of each event ID i Count vectors learned during training are stored via [persistency](../auxiliar/persistency.md), so a trained model can be saved and restored with a `persist:` block. A count vector is only comparable within the window it was counted over, so restoring state at a different `window_size` logs a warning — the restored vectors cannot match and every window would alert. +✅ Federation compatible (Binary not available). ## Configuration example diff --git a/docs/detectors/value_range.md b/docs/detectors/value_range.md index 16fd184e..264d3242 100644 --- a/docs/detectors/value_range.md +++ b/docs/detectors/value_range.md @@ -7,6 +7,9 @@ The Value Range Detector raises alerts when numerical values outside of known ra | **Input** | [ParserSchema](../schemas.md) | Structured log | | **Output** | [DetectorSchema](../schemas.md) | Alert / finding | + +✅ Federation compatible. + ## Description This detector maintains a lightweight set of observed values per monitored field and emits an alert when a value outside the learned range is seen (subject to configuration). diff --git a/tests/test_detectors/test_new_event_detector.py b/tests/test_detectors/test_new_event_detector.py index d2612b17..ff6d5df8 100644 --- a/tests/test_detectors/test_new_event_detector.py +++ b/tests/test_detectors/test_new_event_detector.py @@ -177,6 +177,34 @@ def test_audit_log_anomalies(self): assert detected_ids == {"1863"} + @pytest.mark.ignored + def test_audit_log_anomalie_fed(self): + parser = MatcherParser(config=_PARSER_CONFIG) + detector1 = NewEventDetector() + detector2 = NewEventDetector() + + logs = list(From.log(parser, in_path=AUDIT_LOG, do_process=True)) + for log in logs[:TRAIN_UNTIL]: + detector1.configure(log) + detector2.configure(log) + + detector1.set_configuration() + detector2.set_configuration() + + for log in logs[:TRAIN_UNTIL]: + detector1.train(log) + + (detector1 + detector2).aggregate() + assert detector2.persistency == detector1.persistency + + detected_ids: set[str] = set() + for log in logs[TRAIN_UNTIL:]: + output = schemas.DetectorSchema() + if detector2.detect(log, output_=output): + detected_ids.add(log["logID"]) + + assert detected_ids == {"1863"} + class TestNewEventDetectorAutoConfig: """Test that process() drives configure/set_configuration/train/detect @@ -211,34 +239,6 @@ def test_audit_log_anomalies_via_process(self): assert detected_ids == {"1863"} - @pytest.mark.ignored - def test_audit_log_anomalie_fed(self): - parser = MatcherParser(config=_PARSER_CONFIG) - detector1 = NewEventDetector() - detector2 = NewEventDetector() - - logs = list(From.log(parser, in_path=AUDIT_LOG, do_process=True)) - for log in logs[:TRAIN_UNTIL]: - detector1.configure(log) - detector2.configure(log) - - detector1.set_configuration() - detector2.set_configuration() - - for log in logs[:TRAIN_UNTIL]: - detector1.train(log) - - (detector1 + detector2).aggregate() - assert detector2.persistency == detector1.persistency - - detected_ids: set[str] = set() - for log in logs[TRAIN_UNTIL:]: - output = schemas.DetectorSchema() - if detector2.detect(log, output_=output): - detected_ids.add(log["logID"]) - - assert detected_ids == {"1863"} - class TestNewEventDetectorGlobalInstances: """Tests event-ID-independent global instance detection.""" From bbd58396a60331ec7b6b8b471aa63bb87d2d75f9 Mon Sep 17 00:00:00 2001 From: "angre.garcia-gomez@ait.ac.at" Date: Mon, 14 Sep 2026 15:18:08 +0200 Subject: [PATCH 25/28] minor correction --- docs/detectors/combo.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/detectors/combo.md b/docs/detectors/combo.md index a98552c1..e8c81081 100644 --- a/docs/detectors/combo.md +++ b/docs/detectors/combo.md @@ -7,7 +7,7 @@ The New Combo Value Detector raises alerts when previously unseen combinations o | **Input** | [ParserSchema](../schemas.md) | Structured log | | **Output** | [DetectorSchema](../schemas.md) | Combined alert / finding | -✅ Federation compatible (Binary not available). +✅ Federation compatible. ## Description This detector maintains a lightweight set of observed combination of values per monitored fields and emits an alert when a combination is not present in the set seen for the first time (subject to configuration). From df69a6d23db71325a859703a848d3d93f056c867 Mon Sep 17 00:00:00 2001 From: ipmach Date: Thu, 17 Sep 2026 09:20:33 +0200 Subject: [PATCH 26/28] Fix formatting for compatibility note in charset.md --- docs/detectors/charset.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/detectors/charset.md b/docs/detectors/charset.md index f4ebf568..6f904028 100644 --- a/docs/detectors/charset.md +++ b/docs/detectors/charset.md @@ -7,7 +7,7 @@ The Charset Detector raises alerts when previously unseen characters appear in c | **Input** | [ParserSchema](../schemas.md) | Structured log | | **Output** | [DetectorSchema](../schemas.md) | Alert / finding | -✅ Federation compatible, +✅ Federation compatible. ## Description From 2424f9f532404c89898351e4dfd4dcb583b0dce4 Mon Sep 17 00:00:00 2001 From: "angre.garcia-gomez@ait.ac.at" Date: Thu, 17 Sep 2026 11:07:12 +0200 Subject: [PATCH 27/28] rename EventDataStructure to EventDataset --- .../utils/persistency/__init__.py | 4 ++-- .../utils/persistency/basic_persistency.py | 16 ++++++++-------- .../persistency/event_data_structures/base.py | 4 ++-- .../dataframes/chunked_event_dataframe.py | 4 ++-- .../dataframes/event_dataframe.py | 4 ++-- .../trackers/base/event_tracker.py | 4 ++-- .../utils/persistency/event_persistency.py | 4 ++-- .../utils/persistency/persistency_saver.py | 8 ++++---- .../test_persistency_dump_load.py | 8 ++++---- 9 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/detectmatelibrary/utils/persistency/__init__.py b/src/detectmatelibrary/utils/persistency/__init__.py index 9d4ce653..02e6d8e4 100644 --- a/src/detectmatelibrary/utils/persistency/__init__.py +++ b/src/detectmatelibrary/utils/persistency/__init__.py @@ -2,7 +2,7 @@ from .event_persistency import EventPersistency from .persistency_saver import PersistencySaver, PersistencySaverConfig, PersistencyLoadError, save, load -from .event_data_structures.base import EventDataStructure +from .event_data_structures.base import EventDataset from .event_data_structures.trackers.stability.stability_tracker import EventStabilityTracker __all__ = [ @@ -10,7 +10,7 @@ "PersistencySaver", "PersistencySaverConfig", "PersistencyLoadError", - "EventDataStructure", + "EventDataset", "EventDataFrame", "ChunkedEventDataFrame", "EventStabilityTracker", diff --git a/src/detectmatelibrary/utils/persistency/basic_persistency.py b/src/detectmatelibrary/utils/persistency/basic_persistency.py index 99ee6562..7650e26c 100644 --- a/src/detectmatelibrary/utils/persistency/basic_persistency.py +++ b/src/detectmatelibrary/utils/persistency/basic_persistency.py @@ -1,4 +1,4 @@ -from .event_data_structures.base import EventDataStructure +from .event_data_structures.base import EventDataset from typing import Any, Dict, List, Type, Optional @@ -29,10 +29,10 @@ class EventStruct: """Event structure of the Event Persistency.""" def __init__( self, - event_data_class: Type[EventDataStructure], + event_data_class: Type[EventDataset], event_data_kwargs: Optional[dict[str, Any]] = None, ) -> None: - self.data: Dict[int | str, EventDataStructure] = {} + self.data: Dict[int | str, EventDataset] = {} self.data_class = event_data_class self.data_kwargs = event_data_kwargs or {} self.templates: Dict[int | str, str] = {} @@ -40,7 +40,7 @@ def __init__( def __contains__(self, event_id: int | str) -> bool: return event_id in self.data - def __getitem__(self, event_id: int | str) -> EventDataStructure | None: + def __getitem__(self, event_id: int | str) -> EventDataset | None: return self.data.get(event_id, None) def get_events(self) -> list[int | str]: @@ -79,7 +79,7 @@ class EventPersistencyBase: """Event Persistency without lock protection.""" def __init__( self, - event_data_class: Type[EventDataStructure], + event_data_class: Type[EventDataset], variable_blacklist: Optional[List[str | int]] = ["Content"], *, event_data_kwargs: Optional[dict[str, Any]] = None, @@ -136,7 +136,7 @@ def get_event_data(self, event_id: int | str) -> Any | None: """Retrieve the data for a specific event ID.""" return d_struct.get_data() if (d_struct := self.event_struct[event_id]) is not None else None - def get_events_data(self) -> Dict[int | str, EventDataStructure]: + def get_events_data(self) -> Dict[int | str, EventDataset]: """Retrieve the events data that is currently stored.""" return self.event_struct.data @@ -148,10 +148,10 @@ def get_event_templates(self) -> Dict[int | str, str]: """Retrieve all event templates.""" return self.event_struct.templates - def get_class(self) -> Type[EventDataStructure]: + def get_class(self) -> Type[EventDataset]: return self.event_struct.data_class - def __getitem__(self, event_id: int | str) -> EventDataStructure | None: + def __getitem__(self, event_id: int | str) -> EventDataset | None: return self.event_struct[event_id] def __repr__(self) -> str: diff --git a/src/detectmatelibrary/utils/persistency/event_data_structures/base.py b/src/detectmatelibrary/utils/persistency/event_data_structures/base.py index 254e56e6..5cc1b1f3 100644 --- a/src/detectmatelibrary/utils/persistency/event_data_structures/base.py +++ b/src/detectmatelibrary/utils/persistency/event_data_structures/base.py @@ -4,7 +4,7 @@ @dataclass -class EventDataStructure(ABC): +class EventDataset(ABC): """Storage backend interface for event-based data analysis.""" event_id: int = -1 @@ -45,7 +45,7 @@ def dump(self) -> bytes: @classmethod @abstractmethod - def load(cls, data: bytes, **kwargs: Any) -> "EventDataStructure": + def load(cls, data: bytes, **kwargs: Any) -> "EventDataset": """Restore state from bytes produced by dump().""" ... diff --git a/src/detectmatelibrary/utils/persistency/event_data_structures/dataframes/chunked_event_dataframe.py b/src/detectmatelibrary/utils/persistency/event_data_structures/dataframes/chunked_event_dataframe.py index ec875edf..c5595df6 100644 --- a/src/detectmatelibrary/utils/persistency/event_data_structures/dataframes/chunked_event_dataframe.py +++ b/src/detectmatelibrary/utils/persistency/event_data_structures/dataframes/chunked_event_dataframe.py @@ -6,12 +6,12 @@ import msgpack import polars as pl -from ..base import EventDataStructure +from ..base import EventDataset # -------- Polars backends -------- @dataclass -class ChunkedEventDataFrame(EventDataStructure): +class ChunkedEventDataFrame(EventDataset): """ Streaming-friendly Polars DataFrame backend: - Ingest appends chunks (cheap) diff --git a/src/detectmatelibrary/utils/persistency/event_data_structures/dataframes/event_dataframe.py b/src/detectmatelibrary/utils/persistency/event_data_structures/dataframes/event_dataframe.py index 8adc60d1..ca7a305d 100644 --- a/src/detectmatelibrary/utils/persistency/event_data_structures/dataframes/event_dataframe.py +++ b/src/detectmatelibrary/utils/persistency/event_data_structures/dataframes/event_dataframe.py @@ -4,11 +4,11 @@ import pandas as pd -from ..base import EventDataStructure +from ..base import EventDataset @dataclass -class EventDataFrame(EventDataStructure): +class EventDataFrame(EventDataset): """ Pandas DataFrame backend: - Ingest appends data (expensive) diff --git a/src/detectmatelibrary/utils/persistency/event_data_structures/trackers/base/event_tracker.py b/src/detectmatelibrary/utils/persistency/event_data_structures/trackers/base/event_tracker.py index b7c151d5..730f71d9 100644 --- a/src/detectmatelibrary/utils/persistency/event_data_structures/trackers/base/event_tracker.py +++ b/src/detectmatelibrary/utils/persistency/event_data_structures/trackers/base/event_tracker.py @@ -9,10 +9,10 @@ from .multi_tracker import MultiTracker from .single_tracker import SingleTracker -from ...base import EventDataStructure +from ...base import EventDataset -class EventTracker(EventDataStructure): +class EventTracker(EventDataset): """Event data structure that tracks the behavior of each event over time / number of events.""" diff --git a/src/detectmatelibrary/utils/persistency/event_persistency.py b/src/detectmatelibrary/utils/persistency/event_persistency.py index 3ba56e6e..6fe0360b 100644 --- a/src/detectmatelibrary/utils/persistency/event_persistency.py +++ b/src/detectmatelibrary/utils/persistency/event_persistency.py @@ -1,4 +1,4 @@ -from .event_data_structures.base import EventDataStructure +from .event_data_structures.base import EventDataset from .basic_persistency import EventPersistencyBase from typing import Any, Callable, Dict, List, Optional, Type, Self @@ -20,7 +20,7 @@ class EventPersistency(EventPersistencyBase): def __init__( self, - event_data_class: Type[EventDataStructure], + event_data_class: Type[EventDataset], variable_blacklist: Optional[List[str | int]] = ["Content"], *, event_data_kwargs: Optional[dict[str, Any]] = None, diff --git a/src/detectmatelibrary/utils/persistency/persistency_saver.py b/src/detectmatelibrary/utils/persistency/persistency_saver.py index b0239f45..a7b7e3f2 100644 --- a/src/detectmatelibrary/utils/persistency/persistency_saver.py +++ b/src/detectmatelibrary/utils/persistency/persistency_saver.py @@ -11,7 +11,7 @@ import fsspec -from detectmatelibrary.utils.persistency.event_data_structures.base import EventDataStructure +from detectmatelibrary.utils.persistency.event_data_structures.base import EventDataset from detectmatelibrary.utils.persistency.event_data_structures.trackers import ( EventTracker, EventStabilityTracker, @@ -19,7 +19,7 @@ from detectmatelibrary.utils.persistency.event_persistency import EventPersistency from detectmatelibrary.tools.logging import logger -_BACKEND_REGISTRY: dict[str, type[EventDataStructure]] = { +_BACKEND_REGISTRY: dict[str, type[EventDataset]] = { "EventTracker": EventTracker, "EventStabilityTracker": EventStabilityTracker, } @@ -27,7 +27,7 @@ _DATAFRAME_BACKENDS = {"EventDataFrame", "ChunkedEventDataFrame"} -def _get_backend_cls(name: str) -> type[EventDataStructure]: +def _get_backend_cls(name: str) -> type[EventDataset]: if name in _BACKEND_REGISTRY: return _BACKEND_REGISTRY[name] if name in _DATAFRAME_BACKENDS: @@ -41,7 +41,7 @@ def _get_backend_cls(name: str) -> type[EventDataStructure]: f"Backend '{name}' requires the 'dataframes' extra: " "pip install 'detectmatelibrary[dataframes]'" ) from e - df_registry: dict[str, type[EventDataStructure]] = { + df_registry: dict[str, type[EventDataset]] = { "EventDataFrame": EventDataFrame, "ChunkedEventDataFrame": ChunkedEventDataFrame, } diff --git a/tests/test_persistency/test_persistency_dump_load.py b/tests/test_persistency/test_persistency_dump_load.py index b1655974..d0d1d440 100644 --- a/tests/test_persistency/test_persistency_dump_load.py +++ b/tests/test_persistency/test_persistency_dump_load.py @@ -5,7 +5,7 @@ from detectmatelibrary.utils.persistency.persistency_saver import PersistencyLoadError from detectmatelibrary.utils.persistency.event_data_structures.base import ( - EventDataStructure, + EventDataset, ) from detectmatelibrary.utils.persistency.event_data_structures.dataframes.event_dataframe import ( EventDataFrame, @@ -26,13 +26,13 @@ def test_persistency_load_error_is_exception(): def test_event_data_structure_has_dump_load(): - assert hasattr(EventDataStructure, "dump") - assert hasattr(EventDataStructure, "load") + assert hasattr(EventDataset, "dump") + assert hasattr(EventDataset, "load") def test_subclass_without_dump_load_cannot_be_instantiated(): @dataclass - class _Incomplete(EventDataStructure): + class _Incomplete(EventDataset): def add_data(self, data_object): pass def get_data(self): pass def get_variables(self): pass From 7da6d30cb249dbecb74b5fa4c6a289b3b941b992 Mon Sep 17 00:00:00 2001 From: "angre.garcia-gomez@ait.ac.at" Date: Thu, 17 Sep 2026 11:08:32 +0200 Subject: [PATCH 28/28] rename EventStrut to PersistencyStruct --- .../utils/persistency/basic_persistency.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/detectmatelibrary/utils/persistency/basic_persistency.py b/src/detectmatelibrary/utils/persistency/basic_persistency.py index 7650e26c..f71cc091 100644 --- a/src/detectmatelibrary/utils/persistency/basic_persistency.py +++ b/src/detectmatelibrary/utils/persistency/basic_persistency.py @@ -25,7 +25,7 @@ def get_all_variables( return all_vars -class EventStruct: +class PersistencyStruct: """Event structure of the Event Persistency.""" def __init__( self, @@ -66,7 +66,7 @@ def __len__(self) -> int: return len(self.data) def __eq__(self, other: object) -> bool: - if not isinstance(other, EventStruct) or len(self) != len(other): + if not isinstance(other, PersistencyStruct) or len(self) != len(other): return False for elem1, elem2 in zip(self.data.values(), other.data.values()): if elem1.as_dict() != elem2.as_dict(): @@ -84,7 +84,7 @@ def __init__( *, event_data_kwargs: Optional[dict[str, Any]] = None, ): - self.event_struct = EventStruct( + self.event_struct = PersistencyStruct( event_data_class, event_data_kwargs=event_data_kwargs )