oscura 0.5.0__py3-none-any.whl → 0.6.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 (513) 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/__init__.py +0 -48
  5. oscura/analyzers/digital/edges.py +325 -65
  6. oscura/analyzers/digital/extraction.py +0 -195
  7. oscura/analyzers/digital/quality.py +293 -166
  8. oscura/analyzers/digital/timing.py +260 -115
  9. oscura/analyzers/digital/timing_numba.py +334 -0
  10. oscura/analyzers/entropy.py +605 -0
  11. oscura/analyzers/eye/diagram.py +176 -109
  12. oscura/analyzers/eye/metrics.py +5 -5
  13. oscura/analyzers/jitter/__init__.py +6 -4
  14. oscura/analyzers/jitter/ber.py +52 -52
  15. oscura/analyzers/jitter/classification.py +156 -0
  16. oscura/analyzers/jitter/decomposition.py +163 -113
  17. oscura/analyzers/jitter/spectrum.py +80 -64
  18. oscura/analyzers/ml/__init__.py +39 -0
  19. oscura/analyzers/ml/features.py +600 -0
  20. oscura/analyzers/ml/signal_classifier.py +604 -0
  21. oscura/analyzers/packet/daq.py +246 -158
  22. oscura/analyzers/packet/parser.py +12 -1
  23. oscura/analyzers/packet/payload.py +50 -2110
  24. oscura/analyzers/packet/payload_analysis.py +361 -181
  25. oscura/analyzers/packet/payload_patterns.py +133 -70
  26. oscura/analyzers/packet/stream.py +84 -23
  27. oscura/analyzers/patterns/__init__.py +26 -5
  28. oscura/analyzers/patterns/anomaly_detection.py +908 -0
  29. oscura/analyzers/patterns/clustering.py +169 -108
  30. oscura/analyzers/patterns/clustering_optimized.py +227 -0
  31. oscura/analyzers/patterns/discovery.py +1 -1
  32. oscura/analyzers/patterns/matching.py +581 -197
  33. oscura/analyzers/patterns/pattern_mining.py +778 -0
  34. oscura/analyzers/patterns/periodic.py +121 -38
  35. oscura/analyzers/patterns/sequences.py +175 -78
  36. oscura/analyzers/power/conduction.py +1 -1
  37. oscura/analyzers/power/soa.py +6 -6
  38. oscura/analyzers/power/switching.py +250 -110
  39. oscura/analyzers/protocol/__init__.py +17 -1
  40. oscura/analyzers/protocols/__init__.py +1 -22
  41. oscura/analyzers/protocols/base.py +6 -6
  42. oscura/analyzers/protocols/ble/__init__.py +38 -0
  43. oscura/analyzers/protocols/ble/analyzer.py +809 -0
  44. oscura/analyzers/protocols/ble/uuids.py +288 -0
  45. oscura/analyzers/protocols/can.py +257 -127
  46. oscura/analyzers/protocols/can_fd.py +107 -80
  47. oscura/analyzers/protocols/flexray.py +139 -80
  48. oscura/analyzers/protocols/hdlc.py +93 -58
  49. oscura/analyzers/protocols/i2c.py +247 -106
  50. oscura/analyzers/protocols/i2s.py +138 -86
  51. oscura/analyzers/protocols/industrial/__init__.py +40 -0
  52. oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
  53. oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
  54. oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
  55. oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
  56. oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
  57. oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
  58. oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
  59. oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
  60. oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
  61. oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
  62. oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
  63. oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
  64. oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
  65. oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
  66. oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
  67. oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
  68. oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
  69. oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
  70. oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
  71. oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
  72. oscura/analyzers/protocols/jtag.py +180 -98
  73. oscura/analyzers/protocols/lin.py +219 -114
  74. oscura/analyzers/protocols/manchester.py +4 -4
  75. oscura/analyzers/protocols/onewire.py +253 -149
  76. oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
  77. oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
  78. oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
  79. oscura/analyzers/protocols/spi.py +192 -95
  80. oscura/analyzers/protocols/swd.py +321 -167
  81. oscura/analyzers/protocols/uart.py +267 -125
  82. oscura/analyzers/protocols/usb.py +235 -131
  83. oscura/analyzers/side_channel/power.py +17 -12
  84. oscura/analyzers/signal/__init__.py +15 -0
  85. oscura/analyzers/signal/timing_analysis.py +1086 -0
  86. oscura/analyzers/signal_integrity/__init__.py +4 -1
  87. oscura/analyzers/signal_integrity/sparams.py +2 -19
  88. oscura/analyzers/spectral/chunked.py +129 -60
  89. oscura/analyzers/spectral/chunked_fft.py +300 -94
  90. oscura/analyzers/spectral/chunked_wavelet.py +100 -80
  91. oscura/analyzers/statistical/checksum.py +376 -217
  92. oscura/analyzers/statistical/classification.py +229 -107
  93. oscura/analyzers/statistical/entropy.py +78 -53
  94. oscura/analyzers/statistics/correlation.py +407 -211
  95. oscura/analyzers/statistics/outliers.py +2 -2
  96. oscura/analyzers/statistics/streaming.py +30 -5
  97. oscura/analyzers/validation.py +216 -101
  98. oscura/analyzers/waveform/measurements.py +9 -0
  99. oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
  100. oscura/analyzers/waveform/spectral.py +500 -228
  101. oscura/api/__init__.py +31 -5
  102. oscura/api/dsl/__init__.py +582 -0
  103. oscura/{dsl → api/dsl}/commands.py +43 -76
  104. oscura/{dsl → api/dsl}/interpreter.py +26 -51
  105. oscura/{dsl → api/dsl}/parser.py +107 -77
  106. oscura/{dsl → api/dsl}/repl.py +2 -2
  107. oscura/api/dsl.py +1 -1
  108. oscura/{integrations → api/integrations}/__init__.py +1 -1
  109. oscura/{integrations → api/integrations}/llm.py +201 -102
  110. oscura/api/operators.py +3 -3
  111. oscura/api/optimization.py +144 -30
  112. oscura/api/rest_server.py +921 -0
  113. oscura/api/server/__init__.py +17 -0
  114. oscura/api/server/dashboard.py +850 -0
  115. oscura/api/server/static/README.md +34 -0
  116. oscura/api/server/templates/base.html +181 -0
  117. oscura/api/server/templates/export.html +120 -0
  118. oscura/api/server/templates/home.html +284 -0
  119. oscura/api/server/templates/protocols.html +58 -0
  120. oscura/api/server/templates/reports.html +43 -0
  121. oscura/api/server/templates/session_detail.html +89 -0
  122. oscura/api/server/templates/sessions.html +83 -0
  123. oscura/api/server/templates/waveforms.html +73 -0
  124. oscura/automotive/__init__.py +8 -1
  125. oscura/automotive/can/__init__.py +10 -0
  126. oscura/automotive/can/checksum.py +3 -1
  127. oscura/automotive/can/dbc_generator.py +590 -0
  128. oscura/automotive/can/message_wrapper.py +121 -74
  129. oscura/automotive/can/patterns.py +98 -21
  130. oscura/automotive/can/session.py +292 -56
  131. oscura/automotive/can/state_machine.py +6 -3
  132. oscura/automotive/can/stimulus_response.py +97 -75
  133. oscura/automotive/dbc/__init__.py +10 -2
  134. oscura/automotive/dbc/generator.py +84 -56
  135. oscura/automotive/dbc/parser.py +6 -6
  136. oscura/automotive/dtc/data.json +2763 -0
  137. oscura/automotive/dtc/database.py +2 -2
  138. oscura/automotive/flexray/__init__.py +31 -0
  139. oscura/automotive/flexray/analyzer.py +504 -0
  140. oscura/automotive/flexray/crc.py +185 -0
  141. oscura/automotive/flexray/fibex.py +449 -0
  142. oscura/automotive/j1939/__init__.py +45 -8
  143. oscura/automotive/j1939/analyzer.py +605 -0
  144. oscura/automotive/j1939/spns.py +326 -0
  145. oscura/automotive/j1939/transport.py +306 -0
  146. oscura/automotive/lin/__init__.py +47 -0
  147. oscura/automotive/lin/analyzer.py +612 -0
  148. oscura/automotive/loaders/blf.py +13 -2
  149. oscura/automotive/loaders/csv_can.py +143 -72
  150. oscura/automotive/loaders/dispatcher.py +50 -2
  151. oscura/automotive/loaders/mdf.py +86 -45
  152. oscura/automotive/loaders/pcap.py +111 -61
  153. oscura/automotive/uds/__init__.py +4 -0
  154. oscura/automotive/uds/analyzer.py +725 -0
  155. oscura/automotive/uds/decoder.py +140 -58
  156. oscura/automotive/uds/models.py +7 -1
  157. oscura/automotive/visualization.py +1 -1
  158. oscura/cli/analyze.py +348 -0
  159. oscura/cli/batch.py +142 -122
  160. oscura/cli/benchmark.py +275 -0
  161. oscura/cli/characterize.py +137 -82
  162. oscura/cli/compare.py +224 -131
  163. oscura/cli/completion.py +250 -0
  164. oscura/cli/config_cmd.py +361 -0
  165. oscura/cli/decode.py +164 -87
  166. oscura/cli/export.py +286 -0
  167. oscura/cli/main.py +115 -31
  168. oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
  169. oscura/{onboarding → cli/onboarding}/help.py +80 -58
  170. oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
  171. oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
  172. oscura/cli/progress.py +147 -0
  173. oscura/cli/shell.py +157 -135
  174. oscura/cli/validate_cmd.py +204 -0
  175. oscura/cli/visualize.py +158 -0
  176. oscura/convenience.py +125 -79
  177. oscura/core/__init__.py +4 -2
  178. oscura/core/backend_selector.py +3 -3
  179. oscura/core/cache.py +126 -15
  180. oscura/core/cancellation.py +1 -1
  181. oscura/{config → core/config}/__init__.py +20 -11
  182. oscura/{config → core/config}/defaults.py +1 -1
  183. oscura/{config → core/config}/loader.py +7 -5
  184. oscura/{config → core/config}/memory.py +5 -5
  185. oscura/{config → core/config}/migration.py +1 -1
  186. oscura/{config → core/config}/pipeline.py +99 -23
  187. oscura/{config → core/config}/preferences.py +1 -1
  188. oscura/{config → core/config}/protocol.py +3 -3
  189. oscura/{config → core/config}/schema.py +426 -272
  190. oscura/{config → core/config}/settings.py +1 -1
  191. oscura/{config → core/config}/thresholds.py +195 -153
  192. oscura/core/correlation.py +5 -6
  193. oscura/core/cross_domain.py +0 -2
  194. oscura/core/debug.py +9 -5
  195. oscura/{extensibility → core/extensibility}/docs.py +158 -70
  196. oscura/{extensibility → core/extensibility}/extensions.py +160 -76
  197. oscura/{extensibility → core/extensibility}/logging.py +1 -1
  198. oscura/{extensibility → core/extensibility}/measurements.py +1 -1
  199. oscura/{extensibility → core/extensibility}/plugins.py +1 -1
  200. oscura/{extensibility → core/extensibility}/templates.py +73 -3
  201. oscura/{extensibility → core/extensibility}/validation.py +1 -1
  202. oscura/core/gpu_backend.py +11 -7
  203. oscura/core/log_query.py +101 -11
  204. oscura/core/logging.py +126 -54
  205. oscura/core/logging_advanced.py +5 -5
  206. oscura/core/memory_limits.py +108 -70
  207. oscura/core/memory_monitor.py +2 -2
  208. oscura/core/memory_progress.py +7 -7
  209. oscura/core/memory_warnings.py +1 -1
  210. oscura/core/numba_backend.py +13 -13
  211. oscura/{plugins → core/plugins}/__init__.py +9 -9
  212. oscura/{plugins → core/plugins}/base.py +7 -7
  213. oscura/{plugins → core/plugins}/cli.py +3 -3
  214. oscura/{plugins → core/plugins}/discovery.py +186 -106
  215. oscura/{plugins → core/plugins}/lifecycle.py +1 -1
  216. oscura/{plugins → core/plugins}/manager.py +7 -7
  217. oscura/{plugins → core/plugins}/registry.py +3 -3
  218. oscura/{plugins → core/plugins}/versioning.py +1 -1
  219. oscura/core/progress.py +16 -1
  220. oscura/core/provenance.py +8 -2
  221. oscura/{schemas → core/schemas}/__init__.py +2 -2
  222. oscura/core/schemas/bus_configuration.json +322 -0
  223. oscura/core/schemas/device_mapping.json +182 -0
  224. oscura/core/schemas/packet_format.json +418 -0
  225. oscura/core/schemas/protocol_definition.json +363 -0
  226. oscura/core/types.py +4 -0
  227. oscura/core/uncertainty.py +3 -3
  228. oscura/correlation/__init__.py +52 -0
  229. oscura/correlation/multi_protocol.py +811 -0
  230. oscura/discovery/auto_decoder.py +117 -35
  231. oscura/discovery/comparison.py +191 -86
  232. oscura/discovery/quality_validator.py +155 -68
  233. oscura/discovery/signal_detector.py +196 -79
  234. oscura/export/__init__.py +18 -20
  235. oscura/export/kaitai_struct.py +513 -0
  236. oscura/export/scapy_layer.py +801 -0
  237. oscura/export/wireshark/README.md +15 -15
  238. oscura/export/wireshark/generator.py +1 -1
  239. oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
  240. oscura/export/wireshark_dissector.py +746 -0
  241. oscura/guidance/wizard.py +207 -111
  242. oscura/hardware/__init__.py +19 -0
  243. oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
  244. oscura/{acquisition → hardware/acquisition}/file.py +2 -2
  245. oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
  246. oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
  247. oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
  248. oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
  249. oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
  250. oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
  251. oscura/hardware/firmware/__init__.py +29 -0
  252. oscura/hardware/firmware/pattern_recognition.py +874 -0
  253. oscura/hardware/hal_detector.py +736 -0
  254. oscura/hardware/security/__init__.py +37 -0
  255. oscura/hardware/security/side_channel_detector.py +1126 -0
  256. oscura/inference/__init__.py +4 -0
  257. oscura/inference/active_learning/README.md +7 -7
  258. oscura/inference/active_learning/observation_table.py +4 -1
  259. oscura/inference/alignment.py +216 -123
  260. oscura/inference/bayesian.py +113 -33
  261. oscura/inference/crc_reverse.py +101 -55
  262. oscura/inference/logic.py +6 -2
  263. oscura/inference/message_format.py +342 -183
  264. oscura/inference/protocol.py +95 -44
  265. oscura/inference/protocol_dsl.py +180 -82
  266. oscura/inference/signal_intelligence.py +1439 -706
  267. oscura/inference/spectral.py +99 -57
  268. oscura/inference/state_machine.py +810 -158
  269. oscura/inference/stream.py +270 -110
  270. oscura/iot/__init__.py +34 -0
  271. oscura/iot/coap/__init__.py +32 -0
  272. oscura/iot/coap/analyzer.py +668 -0
  273. oscura/iot/coap/options.py +212 -0
  274. oscura/iot/lorawan/__init__.py +21 -0
  275. oscura/iot/lorawan/crypto.py +206 -0
  276. oscura/iot/lorawan/decoder.py +801 -0
  277. oscura/iot/lorawan/mac_commands.py +341 -0
  278. oscura/iot/mqtt/__init__.py +27 -0
  279. oscura/iot/mqtt/analyzer.py +999 -0
  280. oscura/iot/mqtt/properties.py +315 -0
  281. oscura/iot/zigbee/__init__.py +31 -0
  282. oscura/iot/zigbee/analyzer.py +615 -0
  283. oscura/iot/zigbee/security.py +153 -0
  284. oscura/iot/zigbee/zcl.py +349 -0
  285. oscura/jupyter/display.py +125 -45
  286. oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
  287. oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
  288. oscura/jupyter/exploratory/fuzzy.py +746 -0
  289. oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
  290. oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
  291. oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
  292. oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
  293. oscura/jupyter/exploratory/sync.py +612 -0
  294. oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
  295. oscura/jupyter/magic.py +4 -4
  296. oscura/{ui → jupyter/ui}/__init__.py +2 -2
  297. oscura/{ui → jupyter/ui}/formatters.py +3 -3
  298. oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
  299. oscura/loaders/__init__.py +171 -63
  300. oscura/loaders/binary.py +88 -1
  301. oscura/loaders/chipwhisperer.py +153 -137
  302. oscura/loaders/configurable.py +208 -86
  303. oscura/loaders/csv_loader.py +458 -215
  304. oscura/loaders/hdf5_loader.py +278 -119
  305. oscura/loaders/lazy.py +87 -54
  306. oscura/loaders/mmap_loader.py +1 -1
  307. oscura/loaders/numpy_loader.py +253 -116
  308. oscura/loaders/pcap.py +226 -151
  309. oscura/loaders/rigol.py +110 -49
  310. oscura/loaders/sigrok.py +201 -78
  311. oscura/loaders/tdms.py +81 -58
  312. oscura/loaders/tektronix.py +291 -174
  313. oscura/loaders/touchstone.py +182 -87
  314. oscura/loaders/vcd.py +215 -117
  315. oscura/loaders/wav.py +155 -68
  316. oscura/reporting/__init__.py +9 -7
  317. oscura/reporting/analyze.py +352 -146
  318. oscura/reporting/argument_preparer.py +69 -14
  319. oscura/reporting/auto_report.py +97 -61
  320. oscura/reporting/batch.py +131 -58
  321. oscura/reporting/chart_selection.py +57 -45
  322. oscura/reporting/comparison.py +63 -17
  323. oscura/reporting/content/executive.py +76 -24
  324. oscura/reporting/core_formats/multi_format.py +11 -8
  325. oscura/reporting/engine.py +312 -158
  326. oscura/reporting/enhanced_reports.py +949 -0
  327. oscura/reporting/export.py +86 -43
  328. oscura/reporting/formatting/numbers.py +69 -42
  329. oscura/reporting/html.py +139 -58
  330. oscura/reporting/index.py +137 -65
  331. oscura/reporting/output.py +158 -67
  332. oscura/reporting/pdf.py +67 -102
  333. oscura/reporting/plots.py +191 -112
  334. oscura/reporting/sections.py +88 -47
  335. oscura/reporting/standards.py +104 -61
  336. oscura/reporting/summary_generator.py +75 -55
  337. oscura/reporting/tables.py +138 -54
  338. oscura/reporting/templates/enhanced/protocol_re.html +525 -0
  339. oscura/reporting/templates/index.md +13 -13
  340. oscura/sessions/__init__.py +14 -23
  341. oscura/sessions/base.py +3 -3
  342. oscura/sessions/blackbox.py +106 -10
  343. oscura/sessions/generic.py +2 -2
  344. oscura/sessions/legacy.py +783 -0
  345. oscura/side_channel/__init__.py +63 -0
  346. oscura/side_channel/dpa.py +1025 -0
  347. oscura/utils/__init__.py +15 -1
  348. oscura/utils/autodetect.py +1 -5
  349. oscura/utils/bitwise.py +118 -0
  350. oscura/{builders → utils/builders}/__init__.py +1 -1
  351. oscura/{comparison → utils/comparison}/__init__.py +6 -6
  352. oscura/{comparison → utils/comparison}/compare.py +202 -101
  353. oscura/{comparison → utils/comparison}/golden.py +83 -63
  354. oscura/{comparison → utils/comparison}/limits.py +313 -89
  355. oscura/{comparison → utils/comparison}/mask.py +151 -45
  356. oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
  357. oscura/{comparison → utils/comparison}/visualization.py +147 -89
  358. oscura/{component → utils/component}/__init__.py +3 -3
  359. oscura/{component → utils/component}/impedance.py +122 -58
  360. oscura/{component → utils/component}/reactive.py +165 -168
  361. oscura/{component → utils/component}/transmission_line.py +3 -3
  362. oscura/{filtering → utils/filtering}/__init__.py +6 -6
  363. oscura/{filtering → utils/filtering}/base.py +1 -1
  364. oscura/{filtering → utils/filtering}/convenience.py +2 -2
  365. oscura/{filtering → utils/filtering}/design.py +169 -93
  366. oscura/{filtering → utils/filtering}/filters.py +2 -2
  367. oscura/{filtering → utils/filtering}/introspection.py +2 -2
  368. oscura/utils/geometry.py +31 -0
  369. oscura/utils/imports.py +184 -0
  370. oscura/utils/lazy.py +1 -1
  371. oscura/{math → utils/math}/__init__.py +2 -2
  372. oscura/{math → utils/math}/arithmetic.py +114 -48
  373. oscura/{math → utils/math}/interpolation.py +139 -106
  374. oscura/utils/memory.py +129 -66
  375. oscura/utils/memory_advanced.py +92 -9
  376. oscura/utils/memory_extensions.py +10 -8
  377. oscura/{optimization → utils/optimization}/__init__.py +1 -1
  378. oscura/{optimization → utils/optimization}/search.py +2 -2
  379. oscura/utils/performance/__init__.py +58 -0
  380. oscura/utils/performance/caching.py +889 -0
  381. oscura/utils/performance/lsh_clustering.py +333 -0
  382. oscura/utils/performance/memory_optimizer.py +699 -0
  383. oscura/utils/performance/optimizations.py +675 -0
  384. oscura/utils/performance/parallel.py +654 -0
  385. oscura/utils/performance/profiling.py +661 -0
  386. oscura/{pipeline → utils/pipeline}/base.py +1 -1
  387. oscura/{pipeline → utils/pipeline}/composition.py +11 -3
  388. oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
  389. oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
  390. oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
  391. oscura/{search → utils/search}/__init__.py +3 -3
  392. oscura/{search → utils/search}/anomaly.py +188 -58
  393. oscura/utils/search/context.py +294 -0
  394. oscura/{search → utils/search}/pattern.py +138 -10
  395. oscura/utils/serial.py +51 -0
  396. oscura/utils/storage/__init__.py +61 -0
  397. oscura/utils/storage/database.py +1166 -0
  398. oscura/{streaming → utils/streaming}/chunked.py +302 -143
  399. oscura/{streaming → utils/streaming}/progressive.py +1 -1
  400. oscura/{streaming → utils/streaming}/realtime.py +3 -2
  401. oscura/{triggering → utils/triggering}/__init__.py +6 -6
  402. oscura/{triggering → utils/triggering}/base.py +6 -6
  403. oscura/{triggering → utils/triggering}/edge.py +2 -2
  404. oscura/{triggering → utils/triggering}/pattern.py +2 -2
  405. oscura/{triggering → utils/triggering}/pulse.py +115 -74
  406. oscura/{triggering → utils/triggering}/window.py +2 -2
  407. oscura/utils/validation.py +32 -0
  408. oscura/validation/__init__.py +121 -0
  409. oscura/{compliance → validation/compliance}/__init__.py +5 -5
  410. oscura/{compliance → validation/compliance}/advanced.py +5 -5
  411. oscura/{compliance → validation/compliance}/masks.py +1 -1
  412. oscura/{compliance → validation/compliance}/reporting.py +127 -53
  413. oscura/{compliance → validation/compliance}/testing.py +114 -52
  414. oscura/validation/compliance_tests.py +915 -0
  415. oscura/validation/fuzzer.py +990 -0
  416. oscura/validation/grammar_tests.py +596 -0
  417. oscura/validation/grammar_validator.py +904 -0
  418. oscura/validation/hil_testing.py +977 -0
  419. oscura/{quality → validation/quality}/__init__.py +4 -4
  420. oscura/{quality → validation/quality}/ensemble.py +251 -171
  421. oscura/{quality → validation/quality}/explainer.py +3 -3
  422. oscura/{quality → validation/quality}/scoring.py +1 -1
  423. oscura/{quality → validation/quality}/warnings.py +4 -4
  424. oscura/validation/regression_suite.py +808 -0
  425. oscura/validation/replay.py +788 -0
  426. oscura/{testing → validation/testing}/__init__.py +2 -2
  427. oscura/{testing → validation/testing}/synthetic.py +5 -5
  428. oscura/visualization/__init__.py +9 -0
  429. oscura/visualization/accessibility.py +1 -1
  430. oscura/visualization/annotations.py +64 -67
  431. oscura/visualization/colors.py +7 -7
  432. oscura/visualization/digital.py +180 -81
  433. oscura/visualization/eye.py +236 -85
  434. oscura/visualization/interactive.py +320 -143
  435. oscura/visualization/jitter.py +587 -247
  436. oscura/visualization/layout.py +169 -134
  437. oscura/visualization/optimization.py +103 -52
  438. oscura/visualization/palettes.py +1 -1
  439. oscura/visualization/power.py +427 -211
  440. oscura/visualization/power_extended.py +626 -297
  441. oscura/visualization/presets.py +2 -0
  442. oscura/visualization/protocols.py +495 -181
  443. oscura/visualization/render.py +79 -63
  444. oscura/visualization/reverse_engineering.py +171 -124
  445. oscura/visualization/signal_integrity.py +460 -279
  446. oscura/visualization/specialized.py +190 -100
  447. oscura/visualization/spectral.py +670 -255
  448. oscura/visualization/thumbnails.py +166 -137
  449. oscura/visualization/waveform.py +150 -63
  450. oscura/workflows/__init__.py +3 -0
  451. oscura/{batch → workflows/batch}/__init__.py +5 -5
  452. oscura/{batch → workflows/batch}/advanced.py +150 -75
  453. oscura/workflows/batch/aggregate.py +531 -0
  454. oscura/workflows/batch/analyze.py +236 -0
  455. oscura/{batch → workflows/batch}/logging.py +2 -2
  456. oscura/{batch → workflows/batch}/metrics.py +1 -1
  457. oscura/workflows/complete_re.py +1144 -0
  458. oscura/workflows/compliance.py +44 -54
  459. oscura/workflows/digital.py +197 -51
  460. oscura/workflows/legacy/__init__.py +12 -0
  461. oscura/{workflow → workflows/legacy}/dag.py +4 -1
  462. oscura/workflows/multi_trace.py +9 -9
  463. oscura/workflows/power.py +42 -62
  464. oscura/workflows/protocol.py +82 -49
  465. oscura/workflows/reverse_engineering.py +351 -150
  466. oscura/workflows/signal_integrity.py +157 -82
  467. oscura-0.6.0.dist-info/METADATA +643 -0
  468. oscura-0.6.0.dist-info/RECORD +590 -0
  469. oscura/analyzers/digital/ic_database.py +0 -498
  470. oscura/analyzers/digital/timing_paths.py +0 -339
  471. oscura/analyzers/digital/vintage.py +0 -377
  472. oscura/analyzers/digital/vintage_result.py +0 -148
  473. oscura/analyzers/protocols/parallel_bus.py +0 -449
  474. oscura/batch/aggregate.py +0 -300
  475. oscura/batch/analyze.py +0 -139
  476. oscura/dsl/__init__.py +0 -73
  477. oscura/exceptions.py +0 -59
  478. oscura/exploratory/fuzzy.py +0 -513
  479. oscura/exploratory/sync.py +0 -384
  480. oscura/export/wavedrom.py +0 -430
  481. oscura/exporters/__init__.py +0 -94
  482. oscura/exporters/csv.py +0 -303
  483. oscura/exporters/exporters.py +0 -44
  484. oscura/exporters/hdf5.py +0 -217
  485. oscura/exporters/html_export.py +0 -701
  486. oscura/exporters/json_export.py +0 -338
  487. oscura/exporters/markdown_export.py +0 -367
  488. oscura/exporters/matlab_export.py +0 -354
  489. oscura/exporters/npz_export.py +0 -219
  490. oscura/exporters/spice_export.py +0 -210
  491. oscura/exporters/vintage_logic_csv.py +0 -247
  492. oscura/reporting/vintage_logic_report.py +0 -523
  493. oscura/search/context.py +0 -149
  494. oscura/session/__init__.py +0 -34
  495. oscura/session/annotations.py +0 -289
  496. oscura/session/history.py +0 -313
  497. oscura/session/session.py +0 -520
  498. oscura/visualization/digital_advanced.py +0 -718
  499. oscura/visualization/figure_manager.py +0 -156
  500. oscura/workflow/__init__.py +0 -13
  501. oscura-0.5.0.dist-info/METADATA +0 -407
  502. oscura-0.5.0.dist-info/RECORD +0 -486
  503. /oscura/core/{config.py → config/legacy.py} +0 -0
  504. /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
  505. /oscura/{extensibility → core/extensibility}/registry.py +0 -0
  506. /oscura/{plugins → core/plugins}/isolation.py +0 -0
  507. /oscura/{builders → utils/builders}/signal_builder.py +0 -0
  508. /oscura/{optimization → utils/optimization}/parallel.py +0 -0
  509. /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
  510. /oscura/{streaming → utils/streaming}/__init__.py +0 -0
  511. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/WHEEL +0 -0
  512. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/entry_points.txt +0 -0
  513. {oscura-0.5.0.dist-info → oscura-0.6.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