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.
- {rcdb-2.3.1/rcdb.egg-info → rcdb-2.4.0}/PKG-INFO +4 -1
- {rcdb-2.3.1 → rcdb-2.4.0}/pyproject.toml +4 -1
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/app_context.py +12 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/cli/app.py +6 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/cli/db.py +4 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/cli/rp.py +3 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/provider.py +40 -15
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/version.py +1 -1
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/__init__.py +88 -13
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/modules/runs.py +5 -2
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/modules/select_values.py +10 -3
- rcdb-2.4.0/rcdb/web/templates/db_error.html +20 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/select_values/index.html +51 -3
- {rcdb-2.3.1 → rcdb-2.4.0/rcdb.egg-info}/PKG-INFO +4 -1
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb.egg-info/SOURCES.txt +3 -1
- rcdb-2.4.0/tests/test_web_db_failure.py +119 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/MANIFEST.in +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/README.md +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/__init__.py +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/__main__.py +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/alias.py +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/cli/__init__.py +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/cli/__main__.py +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/cli/add.py +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/cli/context.py +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/cli/file.py +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/cli/info.py +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/cli/ls.py +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/cli/repair/__init__.py +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/cli/repair/evio_files.py +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/cli/run.py +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/cli/select.py +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/cli/test_data.py +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/cli/web.py +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/coda_parser.py +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/config_parser.py +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/constants.py +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/errors.py +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/file_archiver.py +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/halld_daq_config_parser.py +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/lexer.py +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/log_format.py +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/model.py +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/stopwatch.py +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/update.py +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/__main__.py +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/modules/__init__.py +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/modules/conditions.py +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/modules/files.py +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/modules/logs.py +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/modules/statistics.py +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/pagination.py +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/run_table.py +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/DataTables-1.10.10/css/dataTables.bootstrap.css +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/DataTables-1.10.10/css/dataTables.bootstrap.min.css +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/DataTables-1.10.10/css/dataTables.foundation.css +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/DataTables-1.10.10/css/dataTables.foundation.min.css +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/DataTables-1.10.10/css/dataTables.jqueryui.css +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/DataTables-1.10.10/css/dataTables.jqueryui.min.css +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/DataTables-1.10.10/css/jquery.dataTables.css +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/DataTables-1.10.10/css/jquery.dataTables.min.css +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/DataTables-1.10.10/css/jquery.dataTables_themeroller.css +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/DataTables-1.10.10/images/sort_asc.png +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/DataTables-1.10.10/images/sort_asc_disabled.png +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/DataTables-1.10.10/images/sort_both.png +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/DataTables-1.10.10/images/sort_desc.png +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/DataTables-1.10.10/images/sort_desc_disabled.png +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/DataTables-1.10.10/js/dataTables.bootstrap.js +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/DataTables-1.10.10/js/dataTables.bootstrap.min.js +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/DataTables-1.10.10/js/dataTables.foundation.js +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/DataTables-1.10.10/js/dataTables.foundation.min.js +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/DataTables-1.10.10/js/dataTables.jqueryui.js +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/DataTables-1.10.10/js/dataTables.jqueryui.min.js +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/DataTables-1.10.10/js/jquery.dataTables.js +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/DataTables-1.10.10/js/jquery.dataTables.min.js +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/Responsive-2.0.0/css/responsive.bootstrap.css +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/Responsive-2.0.0/css/responsive.bootstrap.min.css +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/Responsive-2.0.0/css/responsive.dataTables.css +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/Responsive-2.0.0/css/responsive.dataTables.min.css +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/Responsive-2.0.0/css/responsive.foundation.css +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/Responsive-2.0.0/css/responsive.foundation.min.css +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/Responsive-2.0.0/css/responsive.jqueryui.css +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/Responsive-2.0.0/css/responsive.jqueryui.min.css +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/Responsive-2.0.0/js/dataTables.responsive.js +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/Responsive-2.0.0/js/dataTables.responsive.min.js +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/Responsive-2.0.0/js/responsive.bootstrap.js +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/Responsive-2.0.0/js/responsive.bootstrap.min.js +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/Responsive-2.0.0/js/responsive.foundation.js +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/Responsive-2.0.0/js/responsive.foundation.min.js +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/Responsive-2.0.0/js/responsive.jqueryui.js +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/Responsive-2.0.0/js/responsive.jqueryui.min.js +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/RowReorder-1.1.0/css/rowReorder.bootstrap.css +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/RowReorder-1.1.0/css/rowReorder.bootstrap.min.css +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/RowReorder-1.1.0/css/rowReorder.dataTables.css +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/RowReorder-1.1.0/css/rowReorder.dataTables.min.css +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/RowReorder-1.1.0/css/rowReorder.foundation.css +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/RowReorder-1.1.0/css/rowReorder.foundation.min.css +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/RowReorder-1.1.0/css/rowReorder.jqueryui.css +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/RowReorder-1.1.0/css/rowReorder.jqueryui.min.css +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/RowReorder-1.1.0/js/dataTables.rowReorder.js +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/RowReorder-1.1.0/js/dataTables.rowReorder.min.js +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/datatables.css +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/datatables.js +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/datatables.min.css +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/DataTables/datatables.min.js +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/css/bootstrap-theme.css +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/css/bootstrap-theme.css.map +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/css/bootstrap-theme.min.css +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/css/bootstrap-theme.min.css.map +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/css/bootstrap.css +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/css/bootstrap.css.map +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/css/bootstrap.min.css +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/css/bootstrap.min.css.map +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/css/buttons.dataTables.min.css +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/css/dataTables.tableTools.css +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/css/jquery-ui.min.css +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/css/jquery-ui.structure.min.css +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/css/jquery-ui.theme.min.css +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/css/jquery.dataTables.min.css +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/css/jquery.dataTables.yadcf.css +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/css/jquery.jsonview.css +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/css/main.css +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/css/select.dataTables.min.css +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/css/webintellisense.css +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/favicon.png +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/fonts/glyphicons-halflings-regular.eot +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/fonts/glyphicons-halflings-regular.svg +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/fonts/glyphicons-halflings-regular.ttf +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/fonts/glyphicons-halflings-regular.woff +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/fonts/glyphicons-halflings-regular.woff2 +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/humans.txt +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/img/glyphicons-halflings-white.png +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/img/glyphicons-halflings.png +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/img/logo.png +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/js/bootstrap.js +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/js/bootstrap.min.js +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/js/clipboard.min.js +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/js/copy_clipboard.js +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/js/dataTables.buttons.min.js +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/js/dataTables.select.min.js +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/js/jquery-2.1.0.min.js +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/js/jquery-2.1.4.min.js +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/js/jquery-ui.min.js +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/js/jquery.dataTables.min.js +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/js/jquery.dataTables.yadcf.js +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/js/jquery.flot.min.js +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/js/jquery.jsonview.js +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/js/npm.js +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/js/webintellisense-ace.js +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/js/webintellisense-codemirror.js +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/js/webintellisense-textbox.js +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/js/webintellisense.js +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/robots.txt +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/static/style.css +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/404.html +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/admin/index.html +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/admin/layout.html +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/admin/user.html +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/admin/users.html +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/conditions/index.html +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/custom_column_run_table.html +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/default_run_table.html +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/errors/forbidden_page.html +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/errors/layout.html +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/errors/page_not_found.html +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/errors/server_error.html +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/files/index.html +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/files/info.html +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/files/not_found.html +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/frontend/create_profile.html +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/frontend/footers/help.html +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/frontend/login.html +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/frontend/login_openid.html +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/frontend/reauth.html +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/frontend/reset_password.html +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/frontend/search.html +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/frontend/signup.html +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/index.html +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/layout.html +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/layouts/base.html +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/layouts/base2.html +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/login.html +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/logs/index.html +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/macros/_ask_user_to_update_ie.html +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/macros/_form.html +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/macros/_google_analytics.html +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/macros/_misc.html +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/macros/_reset_password.html +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/render_pagination.html +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/run_search_box.html +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/runs/conditions.html +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/runs/example.html +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/runs/index.html +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/runs/info.html +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/runs/not_found.html +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/settings/layout.html +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/settings/password.html +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/settings/profile.html +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/show_entries.html +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/statistics/index.html +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/user/index.html +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb/web/templates/user/profile.html +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb.egg-info/dependency_links.txt +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb.egg-info/entry_points.txt +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb.egg-info/requires.txt +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/rcdb.egg-info/top_level.txt +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/setup.cfg +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/tests/test_cli_add_condition.py +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/tests/test_cli_add_file.py +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/tests/test_cli_add_type.py +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/tests/test_cli_file.py +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/tests/test_cli_run.py +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/tests/test_coda_parser.py +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/tests/test_conditions.py +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/tests/test_config_parser.py +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/tests/test_get_run_periods.py +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/tests/test_get_runs.py +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/tests/test_halld_run_config_parser.py +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/tests/test_save_files.py +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/tests/test_select_runs.py +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/tests/test_select_values.py +0 -0
- {rcdb-2.3.1 → rcdb-2.4.0}/tests/test_sql_schema_version.py +0 -0
- {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
|
+
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
message
|
|
146
|
-
|
|
147
|
-
|
|
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
|
|
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
|
|
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
|
|
@@ -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
|
-
|
|
64
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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, '
|
|
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
|
-
|
|
161
|
-
|
|
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
|
-
|
|
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=
|
|
34
|
-
run_max=
|
|
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
|
-
|
|
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
|
+
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
|