LbAPLocal 0.12.2__tar.gz → 0.14.0__tar.gz

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 (168) hide show
  1. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/.gitlab-ci.yml +7 -1
  2. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/PKG-INFO +1 -1
  3. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/src/LbAPLocal/cli/legacy.py +14 -6
  4. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/src/LbAPLocal/cli/main.py +101 -2
  5. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/src/LbAPLocal/cli/wizards.py +8 -4
  6. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/src/LbAPLocal/handlers/anaprod.py +4 -2
  7. lbaplocal-0.14.0/src/LbAPLocal/reason_analysis.py +281 -0
  8. lbaplocal-0.14.0/src/LbAPLocal/reason_driver.py +196 -0
  9. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/src/LbAPLocal/testing.py +3 -2
  10. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/src/LbAPLocal/utils.py +18 -0
  11. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/src/LbAPLocal.egg-info/PKG-INFO +1 -1
  12. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/src/LbAPLocal.egg-info/SOURCES.txt +5 -0
  13. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/src/LbAPLocal.egg-info/scm_file_list.json +6 -1
  14. lbaplocal-0.14.0/src/LbAPLocal.egg-info/scm_version.json +8 -0
  15. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/test_cli.py +10 -0
  16. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/test_cwl_cli.py +4 -2
  17. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/test_handlers.py +1 -1
  18. lbaplocal-0.14.0/tests/test_parse_yaml_catalog.py +87 -0
  19. lbaplocal-0.14.0/tests/test_reason_analysis.py +529 -0
  20. lbaplocal-0.14.0/tests/test_reason_driver_integration.py +67 -0
  21. lbaplocal-0.12.2/src/LbAPLocal.egg-info/scm_version.json +0 -8
  22. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/.coveragerc +0 -0
  23. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/.gitattributes +0 -0
  24. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/.gitignore +0 -0
  25. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/.pre-commit-config.yaml +0 -0
  26. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/CONTRIBUTING.md +0 -0
  27. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/LICENSE +0 -0
  28. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/README.md +0 -0
  29. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/example.json +0 -0
  30. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/pyproject.toml +0 -0
  31. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/setup.cfg +0 -0
  32. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/src/LbAPLocal/__init__.py +0 -0
  33. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/src/LbAPLocal/cli/__init__.py +0 -0
  34. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/src/LbAPLocal/cli/cvmfs.py +0 -0
  35. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/src/LbAPLocal/cli/decfile_lexer.py +0 -0
  36. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/src/LbAPLocal/cli/generate.py +0 -0
  37. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/src/LbAPLocal/cli/mc_lookup.py +0 -0
  38. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/src/LbAPLocal/cli/mc_wizard.py +0 -0
  39. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/src/LbAPLocal/cli/textual_ui.py +0 -0
  40. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/src/LbAPLocal/cwl/__init__.py +0 -0
  41. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/src/LbAPLocal/cwl/app_runner.py +0 -0
  42. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/src/LbAPLocal/cwl/generate_replica_map.py +0 -0
  43. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/src/LbAPLocal/cwl/quickjs_engine.py +0 -0
  44. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/src/LbAPLocal/cwl/rewrite.py +0 -0
  45. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/src/LbAPLocal/cwl/runner.py +0 -0
  46. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/src/LbAPLocal/handlers/__init__.py +0 -0
  47. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/src/LbAPLocal/handlers/mc_request.py +0 -0
  48. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/src/LbAPLocal/job_slice.py +0 -0
  49. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/src/LbAPLocal/log_parsing.py +0 -0
  50. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/src/LbAPLocal/models.py +0 -0
  51. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/src/LbAPLocal.egg-info/dependency_links.txt +0 -0
  52. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/src/LbAPLocal.egg-info/entry_points.txt +0 -0
  53. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/src/LbAPLocal.egg-info/requires.txt +0 -0
  54. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/src/LbAPLocal.egg-info/top_level.txt +0 -0
  55. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/capture_fixtures.py +0 -0
  56. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/cassettes/test_execute/test_run_test[2016_MagDown_PromptMC_D02KK-2444].yaml +0 -0
  57. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/cassettes/test_execute/test_run_test[2016_MagDown_PromptMC_D02KK_noautoconf-2444].yaml +0 -0
  58. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/cassettes/test_execute/test_run_test[2016_MagUp_PromptMC_D02KK-2393].yaml +0 -0
  59. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/cassettes/test_execute/test_run_test[2016_MagUp_PromptMC_D02KK_noautoconf-2393].yaml +0 -0
  60. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/conftest.py +0 -0
  61. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/.gitignore +0 -0
  62. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/.gitlab-ci.yml +0 -0
  63. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/AnalysisProductions.xenv +0 -0
  64. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/BsToJpsiPhi/Bc2Bspi2JpsiPhi.py +0 -0
  65. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/BsToJpsiPhi/Bd2JpsiKstar.py +0 -0
  66. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/BsToJpsiPhi/Bd2JpsiKstar_stripping.py +0 -0
  67. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/BsToJpsiPhi/Bs2DsPi.py +0 -0
  68. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/BsToJpsiPhi/Bs2JpsiPhi.py +0 -0
  69. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/BsToJpsiPhi/Bs2JpsiPhi_Prompt.py +0 -0
  70. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/BsToJpsiPhi/Bs2JpsiPhi_Prompt_PVMixer.py +0 -0
  71. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/BsToJpsiPhi/Bs2JpsiPhi_Swave.py +0 -0
  72. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/BsToJpsiPhi/Bu2JpsiKplus.py +0 -0
  73. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/BsToJpsiPhi/Bu2JpsiKplus_stripping.py +0 -0
  74. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/BsToJpsiPhi/LbJpsipK.py +0 -0
  75. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/BsToJpsiPhi/README.md +0 -0
  76. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/BsToJpsiPhi/helpers/__init__.py +0 -0
  77. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/BsToJpsiPhi/helpers/stripping.py +0 -0
  78. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/BsToJpsiPhi/helpers/tuple_maker.py +0 -0
  79. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/BsToJpsiPhi/info.yaml +0 -0
  80. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/BsToJpsiPhi/sequence_setup.py +0 -0
  81. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/COPYING +0 -0
  82. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/RDs/README.md +0 -0
  83. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/RDs/Run1_Signal.py +0 -0
  84. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/RDs/Run2_Signal.py +0 -0
  85. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/RDs/helpers/__init__.py +0 -0
  86. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/RDs/helpers/signal_tuple_maker.py +0 -0
  87. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/RDs/info.yaml +0 -0
  88. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/README.md +0 -0
  89. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/__init__.py +0 -0
  90. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/cbtesting/2011_datatype.py +0 -0
  91. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/cbtesting/2012_datatype.py +0 -0
  92. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/cbtesting/2015_datatype.py +0 -0
  93. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/cbtesting/2016_datatype.py +0 -0
  94. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/cbtesting/2017_datatype.py +0 -0
  95. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/cbtesting/2018_datatype.py +0 -0
  96. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/cbtesting/B0ToK0ee.py +0 -0
  97. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/cbtesting/B0ToK0mumu.py +0 -0
  98. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/cbtesting/BpToKee.py +0 -0
  99. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/cbtesting/BpToKmumu.py +0 -0
  100. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/cbtesting/LbToL0ee.py +0 -0
  101. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/cbtesting/LbToL0eeSS.py +0 -0
  102. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/cbtesting/LbToL0emu.py +0 -0
  103. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/cbtesting/LbToL0emuSS.py +0 -0
  104. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/cbtesting/LbToL0mumu.py +0 -0
  105. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/cbtesting/LbToL0mumuSS.py +0 -0
  106. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/cbtesting/README.md +0 -0
  107. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/cbtesting/__init__.py +0 -0
  108. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/cbtesting/general_options.py +0 -0
  109. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/cbtesting/helpers/MC_tuple_maker.py +0 -0
  110. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/cbtesting/helpers/__init__.py +0 -0
  111. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/cbtesting/helpers/decay_descriptors.py +0 -0
  112. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/cbtesting/helpers/locations.py +0 -0
  113. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/cbtesting/helpers/related_info.py +0 -0
  114. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/cbtesting/helpers/stripping.py +0 -0
  115. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/cbtesting/helpers/tags.py +0 -0
  116. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/cbtesting/helpers/tuple_maker.py +0 -0
  117. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/cbtesting/info.yaml +0 -0
  118. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/cbtesting/mc_general_options_DST.py +0 -0
  119. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/cbtesting/mc_general_options_FDST.py +0 -0
  120. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/cbtesting/mc_general_options_FMDST.py +0 -0
  121. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/cbtesting/mc_general_options_MDST.py +0 -0
  122. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/cbtesting/mc_samples/MC_Bd2JpsiKs_ee.py +0 -0
  123. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/cbtesting/mc_samples/MC_Bd2JpsiKs_mm.py +0 -0
  124. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/cbtesting/mc_samples/MC_Bd2Ksee.py +0 -0
  125. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/cbtesting/mc_samples/MC_Bd2Ksmm.py +0 -0
  126. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/cbtesting/mc_samples/MC_Bd2PsiKs_ee.py +0 -0
  127. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/cbtesting/mc_samples/MC_Bd2PsiKs_mm.py +0 -0
  128. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/cbtesting/mc_samples/MC_Bu2JpsiK_ee.py +0 -0
  129. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/cbtesting/mc_samples/MC_Bu2JpsiK_mm.py +0 -0
  130. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/cbtesting/mc_samples/MC_Lb2JpsiL_ee.py +0 -0
  131. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/cbtesting/mc_samples/MC_Lb2JpsiL_mm.py +0 -0
  132. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/cbtesting/mc_samples/MC_Lb2Lee.py +0 -0
  133. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/cbtesting/mc_samples/MC_Lb2Lem.py +0 -0
  134. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/cbtesting/mc_samples/MC_Lb2Lmm.py +0 -0
  135. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/cbtesting/mc_samples/MC_Lb2PsiL_ee.py +0 -0
  136. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/cbtesting/mc_samples/MC_Lb2PsiL_mm.py +0 -0
  137. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/cbtesting/sequence_setup.py +0 -0
  138. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/cbtesting/stripping_seq.py +0 -0
  139. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/executabletests/2016_MagDown_PromptMC_D02KK_autoconf.py +0 -0
  140. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/executabletests/2016_MagUp_PromptMC_D02KK_autoconf.py +0 -0
  141. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/executabletests/info.yaml +0 -0
  142. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/data-pkg-repo/executabletests/ntuple_options.py +0 -0
  143. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/example-logs/error-failed-to-read-file.log +0 -0
  144. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/example-logs/error-illegal-instruction.log +0 -0
  145. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/example-logs/error-missing-shared-library.log +0 -0
  146. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/example-logs/error-platform-unsupported.log +0 -0
  147. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/example-logs/error-related-info-missing.log +0 -0
  148. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/example-logs/good-DaVinci_00110296_00000038_1.log +0 -0
  149. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/example-logs/good-DaVinci_00110296_00000194_1.log +0 -0
  150. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/data/example-logs/good-Gauss_00104988_00000011_1.log +0 -0
  151. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/fixture_loader.py +0 -0
  152. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/fixtures/executabletests/2016_MagDown_PromptMC_D02KK/query_result.json +0 -0
  153. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/fixtures/executabletests/2016_MagUp_PromptMC_D02KK/query_result.json +0 -0
  154. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/fixtures/executabletests/manifest.yaml +0 -0
  155. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/reslice_fixture.py +0 -0
  156. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/test-input-dst/00070767_00000711_7.AllStreams_slice.dst +0 -0
  157. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/test-input-dst/00070793_00000685_7.AllStreams_slice.dst +0 -0
  158. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/test_app_runner.py +0 -0
  159. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/test_cwl_runner.py +0 -0
  160. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/test_execute.py +0 -0
  161. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/test_generate.py +0 -0
  162. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/test_get_mc.py +0 -0
  163. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/test_input_resolution.py +0 -0
  164. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/test_local_datapkg.py +0 -0
  165. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/test_log_parse.py +0 -0
  166. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/test_mc_lookup.py +0 -0
  167. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/test_no_dirac_cwl.py +0 -0
  168. {lbaplocal-0.12.2 → lbaplocal-0.14.0}/tests/test_quickjs_engine.py +0 -0
@@ -16,7 +16,7 @@ pixi:
16
16
  parallel:
17
17
  matrix:
18
18
  - task: pre-commit run --all-files
19
- - task: pytest --runslow
19
+ - task: pytest --runslow --cov-report=xml
20
20
  rules:
21
21
  - when: always
22
22
  image: gitlab-registry.cern.ch/lhcb-docker/os-base/alma9-devel:latest
@@ -28,6 +28,12 @@ pixi:
28
28
  - source ~/.bashrc || true
29
29
  script:
30
30
  - pixi run $task
31
+ coverage: '/TOTAL.*\s+(\d+%)$/'
32
+ artifacts:
33
+ reports:
34
+ coverage_report:
35
+ coverage_format: cobertura
36
+ path: coverage.xml
31
37
 
32
38
  test-build:
33
39
  stage: test
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: LbAPLocal
3
- Version: 0.12.2
3
+ Version: 0.14.0
4
4
  Summary: Tool to locally run tests for AnalysisProductions
5
5
  Author: LHCb
6
6
  Project-URL: Homepage, https://gitlab.cern.ch/lhcb-dpa/analysis-productions/lbaplocal
@@ -20,7 +20,7 @@ from typing import Iterable
20
20
 
21
21
  import click
22
22
  from apd.authentication import get_auth_headers
