ntermqt 0.1.4__py3-none-any.whl → 0.1.5__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.
@@ -0,0 +1,237 @@
1
+ import sqlite3
2
+ import textfsm
3
+ from typing import Dict, List, Tuple, Optional
4
+ import io
5
+ import time
6
+ import click
7
+ from multiprocessing import Process, Queue
8
+ import multiprocessing
9
+ import sys
10
+ import threading
11
+ from contextlib import contextmanager
12
+
13
+
14
+ class ThreadSafeConnection:
15
+ """Thread-local storage for SQLite connections"""
16
+
17
+ def __init__(self, db_path: str, verbose: bool = False):
18
+ self.db_path = db_path
19
+ self.verbose = verbose
20
+ self._local = threading.local()
21
+
22
+ @contextmanager
23
+ def get_connection(self):
24
+ """Get a thread-local connection"""
25
+ if not hasattr(self._local, 'connection'):
26
+ self._local.connection = sqlite3.connect(self.db_path)
27
+ self._local.connection.row_factory = sqlite3.Row
28
+ if self.verbose:
29
+ click.echo(f"Created new connection in thread {threading.get_ident()}")
30
+
31
+ try:
32
+ yield self._local.connection
33
+ except Exception as e:
34
+ if hasattr(self._local, 'connection'):
35
+ self._local.connection.close()
36
+ delattr(self._local, 'connection')
37
+ raise e
38
+
39
+ def close_all(self):
40
+ """Close connection if it exists for current thread"""
41
+ if hasattr(self._local, 'connection'):
42
+ self._local.connection.close()
43
+ delattr(self._local, 'connection')
44
+
45
+
46
+ class TextFSMAutoEngine:
47
+ def __init__(self, db_path: str, verbose: bool = False):
48
+ self.db_path = db_path
49
+ self.verbose = verbose
50
+ self.connection_manager = ThreadSafeConnection(db_path, verbose)
51
+
52
+ def _calculate_template_score(
53
+ self,
54
+ parsed_data: List[Dict],
55
+ template: sqlite3.Row,
56
+ raw_output: str
57
+ ) -> float:
58
+ """
59
+ Score template match quality (0-100 scale).
60
+
61
+ Factors:
62
+ - Record count (0-30 pts): Did the template find data?
63
+ - Field richness (0-30 pts): How many fields per record?
64
+ - Population rate (0-25 pts): Are fields actually filled?
65
+ - Consistency (0-15 pts): Uniform data across records?
66
+ """
67
+ if not parsed_data:
68
+ return 0.0
69
+
70
+ num_records = len(parsed_data)
71
+ num_fields = len(parsed_data[0].keys()) if parsed_data else 0
72
+ is_version_cmd = 'version' in template['cli_command'].lower()
73
+
74
+ # === Factor 1: Record Count (0-30 points) ===
75
+ if is_version_cmd:
76
+ # Version commands: expect exactly 1 record
77
+ record_score = 30.0 if num_records == 1 else max(0, 15 - (num_records - 1) * 5)
78
+ else:
79
+ # Diminishing returns: log scale capped at 30
80
+ # 1 rec = 10, 3 rec = 20, 10+ rec = 30
81
+ if num_records >= 10:
82
+ record_score = 30.0
83
+ elif num_records >= 3:
84
+ record_score = 20.0 + (num_records - 3) * (10.0 / 7.0)
85
+ else:
86
+ record_score = num_records * 10.0
87
+
88
+ # === Factor 2: Field Richness (0-30 points) ===
89
+ # More fields = richer data extraction
90
+ # 1-2 fields = weak, 3-5 = decent, 6-10 = good, 10+ = excellent
91
+ if num_fields >= 10:
92
+ field_score = 30.0
93
+ elif num_fields >= 6:
94
+ field_score = 20.0 + (num_fields - 6) * 2.5
95
+ elif num_fields >= 3:
96
+ field_score = 10.0 + (num_fields - 3) * (10.0 / 3.0)
97
+ else:
98
+ field_score = num_fields * 5.0
99
+
100
+ # === Factor 3: Population Rate (0-25 points) ===
101
+ # What percentage of cells have actual data?
102
+ total_cells = num_records * num_fields
103
+ populated_cells = 0
104
+
105
+ for record in parsed_data:
106
+ for value in record.values():
107
+ if value is not None and str(value).strip():
108
+ populated_cells += 1
109
+
110
+ population_rate = populated_cells / total_cells if total_cells > 0 else 0
111
+ population_score = population_rate * 25.0
112
+
113
+ # === Factor 4: Consistency (0-15 points) ===
114
+ # Are the same fields populated across all records?
115
+ if num_records > 1:
116
+ # Check which fields are populated in each record
117
+ field_fill_counts = {key: 0 for key in parsed_data[0].keys()}
118
+
119
+ for record in parsed_data:
120
+ for key, value in record.items():
121
+ if value is not None and str(value).strip():
122
+ field_fill_counts[key] += 1
123
+
124
+ # Consistency = fields that are either always filled or never filled
125
+ consistent_fields = sum(
126
+ 1 for count in field_fill_counts.values()
127
+ if count == 0 or count == num_records
128
+ )
129
+ consistency_rate = consistent_fields / num_fields if num_fields > 0 else 0
130
+ consistency_score = consistency_rate * 15.0
131
+ else:
132
+ # Single record = perfectly consistent
133
+ consistency_score = 15.0
134
+
135
+ total_score = record_score + field_score + population_score + consistency_score
136
+
137
+ if self.verbose:
138
+ click.echo(f" Scoring: records={record_score:.1f}, fields={field_score:.1f}, "
139
+ f"population={population_score:.1f}, consistency={consistency_score:.1f} "
140
+ f"-> {total_score:.1f}")
141
+
142
+ return total_score
143
+
144
+ def find_best_template(self, device_output: str, filter_string: Optional[str] = None) -> Tuple[
145
+ Optional[str], Optional[List[Dict]], float, List[Tuple[str, float, int]]]:
146
+ """Try filtered templates against the output and return the best match plus all non-zero scores."""
147
+ best_template = None
148
+ best_parsed_output = None
149
+ best_score = 0
150
+ all_scores = [] # List of (template_name, score, record_count)
151
+
152
+ with self.connection_manager.get_connection() as conn:
153
+ templates = self.get_filtered_templates(conn, filter_string)
154
+ total_templates = len(templates)
155
+
156
+ if self.verbose:
157
+ click.echo(f"Found {total_templates} matching templates for filter: {filter_string}")
158
+
159
+ for idx, template in enumerate(templates, 1):
160
+ if self.verbose:
161
+ percentage = (idx / total_templates) * 100
162
+ click.echo(f"\nTemplate {idx}/{total_templates} ({percentage:.1f}%): {template['cli_command']}")
163
+
164
+ try:
165
+ textfsm_template = textfsm.TextFSM(io.StringIO(template['textfsm_content']))
166
+ parsed = textfsm_template.ParseText(device_output)
167
+ parsed_dicts = [dict(zip(textfsm_template.header, row)) for row in parsed]
168
+ score = self._calculate_template_score(parsed_dicts, template, device_output)
169
+
170
+ if self.verbose:
171
+ click.echo(f" -> Score={score:.2f}, Records={len(parsed_dicts)}")
172
+
173
+ # Track all non-zero scores
174
+ if score > 0:
175
+ all_scores.append((template['cli_command'], score, len(parsed_dicts)))
176
+
177
+ if score > best_score:
178
+ best_score = score
179
+ best_template = template['cli_command']
180
+ best_parsed_output = parsed_dicts
181
+ if self.verbose:
182
+ click.echo(click.style(" New best match!", fg='green'))
183
+
184
+ except Exception as e:
185
+ if self.verbose:
186
+ click.echo(f" -> Failed to parse: {str(e)}")
187
+ continue
188
+
189
+ # Sort all_scores by score descending
190
+ all_scores.sort(key=lambda x: x[1], reverse=True)
191
+
192
+ return best_template, best_parsed_output, best_score, all_scores
193
+
194
+ def get_filtered_templates(self, connection: sqlite3.Connection, filter_string: Optional[str] = None):
195
+ """Get filtered templates from database using provided connection."""
196
+ cursor = connection.cursor()
197
+ if filter_string:
198
+ filter_terms = filter_string.replace('-', '_').split('_')
199
+ query = "SELECT * FROM templates WHERE 1=1"
200
+ params = []
201
+ for term in filter_terms:
202
+ if term and len(term) > 2:
203
+ query += " AND cli_command LIKE ?"
204
+ params.append(f"%{term}%")
205
+ cursor.execute(query, params)
206
+ else:
207
+ cursor.execute("SELECT * FROM templates")
208
+ return cursor.fetchall()
209
+
210
+ def __del__(self):
211
+ """Clean up connections on deletion"""
212
+ self.connection_manager.close_all()
213
+
214
+
215
+ # Example usage
216
+ if __name__ == '__main__':
217
+ multiprocessing.freeze_support()
218
+
219
+
220
+ # Example of using the engine in multiple threads
221
+ def worker(engine, output, filter_str):
222
+ result = engine.find_best_template(output, filter_str)
223
+ print(f"Thread {threading.get_ident()}: Found template: {result[0]}")
224
+
225
+
226
+ engine = TextFSMAutoEngine("./secure_cartography/tfsm_templates.db", verbose=True)
227
+ threads = []
228
+
229
+ # Create multiple threads
230
+ for i in range(3):
231
+ t = threading.Thread(target=worker, args=(engine, "sample output", "show version"))
232
+ threads.append(t)
233
+ t.start()
234
+
235
+ # Wait for all threads to complete
236
+ for t in threads:
237
+ t.join()