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,959 @@
1
+ """Plugin lifecycle management and dependency resolution.
2
+
3
+ This module provides advanced plugin lifecycle management including
4
+ dependency resolution, graceful enable/disable, lazy loading, and
5
+ hot reload capabilities.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import importlib
11
+ import importlib.util
12
+ import logging
13
+ import sys
14
+ import threading
15
+ from dataclasses import dataclass, field
16
+ from enum import Enum, auto
17
+ from pathlib import Path
18
+ from typing import TYPE_CHECKING, Any
19
+
20
+ if TYPE_CHECKING:
21
+ from collections.abc import Callable
22
+
23
+ from oscura.plugins.base import PluginBase, PluginMetadata
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ class PluginState(Enum):
29
+ """Plugin lifecycle states."""
30
+
31
+ DISCOVERED = auto() # Found but not loaded
32
+ LOADING = auto() # Currently loading
33
+ LOADED = auto() # Loaded but not configured
34
+ CONFIGURED = auto() # Configured and ready
35
+ ENABLED = auto() # Fully enabled
36
+ DISABLED = auto() # Disabled by user
37
+ ERROR = auto() # Load/configure error
38
+ UNLOADING = auto() # Currently unloading
39
+
40
+
41
+ @dataclass
42
+ class PluginLoadError:
43
+ """Plugin load error details.
44
+
45
+ Attributes:
46
+ plugin_name: Name of plugin that failed
47
+ error: Exception that occurred
48
+ traceback: Traceback string
49
+ stage: Stage where failure occurred
50
+ recoverable: Whether error is recoverable
51
+ """
52
+
53
+ plugin_name: str
54
+ error: Exception
55
+ traceback: str = ""
56
+ stage: str = "load" # discovery, load, configure, enable
57
+ recoverable: bool = True
58
+
59
+
60
+ @dataclass
61
+ class DependencyInfo:
62
+ """Plugin dependency information.
63
+
64
+ Attributes:
65
+ name: Dependency plugin name
66
+ version_spec: Version specification (semver)
67
+ optional: Whether dependency is optional
68
+ resolved: Whether dependency has been resolved
69
+ """
70
+
71
+ name: str
72
+ version_spec: str = "*"
73
+ optional: bool = False
74
+ resolved: bool = False
75
+
76
+
77
+ @dataclass
78
+ class PluginHandle:
79
+ """Handle for managing a plugin instance.
80
+
81
+ Attributes:
82
+ metadata: Plugin metadata
83
+ instance: Plugin instance (None if not loaded)
84
+ state: Current lifecycle state
85
+ dependencies: Plugin dependencies
86
+ dependents: Plugins that depend on this one
87
+ errors: List of errors encountered
88
+ load_time: Time taken to load (seconds)
89
+ """
90
+
91
+ metadata: PluginMetadata
92
+ instance: PluginBase | None = None
93
+ state: PluginState = PluginState.DISCOVERED
94
+ dependencies: list[DependencyInfo] = field(default_factory=list)
95
+ dependents: list[str] = field(default_factory=list)
96
+ errors: list[PluginLoadError] = field(default_factory=list)
97
+ load_time: float = 0.0
98
+
99
+
100
+ class DependencyGraph:
101
+ """Dependency resolution graph for plugins.
102
+
103
+ Resolves plugin dependencies using topological sort to ensure
104
+ correct load order and detect cycles.
105
+
106
+ Example:
107
+ >>> graph = DependencyGraph()
108
+ >>> graph.add_plugin("core")
109
+ >>> graph.add_dependency("decoder", "core", ">=1.0.0")
110
+ >>> order = graph.resolve_order()
111
+ >>> print(order) # ['core', 'decoder']
112
+
113
+ References:
114
+ PLUG-005: Dependency Resolution
115
+ """
116
+
117
+ def __init__(self) -> None:
118
+ """Initialize empty dependency graph."""
119
+ self._nodes: dict[str, list[DependencyInfo]] = {}
120
+ self._in_degree: dict[str, int] = {}
121
+ # Reverse adjacency: maps dependency -> list of dependents
122
+ self._reverse_adj: dict[str, list[str]] = {}
123
+
124
+ def add_plugin(self, name: str) -> None:
125
+ """Add plugin node to graph.
126
+
127
+ Args:
128
+ name: Plugin name
129
+ """
130
+ if name not in self._nodes:
131
+ self._nodes[name] = []
132
+ self._in_degree[name] = 0
133
+ self._reverse_adj[name] = []
134
+
135
+ def add_dependency(
136
+ self,
137
+ plugin: str,
138
+ depends_on: str,
139
+ version_spec: str = "*",
140
+ optional: bool = False,
141
+ ) -> None:
142
+ """Add dependency edge.
143
+
144
+ Args:
145
+ plugin: Plugin that has the dependency
146
+ depends_on: Plugin being depended on
147
+ version_spec: Version specification
148
+ optional: Whether dependency is optional
149
+ """
150
+ self.add_plugin(plugin)
151
+ self.add_plugin(depends_on)
152
+
153
+ dep = DependencyInfo(name=depends_on, version_spec=version_spec, optional=optional)
154
+ self._nodes[plugin].append(dep)
155
+ self._in_degree[plugin] += 1
156
+ # Track reverse edge: depends_on -> plugin (plugin depends on depends_on)
157
+ self._reverse_adj[depends_on].append(plugin)
158
+
159
+ def resolve_order(self) -> list[str]:
160
+ """Resolve topological order for loading.
161
+
162
+ Returns:
163
+ List of plugin names in load order
164
+
165
+ Raises:
166
+ ValueError: If circular dependency detected
167
+
168
+ References:
169
+ PLUG-005: Dependency Resolution - circular dependency detection
170
+ """
171
+ # Kahn's algorithm
172
+ in_degree = dict(self._in_degree)
173
+ queue = [n for n, d in in_degree.items() if d == 0]
174
+ result = []
175
+
176
+ while queue:
177
+ node = queue.pop(0)
178
+ result.append(node)
179
+
180
+ # Decrement in_degree for nodes that depend on this one
181
+ for dependent in self._reverse_adj.get(node, []):
182
+ in_degree[dependent] -= 1
183
+ if in_degree[dependent] == 0:
184
+ queue.append(dependent)
185
+
186
+ if len(result) != len(self._nodes):
187
+ # Cycle detected - find the cycle
188
+ remaining = set(self._nodes.keys()) - set(result)
189
+ cycle = self._find_cycle(remaining)
190
+
191
+ raise ValueError(f"Circular dependency detected: {' -> '.join([*cycle, cycle[0]])}")
192
+
193
+ return result
194
+
195
+ def _find_cycle(self, nodes: set[str]) -> list[str]:
196
+ """Find a cycle in the dependency graph.
197
+
198
+ Args:
199
+ nodes: Set of nodes that may be in a cycle
200
+
201
+ Returns:
202
+ List of nodes forming a cycle
203
+
204
+ References:
205
+ PLUG-005: Dependency Resolution - circular dependency detection
206
+ """
207
+ visited: set[str] = set()
208
+ rec_stack: list[str] = []
209
+
210
+ def dfs(node: str) -> list[str] | None:
211
+ visited.add(node)
212
+ rec_stack.append(node)
213
+
214
+ for dep in self._nodes.get(node, []):
215
+ if dep.name not in nodes:
216
+ continue
217
+
218
+ if dep.name not in visited:
219
+ cycle = dfs(dep.name)
220
+ if cycle:
221
+ return cycle
222
+ elif dep.name in rec_stack:
223
+ # Found cycle
224
+ idx = rec_stack.index(dep.name)
225
+ return rec_stack[idx:]
226
+
227
+ rec_stack.pop()
228
+ return None
229
+
230
+ for node in nodes:
231
+ if node not in visited:
232
+ cycle = dfs(node)
233
+ if cycle:
234
+ return cycle
235
+
236
+ return []
237
+
238
+ def get_dependencies(self, plugin: str) -> list[DependencyInfo]:
239
+ """Get dependencies for a plugin.
240
+
241
+ Args:
242
+ plugin: Plugin name
243
+
244
+ Returns:
245
+ List of dependencies
246
+ """
247
+ return self._nodes.get(plugin, [])
248
+
249
+ def get_dependents(self, plugin: str) -> list[str]:
250
+ """Get plugins that depend on given plugin.
251
+
252
+ Args:
253
+ plugin: Plugin name
254
+
255
+ Returns:
256
+ List of dependent plugin names
257
+ """
258
+ dependents = []
259
+ for name, deps in self._nodes.items():
260
+ if any(d.name == plugin for d in deps):
261
+ dependents.append(name)
262
+ return dependents
263
+
264
+
265
+ class PluginLifecycleManager:
266
+ """Manager for plugin lifecycle operations.
267
+
268
+ Handles plugin loading, configuration, enabling/disabling,
269
+ hot reload, and graceful degradation.
270
+
271
+ Example:
272
+ >>> manager = PluginLifecycleManager()
273
+ >>> manager.discover_plugins()
274
+ >>> manager.load_plugin("uart_decoder")
275
+ >>> manager.enable_plugin("uart_decoder")
276
+
277
+ References:
278
+ PLUG-004: Plugin Lifecycle (enable/disable/reload)
279
+ PLUG-005: Dependency Resolution
280
+ PLUG-006: Graceful Degradation
281
+ PLUG-007: Lazy Loading
282
+ PLUG-008: Plugin Hot Reload
283
+ """
284
+
285
+ def __init__(self, plugin_dirs: list[Path] | None = None) -> None:
286
+ """Initialize lifecycle manager.
287
+
288
+ Args:
289
+ plugin_dirs: Directories to search for plugins
290
+ """
291
+ self._plugin_dirs = plugin_dirs or []
292
+ self._handles: dict[str, PluginHandle] = {}
293
+ self._dependency_graph = DependencyGraph()
294
+ self._lock = threading.RLock()
295
+ self._lazy_loaders: dict[str, Callable[[], PluginBase]] = {}
296
+ self._file_watchers: dict[str, float] = {} # path -> mtime
297
+ self._lifecycle_callbacks: list[Callable[[str, PluginState], None]] = []
298
+
299
+ def discover_plugins(self) -> list[str]:
300
+ """Discover available plugins.
301
+
302
+ Scans plugin directories for plugin manifests and Python files.
303
+
304
+ Returns:
305
+ List of discovered plugin names
306
+
307
+ References:
308
+ PLUG-007: Lazy Loading
309
+ """
310
+ discovered = []
311
+
312
+ for plugin_dir in self._plugin_dirs:
313
+ if not plugin_dir.exists():
314
+ continue
315
+
316
+ for item in plugin_dir.iterdir():
317
+ if item.is_dir() and (item / "__init__.py").exists():
318
+ # Package plugin
319
+ name = item.name
320
+ self._register_lazy_loader(name, item)
321
+ discovered.append(name)
322
+ elif item.suffix == ".py" and not item.name.startswith("_"):
323
+ # Single file plugin
324
+ name = item.stem
325
+ self._register_lazy_loader(name, item)
326
+ discovered.append(name)
327
+
328
+ logger.info(f"Discovered {len(discovered)} plugins")
329
+ return discovered
330
+
331
+ def _register_lazy_loader(self, name: str, path: Path) -> None:
332
+ """Register lazy loader for a plugin.
333
+
334
+ Args:
335
+ name: Plugin name
336
+ path: Path to plugin
337
+
338
+ References:
339
+ PLUG-007: Lazy Loading
340
+ """
341
+
342
+ def loader() -> PluginBase:
343
+ return self._load_plugin_from_path(name, path)
344
+
345
+ self._lazy_loaders[name] = loader
346
+
347
+ # Create handle in DISCOVERED state
348
+ handle = PluginHandle(
349
+ metadata=PluginMetadata(name=name, version="0.0.0"),
350
+ state=PluginState.DISCOVERED,
351
+ )
352
+ self._handles[name] = handle
353
+
354
+ # Track file for hot reload
355
+ if path.is_file():
356
+ self._file_watchers[str(path)] = path.stat().st_mtime
357
+ else:
358
+ init_path = path / "__init__.py"
359
+ if init_path.exists():
360
+ self._file_watchers[str(init_path)] = init_path.stat().st_mtime
361
+
362
+ def _load_plugin_from_path(self, name: str, path: Path) -> PluginBase:
363
+ """Load plugin from path.
364
+
365
+ Args:
366
+ name: Plugin name
367
+ path: Path to plugin
368
+
369
+ Returns:
370
+ Loaded plugin instance
371
+
372
+ Raises:
373
+ ImportError: If plugin cannot be loaded or no PluginBase subclass found
374
+ """
375
+ if path.is_dir():
376
+ spec = importlib.util.spec_from_file_location(name, path / "__init__.py")
377
+ else:
378
+ spec = importlib.util.spec_from_file_location(name, path)
379
+
380
+ if spec is None or spec.loader is None:
381
+ raise ImportError(f"Cannot load plugin from {path}")
382
+
383
+ module = importlib.util.module_from_spec(spec)
384
+ sys.modules[name] = module
385
+ spec.loader.exec_module(module)
386
+
387
+ # Find PluginBase subclass
388
+ for attr_name in dir(module):
389
+ attr = getattr(module, attr_name)
390
+ if isinstance(attr, type) and issubclass(attr, PluginBase) and attr is not PluginBase:
391
+ return attr()
392
+
393
+ raise ImportError(f"No PluginBase subclass found in {path}")
394
+
395
+ def load_plugin(
396
+ self, name: str, *, lazy: bool = True, resolve_deps: bool = True
397
+ ) -> PluginHandle:
398
+ """Load a plugin.
399
+
400
+ Args:
401
+ name: Plugin name
402
+ lazy: Use lazy loading if available
403
+ resolve_deps: Resolve dependencies first
404
+
405
+ Returns:
406
+ Plugin handle
407
+
408
+ Raises:
409
+ Exception: If plugin loading or initialization fails.
410
+ ValueError: If plugin not found or dependency resolution fails
411
+
412
+ References:
413
+ PLUG-004: Plugin Lifecycle
414
+ PLUG-005: Dependency Resolution
415
+ PLUG-007: Lazy Loading
416
+ """
417
+ with self._lock:
418
+ if name not in self._handles:
419
+ raise ValueError(f"Plugin '{name}' not discovered")
420
+
421
+ handle = self._handles[name]
422
+
423
+ if handle.state == PluginState.LOADED:
424
+ return handle
425
+
426
+ # Resolve dependencies first
427
+ if resolve_deps:
428
+ self._resolve_dependencies(name)
429
+
430
+ handle.state = PluginState.LOADING
431
+ self._notify_state_change(name, handle.state)
432
+
433
+ try:
434
+ import time
435
+
436
+ start = time.time()
437
+
438
+ # Use lazy loader if available
439
+ if lazy and name in self._lazy_loaders:
440
+ instance = self._lazy_loaders[name]()
441
+ else:
442
+ instance = self._load_plugin_from_path(name, self._get_plugin_path(name))
443
+
444
+ handle.instance = instance
445
+ handle.metadata = instance.metadata
446
+ handle.load_time = time.time() - start
447
+
448
+ # Call on_load
449
+ instance.on_load()
450
+
451
+ handle.state = PluginState.LOADED
452
+ self._notify_state_change(name, handle.state)
453
+
454
+ logger.info(
455
+ f"Loaded plugin '{name}' v{handle.metadata.version} in {handle.load_time:.3f}s"
456
+ )
457
+
458
+ return handle
459
+
460
+ except Exception as e:
461
+ import traceback
462
+
463
+ error = PluginLoadError(
464
+ plugin_name=name,
465
+ error=e,
466
+ traceback=traceback.format_exc(),
467
+ stage="load",
468
+ recoverable=True,
469
+ )
470
+ handle.errors.append(error)
471
+ handle.state = PluginState.ERROR
472
+ self._notify_state_change(name, handle.state)
473
+
474
+ logger.error(f"Failed to load plugin '{name}': {e}")
475
+ raise
476
+
477
+ def _resolve_dependencies(self, name: str) -> None:
478
+ """Resolve and load dependencies.
479
+
480
+ Args:
481
+ name: Plugin name
482
+
483
+ Raises:
484
+ ValueError: If required dependency not found
485
+
486
+ References:
487
+ PLUG-005: Dependency Resolution
488
+ """
489
+ handle = self._handles[name]
490
+
491
+ for dep in handle.dependencies:
492
+ if dep.resolved:
493
+ continue
494
+
495
+ if dep.name not in self._handles:
496
+ if dep.optional:
497
+ logger.warning(f"Optional dependency '{dep.name}' for '{name}' not found")
498
+ continue
499
+ else:
500
+ raise ValueError(f"Required dependency '{dep.name}' for '{name}' not found")
501
+
502
+ dep_handle = self._handles[dep.name]
503
+ if dep_handle.state not in (PluginState.LOADED, PluginState.ENABLED):
504
+ self.load_plugin(dep.name)
505
+
506
+ dep.resolved = True
507
+
508
+ def configure_plugin(self, name: str, config: dict[str, Any]) -> PluginHandle:
509
+ """Configure a loaded plugin.
510
+
511
+ Args:
512
+ name: Plugin name
513
+ config: Configuration dictionary
514
+
515
+ Returns:
516
+ Updated plugin handle
517
+
518
+ Raises:
519
+ ValueError: If plugin not found or in invalid state
520
+ Exception: If configuration fails
521
+
522
+ References:
523
+ PLUG-004: Plugin Lifecycle
524
+ """
525
+ with self._lock:
526
+ handle = self._handles.get(name)
527
+ if handle is None:
528
+ raise ValueError(f"Plugin '{name}' not found")
529
+
530
+ if handle.state not in (PluginState.LOADED, PluginState.CONFIGURED):
531
+ raise ValueError(f"Cannot configure plugin in state {handle.state}")
532
+
533
+ try:
534
+ if handle.instance:
535
+ handle.instance.on_configure(config)
536
+ handle.state = PluginState.CONFIGURED
537
+ self._notify_state_change(name, handle.state)
538
+ logger.info(f"Configured plugin '{name}'")
539
+ return handle
540
+
541
+ except Exception as e:
542
+ import traceback
543
+
544
+ error = PluginLoadError(
545
+ plugin_name=name,
546
+ error=e,
547
+ traceback=traceback.format_exc(),
548
+ stage="configure",
549
+ )
550
+ handle.errors.append(error)
551
+ handle.state = PluginState.ERROR
552
+ self._notify_state_change(name, handle.state)
553
+ raise
554
+
555
+ def enable_plugin(self, name: str) -> PluginHandle:
556
+ """Enable a configured plugin.
557
+
558
+ Args:
559
+ name: Plugin name
560
+
561
+ Returns:
562
+ Updated plugin handle
563
+
564
+ Raises:
565
+ ValueError: If plugin not found
566
+
567
+ References:
568
+ PLUG-002: Plugin Registration - lifecycle hooks
569
+ PLUG-004: Plugin Lifecycle
570
+ """
571
+ with self._lock:
572
+ handle = self._handles.get(name)
573
+ if handle is None:
574
+ raise ValueError(f"Plugin '{name}' not found")
575
+
576
+ if handle.state == PluginState.ENABLED:
577
+ return handle
578
+
579
+ if handle.state == PluginState.DISCOVERED:
580
+ self.load_plugin(name)
581
+ handle = self._handles[name]
582
+
583
+ if handle.state == PluginState.LOADED:
584
+ self.configure_plugin(name, {})
585
+ handle = self._handles[name]
586
+
587
+ # Call on_enable hook
588
+ if handle.instance:
589
+ handle.instance.on_enable()
590
+
591
+ handle.state = PluginState.ENABLED
592
+ self._notify_state_change(name, handle.state)
593
+ logger.info(f"Enabled plugin '{name}'")
594
+ return handle
595
+
596
+ def disable_plugin(self, name: str, force: bool = False) -> PluginHandle:
597
+ """Disable a plugin.
598
+
599
+ Args:
600
+ name: Plugin name
601
+ force: Force disable even if dependents exist
602
+
603
+ Returns:
604
+ Updated plugin handle
605
+
606
+ Raises:
607
+ ValueError: If dependents exist and force=False
608
+
609
+ References:
610
+ PLUG-002: Plugin Registration - lifecycle hooks
611
+ PLUG-004: Plugin Lifecycle
612
+ PLUG-006: Graceful Degradation
613
+ """
614
+ with self._lock:
615
+ handle = self._handles.get(name)
616
+ if handle is None:
617
+ raise ValueError(f"Plugin '{name}' not found")
618
+
619
+ # Check for dependents
620
+ dependents = [
621
+ n
622
+ for n, h in self._handles.items()
623
+ if any(d.name == name for d in h.dependencies) and h.state == PluginState.ENABLED
624
+ ]
625
+
626
+ if dependents and not force:
627
+ raise ValueError(f"Cannot disable '{name}': required by {dependents}")
628
+
629
+ # Call on_disable hook
630
+ if handle.instance:
631
+ handle.instance.on_disable()
632
+
633
+ handle.state = PluginState.DISABLED
634
+ self._notify_state_change(name, handle.state)
635
+ logger.info(f"Disabled plugin '{name}'")
636
+ return handle
637
+
638
+ def unload_plugin(self, name: str, force: bool = False) -> None:
639
+ """Unload a plugin completely.
640
+
641
+ Args:
642
+ name: Plugin name
643
+ force: Force unload even if enabled
644
+
645
+ References:
646
+ PLUG-004: Plugin Lifecycle
647
+ """
648
+ with self._lock:
649
+ handle = self._handles.get(name)
650
+ if handle is None:
651
+ return
652
+
653
+ if handle.state == PluginState.ENABLED and not force:
654
+ self.disable_plugin(name)
655
+
656
+ handle.state = PluginState.UNLOADING
657
+ self._notify_state_change(name, handle.state)
658
+
659
+ if handle.instance:
660
+ try:
661
+ handle.instance.on_unload()
662
+ except Exception as e:
663
+ logger.warning(f"Error during unload of '{name}': {e}")
664
+
665
+ handle.instance = None
666
+ handle.state = PluginState.DISCOVERED
667
+ self._notify_state_change(name, handle.state)
668
+ logger.info(f"Unloaded plugin '{name}'")
669
+
670
+ def reload_plugin(self, name: str) -> PluginHandle:
671
+ """Hot reload a plugin.
672
+
673
+ Args:
674
+ name: Plugin name
675
+
676
+ Returns:
677
+ Updated plugin handle
678
+
679
+ Raises:
680
+ ValueError: If plugin not found
681
+
682
+ References:
683
+ PLUG-006: Plugin Hot Reload - state preservation, memory leak prevention
684
+ """
685
+ with self._lock:
686
+ handle = self._handles.get(name)
687
+ if handle is None:
688
+ raise ValueError(f"Plugin '{name}' not found")
689
+
690
+ was_enabled = handle.state == PluginState.ENABLED
691
+ config = handle.instance._config if handle.instance else {}
692
+
693
+ # Preserve plugin state for restoration
694
+ saved_state = self._save_plugin_state(handle)
695
+
696
+ # Unload and cleanup old references
697
+ self.unload_plugin(name, force=True)
698
+ self._cleanup_plugin_references(name)
699
+
700
+ # Clear from sys.modules to force reimport
701
+ modules_to_clear = [mod for mod in sys.modules if mod.startswith(f"{name}.")]
702
+ for mod in modules_to_clear:
703
+ del sys.modules[mod]
704
+ if name in sys.modules:
705
+ del sys.modules[name]
706
+
707
+ # Reload
708
+ handle = self.load_plugin(name)
709
+
710
+ # Restore state
711
+ self._restore_plugin_state(handle, saved_state)
712
+
713
+ if config:
714
+ self.configure_plugin(name, config)
715
+
716
+ if was_enabled:
717
+ self.enable_plugin(name)
718
+
719
+ logger.info(f"Hot reloaded plugin '{name}'")
720
+ return handle
721
+
722
+ def _save_plugin_state(self, handle: PluginHandle) -> dict[str, Any]:
723
+ """Save plugin state before reload.
724
+
725
+ Args:
726
+ handle: Plugin handle
727
+
728
+ Returns:
729
+ Saved state dictionary
730
+
731
+ References:
732
+ PLUG-006: Plugin Hot Reload - state preservation
733
+ """
734
+ state: dict[str, Any] = {
735
+ "config": handle.instance._config if handle.instance else {},
736
+ "registered_protocols": (
737
+ handle.instance._registered_protocols.copy() if handle.instance else []
738
+ ),
739
+ "registered_algorithms": (
740
+ handle.instance._registered_algorithms.copy() if handle.instance else []
741
+ ),
742
+ }
743
+ return state
744
+
745
+ def _restore_plugin_state(self, handle: PluginHandle, state: dict[str, Any]) -> None:
746
+ """Restore plugin state after reload.
747
+
748
+ Args:
749
+ handle: Plugin handle
750
+ state: Saved state dictionary
751
+
752
+ References:
753
+ PLUG-006: Plugin Hot Reload - state preservation
754
+ """
755
+ if handle.instance:
756
+ handle.instance._config = state.get("config", {})
757
+ handle.instance._registered_protocols = state.get("registered_protocols", [])
758
+ handle.instance._registered_algorithms = state.get("registered_algorithms", [])
759
+
760
+ def _cleanup_plugin_references(self, name: str) -> None:
761
+ """Clean up plugin references to prevent memory leaks.
762
+
763
+ Args:
764
+ name: Plugin name
765
+
766
+ References:
767
+ PLUG-006: Plugin Hot Reload - memory leak prevention
768
+ """
769
+ import gc
770
+
771
+ # Remove from lazy loaders
772
+ if name in self._lazy_loaders:
773
+ del self._lazy_loaders[name]
774
+
775
+ # Force garbage collection to clean up old references
776
+ gc.collect()
777
+
778
+ logger.debug(f"Cleaned up references for plugin '{name}'")
779
+
780
+ def check_for_changes(self) -> list[str]:
781
+ """Check for plugin file changes.
782
+
783
+ Returns:
784
+ List of plugin names with changed files
785
+
786
+ References:
787
+ PLUG-008: Plugin Hot Reload
788
+ """
789
+ changed = []
790
+
791
+ for path_str, old_mtime in self._file_watchers.items():
792
+ path = Path(path_str)
793
+ if path.exists():
794
+ new_mtime = path.stat().st_mtime
795
+ if new_mtime > old_mtime:
796
+ # Find plugin name
797
+ for name, handle in self._handles.items():
798
+ if handle.metadata.path and str(handle.metadata.path) in path_str:
799
+ changed.append(name)
800
+ break
801
+ self._file_watchers[path_str] = new_mtime
802
+
803
+ return changed
804
+
805
+ def auto_reload_changed(self) -> list[str]:
806
+ """Automatically reload changed plugins.
807
+
808
+ Returns:
809
+ List of reloaded plugin names
810
+
811
+ References:
812
+ PLUG-008: Plugin Hot Reload
813
+ """
814
+ changed = self.check_for_changes()
815
+ reloaded = []
816
+
817
+ for name in changed:
818
+ try:
819
+ self.reload_plugin(name)
820
+ reloaded.append(name)
821
+ except Exception as e:
822
+ logger.error(f"Failed to auto-reload '{name}': {e}")
823
+
824
+ return reloaded
825
+
826
+ def graceful_degradation(self, name: str) -> dict[str, Any]:
827
+ """Handle plugin failure gracefully.
828
+
829
+ Returns fallback options when a plugin fails.
830
+
831
+ Args:
832
+ name: Plugin name
833
+
834
+ Returns:
835
+ Dictionary with degradation options
836
+
837
+ References:
838
+ PLUG-006: Graceful Degradation
839
+ """
840
+ handle = self._handles.get(name)
841
+ if handle is None:
842
+ return {"status": "not_found", "alternatives": []}
843
+
844
+ # Find alternatives
845
+ alternatives = []
846
+ if handle.instance:
847
+ # Look for plugins with same capabilities
848
+ for cap in handle.metadata.capabilities:
849
+ for other_name, other_handle in self._handles.items():
850
+ if other_name != name and other_handle.state == PluginState.ENABLED:
851
+ if cap in other_handle.metadata.capabilities:
852
+ alternatives.append(other_name)
853
+
854
+ return {
855
+ "status": "degraded",
856
+ "plugin": name,
857
+ "error": str(handle.errors[-1].error) if handle.errors else None,
858
+ "alternatives": alternatives,
859
+ "recoverable": handle.errors[-1].recoverable if handle.errors else True,
860
+ }
861
+
862
+ def get_handle(self, name: str) -> PluginHandle | None:
863
+ """Get plugin handle.
864
+
865
+ Args:
866
+ name: Plugin name
867
+
868
+ Returns:
869
+ Plugin handle or None
870
+ """
871
+ return self._handles.get(name)
872
+
873
+ def get_enabled_plugins(self) -> list[str]:
874
+ """Get list of enabled plugins.
875
+
876
+ Returns:
877
+ List of plugin names
878
+ """
879
+ return [
880
+ name for name, handle in self._handles.items() if handle.state == PluginState.ENABLED
881
+ ]
882
+
883
+ def on_state_change(self, callback: Callable[[str, PluginState], None]) -> None:
884
+ """Register state change callback.
885
+
886
+ Args:
887
+ callback: Function called with (plugin_name, new_state)
888
+ """
889
+ self._lifecycle_callbacks.append(callback)
890
+
891
+ def _notify_state_change(self, name: str, state: PluginState) -> None:
892
+ """Notify callbacks of state change."""
893
+ for callback in self._lifecycle_callbacks:
894
+ try:
895
+ callback(name, state)
896
+ except Exception as e:
897
+ logger.warning(f"State change callback failed: {e}")
898
+
899
+ def _get_plugin_path(self, name: str) -> Path:
900
+ """Get path to plugin.
901
+
902
+ Args:
903
+ name: Plugin name
904
+
905
+ Returns:
906
+ Path to plugin
907
+
908
+ Raises:
909
+ ValueError: If plugin path not found
910
+ """
911
+ for plugin_dir in self._plugin_dirs:
912
+ # Check for package
913
+ pkg_path = plugin_dir / name
914
+ if pkg_path.is_dir() and (pkg_path / "__init__.py").exists():
915
+ return pkg_path
916
+ # Check for single file
917
+ file_path = plugin_dir / f"{name}.py"
918
+ if file_path.exists():
919
+ return file_path
920
+
921
+ raise ValueError(f"Plugin path not found for '{name}'")
922
+
923
+
924
+ # Global lifecycle manager
925
+ _lifecycle_manager: PluginLifecycleManager | None = None
926
+
927
+
928
+ def get_lifecycle_manager() -> PluginLifecycleManager:
929
+ """Get global lifecycle manager.
930
+
931
+ Returns:
932
+ Global PluginLifecycleManager instance
933
+ """
934
+ global _lifecycle_manager
935
+ if _lifecycle_manager is None:
936
+ _lifecycle_manager = PluginLifecycleManager()
937
+ return _lifecycle_manager
938
+
939
+
940
+ def set_plugin_directories(directories: list[Path]) -> None:
941
+ """Set plugin directories for global manager.
942
+
943
+ Args:
944
+ directories: List of plugin directories
945
+ """
946
+ global _lifecycle_manager
947
+ _lifecycle_manager = PluginLifecycleManager(directories)
948
+
949
+
950
+ __all__ = [
951
+ "DependencyGraph",
952
+ "DependencyInfo",
953
+ "PluginHandle",
954
+ "PluginLifecycleManager",
955
+ "PluginLoadError",
956
+ "PluginState",
957
+ "get_lifecycle_manager",
958
+ "set_plugin_directories",
959
+ ]