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
@@ -0,0 +1,1234 @@
1
+ """Advanced reporting features for TraceKit.
2
+
3
+ This module provides advanced reporting capabilities including interactive
4
+ reports, scheduled generation, distribution, versioning, and compliance.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import hashlib
10
+ import json
11
+ import logging
12
+ import re
13
+ import uuid
14
+ from dataclasses import dataclass, field
15
+ from datetime import datetime, timedelta
16
+ from enum import Enum, auto
17
+ from typing import TYPE_CHECKING, Any
18
+
19
+ if TYPE_CHECKING:
20
+ from collections.abc import Callable
21
+ from pathlib import Path
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ # =============================================================================
27
+ # =============================================================================
28
+
29
+
30
+ @dataclass
31
+ class TemplateField:
32
+ """Customizable template field.
33
+
34
+ Attributes:
35
+ name: Field identifier
36
+ type: Field type (text, number, image, table, chart)
37
+ default: Default value
38
+ required: Whether field is required
39
+ validation: Validation rule (regex pattern)
40
+ """
41
+
42
+ name: str
43
+ type: str = "text"
44
+ default: Any = None
45
+ required: bool = False
46
+ validation: str | None = None
47
+ description: str = ""
48
+
49
+
50
+ @dataclass
51
+ class CustomTemplate:
52
+ """Customizable report template.
53
+
54
+ Attributes:
55
+ name: Template name
56
+ version: Template version
57
+ fields: List of customizable fields
58
+ layout: Layout configuration
59
+ styles: CSS/style overrides
60
+ includes: Included partial templates
61
+
62
+ Example:
63
+ >>> template = CustomTemplate(
64
+ ... name="compliance_report",
65
+ ... fields=[
66
+ ... TemplateField("company_name", required=True),
67
+ ... TemplateField("logo", type="image")
68
+ ... ]
69
+ ... )
70
+
71
+ References:
72
+ REPORT-011: Report Customization Templates
73
+ """
74
+
75
+ name: str
76
+ version: str = "1.0.0"
77
+ fields: list[TemplateField] = field(default_factory=list)
78
+ layout: dict[str, Any] = field(default_factory=dict)
79
+ styles: dict[str, str] = field(default_factory=dict)
80
+ includes: list[str] = field(default_factory=list)
81
+ description: str = ""
82
+
83
+ def validate_data(self, data: dict[str, Any]) -> tuple[bool, list[str]]:
84
+ """Validate data against template fields.
85
+
86
+ Args:
87
+ data: Data dictionary
88
+
89
+ Returns:
90
+ Tuple of (is_valid, list of errors)
91
+ """
92
+ errors = []
93
+ for f in self.fields:
94
+ if f.required and f.name not in data:
95
+ errors.append(f"Required field '{f.name}' missing")
96
+ elif f.name in data and f.validation:
97
+ if not re.match(f.validation, str(data[f.name])):
98
+ errors.append(f"Field '{f.name}' failed validation")
99
+ return len(errors) == 0, errors
100
+
101
+ def render(self, data: dict[str, Any]) -> str:
102
+ """Render template with data.
103
+
104
+ Args:
105
+ data: Data dictionary
106
+
107
+ Returns:
108
+ Rendered content
109
+ """
110
+ # Simple placeholder substitution
111
+ content = self.layout.get("template", "")
112
+ for f in self.fields:
113
+ placeholder = f"{{{{{f.name}}}}}"
114
+ value = data.get(f.name, f.default or "")
115
+ content = content.replace(placeholder, str(value))
116
+ return content # type: ignore[no-any-return]
117
+
118
+
119
+ # =============================================================================
120
+ # =============================================================================
121
+
122
+
123
+ class InteractiveElementType(Enum):
124
+ """Types of interactive elements."""
125
+
126
+ ZOOMABLE_CHART = auto()
127
+ COLLAPSIBLE_SECTION = auto()
128
+ FILTER_DROPDOWN = auto()
129
+ SORTABLE_TABLE = auto()
130
+ TOOLTIP = auto()
131
+ DRILL_DOWN = auto()
132
+ TOGGLE = auto()
133
+
134
+
135
+ @dataclass
136
+ class InteractiveElement:
137
+ """Interactive element for HTML reports.
138
+
139
+ Attributes:
140
+ id: Element ID
141
+ type: Element type
142
+ data: Element data
143
+ options: Configuration options
144
+ script: JavaScript code for interactivity
145
+
146
+ Example:
147
+ >>> element = InteractiveElement(
148
+ ... id="chart1",
149
+ ... type=InteractiveElementType.ZOOMABLE_CHART,
150
+ ... data=chart_data
151
+ ... )
152
+
153
+ References:
154
+ REPORT-012: Interactive Report Elements
155
+ """
156
+
157
+ id: str
158
+ type: InteractiveElementType
159
+ data: Any = None
160
+ options: dict[str, Any] = field(default_factory=dict)
161
+ script: str = ""
162
+
163
+ def to_html(self) -> str:
164
+ """Generate HTML for interactive element."""
165
+ html_parts = [f'<div id="{self.id}" class="interactive-{self.type.name.lower()}">']
166
+
167
+ if self.type == InteractiveElementType.COLLAPSIBLE_SECTION:
168
+ html_parts.append(f"""
169
+ <button class="collapsible" onclick="toggleSection('{self.id}')">
170
+ {self.options.get("title", "Section")}
171
+ </button>
172
+ <div class="content" style="display:none;">
173
+ {self.data or ""}
174
+ </div>
175
+ """)
176
+ elif self.type == InteractiveElementType.SORTABLE_TABLE:
177
+ html_parts.append(f"""
178
+ <table class="sortable" data-sort-enabled="true">
179
+ {self.data or ""}
180
+ </table>
181
+ """)
182
+ elif self.type == InteractiveElementType.TOOLTIP:
183
+ html_parts.append(f'''
184
+ <span class="tooltip" data-tooltip="{self.options.get("text", "")}">
185
+ {self.data or ""}
186
+ </span>
187
+ ''')
188
+ else:
189
+ html_parts.append(str(self.data or ""))
190
+
191
+ html_parts.append("</div>")
192
+ return "\n".join(html_parts)
193
+
194
+
195
+ # =============================================================================
196
+ # =============================================================================
197
+
198
+
199
+ @dataclass
200
+ class Annotation:
201
+ """Report annotation.
202
+
203
+ Attributes:
204
+ id: Unique annotation ID
205
+ target: Target element ID or location
206
+ text: Annotation text
207
+ author: Author name
208
+ created: Creation timestamp
209
+ type: Annotation type (note, warning, highlight, etc.)
210
+ position: Position info for placement
211
+
212
+ References:
213
+ REPORT-013: Report Annotations
214
+ """
215
+
216
+ id: str
217
+ target: str
218
+ text: str
219
+ author: str = ""
220
+ created: datetime = field(default_factory=datetime.now)
221
+ type: str = "note"
222
+ position: dict[str, float] = field(default_factory=dict)
223
+
224
+ def to_dict(self) -> dict[str, Any]:
225
+ """Convert to dictionary."""
226
+ return {
227
+ "id": self.id,
228
+ "target": self.target,
229
+ "text": self.text,
230
+ "author": self.author,
231
+ "created": self.created.isoformat(),
232
+ "type": self.type,
233
+ "position": self.position,
234
+ }
235
+
236
+
237
+ class AnnotationManager:
238
+ """Manager for report annotations.
239
+
240
+ References:
241
+ REPORT-013: Report Annotations
242
+ """
243
+
244
+ def __init__(self, report_id: str):
245
+ self.report_id = report_id
246
+ self._annotations: list[Annotation] = []
247
+
248
+ def add(self, target: str, text: str, author: str = "", type_: str = "note") -> Annotation:
249
+ """Add annotation."""
250
+ annotation = Annotation(
251
+ id=str(uuid.uuid4()), target=target, text=text, author=author, type=type_
252
+ )
253
+ self._annotations.append(annotation)
254
+ return annotation
255
+
256
+ def remove(self, annotation_id: str) -> bool:
257
+ """Remove annotation."""
258
+ for i, ann in enumerate(self._annotations):
259
+ if ann.id == annotation_id:
260
+ del self._annotations[i]
261
+ return True
262
+ return False
263
+
264
+ def get_for_target(self, target: str) -> list[Annotation]:
265
+ """Get annotations for target."""
266
+ return [a for a in self._annotations if a.target == target]
267
+
268
+ def export(self) -> list[dict[str, Any]]:
269
+ """Export all annotations."""
270
+ return [a.to_dict() for a in self._annotations]
271
+
272
+
273
+ # =============================================================================
274
+ # =============================================================================
275
+
276
+
277
+ class ScheduleFrequency(Enum):
278
+ """Report schedule frequency."""
279
+
280
+ ONCE = auto()
281
+ HOURLY = auto()
282
+ DAILY = auto()
283
+ WEEKLY = auto()
284
+ MONTHLY = auto()
285
+ CUSTOM = auto()
286
+
287
+
288
+ @dataclass
289
+ class ReportSchedule:
290
+ """Scheduled report configuration.
291
+
292
+ Attributes:
293
+ id: Schedule ID
294
+ report_config: Report configuration
295
+ frequency: Generation frequency
296
+ next_run: Next scheduled run time
297
+ enabled: Whether schedule is active
298
+ recipients: Email recipients
299
+ cron_expression: Cron expression for custom schedules
300
+
301
+ References:
302
+ REPORT-017: Report Scheduling
303
+ """
304
+
305
+ id: str
306
+ report_config: dict[str, Any]
307
+ frequency: ScheduleFrequency = ScheduleFrequency.DAILY
308
+ next_run: datetime = field(default_factory=datetime.now)
309
+ enabled: bool = True
310
+ recipients: list[str] = field(default_factory=list)
311
+ cron_expression: str | None = None
312
+
313
+ def calculate_next_run(self) -> datetime:
314
+ """Calculate next run time."""
315
+ now = datetime.now()
316
+ if self.frequency == ScheduleFrequency.HOURLY:
317
+ return now + timedelta(hours=1)
318
+ elif self.frequency == ScheduleFrequency.DAILY:
319
+ return now + timedelta(days=1)
320
+ elif self.frequency == ScheduleFrequency.WEEKLY:
321
+ return now + timedelta(weeks=1)
322
+ elif self.frequency == ScheduleFrequency.MONTHLY:
323
+ return now + timedelta(days=30)
324
+ return now
325
+
326
+
327
+ class ReportScheduler:
328
+ """Report scheduler for automated generation.
329
+
330
+ References:
331
+ REPORT-017: Report Scheduling
332
+ """
333
+
334
+ def __init__(self): # type: ignore[no-untyped-def]
335
+ self._schedules: dict[str, ReportSchedule] = {}
336
+ self._running = False
337
+
338
+ def add_schedule(
339
+ self,
340
+ report_config: dict[str, Any],
341
+ frequency: ScheduleFrequency,
342
+ recipients: list[str] | None = None,
343
+ ) -> str:
344
+ """Add new schedule."""
345
+ schedule = ReportSchedule(
346
+ id=str(uuid.uuid4()),
347
+ report_config=report_config,
348
+ frequency=frequency,
349
+ recipients=recipients or [],
350
+ )
351
+ self._schedules[schedule.id] = schedule
352
+ return schedule.id
353
+
354
+ def remove_schedule(self, schedule_id: str) -> bool:
355
+ """Remove schedule."""
356
+ if schedule_id in self._schedules:
357
+ del self._schedules[schedule_id]
358
+ return True
359
+ return False
360
+
361
+ def get_pending(self) -> list[ReportSchedule]:
362
+ """Get schedules due for execution."""
363
+ now = datetime.now()
364
+ return [s for s in self._schedules.values() if s.enabled and s.next_run <= now]
365
+
366
+ def execute_pending(self, generator: Callable[[dict[str, Any]], Any]) -> list[str]:
367
+ """Execute pending schedules."""
368
+ executed = []
369
+ for schedule in self.get_pending():
370
+ try:
371
+ generator(schedule.report_config)
372
+ schedule.next_run = schedule.calculate_next_run()
373
+ executed.append(schedule.id)
374
+ except Exception as e:
375
+ logger.error(f"Scheduled report failed: {e}")
376
+ return executed
377
+
378
+
379
+ # =============================================================================
380
+ # =============================================================================
381
+
382
+
383
+ class DistributionChannel(Enum):
384
+ """Distribution channels."""
385
+
386
+ EMAIL = auto()
387
+ FILE_SHARE = auto()
388
+ WEBHOOK = auto()
389
+ S3 = auto()
390
+ SFTP = auto()
391
+
392
+
393
+ @dataclass
394
+ class DistributionConfig:
395
+ """Distribution configuration.
396
+
397
+ References:
398
+ REPORT-020: Report Distribution
399
+ """
400
+
401
+ channel: DistributionChannel
402
+ recipients: list[str] = field(default_factory=list)
403
+ settings: dict[str, Any] = field(default_factory=dict)
404
+
405
+
406
+ class ReportDistributor:
407
+ """Distributes reports to configured channels.
408
+
409
+ References:
410
+ REPORT-020: Report Distribution
411
+ """
412
+
413
+ def __init__(self): # type: ignore[no-untyped-def]
414
+ self._handlers: dict[DistributionChannel, Callable] = {} # type: ignore[type-arg]
415
+
416
+ def register_handler(
417
+ self,
418
+ channel: DistributionChannel,
419
+ handler: Callable[[Path, DistributionConfig], bool],
420
+ ) -> None:
421
+ """Register distribution handler."""
422
+ self._handlers[channel] = handler
423
+
424
+ def distribute(self, report_path: Path, configs: list[DistributionConfig]) -> dict[str, bool]:
425
+ """Distribute report to all configured channels."""
426
+ results = {}
427
+ for config in configs:
428
+ handler = self._handlers.get(config.channel)
429
+ if handler:
430
+ try:
431
+ results[config.channel.name] = handler(report_path, config)
432
+ except Exception as e:
433
+ logger.error(f"Distribution failed for {config.channel}: {e}")
434
+ results[config.channel.name] = False
435
+ else:
436
+ logger.warning(f"No handler for channel: {config.channel}")
437
+ results[config.channel.name] = False
438
+ return results
439
+
440
+
441
+ # =============================================================================
442
+ # =============================================================================
443
+
444
+
445
+ @dataclass
446
+ class ArchivedReport:
447
+ """Archived report metadata.
448
+
449
+ References:
450
+ REPORT-021: Report Archiving
451
+ """
452
+
453
+ id: str
454
+ name: str
455
+ path: Path
456
+ created: datetime
457
+ size: int
458
+ checksum: str
459
+ metadata: dict[str, Any] = field(default_factory=dict)
460
+ retention_days: int = 365
461
+
462
+
463
+ class ReportArchive:
464
+ """Report archiving system.
465
+
466
+ References:
467
+ REPORT-021: Report Archiving
468
+ """
469
+
470
+ def __init__(self, archive_dir: Path):
471
+ self.archive_dir = archive_dir
472
+ archive_dir.mkdir(parents=True, exist_ok=True)
473
+ self._index: dict[str, ArchivedReport] = {}
474
+
475
+ def archive(self, report_path: Path, metadata: dict[str, Any] | None = None) -> str:
476
+ """Archive a report."""
477
+ report_id = str(uuid.uuid4())
478
+
479
+ # Calculate checksum
480
+ with open(report_path, "rb") as f:
481
+ checksum = hashlib.sha256(f.read()).hexdigest()
482
+
483
+ # Copy to archive
484
+ archive_path = self.archive_dir / f"{report_id}_{report_path.name}"
485
+ import shutil
486
+
487
+ shutil.copy2(report_path, archive_path)
488
+
489
+ archived = ArchivedReport(
490
+ id=report_id,
491
+ name=report_path.name,
492
+ path=archive_path,
493
+ created=datetime.now(),
494
+ size=archive_path.stat().st_size,
495
+ checksum=checksum,
496
+ metadata=metadata or {},
497
+ )
498
+ self._index[report_id] = archived
499
+
500
+ logger.info(f"Archived report: {report_id}")
501
+ return report_id
502
+
503
+ def retrieve(self, report_id: str) -> Path | None:
504
+ """Retrieve archived report."""
505
+ if report_id in self._index:
506
+ return self._index[report_id].path
507
+ return None
508
+
509
+ def cleanup_expired(self) -> int:
510
+ """Remove expired archives."""
511
+ now = datetime.now()
512
+ removed = 0
513
+ for report_id, archived in list(self._index.items()):
514
+ age = (now - archived.created).days
515
+ if age > archived.retention_days:
516
+ archived.path.unlink(missing_ok=True)
517
+ del self._index[report_id]
518
+ removed += 1
519
+ return removed
520
+
521
+
522
+ # =============================================================================
523
+ # =============================================================================
524
+
525
+
526
+ @dataclass
527
+ class SearchResult:
528
+ """Report search result.
529
+
530
+ References:
531
+ REPORT-022: Report Search
532
+ """
533
+
534
+ report_id: str
535
+ name: str
536
+ score: float
537
+ highlights: list[str] = field(default_factory=list)
538
+ metadata: dict[str, Any] = field(default_factory=dict)
539
+
540
+
541
+ class ReportSearchIndex:
542
+ """Full-text search index for reports.
543
+
544
+ References:
545
+ REPORT-022: Report Search
546
+ """
547
+
548
+ def __init__(self): # type: ignore[no-untyped-def]
549
+ self._index: dict[str, dict[str, Any]] = {}
550
+
551
+ def index_report(self, report_id: str, content: str, metadata: dict[str, Any]) -> None:
552
+ """Add report to search index."""
553
+ # Simple word-based indexing
554
+ words = set(content.lower().split())
555
+ self._index[report_id] = {
556
+ "words": words,
557
+ "content": content,
558
+ "metadata": metadata,
559
+ }
560
+
561
+ def search(self, query: str, limit: int = 10) -> list[SearchResult]:
562
+ """Search for reports."""
563
+ query_words = set(query.lower().split())
564
+ results = []
565
+
566
+ for report_id, doc in self._index.items():
567
+ # Simple scoring: intersection of words
568
+ matches = query_words & doc["words"]
569
+ if matches:
570
+ score = len(matches) / len(query_words)
571
+ results.append(
572
+ SearchResult(
573
+ report_id=report_id,
574
+ name=doc["metadata"].get("name", report_id),
575
+ score=score,
576
+ highlights=[f"...{m}..." for m in matches],
577
+ metadata=doc["metadata"],
578
+ )
579
+ )
580
+
581
+ # Sort by score
582
+ results.sort(key=lambda r: r.score, reverse=True)
583
+ return results[:limit]
584
+
585
+
586
+ # =============================================================================
587
+ # =============================================================================
588
+
589
+
590
+ @dataclass
591
+ class ReportVersion:
592
+ """Report version entry.
593
+
594
+ References:
595
+ REPORT-023: Report Versioning
596
+ """
597
+
598
+ version: int
599
+ created: datetime
600
+ author: str
601
+ changes: str
602
+ checksum: str
603
+ path: Path
604
+
605
+
606
+ class ReportVersionControl:
607
+ """Version control for reports.
608
+
609
+ References:
610
+ REPORT-023: Report Versioning
611
+ """
612
+
613
+ def __init__(self, storage_dir: Path):
614
+ self.storage_dir = storage_dir
615
+ storage_dir.mkdir(parents=True, exist_ok=True)
616
+ self._versions: dict[str, list[ReportVersion]] = {}
617
+
618
+ def commit(self, report_id: str, report_path: Path, author: str, changes: str) -> int:
619
+ """Commit new version of report."""
620
+ if report_id not in self._versions:
621
+ self._versions[report_id] = []
622
+
623
+ version = len(self._versions[report_id]) + 1
624
+
625
+ # Copy to versioned storage
626
+ version_path = self.storage_dir / f"{report_id}_v{version}{report_path.suffix}"
627
+ import shutil
628
+
629
+ shutil.copy2(report_path, version_path)
630
+
631
+ # Calculate checksum
632
+ with open(version_path, "rb") as f:
633
+ checksum = hashlib.sha256(f.read()).hexdigest()
634
+
635
+ entry = ReportVersion(
636
+ version=version,
637
+ created=datetime.now(),
638
+ author=author,
639
+ changes=changes,
640
+ checksum=checksum,
641
+ path=version_path,
642
+ )
643
+ self._versions[report_id].append(entry)
644
+
645
+ logger.info(f"Committed {report_id} version {version}")
646
+ return version
647
+
648
+ def get_version(self, report_id: str, version: int) -> Path | None:
649
+ """Get specific version of report."""
650
+ if report_id in self._versions:
651
+ for v in self._versions[report_id]:
652
+ if v.version == version:
653
+ return v.path
654
+ return None
655
+
656
+ def get_history(self, report_id: str) -> list[ReportVersion]:
657
+ """Get version history."""
658
+ return self._versions.get(report_id, [])
659
+
660
+ def diff(self, report_id: str, v1: int, v2: int) -> str:
661
+ """Get diff between versions."""
662
+ path1 = self.get_version(report_id, v1)
663
+ path2 = self.get_version(report_id, v2)
664
+
665
+ if not path1 or not path2:
666
+ return "Version not found"
667
+
668
+ # Simple text diff
669
+ with open(path1) as f1, open(path2) as f2:
670
+ lines1 = f1.readlines()
671
+ lines2 = f2.readlines()
672
+
673
+ import difflib
674
+
675
+ diff = difflib.unified_diff(lines1, lines2, lineterm="")
676
+ return "\n".join(diff)
677
+
678
+
679
+ # =============================================================================
680
+ # =============================================================================
681
+
682
+
683
+ class ApprovalStatus(Enum):
684
+ """Approval status."""
685
+
686
+ DRAFT = auto()
687
+ PENDING_REVIEW = auto()
688
+ APPROVED = auto()
689
+ REJECTED = auto()
690
+ PUBLISHED = auto()
691
+
692
+
693
+ @dataclass
694
+ class ApprovalRecord:
695
+ """Approval workflow record.
696
+
697
+ References:
698
+ REPORT-024: Report Approval Workflow
699
+ """
700
+
701
+ report_id: str
702
+ status: ApprovalStatus = ApprovalStatus.DRAFT
703
+ submitter: str = ""
704
+ reviewer: str | None = None
705
+ submitted_at: datetime | None = None
706
+ reviewed_at: datetime | None = None
707
+ comments: str = ""
708
+
709
+
710
+ class ApprovalWorkflow:
711
+ """Report approval workflow manager.
712
+
713
+ References:
714
+ REPORT-024: Report Approval Workflow
715
+ """
716
+
717
+ def __init__(self): # type: ignore[no-untyped-def]
718
+ self._records: dict[str, ApprovalRecord] = {}
719
+ self._callbacks: dict[ApprovalStatus, list[Callable]] = {} # type: ignore[type-arg]
720
+
721
+ def submit_for_review(self, report_id: str, submitter: str) -> ApprovalRecord:
722
+ """Submit report for review."""
723
+ record = ApprovalRecord(
724
+ report_id=report_id,
725
+ status=ApprovalStatus.PENDING_REVIEW,
726
+ submitter=submitter,
727
+ submitted_at=datetime.now(),
728
+ )
729
+ self._records[report_id] = record
730
+ self._trigger_callbacks(ApprovalStatus.PENDING_REVIEW, record)
731
+ return record
732
+
733
+ def approve(self, report_id: str, reviewer: str, comments: str = "") -> ApprovalRecord:
734
+ """Approve report."""
735
+ record = self._records.get(report_id)
736
+ if not record:
737
+ raise ValueError(f"Report {report_id} not in workflow")
738
+
739
+ record.status = ApprovalStatus.APPROVED
740
+ record.reviewer = reviewer
741
+ record.reviewed_at = datetime.now()
742
+ record.comments = comments
743
+ self._trigger_callbacks(ApprovalStatus.APPROVED, record)
744
+ return record
745
+
746
+ def reject(self, report_id: str, reviewer: str, comments: str) -> ApprovalRecord:
747
+ """Reject report."""
748
+ record = self._records.get(report_id)
749
+ if not record:
750
+ raise ValueError(f"Report {report_id} not in workflow")
751
+
752
+ record.status = ApprovalStatus.REJECTED
753
+ record.reviewer = reviewer
754
+ record.reviewed_at = datetime.now()
755
+ record.comments = comments
756
+ self._trigger_callbacks(ApprovalStatus.REJECTED, record)
757
+ return record
758
+
759
+ def on_status_change(
760
+ self, status: ApprovalStatus, callback: Callable[[ApprovalRecord], None]
761
+ ) -> None:
762
+ """Register callback for status change."""
763
+ if status not in self._callbacks:
764
+ self._callbacks[status] = []
765
+ self._callbacks[status].append(callback)
766
+
767
+ def _trigger_callbacks(self, status: ApprovalStatus, record: ApprovalRecord) -> None:
768
+ """Trigger callbacks for status."""
769
+ for callback in self._callbacks.get(status, []):
770
+ try:
771
+ callback(record)
772
+ except Exception as e:
773
+ logger.warning(f"Approval callback failed: {e}")
774
+
775
+
776
+ # =============================================================================
777
+ # =============================================================================
778
+
779
+
780
+ @dataclass
781
+ class ComplianceRule:
782
+ """Compliance checking rule.
783
+
784
+ References:
785
+ REPORT-025: Report Compliance Checking
786
+ """
787
+
788
+ id: str
789
+ name: str
790
+ description: str
791
+ check: Callable[[dict[str, Any]], bool]
792
+ severity: str = "error" # error, warning, info
793
+
794
+
795
+ @dataclass
796
+ class ComplianceResult:
797
+ """Compliance check result."""
798
+
799
+ passed: bool
800
+ violations: list[tuple[str, str]] = field(default_factory=list)
801
+ warnings: list[tuple[str, str]] = field(default_factory=list)
802
+
803
+
804
+ class ComplianceChecker:
805
+ """Report compliance checker.
806
+
807
+ References:
808
+ REPORT-025: Report Compliance Checking
809
+ """
810
+
811
+ def __init__(self): # type: ignore[no-untyped-def]
812
+ self._rules: list[ComplianceRule] = []
813
+
814
+ def add_rule(
815
+ self,
816
+ name: str,
817
+ check: Callable[[dict[str, Any]], bool],
818
+ description: str = "",
819
+ severity: str = "error",
820
+ ) -> None:
821
+ """Add compliance rule."""
822
+ rule = ComplianceRule(
823
+ id=str(uuid.uuid4()),
824
+ name=name,
825
+ description=description,
826
+ check=check,
827
+ severity=severity,
828
+ )
829
+ self._rules.append(rule)
830
+
831
+ def check(self, report_data: dict[str, Any]) -> ComplianceResult:
832
+ """Check report against all rules."""
833
+ violations = []
834
+ warnings = []
835
+
836
+ for rule in self._rules:
837
+ try:
838
+ if not rule.check(report_data):
839
+ if rule.severity == "error":
840
+ violations.append((rule.name, rule.description))
841
+ else:
842
+ warnings.append((rule.name, rule.description))
843
+ except Exception as e:
844
+ logger.warning(f"Compliance rule {rule.name} failed: {e}")
845
+
846
+ return ComplianceResult(
847
+ passed=len(violations) == 0, violations=violations, warnings=warnings
848
+ )
849
+
850
+
851
+ # =============================================================================
852
+ # =============================================================================
853
+
854
+
855
+ @dataclass
856
+ class LocaleStrings:
857
+ """Localized strings for a locale.
858
+
859
+ References:
860
+ REPORT-026: Report Localization
861
+ """
862
+
863
+ locale: str
864
+ strings: dict[str, str] = field(default_factory=dict)
865
+ date_format: str = "%Y-%m-%d"
866
+ time_format: str = "%H:%M:%S"
867
+ number_decimal: str = "."
868
+ number_thousand: str = ","
869
+
870
+
871
+ class ReportLocalizer:
872
+ """Report localization manager.
873
+
874
+ References:
875
+ REPORT-026: Report Localization
876
+ """
877
+
878
+ def __init__(self, default_locale: str = "en_US"):
879
+ self.default_locale = default_locale
880
+ self._locales: dict[str, LocaleStrings] = {}
881
+ self._register_defaults()
882
+
883
+ def _register_defaults(self) -> None:
884
+ """Register default locales."""
885
+ self._locales["en_US"] = LocaleStrings(
886
+ locale="en_US",
887
+ strings={
888
+ "title": "Report",
889
+ "summary": "Summary",
890
+ "pass": "PASS",
891
+ "fail": "FAIL",
892
+ },
893
+ )
894
+ self._locales["de_DE"] = LocaleStrings(
895
+ locale="de_DE",
896
+ strings={
897
+ "title": "Bericht",
898
+ "summary": "Zusammenfassung",
899
+ "pass": "BESTANDEN",
900
+ "fail": "DURCHGEFALLEN",
901
+ },
902
+ date_format="%d.%m.%Y",
903
+ number_decimal=",",
904
+ number_thousand=".",
905
+ )
906
+
907
+ def get_string(self, key: str, locale: str | None = None) -> str:
908
+ """Get localized string."""
909
+ loc = locale or self.default_locale
910
+ strings = self._locales.get(loc, self._locales[self.default_locale])
911
+ return strings.strings.get(key, key)
912
+
913
+ def format_number(self, value: float, locale: str | None = None) -> str:
914
+ """Format number for locale."""
915
+ loc_strings = self._locales.get(
916
+ locale or self.default_locale, self._locales[self.default_locale]
917
+ )
918
+ formatted = f"{value:,.2f}"
919
+ # Replace separators
920
+ formatted = formatted.replace(",", "TEMP")
921
+ formatted = formatted.replace(".", loc_strings.number_decimal)
922
+ formatted = formatted.replace("TEMP", loc_strings.number_thousand)
923
+ return formatted
924
+
925
+
926
+ # =============================================================================
927
+ # =============================================================================
928
+
929
+
930
+ @dataclass
931
+ class AccessibilityOptions:
932
+ """Accessibility options for reports.
933
+
934
+ References:
935
+ REPORT-027: Report Accessibility
936
+ """
937
+
938
+ alt_text_required: bool = True
939
+ high_contrast: bool = False
940
+ screen_reader_friendly: bool = True
941
+ keyboard_navigable: bool = True
942
+ wcag_level: str = "AA" # A, AA, AAA
943
+
944
+
945
+ def add_accessibility_features(html_content: str, options: AccessibilityOptions) -> str:
946
+ """Add accessibility features to HTML report.
947
+
948
+ Args:
949
+ html_content: HTML content
950
+ options: Accessibility options
951
+
952
+ Returns:
953
+ Enhanced HTML content
954
+
955
+ References:
956
+ REPORT-027: Report Accessibility
957
+ """
958
+ # Add ARIA landmarks
959
+ html_content = html_content.replace(
960
+ '<div class="report">',
961
+ '<div class="report" role="main" aria-label="Report Content">',
962
+ )
963
+
964
+ # Add skip navigation link
965
+ skip_nav = '<a href="#main-content" class="skip-link">Skip to main content</a>'
966
+ html_content = html_content.replace("<body>", f"<body>{skip_nav}")
967
+
968
+ # Add high contrast styles if enabled
969
+ if options.high_contrast:
970
+ contrast_styles = """
971
+ <style>
972
+ body { background: white !important; color: black !important; }
973
+ a { color: blue !important; }
974
+ .pass { background: green !important; color: white !important; }
975
+ .fail { background: red !important; color: white !important; }
976
+ </style>
977
+ """
978
+ html_content = html_content.replace("</head>", f"{contrast_styles}</head>")
979
+
980
+ return html_content
981
+
982
+
983
+ # =============================================================================
984
+ # =============================================================================
985
+
986
+
987
+ class ReportEncryption:
988
+ """Report encryption utilities.
989
+
990
+ References:
991
+ REPORT-028: Report Encryption
992
+ """
993
+
994
+ @staticmethod
995
+ def encrypt_content(content: bytes, password: str) -> bytes:
996
+ """Encrypt report content.
997
+
998
+ Args:
999
+ content: Content bytes to encrypt.
1000
+ password: Encryption password.
1001
+
1002
+ Returns:
1003
+ Encrypted content bytes.
1004
+
1005
+ Note:
1006
+ Uses simple XOR encryption for demonstration.
1007
+ In production, use proper encryption (AES, etc.).
1008
+ """
1009
+ key = hashlib.sha256(password.encode()).digest()
1010
+ encrypted = bytearray()
1011
+ for i, byte in enumerate(content):
1012
+ encrypted.append(byte ^ key[i % len(key)])
1013
+ return bytes(encrypted)
1014
+
1015
+ @staticmethod
1016
+ def decrypt_content(encrypted: bytes, password: str) -> bytes:
1017
+ """Decrypt report content."""
1018
+ # XOR is symmetric
1019
+ return ReportEncryption.encrypt_content(encrypted, password)
1020
+
1021
+ @staticmethod
1022
+ def encrypt_file(input_path: Path, output_path: Path, password: str) -> None:
1023
+ """Encrypt report file."""
1024
+ with open(input_path, "rb") as f:
1025
+ content = f.read()
1026
+ encrypted = ReportEncryption.encrypt_content(content, password)
1027
+ with open(output_path, "wb") as f:
1028
+ f.write(encrypted)
1029
+
1030
+ @staticmethod
1031
+ def decrypt_file(input_path: Path, output_path: Path, password: str) -> None:
1032
+ """Decrypt report file."""
1033
+ with open(input_path, "rb") as f:
1034
+ encrypted = f.read()
1035
+ decrypted = ReportEncryption.decrypt_content(encrypted, password)
1036
+ with open(output_path, "wb") as f:
1037
+ f.write(decrypted)
1038
+
1039
+
1040
+ # =============================================================================
1041
+ # =============================================================================
1042
+
1043
+
1044
+ @dataclass
1045
+ class Watermark:
1046
+ """Report watermark configuration.
1047
+
1048
+ References:
1049
+ REPORT-029: Report Watermarking
1050
+ """
1051
+
1052
+ text: str = "CONFIDENTIAL"
1053
+ opacity: float = 0.1
1054
+ rotation: int = -45
1055
+ position: str = "center" # center, header, footer
1056
+ font_size: int = 48
1057
+
1058
+
1059
+ def add_watermark(html_content: str, watermark: Watermark) -> str:
1060
+ """Add watermark to HTML report.
1061
+
1062
+ Args:
1063
+ html_content: HTML content
1064
+ watermark: Watermark configuration
1065
+
1066
+ Returns:
1067
+ HTML with watermark
1068
+
1069
+ References:
1070
+ REPORT-029: Report Watermarking
1071
+ """
1072
+ watermark_css = f"""
1073
+ <style>
1074
+ .watermark {{
1075
+ position: fixed;
1076
+ top: 50%;
1077
+ left: 50%;
1078
+ transform: translate(-50%, -50%) rotate({watermark.rotation}deg);
1079
+ font-size: {watermark.font_size}px;
1080
+ color: rgba(128, 128, 128, {watermark.opacity});
1081
+ pointer-events: none;
1082
+ z-index: 1000;
1083
+ white-space: nowrap;
1084
+ }}
1085
+ </style>
1086
+ """
1087
+ watermark_div = f'<div class="watermark">{watermark.text}</div>'
1088
+
1089
+ html_content = html_content.replace("</head>", f"{watermark_css}</head>")
1090
+ html_content = html_content.replace("<body>", f"<body>{watermark_div}")
1091
+
1092
+ return html_content
1093
+
1094
+
1095
+ # =============================================================================
1096
+ # =============================================================================
1097
+
1098
+
1099
+ @dataclass
1100
+ class AuditEntry:
1101
+ """Audit trail entry.
1102
+
1103
+ References:
1104
+ REPORT-030: Report Audit Trail
1105
+ """
1106
+
1107
+ id: str
1108
+ report_id: str
1109
+ action: str
1110
+ user: str
1111
+ timestamp: datetime
1112
+ details: dict[str, Any] = field(default_factory=dict)
1113
+ ip_address: str = ""
1114
+
1115
+
1116
+ class AuditTrail:
1117
+ """Report audit trail manager.
1118
+
1119
+ References:
1120
+ REPORT-030: Report Audit Trail
1121
+ """
1122
+
1123
+ def __init__(self, storage_path: Path | None = None):
1124
+ self.storage_path = storage_path
1125
+ self._entries: list[AuditEntry] = []
1126
+
1127
+ def log(
1128
+ self,
1129
+ report_id: str,
1130
+ action: str,
1131
+ user: str,
1132
+ details: dict[str, Any] | None = None,
1133
+ ) -> AuditEntry:
1134
+ """Log audit entry."""
1135
+ entry = AuditEntry(
1136
+ id=str(uuid.uuid4()),
1137
+ report_id=report_id,
1138
+ action=action,
1139
+ user=user,
1140
+ timestamp=datetime.now(),
1141
+ details=details or {},
1142
+ )
1143
+ self._entries.append(entry)
1144
+
1145
+ # Persist if storage configured
1146
+ if self.storage_path:
1147
+ self._persist()
1148
+
1149
+ return entry
1150
+
1151
+ def get_for_report(self, report_id: str) -> list[AuditEntry]:
1152
+ """Get audit entries for report."""
1153
+ return [e for e in self._entries if e.report_id == report_id]
1154
+
1155
+ def get_by_user(self, user: str) -> list[AuditEntry]:
1156
+ """Get audit entries by user."""
1157
+ return [e for e in self._entries if e.user == user]
1158
+
1159
+ def export(self, format_: str = "json") -> str:
1160
+ """Export audit trail."""
1161
+ if format_ == "json":
1162
+ return json.dumps(
1163
+ [
1164
+ {
1165
+ "id": e.id,
1166
+ "report_id": e.report_id,
1167
+ "action": e.action,
1168
+ "user": e.user,
1169
+ "timestamp": e.timestamp.isoformat(),
1170
+ "details": e.details,
1171
+ }
1172
+ for e in self._entries
1173
+ ],
1174
+ indent=2,
1175
+ )
1176
+ return ""
1177
+
1178
+ def _persist(self) -> None:
1179
+ """Persist audit trail to storage."""
1180
+ if self.storage_path:
1181
+ with open(self.storage_path, "w") as f:
1182
+ f.write(self.export("json"))
1183
+
1184
+
1185
+ __all__ = [
1186
+ # Accessibility (REPORT-027)
1187
+ "AccessibilityOptions",
1188
+ # Annotations (REPORT-013)
1189
+ "Annotation",
1190
+ "AnnotationManager",
1191
+ # Approval (REPORT-024)
1192
+ "ApprovalRecord",
1193
+ "ApprovalStatus",
1194
+ "ApprovalWorkflow",
1195
+ # Archiving (REPORT-021)
1196
+ "ArchivedReport",
1197
+ # Audit Trail (REPORT-030)
1198
+ "AuditEntry",
1199
+ "AuditTrail",
1200
+ # Compliance (REPORT-025)
1201
+ "ComplianceChecker",
1202
+ "ComplianceResult",
1203
+ "ComplianceRule",
1204
+ # Templates (REPORT-011)
1205
+ "CustomTemplate",
1206
+ # Distribution (REPORT-020)
1207
+ "DistributionChannel",
1208
+ "DistributionConfig",
1209
+ # Interactive (REPORT-012)
1210
+ "InteractiveElement",
1211
+ "InteractiveElementType",
1212
+ # Localization (REPORT-026)
1213
+ "LocaleStrings",
1214
+ "ReportArchive",
1215
+ "ReportDistributor",
1216
+ # Encryption (REPORT-028)
1217
+ "ReportEncryption",
1218
+ "ReportLocalizer",
1219
+ # Scheduling (REPORT-017)
1220
+ "ReportSchedule",
1221
+ "ReportScheduler",
1222
+ # Search (REPORT-022)
1223
+ "ReportSearchIndex",
1224
+ # Versioning (REPORT-023)
1225
+ "ReportVersion",
1226
+ "ReportVersionControl",
1227
+ "ScheduleFrequency",
1228
+ "SearchResult",
1229
+ "TemplateField",
1230
+ # Watermarking (REPORT-029)
1231
+ "Watermark",
1232
+ "add_accessibility_features",
1233
+ "add_watermark",
1234
+ ]