detectmatelibrary 0.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. detectmatelibrary/__init__.py +24 -0
  2. detectmatelibrary/common/__init__.py +0 -0
  3. detectmatelibrary/common/_config/__init__.py +119 -0
  4. detectmatelibrary/common/_config/_compile.py +212 -0
  5. detectmatelibrary/common/_config/_formats.py +147 -0
  6. detectmatelibrary/common/_core_op/_fit_logic.py +153 -0
  7. detectmatelibrary/common/_core_op/_schema_pipeline.py +28 -0
  8. detectmatelibrary/common/core.py +171 -0
  9. detectmatelibrary/common/detector.py +224 -0
  10. detectmatelibrary/common/parser.py +84 -0
  11. detectmatelibrary/common/persist.py +32 -0
  12. detectmatelibrary/constants.py +7 -0
  13. detectmatelibrary/detectors/__init__.py +19 -0
  14. detectmatelibrary/detectors/bigram_frequency_detector.py +285 -0
  15. detectmatelibrary/detectors/charset_detector.py +158 -0
  16. detectmatelibrary/detectors/new_event_detector.py +96 -0
  17. detectmatelibrary/detectors/new_value_combo_detector.py +237 -0
  18. detectmatelibrary/detectors/new_value_detector.py +150 -0
  19. detectmatelibrary/detectors/random_detector.py +65 -0
  20. detectmatelibrary/detectors/rule_detector.py +104 -0
  21. detectmatelibrary/detectors/value_range_detector.py +184 -0
  22. detectmatelibrary/helper/from_to.py +305 -0
  23. detectmatelibrary/metadata.py +12 -0
  24. detectmatelibrary/parsers/__init__.py +0 -0
  25. detectmatelibrary/parsers/drain.py +0 -0
  26. detectmatelibrary/parsers/json_parser.py +92 -0
  27. detectmatelibrary/parsers/logbatcher/__init__.py +29 -0
  28. detectmatelibrary/parsers/logbatcher/engine/__init__.py +5 -0
  29. detectmatelibrary/parsers/logbatcher/engine/additional_cluster.py +185 -0
  30. detectmatelibrary/parsers/logbatcher/engine/cluster.py +175 -0
  31. detectmatelibrary/parsers/logbatcher/engine/matching.py +110 -0
  32. detectmatelibrary/parsers/logbatcher/engine/parser.py +120 -0
  33. detectmatelibrary/parsers/logbatcher/engine/parsing_base.py +220 -0
  34. detectmatelibrary/parsers/logbatcher/engine/parsing_cache.py +416 -0
  35. detectmatelibrary/parsers/logbatcher/engine/postprocess.py +195 -0
  36. detectmatelibrary/parsers/logbatcher/engine/sample.py +140 -0
  37. detectmatelibrary/parsers/logbatcher/engine/util.py +169 -0
  38. detectmatelibrary/parsers/logbatcher/engine/vars.py +41 -0
  39. detectmatelibrary/parsers/logbatcher/parser.py +66 -0
  40. detectmatelibrary/parsers/template_matcher/__init__.py +6 -0
  41. detectmatelibrary/parsers/template_matcher/_matcher_op.py +241 -0
  42. detectmatelibrary/parsers/template_matcher/_parser.py +155 -0
  43. detectmatelibrary/parsers/tree_matcher.py +43 -0
  44. detectmatelibrary/schemas/__init__.py +23 -0
  45. detectmatelibrary/schemas/_classes.py +169 -0
  46. detectmatelibrary/schemas/_op.py +110 -0
  47. detectmatelibrary/schemas/schemas_pb2.py +45 -0
  48. detectmatelibrary/utils/aux.py +15 -0
  49. detectmatelibrary/utils/data_buffer.py +108 -0
  50. detectmatelibrary/utils/data_normalizer.py +31 -0
  51. detectmatelibrary/utils/id_generator.py +7 -0
  52. detectmatelibrary/utils/key_extractor.py +103 -0
  53. detectmatelibrary/utils/log_format_utils.py +56 -0
  54. detectmatelibrary/utils/persistency/__init__.py +17 -0
  55. detectmatelibrary/utils/persistency/event_data_structures/base.py +45 -0
  56. detectmatelibrary/utils/persistency/event_data_structures/dataframes/__init__.py +4 -0
  57. detectmatelibrary/utils/persistency/event_data_structures/dataframes/chunked_event_dataframe.py +119 -0
  58. detectmatelibrary/utils/persistency/event_data_structures/dataframes/event_dataframe.py +54 -0
  59. detectmatelibrary/utils/persistency/event_data_structures/trackers/__init__.py +32 -0
  60. detectmatelibrary/utils/persistency/event_data_structures/trackers/base/__init__.py +10 -0
  61. detectmatelibrary/utils/persistency/event_data_structures/trackers/base/event_tracker.py +118 -0
  62. detectmatelibrary/utils/persistency/event_data_structures/trackers/base/multi_tracker.py +36 -0
  63. detectmatelibrary/utils/persistency/event_data_structures/trackers/base/single_tracker.py +40 -0
  64. detectmatelibrary/utils/persistency/event_data_structures/trackers/stability/__init__.py +9 -0
  65. detectmatelibrary/utils/persistency/event_data_structures/trackers/stability/stability_classifier.py +90 -0
  66. detectmatelibrary/utils/persistency/event_data_structures/trackers/stability/stability_tracker.py +157 -0
  67. detectmatelibrary/utils/persistency/event_persistency.py +137 -0
  68. detectmatelibrary/utils/persistency/persistency_saver.py +215 -0
  69. detectmatelibrary/utils/persistency/rle_list.py +63 -0
  70. detectmatelibrary/utils/preview_helpers.py +19 -0
  71. detectmatelibrary/utils/time_format_handler.py +125 -0
  72. detectmatelibrary-0.3.1.dist-info/METADATA +145 -0
  73. detectmatelibrary-0.3.1.dist-info/RECORD +145 -0
  74. detectmatelibrary-0.3.1.dist-info/WHEEL +5 -0
  75. detectmatelibrary-0.3.1.dist-info/entry_points.txt +2 -0
  76. detectmatelibrary-0.3.1.dist-info/licenses/LICENSE.md +287 -0
  77. detectmatelibrary-0.3.1.dist-info/top_level.txt +3 -0
  78. detectmatelibrary_tests/__init__.py +0 -0
  79. detectmatelibrary_tests/test_common/__init__.py +0 -0
  80. detectmatelibrary_tests/test_common/test_config.py +192 -0
  81. detectmatelibrary_tests/test_common/test_config_roundtrip.py +379 -0
  82. detectmatelibrary_tests/test_common/test_core.py +524 -0
  83. detectmatelibrary_tests/test_common/test_core_detector.py +196 -0
  84. detectmatelibrary_tests/test_common/test_core_parser.py +165 -0
  85. detectmatelibrary_tests/test_common/test_extract_timestamp.py +52 -0
  86. detectmatelibrary_tests/test_common/test_fit_logic.py +62 -0
  87. detectmatelibrary_tests/test_common/test_persist_config.py +145 -0
  88. detectmatelibrary_tests/test_detectors/__init__.py +0 -0
  89. detectmatelibrary_tests/test_detectors/dummy_detector.py +43 -0
  90. detectmatelibrary_tests/test_detectors/test_bigram_frequency_detector.py +596 -0
  91. detectmatelibrary_tests/test_detectors/test_charset_detector.py +422 -0
  92. detectmatelibrary_tests/test_detectors/test_mismatch_warnings.py +218 -0
  93. detectmatelibrary_tests/test_detectors/test_new_event_detector.py +250 -0
  94. detectmatelibrary_tests/test_detectors/test_new_value_combo_detector.py +578 -0
  95. detectmatelibrary_tests/test_detectors/test_new_value_detector.py +312 -0
  96. detectmatelibrary_tests/test_detectors/test_persist_integration.py +132 -0
  97. detectmatelibrary_tests/test_detectors/test_random_detector.py +72 -0
  98. detectmatelibrary_tests/test_detectors/test_rule_detector.py +179 -0
  99. detectmatelibrary_tests/test_detectors/test_value_range_detector.py +427 -0
  100. detectmatelibrary_tests/test_folder/__init__.py +0 -0
  101. detectmatelibrary_tests/test_helper/__init__.py +0 -0
  102. detectmatelibrary_tests/test_helper/test_from_to.py +497 -0
  103. detectmatelibrary_tests/test_others/__init__.py +0 -0
  104. detectmatelibrary_tests/test_others/test_logging.py +84 -0
  105. detectmatelibrary_tests/test_parsers/__init__.py +0 -0
  106. detectmatelibrary_tests/test_parsers/dummy_parser.py +33 -0
  107. detectmatelibrary_tests/test_parsers/test_json_parser.py +211 -0
  108. detectmatelibrary_tests/test_parsers/test_logbatcher_parser.py +82 -0
  109. detectmatelibrary_tests/test_parsers/test_template_matcher.py +241 -0
  110. detectmatelibrary_tests/test_parsers/test_tree_macther.py +47 -0
  111. detectmatelibrary_tests/test_persistency/__init__.py +0 -0
  112. detectmatelibrary_tests/test_persistency/test_stability_tracker.py +132 -0
  113. detectmatelibrary_tests/test_pipelines/__init__.py +0 -0
  114. detectmatelibrary_tests/test_pipelines/test_bad_players.py +70 -0
  115. detectmatelibrary_tests/test_pipelines/test_basic_pipelines.py +117 -0
  116. detectmatelibrary_tests/test_pipelines/test_configuration_engine.py +88 -0
  117. detectmatelibrary_tests/test_pipelines/test_dummy_methods.py +76 -0
  118. detectmatelibrary_tests/test_schemas/__init__.py +0 -0
  119. detectmatelibrary_tests/test_schemas/test_default_values.py +61 -0
  120. detectmatelibrary_tests/test_schemas/test_ops.py +170 -0
  121. detectmatelibrary_tests/test_schemas/test_schema_class.py +165 -0
  122. detectmatelibrary_tests/test_utils/__init__.py +0 -0
  123. detectmatelibrary_tests/test_utils/test_aux.py +25 -0
  124. detectmatelibrary_tests/test_utils/test_data_buffer.py +83 -0
  125. detectmatelibrary_tests/test_utils/test_data_normalizer.py +9 -0
  126. detectmatelibrary_tests/test_utils/test_events_seen.py +66 -0
  127. detectmatelibrary_tests/test_utils/test_log_format_utils.py +45 -0
  128. detectmatelibrary_tests/test_utils/test_persistency.py +486 -0
  129. detectmatelibrary_tests/test_utils/test_persistency_dump_load.py +190 -0
  130. detectmatelibrary_tests/test_utils/test_persistency_saver.py +278 -0
  131. detectmatelibrary_tests/test_utils/test_stability_tracking.py +554 -0
  132. detectmatelibrary_tests/test_workspace/__init__.py +0 -0
  133. detectmatelibrary_tests/test_workspace/test_create_workspace.py +206 -0
  134. detectmatelibrary_tests/test_workspace/test_utils.py +21 -0
  135. detectmatelibrary_tools/__init__.py +0 -0
  136. detectmatelibrary_tools/logging.py +51 -0
  137. detectmatelibrary_tools/workspace/__init__.py +0 -0
  138. detectmatelibrary_tools/workspace/create_workspace.py +191 -0
  139. detectmatelibrary_tools/workspace/templates/CustomDetector.py +66 -0
  140. detectmatelibrary_tools/workspace/templates/CustomParser.py +55 -0
  141. detectmatelibrary_tools/workspace/templates/__init__.py +0 -0
  142. detectmatelibrary_tools/workspace/templates/test_templates/__init__.py +0 -0
  143. detectmatelibrary_tools/workspace/templates/test_templates/test_CustomDetector.py +37 -0
  144. detectmatelibrary_tools/workspace/templates/test_templates/test_CustomParser.py +32 -0
  145. detectmatelibrary_tools/workspace/utils.py +155 -0
