oscura 0.5.1__py3-none-any.whl → 0.7.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 (497) hide show
  1. oscura/__init__.py +169 -167
  2. oscura/analyzers/__init__.py +3 -0
  3. oscura/analyzers/classification.py +659 -0
  4. oscura/analyzers/digital/edges.py +325 -65
  5. oscura/analyzers/digital/quality.py +293 -166
  6. oscura/analyzers/digital/timing.py +260 -115
  7. oscura/analyzers/digital/timing_numba.py +334 -0
  8. oscura/analyzers/entropy.py +605 -0
  9. oscura/analyzers/eye/diagram.py +176 -109
  10. oscura/analyzers/eye/metrics.py +5 -5
  11. oscura/analyzers/jitter/__init__.py +6 -4
  12. oscura/analyzers/jitter/ber.py +52 -52
  13. oscura/analyzers/jitter/classification.py +156 -0
  14. oscura/analyzers/jitter/decomposition.py +163 -113
  15. oscura/analyzers/jitter/spectrum.py +80 -64
  16. oscura/analyzers/ml/__init__.py +39 -0
  17. oscura/analyzers/ml/features.py +600 -0
  18. oscura/analyzers/ml/signal_classifier.py +604 -0
  19. oscura/analyzers/packet/daq.py +246 -158
  20. oscura/analyzers/packet/parser.py +12 -1
  21. oscura/analyzers/packet/payload.py +50 -2110
  22. oscura/analyzers/packet/payload_analysis.py +361 -181
  23. oscura/analyzers/packet/payload_patterns.py +133 -70
  24. oscura/analyzers/packet/stream.py +84 -23
  25. oscura/analyzers/patterns/__init__.py +26 -5
  26. oscura/analyzers/patterns/anomaly_detection.py +908 -0
  27. oscura/analyzers/patterns/clustering.py +169 -108
  28. oscura/analyzers/patterns/clustering_optimized.py +227 -0
  29. oscura/analyzers/patterns/discovery.py +1 -1
  30. oscura/analyzers/patterns/matching.py +581 -197
  31. oscura/analyzers/patterns/pattern_mining.py +778 -0
  32. oscura/analyzers/patterns/periodic.py +121 -38
  33. oscura/analyzers/patterns/sequences.py +175 -78
  34. oscura/analyzers/power/conduction.py +1 -1
  35. oscura/analyzers/power/soa.py +6 -6
  36. oscura/analyzers/power/switching.py +250 -110
  37. oscura/analyzers/protocol/__init__.py +17 -1
  38. oscura/analyzers/protocols/base.py +6 -6
  39. oscura/analyzers/protocols/ble/__init__.py +38 -0
  40. oscura/analyzers/protocols/ble/analyzer.py +809 -0
  41. oscura/analyzers/protocols/ble/uuids.py +288 -0
  42. oscura/analyzers/protocols/can.py +257 -127
  43. oscura/analyzers/protocols/can_fd.py +107 -80
  44. oscura/analyzers/protocols/flexray.py +139 -80
  45. oscura/analyzers/protocols/hdlc.py +93 -58
  46. oscura/analyzers/protocols/i2c.py +247 -106
  47. oscura/analyzers/protocols/i2s.py +138 -86
  48. oscura/analyzers/protocols/industrial/__init__.py +40 -0
  49. oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
  50. oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
  51. oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
  52. oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
  53. oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
  54. oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
  55. oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
  56. oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
  57. oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
  58. oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
  59. oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
  60. oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
  61. oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
  62. oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
  63. oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
  64. oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
  65. oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
  66. oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
  67. oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
  68. oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
  69. oscura/analyzers/protocols/jtag.py +180 -98
  70. oscura/analyzers/protocols/lin.py +219 -114
  71. oscura/analyzers/protocols/manchester.py +4 -4
  72. oscura/analyzers/protocols/onewire.py +253 -149
  73. oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
  74. oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
  75. oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
  76. oscura/analyzers/protocols/spi.py +192 -95
  77. oscura/analyzers/protocols/swd.py +321 -167
  78. oscura/analyzers/protocols/uart.py +267 -125
  79. oscura/analyzers/protocols/usb.py +235 -131
  80. oscura/analyzers/side_channel/power.py +17 -12
  81. oscura/analyzers/signal/__init__.py +15 -0
  82. oscura/analyzers/signal/timing_analysis.py +1086 -0
  83. oscura/analyzers/signal_integrity/__init__.py +4 -1
  84. oscura/analyzers/signal_integrity/sparams.py +2 -19
  85. oscura/analyzers/spectral/chunked.py +129 -60
  86. oscura/analyzers/spectral/chunked_fft.py +300 -94
  87. oscura/analyzers/spectral/chunked_wavelet.py +100 -80
  88. oscura/analyzers/statistical/checksum.py +376 -217
  89. oscura/analyzers/statistical/classification.py +229 -107
  90. oscura/analyzers/statistical/entropy.py +78 -53
  91. oscura/analyzers/statistics/correlation.py +407 -211
  92. oscura/analyzers/statistics/outliers.py +2 -2
  93. oscura/analyzers/statistics/streaming.py +30 -5
  94. oscura/analyzers/validation.py +216 -101
  95. oscura/analyzers/waveform/measurements.py +9 -0
  96. oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
  97. oscura/analyzers/waveform/spectral.py +500 -228
  98. oscura/api/__init__.py +31 -5
  99. oscura/api/dsl/__init__.py +582 -0
  100. oscura/{dsl → api/dsl}/commands.py +43 -76
  101. oscura/{dsl → api/dsl}/interpreter.py +26 -51
  102. oscura/{dsl → api/dsl}/parser.py +107 -77
  103. oscura/{dsl → api/dsl}/repl.py +2 -2
  104. oscura/api/dsl.py +1 -1
  105. oscura/{integrations → api/integrations}/__init__.py +1 -1
  106. oscura/{integrations → api/integrations}/llm.py +201 -102
  107. oscura/api/operators.py +3 -3
  108. oscura/api/optimization.py +144 -30
  109. oscura/api/rest_server.py +921 -0
  110. oscura/api/server/__init__.py +17 -0
  111. oscura/api/server/dashboard.py +850 -0
  112. oscura/api/server/static/README.md +34 -0
  113. oscura/api/server/templates/base.html +181 -0
  114. oscura/api/server/templates/export.html +120 -0
  115. oscura/api/server/templates/home.html +284 -0
  116. oscura/api/server/templates/protocols.html +58 -0
  117. oscura/api/server/templates/reports.html +43 -0
  118. oscura/api/server/templates/session_detail.html +89 -0
  119. oscura/api/server/templates/sessions.html +83 -0
  120. oscura/api/server/templates/waveforms.html +73 -0
  121. oscura/automotive/__init__.py +8 -1
  122. oscura/automotive/can/__init__.py +10 -0
  123. oscura/automotive/can/checksum.py +3 -1
  124. oscura/automotive/can/dbc_generator.py +590 -0
  125. oscura/automotive/can/message_wrapper.py +121 -74
  126. oscura/automotive/can/patterns.py +98 -21
  127. oscura/automotive/can/session.py +292 -56
  128. oscura/automotive/can/state_machine.py +6 -3
  129. oscura/automotive/can/stimulus_response.py +97 -75
  130. oscura/automotive/dbc/__init__.py +10 -2
  131. oscura/automotive/dbc/generator.py +84 -56
  132. oscura/automotive/dbc/parser.py +6 -6
  133. oscura/automotive/dtc/data.json +17 -102
  134. oscura/automotive/dtc/database.py +2 -2
  135. oscura/automotive/flexray/__init__.py +31 -0
  136. oscura/automotive/flexray/analyzer.py +504 -0
  137. oscura/automotive/flexray/crc.py +185 -0
  138. oscura/automotive/flexray/fibex.py +449 -0
  139. oscura/automotive/j1939/__init__.py +45 -8
  140. oscura/automotive/j1939/analyzer.py +605 -0
  141. oscura/automotive/j1939/spns.py +326 -0
  142. oscura/automotive/j1939/transport.py +306 -0
  143. oscura/automotive/lin/__init__.py +47 -0
  144. oscura/automotive/lin/analyzer.py +612 -0
  145. oscura/automotive/loaders/blf.py +13 -2
  146. oscura/automotive/loaders/csv_can.py +143 -72
  147. oscura/automotive/loaders/dispatcher.py +50 -2
  148. oscura/automotive/loaders/mdf.py +86 -45
  149. oscura/automotive/loaders/pcap.py +111 -61
  150. oscura/automotive/uds/__init__.py +4 -0
  151. oscura/automotive/uds/analyzer.py +725 -0
  152. oscura/automotive/uds/decoder.py +140 -58
  153. oscura/automotive/uds/models.py +7 -1
  154. oscura/automotive/visualization.py +1 -1
  155. oscura/cli/analyze.py +348 -0
  156. oscura/cli/batch.py +142 -122
  157. oscura/cli/benchmark.py +275 -0
  158. oscura/cli/characterize.py +137 -82
  159. oscura/cli/compare.py +224 -131
  160. oscura/cli/completion.py +250 -0
  161. oscura/cli/config_cmd.py +361 -0
  162. oscura/cli/decode.py +164 -87
  163. oscura/cli/export.py +286 -0
  164. oscura/cli/main.py +115 -31
  165. oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
  166. oscura/{onboarding → cli/onboarding}/help.py +80 -58
  167. oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
  168. oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
  169. oscura/cli/progress.py +147 -0
  170. oscura/cli/shell.py +157 -135
  171. oscura/cli/validate_cmd.py +204 -0
  172. oscura/cli/visualize.py +158 -0
  173. oscura/convenience.py +125 -79
  174. oscura/core/__init__.py +4 -2
  175. oscura/core/backend_selector.py +3 -3
  176. oscura/core/cache.py +126 -15
  177. oscura/core/cancellation.py +1 -1
  178. oscura/{config → core/config}/__init__.py +20 -11
  179. oscura/{config → core/config}/defaults.py +1 -1
  180. oscura/{config → core/config}/loader.py +7 -5
  181. oscura/{config → core/config}/memory.py +5 -5
  182. oscura/{config → core/config}/migration.py +1 -1
  183. oscura/{config → core/config}/pipeline.py +99 -23
  184. oscura/{config → core/config}/preferences.py +1 -1
  185. oscura/{config → core/config}/protocol.py +3 -3
  186. oscura/{config → core/config}/schema.py +426 -272
  187. oscura/{config → core/config}/settings.py +1 -1
  188. oscura/{config → core/config}/thresholds.py +195 -153
  189. oscura/core/correlation.py +5 -6
  190. oscura/core/cross_domain.py +0 -2
  191. oscura/core/debug.py +9 -5
  192. oscura/{extensibility → core/extensibility}/docs.py +158 -70
  193. oscura/{extensibility → core/extensibility}/extensions.py +160 -76
  194. oscura/{extensibility → core/extensibility}/logging.py +1 -1
  195. oscura/{extensibility → core/extensibility}/measurements.py +1 -1
  196. oscura/{extensibility → core/extensibility}/plugins.py +1 -1
  197. oscura/{extensibility → core/extensibility}/templates.py +73 -3
  198. oscura/{extensibility → core/extensibility}/validation.py +1 -1
  199. oscura/core/gpu_backend.py +11 -7
  200. oscura/core/log_query.py +101 -11
  201. oscura/core/logging.py +126 -54
  202. oscura/core/logging_advanced.py +5 -5
  203. oscura/core/memory_limits.py +108 -70
  204. oscura/core/memory_monitor.py +2 -2
  205. oscura/core/memory_progress.py +7 -7
  206. oscura/core/memory_warnings.py +1 -1
  207. oscura/core/numba_backend.py +13 -13
  208. oscura/{plugins → core/plugins}/__init__.py +9 -9
  209. oscura/{plugins → core/plugins}/base.py +7 -7
  210. oscura/{plugins → core/plugins}/cli.py +3 -3
  211. oscura/{plugins → core/plugins}/discovery.py +186 -106
  212. oscura/{plugins → core/plugins}/lifecycle.py +1 -1
  213. oscura/{plugins → core/plugins}/manager.py +7 -7
  214. oscura/{plugins → core/plugins}/registry.py +3 -3
  215. oscura/{plugins → core/plugins}/versioning.py +1 -1
  216. oscura/core/progress.py +16 -1
  217. oscura/core/provenance.py +8 -2
  218. oscura/{schemas → core/schemas}/__init__.py +2 -2
  219. oscura/{schemas → core/schemas}/device_mapping.json +2 -8
  220. oscura/{schemas → core/schemas}/packet_format.json +4 -24
  221. oscura/{schemas → core/schemas}/protocol_definition.json +2 -12
  222. oscura/core/types.py +4 -0
  223. oscura/core/uncertainty.py +3 -3
  224. oscura/correlation/__init__.py +52 -0
  225. oscura/correlation/multi_protocol.py +811 -0
  226. oscura/discovery/auto_decoder.py +117 -35
  227. oscura/discovery/comparison.py +191 -86
  228. oscura/discovery/quality_validator.py +155 -68
  229. oscura/discovery/signal_detector.py +196 -79
  230. oscura/export/__init__.py +18 -8
  231. oscura/export/kaitai_struct.py +513 -0
  232. oscura/export/scapy_layer.py +801 -0
  233. oscura/export/wireshark/generator.py +1 -1
  234. oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
  235. oscura/export/wireshark_dissector.py +746 -0
  236. oscura/guidance/wizard.py +207 -111
  237. oscura/hardware/__init__.py +19 -0
  238. oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
  239. oscura/{acquisition → hardware/acquisition}/file.py +2 -2
  240. oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
  241. oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
  242. oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
  243. oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
  244. oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
  245. oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
  246. oscura/hardware/firmware/__init__.py +29 -0
  247. oscura/hardware/firmware/pattern_recognition.py +874 -0
  248. oscura/hardware/hal_detector.py +736 -0
  249. oscura/hardware/security/__init__.py +37 -0
  250. oscura/hardware/security/side_channel_detector.py +1126 -0
  251. oscura/inference/__init__.py +4 -0
  252. oscura/inference/active_learning/observation_table.py +4 -1
  253. oscura/inference/alignment.py +216 -123
  254. oscura/inference/bayesian.py +113 -33
  255. oscura/inference/crc_reverse.py +101 -55
  256. oscura/inference/logic.py +6 -2
  257. oscura/inference/message_format.py +342 -183
  258. oscura/inference/protocol.py +95 -44
  259. oscura/inference/protocol_dsl.py +180 -82
  260. oscura/inference/signal_intelligence.py +1439 -706
  261. oscura/inference/spectral.py +99 -57
  262. oscura/inference/state_machine.py +810 -158
  263. oscura/inference/stream.py +270 -110
  264. oscura/iot/__init__.py +34 -0
  265. oscura/iot/coap/__init__.py +32 -0
  266. oscura/iot/coap/analyzer.py +668 -0
  267. oscura/iot/coap/options.py +212 -0
  268. oscura/iot/lorawan/__init__.py +21 -0
  269. oscura/iot/lorawan/crypto.py +206 -0
  270. oscura/iot/lorawan/decoder.py +801 -0
  271. oscura/iot/lorawan/mac_commands.py +341 -0
  272. oscura/iot/mqtt/__init__.py +27 -0
  273. oscura/iot/mqtt/analyzer.py +999 -0
  274. oscura/iot/mqtt/properties.py +315 -0
  275. oscura/iot/zigbee/__init__.py +31 -0
  276. oscura/iot/zigbee/analyzer.py +615 -0
  277. oscura/iot/zigbee/security.py +153 -0
  278. oscura/iot/zigbee/zcl.py +349 -0
  279. oscura/jupyter/display.py +125 -45
  280. oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
  281. oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
  282. oscura/jupyter/exploratory/fuzzy.py +746 -0
  283. oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
  284. oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
  285. oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
  286. oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
  287. oscura/jupyter/exploratory/sync.py +612 -0
  288. oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
  289. oscura/jupyter/magic.py +4 -4
  290. oscura/{ui → jupyter/ui}/__init__.py +2 -2
  291. oscura/{ui → jupyter/ui}/formatters.py +3 -3
  292. oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
  293. oscura/loaders/__init__.py +183 -67
  294. oscura/loaders/binary.py +88 -1
  295. oscura/loaders/chipwhisperer.py +153 -137
  296. oscura/loaders/configurable.py +208 -86
  297. oscura/loaders/csv_loader.py +458 -215
  298. oscura/loaders/hdf5_loader.py +278 -119
  299. oscura/loaders/lazy.py +87 -54
  300. oscura/loaders/mmap_loader.py +1 -1
  301. oscura/loaders/numpy_loader.py +253 -116
  302. oscura/loaders/pcap.py +226 -151
  303. oscura/loaders/rigol.py +110 -49
  304. oscura/loaders/sigrok.py +201 -78
  305. oscura/loaders/tdms.py +81 -58
  306. oscura/loaders/tektronix.py +291 -174
  307. oscura/loaders/touchstone.py +182 -87
  308. oscura/loaders/tss.py +456 -0
  309. oscura/loaders/vcd.py +215 -117
  310. oscura/loaders/wav.py +155 -68
  311. oscura/reporting/__init__.py +9 -0
  312. oscura/reporting/analyze.py +352 -146
  313. oscura/reporting/argument_preparer.py +69 -14
  314. oscura/reporting/auto_report.py +97 -61
  315. oscura/reporting/batch.py +131 -58
  316. oscura/reporting/chart_selection.py +57 -45
  317. oscura/reporting/comparison.py +63 -17
  318. oscura/reporting/content/executive.py +76 -24
  319. oscura/reporting/core_formats/multi_format.py +11 -8
  320. oscura/reporting/engine.py +312 -158
  321. oscura/reporting/enhanced_reports.py +949 -0
  322. oscura/reporting/export.py +86 -43
  323. oscura/reporting/formatting/numbers.py +69 -42
  324. oscura/reporting/html.py +139 -58
  325. oscura/reporting/index.py +137 -65
  326. oscura/reporting/output.py +158 -67
  327. oscura/reporting/pdf.py +67 -102
  328. oscura/reporting/plots.py +191 -112
  329. oscura/reporting/sections.py +88 -47
  330. oscura/reporting/standards.py +104 -61
  331. oscura/reporting/summary_generator.py +75 -55
  332. oscura/reporting/tables.py +138 -54
  333. oscura/reporting/templates/enhanced/protocol_re.html +525 -0
  334. oscura/sessions/__init__.py +14 -23
  335. oscura/sessions/base.py +3 -3
  336. oscura/sessions/blackbox.py +106 -10
  337. oscura/sessions/generic.py +2 -2
  338. oscura/sessions/legacy.py +783 -0
  339. oscura/side_channel/__init__.py +63 -0
  340. oscura/side_channel/dpa.py +1025 -0
  341. oscura/utils/__init__.py +15 -1
  342. oscura/utils/bitwise.py +118 -0
  343. oscura/{builders → utils/builders}/__init__.py +1 -1
  344. oscura/{comparison → utils/comparison}/__init__.py +6 -6
  345. oscura/{comparison → utils/comparison}/compare.py +202 -101
  346. oscura/{comparison → utils/comparison}/golden.py +83 -63
  347. oscura/{comparison → utils/comparison}/limits.py +313 -89
  348. oscura/{comparison → utils/comparison}/mask.py +151 -45
  349. oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
  350. oscura/{comparison → utils/comparison}/visualization.py +147 -89
  351. oscura/{component → utils/component}/__init__.py +3 -3
  352. oscura/{component → utils/component}/impedance.py +122 -58
  353. oscura/{component → utils/component}/reactive.py +165 -168
  354. oscura/{component → utils/component}/transmission_line.py +3 -3
  355. oscura/{filtering → utils/filtering}/__init__.py +6 -6
  356. oscura/{filtering → utils/filtering}/base.py +1 -1
  357. oscura/{filtering → utils/filtering}/convenience.py +2 -2
  358. oscura/{filtering → utils/filtering}/design.py +169 -93
  359. oscura/{filtering → utils/filtering}/filters.py +2 -2
  360. oscura/{filtering → utils/filtering}/introspection.py +2 -2
  361. oscura/utils/geometry.py +31 -0
  362. oscura/utils/imports.py +184 -0
  363. oscura/utils/lazy.py +1 -1
  364. oscura/{math → utils/math}/__init__.py +2 -2
  365. oscura/{math → utils/math}/arithmetic.py +114 -48
  366. oscura/{math → utils/math}/interpolation.py +139 -106
  367. oscura/utils/memory.py +129 -66
  368. oscura/utils/memory_advanced.py +92 -9
  369. oscura/utils/memory_extensions.py +10 -8
  370. oscura/{optimization → utils/optimization}/__init__.py +1 -1
  371. oscura/{optimization → utils/optimization}/search.py +2 -2
  372. oscura/utils/performance/__init__.py +58 -0
  373. oscura/utils/performance/caching.py +889 -0
  374. oscura/utils/performance/lsh_clustering.py +333 -0
  375. oscura/utils/performance/memory_optimizer.py +699 -0
  376. oscura/utils/performance/optimizations.py +675 -0
  377. oscura/utils/performance/parallel.py +654 -0
  378. oscura/utils/performance/profiling.py +661 -0
  379. oscura/{pipeline → utils/pipeline}/base.py +1 -1
  380. oscura/{pipeline → utils/pipeline}/composition.py +1 -1
  381. oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
  382. oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
  383. oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
  384. oscura/{search → utils/search}/__init__.py +3 -3
  385. oscura/{search → utils/search}/anomaly.py +188 -58
  386. oscura/utils/search/context.py +294 -0
  387. oscura/{search → utils/search}/pattern.py +138 -10
  388. oscura/utils/serial.py +51 -0
  389. oscura/utils/storage/__init__.py +61 -0
  390. oscura/utils/storage/database.py +1166 -0
  391. oscura/{streaming → utils/streaming}/chunked.py +302 -143
  392. oscura/{streaming → utils/streaming}/progressive.py +1 -1
  393. oscura/{streaming → utils/streaming}/realtime.py +3 -2
  394. oscura/{triggering → utils/triggering}/__init__.py +6 -6
  395. oscura/{triggering → utils/triggering}/base.py +6 -6
  396. oscura/{triggering → utils/triggering}/edge.py +2 -2
  397. oscura/{triggering → utils/triggering}/pattern.py +2 -2
  398. oscura/{triggering → utils/triggering}/pulse.py +115 -74
  399. oscura/{triggering → utils/triggering}/window.py +2 -2
  400. oscura/utils/validation.py +32 -0
  401. oscura/validation/__init__.py +121 -0
  402. oscura/{compliance → validation/compliance}/__init__.py +5 -5
  403. oscura/{compliance → validation/compliance}/advanced.py +5 -5
  404. oscura/{compliance → validation/compliance}/masks.py +1 -1
  405. oscura/{compliance → validation/compliance}/reporting.py +127 -53
  406. oscura/{compliance → validation/compliance}/testing.py +114 -52
  407. oscura/validation/compliance_tests.py +915 -0
  408. oscura/validation/fuzzer.py +990 -0
  409. oscura/validation/grammar_tests.py +596 -0
  410. oscura/validation/grammar_validator.py +904 -0
  411. oscura/validation/hil_testing.py +977 -0
  412. oscura/{quality → validation/quality}/__init__.py +4 -4
  413. oscura/{quality → validation/quality}/ensemble.py +251 -171
  414. oscura/{quality → validation/quality}/explainer.py +3 -3
  415. oscura/{quality → validation/quality}/scoring.py +1 -1
  416. oscura/{quality → validation/quality}/warnings.py +4 -4
  417. oscura/validation/regression_suite.py +808 -0
  418. oscura/validation/replay.py +788 -0
  419. oscura/{testing → validation/testing}/__init__.py +2 -2
  420. oscura/{testing → validation/testing}/synthetic.py +5 -5
  421. oscura/visualization/__init__.py +9 -0
  422. oscura/visualization/accessibility.py +1 -1
  423. oscura/visualization/annotations.py +64 -67
  424. oscura/visualization/colors.py +7 -7
  425. oscura/visualization/digital.py +180 -81
  426. oscura/visualization/eye.py +236 -85
  427. oscura/visualization/interactive.py +320 -143
  428. oscura/visualization/jitter.py +587 -247
  429. oscura/visualization/layout.py +169 -134
  430. oscura/visualization/optimization.py +103 -52
  431. oscura/visualization/palettes.py +1 -1
  432. oscura/visualization/power.py +427 -211
  433. oscura/visualization/power_extended.py +626 -297
  434. oscura/visualization/presets.py +2 -0
  435. oscura/visualization/protocols.py +495 -181
  436. oscura/visualization/render.py +79 -63
  437. oscura/visualization/reverse_engineering.py +171 -124
  438. oscura/visualization/signal_integrity.py +460 -279
  439. oscura/visualization/specialized.py +190 -100
  440. oscura/visualization/spectral.py +670 -255
  441. oscura/visualization/thumbnails.py +166 -137
  442. oscura/visualization/waveform.py +150 -63
  443. oscura/workflows/__init__.py +3 -0
  444. oscura/{batch → workflows/batch}/__init__.py +5 -5
  445. oscura/{batch → workflows/batch}/advanced.py +150 -75
  446. oscura/workflows/batch/aggregate.py +531 -0
  447. oscura/workflows/batch/analyze.py +236 -0
  448. oscura/{batch → workflows/batch}/logging.py +2 -2
  449. oscura/{batch → workflows/batch}/metrics.py +1 -1
  450. oscura/workflows/complete_re.py +1144 -0
  451. oscura/workflows/compliance.py +44 -54
  452. oscura/workflows/digital.py +197 -51
  453. oscura/workflows/legacy/__init__.py +12 -0
  454. oscura/{workflow → workflows/legacy}/dag.py +4 -1
  455. oscura/workflows/multi_trace.py +9 -9
  456. oscura/workflows/power.py +42 -62
  457. oscura/workflows/protocol.py +82 -49
  458. oscura/workflows/reverse_engineering.py +351 -150
  459. oscura/workflows/signal_integrity.py +157 -82
  460. oscura-0.7.0.dist-info/METADATA +661 -0
  461. oscura-0.7.0.dist-info/RECORD +591 -0
  462. oscura/batch/aggregate.py +0 -300
  463. oscura/batch/analyze.py +0 -139
  464. oscura/dsl/__init__.py +0 -73
  465. oscura/exceptions.py +0 -59
  466. oscura/exploratory/fuzzy.py +0 -513
  467. oscura/exploratory/sync.py +0 -384
  468. oscura/exporters/__init__.py +0 -94
  469. oscura/exporters/csv.py +0 -303
  470. oscura/exporters/exporters.py +0 -44
  471. oscura/exporters/hdf5.py +0 -217
  472. oscura/exporters/html_export.py +0 -701
  473. oscura/exporters/json_export.py +0 -291
  474. oscura/exporters/markdown_export.py +0 -367
  475. oscura/exporters/matlab_export.py +0 -354
  476. oscura/exporters/npz_export.py +0 -219
  477. oscura/exporters/spice_export.py +0 -210
  478. oscura/search/context.py +0 -149
  479. oscura/session/__init__.py +0 -34
  480. oscura/session/annotations.py +0 -289
  481. oscura/session/history.py +0 -313
  482. oscura/session/session.py +0 -520
  483. oscura/workflow/__init__.py +0 -13
  484. oscura-0.5.1.dist-info/METADATA +0 -583
  485. oscura-0.5.1.dist-info/RECORD +0 -481
  486. /oscura/core/{config.py → config/legacy.py} +0 -0
  487. /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
  488. /oscura/{extensibility → core/extensibility}/registry.py +0 -0
  489. /oscura/{plugins → core/plugins}/isolation.py +0 -0
  490. /oscura/{schemas → core/schemas}/bus_configuration.json +0 -0
  491. /oscura/{builders → utils/builders}/signal_builder.py +0 -0
  492. /oscura/{optimization → utils/optimization}/parallel.py +0 -0
  493. /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
  494. /oscura/{streaming → utils/streaming}/__init__.py +0 -0
  495. {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/WHEEL +0 -0
  496. {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/entry_points.txt +0 -0
  497. {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/licenses/LICENSE +0 -0
@@ -24,7 +24,7 @@ from contextlib import contextmanager
24
24
  from dataclasses import dataclass
25
25
  from typing import TYPE_CHECKING, Any
26
26
 
27
- from oscura.config.memory import get_memory_config
27
+ from oscura.core.config.memory import get_memory_config
28
28
  from oscura.utils.memory import get_available_memory, get_max_memory
29
29
 
30
30
  if TYPE_CHECKING:
@@ -97,7 +97,7 @@ class MemoryMonitor:
97
97
  if max_memory is None:
98
98
  self.max_memory = get_max_memory()
99
99
  elif isinstance(max_memory, str):
100
- from oscura.config.memory import _parse_memory_string
100
+ from oscura.core.config.memory import _parse_memory_string
101
101
 
102
102
  self.max_memory = _parse_memory_string(max_memory)
103
103
  else:
@@ -131,7 +131,7 @@ class MemoryLogger:
131
131
  # Initialize CSV writer if needed
132
132
  if self.format == "csv":
133
133
  self._csv_writer = csv.DictWriter(
134
- self._file_handle, # type: ignore[arg-type]
134
+ self._file_handle,
135
135
  fieldnames=[
136
136
  "timestamp",
137
137
  "operation",
@@ -144,9 +144,9 @@ class MemoryLogger:
144
144
  "message",
145
145
  ],
146
146
  )
147
- self._csv_writer.writeheader() # type: ignore[attr-defined]
147
+ self._csv_writer.writeheader()
148
148
  if self.auto_flush:
149
- self._file_handle.flush() # type: ignore[attr-defined]
149
+ self._file_handle.flush()
150
150
 
151
151
  return self
152
152
 
@@ -155,7 +155,7 @@ class MemoryLogger:
155
155
  # Note: exc_val and exc_tb intentionally unused but required for Python 3.11+ compatibility
156
156
  # Write summary for JSON format
157
157
  if self.format == "json" and self._file_handle:
158
- summary = { # type: ignore[unreachable]
158
+ summary = {
159
159
  "entries": [asdict(entry) for entry in self._entries],
160
160
  "summary": self._get_summary_dict(),
161
161
  }
@@ -163,7 +163,7 @@ class MemoryLogger:
163
163
 
164
164
  # Close file
165
165
  if self._file_handle:
166
- self._file_handle.close() # type: ignore[unreachable]
166
+ self._file_handle.close()
167
167
  self._file_handle = None
168
168
 
169
169
  def log_operation(
@@ -213,8 +213,8 @@ class MemoryLogger:
213
213
  self._entries.append(entry)
214
214
 
215
215
  # Write to file
216
- if self._file_handle and self.format == "csv": # type: ignore[unreachable]
217
- self._csv_writer.writerow(asdict(entry)) # type: ignore[unreachable]
216
+ if self._file_handle and self.format == "csv":
217
+ self._csv_writer.writerow(asdict(entry))
218
218
  if self.auto_flush:
219
219
  self._file_handle.flush()
220
220
 
@@ -20,7 +20,7 @@ import warnings
20
20
  from enum import Enum
21
21
  from typing import TYPE_CHECKING
22
22
 
23
- from oscura.config.memory import get_memory_config
23
+ from oscura.core.config.memory import get_memory_config
24
24
  from oscura.utils.memory import get_available_memory, get_memory_pressure
25
25
 
26
26
  if TYPE_CHECKING:
@@ -43,11 +43,11 @@ import numpy as np
43
43
 
44
44
  # Try to import Numba
45
45
  try:
46
- from numba import guvectorize as _numba_guvectorize # type: ignore[import-not-found]
47
- from numba import jit as _numba_jit # type: ignore[import-not-found]
48
- from numba import njit as _numba_njit # type: ignore[import-not-found]
49
- from numba import prange as _numba_prange # type: ignore[import-not-found]
50
- from numba import vectorize as _numba_vectorize # type: ignore[import-not-found]
46
+ from numba import guvectorize as _numba_guvectorize
47
+ from numba import jit as _numba_jit
48
+ from numba import njit as _numba_njit
49
+ from numba import prange as _numba_prange
50
+ from numba import vectorize as _numba_vectorize
51
51
 
52
52
  HAS_NUMBA = True
53
53
  except ImportError:
@@ -92,7 +92,7 @@ else:
92
92
  # Handle both @njit and @njit() syntax
93
93
  if len(args) == 1 and callable(args[0]) and not kwargs:
94
94
  return decorator(args[0]) # type: ignore[no-any-return]
95
- return decorator # type: ignore[no-any-return]
95
+ return decorator
96
96
 
97
97
  def prange(*args: Any, **kwargs: Any) -> range:
98
98
  """Fallback to regular range when Numba is not available.
@@ -122,7 +122,7 @@ else:
122
122
 
123
123
  if len(args) == 1 and callable(args[0]):
124
124
  return decorator(args[0]) # type: ignore[no-any-return]
125
- return decorator # type: ignore[no-any-return]
125
+ return decorator
126
126
 
127
127
  def guvectorize(*args: Any, **kwargs: Any) -> Callable[[F], F]:
128
128
  """No-op decorator when Numba is not available.
@@ -140,7 +140,7 @@ else:
140
140
 
141
141
  if len(args) == 1 and callable(args[0]):
142
142
  return decorator(args[0]) # type: ignore[no-any-return]
143
- return decorator # type: ignore[no-any-return]
143
+ return decorator
144
144
 
145
145
  def jit(*args: Any, **kwargs: Any) -> Callable[[F], F]:
146
146
  """No-op decorator when Numba is not available.
@@ -162,7 +162,7 @@ else:
162
162
 
163
163
  if len(args) == 1 and callable(args[0]) and not kwargs:
164
164
  return decorator(args[0]) # type: ignore[no-any-return]
165
- return decorator # type: ignore[no-any-return]
165
+ return decorator
166
166
 
167
167
 
168
168
  def get_optimal_numba_config(
@@ -202,7 +202,7 @@ def get_optimal_numba_config(
202
202
  # Example Numba-optimized functions for common operations
203
203
 
204
204
 
205
- @njit(cache=True) # type: ignore[misc,untyped-decorator]
205
+ @njit(cache=True) # type: ignore[untyped-decorator] # Numba JIT decorator
206
206
  def find_crossings_numba(
207
207
  data: np.ndarray, # type: ignore[type-arg]
208
208
  threshold: float,
@@ -233,7 +233,7 @@ def find_crossings_numba(
233
233
  return np.array(crossings, dtype=np.int64)
234
234
 
235
235
 
236
- @njit(parallel=True, cache=True) # type: ignore[misc,untyped-decorator]
236
+ @njit(parallel=True, cache=True) # type: ignore[untyped-decorator] # Numba JIT decorator
237
237
  def moving_average_numba(
238
238
  data: np.ndarray, # type: ignore[type-arg]
239
239
  window_size: int,
@@ -259,7 +259,7 @@ def moving_average_numba(
259
259
  return result
260
260
 
261
261
 
262
- @njit(cache=True) # type: ignore[misc,untyped-decorator]
262
+ @njit(cache=True) # type: ignore[untyped-decorator] # Numba JIT decorator
263
263
  def argrelextrema_numba(
264
264
  data: np.ndarray, # type: ignore[type-arg]
265
265
  comparator: int,
@@ -297,7 +297,7 @@ def argrelextrema_numba(
297
297
  return np.array(extrema, dtype=np.int64)
298
298
 
299
299
 
300
- @njit(cache=True) # type: ignore[misc,untyped-decorator]
300
+ @njit(cache=True) # type: ignore[untyped-decorator] # Numba JIT decorator
301
301
  def interpolate_linear_numba(
302
302
  x: np.ndarray, # type: ignore[type-arg]
303
303
  y: np.ndarray, # type: ignore[type-arg]
@@ -5,18 +5,18 @@ for extending Oscura functionality.
5
5
 
6
6
 
7
7
  Example:
8
- >>> from oscura.plugins import discover_plugins, get_plugin
8
+ >>> from oscura.core.plugins import discover_plugins, get_plugin
9
9
  >>> plugins = discover_plugins()
10
10
  >>> for plugin in plugins:
11
11
  ... print(f"{plugin.name} v{plugin.version}")
12
12
  """
13
13
 
14
- from oscura.plugins.base import (
14
+ from oscura.core.plugins.base import (
15
15
  PluginBase,
16
16
  PluginCapability,
17
17
  PluginMetadata,
18
18
  )
19
- from oscura.plugins.cli import (
19
+ from oscura.core.plugins.cli import (
20
20
  PluginInstaller,
21
21
  cli_disable_plugin,
22
22
  cli_enable_plugin,
@@ -25,12 +25,12 @@ from oscura.plugins.cli import (
25
25
  cli_plugin_info,
26
26
  cli_validate_plugin,
27
27
  )
28
- from oscura.plugins.discovery import (
28
+ from oscura.core.plugins.discovery import (
29
29
  discover_plugins,
30
30
  get_plugin_paths,
31
31
  scan_directory,
32
32
  )
33
- from oscura.plugins.isolation import (
33
+ from oscura.core.plugins.isolation import (
34
34
  IsolationManager,
35
35
  Permission,
36
36
  PermissionSet,
@@ -38,7 +38,7 @@ from oscura.plugins.isolation import (
38
38
  ResourceLimits,
39
39
  get_isolation_manager,
40
40
  )
41
- from oscura.plugins.lifecycle import (
41
+ from oscura.core.plugins.lifecycle import (
42
42
  DependencyGraph,
43
43
  DependencyInfo,
44
44
  PluginHandle,
@@ -48,12 +48,12 @@ from oscura.plugins.lifecycle import (
48
48
  get_lifecycle_manager,
49
49
  set_plugin_directories,
50
50
  )
51
- from oscura.plugins.manager import (
51
+ from oscura.core.plugins.manager import (
52
52
  PluginManager,
53
53
  get_plugin_manager,
54
54
  reset_plugin_manager,
55
55
  )
56
- from oscura.plugins.registry import (
56
+ from oscura.core.plugins.registry import (
57
57
  PluginRegistry,
58
58
  get_plugin,
59
59
  get_plugin_registry,
@@ -61,7 +61,7 @@ from oscura.plugins.registry import (
61
61
  list_plugins,
62
62
  register_plugin,
63
63
  )
64
- from oscura.plugins.versioning import (
64
+ from oscura.core.plugins.versioning import (
65
65
  Migration,
66
66
  MigrationManager,
67
67
  VersionCompatibilityLayer,
@@ -112,7 +112,7 @@ class PluginMetadata:
112
112
  return False
113
113
 
114
114
 
115
- class PluginBase(ABC): # noqa: B024
115
+ class PluginBase(ABC):
116
116
  """Base class for all Oscura plugins.
117
117
 
118
118
  Subclass this to create a plugin. Define class attributes for
@@ -143,8 +143,8 @@ class PluginBase(ABC): # noqa: B024
143
143
  description: str = ""
144
144
  homepage: str = ""
145
145
  license: str = ""
146
- capabilities: list[PluginCapability] = [] # noqa: RUF012
147
- requires_plugins: list[tuple[str, str]] = [] # (name, version_spec) # noqa: RUF012
146
+ capabilities: list[PluginCapability] = []
147
+ requires_plugins: list[tuple[str, str]] = [] # (name, version_spec)
148
148
 
149
149
  def __init__(self) -> None:
150
150
  """Initialize plugin instance."""
@@ -174,7 +174,7 @@ class PluginBase(ABC): # noqa: B024
174
174
  )
175
175
  return self._metadata
176
176
 
177
- def on_load(self) -> None: # noqa: B027
177
+ def on_load(self) -> None:
178
178
  """Called when plugin is loaded.
179
179
 
180
180
  Override to register capabilities, initialize resources, etc.
@@ -196,7 +196,7 @@ class PluginBase(ABC): # noqa: B024
196
196
  """
197
197
  self._config = config
198
198
 
199
- def on_enable(self) -> None: # noqa: B027
199
+ def on_enable(self) -> None:
200
200
  """Called when plugin is enabled.
201
201
 
202
202
  Override to activate plugin functionality, start services, etc.
@@ -205,7 +205,7 @@ class PluginBase(ABC): # noqa: B024
205
205
  PLUG-002: Plugin Registration - lifecycle hooks
206
206
  """
207
207
 
208
- def on_disable(self) -> None: # noqa: B027
208
+ def on_disable(self) -> None:
209
209
  """Called when plugin is disabled.
210
210
 
211
211
  Override to pause plugin functionality, stop services, etc.
@@ -214,7 +214,7 @@ class PluginBase(ABC): # noqa: B024
214
214
  PLUG-002: Plugin Registration - lifecycle hooks
215
215
  """
216
216
 
217
- def on_unload(self) -> None: # noqa: B027
217
+ def on_unload(self) -> None:
218
218
  """Called when plugin is unloaded.
219
219
 
220
220
  Override to clean up resources.
@@ -13,9 +13,9 @@ import tempfile
13
13
  from pathlib import Path
14
14
  from urllib.parse import urlparse
15
15
 
16
- from oscura.plugins.discovery import discover_plugins, get_plugin_paths
17
- from oscura.plugins.lifecycle import get_lifecycle_manager
18
- from oscura.plugins.registry import get_plugin_registry
16
+ from oscura.core.plugins.discovery import discover_plugins, get_plugin_paths
17
+ from oscura.core.plugins.lifecycle import get_lifecycle_manager
18
+ from oscura.core.plugins.registry import get_plugin_registry
19
19
 
20
20
  logger = logging.getLogger(__name__)
21
21
 
@@ -5,7 +5,7 @@ and Python entry points.
5
5
 
6
6
 
7
7
  Example:
8
- >>> from oscura.plugins.discovery import discover_plugins
8
+ >>> from oscura.core.plugins.discovery import discover_plugins
9
9
  >>> plugins = discover_plugins()
10
10
  >>> for plugin in plugins:
11
11
  ... print(f"Found: {plugin.name} v{plugin.version}")
@@ -20,9 +20,9 @@ import os
20
20
  import sys
21
21
  from dataclasses import dataclass
22
22
  from pathlib import Path
23
- from typing import TYPE_CHECKING
23
+ from typing import TYPE_CHECKING, Any
24
24
 
25
- from oscura.plugins.base import PluginBase, PluginMetadata
25
+ from oscura.core.plugins.base import PluginBase, PluginMetadata
26
26
 
27
27
  if TYPE_CHECKING:
28
28
  from collections.abc import Iterator
@@ -256,47 +256,11 @@ def _load_plugin_from_yaml(yaml_path: Path) -> DiscoveredPlugin | None:
256
256
  return None
257
257
 
258
258
  try:
259
- with open(yaml_path, encoding="utf-8") as f:
260
- data = yaml.safe_load(f)
261
-
259
+ data = _read_yaml_file(yaml_path)
262
260
  if not isinstance(data, dict):
263
261
  return None
264
262
 
265
- # Extract metadata
266
- metadata = PluginMetadata(
267
- name=data.get("name", yaml_path.parent.name),
268
- version=data.get("version", "0.0.0"),
269
- api_version=data.get("api_version", "1.0.0"),
270
- author=data.get("author", ""),
271
- description=data.get("description", ""),
272
- homepage=data.get("homepage", ""),
273
- license=data.get("license", ""),
274
- path=yaml_path.parent,
275
- enabled=data.get("enabled", True),
276
- )
277
-
278
- # Parse dependencies
279
- if "dependencies" in data:
280
- deps = data["dependencies"]
281
- if isinstance(deps, list):
282
- for dep in deps:
283
- if isinstance(dep, dict):
284
- if "plugin" in dep:
285
- metadata.dependencies[dep["plugin"]] = dep.get("version", "*")
286
- elif "package" in dep:
287
- metadata.dependencies[dep["package"]] = dep.get("version", "*")
288
-
289
- # Parse provides
290
- if "provides" in data:
291
- provides = data["provides"]
292
- if isinstance(provides, list):
293
- for item in provides:
294
- if isinstance(item, dict):
295
- for key, value in item.items():
296
- if key not in metadata.provides:
297
- metadata.provides[key] = []
298
- metadata.provides[key].append(value)
299
-
263
+ metadata = _build_plugin_metadata(yaml_path, data)
300
264
  compatible = metadata.is_compatible_with(OSCURA_API_VERSION)
301
265
 
302
266
  return DiscoveredPlugin(
@@ -306,16 +270,96 @@ def _load_plugin_from_yaml(yaml_path: Path) -> DiscoveredPlugin | None:
306
270
  )
307
271
 
308
272
  except Exception as e:
309
- return DiscoveredPlugin(
310
- metadata=PluginMetadata(
311
- name=yaml_path.parent.name,
312
- version="0.0.0",
313
- path=yaml_path.parent,
314
- ),
315
- path=yaml_path.parent,
316
- compatible=False,
317
- load_error=str(e),
318
- )
273
+ return _create_failed_plugin(yaml_path.parent, str(e))
274
+
275
+
276
+ def _read_yaml_file(yaml_path: Path) -> Any:
277
+ """Read and parse YAML file.
278
+
279
+ Args:
280
+ yaml_path: Path to YAML file.
281
+
282
+ Returns:
283
+ Parsed YAML data.
284
+ """
285
+ with open(yaml_path, encoding="utf-8") as f:
286
+ return yaml.safe_load(f)
287
+
288
+
289
+ def _build_plugin_metadata(yaml_path: Path, data: dict[str, Any]) -> PluginMetadata:
290
+ """Build plugin metadata from YAML data.
291
+
292
+ Args:
293
+ yaml_path: Path to plugin.yaml file.
294
+ data: Parsed YAML data.
295
+
296
+ Returns:
297
+ PluginMetadata instance.
298
+ """
299
+ metadata = PluginMetadata(
300
+ name=data.get("name", yaml_path.parent.name),
301
+ version=data.get("version", "0.0.0"),
302
+ api_version=data.get("api_version", "1.0.0"),
303
+ author=data.get("author", ""),
304
+ description=data.get("description", ""),
305
+ homepage=data.get("homepage", ""),
306
+ license=data.get("license", ""),
307
+ path=yaml_path.parent,
308
+ enabled=data.get("enabled", True),
309
+ )
310
+
311
+ _parse_plugin_dependencies(metadata, data)
312
+ _parse_plugin_provides(metadata, data)
313
+
314
+ return metadata
315
+
316
+
317
+ def _parse_plugin_dependencies(metadata: PluginMetadata, data: dict[str, Any]) -> None:
318
+ """Parse plugin dependencies from YAML data.
319
+
320
+ Args:
321
+ metadata: PluginMetadata to update.
322
+ data: Parsed YAML data.
323
+ """
324
+ if "dependencies" not in data:
325
+ return
326
+
327
+ deps = data["dependencies"]
328
+ if not isinstance(deps, list):
329
+ return
330
+
331
+ for dep in deps:
332
+ if not isinstance(dep, dict):
333
+ continue
334
+
335
+ if "plugin" in dep:
336
+ metadata.dependencies[dep["plugin"]] = dep.get("version", "*")
337
+ elif "package" in dep:
338
+ metadata.dependencies[dep["package"]] = dep.get("version", "*")
339
+
340
+
341
+ def _parse_plugin_provides(metadata: PluginMetadata, data: dict[str, Any]) -> None:
342
+ """Parse plugin provides from YAML data.
343
+
344
+ Args:
345
+ metadata: PluginMetadata to update.
346
+ data: Parsed YAML data.
347
+ """
348
+ if "provides" not in data:
349
+ return
350
+
351
+ provides = data["provides"]
352
+ if not isinstance(provides, list):
353
+ return
354
+
355
+ for item in provides:
356
+ if not isinstance(item, dict):
357
+ continue
358
+
359
+ for key, value in item.items():
360
+ if key not in metadata.provides:
361
+ metadata.provides[key] = []
362
+ metadata.provides[key].append(value)
319
363
 
320
364
 
321
365
  def _load_plugin_from_module(module_path: Path) -> DiscoveredPlugin | None:
@@ -328,77 +372,113 @@ def _load_plugin_from_module(module_path: Path) -> DiscoveredPlugin | None:
328
372
  DiscoveredPlugin or None if load fails.
329
373
  """
330
374
  try:
331
- # Add parent to path temporarily
332
375
  parent = str(module_path.parent)
333
- if parent not in sys.path:
376
+ added_path = parent not in sys.path
377
+
378
+ if added_path:
334
379
  sys.path.insert(0, parent)
335
- added_path = True
336
- else:
337
- added_path = False
338
380
 
339
381
  try:
340
- # Import the module
341
- module_name = module_path.name
342
- spec = importlib.util.spec_from_file_location(module_name, module_path / "__init__.py")
382
+ module = _import_plugin_module(module_path)
383
+ if module is None:
384
+ return None
343
385
 
344
- if spec is None or spec.loader is None:
386
+ plugin_class = _find_plugin_class(module)
387
+ if plugin_class is None:
345
388
  return None
346
389
 
347
- module = importlib.util.module_from_spec(spec)
348
- spec.loader.exec_module(module)
390
+ return _create_discovered_plugin(plugin_class, module_path)
349
391
 
350
- # Look for Plugin class
351
- plugin_class = None
392
+ finally:
393
+ if added_path:
394
+ sys.path.remove(parent)
352
395
 
353
- # Check for explicit Plugin class
354
- if hasattr(module, "Plugin"):
355
- plugin_class = module.Plugin
356
- elif hasattr(module, "plugin"):
357
- plugin_class = module.plugin
396
+ except Exception as e:
397
+ return _create_failed_plugin(module_path, str(e))
358
398
 
359
- # Check for any PluginBase subclass
360
- if plugin_class is None:
361
- for attr_name in dir(module):
362
- attr = getattr(module, attr_name)
363
- if (
364
- isinstance(attr, type)
365
- and issubclass(attr, PluginBase)
366
- and attr is not PluginBase
367
- ):
368
- plugin_class = attr
369
- break
370
399
 
371
- if plugin_class is None:
372
- return None
400
+ def _import_plugin_module(module_path: Path) -> Any:
401
+ """Import plugin module from path.
373
402
 
374
- # Create instance and get metadata
375
- instance = plugin_class()
376
- metadata = instance.metadata
377
- metadata.path = module_path
403
+ Args:
404
+ module_path: Path to plugin package.
378
405
 
379
- compatible = metadata.is_compatible_with(OSCURA_API_VERSION)
406
+ Returns:
407
+ Imported module or None.
408
+ """
409
+ module_name = module_path.name
410
+ spec = importlib.util.spec_from_file_location(module_name, module_path / "__init__.py")
380
411
 
381
- return DiscoveredPlugin(
382
- metadata=metadata,
383
- path=module_path,
384
- compatible=compatible,
385
- )
412
+ if spec is None or spec.loader is None:
413
+ return None
386
414
 
387
- finally:
388
- if added_path:
389
- sys.path.remove(parent)
415
+ module = importlib.util.module_from_spec(spec)
416
+ spec.loader.exec_module(module)
417
+ return module
390
418
 
391
- except Exception as e:
392
- return DiscoveredPlugin(
393
- metadata=PluginMetadata(
394
- name=module_path.name,
395
- version="0.0.0",
396
- path=module_path,
397
- ),
398
- path=module_path,
399
- compatible=False,
400
- load_error=str(e),
401
- )
419
+
420
+ def _find_plugin_class(module: Any) -> type | None:
421
+ """Find plugin class in module.
422
+
423
+ Args:
424
+ module: Imported module to search.
425
+
426
+ Returns:
427
+ Plugin class or None.
428
+ """
429
+ # Check explicit names first
430
+ if hasattr(module, "Plugin"):
431
+ plugin_attr = module.Plugin
432
+ if isinstance(plugin_attr, type):
433
+ return plugin_attr
434
+ if hasattr(module, "plugin"):
435
+ plugin_attr = module.plugin
436
+ if isinstance(plugin_attr, type):
437
+ return plugin_attr
438
+
439
+ # Search for PluginBase subclass
440
+ for attr_name in dir(module):
441
+ attr = getattr(module, attr_name)
442
+ if isinstance(attr, type) and issubclass(attr, PluginBase) and attr is not PluginBase:
443
+ return attr
444
+
445
+ return None
446
+
447
+
448
+ def _create_discovered_plugin(plugin_class: type, module_path: Path) -> DiscoveredPlugin:
449
+ """Create DiscoveredPlugin from plugin class.
450
+
451
+ Args:
452
+ plugin_class: Plugin class to instantiate.
453
+ module_path: Path to plugin module.
454
+
455
+ Returns:
456
+ DiscoveredPlugin instance.
457
+ """
458
+ instance = plugin_class()
459
+ metadata = instance.metadata
460
+ metadata.path = module_path
461
+ compatible = metadata.is_compatible_with(OSCURA_API_VERSION)
462
+
463
+ return DiscoveredPlugin(metadata=metadata, path=module_path, compatible=compatible)
464
+
465
+
466
+ def _create_failed_plugin(module_path: Path, error: str) -> DiscoveredPlugin:
467
+ """Create DiscoveredPlugin for failed load.
468
+
469
+ Args:
470
+ module_path: Path to plugin.
471
+ error: Error message.
472
+
473
+ Returns:
474
+ DiscoveredPlugin with error.
475
+ """
476
+ return DiscoveredPlugin(
477
+ metadata=PluginMetadata(name=module_path.name, version="0.0.0", path=module_path),
478
+ path=module_path,
479
+ compatible=False,
480
+ load_error=error,
481
+ )
402
482
 
403
483
 
404
484
  __all__ = [
@@ -20,7 +20,7 @@ from typing import TYPE_CHECKING, Any
20
20
  if TYPE_CHECKING:
21
21
  from collections.abc import Callable
22
22
 
23
- from oscura.plugins.base import PluginBase, PluginMetadata
23
+ from oscura.core.plugins.base import PluginBase, PluginMetadata
24
24
 
25
25
  logger = logging.getLogger(__name__)
26
26