oscura 0.0.1__py3-none-any.whl → 0.1.0__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 (465) hide show
  1. oscura/__init__.py +813 -8
  2. oscura/__main__.py +392 -0
  3. oscura/analyzers/__init__.py +37 -0
  4. oscura/analyzers/digital/__init__.py +177 -0
  5. oscura/analyzers/digital/bus.py +691 -0
  6. oscura/analyzers/digital/clock.py +805 -0
  7. oscura/analyzers/digital/correlation.py +720 -0
  8. oscura/analyzers/digital/edges.py +632 -0
  9. oscura/analyzers/digital/extraction.py +413 -0
  10. oscura/analyzers/digital/quality.py +878 -0
  11. oscura/analyzers/digital/signal_quality.py +877 -0
  12. oscura/analyzers/digital/thresholds.py +708 -0
  13. oscura/analyzers/digital/timing.py +1104 -0
  14. oscura/analyzers/eye/__init__.py +46 -0
  15. oscura/analyzers/eye/diagram.py +434 -0
  16. oscura/analyzers/eye/metrics.py +555 -0
  17. oscura/analyzers/jitter/__init__.py +83 -0
  18. oscura/analyzers/jitter/ber.py +333 -0
  19. oscura/analyzers/jitter/decomposition.py +759 -0
  20. oscura/analyzers/jitter/measurements.py +413 -0
  21. oscura/analyzers/jitter/spectrum.py +220 -0
  22. oscura/analyzers/measurements.py +40 -0
  23. oscura/analyzers/packet/__init__.py +171 -0
  24. oscura/analyzers/packet/daq.py +1077 -0
  25. oscura/analyzers/packet/metrics.py +437 -0
  26. oscura/analyzers/packet/parser.py +327 -0
  27. oscura/analyzers/packet/payload.py +2156 -0
  28. oscura/analyzers/packet/payload_analysis.py +1312 -0
  29. oscura/analyzers/packet/payload_extraction.py +236 -0
  30. oscura/analyzers/packet/payload_patterns.py +670 -0
  31. oscura/analyzers/packet/stream.py +359 -0
  32. oscura/analyzers/patterns/__init__.py +266 -0
  33. oscura/analyzers/patterns/clustering.py +1036 -0
  34. oscura/analyzers/patterns/discovery.py +539 -0
  35. oscura/analyzers/patterns/learning.py +797 -0
  36. oscura/analyzers/patterns/matching.py +1091 -0
  37. oscura/analyzers/patterns/periodic.py +650 -0
  38. oscura/analyzers/patterns/sequences.py +767 -0
  39. oscura/analyzers/power/__init__.py +116 -0
  40. oscura/analyzers/power/ac_power.py +391 -0
  41. oscura/analyzers/power/basic.py +383 -0
  42. oscura/analyzers/power/conduction.py +314 -0
  43. oscura/analyzers/power/efficiency.py +297 -0
  44. oscura/analyzers/power/ripple.py +356 -0
  45. oscura/analyzers/power/soa.py +372 -0
  46. oscura/analyzers/power/switching.py +479 -0
  47. oscura/analyzers/protocol/__init__.py +150 -0
  48. oscura/analyzers/protocols/__init__.py +150 -0
  49. oscura/analyzers/protocols/base.py +500 -0
  50. oscura/analyzers/protocols/can.py +620 -0
  51. oscura/analyzers/protocols/can_fd.py +448 -0
  52. oscura/analyzers/protocols/flexray.py +405 -0
  53. oscura/analyzers/protocols/hdlc.py +399 -0
  54. oscura/analyzers/protocols/i2c.py +368 -0
  55. oscura/analyzers/protocols/i2s.py +296 -0
  56. oscura/analyzers/protocols/jtag.py +393 -0
  57. oscura/analyzers/protocols/lin.py +445 -0
  58. oscura/analyzers/protocols/manchester.py +333 -0
  59. oscura/analyzers/protocols/onewire.py +501 -0
  60. oscura/analyzers/protocols/spi.py +334 -0
  61. oscura/analyzers/protocols/swd.py +325 -0
  62. oscura/analyzers/protocols/uart.py +393 -0
  63. oscura/analyzers/protocols/usb.py +495 -0
  64. oscura/analyzers/signal_integrity/__init__.py +63 -0
  65. oscura/analyzers/signal_integrity/embedding.py +294 -0
  66. oscura/analyzers/signal_integrity/equalization.py +370 -0
  67. oscura/analyzers/signal_integrity/sparams.py +484 -0
  68. oscura/analyzers/spectral/__init__.py +53 -0
  69. oscura/analyzers/spectral/chunked.py +273 -0
  70. oscura/analyzers/spectral/chunked_fft.py +571 -0
  71. oscura/analyzers/spectral/chunked_wavelet.py +391 -0
  72. oscura/analyzers/spectral/fft.py +92 -0
  73. oscura/analyzers/statistical/__init__.py +250 -0
  74. oscura/analyzers/statistical/checksum.py +923 -0
  75. oscura/analyzers/statistical/chunked_corr.py +228 -0
  76. oscura/analyzers/statistical/classification.py +778 -0
  77. oscura/analyzers/statistical/entropy.py +1113 -0
  78. oscura/analyzers/statistical/ngrams.py +614 -0
  79. oscura/analyzers/statistics/__init__.py +119 -0
  80. oscura/analyzers/statistics/advanced.py +885 -0
  81. oscura/analyzers/statistics/basic.py +263 -0
  82. oscura/analyzers/statistics/correlation.py +630 -0
  83. oscura/analyzers/statistics/distribution.py +298 -0
  84. oscura/analyzers/statistics/outliers.py +463 -0
  85. oscura/analyzers/statistics/streaming.py +93 -0
  86. oscura/analyzers/statistics/trend.py +520 -0
  87. oscura/analyzers/validation.py +598 -0
  88. oscura/analyzers/waveform/__init__.py +36 -0
  89. oscura/analyzers/waveform/measurements.py +943 -0
  90. oscura/analyzers/waveform/measurements_with_uncertainty.py +371 -0
  91. oscura/analyzers/waveform/spectral.py +1689 -0
  92. oscura/analyzers/waveform/wavelets.py +298 -0
  93. oscura/api/__init__.py +62 -0
  94. oscura/api/dsl.py +538 -0
  95. oscura/api/fluent.py +571 -0
  96. oscura/api/operators.py +498 -0
  97. oscura/api/optimization.py +392 -0
  98. oscura/api/profiling.py +396 -0
  99. oscura/automotive/__init__.py +73 -0
  100. oscura/automotive/can/__init__.py +52 -0
  101. oscura/automotive/can/analysis.py +356 -0
  102. oscura/automotive/can/checksum.py +250 -0
  103. oscura/automotive/can/correlation.py +212 -0
  104. oscura/automotive/can/discovery.py +355 -0
  105. oscura/automotive/can/message_wrapper.py +375 -0
  106. oscura/automotive/can/models.py +385 -0
  107. oscura/automotive/can/patterns.py +381 -0
  108. oscura/automotive/can/session.py +452 -0
  109. oscura/automotive/can/state_machine.py +300 -0
  110. oscura/automotive/can/stimulus_response.py +461 -0
  111. oscura/automotive/dbc/__init__.py +15 -0
  112. oscura/automotive/dbc/generator.py +156 -0
  113. oscura/automotive/dbc/parser.py +146 -0
  114. oscura/automotive/dtc/__init__.py +30 -0
  115. oscura/automotive/dtc/database.py +3036 -0
  116. oscura/automotive/j1939/__init__.py +14 -0
  117. oscura/automotive/j1939/decoder.py +745 -0
  118. oscura/automotive/loaders/__init__.py +35 -0
  119. oscura/automotive/loaders/asc.py +98 -0
  120. oscura/automotive/loaders/blf.py +77 -0
  121. oscura/automotive/loaders/csv_can.py +136 -0
  122. oscura/automotive/loaders/dispatcher.py +136 -0
  123. oscura/automotive/loaders/mdf.py +331 -0
  124. oscura/automotive/loaders/pcap.py +132 -0
  125. oscura/automotive/obd/__init__.py +14 -0
  126. oscura/automotive/obd/decoder.py +707 -0
  127. oscura/automotive/uds/__init__.py +48 -0
  128. oscura/automotive/uds/decoder.py +265 -0
  129. oscura/automotive/uds/models.py +64 -0
  130. oscura/automotive/visualization.py +369 -0
  131. oscura/batch/__init__.py +55 -0
  132. oscura/batch/advanced.py +627 -0
  133. oscura/batch/aggregate.py +300 -0
  134. oscura/batch/analyze.py +139 -0
  135. oscura/batch/logging.py +487 -0
  136. oscura/batch/metrics.py +556 -0
  137. oscura/builders/__init__.py +41 -0
  138. oscura/builders/signal_builder.py +1131 -0
  139. oscura/cli/__init__.py +14 -0
  140. oscura/cli/batch.py +339 -0
  141. oscura/cli/characterize.py +273 -0
  142. oscura/cli/compare.py +775 -0
  143. oscura/cli/decode.py +551 -0
  144. oscura/cli/main.py +247 -0
  145. oscura/cli/shell.py +350 -0
  146. oscura/comparison/__init__.py +66 -0
  147. oscura/comparison/compare.py +397 -0
  148. oscura/comparison/golden.py +487 -0
  149. oscura/comparison/limits.py +391 -0
  150. oscura/comparison/mask.py +434 -0
  151. oscura/comparison/trace_diff.py +30 -0
  152. oscura/comparison/visualization.py +481 -0
  153. oscura/compliance/__init__.py +70 -0
  154. oscura/compliance/advanced.py +756 -0
  155. oscura/compliance/masks.py +363 -0
  156. oscura/compliance/reporting.py +483 -0
  157. oscura/compliance/testing.py +298 -0
  158. oscura/component/__init__.py +38 -0
  159. oscura/component/impedance.py +365 -0
  160. oscura/component/reactive.py +598 -0
  161. oscura/component/transmission_line.py +312 -0
  162. oscura/config/__init__.py +191 -0
  163. oscura/config/defaults.py +254 -0
  164. oscura/config/loader.py +348 -0
  165. oscura/config/memory.py +271 -0
  166. oscura/config/migration.py +458 -0
  167. oscura/config/pipeline.py +1077 -0
  168. oscura/config/preferences.py +530 -0
  169. oscura/config/protocol.py +875 -0
  170. oscura/config/schema.py +713 -0
  171. oscura/config/settings.py +420 -0
  172. oscura/config/thresholds.py +599 -0
  173. oscura/convenience.py +457 -0
  174. oscura/core/__init__.py +299 -0
  175. oscura/core/audit.py +457 -0
  176. oscura/core/backend_selector.py +405 -0
  177. oscura/core/cache.py +590 -0
  178. oscura/core/cancellation.py +439 -0
  179. oscura/core/confidence.py +225 -0
  180. oscura/core/config.py +506 -0
  181. oscura/core/correlation.py +216 -0
  182. oscura/core/cross_domain.py +422 -0
  183. oscura/core/debug.py +301 -0
  184. oscura/core/edge_cases.py +541 -0
  185. oscura/core/exceptions.py +535 -0
  186. oscura/core/gpu_backend.py +523 -0
  187. oscura/core/lazy.py +832 -0
  188. oscura/core/log_query.py +540 -0
  189. oscura/core/logging.py +931 -0
  190. oscura/core/logging_advanced.py +952 -0
  191. oscura/core/memoize.py +171 -0
  192. oscura/core/memory_check.py +274 -0
  193. oscura/core/memory_guard.py +290 -0
  194. oscura/core/memory_limits.py +336 -0
  195. oscura/core/memory_monitor.py +453 -0
  196. oscura/core/memory_progress.py +465 -0
  197. oscura/core/memory_warnings.py +315 -0
  198. oscura/core/numba_backend.py +362 -0
  199. oscura/core/performance.py +352 -0
  200. oscura/core/progress.py +524 -0
  201. oscura/core/provenance.py +358 -0
  202. oscura/core/results.py +331 -0
  203. oscura/core/types.py +504 -0
  204. oscura/core/uncertainty.py +383 -0
  205. oscura/discovery/__init__.py +52 -0
  206. oscura/discovery/anomaly_detector.py +672 -0
  207. oscura/discovery/auto_decoder.py +415 -0
  208. oscura/discovery/comparison.py +497 -0
  209. oscura/discovery/quality_validator.py +528 -0
  210. oscura/discovery/signal_detector.py +769 -0
  211. oscura/dsl/__init__.py +73 -0
  212. oscura/dsl/commands.py +246 -0
  213. oscura/dsl/interpreter.py +455 -0
  214. oscura/dsl/parser.py +689 -0
  215. oscura/dsl/repl.py +172 -0
  216. oscura/exceptions.py +59 -0
  217. oscura/exploratory/__init__.py +111 -0
  218. oscura/exploratory/error_recovery.py +642 -0
  219. oscura/exploratory/fuzzy.py +513 -0
  220. oscura/exploratory/fuzzy_advanced.py +786 -0
  221. oscura/exploratory/legacy.py +831 -0
  222. oscura/exploratory/parse.py +358 -0
  223. oscura/exploratory/recovery.py +275 -0
  224. oscura/exploratory/sync.py +382 -0
  225. oscura/exploratory/unknown.py +707 -0
  226. oscura/export/__init__.py +25 -0
  227. oscura/export/wireshark/README.md +265 -0
  228. oscura/export/wireshark/__init__.py +47 -0
  229. oscura/export/wireshark/generator.py +312 -0
  230. oscura/export/wireshark/lua_builder.py +159 -0
  231. oscura/export/wireshark/templates/dissector.lua.j2 +92 -0
  232. oscura/export/wireshark/type_mapping.py +165 -0
  233. oscura/export/wireshark/validator.py +105 -0
  234. oscura/exporters/__init__.py +94 -0
  235. oscura/exporters/csv.py +303 -0
  236. oscura/exporters/exporters.py +44 -0
  237. oscura/exporters/hdf5.py +219 -0
  238. oscura/exporters/html_export.py +701 -0
  239. oscura/exporters/json_export.py +291 -0
  240. oscura/exporters/markdown_export.py +367 -0
  241. oscura/exporters/matlab_export.py +354 -0
  242. oscura/exporters/npz_export.py +219 -0
  243. oscura/exporters/spice_export.py +210 -0
  244. oscura/extensibility/__init__.py +131 -0
  245. oscura/extensibility/docs.py +752 -0
  246. oscura/extensibility/extensions.py +1125 -0
  247. oscura/extensibility/logging.py +259 -0
  248. oscura/extensibility/measurements.py +485 -0
  249. oscura/extensibility/plugins.py +414 -0
  250. oscura/extensibility/registry.py +346 -0
  251. oscura/extensibility/templates.py +913 -0
  252. oscura/extensibility/validation.py +651 -0
  253. oscura/filtering/__init__.py +89 -0
  254. oscura/filtering/base.py +563 -0
  255. oscura/filtering/convenience.py +564 -0
  256. oscura/filtering/design.py +725 -0
  257. oscura/filtering/filters.py +32 -0
  258. oscura/filtering/introspection.py +605 -0
  259. oscura/guidance/__init__.py +24 -0
  260. oscura/guidance/recommender.py +429 -0
  261. oscura/guidance/wizard.py +518 -0
  262. oscura/inference/__init__.py +251 -0
  263. oscura/inference/active_learning/README.md +153 -0
  264. oscura/inference/active_learning/__init__.py +38 -0
  265. oscura/inference/active_learning/lstar.py +257 -0
  266. oscura/inference/active_learning/observation_table.py +230 -0
  267. oscura/inference/active_learning/oracle.py +78 -0
  268. oscura/inference/active_learning/teachers/__init__.py +15 -0
  269. oscura/inference/active_learning/teachers/simulator.py +192 -0
  270. oscura/inference/adaptive_tuning.py +453 -0
  271. oscura/inference/alignment.py +653 -0
  272. oscura/inference/bayesian.py +943 -0
  273. oscura/inference/binary.py +1016 -0
  274. oscura/inference/crc_reverse.py +711 -0
  275. oscura/inference/logic.py +288 -0
  276. oscura/inference/message_format.py +1305 -0
  277. oscura/inference/protocol.py +417 -0
  278. oscura/inference/protocol_dsl.py +1084 -0
  279. oscura/inference/protocol_library.py +1230 -0
  280. oscura/inference/sequences.py +809 -0
  281. oscura/inference/signal_intelligence.py +1509 -0
  282. oscura/inference/spectral.py +215 -0
  283. oscura/inference/state_machine.py +634 -0
  284. oscura/inference/stream.py +918 -0
  285. oscura/integrations/__init__.py +59 -0
  286. oscura/integrations/llm.py +1827 -0
  287. oscura/jupyter/__init__.py +32 -0
  288. oscura/jupyter/display.py +268 -0
  289. oscura/jupyter/magic.py +334 -0
  290. oscura/loaders/__init__.py +526 -0
  291. oscura/loaders/binary.py +69 -0
  292. oscura/loaders/configurable.py +1255 -0
  293. oscura/loaders/csv.py +26 -0
  294. oscura/loaders/csv_loader.py +473 -0
  295. oscura/loaders/hdf5.py +9 -0
  296. oscura/loaders/hdf5_loader.py +510 -0
  297. oscura/loaders/lazy.py +370 -0
  298. oscura/loaders/mmap_loader.py +583 -0
  299. oscura/loaders/numpy_loader.py +436 -0
  300. oscura/loaders/pcap.py +432 -0
  301. oscura/loaders/preprocessing.py +368 -0
  302. oscura/loaders/rigol.py +287 -0
  303. oscura/loaders/sigrok.py +321 -0
  304. oscura/loaders/tdms.py +367 -0
  305. oscura/loaders/tektronix.py +711 -0
  306. oscura/loaders/validation.py +584 -0
  307. oscura/loaders/vcd.py +464 -0
  308. oscura/loaders/wav.py +233 -0
  309. oscura/math/__init__.py +45 -0
  310. oscura/math/arithmetic.py +824 -0
  311. oscura/math/interpolation.py +413 -0
  312. oscura/onboarding/__init__.py +39 -0
  313. oscura/onboarding/help.py +498 -0
  314. oscura/onboarding/tutorials.py +405 -0
  315. oscura/onboarding/wizard.py +466 -0
  316. oscura/optimization/__init__.py +19 -0
  317. oscura/optimization/parallel.py +440 -0
  318. oscura/optimization/search.py +532 -0
  319. oscura/pipeline/__init__.py +43 -0
  320. oscura/pipeline/base.py +338 -0
  321. oscura/pipeline/composition.py +242 -0
  322. oscura/pipeline/parallel.py +448 -0
  323. oscura/pipeline/pipeline.py +375 -0
  324. oscura/pipeline/reverse_engineering.py +1119 -0
  325. oscura/plugins/__init__.py +122 -0
  326. oscura/plugins/base.py +272 -0
  327. oscura/plugins/cli.py +497 -0
  328. oscura/plugins/discovery.py +411 -0
  329. oscura/plugins/isolation.py +418 -0
  330. oscura/plugins/lifecycle.py +959 -0
  331. oscura/plugins/manager.py +493 -0
  332. oscura/plugins/registry.py +421 -0
  333. oscura/plugins/versioning.py +372 -0
  334. oscura/py.typed +0 -0
  335. oscura/quality/__init__.py +65 -0
  336. oscura/quality/ensemble.py +740 -0
  337. oscura/quality/explainer.py +338 -0
  338. oscura/quality/scoring.py +616 -0
  339. oscura/quality/warnings.py +456 -0
  340. oscura/reporting/__init__.py +248 -0
  341. oscura/reporting/advanced.py +1234 -0
  342. oscura/reporting/analyze.py +448 -0
  343. oscura/reporting/argument_preparer.py +596 -0
  344. oscura/reporting/auto_report.py +507 -0
  345. oscura/reporting/batch.py +615 -0
  346. oscura/reporting/chart_selection.py +223 -0
  347. oscura/reporting/comparison.py +330 -0
  348. oscura/reporting/config.py +615 -0
  349. oscura/reporting/content/__init__.py +39 -0
  350. oscura/reporting/content/executive.py +127 -0
  351. oscura/reporting/content/filtering.py +191 -0
  352. oscura/reporting/content/minimal.py +257 -0
  353. oscura/reporting/content/verbosity.py +162 -0
  354. oscura/reporting/core.py +508 -0
  355. oscura/reporting/core_formats/__init__.py +17 -0
  356. oscura/reporting/core_formats/multi_format.py +210 -0
  357. oscura/reporting/engine.py +836 -0
  358. oscura/reporting/export.py +366 -0
  359. oscura/reporting/formatting/__init__.py +129 -0
  360. oscura/reporting/formatting/emphasis.py +81 -0
  361. oscura/reporting/formatting/numbers.py +403 -0
  362. oscura/reporting/formatting/standards.py +55 -0
  363. oscura/reporting/formatting.py +466 -0
  364. oscura/reporting/html.py +578 -0
  365. oscura/reporting/index.py +590 -0
  366. oscura/reporting/multichannel.py +296 -0
  367. oscura/reporting/output.py +379 -0
  368. oscura/reporting/pdf.py +373 -0
  369. oscura/reporting/plots.py +731 -0
  370. oscura/reporting/pptx_export.py +360 -0
  371. oscura/reporting/renderers/__init__.py +11 -0
  372. oscura/reporting/renderers/pdf.py +94 -0
  373. oscura/reporting/sections.py +471 -0
  374. oscura/reporting/standards.py +680 -0
  375. oscura/reporting/summary_generator.py +368 -0
  376. oscura/reporting/tables.py +397 -0
  377. oscura/reporting/template_system.py +724 -0
  378. oscura/reporting/templates/__init__.py +15 -0
  379. oscura/reporting/templates/definition.py +205 -0
  380. oscura/reporting/templates/index.html +649 -0
  381. oscura/reporting/templates/index.md +173 -0
  382. oscura/schemas/__init__.py +158 -0
  383. oscura/schemas/bus_configuration.json +322 -0
  384. oscura/schemas/device_mapping.json +182 -0
  385. oscura/schemas/packet_format.json +418 -0
  386. oscura/schemas/protocol_definition.json +363 -0
  387. oscura/search/__init__.py +16 -0
  388. oscura/search/anomaly.py +292 -0
  389. oscura/search/context.py +149 -0
  390. oscura/search/pattern.py +160 -0
  391. oscura/session/__init__.py +34 -0
  392. oscura/session/annotations.py +289 -0
  393. oscura/session/history.py +313 -0
  394. oscura/session/session.py +445 -0
  395. oscura/streaming/__init__.py +43 -0
  396. oscura/streaming/chunked.py +611 -0
  397. oscura/streaming/progressive.py +393 -0
  398. oscura/streaming/realtime.py +622 -0
  399. oscura/testing/__init__.py +54 -0
  400. oscura/testing/synthetic.py +808 -0
  401. oscura/triggering/__init__.py +68 -0
  402. oscura/triggering/base.py +229 -0
  403. oscura/triggering/edge.py +353 -0
  404. oscura/triggering/pattern.py +344 -0
  405. oscura/triggering/pulse.py +581 -0
  406. oscura/triggering/window.py +453 -0
  407. oscura/ui/__init__.py +48 -0
  408. oscura/ui/formatters.py +526 -0
  409. oscura/ui/progressive_display.py +340 -0
  410. oscura/utils/__init__.py +99 -0
  411. oscura/utils/autodetect.py +338 -0
  412. oscura/utils/buffer.py +389 -0
  413. oscura/utils/lazy.py +407 -0
  414. oscura/utils/lazy_imports.py +147 -0
  415. oscura/utils/memory.py +836 -0
  416. oscura/utils/memory_advanced.py +1326 -0
  417. oscura/utils/memory_extensions.py +465 -0
  418. oscura/utils/progressive.py +352 -0
  419. oscura/utils/windowing.py +362 -0
  420. oscura/visualization/__init__.py +321 -0
  421. oscura/visualization/accessibility.py +526 -0
  422. oscura/visualization/annotations.py +374 -0
  423. oscura/visualization/axis_scaling.py +305 -0
  424. oscura/visualization/colors.py +453 -0
  425. oscura/visualization/digital.py +337 -0
  426. oscura/visualization/eye.py +420 -0
  427. oscura/visualization/histogram.py +281 -0
  428. oscura/visualization/interactive.py +858 -0
  429. oscura/visualization/jitter.py +702 -0
  430. oscura/visualization/keyboard.py +394 -0
  431. oscura/visualization/layout.py +365 -0
  432. oscura/visualization/optimization.py +1028 -0
  433. oscura/visualization/palettes.py +446 -0
  434. oscura/visualization/plot.py +92 -0
  435. oscura/visualization/power.py +290 -0
  436. oscura/visualization/power_extended.py +626 -0
  437. oscura/visualization/presets.py +467 -0
  438. oscura/visualization/protocols.py +932 -0
  439. oscura/visualization/render.py +207 -0
  440. oscura/visualization/rendering.py +444 -0
  441. oscura/visualization/reverse_engineering.py +791 -0
  442. oscura/visualization/signal_integrity.py +808 -0
  443. oscura/visualization/specialized.py +553 -0
  444. oscura/visualization/spectral.py +811 -0
  445. oscura/visualization/styles.py +381 -0
  446. oscura/visualization/thumbnails.py +311 -0
  447. oscura/visualization/time_axis.py +351 -0
  448. oscura/visualization/waveform.py +367 -0
  449. oscura/workflow/__init__.py +13 -0
  450. oscura/workflow/dag.py +377 -0
  451. oscura/workflows/__init__.py +58 -0
  452. oscura/workflows/compliance.py +280 -0
  453. oscura/workflows/digital.py +272 -0
  454. oscura/workflows/multi_trace.py +502 -0
  455. oscura/workflows/power.py +178 -0
  456. oscura/workflows/protocol.py +492 -0
  457. oscura/workflows/reverse_engineering.py +639 -0
  458. oscura/workflows/signal_integrity.py +227 -0
  459. oscura-0.1.0.dist-info/METADATA +300 -0
  460. oscura-0.1.0.dist-info/RECORD +463 -0
  461. oscura-0.1.0.dist-info/entry_points.txt +2 -0
  462. {oscura-0.0.1.dist-info → oscura-0.1.0.dist-info}/licenses/LICENSE +1 -1
  463. oscura-0.0.1.dist-info/METADATA +0 -63
  464. oscura-0.0.1.dist-info/RECORD +0 -5
  465. {oscura-0.0.1.dist-info → oscura-0.1.0.dist-info}/WHEEL +0 -0
