rcdb 2.3.1__tar.gz → 2.4.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 (223) hide show
  1. {rcdb-2.3.1/rcdb.egg-info → rcdb-2.4.0}/PKG-INFO +4 -1
  2. {rcdb-2.3.1 → rcdb-2.4.0}/pyproject.toml +4 -1
  3. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/app_context.py +12 -0
  4. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/cli/app.py +6 -0
  5. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/cli/db.py +4 -0
  6. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/cli/rp.py +3 -0
  7. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/provider.py +40 -15
  8. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/version.py +1 -1
  9. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/__init__.py +88 -13
  10. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/modules/runs.py +5 -2
  11. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/modules/select_values.py +10 -3
  12. rcdb-2.4.0/rcdb/web/templates/db_error.html +20 -0
  13. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/select_values/index.html +51 -3
  14. {rcdb-2.3.1 → rcdb-2.4.0/rcdb.egg-info}/PKG-INFO +4 -1
  15. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb.egg-info/SOURCES.txt +3 -1
  16. rcdb-2.4.0/tests/test_web_db_failure.py +119 -0
  17. {rcdb-2.3.1 → rcdb-2.4.0}/MANIFEST.in +0 -0
  18. {rcdb-2.3.1 → rcdb-2.4.0}/README.md +0 -0
  19. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/__init__.py +0 -0
  20. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/__main__.py +0 -0
  21. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/alias.py +0 -0
  22. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/cli/__init__.py +0 -0
  23. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/cli/__main__.py +0 -0
  24. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/cli/add.py +0 -0
  25. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/cli/context.py +0 -0
  26. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/cli/file.py +0 -0
  27. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/cli/info.py +0 -0
  28. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/cli/ls.py +0 -0
  29. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/cli/repair/__init__.py +0 -0
  30. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/cli/repair/evio_files.py +0 -0
  31. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/cli/run.py +0 -0
  32. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/cli/select.py +0 -0
  33. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/cli/test_data.py +0 -0
  34. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/cli/web.py +0 -0
  35. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/coda_parser.py +0 -0
  36. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/config_parser.py +0 -0
  37. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/constants.py +0 -0
  38. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/errors.py +0 -0
  39. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/file_archiver.py +0 -0
  40. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/halld_daq_config_parser.py +0 -0
  41. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/lexer.py +0 -0
  42. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/log_format.py +0 -0
  43. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/model.py +0 -0
  44. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/stopwatch.py +0 -0
  45. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/update.py +0 -0
  46. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/__main__.py +0 -0
  47. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/modules/__init__.py +0 -0
  48. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/modules/conditions.py +0 -0
  49. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/modules/files.py +0 -0
  50. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/modules/logs.py +0 -0
  51. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/modules/statistics.py +0 -0
  52. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/pagination.py +0 -0
  53. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/run_table.py +0 -0
  54. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/DataTables-1.10.10/css/dataTables.bootstrap.css +0 -0
  55. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/DataTables-1.10.10/css/dataTables.bootstrap.min.css +0 -0
  56. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/DataTables-1.10.10/css/dataTables.foundation.css +0 -0
  57. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/DataTables-1.10.10/css/dataTables.foundation.min.css +0 -0
  58. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/DataTables-1.10.10/css/dataTables.jqueryui.css +0 -0
  59. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/DataTables-1.10.10/css/dataTables.jqueryui.min.css +0 -0
  60. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/DataTables-1.10.10/css/jquery.dataTables.css +0 -0
  61. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/DataTables-1.10.10/css/jquery.dataTables.min.css +0 -0
  62. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/DataTables-1.10.10/css/jquery.dataTables_themeroller.css +0 -0
  63. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/DataTables-1.10.10/images/sort_asc.png +0 -0
  64. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/DataTables-1.10.10/images/sort_asc_disabled.png +0 -0
  65. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/DataTables-1.10.10/images/sort_both.png +0 -0
  66. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/DataTables-1.10.10/images/sort_desc.png +0 -0
  67. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/DataTables-1.10.10/images/sort_desc_disabled.png +0 -0
  68. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/DataTables-1.10.10/js/dataTables.bootstrap.js +0 -0
  69. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/DataTables-1.10.10/js/dataTables.bootstrap.min.js +0 -0
  70. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/DataTables-1.10.10/js/dataTables.foundation.js +0 -0
  71. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/DataTables-1.10.10/js/dataTables.foundation.min.js +0 -0
  72. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/DataTables-1.10.10/js/dataTables.jqueryui.js +0 -0
  73. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/DataTables-1.10.10/js/dataTables.jqueryui.min.js +0 -0
  74. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/DataTables-1.10.10/js/jquery.dataTables.js +0 -0
  75. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/DataTables-1.10.10/js/jquery.dataTables.min.js +0 -0
  76. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/Responsive-2.0.0/css/responsive.bootstrap.css +0 -0
  77. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/Responsive-2.0.0/css/responsive.bootstrap.min.css +0 -0
  78. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/Responsive-2.0.0/css/responsive.dataTables.css +0 -0
  79. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/Responsive-2.0.0/css/responsive.dataTables.min.css +0 -0
  80. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/Responsive-2.0.0/css/responsive.foundation.css +0 -0
  81. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/Responsive-2.0.0/css/responsive.foundation.min.css +0 -0
  82. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/Responsive-2.0.0/css/responsive.jqueryui.css +0 -0
  83. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/Responsive-2.0.0/css/responsive.jqueryui.min.css +0 -0
  84. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/Responsive-2.0.0/js/dataTables.responsive.js +0 -0
  85. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/Responsive-2.0.0/js/dataTables.responsive.min.js +0 -0
  86. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/Responsive-2.0.0/js/responsive.bootstrap.js +0 -0
  87. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/Responsive-2.0.0/js/responsive.bootstrap.min.js +0 -0
  88. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/Responsive-2.0.0/js/responsive.foundation.js +0 -0
  89. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/Responsive-2.0.0/js/responsive.foundation.min.js +0 -0
  90. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/Responsive-2.0.0/js/responsive.jqueryui.js +0 -0
  91. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/Responsive-2.0.0/js/responsive.jqueryui.min.js +0 -0
  92. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/RowReorder-1.1.0/css/rowReorder.bootstrap.css +0 -0
  93. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/RowReorder-1.1.0/css/rowReorder.bootstrap.min.css +0 -0
  94. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/RowReorder-1.1.0/css/rowReorder.dataTables.css +0 -0
  95. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/RowReorder-1.1.0/css/rowReorder.dataTables.min.css +0 -0
  96. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/RowReorder-1.1.0/css/rowReorder.foundation.css +0 -0
  97. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/RowReorder-1.1.0/css/rowReorder.foundation.min.css +0 -0
  98. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/RowReorder-1.1.0/css/rowReorder.jqueryui.css +0 -0
  99. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/RowReorder-1.1.0/css/rowReorder.jqueryui.min.css +0 -0
  100. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/RowReorder-1.1.0/js/dataTables.rowReorder.js +0 -0
  101. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/RowReorder-1.1.0/js/dataTables.rowReorder.min.js +0 -0
  102. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/datatables.css +0 -0
  103. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/datatables.js +0 -0
  104. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/datatables.min.css +0 -0
  105. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/datatables.min.js +0 -0
  106. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/css/bootstrap-theme.css +0 -0
  107. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/css/bootstrap-theme.css.map +0 -0
  108. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/css/bootstrap-theme.min.css +0 -0
  109. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/css/bootstrap-theme.min.css.map +0 -0
  110. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/css/bootstrap.css +0 -0
  111. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/css/bootstrap.css.map +0 -0
  112. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/css/bootstrap.min.css +0 -0
  113. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/css/bootstrap.min.css.map +0 -0
  114. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/css/buttons.dataTables.min.css +0 -0
  115. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/css/dataTables.tableTools.css +0 -0
  116. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/css/jquery-ui.min.css +0 -0
  117. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/css/jquery-ui.structure.min.css +0 -0
  118. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/css/jquery-ui.theme.min.css +0 -0
  119. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/css/jquery.dataTables.min.css +0 -0
  120. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/css/jquery.dataTables.yadcf.css +0 -0
  121. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/css/jquery.jsonview.css +0 -0
  122. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/css/main.css +0 -0
  123. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/css/select.dataTables.min.css +0 -0
  124. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/css/webintellisense.css +0 -0
  125. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/favicon.png +0 -0
  126. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/fonts/glyphicons-halflings-regular.eot +0 -0
  127. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/fonts/glyphicons-halflings-regular.svg +0 -0
  128. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/fonts/glyphicons-halflings-regular.ttf +0 -0
  129. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/fonts/glyphicons-halflings-regular.woff +0 -0
  130. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/fonts/glyphicons-halflings-regular.woff2 +0 -0
  131. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/humans.txt +0 -0
  132. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/img/glyphicons-halflings-white.png +0 -0
  133. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/img/glyphicons-halflings.png +0 -0
  134. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/img/logo.png +0 -0
  135. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/js/bootstrap.js +0 -0
  136. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/js/bootstrap.min.js +0 -0
  137. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/js/clipboard.min.js +0 -0
  138. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/js/copy_clipboard.js +0 -0
  139. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/js/dataTables.buttons.min.js +0 -0
  140. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/js/dataTables.select.min.js +0 -0
  141. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/js/jquery-2.1.0.min.js +0 -0
  142. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/js/jquery-2.1.4.min.js +0 -0
  143. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/js/jquery-ui.min.js +0 -0
  144. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/js/jquery.dataTables.min.js +0 -0
  145. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/js/jquery.dataTables.yadcf.js +0 -0
  146. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/js/jquery.flot.min.js +0 -0
  147. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/js/jquery.jsonview.js +0 -0
  148. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/js/npm.js +0 -0
  149. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/js/webintellisense-ace.js +0 -0
  150. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/js/webintellisense-codemirror.js +0 -0
  151. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/js/webintellisense-textbox.js +0 -0
  152. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/js/webintellisense.js +0 -0
  153. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/robots.txt +0 -0
  154. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/style.css +0 -0
  155. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/404.html +0 -0
  156. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/admin/index.html +0 -0
  157. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/admin/layout.html +0 -0
  158. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/admin/user.html +0 -0
  159. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/admin/users.html +0 -0
  160. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/conditions/index.html +0 -0
  161. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/custom_column_run_table.html +0 -0
  162. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/default_run_table.html +0 -0
  163. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/errors/forbidden_page.html +0 -0
  164. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/errors/layout.html +0 -0
  165. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/errors/page_not_found.html +0 -0
  166. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/errors/server_error.html +0 -0
  167. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/files/index.html +0 -0
  168. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/files/info.html +0 -0
  169. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/files/not_found.html +0 -0
  170. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/frontend/create_profile.html +0 -0
  171. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/frontend/footers/help.html +0 -0
  172. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/frontend/login.html +0 -0
  173. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/frontend/login_openid.html +0 -0
  174. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/frontend/reauth.html +0 -0
  175. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/frontend/reset_password.html +0 -0
  176. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/frontend/search.html +0 -0
  177. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/frontend/signup.html +0 -0
  178. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/index.html +0 -0
  179. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/layout.html +0 -0
  180. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/layouts/base.html +0 -0
  181. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/layouts/base2.html +0 -0
  182. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/login.html +0 -0
  183. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/logs/index.html +0 -0
  184. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/macros/_ask_user_to_update_ie.html +0 -0
  185. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/macros/_form.html +0 -0
  186. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/macros/_google_analytics.html +0 -0
  187. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/macros/_misc.html +0 -0
  188. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/macros/_reset_password.html +0 -0
  189. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/render_pagination.html +0 -0
  190. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/run_search_box.html +0 -0
  191. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/runs/conditions.html +0 -0
  192. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/runs/example.html +0 -0
  193. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/runs/index.html +0 -0
  194. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/runs/info.html +0 -0
  195. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/runs/not_found.html +0 -0
  196. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/settings/layout.html +0 -0
  197. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/settings/password.html +0 -0
  198. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/settings/profile.html +0 -0
  199. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/show_entries.html +0 -0
  200. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/statistics/index.html +0 -0
  201. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/user/index.html +0 -0
  202. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/user/profile.html +0 -0
  203. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb.egg-info/dependency_links.txt +0 -0
  204. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb.egg-info/entry_points.txt +0 -0
  205. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb.egg-info/requires.txt +0 -0
  206. {rcdb-2.3.1 → rcdb-2.4.0}/rcdb.egg-info/top_level.txt +0 -0
  207. {rcdb-2.3.1 → rcdb-2.4.0}/setup.cfg +0 -0
  208. {rcdb-2.3.1 → rcdb-2.4.0}/tests/test_cli_add_condition.py +0 -0
  209. {rcdb-2.3.1 → rcdb-2.4.0}/tests/test_cli_add_file.py +0 -0
  210. {rcdb-2.3.1 → rcdb-2.4.0}/tests/test_cli_add_type.py +0 -0
  211. {rcdb-2.3.1 → rcdb-2.4.0}/tests/test_cli_file.py +0 -0
  212. {rcdb-2.3.1 → rcdb-2.4.0}/tests/test_cli_run.py +0 -0
  213. {rcdb-2.3.1 → rcdb-2.4.0}/tests/test_coda_parser.py +0 -0
  214. {rcdb-2.3.1 → rcdb-2.4.0}/tests/test_conditions.py +0 -0
  215. {rcdb-2.3.1 → rcdb-2.4.0}/tests/test_config_parser.py +0 -0
  216. {rcdb-2.3.1 → rcdb-2.4.0}/tests/test_get_run_periods.py +0 -0
  217. {rcdb-2.3.1 → rcdb-2.4.0}/tests/test_get_runs.py +0 -0
  218. {rcdb-2.3.1 → rcdb-2.4.0}/tests/test_halld_run_config_parser.py +0 -0
  219. {rcdb-2.3.1 → rcdb-2.4.0}/tests/test_save_files.py +0 -0
  220. {rcdb-2.3.1 → rcdb-2.4.0}/tests/test_select_runs.py +0 -0
  221. {rcdb-2.3.1 → rcdb-2.4.0}/tests/test_select_values.py +0 -0
  222. {rcdb-2.3.1 → rcdb-2.4.0}/tests/test_sql_schema_version.py +0 -0
  223. {rcdb-2.3.1 → rcdb-2.4.0}/tests/test_value_comparison.py +0 -0