@@ -0,0 +1,24 @@
1
+ from . import common
2
+ from . import detectors
3
+ from . import parsers
4
+ from . import schemas
5
+ from . import utils
6
+ from .metadata import (__authors__, __contact__, __copyright__, __date__, __deprecated__, __website__,
7
+ __license__, __status__, __version__)
8
+
9
+ __all__ = [
10
+ "common",
11
+ "detectors",
12
+ "parsers",
13
+ "schemas",
14
+ "utils",
15
+ "__authors__",
16
+ "__contact__",
17
+ "__copyright__",
18
+ "__date__",
19
+ "__deprecated__",
20
+ "__website__",
21
+ "__license__",
22
+ "__status__",
23
+ "__version__"
24
+ ]
File without changes
@@ -0,0 +1,119 @@
1
+ from ._compile import ConfigMethods, generate_detector_config
2
+ from ._formats import EventsConfig
3
+
4
+ __all__ = ["ConfigMethods", "generate_detector_config", "EventsConfig", "BasicConfig"]
5
+
6
+ from pydantic import BaseModel, ConfigDict
7
+
8
+ from typing_extensions import Self
9
+ from typing import Any, Dict
10
+ from copy import deepcopy
11
+
12
+ from numpy.random import choice
13
+ import string
14
+
15
+
16
+ def random_id(length: int = 10) -> str:
17
+ characters = [s for s in string.ascii_letters + string.digits]
18
+ return "".join(str(choice(characters)) for _ in range(length))
19
+
20
+
21
+ class BasicConfig(BaseModel):
22
+ """Base configuration class with helper methods."""
23
+
24
+ model_config = ConfigDict(extra="forbid")
25
+
26
+ method_type: str = "default_method_type"
27
+ component_type: str = "default_type"
28
+
29
+ auto_config: bool = False
30
+
31
+ def get_config(self) -> Dict[str, Any]:
32
+ """Return the configuration as a dictionary."""
33
+ return self.model_dump()
34
+
35
+ def update_config(self, new_config: Dict[str, Any]) -> None:
36
+ """Update the configuration with new values."""
37
+ for key, value in new_config.items():
38
+ setattr(self, key, value)
39
+
40
+ @classmethod
41
+ def from_dict(cls, data: Dict[str, Any], method_id: str) -> Self:
42
+ aux = cls()
43
+ config_ = ConfigMethods.get_method(
44
+ deepcopy(data), component_type=aux.component_type, method_id=method_id
45
+ )
46
+ ConfigMethods.check_type(config_, method_type=aux.method_type)
47
+
48
+ return cls(**ConfigMethods.process(config_))
49
+
50
+ def to_dict(self, method_id: str = random_id()) -> Dict[str, Any]:
51
+ """Convert the config back to YAML-compatible dictionary format.
52
+
53
+ This is the inverse of from_dict() and ensures yaml -> pydantic -> yaml preservation.
54
+
55
+ Args:
56
+ method_id: The method identifier to use in the output structure
57
+
58
+ Returns:
59
+ Dictionary with structure: {component_type: {method_id: config_data}}
60
+ """
61
+ # Build the config in the format expected by from_dict
62
+ result: Dict[str, Any] = {
63
+ "method_type": self.method_type,
64
+ "auto_config": self.auto_config,
65
+ }
66
+
67
+ # Collect all non-meta fields for params
68
+ params = {}
69
+ events_data = None
70
+ instances_data = None
71
+ persist_data: dict[str, Any] | None = None
72
+
73
+ for field_name, field_value in self:
74
+ # Skip meta fields
75
+ if field_name in ("component_type", "method_type", "auto_config"):
76
+ continue
77
+
78
+ # Handle EventsConfig specially
79
+ if field_name == "events":
80
+ if field_value is not None:
81
+ if isinstance(field_value, EventsConfig):
82
+ events_data = field_value.to_dict()
83
+ else:
84
+ events_data = field_value
85
+ # Handle global instances specially (top-level, not in params)
86
+ # Serialized as "global" in YAML (Python field is "global_instances")
87
+ elif field_name == "global_instances" and field_value:
88
+ instances_data = {
89
+ name: inst.to_dict()
90
+ for name, inst in field_value.items()
91
+ }
92
+ elif field_name == "persist":
93
+ if field_value is not None:
94
+ persist_data = field_value.model_dump()
95
+ else:
96
+ # All other fields go into params
97
+ params[field_name] = field_value
98
+
99
+ # Add params if there are any
100
+ if params:
101
+ result["params"] = params
102
+
103
+ # Add global instances if they exist (serialized as "global" in YAML)
104
+ if instances_data is not None:
105
+ result["global"] = instances_data
106
+
107
+ # Add events if they exist
108
+ if events_data is not None:
109
+ result["events"] = events_data
110
+
111
+ if persist_data is not None:
112
+ result["persist"] = persist_data
113
+
114
+ # Wrap in the component_type and method_id structure
115
+ return {
116
+ self.component_type: {
117
+ method_id: result
118
+ }
119
+ }
@@ -0,0 +1,212 @@
1
+ from detectmatelibrary.common._config._formats import EventsConfig, _EventInstance
2
+
3
+ from typing import Any, Dict, List, Sequence, Tuple, Union
4
+ import warnings
5
+ import re
6
+
7
+
8
+ def _classify_variables(
9
+ var_names: Sequence[str], var_pattern: re.Pattern[str]
10
+ ) -> Dict[str, Any]:
11
+ """Classify variable names into positional and header variables.
12
+
13
+ Returns an instance config dict with 'params', 'variables', and
14
+ 'header_variables' keys.
15
+ """
16
+ positional_variables = []
17
+ header_variables = []
18
+
19
+ for var_name in var_names:
20
+ match = var_pattern.match(var_name)
21
+ if match:
22
+ position = int(match.group(1))
23
+ positional_variables.append({
24
+ "pos": position,
25
+ "name": var_name,
26
+ "params": {}
27
+ })
28
+ else:
29
+ header_variables.append({
30
+ "pos": var_name,
31
+ "params": {}
32
+ })
33
+
34
+ instance_config: Dict[str, Any] = {"params": {}}
35
+ if positional_variables:
36
+ instance_config["variables"] = positional_variables
37
+ if header_variables:
38
+ instance_config["header_variables"] = header_variables
39
+
40
+ return instance_config
41
+
42
+
43
+ class MethodNotFoundError(Exception):
44
+ def __init__(self, method_id: str, component_type: str) -> None:
45
+ super().__init__(
46
+ f"Method '{method_id}' of type '{component_type}' not found in configuration."
47
+ )
48
+
49
+
50
+ class TypeNotFoundError(Exception):
51
+ def __init__(self, type_name: str) -> None:
52
+ super().__init__(f"Type '{type_name}' not found in configuration.")
53
+
54
+
55
+ class MethodTypeNotMatch(Exception):
56
+ def __init__(self, expected_type: str, actual_type: str) -> None:
57
+ super().__init__(
58
+ f"Type mismatch: expected '{expected_type}', got '{actual_type}'."
59
+ )
60
+
61
+
62
+ class MissingParamsWarning(UserWarning):
63
+ def __init__(self) -> None:
64
+ super().__init__(
65
+ "'auto_config = False' and no 'params', 'events', 'global', or 'persist' provided. "
66
+ "Is that intended?"
67
+ )
68
+
69
+
70
+ class AutoConfigWarning(UserWarning):
71
+ def __init__(self) -> None:
72
+ super().__init__("'auto_config = True' will overwrite 'events' and 'params'.")
73
+
74
+
75
+ class ConfigMethods:
76
+ @staticmethod
77
+ def get_method(
78
+ config: Dict[str, Dict[str, Dict[str, Any]]], method_id: str, component_type: str
79
+ ) -> Dict[str, Any]:
80
+
81
+ if component_type not in config:
82
+ raise TypeNotFoundError(component_type)
83
+
84
+ args = config[component_type]
85
+ if method_id not in args:
86
+ raise MethodNotFoundError(method_id, component_type)
87
+
88
+ return args[method_id]
89
+
90
+ @staticmethod
91
+ def check_type(config: Dict[str, Any], method_type: str) -> None:
92
+ if config["method_type"] != method_type:
93
+ raise MethodTypeNotMatch(expected_type=method_type, actual_type=config["method_type"])
94
+
95
+ @staticmethod
96
+ def process(config: Dict[str, Any]) -> Dict[str, Any]:
97
+ has_params = "params" in config
98
+ has_events = "events" in config
99
+ has_instances = "global" in config
100
+ has_persist = "persist" in config
101
+
102
+ no_data = not has_params and not has_events and not has_instances and not has_persist
103
+ if no_data and not config.get("auto_config", False):
104
+ warnings.warn(MissingParamsWarning())
105
+
106
+ if has_params:
107
+ if config.get("auto_config", False):
108
+ warnings.warn(AutoConfigWarning())
109
+
110
+ config.update(config["params"])
111
+ config.pop("params")
112
+
113
+ # Handle "events" key at top level
114
+ if has_events:
115
+ config["events"] = EventsConfig._init(config["events"])
116
+
117
+ # Handle "global" key: event-ID-independent global instances
118
+ # Renamed to "global_instances" to avoid collision with Python keyword
119
+ if has_instances:
120
+ config["global_instances"] = {
121
+ name: _EventInstance._init(**data)
122
+ for name, data in config.pop("global").items()
123
+ }
124
+
125
+ return config
126
+
127
+
128
+ def generate_detector_config(
129
+ variable_selection: Dict[int | str, List[Union[str, Tuple[str, ...]]]],
130
+ detector_name: str,
131
+ method_type: str,
132
+ **additional_params: Any
133
+ ) -> Dict[str, Any]:
134
+ """Generate a detector configuration dictionary from variable selections.
135
+
136
+ Transforms a variable selection mapping into the nested configuration
137
+ structure required by detector configs. Supports both individual variable
138
+ names (strings) and tuples of variable names. Each tuple produces a
139
+ separate detector instance in the config.
140
+
141
+ Args:
142
+ variable_selection: Maps event_id to list of variable names or tuples
143
+ of variable names. Strings are grouped into a single instance.
144
+ Each tuple becomes its own instance. Variable names matching
145
+ 'var_\\d+' are positional template variables; others are header
146
+ variables.
147
+ detector_name: Name of the detector, used as the base instance_id.
148
+ method_type: Type of detection method (e.g., "new_value_detector").
149
+ **additional_params: Additional parameters for the detector's params
150
+ dict (e.g., max_combo_size=3).
151
+
152
+ Returns:
153
+ Dictionary with structure compatible with detector config classes.
154
+
155
+ Examples:
156
+ Single variable names (one instance per event)::
157
+
158
+ config = generate_detector_config(
159
+ variable_selection={1: ["var_0", "var_1", "level"]},
160
+ detector_name="MyDetector",
161
+ method_type="new_value_detector",
162
+ )
163
+
164
+ Tuples of variable names (one instance per tuple)::
165
+
166
+ config = generate_detector_config(
167
+ variable_selection={1: [("username", "src_ip"), ("var_0", "var_1")]},
168
+ detector_name="MyDetector",
169
+ method_type="new_value_combo_detector",
170
+ max_combo_size=2,
171
+ )
172
+ """
173
+ var_pattern = re.compile(r"^var_(\d+)$")
174
+
175
+ events_config: Dict[int | str, Dict[str, Any]] = {}
176
+
177
+ for event_id, variable_names in variable_selection.items():
178
+ instances: Dict[str, Any] = {}
179
+
180
+ # Separate plain strings from tuples
181
+ single_vars: List[str] = []
182
+ tuple_vars: List[Tuple[str, ...]] = []
183
+
184
+ for entry in variable_names:
185
+ if isinstance(entry, tuple):
186
+ tuple_vars.append(entry)
187
+ else:
188
+ single_vars.append(entry)
189
+
190
+ # Plain strings -> single instance keyed by detector_name
191
+ if single_vars:
192
+ instances[detector_name] = _classify_variables(single_vars, var_pattern)
193
+
194
+ # Each tuple -> its own instance, keyed by joined variable names
195
+ for combo in tuple_vars:
196
+ instance_id = f"{detector_name}_{'_'.join(combo)}"
197
+ instances[instance_id] = _classify_variables(combo, var_pattern)
198
+
199
+ events_config[event_id] = instances
200
+
201
+ config_dict = {
202
+ "detectors": {
203
+ detector_name: {
204
+ "method_type": method_type,
205
+ "auto_config": False,
206
+ "params": additional_params,
207
+ "events": events_config
208
+ }
209
+ }
210
+ }
211
+
212
+ return config_dict
@@ -0,0 +1,147 @@
1
+ from pydantic import BaseModel
2
+
3
+ from typing_extensions import Self
4
+ from typing import Dict, Any
5
+
6
+
7
+ # Sub-formats ********************************************************+
8
+ class Variable(BaseModel):
9
+ pos: str | int
10
+ name: str = ""
11
+ params: Dict[str, Any] = {}
12
+
13
+ def to_dict(self) -> Dict[str, Any]:
14
+ """Convert Variable to YAML-compatible dictionary."""
15
+ result: Dict[str, Any] = {
16
+ "pos": self.pos,
17
+ }
18
+ if self.name:
19
+ result["name"] = self.name
20
+ if self.params:
21
+ result["params"] = self.params
22
+ return result
23
+
24
+
25
+ class Header(BaseModel):
26
+ pos: str
27
+ params: Dict[str, Any] = {}
28
+
29
+ def to_dict(self) -> Dict[str, Any]:
30
+ """Convert Header to YAML-compatible dictionary."""
31
+ result: Dict[str, Any] = {
32
+ "pos": self.pos,
33
+ }
34
+ if self.params:
35
+ result["params"] = self.params
36
+ return result
37
+
38
+
39
+ class _EventInstance(BaseModel):
40
+ """Configuration for a specific instance within an event."""
41
+ params: Dict[str, Any] = {}
42
+ variables: Dict[str | int, Variable] = {}
43
+ header_variables: Dict[str, Header] = {}
44
+
45
+ @classmethod
46
+ def _init(cls, **kwargs: dict[str, Any]) -> "_EventInstance":
47
+ for var, cl in zip(["variables", "header_variables"], [Variable, Header]):
48
+ if var in kwargs:
49
+ new_dict = {}
50
+ for v in kwargs[var]:
51
+ aux = cl(**v) # type: ignore
52
+ new_dict[aux.pos] = aux
53
+ kwargs[var] = new_dict
54
+ return cls(**kwargs) # type: ignore
55
+
56
+ def get_all(self) -> Dict[Any, Header | Variable]:
57
+ return {**self.variables, **self.header_variables}
58
+
59
+ def to_dict(self) -> Dict[str, Any]:
60
+ """Convert _EventInstance to YAML-compatible dictionary."""
61
+ result: Dict[str, Any] = {}
62
+ # Always include params even if empty (to match YAML structure)
63
+ result["params"] = self.params
64
+ if self.variables:
65
+ result["variables"] = [var.to_dict() for var in self.variables.values()]
66
+ if self.header_variables:
67
+ result["header_variables"] = [header.to_dict() for header in self.header_variables.values()]
68
+ return result
69
+
70
+
71
+ class _EventConfig(BaseModel):
72
+ """Configuration for all instances of a specific event."""
73
+ instances: Dict[str, _EventInstance]
74
+
75
+ @classmethod
76
+ def _init(cls, instances_dict: Dict[str, Dict[str, Any]]) -> "_EventConfig":
77
+ instances = {}
78
+ for instance_id, instance_data in instances_dict.items():
79
+ instances[instance_id] = _EventInstance._init(**instance_data)
80
+ return cls(instances=instances)
81
+
82
+ @property
83
+ def variables(self) -> Dict[str | int, Variable]:
84
+ """Pass-through to first instance for compatibility."""
85
+ if self.instances:
86
+ return next(iter(self.instances.values())).variables
87
+ return {}
88
+
89
+ @property
90
+ def header_variables(self) -> Dict[str, Header]:
91
+ """Pass-through to first instance for compatibility."""
92
+ if self.instances:
93
+ return next(iter(self.instances.values())).header_variables
94
+ return {}
95
+
96
+ def get_all(self) -> Dict[Any, Header | Variable]:
97
+ """Pass-through to first instance for compatibility."""
98
+ if self.instances:
99
+ return next(iter(self.instances.values())).get_all()
100
+ return {}
101
+
102
+ def to_dict(self) -> Dict[str, Any]:
103
+ """Convert _EventConfig to YAML-compatible dictionary."""
104
+ return {
105
+ instance_id: instance.to_dict()
106
+ for instance_id, instance in self.instances.items()
107
+ }
108
+
109
+
110
+ # Main-formats ********************************************************+
111
+ class EventsConfig(BaseModel):
112
+ """Events configuration dict keyed by event_id."""
113
+ events: Dict[Any, _EventConfig]
114
+
115
+ @classmethod
116
+ def _init(cls, events_dict: Dict[Any, Dict[str, Any]]) -> Self:
117
+ new_dict = {}
118
+ for event_id, instances in events_dict.items():
119
+ new_dict[event_id] = _EventConfig._init(instances)
120
+ return cls(events=new_dict)
121
+
122
+ def __getitem__(self, idx: str | int) -> _EventConfig | None:
123
+ if idx not in self.events:
124
+ return None
125
+ return self.events[idx]
126
+
127
+ def __contains__(self, idx: str | int) -> bool:
128
+ return idx in self.events
129
+
130
+ def to_dict(self) -> Dict[Any, Any]:
131
+ """Convert EventsConfig to YAML-compatible dictionary.
132
+
133
+ This unwraps the internal 'events' dict structure to match YAML
134
+ format.
135
+ """
136
+ result = {}
137
+ for event_id, event_config in self.events.items():
138
+ # Skip events with no instances
139
+ if not event_config.instances:
140
+ continue
141
+ # Convert string keys back to int if they were originally int
142
+ try:
143
+ key = int(event_id)
144
+ except (ValueError, TypeError):
145
+ key = event_id
146
+ result[key] = event_config.to_dict()
147
+ return result
@@ -0,0 +1,153 @@
1
+
2
+ from typing import Literal, get_args
3
+ from enum import Enum
4
+ import warnings
5
+
6
+
7
+ class TrainState(Enum):
8
+ DEFAULT = 0
9
+ STOP_TRAINING = 1
10
+ KEEP_TRAINING = 2
11
+
12
+ def describe(self) -> str:
13
+ descriptions = [
14
+ "Follow default training behavior.",
15
+ "Force stop training.",
16
+ "Keep training regardless of default behavior."
17
+ ]
18
+
19
+ return descriptions[self.value]
20
+
21
+
22
+ class ConfigState(Enum):
23
+ DEFAULT = 0
24
+ STOP_CONFIGURE = 1
25
+ KEEP_CONFIGURE = 2
26
+
27
+ def describe(self) -> str:
28
+ descriptions = [
29
+ "Follow default configuration behavior.",
30
+ "Force stop configuration.",
31
+ "Keep configuring regardless of default behavior."
32
+ ]
33
+ return descriptions[self.value]
34
+
35
+
36
+ StatesL = Literal["keep_training", "stop_training", "keep_configuring", "stop_configuring"]
37
+
38
+ def update_state(
39
+ state: StatesL, train_state: TrainState, config_state: ConfigState
40
+ ) -> tuple[TrainState, ConfigState]:
41
+ if state == "keep_training":
42
+ train_state = TrainState.KEEP_TRAINING
43
+ elif state == "stop_training":
44
+ train_state = TrainState.STOP_TRAINING
45
+ elif state == "keep_configuring":
46
+ config_state = ConfigState.KEEP_CONFIGURE
47
+ elif state == "stop_configuring":
48
+ config_state = ConfigState.STOP_CONFIGURE
49
+ else:
50
+ warnings.warn(f"State {state} unknown, use: {get_args(StatesL)}")
51
+
52
+ return train_state, config_state
53
+
54
+
55
+ def do_training(
56
+ data_use_training: int | None, index: int, train_state: TrainState
57
+ ) -> bool:
58
+ if train_state == TrainState.STOP_TRAINING:
59
+ return False
60
+ elif train_state == TrainState.KEEP_TRAINING:
61
+ return True
62
+
63
+ return data_use_training is not None and data_use_training > index
64
+
65
+
66
+ def do_configure(
67
+ data_use_configure: int | None, index: int, configure_state: ConfigState
68
+ ) -> bool:
69
+ if configure_state == ConfigState.STOP_CONFIGURE:
70
+ return False
71
+ elif configure_state == ConfigState.KEEP_CONFIGURE:
72
+ return True
73
+
74
+ return data_use_configure is not None and data_use_configure > index
75
+
76
+
77
+ class FitLogicState(Enum):
78
+ DO_CONFIG = 0
79
+ DO_TRAIN = 1
80
+ NOTHING = 2
81
+
82
+ def describe(self) -> str:
83
+ descriptions = [
84
+ "Configuring",
85
+ "Training.",
86
+ "Default"
87
+ ]
88
+ return descriptions[self.value]
89
+
90
+
91
+ class FitLogic:
92
+ def __init__(
93
+ self, data_use_configure: int | None, data_use_training: int | None
94
+ ) -> None:
95
+
96
+ self.train_state = TrainState.DEFAULT
97
+ self.configure_state = ConfigState.DEFAULT
98
+ self.last_state = FitLogicState.NOTHING
99
+
100
+ self.data_used_train, self.data_used_configure = 0, 0
101
+ self._configuration_done, self.config_finished = False, False
102
+ self._training_done, self.training_finished = False, False
103
+
104
+ self.data_use_configure = data_use_configure
105
+ self.data_use_training = data_use_training
106
+
107
+ def get_last_state(self) -> str:
108
+ return self.last_state.describe()
109
+
110
+ def update_state(self, state: StatesL) -> None:
111
+ self.train_state, self.configure_state = update_state(
112
+ state=state, train_state=self.train_state, config_state=self.configure_state
113
+ )
114
+
115
+ def finish_config(self) -> bool:
116
+ if self._configuration_done and not self.config_finished:
117
+ self.config_finished = True
118
+ return True
119
+ return False
120
+
121
+ def finish_training(self) -> bool:
122
+ if self._training_done and not self.training_finished:
123
+ self.training_finished = True
124
+ return True
125
+ return False
126
+
127
+ def __check_state(self) -> FitLogicState:
128
+ if do_configure(
129
+ data_use_configure=self.data_use_configure,
130
+ index=self.data_used_configure,
131
+ configure_state=self.configure_state
132
+ ):
133
+ self.data_used_configure += 1
134
+ return FitLogicState.DO_CONFIG
135
+ else:
136
+ if self.data_used_configure > 0 and not self._configuration_done:
137
+ self._configuration_done = True
138
+
139
+ if do_training(
140
+ data_use_training=self.data_use_training,
141
+ index=self.data_used_train,
142
+ train_state=self.train_state
143
+ ):
144
+ self.data_used_train += 1
145
+ return FitLogicState.DO_TRAIN
146
+ elif self.data_used_train > 0 and not self._training_done:
147
+ self._training_done = True
148
+
149
+ return FitLogicState.NOTHING
150
+
151
+ def run(self) -> FitLogicState:
152
+ self.last_state = self.__check_state()
153
+ return self.last_state
@@ -0,0 +1,28 @@
1
+ from detectmatelibrary.schemas import BaseSchema
2
+
3
+
4
+ from typing import Tuple
5
+
6
+
7
+ class SchemaPipeline:
8
+ @staticmethod
9
+ def preprocess(
10
+ input_: BaseSchema, data: BaseSchema | bytes
11
+ ) -> Tuple[bool, BaseSchema]:
12
+
13
+ is_byte = False
14
+ if isinstance(data, bytes):
15
+ is_byte = True
16
+ input_.deserialize(data)
17
+ data = input_.copy()
18
+ else:
19
+ data = data.copy()
20
+
21
+ return is_byte, data
22
+
23
+ @staticmethod
24
+ def postprocess(
25
+ data: BaseSchema, is_byte: bool
26
+ ) -> BaseSchema | bytes:
27
+
28
+ return data if not is_byte else data.serialize()