oscura 0.0.1__py3-none-any.whl → 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (465) hide show
  1. oscura/__init__.py +813 -8
  2. oscura/__main__.py +392 -0
  3. oscura/analyzers/__init__.py +37 -0
  4. oscura/analyzers/digital/__init__.py +177 -0
  5. oscura/analyzers/digital/bus.py +691 -0
  6. oscura/analyzers/digital/clock.py +805 -0
  7. oscura/analyzers/digital/correlation.py +720 -0
  8. oscura/analyzers/digital/edges.py +632 -0
  9. oscura/analyzers/digital/extraction.py +413 -0
  10. oscura/analyzers/digital/quality.py +878 -0
  11. oscura/analyzers/digital/signal_quality.py +877 -0
  12. oscura/analyzers/digital/thresholds.py +708 -0
  13. oscura/analyzers/digital/timing.py +1104 -0
  14. oscura/analyzers/eye/__init__.py +46 -0
  15. oscura/analyzers/eye/diagram.py +434 -0
  16. oscura/analyzers/eye/metrics.py +555 -0
  17. oscura/analyzers/jitter/__init__.py +83 -0
  18. oscura/analyzers/jitter/ber.py +333 -0
  19. oscura/analyzers/jitter/decomposition.py +759 -0
  20. oscura/analyzers/jitter/measurements.py +413 -0
  21. oscura/analyzers/jitter/spectrum.py +220 -0
  22. oscura/analyzers/measurements.py +40 -0
  23. oscura/analyzers/packet/__init__.py +171 -0
  24. oscura/analyzers/packet/daq.py +1077 -0
  25. oscura/analyzers/packet/metrics.py +437 -0
  26. oscura/analyzers/packet/parser.py +327 -0
  27. oscura/analyzers/packet/payload.py +2156 -0
  28. oscura/analyzers/packet/payload_analysis.py +1312 -0
  29. oscura/analyzers/packet/payload_extraction.py +236 -0
  30. oscura/analyzers/packet/payload_patterns.py +670 -0
  31. oscura/analyzers/packet/stream.py +359 -0
  32. oscura/analyzers/patterns/__init__.py +266 -0
  33. oscura/analyzers/patterns/clustering.py +1036 -0
  34. oscura/analyzers/patterns/discovery.py +539 -0
  35. oscura/analyzers/patterns/learning.py +797 -0
  36. oscura/analyzers/patterns/matching.py +1091 -0
  37. oscura/analyzers/patterns/periodic.py +650 -0
  38. oscura/analyzers/patterns/sequences.py +767 -0
  39. oscura/analyzers/power/__init__.py +116 -0
  40. oscura/analyzers/power/ac_power.py +391 -0
  41. oscura/analyzers/power/basic.py +383 -0
  42. oscura/analyzers/power/conduction.py +314 -0
  43. oscura/analyzers/power/efficiency.py +297 -0
  44. oscura/analyzers/power/ripple.py +356 -0
  45. oscura/analyzers/power/soa.py +372 -0
  46. oscura/analyzers/power/switching.py +479 -0
  47. oscura/analyzers/protocol/__init__.py +150 -0
  48. oscura/analyzers/protocols/__init__.py +150 -0
  49. oscura/analyzers/protocols/base.py +500 -0
  50. oscura/analyzers/protocols/can.py +620 -0
  51. oscura/analyzers/protocols/can_fd.py +448 -0
  52. oscura/analyzers/protocols/flexray.py +405 -0
  53. oscura/analyzers/protocols/hdlc.py +399 -0
  54. oscura/analyzers/protocols/i2c.py +368 -0
  55. oscura/analyzers/protocols/i2s.py +296 -0
  56. oscura/analyzers/protocols/jtag.py +393 -0
  57. oscura/analyzers/protocols/lin.py +445 -0
  58. oscura/analyzers/protocols/manchester.py +333 -0
  59. oscura/analyzers/protocols/onewire.py +501 -0
  60. oscura/analyzers/protocols/spi.py +334 -0
  61. oscura/analyzers/protocols/swd.py +325 -0
  62. oscura/analyzers/protocols/uart.py +393 -0
  63. oscura/analyzers/protocols/usb.py +495 -0
  64. oscura/analyzers/signal_integrity/__init__.py +63 -0
  65. oscura/analyzers/signal_integrity/embedding.py +294 -0
  66. oscura/analyzers/signal_integrity/equalization.py +370 -0
  67. oscura/analyzers/signal_integrity/sparams.py +484 -0
  68. oscura/analyzers/spectral/__init__.py +53 -0
  69. oscura/analyzers/spectral/chunked.py +273 -0
  70. oscura/analyzers/spectral/chunked_fft.py +571 -0
  71. oscura/analyzers/spectral/chunked_wavelet.py +391 -0
  72. oscura/analyzers/spectral/fft.py +92 -0
  73. oscura/analyzers/statistical/__init__.py +250 -0
  74. oscura/analyzers/statistical/checksum.py +923 -0
  75. oscura/analyzers/statistical/chunked_corr.py +228 -0
  76. oscura/analyzers/statistical/classification.py +778 -0
  77. oscura/analyzers/statistical/entropy.py +1113 -0
  78. oscura/analyzers/statistical/ngrams.py +614 -0
  79. oscura/analyzers/statistics/__init__.py +119 -0
  80. oscura/analyzers/statistics/advanced.py +885 -0
  81. oscura/analyzers/statistics/basic.py +263 -0
  82. oscura/analyzers/statistics/correlation.py +630 -0
  83. oscura/analyzers/statistics/distribution.py +298 -0
  84. oscura/analyzers/statistics/outliers.py +463 -0
  85. oscura/analyzers/statistics/streaming.py +93 -0
  86. oscura/analyzers/statistics/trend.py +520 -0
  87. oscura/analyzers/validation.py +598 -0
  88. oscura/analyzers/waveform/__init__.py +36 -0
  89. oscura/analyzers/waveform/measurements.py +943 -0
  90. oscura/analyzers/waveform/measurements_with_uncertainty.py +371 -0
  91. oscura/analyzers/waveform/spectral.py +1689 -0
  92. oscura/analyzers/waveform/wavelets.py +298 -0
  93. oscura/api/__init__.py +62 -0
  94. oscura/api/dsl.py +538 -0
  95. oscura/api/fluent.py +571 -0
  96. oscura/api/operators.py +498 -0
  97. oscura/api/optimization.py +392 -0
  98. oscura/api/profiling.py +396 -0
  99. oscura/automotive/__init__.py +73 -0
  100. oscura/automotive/can/__init__.py +52 -0
  101. oscura/automotive/can/analysis.py +356 -0
  102. oscura/automotive/can/checksum.py +250 -0
  103. oscura/automotive/can/correlation.py +212 -0
  104. oscura/automotive/can/discovery.py +355 -0
  105. oscura/automotive/can/message_wrapper.py +375 -0
  106. oscura/automotive/can/models.py +385 -0
  107. oscura/automotive/can/patterns.py +381 -0
  108. oscura/automotive/can/session.py +452 -0
  109. oscura/automotive/can/state_machine.py +300 -0
  110. oscura/automotive/can/stimulus_response.py +461 -0
  111. oscura/automotive/dbc/__init__.py +15 -0
  112. oscura/automotive/dbc/generator.py +156 -0
  113. oscura/automotive/dbc/parser.py +146 -0
  114. oscura/automotive/dtc/__init__.py +30 -0
  115. oscura/automotive/dtc/database.py +3036 -0
  116. oscura/automotive/j1939/__init__.py +14 -0
  117. oscura/automotive/j1939/decoder.py +745 -0
  118. oscura/automotive/loaders/__init__.py +35 -0
  119. oscura/automotive/loaders/asc.py +98 -0
  120. oscura/automotive/loaders/blf.py +77 -0
  121. oscura/automotive/loaders/csv_can.py +136 -0
  122. oscura/automotive/loaders/dispatcher.py +136 -0
  123. oscura/automotive/loaders/mdf.py +331 -0
  124. oscura/automotive/loaders/pcap.py +132 -0
  125. oscura/automotive/obd/__init__.py +14 -0
  126. oscura/automotive/obd/decoder.py +707 -0
  127. oscura/automotive/uds/__init__.py +48 -0
  128. oscura/automotive/uds/decoder.py +265 -0
  129. oscura/automotive/uds/models.py +64 -0
  130. oscura/automotive/visualization.py +369 -0
  131. oscura/batch/__init__.py +55 -0
  132. oscura/batch/advanced.py +627 -0
  133. oscura/batch/aggregate.py +300 -0
  134. oscura/batch/analyze.py +139 -0
  135. oscura/batch/logging.py +487 -0
  136. oscura/batch/metrics.py +556 -0
  137. oscura/builders/__init__.py +41 -0
  138. oscura/builders/signal_builder.py +1131 -0
  139. oscura/cli/__init__.py +14 -0
  140. oscura/cli/batch.py +339 -0
  141. oscura/cli/characterize.py +273 -0
  142. oscura/cli/compare.py +775 -0
  143. oscura/cli/decode.py +551 -0
  144. oscura/cli/main.py +247 -0
  145. oscura/cli/shell.py +350 -0
  146. oscura/comparison/__init__.py +66 -0
  147. oscura/comparison/compare.py +397 -0
  148. oscura/comparison/golden.py +487 -0
  149. oscura/comparison/limits.py +391 -0
  150. oscura/comparison/mask.py +434 -0
  151. oscura/comparison/trace_diff.py +30 -0
  152. oscura/comparison/visualization.py +481 -0
  153. oscura/compliance/__init__.py +70 -0
  154. oscura/compliance/advanced.py +756 -0
  155. oscura/compliance/masks.py +363 -0
  156. oscura/compliance/reporting.py +483 -0
  157. oscura/compliance/testing.py +298 -0
  158. oscura/component/__init__.py +38 -0
  159. oscura/component/impedance.py +365 -0
  160. oscura/component/reactive.py +598 -0
  161. oscura/component/transmission_line.py +312 -0
  162. oscura/config/__init__.py +191 -0
  163. oscura/config/defaults.py +254 -0
  164. oscura/config/loader.py +348 -0
  165. oscura/config/memory.py +271 -0
  166. oscura/config/migration.py +458 -0
  167. oscura/config/pipeline.py +1077 -0
  168. oscura/config/preferences.py +530 -0
  169. oscura/config/protocol.py +875 -0
  170. oscura/config/schema.py +713 -0
  171. oscura/config/settings.py +420 -0
  172. oscura/config/thresholds.py +599 -0
  173. oscura/convenience.py +457 -0
  174. oscura/core/__init__.py +299 -0
  175. oscura/core/audit.py +457 -0
  176. oscura/core/backend_selector.py +405 -0
  177. oscura/core/cache.py +590 -0
  178. oscura/core/cancellation.py +439 -0
  179. oscura/core/confidence.py +225 -0
  180. oscura/core/config.py +506 -0
  181. oscura/core/correlation.py +216 -0
  182. oscura/core/cross_domain.py +422 -0
  183. oscura/core/debug.py +301 -0
  184. oscura/core/edge_cases.py +541 -0
  185. oscura/core/exceptions.py +535 -0
  186. oscura/core/gpu_backend.py +523 -0
  187. oscura/core/lazy.py +832 -0
  188. oscura/core/log_query.py +540 -0
  189. oscura/core/logging.py +931 -0
  190. oscura/core/logging_advanced.py +952 -0
  191. oscura/core/memoize.py +171 -0
  192. oscura/core/memory_check.py +274 -0
  193. oscura/core/memory_guard.py +290 -0
  194. oscura/core/memory_limits.py +336 -0
  195. oscura/core/memory_monitor.py +453 -0
  196. oscura/core/memory_progress.py +465 -0
  197. oscura/core/memory_warnings.py +315 -0
  198. oscura/core/numba_backend.py +362 -0
  199. oscura/core/performance.py +352 -0
  200. oscura/core/progress.py +524 -0
  201. oscura/core/provenance.py +358 -0
  202. oscura/core/results.py +331 -0
  203. oscura/core/types.py +504 -0
  204. oscura/core/uncertainty.py +383 -0
  205. oscura/discovery/__init__.py +52 -0
  206. oscura/discovery/anomaly_detector.py +672 -0
  207. oscura/discovery/auto_decoder.py +415 -0
  208. oscura/discovery/comparison.py +497 -0
  209. oscura/discovery/quality_validator.py +528 -0
  210. oscura/discovery/signal_detector.py +769 -0
  211. oscura/dsl/__init__.py +73 -0
  212. oscura/dsl/commands.py +246 -0
  213. oscura/dsl/interpreter.py +455 -0
  214. oscura/dsl/parser.py +689 -0
  215. oscura/dsl/repl.py +172 -0
  216. oscura/exceptions.py +59 -0
  217. oscura/exploratory/__init__.py +111 -0
  218. oscura/exploratory/error_recovery.py +642 -0
  219. oscura/exploratory/fuzzy.py +513 -0
  220. oscura/exploratory/fuzzy_advanced.py +786 -0
  221. oscura/exploratory/legacy.py +831 -0
  222. oscura/exploratory/parse.py +358 -0
  223. oscura/exploratory/recovery.py +275 -0
  224. oscura/exploratory/sync.py +382 -0
  225. oscura/exploratory/unknown.py +707 -0
  226. oscura/export/__init__.py +25 -0
  227. oscura/export/wireshark/README.md +265 -0
  228. oscura/export/wireshark/__init__.py +47 -0
  229. oscura/export/wireshark/generator.py +312 -0
  230. oscura/export/wireshark/lua_builder.py +159 -0
  231. oscura/export/wireshark/templates/dissector.lua.j2 +92 -0
  232. oscura/export/wireshark/type_mapping.py +165 -0
  233. oscura/export/wireshark/validator.py +105 -0
  234. oscura/exporters/__init__.py +94 -0
  235. oscura/exporters/csv.py +303 -0
  236. oscura/exporters/exporters.py +44 -0
  237. oscura/exporters/hdf5.py +219 -0
  238. oscura/exporters/html_export.py +701 -0
  239. oscura/exporters/json_export.py +291 -0
  240. oscura/exporters/markdown_export.py +367 -0
  241. oscura/exporters/matlab_export.py +354 -0
  242. oscura/exporters/npz_export.py +219 -0
  243. oscura/exporters/spice_export.py +210 -0
  244. oscura/extensibility/__init__.py +131 -0
  245. oscura/extensibility/docs.py +752 -0
  246. oscura/extensibility/extensions.py +1125 -0
  247. oscura/extensibility/logging.py +259 -0
  248. oscura/extensibility/measurements.py +485 -0
  249. oscura/extensibility/plugins.py +414 -0
  250. oscura/extensibility/registry.py +346 -0
  251. oscura/extensibility/templates.py +913 -0
  252. oscura/extensibility/validation.py +651 -0
  253. oscura/filtering/__init__.py +89 -0
  254. oscura/filtering/base.py +563 -0
  255. oscura/filtering/convenience.py +564 -0
  256. oscura/filtering/design.py +725 -0
  257. oscura/filtering/filters.py +32 -0
  258. oscura/filtering/introspection.py +605 -0
  259. oscura/guidance/__init__.py +24 -0
  260. oscura/guidance/recommender.py +429 -0
  261. oscura/guidance/wizard.py +518 -0
  262. oscura/inference/__init__.py +251 -0
  263. oscura/inference/active_learning/README.md +153 -0
  264. oscura/inference/active_learning/__init__.py +38 -0
  265. oscura/inference/active_learning/lstar.py +257 -0
  266. oscura/inference/active_learning/observation_table.py +230 -0
  267. oscura/inference/active_learning/oracle.py +78 -0
  268. oscura/inference/active_learning/teachers/__init__.py +15 -0
  269. oscura/inference/active_learning/teachers/simulator.py +192 -0
  270. oscura/inference/adaptive_tuning.py +453 -0
  271. oscura/inference/alignment.py +653 -0
  272. oscura/inference/bayesian.py +943 -0
  273. oscura/inference/binary.py +1016 -0
  274. oscura/inference/crc_reverse.py +711 -0
  275. oscura/inference/logic.py +288 -0
  276. oscura/inference/message_format.py +1305 -0
  277. oscura/inference/protocol.py +417 -0
  278. oscura/inference/protocol_dsl.py +1084 -0
  279. oscura/inference/protocol_library.py +1230 -0
  280. oscura/inference/sequences.py +809 -0
  281. oscura/inference/signal_intelligence.py +1509 -0
  282. oscura/inference/spectral.py +215 -0
  283. oscura/inference/state_machine.py +634 -0
  284. oscura/inference/stream.py +918 -0
  285. oscura/integrations/__init__.py +59 -0
  286. oscura/integrations/llm.py +1827 -0
  287. oscura/jupyter/__init__.py +32 -0
  288. oscura/jupyter/display.py +268 -0
  289. oscura/jupyter/magic.py +334 -0
  290. oscura/loaders/__init__.py +526 -0
  291. oscura/loaders/binary.py +69 -0
  292. oscura/loaders/configurable.py +1255 -0
  293. oscura/loaders/csv.py +26 -0
  294. oscura/loaders/csv_loader.py +473 -0
  295. oscura/loaders/hdf5.py +9 -0
  296. oscura/loaders/hdf5_loader.py +510 -0
  297. oscura/loaders/lazy.py +370 -0
  298. oscura/loaders/mmap_loader.py +583 -0
  299. oscura/loaders/numpy_loader.py +436 -0
  300. oscura/loaders/pcap.py +432 -0
  301. oscura/loaders/preprocessing.py +368 -0
  302. oscura/loaders/rigol.py +287 -0
  303. oscura/loaders/sigrok.py +321 -0
  304. oscura/loaders/tdms.py +367 -0
  305. oscura/loaders/tektronix.py +711 -0
  306. oscura/loaders/validation.py +584 -0
  307. oscura/loaders/vcd.py +464 -0
  308. oscura/loaders/wav.py +233 -0
  309. oscura/math/__init__.py +45 -0
  310. oscura/math/arithmetic.py +824 -0
  311. oscura/math/interpolation.py +413 -0
  312. oscura/onboarding/__init__.py +39 -0
  313. oscura/onboarding/help.py +498 -0
  314. oscura/onboarding/tutorials.py +405 -0
  315. oscura/onboarding/wizard.py +466 -0
  316. oscura/optimization/__init__.py +19 -0
  317. oscura/optimization/parallel.py +440 -0
  318. oscura/optimization/search.py +532 -0
  319. oscura/pipeline/__init__.py +43 -0
  320. oscura/pipeline/base.py +338 -0
  321. oscura/pipeline/composition.py +242 -0
  322. oscura/pipeline/parallel.py +448 -0
  323. oscura/pipeline/pipeline.py +375 -0
  324. oscura/pipeline/reverse_engineering.py +1119 -0
  325. oscura/plugins/__init__.py +122 -0
  326. oscura/plugins/base.py +272 -0
  327. oscura/plugins/cli.py +497 -0
  328. oscura/plugins/discovery.py +411 -0
  329. oscura/plugins/isolation.py +418 -0
  330. oscura/plugins/lifecycle.py +959 -0
  331. oscura/plugins/manager.py +493 -0
  332. oscura/plugins/registry.py +421 -0
  333. oscura/plugins/versioning.py +372 -0
  334. oscura/py.typed +0 -0
  335. oscura/quality/__init__.py +65 -0
  336. oscura/quality/ensemble.py +740 -0
  337. oscura/quality/explainer.py +338 -0
  338. oscura/quality/scoring.py +616 -0
  339. oscura/quality/warnings.py +456 -0
  340. oscura/reporting/__init__.py +248 -0
  341. oscura/reporting/advanced.py +1234 -0
  342. oscura/reporting/analyze.py +448 -0
  343. oscura/reporting/argument_preparer.py +596 -0
  344. oscura/reporting/auto_report.py +507 -0
  345. oscura/reporting/batch.py +615 -0
  346. oscura/reporting/chart_selection.py +223 -0
  347. oscura/reporting/comparison.py +330 -0
  348. oscura/reporting/config.py +615 -0
  349. oscura/reporting/content/__init__.py +39 -0
  350. oscura/reporting/content/executive.py +127 -0
  351. oscura/reporting/content/filtering.py +191 -0
  352. oscura/reporting/content/minimal.py +257 -0
  353. oscura/reporting/content/verbosity.py +162 -0
  354. oscura/reporting/core.py +508 -0
  355. oscura/reporting/core_formats/__init__.py +17 -0
  356. oscura/reporting/core_formats/multi_format.py +210 -0
  357. oscura/reporting/engine.py +836 -0
  358. oscura/reporting/export.py +366 -0
  359. oscura/reporting/formatting/__init__.py +129 -0
  360. oscura/reporting/formatting/emphasis.py +81 -0
  361. oscura/reporting/formatting/numbers.py +403 -0
  362. oscura/reporting/formatting/standards.py +55 -0
  363. oscura/reporting/formatting.py +466 -0
  364. oscura/reporting/html.py +578 -0
  365. oscura/reporting/index.py +590 -0
  366. oscura/reporting/multichannel.py +296 -0
  367. oscura/reporting/output.py +379 -0
  368. oscura/reporting/pdf.py +373 -0
  369. oscura/reporting/plots.py +731 -0
  370. oscura/reporting/pptx_export.py +360 -0
  371. oscura/reporting/renderers/__init__.py +11 -0
  372. oscura/reporting/renderers/pdf.py +94 -0
  373. oscura/reporting/sections.py +471 -0
  374. oscura/reporting/standards.py +680 -0
  375. oscura/reporting/summary_generator.py +368 -0
  376. oscura/reporting/tables.py +397 -0
  377. oscura/reporting/template_system.py +724 -0
  378. oscura/reporting/templates/__init__.py +15 -0
  379. oscura/reporting/templates/definition.py +205 -0
  380. oscura/reporting/templates/index.html +649 -0
  381. oscura/reporting/templates/index.md +173 -0
  382. oscura/schemas/__init__.py +158 -0
  383. oscura/schemas/bus_configuration.json +322 -0
  384. oscura/schemas/device_mapping.json +182 -0
  385. oscura/schemas/packet_format.json +418 -0
  386. oscura/schemas/protocol_definition.json +363 -0
  387. oscura/search/__init__.py +16 -0
  388. oscura/search/anomaly.py +292 -0
  389. oscura/search/context.py +149 -0
  390. oscura/search/pattern.py +160 -0
  391. oscura/session/__init__.py +34 -0
  392. oscura/session/annotations.py +289 -0
  393. oscura/session/history.py +313 -0
  394. oscura/session/session.py +445 -0
  395. oscura/streaming/__init__.py +43 -0
  396. oscura/streaming/chunked.py +611 -0
  397. oscura/streaming/progressive.py +393 -0
  398. oscura/streaming/realtime.py +622 -0
  399. oscura/testing/__init__.py +54 -0
  400. oscura/testing/synthetic.py +808 -0
  401. oscura/triggering/__init__.py +68 -0
  402. oscura/triggering/base.py +229 -0
  403. oscura/triggering/edge.py +353 -0
  404. oscura/triggering/pattern.py +344 -0
  405. oscura/triggering/pulse.py +581 -0
  406. oscura/triggering/window.py +453 -0
  407. oscura/ui/__init__.py +48 -0
  408. oscura/ui/formatters.py +526 -0
  409. oscura/ui/progressive_display.py +340 -0
  410. oscura/utils/__init__.py +99 -0
  411. oscura/utils/autodetect.py +338 -0
  412. oscura/utils/buffer.py +389 -0
  413. oscura/utils/lazy.py +407 -0
  414. oscura/utils/lazy_imports.py +147 -0
  415. oscura/utils/memory.py +836 -0
  416. oscura/utils/memory_advanced.py +1326 -0
  417. oscura/utils/memory_extensions.py +465 -0
  418. oscura/utils/progressive.py +352 -0
  419. oscura/utils/windowing.py +362 -0
  420. oscura/visualization/__init__.py +321 -0
  421. oscura/visualization/accessibility.py +526 -0
  422. oscura/visualization/annotations.py +374 -0
  423. oscura/visualization/axis_scaling.py +305 -0
  424. oscura/visualization/colors.py +453 -0
  425. oscura/visualization/digital.py +337 -0
  426. oscura/visualization/eye.py +420 -0
  427. oscura/visualization/histogram.py +281 -0
  428. oscura/visualization/interactive.py +858 -0
  429. oscura/visualization/jitter.py +702 -0
  430. oscura/visualization/keyboard.py +394 -0
  431. oscura/visualization/layout.py +365 -0
  432. oscura/visualization/optimization.py +1028 -0
  433. oscura/visualization/palettes.py +446 -0
  434. oscura/visualization/plot.py +92 -0
  435. oscura/visualization/power.py +290 -0
  436. oscura/visualization/power_extended.py +626 -0
  437. oscura/visualization/presets.py +467 -0
  438. oscura/visualization/protocols.py +932 -0
  439. oscura/visualization/render.py +207 -0
  440. oscura/visualization/rendering.py +444 -0
  441. oscura/visualization/reverse_engineering.py +791 -0
  442. oscura/visualization/signal_integrity.py +808 -0
  443. oscura/visualization/specialized.py +553 -0
  444. oscura/visualization/spectral.py +811 -0
  445. oscura/visualization/styles.py +381 -0
  446. oscura/visualization/thumbnails.py +311 -0
  447. oscura/visualization/time_axis.py +351 -0
  448. oscura/visualization/waveform.py +367 -0
  449. oscura/workflow/__init__.py +13 -0
  450. oscura/workflow/dag.py +377 -0
  451. oscura/workflows/__init__.py +58 -0
  452. oscura/workflows/compliance.py +280 -0
  453. oscura/workflows/digital.py +272 -0
  454. oscura/workflows/multi_trace.py +502 -0
  455. oscura/workflows/power.py +178 -0
  456. oscura/workflows/protocol.py +492 -0
  457. oscura/workflows/reverse_engineering.py +639 -0
  458. oscura/workflows/signal_integrity.py +227 -0
  459. oscura-0.1.0.dist-info/METADATA +300 -0
  460. oscura-0.1.0.dist-info/RECORD +463 -0
  461. oscura-0.1.0.dist-info/entry_points.txt +2 -0
  462. {oscura-0.0.1.dist-info → oscura-0.1.0.dist-info}/licenses/LICENSE +1 -1
  463. oscura-0.0.1.dist-info/METADATA +0 -63
  464. oscura-0.0.1.dist-info/RECORD +0 -5
  465. {oscura-0.0.1.dist-info → oscura-0.1.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,296 @@
1
+ """Multi-channel report generation for TraceKit.
2
+
3
+ This module provides utilities for generating reports across multiple channels
4
+ with channel comparison and aggregation.
5
+
6
+
7
+ Example:
8
+ >>> from oscura.reporting.multichannel import generate_multichannel_report
9
+ >>> report = generate_multichannel_report(channel_results, "multi_report.pdf")
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import Any
15
+
16
+ from oscura.reporting.core import Report, ReportConfig, Section
17
+ from oscura.reporting.tables import create_measurement_table
18
+
19
+
20
+ def generate_multichannel_report(
21
+ channel_results: dict[str, dict[str, Any]],
22
+ *,
23
+ title: str = "Multi-Channel Analysis Report",
24
+ compare_channels: bool = True,
25
+ aggregate_statistics: bool = True,
26
+ individual_sections: bool = True,
27
+ **kwargs: Any,
28
+ ) -> Report:
29
+ """Generate report for multi-channel analysis.
30
+
31
+ Args:
32
+ channel_results: Dictionary mapping channel name to results.
33
+ title: Report title.
34
+ compare_channels: Include channel comparison section.
35
+ aggregate_statistics: Include aggregate statistics across channels.
36
+ individual_sections: Include individual channel sections.
37
+ **kwargs: Additional report configuration options.
38
+
39
+ Returns:
40
+ Multi-channel Report object.
41
+
42
+ References:
43
+ REPORT-007
44
+ """
45
+ config = ReportConfig(title=title, **kwargs)
46
+ report = Report(config=config)
47
+
48
+ # Add executive summary
49
+ summary_content = _generate_multichannel_summary(channel_results)
50
+ report.add_section("Executive Summary", summary_content, level=1)
51
+
52
+ # Add aggregate statistics
53
+ if aggregate_statistics:
54
+ stats_section = _create_aggregate_statistics_section(channel_results)
55
+ report.sections.append(stats_section)
56
+
57
+ # Add channel comparison
58
+ if compare_channels and len(channel_results) > 1:
59
+ comparison_section = _create_channel_comparison_section(channel_results)
60
+ report.sections.append(comparison_section)
61
+
62
+ # Add individual channel sections
63
+ if individual_sections:
64
+ for channel_name, results in channel_results.items():
65
+ channel_section = _create_channel_section(channel_name, results)
66
+ report.sections.append(channel_section)
67
+
68
+ return report
69
+
70
+
71
+ def _generate_multichannel_summary(channel_results: dict[str, dict[str, Any]]) -> str:
72
+ """Generate summary for multi-channel report."""
73
+ summary_parts = []
74
+
75
+ total_channels = len(channel_results)
76
+ summary_parts.append(f"Analyzed {total_channels} channel(s).")
77
+
78
+ # Aggregate pass/fail across channels
79
+ total_tests = 0
80
+ total_passed = 0
81
+
82
+ for results in channel_results.values():
83
+ total_tests += results.get("total_count", 0)
84
+ total_passed += results.get("pass_count", 0)
85
+
86
+ if total_tests > 0:
87
+ total_failed = total_tests - total_passed
88
+ summary_parts.append(
89
+ f"\nOverall: {total_passed}/{total_tests} tests passed "
90
+ f"({total_passed / total_tests * 100:.1f}% pass rate)."
91
+ )
92
+
93
+ if total_failed > 0:
94
+ summary_parts.append(f"{total_failed} test(s) failed across all channels.")
95
+
96
+ # Channel-specific summary
97
+ failed_channels = []
98
+ for channel_name, results in channel_results.items():
99
+ pass_count = results.get("pass_count", 0)
100
+ total_count = results.get("total_count", 0)
101
+ if total_count > 0 and pass_count < total_count:
102
+ failed_channels.append(channel_name)
103
+
104
+ if failed_channels:
105
+ summary_parts.append(f"\nChannels with failures: {', '.join(failed_channels)}")
106
+ else:
107
+ summary_parts.append("\nAll channels passed all tests.")
108
+
109
+ return "\n".join(summary_parts)
110
+
111
+
112
+ def _create_aggregate_statistics_section(
113
+ channel_results: dict[str, dict[str, Any]],
114
+ ) -> Section:
115
+ """Create aggregate statistics section across all channels."""
116
+ # Collect all measurement parameters
117
+ all_params = set()
118
+ for results in channel_results.values():
119
+ if "measurements" in results:
120
+ all_params.update(results["measurements"].keys())
121
+
122
+ # Build aggregate table
123
+ import numpy as np
124
+
125
+ headers = ["Parameter", "Min", "Mean", "Max", "Std Dev"]
126
+ rows = []
127
+
128
+ for param in sorted(all_params):
129
+ values = []
130
+ unit = ""
131
+
132
+ for results in channel_results.values():
133
+ if "measurements" in results and param in results["measurements"]:
134
+ meas = results["measurements"][param]
135
+ if "value" in meas and meas["value"] is not None:
136
+ values.append(meas["value"])
137
+ if not unit and "unit" in meas:
138
+ unit = meas["unit"]
139
+
140
+ if values:
141
+ from oscura.reporting.formatting import NumberFormatter
142
+
143
+ formatter = NumberFormatter()
144
+ rows.append(
145
+ [
146
+ param,
147
+ formatter.format(np.min(values), unit),
148
+ formatter.format(np.mean(values), unit),
149
+ formatter.format(np.max(values), unit),
150
+ formatter.format(np.std(values), unit),
151
+ ]
152
+ )
153
+
154
+ table = {"type": "table", "headers": headers, "data": rows}
155
+
156
+ return Section(
157
+ title="Aggregate Statistics",
158
+ content=[table],
159
+ level=1,
160
+ visible=True,
161
+ )
162
+
163
+
164
+ def _create_channel_comparison_section(
165
+ channel_results: dict[str, dict[str, Any]],
166
+ ) -> Section:
167
+ """Create channel-to-channel comparison section."""
168
+ from oscura.reporting.formatting import NumberFormatter
169
+
170
+ formatter = NumberFormatter()
171
+
172
+ # Build comparison table
173
+ channel_names = list(channel_results.keys())
174
+ headers = ["Parameter", *channel_names]
175
+
176
+ # Collect all parameters
177
+ all_params = set()
178
+ for results in channel_results.values():
179
+ if "measurements" in results:
180
+ all_params.update(results["measurements"].keys())
181
+
182
+ rows = []
183
+ for param in sorted(all_params):
184
+ row = [param]
185
+
186
+ for channel_name in channel_names:
187
+ results = channel_results[channel_name]
188
+ if "measurements" in results and param in results["measurements"]:
189
+ meas = results["measurements"][param]
190
+ value = meas.get("value")
191
+ unit = meas.get("unit", "")
192
+ if value is not None:
193
+ row.append(formatter.format(value, unit))
194
+ else:
195
+ row.append("-")
196
+ else:
197
+ row.append("-")
198
+
199
+ rows.append(row)
200
+
201
+ table = {"type": "table", "headers": headers, "data": rows}
202
+
203
+ return Section(
204
+ title="Channel Comparison",
205
+ content=[table],
206
+ level=1,
207
+ visible=True,
208
+ )
209
+
210
+
211
+ def _create_channel_section(
212
+ channel_name: str,
213
+ results: dict[str, Any],
214
+ ) -> Section:
215
+ """Create individual channel section."""
216
+ subsections = []
217
+
218
+ # Channel summary
219
+ summary_parts = []
220
+ if "pass_count" in results and "total_count" in results:
221
+ pass_count = results["pass_count"]
222
+ total = results["total_count"]
223
+ summary_parts.append(
224
+ f"{pass_count}/{total} tests passed ({pass_count / total * 100:.1f}% pass rate)."
225
+ )
226
+
227
+ # Measurements
228
+ if "measurements" in results:
229
+ table = create_measurement_table(results["measurements"], format="dict")
230
+ subsections.append(
231
+ Section(
232
+ title="Measurements",
233
+ content=[table],
234
+ level=3,
235
+ visible=True,
236
+ )
237
+ )
238
+
239
+ return Section(
240
+ title=f"Channel: {channel_name}",
241
+ content="\n".join(summary_parts) if summary_parts else "",
242
+ level=2,
243
+ visible=True,
244
+ subsections=subsections,
245
+ )
246
+
247
+
248
+ def create_channel_crosstalk_section(
249
+ crosstalk_results: dict[str, Any],
250
+ ) -> Section:
251
+ """Create channel crosstalk analysis section.
252
+
253
+ Args:
254
+ crosstalk_results: Crosstalk analysis results between channels.
255
+
256
+ Returns:
257
+ Crosstalk Section object.
258
+
259
+ References:
260
+ REPORT-007
261
+ """
262
+ from oscura.reporting.formatting import NumberFormatter
263
+
264
+ formatter = NumberFormatter()
265
+
266
+ if "crosstalk_matrix" in crosstalk_results:
267
+ matrix = crosstalk_results["crosstalk_matrix"]
268
+ channels = crosstalk_results.get("channels", [])
269
+
270
+ headers = ["Aggressor → Victim", *channels]
271
+ rows = []
272
+
273
+ for i, aggressor in enumerate(channels):
274
+ row = [aggressor]
275
+ for j, _victim in enumerate(channels):
276
+ if i == j:
277
+ row.append("-")
278
+ else:
279
+ crosstalk_db = matrix[i][j]
280
+ row.append(formatter.format(crosstalk_db, "dB"))
281
+ rows.append(row)
282
+
283
+ table = {"type": "table", "headers": headers, "data": rows}
284
+ content = [
285
+ "Channel-to-channel crosstalk measurements:\n",
286
+ table,
287
+ ]
288
+ else:
289
+ content = "No crosstalk analysis available." # type: ignore[assignment]
290
+
291
+ return Section(
292
+ title="Channel Crosstalk Analysis",
293
+ content=content,
294
+ level=2,
295
+ visible=True,
296
+ )
@@ -0,0 +1,379 @@
1
+ """Output management for comprehensive analysis reports.
2
+
3
+ This module provides directory structure and file management for analysis
4
+ report outputs, including plots, JSON/YAML data exports, and logs.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ from datetime import datetime
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ import numpy as np
15
+ import yaml
16
+
17
+ from oscura.reporting.config import AnalysisDomain # noqa: TC001
18
+
19
+
20
+ def _sanitize_for_serialization(obj: Any, max_depth: int = 10) -> Any:
21
+ """Convert non-serializable objects for JSON/YAML output.
22
+
23
+ Handles generators, numpy arrays, and other problematic types
24
+ that can appear in analysis results.
25
+
26
+ Args:
27
+ obj: Object to sanitize.
28
+ max_depth: Maximum recursion depth to prevent infinite loops.
29
+
30
+ Returns:
31
+ Serialization-safe version of the object.
32
+ """
33
+ import types
34
+
35
+ from oscura.core.types import DigitalTrace, TraceMetadata, WaveformTrace
36
+
37
+ if max_depth <= 0:
38
+ return "<max depth exceeded>"
39
+
40
+ try:
41
+ # Don't sanitize TraceKit types - let the JSONEncoder handle them
42
+ if isinstance(obj, WaveformTrace | DigitalTrace | TraceMetadata):
43
+ return obj
44
+ if isinstance(obj, dict):
45
+ # Sanitize both keys and values, convert non-string keys to strings
46
+ sanitized = {}
47
+ for k, v in obj.items():
48
+ # Convert bytes keys to hex strings
49
+ if isinstance(k, bytes):
50
+ k = f"0x{k.hex()}"
51
+ # Convert other non-string keys to strings
52
+ elif not isinstance(k, str | int | float | bool | type(None)):
53
+ k = str(k)
54
+ sanitized[k] = _sanitize_for_serialization(v, max_depth - 1)
55
+ return sanitized
56
+ elif isinstance(obj, list | tuple):
57
+ return [_sanitize_for_serialization(item, max_depth - 1) for item in obj]
58
+ elif isinstance(obj, types.GeneratorType):
59
+ # Convert generators to lists, but catch errors
60
+ try:
61
+ items = list(obj)
62
+ return [_sanitize_for_serialization(item, max_depth - 1) for item in items]
63
+ except Exception:
64
+ # Return None for incompatible generators (cleaner than error string)
65
+ return None
66
+ elif isinstance(obj, np.ndarray):
67
+ # Limit large arrays
68
+ if obj.size > 10000:
69
+ return f"<ndarray shape={obj.shape} dtype={obj.dtype}>"
70
+ return obj.tolist()
71
+ elif isinstance(obj, np.generic):
72
+ # Catch all numpy scalar types (int, float, complex, bool, str, etc.)
73
+ # This includes np.integer, np.floating, np.bool_, np.complexfloating, etc.
74
+ return obj.item()
75
+ elif isinstance(obj, np.integer | np.floating):
76
+ # Redundant but kept for clarity
77
+ return obj.item()
78
+ elif isinstance(obj, np.bool_):
79
+ # Redundant but kept for clarity
80
+ return bool(obj)
81
+ elif isinstance(obj, float):
82
+ # Handle Python float inf/nan (not caught by JSONEncoder.default)
83
+ import math
84
+
85
+ if math.isinf(obj) or math.isnan(obj):
86
+ return None
87
+ return obj
88
+ elif isinstance(obj, complex):
89
+ # Handle complex numbers with inf/nan components
90
+ import math
91
+
92
+ if (
93
+ math.isinf(obj.real)
94
+ or math.isnan(obj.real)
95
+ or math.isinf(obj.imag)
96
+ or math.isnan(obj.imag)
97
+ ):
98
+ return None
99
+ return {"real": obj.real, "imag": obj.imag}
100
+ elif isinstance(obj, bytes):
101
+ # Limit large byte sequences
102
+ if len(obj) > 1000:
103
+ return f"<bytes len={len(obj)}>"
104
+ return obj.hex()
105
+ elif hasattr(obj, "__dict__") and not isinstance(obj, type):
106
+ # Convert dataclasses and objects to dicts
107
+ try:
108
+ return {
109
+ k: _sanitize_for_serialization(v, max_depth - 1)
110
+ for k, v in obj.__dict__.items()
111
+ }
112
+ except Exception:
113
+ return str(obj)
114
+ elif callable(obj):
115
+ return f"<callable: {getattr(obj, '__name__', str(obj))}>"
116
+ else:
117
+ # Try to convert to string as last resort
118
+ try:
119
+ return obj
120
+ except Exception:
121
+ return str(obj)
122
+ except Exception as e:
123
+ return f"<error: {type(e).__name__}: {str(e)[:50]}>"
124
+
125
+
126
+ class OutputManager:
127
+ """Manages output directory structure and file operations for analysis reports.
128
+
129
+ Creates timestamped output directories with organized subdirectories for
130
+ different types of analysis outputs (plots, data files, logs, errors).
131
+
132
+ Attributes:
133
+ root: Root directory path for this analysis output.
134
+ timestamp: Timestamp for this output session.
135
+ timestamp_str: Formatted timestamp string.
136
+
137
+ Requirements:
138
+ """
139
+
140
+ def __init__(
141
+ self,
142
+ base_dir: Path,
143
+ input_name: str,
144
+ timestamp: datetime | None = None,
145
+ ) -> None:
146
+ """Initialize output manager.
147
+
148
+ Args:
149
+ base_dir: Base directory for all outputs.
150
+ input_name: Name of the input file/dataset being analyzed.
151
+ timestamp: Timestamp for this session (defaults to now).
152
+
153
+ Examples:
154
+ >>> manager = OutputManager(Path("/output"), "signal_data")
155
+ >>> manager.root.name
156
+ '20260101_120000_signal_data_analysis'
157
+ """
158
+ self._timestamp = timestamp or datetime.now()
159
+ self._timestamp_str = self._timestamp.strftime("%Y%m%d_%H%M%S")
160
+
161
+ # Create timestamped directory name
162
+ dirname = f"{self._timestamp_str}_{input_name}_analysis"
163
+ self._root = base_dir / dirname
164
+
165
+ @property
166
+ def root(self) -> Path:
167
+ """Root directory path for this analysis output."""
168
+ return self._root
169
+
170
+ @property
171
+ def timestamp(self) -> datetime:
172
+ """Timestamp for this output session."""
173
+ return self._timestamp
174
+
175
+ @property
176
+ def timestamp_str(self) -> str:
177
+ """Formatted timestamp string (YYYYMMDD_HHMMSS)."""
178
+ return self._timestamp_str
179
+
180
+ def create(self) -> Path:
181
+ """Create output directory structure.
182
+
183
+ Creates the root directory and standard subdirectories:
184
+ - plots/: Visualization outputs
185
+ - errors/: Error logs and diagnostics
186
+ - logs/: Analysis logs
187
+ - input/: Input file copies/metadata
188
+
189
+ Returns:
190
+ Path to the created root directory.
191
+
192
+ Note:
193
+ This method is idempotent - calling it multiple times is safe.
194
+
195
+ Requirements:
196
+
197
+ Examples:
198
+ >>> manager = OutputManager(Path("/tmp/output"), "test")
199
+ >>> root = manager.create()
200
+ >>> (root / "plots").exists()
201
+ True
202
+ """
203
+ self._root.mkdir(parents=True, exist_ok=True)
204
+
205
+ # Create standard subdirectories
206
+ subdirs = ["plots", "errors", "logs", "input"]
207
+ for subdir in subdirs:
208
+ (self._root / subdir).mkdir(exist_ok=True)
209
+
210
+ return self._root
211
+
212
+ def create_domain_dir(self, domain: AnalysisDomain) -> Path:
213
+ """Create and return domain-specific subdirectory.
214
+
215
+ Creates a subdirectory for organizing outputs from a specific
216
+ analysis domain (e.g., spectral/, digital/, jitter/).
217
+
218
+ Args:
219
+ domain: Analysis domain.
220
+
221
+ Returns:
222
+ Path to the created domain directory.
223
+
224
+ Requirements:
225
+
226
+ Examples:
227
+ >>> manager = OutputManager(Path("/tmp/output"), "test")
228
+ >>> manager.create()
229
+ >>> domain_dir = manager.create_domain_dir(AnalysisDomain.SPECTRAL)
230
+ >>> domain_dir.name
231
+ 'spectral'
232
+ """
233
+ domain_dir = self._root / domain.value
234
+ domain_dir.mkdir(parents=True, exist_ok=True)
235
+ return domain_dir
236
+
237
+ def save_json(
238
+ self,
239
+ name: str,
240
+ data: dict[str, Any],
241
+ subdir: str | None = None,
242
+ ) -> Path:
243
+ """Save data as JSON file with pretty formatting.
244
+
245
+ Args:
246
+ name: Filename (without .json extension).
247
+ data: Dictionary to serialize.
248
+ subdir: Optional subdirectory within root.
249
+
250
+ Returns:
251
+ Path to the saved JSON file.
252
+
253
+ Requirements:
254
+
255
+ Examples:
256
+ >>> manager = OutputManager(Path("/tmp/output"), "test")
257
+ >>> manager.create()
258
+ >>> path = manager.save_json("metrics", {"snr": 42.5})
259
+ >>> path.name
260
+ 'metrics.json'
261
+ """
262
+ target_dir = self._root / subdir if subdir else self._root
263
+ target_dir.mkdir(parents=True, exist_ok=True)
264
+
265
+ filepath = target_dir / f"{name}.json"
266
+ with filepath.open("w") as f:
267
+ json.dump(data, f, indent=2, default=str)
268
+
269
+ return filepath
270
+
271
+ def save_yaml(
272
+ self,
273
+ name: str,
274
+ data: dict[str, Any],
275
+ subdir: str | None = None,
276
+ ) -> Path:
277
+ """Save data as YAML file.
278
+
279
+ Args:
280
+ name: Filename (without .yaml extension).
281
+ data: Dictionary to serialize.
282
+ subdir: Optional subdirectory within root.
283
+
284
+ Returns:
285
+ Path to the saved YAML file.
286
+
287
+ Requirements:
288
+
289
+ Examples:
290
+ >>> manager = OutputManager(Path("/tmp/output"), "test")
291
+ >>> manager.create()
292
+ >>> path = manager.save_yaml("config", {"enabled": True})
293
+ >>> path.name
294
+ 'config.yaml'
295
+ """
296
+ target_dir = self._root / subdir if subdir else self._root
297
+ target_dir.mkdir(parents=True, exist_ok=True)
298
+
299
+ filepath = target_dir / f"{name}.yaml"
300
+ # Sanitize data to handle generators, numpy arrays, etc.
301
+ sanitized_data = _sanitize_for_serialization(data)
302
+ with filepath.open("w") as f:
303
+ yaml.dump(sanitized_data, f, default_flow_style=False, sort_keys=False)
304
+
305
+ return filepath
306
+
307
+ def save_plot(
308
+ self,
309
+ domain: AnalysisDomain,
310
+ name: str,
311
+ fig: Any,
312
+ format: str = "png",
313
+ dpi: int = 150,
314
+ ) -> Path:
315
+ """Save matplotlib figure to plots directory.
316
+
317
+ Saves plot with domain-prefixed filename in the plots/ subdirectory.
318
+
319
+ Args:
320
+ domain: Analysis domain for this plot.
321
+ name: Plot name (without extension).
322
+ fig: Matplotlib figure object.
323
+ format: Image format (png, pdf, svg, etc.).
324
+ dpi: Resolution in dots per inch.
325
+
326
+ Returns:
327
+ Path to the saved plot file.
328
+
329
+ Requirements:
330
+
331
+ Examples:
332
+ >>> import matplotlib.pyplot as plt
333
+ >>> manager = OutputManager(Path("/tmp/output"), "test")
334
+ >>> manager.create()
335
+ >>> fig, ax = plt.subplots()
336
+ >>> path = manager.save_plot(AnalysisDomain.SPECTRAL, "fft", fig)
337
+ >>> path.name
338
+ 'spectral_fft.png'
339
+ """
340
+ plots_dir = self._root / "plots"
341
+ plots_dir.mkdir(parents=True, exist_ok=True)
342
+
343
+ filename = f"{domain.value}_{name}.{format}"
344
+ filepath = plots_dir / filename
345
+
346
+ fig.savefig(filepath, format=format, dpi=dpi, bbox_inches="tight")
347
+
348
+ return filepath
349
+
350
+ def save_text(
351
+ self,
352
+ name: str,
353
+ content: str,
354
+ subdir: str | None = None,
355
+ ) -> Path:
356
+ """Save text content to file.
357
+
358
+ Args:
359
+ name: Filename (with extension).
360
+ content: Text content to write.
361
+ subdir: Optional subdirectory within root.
362
+
363
+ Returns:
364
+ Path to the saved text file.
365
+
366
+ Examples:
367
+ >>> manager = OutputManager(Path("/tmp/output"), "test")
368
+ >>> manager.create()
369
+ >>> path = manager.save_text("summary.txt", "Analysis complete")
370
+ >>> path.name
371
+ 'summary.txt'
372
+ """
373
+ target_dir = self._root / subdir if subdir else self._root
374
+ target_dir.mkdir(parents=True, exist_ok=True)
375
+
376
+ filepath = target_dir / name
377
+ filepath.write_text(content)
378
+
379
+ return filepath