@@ -1,9 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rcdb
3
- Version: 2.3.1
3
+ Version: 2.4.0
4
4
  Summary: RCDB - Run Conditions DataBase
5
5
  Author-email: Dmitry Romanov <romanov@jlab.org>
6
6
  License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/JeffersonLab/rcdb
8
+ Project-URL: Documentation, https://jeffersonlab.github.io/rcdb
9
+ Project-URL: Repository, https://github.com/JeffersonLab/rcdb.git
7
10
  Classifier: Development Status :: 5 - Production/Stable
8
11
  Classifier: Programming Language :: Python :: 3
9
12
  Classifier: Programming Language :: Python :: 3.9
@@ -50,8 +50,11 @@ dependencies = [
50
50
  examples = ["jsonpickle", "pandas"]
51
51
  dev = ["pyflakes>=3"]
52
52
 
53
+ [project.urls]
54
+ Homepage = "https://github.com/JeffersonLab/rcdb"
55
+ Documentation = "https://jeffersonlab.github.io/rcdb"
56
+ Repository = "https://github.com/JeffersonLab/rcdb.git"
53
57
 
54
- # If you want a console script, define it in [project.scripts]
55
58
  [project.scripts]
56
59
  rcdb = "rcdb.cli.app:rcdb_cli"
57
60
 
@@ -19,6 +19,18 @@ class RcdbApplicationContext(object):
19
19
  self._db_instance = RCDBProvider(self.connection_str)
20
20
  return self._db_instance
21
21
 
22
+ def close(self):
23
+ """Disconnect the database if one was opened.
24
+
25
+ Safe to call when no connection was ever made. The CLI registers this on
26
+ the Click context so the engine is disposed deterministically when the
27
+ command finishes, instead of waiting for garbage collection (which on
28
+ Python 3.13+ leaks a ``ResourceWarning: unclosed database``).
29
+ """
30
+ if self._db_instance is not None:
31
+ self._db_instance.close()
32
+ self._db_instance = None
33
+
22
34
  def require_connected_db(self) -> RCDBProvider:
23
35
  """Return the RCDBProvider, or fail with a clear CLI error if no connection was given.
24
36
 
@@ -60,6 +60,12 @@ def rcdb_cli(ctx, user_config, connection, config, verbose):
60
60
  for key, value in config:
61
61
  ctx.obj.set_config(key, value)
62
62
 
63
+ # Dispose the DB engine deterministically when the command finishes, rather
64
+ # than leaving it to garbage collection. On Python 3.13+ a lingering, GC'd
65
+ # SQLite connection emits "ResourceWarning: unclosed database" at an
66
+ # unpredictable time, which can leak into command output captured by tests.
67
+ ctx.call_on_close(ctx.obj.close)
68
+
63
69
  # Bo commands given
64
70
  if ctx.invoked_subcommand is None:
65
71
  # There is a connection but no subcommand
@@ -24,6 +24,7 @@ def db_command(ctx):
24
24
  print("ERROR connection string is missing.")
25
25
  exit(1)
26
26
  provider.connect(connection_str, check_version=False)
27
+ ctx.call_on_close(provider.close)
27
28
  query = select(SchemaVersion).order_by(SchemaVersion.version.desc())
28
29
  schema_version, = provider.session.execute(query).first()
29
30
  print("Schema version: {} - '{}'".format(schema_version.version, schema_version.comment))
@@ -88,6 +89,7 @@ def _print_sqlite_table_sizes(engine):
88
89
  @pass_rcdb_context
89
90
  def update(context):
90
91
  provider = RCDBProvider(context.connection_str, check_version=False)
92
+ click.get_current_context().call_on_close(provider.close)
91
93
 
92
94
  # Check something exists
93
95
  if not sqlalchemy.inspect(provider.engine).has_table(SchemaVersion.__tablename__):
@@ -117,6 +119,7 @@ def update(context):
117
119
  return
118
120
 
119
121
  provider = RCDBProvider(context.connection_str, check_version=False)
122
+ click.get_current_context().call_on_close(provider.close)
120
123
 
121
124
  # Create alias table
122
125
  Alias.__table__.create(provider.engine)
@@ -181,6 +184,7 @@ def init(context, drop_all, no_defaults, confirm, add_tests, add_cpp_tests):
181
184
  # That we will need for DB
182
185
  metadata = rcdb.model.Base.metadata
183
186
  provider = RCDBProvider(context.connection_str, check_version=False)
187
+ click.get_current_context().call_on_close(provider.close)
184
188
 
185
189
  # Drop all if needed
186
190
  if drop_all:
@@ -55,6 +55,7 @@ def add(context, name, description, run_min, run_max, start_date, end_date):
55
55
 
56
56
  provider = RCDBProvider()
57
57
  provider.connect(connection_str, check_version=False)
58
+ click.get_current_context().call_on_close(provider.close)
58
59
  session = provider.session
59
60
 
60
61
  # Convert date strings to Date objects if provided
@@ -104,6 +105,7 @@ def rm(context, period_id, yes):
104
105
 
105
106
  provider = RCDBProvider()
106
107
  provider.connect(connection_str, check_version=False)
108
+ click.get_current_context().call_on_close(provider.close)
107
109
  session = provider.session
108
110
 
109
111
  rp_item = session.query(RunPeriod).filter(RunPeriod.id == period_id).one_or_none()
@@ -142,6 +144,7 @@ def update(context, period_id, name, description, run_min, run_max, start_date,
142
144
 
143
145
  provider = RCDBProvider()
144
146
  provider.connect(connection_str, check_version=False)
147
+ click.get_current_context().call_on_close(provider.close)
145
148
  session = provider.session
146
149
 
147
150
  rp_item = session.query(RunPeriod).filter(RunPeriod.id == period_id).one_or_none()
@@ -133,26 +133,51 @@ class RCDBProvider(object):
133
133
  self._connection_string = connection_string
134
134
 
135
135
  if check_version:
136
- db_version = self.get_schema_version()
137
- if db_version != rcdb.SQL_SCHEMA_VERSION:
138
- message = "SQL schema version doesn't match. " \
139
- "Retrieved DB version is {0}, required version is {1}. " \
140
- .format(db_version, rcdb.SQL_SCHEMA_VERSION)
141
- if db_version is not None and db_version < rcdb.SQL_SCHEMA_VERSION:
142
- message += "The database schema is older than this RCDB version. " \
143
- "Run 'rcdb db update' to migrate the database to the current schema."
144
- else:
145
- message += "The database schema is newer than this RCDB version. " \
146
- "Update your RCDB installation to match the database."
147
- raise rcdb.errors.SqlSchemaVersionError(message)
136
+ # This version query is the first statement that actually opens a
137
+ # DB connection. If it fails (unreachable host, access denied, wrong
138
+ # schema, ...) the engine/session we just built must be disposed
139
+ # here: the exception propagates out of connect()/__init__, so the
140
+ # caller never receives this provider to disconnect() it themselves,
141
+ # and the engine's open connection would otherwise leak until GC.
142
+ try:
143
+ db_version = self.get_schema_version()
144
+ if db_version != rcdb.SQL_SCHEMA_VERSION:
145
+ message = "SQL schema version doesn't match. " \
146
+ "Retrieved DB version is {0}, required version is {1}. " \
147
+ .format(db_version, rcdb.SQL_SCHEMA_VERSION)
148
+ if db_version is not None and db_version < rcdb.SQL_SCHEMA_VERSION:
149
+ message += "The database schema is older than this RCDB version. " \
150
+ "Run 'rcdb db update' to migrate the database to the current schema."
151
+ else:
152
+ message += "The database schema is newer than this RCDB version. " \
153
+ "Update your RCDB installation to match the database."
154
+ raise rcdb.errors.SqlSchemaVersionError(message)
155
+ except Exception:
156
+ self.close()
157
+ raise
148
158
 
149
159
  # ------------------------------------------------
150
160
  # Closes connection to data
151
161
  # ------------------------------------------------
152
- def disconnect(self):
153
- """Closes connection to database"""
162
+ def close(self):
163
+ """Closes connection to database.
164
+
165
+ Closes the ORM session and disposes the SQLAlchemy engine so the
166
+ underlying connection pool releases its DBAPI connections. Without the
167
+ ``engine.dispose()`` the pooled SQLite/MySQL connection stays open until
168
+ the engine is garbage collected, which on Python 3.13+ surfaces as a
169
+ ``ResourceWarning: unclosed database`` at an unpredictable time.
170
+ """
154
171
  self._is_connected = False
155
- self.session.close()
172
+ if self.session is not None:
173
+ self.session.close()
174
+ if self.engine is not None:
175
+ self.engine.dispose()
176
+
177
+ # ``disconnect`` is the historical name for ``close``; kept as an alias so
178
+ # existing RCDB code and users keep working while new code standardizes on
179
+ # ``close()`` (the conventional name other resource-holding objects use).
180
+ disconnect = close
156
181
 
157
182
  # -------------------------------------------------
158
183
  # indicates ether the connection is open or not
@@ -1,5 +1,5 @@
1
1
  # String version. Used both for setup.py and for internal code
2
- version = '2.3.1'
2
+ version = '2.4.0'
3
3
 
4
4
  # version as the tuple of ints
5
5
  version_tuple = tuple(int(val) for val in version.split('.'))
@@ -4,6 +4,7 @@ from datetime import datetime
4
4
  from urllib.parse import urlparse
5
5
 
6
6
  from flask import Flask, render_template, g, request, url_for
7
+ from sqlalchemy.exc import SQLAlchemyError
7
8
  from sqlalchemy.orm import subqueryload
8
9
 
9
10
  import rcdb
@@ -60,8 +61,13 @@ app = Flask(__name__, template_folder=template_folder)
60
61
 
61
62
  app.config.from_object(__name__)
62
63
 
63
- @app.before_request
64
- def before_request():
64
+ def _resolve_active_database():
65
+ """Pick the connection string for this request and expose selector state.
66
+
67
+ Sets ``g.active_db_name`` / ``g.available_databases`` (used by the navbar DB
68
+ selector) and returns the connection string to connect to. Does not touch
69
+ the database -- only reads config and the ``rcdb_database`` cookie.
70
+ """
65
71
  available_dbs = app.config.get("AVAILABLE_DATABASES", {})
66
72
 
67
73
  if available_dbs:
@@ -90,24 +96,93 @@ def before_request():
90
96
  # No default set, use first available
91
97
  active_name = next(iter(available_dbs))
92
98
 
93
- connection_string = available_dbs[active_name]
94
99
  g.active_db_name = active_name
95
100
  g.available_databases = available_dbs
96
- else:
97
- # Original single-database behavior
98
- connection_string = app.config["SQL_CONNECTION_STRING"]
99
- g.active_db_name = None
100
- g.available_databases = {}
101
-
102
- g.tdb = rcdb.ConfigurationProvider()
103
- g.tdb.connect(connection_string)
101
+ return available_dbs[active_name]
102
+
103
+ # Original single-database behavior
104
+ g.active_db_name = None
105
+ g.available_databases = {}
106
+ return app.config["SQL_CONNECTION_STRING"]
107
+
108
+
109
+ @app.before_request
110
+ def before_request():
111
+ # Never gate static assets on the database -- the graceful DB-error page
112
+ # below still needs its CSS/JS to load when the DB is unreachable.
113
+ if request.endpoint == "static":
114
+ return None
115
+
104
116
  app.jinja_env.globals['datetime_now'] = datetime.now
105
117
 
118
+ # Resolve the selector state first so the navbar (and the DB-error page)
119
+ # can render even if the connection below fails.
120
+ connection_string = _resolve_active_database()
121
+
122
+ # Connect fresh, per request. A DB access/connection failure (access
123
+ # denied, too many connections, timeout, host down, ...) must degrade
124
+ # gracefully instead of surfacing a raw 500 -- and must not poison any
125
+ # cookie or cached engine, so a later reload recovers once the DB is back.
126
+ try:
127
+ g.tdb = rcdb.ConfigurationProvider()
128
+ g.tdb.connect(connection_string)
129
+ except SQLAlchemyError:
130
+ return _render_db_error(connection_string)
131
+
132
+ return None
133
+
134
+
135
+ def _render_db_error(connection_string):
136
+ """Log the full failure detail and render the sanitized DB-error page.
137
+
138
+ The *log* gets everything an admin needs (which database, the underlying
139
+ pymysql/SQLAlchemy error code + message, and the stack trace). The *page*
140
+ gets none of it -- no connection string, host, user, or exception text --
141
+ only a red "DB connection failure" strip with the top menu (and the DB
142
+ selector) intact so the visitor can switch to a working database.
143
+ """
144
+ logger.exception(
145
+ "DB connection/access failure for database '%s' (%s); serving graceful "
146
+ "error page instead of a 500.",
147
+ getattr(g, "active_db_name", None) or "<default>",
148
+ _connection_hint(connection_string),
149
+ )
150
+
151
+ # Drop the half-built provider so its engine/pool is released now rather
152
+ # than lingering as a broken cached connection -- the teardown below only
153
+ # disposes fully-connected providers.
154
+ broken = getattr(g, "tdb", None)
155
+ if broken is not None:
156
+ try:
157
+ broken.close()
158
+ except Exception:
159
+ logger.debug("Ignoring error while disposing failed DB engine.",
160
+ exc_info=True)
161
+ g.tdb = None
162
+
163
+ # 503 Service Unavailable: the failure is transient-friendly (e.g. "too
164
+ # many connections" recovers), so signal a retryable condition rather than
165
+ # a 200 or a permanent error. A plain reload after recovery renders fine.
166
+ return render_template("db_error.html"), 503
167
+
168
+
169
+ @app.errorhandler(SQLAlchemyError)
170
+ def handle_db_error(error):
171
+ """Catch DB errors raised while a view runs (not just at connect time).
172
+
173
+ ``before_request`` already handles the common case (``connect()`` exercises
174
+ the connection via the schema-version check), but a lazily-issued query in
175
+ a view can still raise. Route those through the same graceful page instead
176
+ of a 500. ``g.active_db_name`` / ``g.available_databases`` were set by
177
+ ``before_request`` before the failure, so the selector still renders.
178
+ """
179
+ return _render_db_error(getattr(g.get("tdb", None), "connection_string", ""))
180
+
106
181
 
107
182
  @app.teardown_request
108
183
  def teardown_request(exception):
109
- tdb = getattr(g, 'db', None)
110
- if tdb:
184
+ tdb = getattr(g, 'tdb', None)
185
+ if tdb is not None:
111
186
  tdb.close()
112
187
 
113
188
 
@@ -157,8 +157,11 @@ def elog(run_number):
157
157
  from urllib.request import urlopen
158
158
  from urllib.error import HTTPError
159
159
  try:
160
- elog_json = urlopen('https://logbooks.jlab.org/api/elog/entries?book=hdrun&title=Run_{}&limit=1'
161
- .format(run_number)).read()
160
+ # Context-manage the response so the underlying socket is closed
161
+ # deterministically instead of leaking until GC.
162
+ with urlopen('https://logbooks.jlab.org/api/elog/entries?book=hdrun&title=Run_{}&limit=1'
163
+ .format(run_number)) as response:
164
+ elog_json = response.read()
162
165
  except HTTPError as e:
163
166
  return jsonify(stat=str(e.code))
164
167
 
@@ -1,3 +1,5 @@
1
+ import sys
2
+
1
3
  from flask import Blueprint, request, render_template, flash, g, redirect, url_for
2
4
  from sqlalchemy import desc
3
5
 
@@ -22,16 +24,21 @@ def index():
22
24
  # Handle search request
23
25
  search_query = request.args.get('q', '')
24
26
  run_range = request.args.get('rr', '')
25
- req_conditions_values = req_conditions_str.split(',')
27
+ # Drop blanks so an empty field or trailing comma doesn't become a bogus condition name
28
+ req_conditions_values = [name.strip() for name in req_conditions_str.split(',') if name.strip()]
26
29
 
27
30
  if run_from_str or run_to_str:
28
31
  run_range = run_from_str + "-" + run_to_str
29
32
 
30
33
  run_from, run_to = _parse_run_range(run_range)
31
34
 
35
+ # An empty field means "no bound" - map it to the full run range the API expects
36
+ run_min = run_from if run_from is not None else 0
37
+ run_max = run_to if run_to is not None else sys.maxsize
38
+
32
39
  try:
33
- table = g.tdb.select_values(val_names=req_conditions_values, search_str=search_query, run_min=run_from,
34
- run_max=run_to, sort_desc=True)
40
+ table = g.tdb.select_values(val_names=req_conditions_values, search_str=search_query, run_min=run_min,
41
+ run_max=run_max, sort_desc=True)
35
42
  except Exception as err:
36
43
  flash("Error in performing request: {}".format(err), 'danger')
37
44
  return redirect(url_for('select_values.index'))
@@ -0,0 +1,20 @@
1
+ {% extends "layouts/base.html" %}
2
+
3
+ {% block title %}Database unavailable{% endblock %}
4
+
5
+ {#
6
+ Graceful page shown when the selected database cannot be reached or used
7
+ (access denied, too many connections, timeout, host down, ...). The top
8
+ menu -- including the database selector -- is inherited from base.html so
9
+ the visitor can switch to a working database and reload. No connection
10
+ string, host, user, or exception text is exposed here; the full detail is
11
+ written to the application log instead.
12
+ #}
13
+
14
+ {% block body %}
15
+ <div class="alert alert-danger"
16
+ role="alert"
17
+ style="text-align: center; font-weight: bold; margin-top: 20px;">
18
+ DB connection failure
19
+ </div>
20
+ {% endblock %}
@@ -10,7 +10,7 @@
10
10
  <div class="container">
11
11
 
12
12
  <div class="blog-header"><h3 class="blog-title">Custom table</h3></div>
13
- <div class="hidden-table">
13
+ <div class="hidden-table hidden">
14
14
  <form id="condition-form">
15
15
  <table class="table condition-table">
16
16
  <tr>
@@ -33,7 +33,12 @@
33
33
  </div>
34
34
  <div class="row">
35
35
  <div class="col-md-4">
36
- <table class="table">
36
+ {% if table %}
37
+ <button id="export-csv-btn" type="button" class="btn btn-default btn-sm" style="margin-bottom: 10px;">
38
+ <span class="glyphicon glyphicon-download-alt" aria-hidden="true"></span> Export to CSV
39
+ </button>
40
+ {% endif %}
41
+ <table id="results-table" class="table">
37
42
  <thead>
38
43
  <tr>
39
44
  <th>Run</th>
@@ -113,7 +118,50 @@
113
118
  });
114
119
  });
115
120
 
116
-
121
+ // Wrap a CSV field in quotes when it contains a comma, quote or newline
122
+ function csvEscape(value) {
123
+ value = (value === null || value === undefined) ? '' : String(value);
124
+ if (/[",\n\r]/.test(value)) {
125
+ value = '"' + value.replace(/"/g, '""') + '"';
126
+ }
127
+ return value;
128
+ }
129
+
130
+ function tableToCsv(table) {
131
+ var lines = [];
132
+ var rows = table.querySelectorAll('tr');
133
+ for (var i = 0; i < rows.length; i++) {
134
+ var cells = rows[i].querySelectorAll('th, td');
135
+ var fields = [];
136
+ for (var j = 0; j < cells.length; j++) {
137
+ fields.push(csvEscape(cells[j].textContent.trim()));
138
+ }
139
+ lines.push(fields.join(','));
140
+ }
141
+ return lines.join('\r\n');
142
+ }
143
+
144
+ function exportCsv() {
145
+ var table = document.getElementById('results-table');
146
+ if (!table) return;
147
+
148
+ var csv = tableToCsv(table);
149
+ var blob = new Blob([csv], {type: 'text/csv;charset=utf-8;'});
150
+ var url = URL.createObjectURL(blob);
151
+
152
+ var link = document.createElement('a');
153
+ link.href = url;
154
+ link.download = 'rcdb_select_values.csv';
155
+ document.body.appendChild(link);
156
+ link.click();
157
+ document.body.removeChild(link);
158
+ URL.revokeObjectURL(url);
159
+ }
160
+
161
+ var exportBtn = document.getElementById('export-csv-btn');
162
+ if (exportBtn) {
163
+ exportBtn.addEventListener('click', exportCsv);
164
+ }
117
165
 
118
166
  </script>
119
167
  {{ search_box.run_search_box_scripts(condition_types, run_periods) }}
@@ -1,9 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rcdb
3
- Version: 2.3.1
3
+ Version: 2.4.0
4
4
  Summary: RCDB - Run Conditions DataBase
5
5
  Author-email: Dmitry Romanov <romanov@jlab.org>
6
6
  License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/JeffersonLab/rcdb
8
+ Project-URL: Documentation, https://jeffersonlab.github.io/rcdb
9
+ Project-URL: Repository, https://github.com/JeffersonLab/rcdb.git
7
10
  Classifier: Development Status :: 5 - Production/Stable
8
11
  Classifier: Programming Language :: Python :: 3
9
12
  Classifier: Programming Language :: Python :: 3.9
@@ -155,6 +155,7 @@ rcdb/web/static/js/webintellisense-textbox.js
155
155
  rcdb/web/static/js/webintellisense.js
156
156
  rcdb/web/templates/404.html
157
157
  rcdb/web/templates/custom_column_run_table.html
158
+ rcdb/web/templates/db_error.html
158
159
  rcdb/web/templates/default_run_table.html
159
160
  rcdb/web/templates/index.html
160
161
  rcdb/web/templates/layout.html
@@ -217,4 +218,5 @@ tests/test_save_files.py
217
218
  tests/test_select_runs.py
218
219
  tests/test_select_values.py
219
220
  tests/test_sql_schema_version.py
220
- tests/test_value_comparison.py
221
+ tests/test_value_comparison.py
222
+ tests/test_web_db_failure.py
@@ -0,0 +1,119 @@
1
+ """Web DB-failure handling.
2
+
3
+ A database selected in the navbar (stored in the ``rcdb_database`` cookie) may
4
+ be unreachable or access-denied server-side (e.g. the MariaDB ``rcdb`` user has
5
+ no GRANT on ``xem2``/``pionct`` -> pymysql ``(1044, "Access denied ...")``).
6
+
7
+ These tests pin the required behaviour:
8
+
9
+ * such a failure renders a graceful page (top menu + DB selector + a red
10
+ "DB connection failure" strip), never a raw 500;
11
+ * no sensitive detail (connection string, host, user, error code/text) leaks
12
+ into the page, while the full exception is written to the application log;
13
+ * the failure is not sticky -- reloading after the DB recovers, or switching to
14
+ a working DB, renders normally with no poisoned cookie or cached engine.
15
+ """
16
+
17
+ import unittest
18
+
19
+ from sqlalchemy.exc import OperationalError
20
+
21
+ import rcdb
22
+ import rcdb.provider
23
+ import rcdb.web
24
+
25
+
26
+ class TestWebDbFailure(unittest.TestCase):
27
+ def setUp(self):
28
+ self.app = rcdb.web.app
29
+ self.app.testing = True
30
+
31
+ # Save config we mutate so other tests see the app untouched.
32
+ self._saved = {k: self.app.config.get(k) for k in
33
+ ("AVAILABLE_DATABASES", "DEFAULT_DATABASE")}
34
+ self.app.config["AVAILABLE_DATABASES"] = {
35
+ "good": "sqlite:///good",
36
+ "bad": "sqlite:///bad",
37
+ }
38
+ self.app.config["DEFAULT_DATABASE"] = "sqlite:///good"
39
+
40
+ self.client = self.app.test_client()
41
+
42
+ # Replace connect(): raise an access-denied-style OperationalError for the
43
+ # "bad" DB (while it is "down"); connect a fresh in-memory schema for the
44
+ # "good" one. ``bad_down`` flips to False to simulate recovery.
45
+ self.bad_down = True
46
+ self._orig_connect = rcdb.ConfigurationProvider.connect
47
+ test = self
48
+
49
+ def fake_connect(provider, connection_string="", check_version=True):
50
+ if "bad" in connection_string and test.bad_down:
51
+ raise OperationalError(
52
+ "SELECT 1", {},
53
+ Exception("(1044, \"Access denied for user 'rcdb'@'%' "
54
+ "to database 'bad'\")"))
55
+ test._orig_connect(provider, "sqlite://", check_version=False)
56
+ rcdb.provider.destroy_all_create_schema(provider)
57
+
58
+ rcdb.ConfigurationProvider.connect = fake_connect
59
+
60
+ def tearDown(self):
61
+ rcdb.ConfigurationProvider.connect = self._orig_connect
62
+ for k, v in self._saved.items():
63
+ if v is None:
64
+ self.app.config.pop(k, None)
65
+ else:
66
+ self.app.config[k] = v
67
+
68
+ def _get(self, path="/", db=None):
69
+ # Fresh client per call so the cookie jar carries only what we set here
70
+ # (the Werkzeug test client ignores a raw Cookie header, hence set_cookie).
71
+ client = self.app.test_client()
72
+ if db:
73
+ client.set_cookie("rcdb_database", db)
74
+ return client.get(path)
75
+
76
+ def test_failure_renders_graceful_page_not_500(self):
77
+ with self.assertLogs("rcdb.web", level="ERROR") as cm:
78
+ resp = self._get("/", db="bad")
79
+
80
+ # Graceful, retryable status -- never a raw 500.
81
+ self.assertEqual(resp.status_code, 503)
82
+ self.assertNotEqual(resp.status_code, 500)
83
+
84
+ body = resp.get_data(as_text=True)
85
+ # Red strip + intact top menu incl. the DB selector to switch away.
86
+ self.assertIn("DB connection failure", body)
87
+ self.assertIn('id="dbSelector"', body)
88
+ self.assertIn(">bad<", body)
89
+ self.assertIn(">good<", body)
90
+
91
+ # No sensitive detail leaks into the page ...
92
+ for secret in ("1044", "Access denied", "sqlite:///bad", "rcdb@",
93
+ "OperationalError", "Traceback"):
94
+ self.assertNotIn(secret, body)
95
+
96
+ # ... but the log has the full picture (db name + underlying error).
97
+ logtext = "\n".join(cm.output)
98
+ self.assertIn("bad", logtext)
99
+ self.assertIn("1044", logtext)
100
+
101
+ def test_reload_recovers_after_db_comes_back(self):
102
+ # Same cookie, DB down -> graceful 503.
103
+ self.assertEqual(self._get("/", db="bad").status_code, 503)
104
+
105
+ # DB recovers; a plain reload (identical cookie) must render normally.
106
+ self.bad_down = False
107
+ resp = self._get("/", db="bad")
108
+ self.assertEqual(resp.status_code, 200)
109
+ self.assertNotIn("DB connection failure", resp.get_data(as_text=True))
110
+
111
+ def test_switching_to_working_db_recovers(self):
112
+ self.assertEqual(self._get("/", db="bad").status_code, 503)
113
+ resp = self._get("/", db="good")
114
+ self.assertEqual(resp.status_code, 200)
115
+ self.assertNotIn("DB connection failure", resp.get_data(as_text=True))
116
+
117
+
118
+ if __name__ == "__main__":
119
+ unittest.main()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes