Coverage for src / tracekit / session / history.py: 83%

105 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-11 23:04 +0000

1"""Operation history tracking. 

2 

3This module provides operation history tracking for analysis sessions. 

4 

5 

6Example: 

7 >>> history = OperationHistory() 

8 >>> history.record('load', {'file': 'capture.wfm'}) 

9 >>> history.record('measure_rise_time', {'result': 1.5e-9}) 

10 >>> print(history.to_script()) 

11""" 

12 

13from __future__ import annotations 

14 

15from dataclasses import dataclass, field 

16from datetime import datetime 

17from typing import Any 

18 

19 

20@dataclass 

21class HistoryEntry: 

22 """Single history entry recording an operation. 

23 

24 Attributes: 

25 operation: Operation name (function/method called) 

26 parameters: Input parameters 

27 result: Operation result (summary) 

28 timestamp: When operation was performed 

29 duration_ms: Operation duration in milliseconds 

30 success: Whether operation succeeded 

31 error_message: Error message if failed 

32 metadata: Additional metadata 

33 """ 

34 

35 operation: str 

36 parameters: dict[str, Any] = field(default_factory=dict) 

37 result: Any = None 

38 timestamp: datetime = field(default_factory=datetime.now) 

39 duration_ms: float = 0.0 

40 success: bool = True 

41 error_message: str | None = None 

42 metadata: dict[str, Any] = field(default_factory=dict) 

43 

44 def to_dict(self) -> dict[str, Any]: 

45 """Convert to dictionary for serialization.""" 

46 return { 

47 "operation": self.operation, 

48 "parameters": self.parameters, 

49 "result": self._serialize_result(self.result), 

50 "timestamp": self.timestamp.isoformat(), 

51 "duration_ms": self.duration_ms, 

52 "success": self.success, 

53 "error_message": self.error_message, 

54 "metadata": self.metadata, 

55 } 

56 

57 @staticmethod 

58 def _serialize_result(result: Any) -> Any: 

59 """Serialize result for JSON storage.""" 

60 if result is None: 

61 return None 

62 if isinstance(result, str | int | float | bool): 62 ↛ 64line 62 didn't jump to line 64 because the condition on line 62 was always true

63 return result 

64 if isinstance(result, dict): 

65 return {k: HistoryEntry._serialize_result(v) for k, v in result.items()} 

66 if isinstance(result, list | tuple): 

67 return [HistoryEntry._serialize_result(v) for v in result] 

68 # For complex objects, store string representation 

69 return str(result) 

70 

71 @classmethod 

72 def from_dict(cls, data: dict[str, Any]) -> HistoryEntry: 

73 """Create from dictionary.""" 

74 data = data.copy() 

75 if "timestamp" in data and isinstance(data["timestamp"], str): 75 ↛ 77line 75 didn't jump to line 77 because the condition on line 75 was always true

76 data["timestamp"] = datetime.fromisoformat(data["timestamp"]) 

77 return cls(**data) 

78 

79 def to_code(self) -> str: 

80 """Generate Python code to replay this operation. 

81 

82 Returns: 

83 Python code string. 

84 """ 

85 # Format parameters 

86 params = [] 

87 for k, v in self.parameters.items(): 

88 if isinstance(v, str): 

89 params.append(f'{k}="{v}"') 

90 else: 

91 params.append(f"{k}={v!r}") 

92 

93 param_str = ", ".join(params) 

94 return f"tk.{self.operation}({param_str})" 

95 

96 

97@dataclass 

98class OperationHistory: 

99 """History of analysis operations. 

100 

101 Supports recording, replaying, and exporting operation history. 

102 

103 Attributes: 

104 entries: List of history entries 

105 max_entries: Maximum entries to keep (0 = unlimited) 

106 auto_record: Whether to automatically record operations 

107 """ 

108 

109 entries: list[HistoryEntry] = field(default_factory=list) 

110 max_entries: int = 0 

111 auto_record: bool = True 

112 _current_session_start: datetime = field(default_factory=datetime.now) 

113 

114 def record( 

115 self, 

116 operation: str, 

117 parameters: dict[str, Any] | None = None, 

118 result: Any = None, 

119 duration_ms: float = 0.0, 

120 success: bool = True, 

121 error_message: str | None = None, 

122 **metadata: Any, 

123 ) -> HistoryEntry: 

124 """Record an operation. 

125 

126 Args: 

127 operation: Operation name. 

128 parameters: Input parameters. 

129 result: Operation result. 

130 duration_ms: Duration in milliseconds. 

131 success: Whether operation succeeded. 

132 error_message: Error message if failed. 

133 **metadata: Additional metadata. 

134 

135 Returns: 

136 Created history entry. 

137 """ 

138 entry = HistoryEntry( 

139 operation=operation, 

140 parameters=parameters or {}, 

141 result=result, 

142 duration_ms=duration_ms, 

143 success=success, 

144 error_message=error_message, 

145 metadata=metadata, 

146 ) 

147 

148 self.entries.append(entry) 

149 

150 # Trim if exceeded max entries 

151 if self.max_entries > 0 and len(self.entries) > self.max_entries: 

152 self.entries = self.entries[-self.max_entries :] 

153 

154 return entry 

155 