23
- from LbAPCommon import parse_yaml, render_yaml, validate_yaml
23
+ from LbAPCommon import render_yaml, validate_yaml
24
24
 
25
25
  from ..utils import (
26
26
  LBAPI_HOST,
@@ -28,6 +28,7 @@ from ..utils import (
28
28
  check_production,
29
29
  inside_ap_datapkg,
30
30
  logging_subprocess_run,
31
+ parse_yaml_with_catalog,
31
32
  production_name_type,
32
33
  production_to_versions,
33
34
  validate_environment,
@@ -63,7 +64,14 @@ def main():
63
64
 
64
65
  NOTE: This is the legacy CLI. Consider using the new 'lb-ap' command instead.
65
66
  """
66
- pass
67
+ click.secho(
68
+ "WARNING: lb-ap-legacy will be retired soon; please switch to 'lb-ap'.\n"
69
+ "If anything stops you from using 'lb-ap', please open an issue so it "
70
+ "can be resolved ASAP:\n"
71
+ "https://gitlab.cern.ch/lhcb-dpa/analysis-productions/lbaplocal/-/work_items",
72
+ fg="yellow",
73
+ err=True,
74
+ )
67
75
 
68
76
 
69
77
  @main.command()
@@ -274,7 +282,7 @@ def list(production_name):
274
282
  # Get rendered yaml and find all the production names
275
283
  with open(os.path.join(production_name, "info.yaml"), "rt") as fp:
276
284
  raw_yaml = fp.read()
277
- job_data = parse_yaml(render_yaml(raw_yaml))
285
+ job_data = parse_yaml_with_catalog(render_yaml(raw_yaml))
278
286
  for job_name in job_data:
279
287
  click.echo(f"* {job_name}")
280
288
  else:
@@ -299,7 +307,7 @@ def render(production_name):
299
307
  render = render_yaml(raw_yaml)
300
308
  click.echo(render)
301
309
  try:
302
- job_data = parse_yaml(render)
310
+ job_data = parse_yaml_with_catalog(render)
303
311
  warnings = validate_yaml(job_data, ".", production_name)
304
312
  except Exception:
305
313
  click.secho("Rendered YAML has errors!", fg="red")
@@ -327,7 +335,7 @@ def validate(production_name):
327
335
  render = render_yaml(raw_yaml)
328
336
 
329
337
  try:
330
- job_data = parse_yaml(render)
338
+ job_data = parse_yaml_with_catalog(render)
331
339
  except Exception as e:
332
340
  click.secho("Error parsing YAML!", fg="red")
333
341
  raise click.ClickException(str(e))
@@ -401,7 +409,7 @@ def test(production_name, job_name, dependent_input, n_events, validate):
401
409
  with open(os.path.join(production_name, "info.yaml"), "rt") as fp:
402
410
  raw_yaml = fp.read()
403
411
  # TODO do we really need to redo parsing and validating when prepare test already does it?
404
- prod_data = parse_yaml(render_yaml(raw_yaml))
412
+ prod_data = parse_yaml_with_catalog(render_yaml(raw_yaml))
405
413
  yaml_warnings = validate_yaml(prod_data, ".", production_name)
406
414
  try:
407
415
  job_data = prod_data[job_name]
@@ -1830,6 +1830,13 @@ def test(
1830
1830
  help="Validate and render configuration without executing the test",
1831
1831
  ),
1832
1832
  ] = False,
1833
+ no_reasons: Annotated[
1834
+ bool,
1835
+ typer.Option(
1836
+ "--no-reasons",
1837
+ help="Skip LHCbDIRAC reason analysis of failed steps",
1838
+ ),
1839
+ ] = False,
1833
1840
  ):
1834
1841
  """
1835
1842
  🧪 Test a job using CWL workflow
@@ -1896,7 +1903,9 @@ def test(
1896
1903
  )
1897
1904
  raise typer.Exit(code=1)
1898
1905
 
1899
- from LbAPCommon import parse_yaml, render_yaml, validate_yaml
1906
+ from LbAPCommon import render_yaml, validate_yaml
1907
+
1908
+ from ..utils import parse_yaml_with_catalog
1900
1909
 
1901
1910
  raw_yaml = info_yaml_path.read_text()
1902
1911
  rendered = render_yaml(raw_yaml)
@@ -1908,7 +1917,7 @@ def test(
1908
1917
  syntax = Syntax(rendered, "yaml", theme="monokai", line_numbers=True)
1909
1918
  console.print(syntax)
1910
1919
 
1911
- jobs_data = parse_yaml(rendered, production_name, None)
1920
+ jobs_data = parse_yaml_with_catalog(rendered, production_name, None)
1912
1921
 
1913
1922
  if job_name not in jobs_data:
1914
1923
  available_jobs = list(jobs_data.keys())
@@ -1948,6 +1957,7 @@ def test(
1948
1957
  pick_smallest=pick_smallest,
1949
1958
  pretty=pretty,
1950
1959
  download=download,
1960
+ no_reasons=no_reasons,
1951
1961
  )
1952
1962
 
1953
1963
 
@@ -2296,6 +2306,13 @@ def parse_log_cmd(
2296
2306
  help="Minimum number of instances required to show WARNING messages",
2297
2307
  ),
2298
2308
  ] = 5,
2309
+ reasons: Annotated[
2310
+ bool,
2311
+ typer.Option(
2312
+ "--reasons",
2313
+ help="Also classify failures with LHCbDIRAC's reason engine (via lb-dirac)",
2314
+ ),
2315
+ ] = False,
2299
2316
  ):
2300
2317
  """Analyse a Gaudi log file and show a summary with advice.
2301
2318
 
@@ -2309,10 +2326,61 @@ def parse_log_cmd(
2309
2326
  analysis = analyse_log(log_text, suppress=suppress)
2310
2327
  render_log_analysis(analysis, console)
2311
2328
 
2329
+ if reasons:
2330
+ from ..reason_analysis import (
2331
+ ReasonAnalysisUnavailable,
2332
+ render_reason_analysis,
2333
+ run_reason_analysis,
2334
+ steps_from_log_files,
2335
+ )
2336
+
2337
+ try:
2338
+ reason_result = run_reason_analysis(steps_from_log_files([log_file]))
2339
+ except ReasonAnalysisUnavailable as e:
2340
+ console.print(f"[dim]Reason analysis unavailable: {e}[/dim]")
2341
+ else:
2342
+ render_reason_analysis(reason_result, console)
2343
+
2312
2344
  if analysis.has_fatal_issues:
2313
2345
  raise typer.Exit(code=1)
2314
2346
 
2315
2347
 
2348
+ @app.command(name="analyse-job")
2349
+ def analyse_job_cmd(
2350
+ path: Annotated[
2351
+ Path,
2352
+ typer.Argument(
2353
+ help="Run/output directory or a single Gaudi log file to analyse",
2354
+ exists=True,
2355
+ ),
2356
+ ],
2357
+ ):
2358
+ """Classify failures using LHCbDIRAC's reason engine (runs via lb-dirac).
2359
+
2360
+ Scans application logs (pairing them with prodConf JSONs when present)
2361
+ against the same curated failure patterns used by the production
2362
+ TransformationDebugAgent.
2363
+ """
2364
+ from ..reason_analysis import (
2365
+ ReasonAnalysisUnavailable,
2366
+ render_reason_analysis,
2367
+ run_reason_analysis,
2368
+ steps_from_path,
2369
+ )
2370
+
2371
+ steps = steps_from_path(path)
2372
+ if not steps:
2373
+ err_console.print(f"[bold red]✗ No .log files found under:[/bold red] {path}")
2374
+ raise typer.Exit(code=1)
2375
+ console.print(f"[dim]Analysing {len(steps)} step log(s) via lb-dirac…[/dim]")
2376
+ try:
2377
+ analysis = run_reason_analysis(steps)
2378
+ except ReasonAnalysisUnavailable as e:
2379
+ err_console.print(f"[bold red]✗ Reason analysis unavailable:[/bold red] {e}")
2380
+ raise typer.Exit(code=1)
2381
+ render_reason_analysis(analysis, console)
2382
+
2383
+
2316
2384
  # ============================================================================
2317
2385
  # Reproducer Commands
2318
2386
  # ============================================================================
@@ -3877,6 +3945,13 @@ def cwl_test(
3877
3945
  help="Download input files locally instead of using remote protocol where available",
3878
3946
  ),
3879
3947
  ] = False,
3948
+ no_reasons: Annotated[
3949
+ bool,
3950
+ typer.Option(
3951
+ "--no-reasons",
3952
+ help="Skip LHCbDIRAC reason analysis of failed steps",
3953
+ ),
3954
+ ] = False,
3880
3955
  ):
3881
3956
  """
3882
3957
  🧪 Test a job using CWL workflow
@@ -4557,6 +4632,7 @@ def cwl_test(
4557
4632
 
4558
4633
  # Analyse log files to show what went wrong
4559
4634
  _show_log_analysis_for_files(log_files, err_console)
4635
+ _maybe_show_reason_analysis(log_files, err_console, no_reasons)
4560
4636
 
4561
4637
  raise typer.Exit(code=1)
4562
4638
 
@@ -4707,6 +4783,29 @@ def _show_log_analysis_for_files(log_files: list[Path], console) -> None:
4707
4783
  )
4708
4784
 
4709
4785
 
4786
+ def _maybe_show_reason_analysis(
4787
+ log_files: list[Path], console, no_reasons: bool
4788
+ ) -> None:
4789
+ """Run the LHCbDIRAC reason engine over failed-run logs. Never raises."""
4790
+ from ..reason_analysis import (
4791
+ ReasonAnalysisUnavailable,
4792
+ reason_analysis_disabled,
4793
+ render_reason_analysis,
4794
+ run_reason_analysis,
4795
+ steps_from_log_files,
4796
+ )
4797
+
4798
+ if no_reasons or reason_analysis_disabled() or not log_files:
4799
+ return
4800
+ console.print("\n[dim]Running LHCbDIRAC reason analysis (lb-dirac)…[/dim]")
4801
+ try:
4802
+ analysis = run_reason_analysis(steps_from_log_files(log_files))
4803
+ except ReasonAnalysisUnavailable as e:
4804
+ console.print(f"[dim]Reason analysis unavailable: {e}[/dim]")
4805
+ return
4806
+ render_reason_analysis(analysis, console)
4807
+
4808
+
4710
4809
  # ============================================================================
4711
4810
  # Helper Functions
4712
4811
  # ============================================================================
@@ -247,7 +247,9 @@ def test_wizard(productions: Optional[List[str]] = None) -> dict:
247
247
  Returns:
248
248
  Dictionary with test parameters or None if cancelled
249
249
  """
250
- from LbAPCommon import parse_yaml, render_yaml
250
+ from LbAPCommon import render_yaml
251
+
252
+ from ..utils import parse_yaml_with_catalog
251
253
 
252
254
  welcome_wizard()
253
255
 
@@ -271,7 +273,7 @@ def test_wizard(productions: Optional[List[str]] = None) -> dict:
271
273
  if info_yaml_path.exists():
272
274
  with open(info_yaml_path, "rt") as fp:
273
275
  raw_yaml = fp.read()
274
- job_data = parse_yaml(render_yaml(raw_yaml))
276
+ job_data = parse_yaml_with_catalog(render_yaml(raw_yaml))
275
277
  jobs = list(job_data.keys())
276
278
 
277
279
  if jobs:
@@ -338,7 +340,9 @@ def cwl_generate_wizard(productions: Optional[List[str]] = None) -> dict:
338
340
  Returns:
339
341
  Dictionary with generation parameters or None if cancelled
340
342
  """
341
- from LbAPCommon import parse_yaml, render_yaml
343
+ from LbAPCommon import render_yaml
344
+
345
+ from ..utils import parse_yaml_with_catalog
342
346
 
343
347
  welcome_wizard()
344
348
 
@@ -372,7 +376,7 @@ def cwl_generate_wizard(productions: Optional[List[str]] = None) -> dict:
372
376
  if info_yaml_path.exists():
373
377
  with open(info_yaml_path, "rt") as fp:
374
378
  raw_yaml = fp.read()
375
- job_data = parse_yaml(render_yaml(raw_yaml))
379
+ job_data = parse_yaml_with_catalog(render_yaml(raw_yaml))
376
380
  jobs = list(job_data.keys())
377
381
 
378
382
  if jobs:
@@ -43,12 +43,14 @@ class APHandler:
43
43
  return [dirname(f) for f in folders]
44
44
 
45
45
  def list_jobs(self, production: str) -> list[str]:
46
- from LbAPCommon import parse_yaml, render_yaml
46
+ from LbAPCommon import render_yaml
47
+
48
+ from LbAPLocal.utils import parse_yaml_with_catalog
47
49
 
48
50
  info_yaml = Path(production) / "info.yaml"
49
51
  raw = info_yaml.read_text()
50
52
  rendered = render_yaml(raw)
51
- jobs = parse_yaml(rendered, production, None)
53
+ jobs = parse_yaml_with_catalog(rendered, production, None)
52
54
  return list(jobs.keys())
53
55
 
54
56
  def generate_request_yaml(
@@ -0,0 +1,281 @@
1
+ ###############################################################################
2
+ # (c) Copyright 2026 CERN for the benefit of the LHCb Collaboration #
3
+ # #
4
+ # This software is distributed under the terms of the GNU General Public #
5
+ # Licence version 3 (GPL Version 3), copied verbatim in the file "COPYING". #
6
+ # #
7
+ # In applying this licence, CERN does not waive the privileges and immunities #
8
+ # granted to it by virtue of its status as an Intergovernmental Organization #
9
+ # or submit itself to any jurisdiction. #
10
+ ###############################################################################
11
+ """Host side of the LHCbDIRAC reason-engine integration.
12
+
13
+ Discovers local run artifacts, runs ``reason_driver.py`` inside the
14
+ ``lb-dirac`` (CVMFS LHCbDIRAC) environment, parses its JSON output and
15
+ renders the result with Rich. See the accepted design
16
+ "LbAPLocal Log Analysis via lb-dirac".
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import importlib.util
22
+ import json
23
+ import os
24
+ import subprocess
25
+ import tempfile
26
+ from collections.abc import Iterable
27
+ from dataclasses import dataclass
28
+ from pathlib import Path
29
+
30
+ SCHEMA_VERSION = 1
31
+
32
+
33
+ class ReasonAnalysisUnavailable(RuntimeError):
34
+ """Reason analysis could not run; callers degrade gracefully."""
35
+
36
+
37
+ class ReasonAnalysisError(ReasonAnalysisUnavailable):
38
+ """The driver produced output we cannot interpret."""
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class ReasonHit:
43
+ type: str
44
+ category: str
45
+ match: str
46
+ explanation: str | None
47
+ issue_url: str | None
48
+ scope: str
49
+ priority: int
50
+ count: int
51
+
52
+
53
+ @dataclass(frozen=True)
54
+ class StepReasons:
55
+ name: str
56
+ mode: str
57
+ reasons: list[ReasonHit]
58
+
59
+
60
+ @dataclass(frozen=True)
61
+ class ReasonAnalysis:
62
+ lhcbdirac_version: str | None
63
+ steps: list[StepReasons]
64
+ errors: list[str]
65
+
66
+
67
+ def parse_driver_output(text: str) -> ReasonAnalysis:
68
+ """Parse and validate the driver's JSON output.
69
+
70
+ Raises:
71
+ ReasonAnalysisError: on invalid JSON, schema mismatch, or missing fields.
72
+ """
73
+ try:
74
+ data = json.loads(text)
75
+ except json.JSONDecodeError as e:
76
+ raise ReasonAnalysisError(f"driver returned invalid JSON: {e}") from e
77
+ if not isinstance(data, dict):
78
+ raise ReasonAnalysisError(
79
+ f"driver output is not a JSON object: {type(data).__name__}"
80
+ )
81
+ if data.get("schema_version") != SCHEMA_VERSION:
82
+ raise ReasonAnalysisError(
83
+ "unexpected schema_version: "
84
+ f"{data.get('schema_version')!r} (expected {SCHEMA_VERSION})"
85
+ )
86
+ try:
87
+ steps = []
88
+ for raw_step in data.get("steps", []):
89
+ reasons = [
90
+ ReasonHit(
91
+ type=r["type"],
92
+ category=r["category"],
93
+ match=r["match"],
94
+ explanation=r.get("explanation"),
95
+ issue_url=r.get("issue_url"),
96
+ scope=r.get("scope", "unattributed"),
97
+ priority=int(r.get("priority", 0)),
98
+ count=int(r.get("count", 1)),
99
+ )
100
+ for r in raw_step.get("reasons", [])
101
+ ]
102
+ steps.append(
103
+ StepReasons(
104
+ name=raw_step["name"],
105
+ mode=raw_step.get("mode", "flat"),
106
+ reasons=reasons,
107
+ )
108
+ )
109
+ engine = data.get("engine") or {}
110
+ return ReasonAnalysis(
111
+ lhcbdirac_version=engine.get("lhcbdirac_version"),
112
+ steps=steps,
113
+ errors=[str(e) for e in data.get("errors", [])],
114
+ )
115
+ except (KeyError, TypeError, ValueError, AttributeError) as e:
116
+ raise ReasonAnalysisError(f"malformed driver output: {e!r}") from e
117
+
118
+
119
+ @dataclass(frozen=True)
120
+ class StepInput:
121
+ name: str
122
+ log: Path
123
+ prodconf: Path | None
124
+
125
+
126
+ def _find_prodconf(log: Path) -> Path | None:
127
+ """Pair a step log with its prodConf JSON, mirroring job_slice.find_failed_step."""
128
+ exact = log.parent / f"prodConf_{log.stem}.json"
129
+ if exact.is_file():
130
+ return exact
131
+ candidates = sorted(log.parent.glob(f"prodConf*{log.stem}*.json"))
132
+ return candidates[0] if candidates else None
133
+
134
+
135
+ def steps_from_log_files(log_files: Iterable[Path]) -> list[StepInput]:
136
+ """Build driver step inputs from application log paths."""
137
+ steps = []
138
+ for log in set(log_files):
139
+ steps.append(StepInput(name=log.stem, log=log, prodconf=_find_prodconf(log)))
140
+ steps.sort(key=lambda s: s.name)
141
+ return steps
142
+
143
+
144
+ def steps_from_path(path: Path) -> list[StepInput]:
145
+ """Discover step inputs from a run directory or a single log file.
146
+
147
+ A nonexistent path yields an empty list; callers decide how to report that.
148
+ """
149
+ if path.is_file():
150
+ return steps_from_log_files([path])
151
+ return steps_from_log_files(path.rglob("*.log"))
152
+
153
+
154
+ DRIVER_TIMEOUT = 120.0
155
+
156
+
157
+ def reason_analysis_disabled() -> bool:
158
+ """True when the user opted out via LBAP_NO_REASON_ANALYSIS."""
159
+ return bool(os.environ.get("LBAP_NO_REASON_ANALYSIS"))
160
+
161
+
162
+ def run_reason_analysis(
163
+ steps: list[StepInput], *, timeout: float = DRIVER_TIMEOUT
164
+ ) -> ReasonAnalysis:
165
+ """Run reason_driver.py under lb-dirac and return the parsed result.
166
+
167
+ Raises:
168
+ ReasonAnalysisUnavailable: for every infrastructure failure --
169
+ missing lb-dirac, timeout, driver crash, or malformed output.
170
+ """
171
+ if not steps:
172
+ raise ReasonAnalysisUnavailable("no log files to analyse")
173
+ spec = importlib.util.find_spec("LbAPLocal.reason_driver")
174
+ if spec is None or spec.origin is None:
175
+ raise ReasonAnalysisUnavailable("could not locate LbAPLocal.reason_driver")
176
+
177
+ request = {
178
+ "steps": [
179
+ {
180
+ "name": s.name,
181
+ "log": str(s.log),
182
+ "prodconf": str(s.prodconf) if s.prodconf else None,
183
+ }
184
+ for s in steps
185
+ ]
186
+ }
187
+ with tempfile.NamedTemporaryFile(
188
+ "w", suffix=".json", prefix="lbap-reasons-", delete=False
189
+ ) as f:
190
+ json.dump(request, f)
191
+ request_path = f.name
192
+ try:
193
+ try:
194
+ result = subprocess.run(
195
+ ["lb-dirac", "python", spec.origin, request_path],
196
+ capture_output=True,
197
+ text=True,
198
+ timeout=timeout,
199
+ )
200
+ except FileNotFoundError as e:
201
+ raise ReasonAnalysisUnavailable(
202
+ "lb-dirac not found (is CVMFS available?)"
203
+ ) from e
204
+ except subprocess.TimeoutExpired as e:
205
+ raise ReasonAnalysisUnavailable(
206
+ f"reason analysis timed out after {timeout:.0f}s"
207
+ ) from e
208
+ if result.returncode != 0:
209
+ detail = (result.stderr or result.stdout or "").strip()
210
+ raise ReasonAnalysisUnavailable(
211
+ f"driver failed (exit {result.returncode}): {detail[:500]}"
212
+ )
213
+ return parse_driver_output(result.stdout)
214
+ finally:
215
+ os.unlink(request_path)
216
+
217
+
218
+ _CATEGORY_STYLES = {
219
+ "KnownUnrecoverableFailure": "red",
220
+ "UnknownFailure": "red",
221
+ "EventTimeout": "dark_orange",
222
+ "RecoverableFailureWithManualIntervention": "yellow",
223
+ "WorkerNodeIssue": "yellow",
224
+ "RecoverableFailure": "yellow",
225
+ }
226
+ _MAX_MATCH_DISPLAY_LINES = 6
227
+
228
+
229
+ def render_reason_analysis(analysis: ReasonAnalysis, console) -> None:
230
+ """Render a ReasonAnalysis with Rich panels (one per step with findings)."""
231
+ from rich.console import Group
232
+ from rich.panel import Panel
233
+ from rich.text import Text
234
+
235
+ for step in analysis.steps:
236
+ if not step.reasons:
237
+ console.print(
238
+ f"[dim]Reason analysis ({step.name}): "
239
+ "no known failure patterns matched[/dim]"
240
+ )
241
+ continue
242
+ parts = []
243
+ border = "yellow"
244
+ for hit in step.reasons:
245
+ style = _CATEGORY_STYLES.get(hit.category, "yellow")
246
+ if style == "red":
247
+ border = "red"
248
+ header = Text()
249
+ header.append(hit.type, style=f"bold {style}")
250
+ header.append(f" ({hit.category}, {hit.scope}", style="dim")
251
+ if hit.count > 1:
252
+ header.append(f", {hit.count}×", style="dim")
253
+ header.append(")", style="dim")
254
+ parts.append(header)
255
+ if hit.explanation:
256
+ parts.append(Text(hit.explanation))
257
+ if hit.issue_url:
258
+ parts.append(Text(hit.issue_url, style="cyan underline"))
259
+ match_lines = hit.match.strip().splitlines()
260
+ snippet = "\n".join(match_lines[:_MAX_MATCH_DISPLAY_LINES])
261
+ if len(match_lines) > _MAX_MATCH_DISPLAY_LINES:
262
+ snippet += (
263
+ f"\n… ({len(match_lines) - _MAX_MATCH_DISPLAY_LINES} more lines)"
264
+ )
265
+ parts.append(Text(snippet, style="dim"))
266
+ parts.append(Text())
267
+ console.print(
268
+ Panel(
269
+ Group(*parts),
270
+ title=f"[bold]Reason analysis: {step.name}[/bold]"
271
+ + (" [dim](unsegmented)[/dim]" if step.mode == "flat" else ""),
272
+ title_align="left",
273
+ border_style=border,
274
+ )
275
+ )
276
+ for err in analysis.errors:
277
+ console.print(f"[dim]Reason analysis: {err}[/dim]")
278
+ if analysis.lhcbdirac_version:
279
+ console.print(
280
+ f"[dim]Reason engine: LHCbDIRAC {analysis.lhcbdirac_version}[/dim]"
281
+ )