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
@@ -284,90 +284,117 @@ def detect_delimiter(
284
284
  >>> result = detect_delimiter(data)
285
285
  >>> print(f"Delimiter: {result.delimiter!r}")
286
286
  """
287
- # Combine payloads if list
288
- if isinstance(payloads, list | tuple):
289
- data: bytes = b"".join(payloads)
290
- else:
291
- # Type narrowing: payloads is bytes here
292
- data = cast("bytes", payloads)
293
-
287
+ data = _combine_payloads(payloads)
294
288
  if not data:
295
- return DelimiterResult(
296
- delimiter=b"",
297
- delimiter_type="fixed",
298
- confidence=0.0,
299
- occurrences=0,
300
- )
289
+ return DelimiterResult(delimiter=b"", delimiter_type="fixed", confidence=0.0, occurrences=0)
301
290
 
302
- # Default candidates
303
- if candidates is None:
304
- candidates = [
305
- b"\r\n", # CRLF
306
- b"\n", # LF
307
- b"\x00", # Null
308
- b"\r", # CR
309
- b"\x0d\x0a", # CRLF (explicit)
310
- ]
291
+ candidates = candidates or _default_delimiter_candidates()
311
292
 
312
293
  best_result = None
313
294
  best_score = 0.0
314
295
 
315
296
  for delim in candidates:
316
- if len(delim) == 0:
317
- continue
297
+ result, score = _evaluate_delimiter_candidate(data, delim)
298
+ if score > best_score:
299
+ best_score = score
300
+ best_result = result
318
301
 
319
- count = data.count(delim)
320
- if count < 2:
321
- continue
302
+ return best_result or DelimiterResult(
303
+ delimiter=b"", delimiter_type="fixed", confidence=0.0, occurrences=0
304
+ )
322
305
 
323
- # Calculate score based on frequency and regularity
324
- positions = []
325
- pos = 0
326
- while True:
327
- pos = data.find(delim, pos)
328
- if pos == -1:
329
- break
330
- positions.append(pos)
331
- pos += len(delim)
332
306
 
333
- if len(positions) < 2:
334
- continue
307
+ def _combine_payloads(payloads: Sequence[bytes] | bytes) -> bytes:
308
+ """Combine payloads into single bytes object."""
309
+ if isinstance(payloads, list | tuple):
310
+ return b"".join(payloads)
311
+ return cast("bytes", payloads)
335
312
 
336
- # Calculate interval regularity
337
- intervals = [positions[i + 1] - positions[i] for i in range(len(positions) - 1)]
338
- if len(intervals) > 0:
339
- mean_interval = sum(intervals) / len(intervals)
340
- if mean_interval > 0:
341
- variance = sum((x - mean_interval) ** 2 for x in intervals) / len(intervals)
342
- cv = (variance**0.5) / mean_interval
343
- regularity = 1.0 / (1.0 + cv)
344
- else:
345
- regularity = 0.0
346
- else:
347
- regularity = 0.0
348
313
 
349
- # Score combines frequency and regularity
350
- score = count * (0.5 + 0.5 * regularity)
314
+ def _default_delimiter_candidates() -> list[bytes]:
315
+ """Return default delimiter candidates."""
316
+ return [
317
+ b"\r\n", # CRLF
318
+ b"\n", # LF
319
+ b"\x00", # Null
320
+ b"\r", # CR
321
+ b"\x0d\x0a", # CRLF (explicit)
322
+ ]
351
323
 
352
- if score > best_score:
353
- best_score = score
354
- best_result = DelimiterResult(
355
- delimiter=delim,
356
- delimiter_type="fixed",
357
- confidence=min(1.0, regularity * 0.8 + 0.2 * min(1.0, count / 10)),
358
- occurrences=count,
359
- positions=positions,
360
- )
361
324
 
362
- if best_result is None:
363
- return DelimiterResult(
364
- delimiter=b"",
365
- delimiter_type="fixed",
366
- confidence=0.0,
367
- occurrences=0,
368
- )
325
+ def _evaluate_delimiter_candidate(
326
+ data: bytes, delim: bytes
327
+ ) -> tuple[DelimiterResult | None, float]:
328
+ """Evaluate a delimiter candidate and return result with score."""
329
+ if len(delim) == 0:
330
+ return None, 0.0
369
331
 
370
- return best_result
332
+ count = data.count(delim)
333
+ if count < 2:
334
+ return None, 0.0
335
+
336
+ positions = _find_delimiter_positions(data, delim)
337
+ if len(positions) < 2:
338
+ return None, 0.0
339
+
340
+ regularity = _calculate_interval_regularity(positions)
341
+ score = count * (0.5 + 0.5 * regularity)
342
+ confidence = min(1.0, regularity * 0.8 + 0.2 * min(1.0, count / 10))
343
+
344
+ result = DelimiterResult(
345
+ delimiter=delim,
346
+ delimiter_type="fixed",
347
+ confidence=confidence,
348
+ occurrences=count,
349
+ positions=positions,
350
+ )
351
+ return result, score
352
+
353
+
354
+ def _find_delimiter_positions(data: bytes, delim: bytes) -> list[int]:
355
+ """Find all positions of delimiter in data.
356
+
357
+ Args:
358
+ data: Data to search.
359
+ delim: Delimiter bytes to find.
360
+
361
+ Returns:
362
+ List of positions where delimiter occurs.
363
+
364
+ Raises:
365
+ ValueError: If delimiter is empty.
366
+ """
367
+ if len(delim) == 0:
368
+ raise ValueError("Delimiter cannot be empty")
369
+
370
+ positions = []
371
+ pos = 0
372
+ while True:
373
+ pos = data.find(delim, pos)
374
+ if pos == -1:
375
+ break
376
+ positions.append(pos)
377
+ pos += len(delim)
378
+ return positions
379
+
380
+
381
+ def _calculate_interval_regularity(positions: list[int]) -> float:
382
+ """Calculate regularity score from delimiter positions."""
383
+ if len(positions) < 2:
384
+ return 0.0
385
+
386
+ intervals = [positions[i + 1] - positions[i] for i in range(len(positions) - 1)]
387
+ if len(intervals) == 0:
388
+ return 0.0
389
+
390
+ mean_interval = sum(intervals) / len(intervals)
391
+ if mean_interval <= 0:
392
+ return 0.0
393
+
394
+ variance = sum((x - mean_interval) ** 2 for x in intervals) / len(intervals)
395
+ cv = (variance**0.5) / mean_interval
396
+ regularity: float = 1.0 / (1.0 + cv)
397
+ return regularity
371
398
 
372
399
 
373
400
  def detect_length_prefix(
@@ -535,8 +562,27 @@ def _find_pattern_in_data(
535
562
  data: bytes,
536
563
  pattern: bytes | str,
537
564
  pattern_type: str,
565
+ max_matches: int = 100000,
538
566
  ) -> list[tuple[int, bytes]]:
539
- """Find pattern occurrences in data."""
567
+ """Find pattern occurrences in data.
568
+
569
+ Args:
570
+ data: Data to search.
571
+ pattern: Pattern to find.
572
+ pattern_type: Type of pattern (exact, wildcard, regex).
573
+ max_matches: Maximum number of matches to return (default 100000).
574
+
575
+ Returns:
576
+ List of (offset, matched_bytes) tuples.
577
+
578
+ Raises:
579
+ ValueError: If max_matches exceeded (prevents infinite loops) or pattern is empty.
580
+ """
581
+ # Validate pattern is not empty (prevents infinite loops)
582
+ if isinstance(pattern, (str, bytes)):
583
+ if len(pattern) == 0:
584
+ raise ValueError("Pattern cannot be empty")
585
+
540
586
  matches = []
541
587
 
542
588
  if pattern_type == "exact":
@@ -550,6 +596,13 @@ def _find_pattern_in_data(
550
596
  matches.append((pos, pattern))
551
597
  pos += 1
552
598
 
599
+ # Prevent infinite loops from excessive matches
600
+ if len(matches) >= max_matches:
601
+ raise ValueError(
602
+ f"Pattern match limit exceeded ({max_matches} matches). "
603
+ "This may indicate a problematic pattern (e.g., empty or too common)."
604
+ )
605
+
553
606
  elif pattern_type == "wildcard":
554
607
  # Convert wildcard pattern to regex
555
608
  if isinstance(pattern, bytes):
@@ -558,6 +611,11 @@ def _find_pattern_in_data(
558
611
  try:
559
612
  for match in re.finditer(regex_pattern, data, re.DOTALL):
560
613
  matches.append((match.start(), match.group()))
614
+ if len(matches) >= max_matches:
615
+ raise ValueError(
616
+ f"Pattern match limit exceeded ({max_matches} matches). "
617
+ "Wildcard pattern may be too permissive."
618
+ )
561
619
  except re.error:
562
620
  pass
563
621
 
@@ -567,6 +625,11 @@ def _find_pattern_in_data(
567
625
  try:
568
626
  for match in re.finditer(pattern, data, re.DOTALL):
569
627
  matches.append((match.start(), match.group()))
628
+ if len(matches) >= max_matches:
629
+ raise ValueError(
630
+ f"Pattern match limit exceeded ({max_matches} matches). "
631
+ "Regex pattern may be too broad."
632
+ )
570
633
  except re.error:
571
634
  pass
572
635
 
@@ -60,6 +60,9 @@ def stream_file(
60
60
  >>> for chunk in stream_file("large_capture.bin"):
61
61
  ... process_chunk(chunk)
62
62
  """
63
+ if chunk_size <= 0:
64
+ raise ValueError(f"chunk_size must be positive, got {chunk_size}")
65
+
63
66
  path = Path(file_path)
64
67
 
65
68
  with open(path, "rb") as f:
@@ -87,20 +90,26 @@ def stream_records(
87
90
  >>> for record in stream_records("data.bin", record_size=128):
88
91
  ... parse_record(record)
89
92
  """
93
+ if record_size <= 0:
94
+ raise ValueError(f"record_size must be positive, got {record_size}")
95
+
90
96
  if isinstance(file_or_buffer, bytes):
91
97
  buffer: BinaryIO = io.BytesIO(file_or_buffer)
92
98
  should_close = True
93
99
  elif isinstance(file_or_buffer, str | Path):
94
- buffer = open(file_or_buffer, "rb") # noqa: SIM115
100
+ buffer = open(file_or_buffer, "rb")
95
101
  should_close = True
96
102
  else:
97
103
  buffer = file_or_buffer
98
104
  should_close = False
99
105
 
106
+ # Cache record_size to avoid attribute lookup in tight loop
107
+ _record_size = record_size
100
108
  try:
101
109
  while True:
102
- record = buffer.read(record_size)
103
- if len(record) < record_size:
110
+ record = buffer.read(_record_size)
111
+ # Equality check is faster than inequality
112
+ if len(record) != _record_size:
104
113
  break
105
114
  yield record
106
115
  finally:
@@ -128,11 +137,16 @@ def stream_packets(
128
137
  Yields:
129
138
  StreamPacket objects.
130
139
 
140
+ Raises:
141
+ ValueError: If packet size exceeds MAX_PACKET_SIZE (100MB).
142
+
131
143
  Example:
132
144
  >>> header = BinaryParser(">HH") # sync, length
133
145
  >>> for pkt in stream_packets("capture.bin", header_parser=header):
134
146
  ... print(f"Packet: {len(pkt.data)} bytes")
135
147
  """
148
+ MAX_PACKET_SIZE = 100 * 1024 * 1024 # 100MB limit to prevent memory exhaustion
149
+
136
150
  if header_parser is None:
137
151
  # Default: 2-byte big-endian length prefix
138
152
  header_parser = BinaryParser(">H")
@@ -144,33 +158,50 @@ def stream_packets(
144
158
  buffer: BinaryIO = io.BytesIO(file_or_buffer)
145
159
  should_close = True
146
160
  elif isinstance(file_or_buffer, str | Path):
147
- buffer = open(file_or_buffer, "rb") # noqa: SIM115
161
+ buffer = open(file_or_buffer, "rb")
148
162
  should_close = True
149
163
  else:
150
164
  buffer = file_or_buffer
151
165
  should_close = False
152
166
 
167
+ # Cache sizes and flags to avoid repeated lookups in tight loop
168
+ _header_size = header_size
169
+ _length_field = length_field
170
+ _header_included = header_included
171
+
153
172
  try:
154
173
  packet_num = 0
155
174
 
156
175
  while True:
157
176
  # Read header
158
- header_bytes = buffer.read(header_size)
159
- if len(header_bytes) < header_size:
177
+ header_bytes = buffer.read(_header_size)
178
+ if len(header_bytes) != _header_size:
160
179
  break
161
180
 
162
181
  header = header_parser.unpack(header_bytes)
163
- length = header[length_field]
164
-
165
- # Compute payload size
166
- payload_size = length - header_size if header_included else length
167
-
168
- if payload_size < 0:
169
- break
182
+ length = header[_length_field]
183
+
184
+ # Validate packet size before allocation
185
+ total_size = length if _header_included else length + _header_size
186
+ if total_size > MAX_PACKET_SIZE:
187
+ raise ValueError(
188
+ f"Packet size {total_size} bytes exceeds maximum allowed size "
189
+ f"{MAX_PACKET_SIZE} bytes (packet {packet_num + 1})"
190
+ )
191
+
192
+ # Compute payload size with optimized branching
193
+ if _header_included:
194
+ if length < _header_size:
195
+ break
196
+ payload_size = length - _header_size
197
+ else:
198
+ if length < 0:
199
+ break
200
+ payload_size = length
170
201
 
171
202
  # Read payload
172
203
  payload = buffer.read(payload_size)
173
- if len(payload) < payload_size:
204
+ if len(payload) != payload_size:
174
205
  break
175
206
 
176
207
  packet_num += 1
@@ -197,20 +228,29 @@ def stream_delimited(
197
228
  Args:
198
229
  file_or_buffer: Source.
199
230
  delimiter: Record delimiter (default newline).
200
- max_record_size: Maximum record size (default 1MB).
231
+ max_record_size: Maximum record size (default 1MB). Records exceeding
232
+ this size will raise ValueError instead of being silently truncated.
201
233
 
202
234
  Yields:
203
235
  Records as bytes (without delimiter).
204
236
 
237
+ Raises:
238
+ ValueError: If record exceeds max_record_size.
239
+
205
240
  Example:
206
241
  >>> for line in stream_delimited("log.txt", b"\\n"):
207
242
  ... process_line(line)
208
243
  """
244
+ if max_record_size <= 0:
245
+ raise ValueError(f"max_record_size must be positive, got {max_record_size}")
246
+ if not delimiter:
247
+ raise ValueError("delimiter cannot be empty")
248
+
209
249
  if isinstance(file_or_buffer, bytes):
210
250
  buffer: BinaryIO = io.BytesIO(file_or_buffer)
211
251
  should_close = True
212
252
  elif isinstance(file_or_buffer, str | Path):
213
- buffer = open(file_or_buffer, "rb") # noqa: SIM115
253
+ buffer = open(file_or_buffer, "rb")
214
254
  should_close = True
215
255
  else:
216
256
  buffer = file_or_buffer
@@ -223,6 +263,12 @@ def stream_delimited(
223
263
  chunk = buffer.read(65536)
224
264
  if not chunk:
225
265
  if partial:
266
+ # Check final partial record
267
+ if len(partial) > max_record_size:
268
+ raise ValueError(
269
+ f"Record size {len(partial)} bytes exceeds maximum allowed size "
270
+ f"{max_record_size} bytes"
271
+ )
226
272
  yield partial
227
273
  break
228
274
 
@@ -231,16 +277,22 @@ def stream_delimited(
231
277
 
232
278
  # Yield complete records
233
279
  for part in parts[:-1]:
234
- if len(part) <= max_record_size:
235
- yield part
280
+ if len(part) > max_record_size:
281
+ raise ValueError(
282
+ f"Record size {len(part)} bytes exceeds maximum allowed size "
283
+ f"{max_record_size} bytes"
284
+ )
285
+ yield part
236
286
 
237
287
  # Keep partial record for next iteration
238
288
  partial = parts[-1]
239
289
 
240
- # Guard against memory issues
290
+ # Guard against memory issues - raise error instead of truncating
241
291
  if len(partial) > max_record_size:
242
- yield partial[:max_record_size]
243
- partial = b""
292
+ raise ValueError(
293
+ f"Partial record size {len(partial)} bytes exceeds maximum allowed size "
294
+ f"{max_record_size} bytes"
295
+ )
244
296
 
245
297
  finally:
246
298
  if should_close:
@@ -299,6 +351,9 @@ def batch(
299
351
  >>> for batch_items in batch(stream_packets(f), size=100):
300
352
  ... process_batch(batch_items)
301
353
  """
354
+ if size <= 0:
355
+ raise ValueError(f"size must be positive, got {size}")
356
+
302
357
  current_batch: list[T] = []
303
358
 
304
359
  for item in source:
@@ -321,12 +376,15 @@ def take(source: Iterator[T], n: int) -> Iterator[T]:
321
376
  Yields:
322
377
  First n items.
323
378
  """
379
+ if n < 0:
380
+ raise ValueError(f"n must be non-negative, got {n}")
381
+
324
382
  count = 0
325
383
  for item in source:
326
384
  if count >= n:
327
385
  break
328
386
  yield item
329
- count += 1 # noqa: SIM113
387
+ count += 1
330
388
 
331
389
 
332
390
  def skip(source: Iterator[T], n: int) -> Iterator[T]:
@@ -339,11 +397,14 @@ def skip(source: Iterator[T], n: int) -> Iterator[T]:
339
397
  Yields:
340
398
  Items after first n.
341
399
  """
400
+ if n < 0:
401
+ raise ValueError(f"n must be non-negative, got {n}")
402
+
342
403
  count = 0
343
404
  for item in source:
344
405
  if count >= n:
345
406
  yield item
346
- count += 1 # noqa: SIM113
407
+ count += 1
347
408
 
348
409
 
349
410
  __all__ = [
@@ -127,21 +127,42 @@ def find_motifs(
127
127
  return results
128
128
 
129
129
 
130
- def extract_motif(data: Any, start: int, length: int) -> NDArray[np.generic]:
130
+ def extract_motif(data: Any, start: int = 0, length: int | None = None) -> NDArray[np.generic]:
131
131
  """Extract a motif from data.
132
132
 
133
133
  Args:
134
- data: Input data array.
135
- start: Start index.
136
- length: Length to extract.
134
+ data: Input data array. If start and length not provided, attempts to detect and extract
135
+ the first repeating motif automatically.
136
+ start: Start index for extraction (default: 0).
137
+ length: Length to extract. If None, attempts to detect motif length automatically.
137
138
 
138
139
  Returns:
139
140
  Extracted motif as numpy array.
141
+
142
+ Raises:
143
+ ValueError: If automatic detection fails and no length specified.
140
144
  """
141
145
  import numpy as np
142
146
 
143
147
  data_arr = np.asarray(data)
144
- result = data_arr[start : start + length]
148
+
149
+ # If length not specified, try to detect motif automatically
150
+ if length is None:
151
+ # Try to find repeating pattern using period detection
152
+ try:
153
+ period_result = detect_period(data_arr)
154
+ if period_result is not None and hasattr(period_result, "period"):
155
+ length = int(period_result.period)
156
+ else:
157
+ # Default to 8 samples if detection fails
158
+ length = min(8, len(data_arr))
159
+ except Exception:
160
+ # Fallback to reasonable default
161
+ length = min(8, len(data_arr))
162
+
163
+ # Ensure we don't exceed array bounds
164
+ end = min(start + length, len(data_arr))
165
+ result = data_arr[start:end]
145
166
  return result
146
167
 
147
168