oscura/core/logging.py ADDED
@@ -0,0 +1,931 @@
1
+ """Structured logging infrastructure for TraceKit.
2
+
3
+ This module provides structured logging with JSON/logfmt support,
4
+ hierarchical loggers, log rotation, and error context capture.
5
+
6
+
7
+ Example:
8
+ >>> from oscura.core.logging import configure_logging, get_logger
9
+ >>> configure_logging(format='json', level='INFO')
10
+ >>> logger = get_logger('oscura.loaders')
11
+ >>> logger.info("Loading trace", file="data.bin", size_mb=1024)
12
+
13
+ References:
14
+ Python logging module best practices
15
+ LOG-001 through LOG-008 requirements
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import gzip
21
+ import json
22
+ import logging
23
+ import logging.handlers
24
+ import os
25
+ import shutil
26
+ import sys
27
+ import time
28
+ import traceback
29
+ from dataclasses import dataclass
30
+ from datetime import UTC, datetime
31
+ from pathlib import Path
32
+ from typing import Any, Literal
33
+
34
+ # Global logging configuration
35
+ _logging_configured = False
36
+ _root_logger_name = "oscura"
37
+
38
+
39
+ @dataclass
40
+ class LogConfig:
41
+ """Logging configuration.
42
+
43
+ Attributes:
44
+ level: Default log level (DEBUG, INFO, WARNING, ERROR, CRITICAL).
45
+ format: Output format (json, logfmt, text).
46
+ timestamp_format: Timestamp format (iso8601, iso8601_local, unix, custom).
47
+ custom_timestamp_format: Custom timestamp format string.
48
+ console_output: Enable console output to stderr.
49
+ file_output: Enable file output.
50
+ file_path: Path to log file.
51
+ max_bytes: Maximum log file size before rotation.
52
+ backup_count: Number of rotated log files to keep.
53
+ compress: Compress rotated log files.
54
+ when: Time-based rotation interval type ('midnight', 'H', 'D', 'W0'-'W6').
55
+ interval: Interval for time-based rotation.
56
+ max_age: Maximum age for log files (e.g., '30d').
57
+
58
+ References:
59
+ LOG-001: Structured Logging Framework
60
+ LOG-003: Automatic Log Rotation and Retention Policies
61
+ """
62
+
63
+ level: str = "INFO"
64
+ format: Literal["json", "logfmt", "text"] = "text"
65
+ timestamp_format: Literal["iso8601", "iso8601_local", "unix", "custom"] = "iso8601"
66
+ custom_timestamp_format: str | None = None
67
+ console_output: bool = True
68
+ file_output: bool = False
69
+ file_path: str | None = None
70
+ max_bytes: int = 10_000_000 # 10 MB
71
+ backup_count: int = 5
72
+ compress: bool = False
73
+ when: str | None = None # Time-based rotation: 'midnight', 'H', 'D', 'W0'-'W6'
74
+ interval: int = 1 # Interval for time-based rotation
75
+ max_age: str | None = None # Maximum age for log files (e.g., '30d')
76
+
77
+
78
+ # Default configuration
79
+ _config = LogConfig()
80
+
81
+
82
+ class CompressingRotatingFileHandler(logging.handlers.RotatingFileHandler):
83
+ """RotatingFileHandler that compresses rotated files with gzip.
84
+
85
+ Extends standard RotatingFileHandler to optionally compress log files
86
+ when they are rotated, saving disk space for historical logs.
87
+
88
+ Args:
89
+ filename: Path to log file.
90
+ mode: File open mode.
91
+ maxBytes: Maximum file size before rotation.
92
+ backupCount: Number of backup files to keep.
93
+ encoding: File encoding.
94
+ compress: Whether to gzip compress rotated files.
95
+
96
+ References:
97
+ LOG-003: Automatic Log Rotation and Retention Policies
98
+ """
99
+
100
+ def __init__(
101
+ self,
102
+ filename: str,
103
+ mode: str = "a",
104
+ maxBytes: int = 0,
105
+ backupCount: int = 0,
106
+ encoding: str | None = None,
107
+ compress: bool = False,
108
+ ):
109
+ """Initialize compressing rotating file handler.
110
+
111
+ Args:
112
+ filename: Path to log file.
113
+ mode: File open mode.
114
+ maxBytes: Maximum file size before rotation.
115
+ backupCount: Number of backup files to keep.
116
+ encoding: File encoding.
117
+ compress: Whether to gzip compress rotated files.
118
+ """
119
+ super().__init__(filename, mode, maxBytes, backupCount, encoding)
120
+ self.compress = compress
121
+
122
+ def doRollover(self) -> None:
123
+ """Perform rollover and optionally compress the rotated file.
124
+
125
+ References:
126
+ LOG-003: Automatic Log Rotation and Retention Policies
127
+ """
128
+ # Standard rollover
129
+ super().doRollover()
130
+
131
+ # Compress the rolled file if compression is enabled
132
+ if self.compress and self.backupCount > 0:
133
+ # The most recently rotated file is .1
134
+ rotated_file = f"{self.baseFilename}.1"
135
+ compressed_file = f"{rotated_file}.gz"
136
+
137
+ if Path(rotated_file).exists():
138
+ # Compress the file
139
+ with open(rotated_file, "rb") as f_in:
140
+ with gzip.open(compressed_file, "wb") as f_out:
141
+ shutil.copyfileobj(f_in, f_out)
142
+
143
+ # Remove the uncompressed file
144
+ Path(rotated_file).unlink()
145
+
146
+
147
+ class CompressingTimedRotatingFileHandler(logging.handlers.TimedRotatingFileHandler):
148
+ """TimedRotatingFileHandler that compresses rotated files with gzip.
149
+
150
+ Extends standard TimedRotatingFileHandler to optionally compress log files
151
+ when they are rotated, saving disk space for historical logs.
152
+
153
+ Args:
154
+ filename: Path to log file.
155
+ when: Type of interval ('midnight', 'H', 'D', 'W0'-'W6').
156
+ interval: Number of intervals between rotations.
157
+ backupCount: Number of backup files to keep.
158
+ encoding: File encoding.
159
+ compress: Whether to gzip compress rotated files.
160
+ max_age: Maximum age for log files (e.g., '30d').
161
+
162
+ References:
163
+ LOG-003: Automatic Log Rotation and Retention Policies
164
+ """
165
+
166
+ def __init__(
167
+ self,
168
+ filename: str,
169
+ when: str = "midnight",
170
+ interval: int = 1,
171
+ backupCount: int = 0,
172
+ encoding: str | None = None,
173
+ compress: bool = False,
174
+ max_age: str | None = None,
175
+ ):
176
+ """Initialize compressing timed rotating file handler.
177
+
178
+ Args:
179
+ filename: Path to log file.
180
+ when: Type of interval ('midnight', 'H', 'D', 'W0'-'W6').
181
+ interval: Number of intervals between rotations.
182
+ backupCount: Number of backup files to keep.
183
+ encoding: File encoding.
184
+ compress: Whether to gzip compress rotated files.
185
+ max_age: Maximum age for log files (e.g., '30d').
186
+ """
187
+ super().__init__(filename, when, interval, backupCount, encoding=encoding)
188
+ self.compress = compress
189
+ self.max_age = max_age
190
+ self._max_age_seconds = self._parse_max_age(max_age) if max_age else None
191
+
192
+ def _parse_max_age(self, max_age: str) -> int:
193
+ """Parse max_age string to seconds.
194
+
195
+ Args:
196
+ max_age: Age string like '30d', '7d', '24h'.
197
+
198
+ Returns:
199
+ Number of seconds.
200
+
201
+ Raises:
202
+ ValueError: If max_age format is invalid.
203
+ """
204
+ if max_age.endswith("d"):
205
+ return int(max_age[:-1]) * 86400 # days to seconds
206
+ elif max_age.endswith("h"):
207
+ return int(max_age[:-1]) * 3600 # hours to seconds
208
+ elif max_age.endswith("m"):
209
+ return int(max_age[:-1]) * 60 # minutes to seconds
210
+ else:
211
+ raise ValueError(f"Invalid max_age format: {max_age}. Use 'd', 'h', or 'm' suffix.")
212
+
213
+ def doRollover(self) -> None:
214
+ """Perform rollover and optionally compress the rotated file.
215
+
216
+ Also cleans up files older than max_age if specified.
217
+
218
+ References:
219
+ LOG-003: Automatic Log Rotation and Retention Policies
220
+ """
221
+ # Close stream before rollover
222
+ if self.stream is not None:
223
+ self.stream.close()
224
+ self.stream = None # type: ignore[assignment]
225
+
226
+ # Determine the file that just got rotated
227
+ current_time = int(self.rolloverAt - self.interval)
228
+ time_tuple = time.gmtime(current_time) if self.utc else time.localtime(current_time)
229
+ dfn = self.rotation_filename(
230
+ self.baseFilename + "." + self.suffix % time_tuple[:6] # type: ignore[arg-type]
231
+ )
232
+
233
+ # Handle the existing rotated file
234
+ if Path(dfn).exists():
235
+ Path(dfn).unlink()
236
+
237
+ # Rotate the current file
238
+ self.rotate(self.baseFilename, dfn)
239
+
240
+ # Compress if enabled
241
+ if self.compress and Path(dfn).exists():
242
+ compressed_file = f"{dfn}.gz"
243
+ with open(dfn, "rb") as f_in, gzip.open(compressed_file, "wb") as f_out:
244
+ shutil.copyfileobj(f_in, f_out)
245
+ Path(dfn).unlink()
246
+
247
+ # Clean up old files based on max_age
248
+ if self._max_age_seconds:
249
+ self._cleanup_old_files()
250
+
251
+ # Delete old files based on backupCount
252
+ if self.backupCount > 0:
253
+ self._delete_old_files()
254
+
255
+ # Set next rollover time
256
+ new_rollover_at = self.computeRollover(current_time)
257
+ while new_rollover_at <= self.rolloverAt:
258
+ new_rollover_at = new_rollover_at + self.interval
259
+ self.rolloverAt = new_rollover_at
260
+
261
+ # Open new log file
262
+ if not self.delay:
263
+ self.stream = self._open()
264
+
265
+ def _cleanup_old_files(self) -> None:
266
+ """Remove log files older than max_age.
267
+
268
+ References:
269
+ LOG-003: Automatic Log Rotation and Retention Policies
270
+ """
271
+ if not self._max_age_seconds:
272
+ return
273
+
274
+ now = datetime.now().timestamp()
275
+ base_path = Path(self.baseFilename)
276
+ log_dir = base_path.parent
277
+ base_name = base_path.name
278
+
279
+ for log_file in log_dir.glob(f"{base_name}.*"):
280
+ try:
281
+ file_age = now - log_file.stat().st_mtime
282
+ if file_age > self._max_age_seconds:
283
+ log_file.unlink()
284
+ except OSError:
285
+ pass # Ignore errors during cleanup
286
+
287
+ def _delete_old_files(self) -> None:
288
+ """Delete files exceeding backup count.
289
+
290
+ References:
291
+ LOG-003: Automatic Log Rotation and Retention Policies
292
+ """
293
+ base_path = Path(self.baseFilename)
294
+ log_dir = base_path.parent
295
+ base_name = base_path.name
296
+
297
+ # Get all rotated files
298
+ rotated_files = sorted(
299
+ log_dir.glob(f"{base_name}.*"),
300
+ key=lambda p: p.stat().st_mtime,
301
+ reverse=True,
302
+ )
303
+
304
+ # Remove files beyond backup count
305
+ for old_file in rotated_files[self.backupCount :]:
306
+ try:
307
+ old_file.unlink()
308
+ except OSError:
309
+ pass
310
+
311
+
312
+ class StructuredFormatter(logging.Formatter):
313
+ """Formatter that produces structured log output (JSON or logfmt).
314
+
315
+ Supports multiple output formats with ISO 8601 timestamps and
316
+ automatic correlation ID injection.
317
+
318
+ Args:
319
+ fmt: Output format (json, logfmt, text).
320
+ timestamp_format: Timestamp format (iso8601, iso8601_local, unix).
321
+
322
+ References:
323
+ LOG-001: Structured Logging Framework
324
+ LOG-005: ISO 8601 Timestamps
325
+ """
326
+
327
+ def __init__(
328
+ self,
329
+ fmt: Literal["json", "logfmt", "text"] = "text",
330
+ timestamp_format: str = "iso8601",
331
+ ):
332
+ """Initialize structured formatter.
333
+
334
+ Args:
335
+ fmt: Output format.
336
+ timestamp_format: Timestamp format.
337
+ """
338
+ super().__init__()
339
+ self.fmt = fmt
340
+ self.timestamp_format = timestamp_format
341
+
342
+ def format(self, record: logging.LogRecord) -> str:
343
+ """Format log record as structured output.
344
+
345
+ Args:
346
+ record: Log record to format.
347
+
348
+ Returns:
349
+ Formatted log string.
350
+ """
351
+ # Get timestamp
352
+ timestamp = self._format_timestamp(record.created)
353
+
354
+ # Build structured data
355
+ data = {
356
+ "timestamp": timestamp,
357
+ "level": record.levelname,
358
+ "module": record.name,
359
+ "message": record.getMessage(),
360
+ }
361
+
362
+ # Add correlation ID if present
363
+ try:
364
+ from oscura.core.correlation import get_correlation_id
365
+
366
+ corr_id = get_correlation_id()
367
+ if corr_id:
368
+ data["correlation_id"] = corr_id
369
+ except ImportError:
370
+ pass # Correlation module not yet loaded
371
+
372
+ # Add extra fields
373
+ for key, value in record.__dict__.items():
374
+ if key not in (
375
+ "name",
376
+ "msg",
377
+ "args",
378
+ "levelname",
379
+ "levelno",
380
+ "pathname",
381
+ "filename",
382
+ "module",
383
+ "exc_info",
384
+ "exc_text",
385
+ "stack_info",
386
+ "lineno",
387
+ "funcName",
388
+ "created",
389
+ "msecs",
390
+ "relativeCreated",
391
+ "thread",
392
+ "threadName",
393
+ "processName",
394
+ "process",
395
+ "message",
396
+ "asctime",
397
+ ):
398
+ data[key] = value
399
+
400
+ # Add exception info if present
401
+ if record.exc_info:
402
+ data["exception"] = self.formatException(record.exc_info)
403
+
404
+ if self.fmt == "json":
405
+ return json.dumps(data, default=str)
406
+ elif self.fmt == "logfmt":
407
+ return self._format_logfmt(data)
408
+ else:
409
+ # Plain text format
410
+ extra = " ".join(
411
+ f"{k}={v}"
412
+ for k, v in data.items()
413
+ if k not in ("timestamp", "level", "module", "message")
414
+ )
415
+ base = f"{timestamp} [{record.levelname}] {record.name}: {record.getMessage()}"
416
+ if extra:
417
+ base += f" | {extra}"
418
+ return base
419
+
420
+ def _format_timestamp(self, created: float) -> str:
421
+ """Format timestamp according to configuration.
422
+
423
+ Args:
424
+ created: Timestamp as float (seconds since epoch).
425
+
426
+ Returns:
427
+ Formatted timestamp string.
428
+
429
+ References:
430
+ LOG-005: ISO 8601 Timestamps
431
+ """
432
+ dt = datetime.fromtimestamp(created, tz=UTC)
433
+ if self.timestamp_format == "iso8601":
434
+ # ISO 8601 with microseconds: 2025-12-20T15:30:45.123456Z
435
+ return dt.strftime("%Y-%m-%dT%H:%M:%S.%f") + "Z"
436
+ elif self.timestamp_format == "iso8601_local":
437
+ dt_local = datetime.fromtimestamp(created)
438
+ return dt_local.strftime("%Y-%m-%dT%H:%M:%S.%f")
439
+ elif self.timestamp_format == "unix":
440
+ return str(created)
441
+ else:
442
+ return dt.strftime(self.timestamp_format)
443
+
444
+ def _format_logfmt(self, data: dict) -> str: # type: ignore[type-arg]
445
+ """Format data as logfmt (key=value pairs).
446
+
447
+ Args:
448
+ data: Dictionary to format.
449
+
450
+ Returns:
451
+ Logfmt formatted string.
452
+ """
453
+ parts = []
454
+ for key, value in data.items():
455
+ if isinstance(value, str) and (" " in value or '"' in value):
456
+ # Quote values with spaces
457
+ value_str = f'"{value.replace(chr(34), chr(92) + chr(34))}"'
458
+ else:
459
+ value_str = str(value)
460
+ parts.append(f"{key}={value_str}")
461
+ return " ".join(parts)
462
+
463
+
464
+ def configure_logging(
465
+ *,
466
+ level: str = "INFO",
467
+ format: Literal["json", "logfmt", "text"] = "text",
468
+ timestamp_format: Literal["iso8601", "iso8601_local", "unix"] = "iso8601",
469
+ handlers: dict[str, dict[str, Any]] | None = None,
470
+ ) -> None:
471
+ """Configure TraceKit logging.
472
+
473
+ Sets up structured logging with the specified format and handlers.
474
+ Supports both size-based and time-based log rotation with optional
475
+ gzip compression.
476
+
477
+ Args:
478
+ level: Default log level (DEBUG, INFO, WARNING, ERROR, CRITICAL).
479
+ format: Output format (json, logfmt, or text).
480
+ timestamp_format: Timestamp format (iso8601, iso8601_local, unix).
481
+ handlers: Dict of handler configurations. Supported handlers:
482
+ - 'console': Console output to stderr.
483
+ - level: Log level for this handler.
484
+ - 'file': File output with rotation.
485
+ - filename: Path to log file.
486
+ - level: Log level for this handler.
487
+ - max_bytes: Max file size before rotation (size-based).
488
+ - backup_count: Number of rotated files to keep.
489
+ - compress: Gzip compress rotated files.
490
+ - when: Time-based rotation ('midnight', 'H', 'D', 'W0'-'W6').
491
+ - interval: Interval for time-based rotation.
492
+ - max_age: Max age for log files (e.g., '30d').
493
+
494
+ Example:
495
+ >>> # Size-based rotation with compression
496
+ >>> configure_logging(handlers={
497
+ ... 'file': {'filename': 'app.log', 'max_bytes': 10e6, 'compress': True}
498
+ ... })
499
+ >>> # Time-based daily rotation
500
+ >>> configure_logging(handlers={
501
+ ... 'file': {'filename': 'app.log', 'when': 'midnight', 'backup_count': 30}
502
+ ... })
503
+ >>> # Combined: time-based with max_age cleanup
504
+ >>> configure_logging(handlers={
505
+ ... 'file': {'filename': 'app.log', 'when': 'midnight',
506
+ ... 'compress': True, 'max_age': '30d'}
507
+ ... })
508
+
509
+ References:
510
+ LOG-001: Structured Logging Framework
511
+ LOG-002: Hierarchical Log Levels
512
+ LOG-003: Automatic Log Rotation and Retention Policies
513
+ """
514
+ global _logging_configured, _config # noqa: PLW0602
515
+
516
+ # Update config
517
+ _config.level = level
518
+ _config.format = format
519
+ _config.timestamp_format = timestamp_format
520
+
521
+ # Get or create root logger
522
+ root_logger = logging.getLogger(_root_logger_name)
523
+ root_logger.setLevel(getattr(logging, level.upper()))
524
+
525
+ # Remove existing handlers and close them to prevent resource leaks
526
+ for handler in root_logger.handlers[:]:
527
+ handler.close()
528
+ root_logger.removeHandler(handler)
529
+
530
+ # Create formatter
531
+ formatter = StructuredFormatter(format, timestamp_format)
532
+
533
+ # Add handlers
534
+ if handlers:
535
+ for name, config in handlers.items():
536
+ if name == "console":
537
+ handler = logging.StreamHandler(sys.stderr)
538
+ handler.setLevel(getattr(logging, config.get("level", level).upper()))
539
+ handler.setFormatter(formatter)
540
+ root_logger.addHandler(handler)
541
+ elif name == "file":
542
+ filename = config.get("filename", "oscura.log")
543
+ handler_level = config.get("level", "DEBUG")
544
+ backup_count = int(config.get("backup_count", 5))
545
+ compress = config.get("compress", False)
546
+
547
+ # Check if time-based rotation is requested
548
+ when = config.get("when")
549
+ if when:
550
+ # Time-based rotation (LOG-003)
551
+ interval = int(config.get("interval", 1))
552
+ max_age = config.get("max_age")
553
+ handler = CompressingTimedRotatingFileHandler(
554
+ filename,
555
+ when=when,
556
+ interval=interval,
557
+ backupCount=backup_count,
558
+ compress=compress,
559
+ max_age=max_age,
560
+ )
561
+ else:
562
+ # Size-based rotation
563
+ max_bytes = int(config.get("max_bytes", 10_000_000))
564
+ handler = CompressingRotatingFileHandler(
565
+ filename,
566
+ maxBytes=max_bytes,
567
+ backupCount=backup_count,
568
+ compress=compress,
569
+ )
570
+
571
+ handler.setLevel(getattr(logging, handler_level.upper()))
572
+ handler.setFormatter(formatter)
573
+ root_logger.addHandler(handler)
574
+ else:
575
+ # Default: console only
576
+ handler = logging.StreamHandler(sys.stderr)
577
+ handler.setLevel(getattr(logging, level.upper()))
578
+ handler.setFormatter(formatter)
579
+ root_logger.addHandler(handler)
580
+
581
+ _logging_configured = True
582
+
583
+
584
+ def get_logger(name: str) -> logging.Logger:
585
+ """Get a logger with the specified name.
586
+
587
+ Returns a logger under the oscura namespace with proper
588
+ configuration.
589
+
590
+ Args:
591
+ name: Logger name (e.g., 'oscura.loaders.binary').
592
+
593
+ Returns:
594
+ Configured logging.Logger instance.
595
+
596
+ Example:
597
+ >>> logger = get_logger('oscura.analyzers.spectral')
598
+ >>> logger.info("Computing FFT", samples=1000000)
599
+
600
+ References:
601
+ LOG-001: Structured Logging Framework
602
+ """
603
+ if not _logging_configured:
604
+ # Auto-configure with defaults
605
+ configure_logging()
606
+
607
+ # Ensure name is under oscura namespace
608
+ if not name.startswith(_root_logger_name):
609
+ name = f"{_root_logger_name}.{name}"
610
+
611
+ return logging.getLogger(name)
612
+
613
+
614
+ def set_log_level(level: str, module: str | None = None) -> None:
615
+ """Set log level globally or for a specific module.
616
+
617
+ Args:
618
+ level: Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL).
619
+ module: Module name to set level for, or None for global.
620
+
621
+ Example:
622
+ >>> set_log_level('DEBUG') # Global
623
+ >>> set_log_level('DEBUG', 'oscura.loaders') # Module-specific
624
+
625
+ References:
626
+ LOG-002: Hierarchical Log Levels
627
+ """
628
+ logger = logging.getLogger(module) if module else logging.getLogger(_root_logger_name)
629
+
630
+ logger.setLevel(getattr(logging, level.upper()))
631
+
632
+
633
+ class ErrorContextCapture:
634
+ """Captures rich error context including stack traces and local variables.
635
+
636
+ Provides detailed error information for debugging including:
637
+ - Full stack trace
638
+ - Local variables at each frame
639
+ - Exception chain
640
+ - System information
641
+
642
+ Example:
643
+ >>> try:
644
+ ... risky_operation()
645
+ ... except Exception as exc:
646
+ ... context = ErrorContextCapture.from_exception(exc, include_locals=True)
647
+ ... logger.error("Operation failed", extra=context.to_dict())
648
+
649
+ References:
650
+ LOG-008: Rich Error Context with Stack Traces
651
+ CORE-006: Helpful exception messages
652
+ """
653
+
654
+ def __init__(
655
+ self,
656
+ exc_type: type[BaseException],
657
+ exc_value: BaseException,
658
+ exc_traceback: Any,
659
+ additional_context: dict[str, Any] | None = None,
660
+ ):
661
+ """Initialize error context capture.
662
+
663
+ Args:
664
+ exc_type: Exception type.
665
+ exc_value: Exception instance.
666
+ exc_traceback: Exception traceback.
667
+ additional_context: Additional context to include.
668
+ """
669
+ self.exc_type = exc_type
670
+ self.exc_value = exc_value
671
+ self.exc_traceback = exc_traceback
672
+ self.additional_context = additional_context or {}
673
+
674
+ @classmethod
675
+ def from_exception(
676
+ cls,
677
+ exc: BaseException,
678
+ include_locals: bool = True, # noqa: ARG003
679
+ additional_context: dict[str, Any] | None = None,
680
+ ) -> ErrorContextCapture:
681
+ """Create error context from an exception.
682
+
683
+ Args:
684
+ exc: Exception to capture.
685
+ include_locals: Whether to include local variables.
686
+ additional_context: Additional context to include.
687
+
688
+ Returns:
689
+ ErrorContextCapture instance.
690
+ """
691
+ exc_type = type(exc)
692
+ exc_value = exc
693
+ exc_traceback = exc.__traceback__
694
+ return cls(exc_type, exc_value, exc_traceback, additional_context)
695
+
696
+ def to_dict(self, include_locals: bool = True) -> dict[str, Any]:
697
+ """Convert error context to dictionary.
698
+
699
+ Args:
700
+ include_locals: Whether to include local variables.
701
+
702
+ Returns:
703
+ Dictionary with error context.
704
+
705
+ References:
706
+ LOG-008: Rich Error Context
707
+ """
708
+ result: dict[str, Any] = {
709
+ "exception_type": self.exc_type.__name__,
710
+ "exception_module": self.exc_type.__module__,
711
+ "exception_message": str(self.exc_value),
712
+ "traceback": traceback.format_exception(
713
+ self.exc_type, self.exc_value, self.exc_traceback
714
+ ),
715
+ }
716
+
717
+ # Add exception chain
718
+ if hasattr(self.exc_value, "__cause__") and self.exc_value.__cause__:
719
+ result["caused_by"] = {
720
+ "type": type(self.exc_value.__cause__).__name__,
721
+ "message": str(self.exc_value.__cause__),
722
+ }
723
+
724
+ # Add local variables if requested
725
+ if include_locals and self.exc_traceback:
726
+ frames = []
727
+ tb = self.exc_traceback
728
+ while tb is not None:
729
+ frame = tb.tb_frame
730
+ frames.append(
731
+ {
732
+ "filename": frame.f_code.co_filename,
733
+ "function": frame.f_code.co_name,
734
+ "lineno": tb.tb_lineno,
735
+ "locals": self._filter_sensitive_data(
736
+ {k: repr(v) for k, v in frame.f_locals.items()}
737
+ ),
738
+ }
739
+ )
740
+ tb = tb.tb_next
741
+ result["frames"] = frames
742
+
743
+ # Add additional context
744
+ if self.additional_context:
745
+ result["context"] = self.additional_context
746
+
747
+ return result
748
+
749
+ def _filter_sensitive_data(self, data: dict[str, str]) -> dict[str, str]:
750
+ """Filter sensitive data from local variables.
751
+
752
+ Args:
753
+ data: Dictionary of local variables.
754
+
755
+ Returns:
756
+ Filtered dictionary with sensitive data redacted.
757
+
758
+ References:
759
+ LOG-008: Rich Error Context (sensitive data filtering)
760
+ """
761
+ sensitive_keys = {
762
+ "password",
763
+ "passwd",
764
+ "pwd",
765
+ "secret",
766
+ "token",
767
+ "api_key",
768
+ "apikey",
769
+ "auth",
770
+ "authorization",
771
+ }
772
+
773
+ filtered = {}
774
+ for key, value in data.items():
775
+ key_lower = key.lower()
776
+ if any(sensitive in key_lower for sensitive in sensitive_keys):
777
+ filtered[key] = "***REDACTED***"
778
+ else:
779
+ filtered[key] = value
780
+ return filtered
781
+
782
+
783
+ def log_exception(
784
+ exc: BaseException,
785
+ logger: logging.Logger | None = None,
786
+ context: dict[str, Any] | None = None,
787
+ include_locals: bool = False,
788
+ ) -> None:
789
+ """Log an exception with full context.
790
+
791
+ Captures rich error context including stack traces, exception chain,
792
+ and optionally local variables for debugging.
793
+
794
+ Args:
795
+ exc: The exception to log.
796
+ logger: Logger to use (default: root oscura logger).
797
+ context: Additional context to include.
798
+ include_locals: Whether to include local variables from stack frames.
799
+
800
+ Example:
801
+ >>> try:
802
+ ... result = complex_computation(data)
803
+ ... except Exception as e:
804
+ ... log_exception(e, context={"data_size": len(data)})
805
+
806
+ References:
807
+ LOG-008: Rich Error Context with Stack Traces
808
+ CORE-006: Helpful exception messages
809
+ """
810
+ if logger is None:
811
+ logger = get_logger("oscura")
812
+
813
+ # Capture error context
814
+ error_context = ErrorContextCapture.from_exception(
815
+ exc, include_locals=include_locals, additional_context=context
816
+ )
817
+
818
+ # Convert to dict and log
819
+ context_dict = error_context.to_dict(include_locals=include_locals)
820
+
821
+ # Log with exception info
822
+ logger.exception("Exception occurred", extra=context_dict)
823
+
824
+
825
+ def format_timestamp(
826
+ dt: datetime | None = None,
827
+ format: Literal["iso8601", "iso8601_local", "unix"] = "iso8601",
828
+ ) -> str:
829
+ """Format a timestamp according to LOG-005 requirements.
830
+
831
+ Args:
832
+ dt: Datetime to format, or None for current time.
833
+ format: Format to use (iso8601, iso8601_local, unix).
834
+
835
+ Returns:
836
+ Formatted timestamp string.
837
+
838
+ Raises:
839
+ ValueError: If format is unknown.
840
+
841
+ Example:
842
+ >>> ts = format_timestamp()
843
+ >>> print(ts) # 2025-12-20T15:30:45.123456Z
844
+
845
+ References:
846
+ LOG-005: ISO 8601 Timestamps
847
+ """
848
+ if dt is None:
849
+ dt = datetime.now(UTC)
850
+
851
+ if format == "iso8601":
852
+ return dt.strftime("%Y-%m-%dT%H:%M:%S.%f") + "Z"
853
+ elif format == "iso8601_local":
854
+ dt_local = dt.astimezone()
855
+ return dt_local.strftime("%Y-%m-%dT%H:%M:%S.%f")
856
+ elif format == "unix":
857
+ return str(dt.timestamp())
858
+ else:
859
+ raise ValueError(f"Unknown timestamp format: {format}")
860
+
861
+
862
+ # Initialize logging on module import (with defaults)
863
+ def _init_logging() -> None:
864
+ """Initialize logging with environment variable configuration.
865
+
866
+ Reads:
867
+ TRACEKIT_LOG_LEVEL: Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
868
+ TRACEKIT_LOG_FORMAT: Log format (json, logfmt, text)
869
+
870
+ References:
871
+ LOG-001: Structured Logging Framework
872
+ LOG-002: Hierarchical Log Levels
873
+ """
874
+ level = os.environ.get("TRACEKIT_LOG_LEVEL", "WARNING")
875
+ log_format = os.environ.get("TRACEKIT_LOG_FORMAT", "text")
876
+
877
+ if log_format in ("json", "logfmt", "text"):
878
+ configure_logging(level=level, format=log_format) # type: ignore[arg-type]
879
+ else:
880
+ configure_logging(level=level)
881
+
882
+
883
+ # Auto-initialize on import
884
+ _init_logging()
885
+
886
+ # Re-export correlation and performance functions for convenience
887
+ # These provide LOG-004 and LOG-006 functionality through this module
888
+ from oscura.core.correlation import ( # noqa: E402
889
+ CorrelationContext,
890
+ generate_correlation_id,
891
+ get_correlation_id,
892
+ set_correlation_id,
893
+ with_correlation_id,
894
+ )
895
+ from oscura.core.performance import ( # noqa: E402
896
+ PerformanceContext,
897
+ PerformanceRecord,
898
+ clear_performance_data,
899
+ get_performance_records,
900
+ get_performance_summary,
901
+ timed,
902
+ )
903
+
904
+ __all__ = [
905
+ "CompressingRotatingFileHandler",
906
+ "CompressingTimedRotatingFileHandler",
907
+ "CorrelationContext",
908
+ # Error handling (LOG-008)
909
+ "ErrorContextCapture",
910
+ "LogConfig",
911
+ "PerformanceContext",
912
+ "PerformanceRecord",
913
+ "StructuredFormatter",
914
+ "clear_performance_data",
915
+ # Logging configuration (LOG-001, LOG-002, LOG-003)
916
+ "configure_logging",
917
+ # Timestamps (LOG-005)
918
+ "format_timestamp",
919
+ "generate_correlation_id",
920
+ # Correlation ID (LOG-004)
921
+ "get_correlation_id",
922
+ "get_logger",
923
+ "get_performance_records",
924
+ "get_performance_summary",
925
+ "log_exception",
926
+ "set_correlation_id",
927
+ "set_log_level",
928
+ # Performance timing (LOG-006)
929
+ "timed",
930
+ "with_correlation_id",
931
+ ]