156 def undo(self) -> HistoryEntry | None: 

157 """Remove and return the last entry. 

158 

159 Returns: 

160 Removed entry, or None if empty. 

161 """ 

162 if self.entries: 162 ↛ 164line 162 didn't jump to line 164 because the condition on line 162 was always true

163 return self.entries.pop() 

164 return None 

165 

166 def clear(self) -> int: 

167 """Clear all history. 

168 

169 Returns: 

170 Number of entries cleared. 

171 """ 

172 count = len(self.entries) 

173 self.entries.clear() 

174 return count 

175 

176 def find( 

177 self, 

178 operation: str | None = None, 

179 success_only: bool = False, 

180 since: datetime | None = None, 

181 ) -> list[HistoryEntry]: 

182 """Find entries matching criteria. 

183 

184 Args: 

185 operation: Filter by operation name. 

186 success_only: Only return successful operations. 

187 since: Only return entries after this time. 

188 

189 Returns: 

190 Matching entries. 

191 """ 

192 results = [] 

193 for entry in self.entries: 

194 if operation and entry.operation != operation: 

195 continue 

196 if success_only and not entry.success: 

197 continue 

198 if since and entry.timestamp < since: 198 ↛ 199line 198 didn't jump to line 199 because the condition on line 198 was never true

199 continue 

200 results.append(entry) 

201 return results 

202 

203 def to_script( 

204 self, 

205 include_imports: bool = True, 

206 include_comments: bool = True, 

207 ) -> str: 

208 """Export history as Python script. 

209 

210 Args: 

211 include_imports: Include import statements. 

212 include_comments: Include timestamp comments. 

213 

214 Returns: 

215 Python script string. 

216 

217 Example: 

218 >>> script = history.to_script() 

219 >>> print(script) 

220 # Generated by TraceKit 

221 import tracekit as tk 

222 tk.load("capture.wfm") 

223 result = tk.measure_rise_time() 

224 """ 

225 lines = [] 

226 

227 if include_imports: 227 ↛ 241line 227 didn't jump to line 241 because the condition on line 227 was always true

228 lines.extend( 

229 [ 

230 "#!/usr/bin/env python3", 

231 '"""TraceKit analysis script.', 

232 "", 

233 f"Generated: {datetime.now().isoformat()}", 

234 '"""', 

235 "", 

236 "import tracekit as tk", 

237 "", 

238 ] 

239 ) 

240 

241 for entry in self.entries: 

242 if not entry.success: 242 ↛ 243line 242 didn't jump to line 243 because the condition on line 242 was never true

243 continue 

244 

245 if include_comments: 245 ↛ 248line 245 didn't jump to line 248 because the condition on line 245 was always true

246 lines.append(f"# {entry.timestamp.strftime('%H:%M:%S')} - {entry.operation}") 

247 

248 lines.append(entry.to_code()) 

249 lines.append("") 

250 

251 return "\n".join(lines) 

252 

253 def summary(self) -> dict[str, Any]: 

254 """Get history summary statistics. 

255 

256 Returns: 

257 Dictionary with summary statistics. 

258 """ 

259 if not self.entries: 259 ↛ 260line 259 didn't jump to line 260 because the condition on line 259 was never true

260 return { 

261 "total_operations": 0, 

262 "successful": 0, 

263 "failed": 0, 

264 "total_duration_ms": 0, 

265 "unique_operations": 0, 

266 } 

267 

268 successful = sum(1 for e in self.entries if e.success) 

269 failed = len(self.entries) - successful 

270 total_duration = sum(e.duration_ms for e in self.entries) 

271 unique_ops = len({e.operation for e in self.entries}) 

272 

273 # Operation frequency 

274 op_counts: dict[str, int] = {} 

275 for entry in self.entries: 

276 op_counts[entry.operation] = op_counts.get(entry.operation, 0) + 1 

277 

278 return { 

279 "total_operations": len(self.entries), 

280 "successful": successful, 

281 "failed": failed, 

282 "total_duration_ms": total_duration, 

283 "unique_operations": unique_ops, 

284 "operation_counts": op_counts, 

285 "session_start": self._current_session_start.isoformat(), 

286 "last_operation": self.entries[-1].timestamp.isoformat() if self.entries else None, 

287 } 

288 

289 def to_dict(self) -> dict[str, Any]: 

290 """Convert to dictionary for serialization.""" 

291 return { 

292 "entries": [e.to_dict() for e in self.entries], 

293 "max_entries": self.max_entries, 

294 "session_start": self._current_session_start.isoformat(), 

295 } 

296 

297 @classmethod 

298 def from_dict(cls, data: dict[str, Any]) -> OperationHistory: 

299 """Create from dictionary.""" 

300 entries = [HistoryEntry.from_dict(e) for e in data.get("entries", [])] 

301 history = cls( 

302 entries=entries, 

303 max_entries=data.get("max_entries", 0), 

304 ) 

305 if "session_start" in data: 305 ↛ 307line 305 didn't jump to line 307 because the condition on line 305 was always true

306 history._current_session_start = datetime.fromisoformat(data["session_start"]) 

307 return history 

308 

309 

310__all__ = [ 

311 "HistoryEntry", 

312 "OperationHistory", 

313]