From 53b367621001e5bb77073529ffe7f45e88fbeacf Mon Sep 17 00:00:00 2001 From: Dmitry Romanov Date: Tue, 13 Jun 2023 17:05:08 -0400 Subject: [PATCH 01/34] New documentation initial draft --- doc/.gitignore | 4 + doc/.nojekyll | 0 doc/Database-Installation.md | 57 + doc/Deployment.md | 106 ++ doc/Development.md | 33 + doc/README.md | 79 + doc/Select-runs-and-get-values.md | 175 +++ doc/Web-quick-query.md | 10 + doc/_sidebar.md | 30 + doc/api/Cpp.md | 153 ++ doc/api/Java.md | 65 + doc/daq/DaqConfigParser.md | 18 + doc/daq/DaqOverview.md | 7 + doc/daq/daq_concepts.md | 0 doc/design/Adding-condition-values.md | 297 ++++ doc/design/Connection.md | 90 ++ doc/design/Creating-condition-types.md | 39 + doc/design/DB-and-API-structure.md | 116 ++ doc/design/Logging.md | 37 + doc/design/Performance.md | 81 + doc/design/SQLAlchemy.md | 338 ++++ doc/images/schema.png | Bin 0 -> 108718 bytes doc/images/schema.svg | 2010 ++++++++++++++++++++++++ doc/images/web_quick_query_top.png | Bin 0 -> 4230 bytes doc/index.html | 129 ++ doc/tutorials/Installation.md | 104 ++ doc/tutorials/Python-basics.md | 110 ++ doc/tutorials/Query-syntax.md | 95 ++ doc/tutorials/Select-values.md | 144 ++ doc/tutorials/rcnd.md | 124 ++ 30 files changed, 4451 insertions(+) create mode 100644 doc/.gitignore create mode 100644 doc/.nojekyll create mode 100644 doc/Database-Installation.md create mode 100644 doc/Deployment.md create mode 100644 doc/Development.md create mode 100644 doc/README.md create mode 100644 doc/Select-runs-and-get-values.md create mode 100644 doc/Web-quick-query.md create mode 100644 doc/_sidebar.md create mode 100644 doc/api/Cpp.md create mode 100644 doc/api/Java.md create mode 100644 doc/daq/DaqConfigParser.md create mode 100644 doc/daq/DaqOverview.md create mode 100644 doc/daq/daq_concepts.md create mode 100644 doc/design/Adding-condition-values.md create mode 100644 doc/design/Connection.md create mode 100644 doc/design/Creating-condition-types.md create mode 100644 doc/design/DB-and-API-structure.md create mode 100644 doc/design/Logging.md create mode 100644 doc/design/Performance.md create mode 100644 doc/design/SQLAlchemy.md create mode 100644 doc/images/schema.png create mode 100644 doc/images/schema.svg create mode 100644 doc/images/web_quick_query_top.png create mode 100644 doc/index.html create mode 100644 doc/tutorials/Installation.md create mode 100644 doc/tutorials/Python-basics.md create mode 100644 doc/tutorials/Query-syntax.md create mode 100644 doc/tutorials/Select-values.md create mode 100644 doc/tutorials/rcnd.md diff --git a/doc/.gitignore b/doc/.gitignore new file mode 100644 index 00000000..7d847284 --- /dev/null +++ b/doc/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +.cache/ +public +.idea/ diff --git a/doc/.nojekyll b/doc/.nojekyll new file mode 100644 index 00000000..e69de29b diff --git a/doc/Database-Installation.md b/doc/Database-Installation.md new file mode 100644 index 00000000..5ab12c9a --- /dev/null +++ b/doc/Database-Installation.md @@ -0,0 +1,57 @@ +## Prerequisites +> Evironment variables have to be set. + TL;DR; run `environment.*` script located in the RCDB root dir([more details](Installation)). + `RCDB_CONNECTION` must also be set (see below). + +Lets assume that one wants to install RCDB to the following database: + +* database location: `localhost` +* database name: `rcdb` +* database user name: `rcdb` +* database user password: `password` + +then the connection string is + +``` +mysql://rcdb:password@localhost/rcdb +``` + +and ```RCDB_CONNECTION``` must be: + +```bash +export RCDB_CONNECTION='mysql://rcdb:password@localhost/rcdb' +``` + +## Create MySQL database +(RCDB is tested for MySQL and MariaDB. Also RCDB should work with other MySQL forks while it hasn't been tested) + +1. Create DB(aka schema) and a user with privileges: + + ```bash + mysql -u root -p + ``` + + > note, that root privileges are required to run ```mysql - u root ...``` for MariaDB by default. Run something like `sudo mysql -u root -p` for MariaDB + + ```sql + CREATE SCHEMA rcdb; + CREATE USER 'rcdb'@'localhost' IDENTIFIED BY 'password'; + GRANT ALL PRIVILEGES ON rcdb.* TO 'rcdb'@'localhost'; + ``` + +## Create or update DB structure + +Both creating schema for the first time for a fresh database or updating the existing schema is done with +[Alembic](https://pypi.python.org/pypi/alembic) + +Just run: + +```bash +cd $RCDB_HOME +./alembic_rcdb upgrade head +``` + +> Since there where problems with installing alembic on some machines in counting house RCDB ships a copy of it within itself; `alembic_rcdb` command runs this embedded alembic version. + + + diff --git a/doc/Deployment.md b/doc/Deployment.md new file mode 100644 index 00000000..8c5d79a6 --- /dev/null +++ b/doc/Deployment.md @@ -0,0 +1,106 @@ +Deploying RCDB MySQL database on local machine. + +### 0. Clone RCDB repo + +Here is a very brief instruction for it (for bash shell): + +```bash +git clone https://github.com/JeffersonLab/rcdb.git +cd rcdb +source environment.bash + +# now to check that everything works, test connect to HallD RCDB +export RCDB_CONNECTION=mysql://rcdb@hallddb.jlab.org/rcdb +rcnd +``` + +rcnd should answer something like: + +``` +Runs total: 10462 +Last run : 42387 +Condition types total: 53 +Conditions: + +``` + +More information is in [Installation chapter](https://github.com/JeffersonLab/rcdb/wiki/Installation) + + +### 1. Create MySQL database + +```bash +mysql -u root -p +``` + +```mysql +CREATE SCHEMA rcdb; +CREATE USER 'rcdb'@'localhost'; +GRANT ALL PRIVILEGES ON rcdb.* TO 'rcdb'@'localhost'; +``` + +### 2. Create DB structure +And fill in a minimal set of common conditions. + +```bash +python $RCDB_HOME/python/create_empty_db.py -c mysql://rcdb@localhost/rcdb --i-am-sure --add-def-con +``` + +The right answer should be: +``` +creating database: +database created +default conditions filled +``` + +### 3. Testing the installation + +Now check rcdn + +``` +export RCDB_CONNECTION=mysql://rcdb@localhost/rcdb +rcnd +``` + +The output should be like: + +``` +Runs total: 0 +Condition types total: 13 +Conditions: + +``` + +Lets create a first run (run 1) and fill event_count=12345 for run 1 + +```bash +rcnd --new-run 1 +rcnd --write 12345 --replace 1 event_count + +#lets check conditions for run 1 +rcnd 1 +``` + +Output should be like: + +``` +event_count = 12345 +``` +More about rcnd command and command line tools one could find in +[CLI basics chapter](https://github.com/JeffersonLab/rcdb/wiki/rcnd). + +### 4. Test run the web site + +```bash +python $RCDB_HOME/start_www.py +``` + +The output should be like: + +``` +* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) +* Restarting with reloader +``` + +Now follow to http://127.0.0.1:5000/ in the browser to see the site. + diff --git a/doc/Development.md b/doc/Development.md new file mode 100644 index 00000000..28206086 --- /dev/null +++ b/doc/Development.md @@ -0,0 +1,33 @@ +# 1. Establishing integration tests + +1. Create MySQL database for the test purposes + + ```bash + mysql -u root -p + ``` + + ```mysql + CREATE SCHEMA test_rcdb; + CREATE USER 'test_rcdb'@'localhost' IDENTIFIED BY 'test_rcdb'; + GRANT ALL PRIVILEGES ON test_rcdb.* TO 'test_rcdb'@'localhost'; + ``` + +2. Set RCDB_MYSQL_TEST_CONNECTION environment variable + + ```bash + export RCDB_MYSQL_TEST_CONNECTION="mysql://test_rcdb@localhost/test_rcdb" + + ``` + +3. run create_test_database.py + + ```bash + python $RCDB_HOME/python/tests/create_test_database.py $RCDB_MYSQL_TEST_CONNECTION + ``` + + +4. Run ```test_all_rcdb``` + + + + diff --git a/doc/README.md b/doc/README.md new file mode 100644 index 00000000..2d79b51b --- /dev/null +++ b/doc/README.md @@ -0,0 +1,79 @@ +# RCDB + +Run Configuration/Conditions Database (RCDB) stores run related information and conditions. It uses MySQL or SQLite databases to store information about runs and provides interfaces to search runs, manage data, automatic CODA integration, etc. + +One can consider two main aspects of what conceptually RCDB is designed for: + +1. Add data to database +2. Provide convenient way to read, introspect and analyze DB stored data providing interfaces: + - Web site + - Command line interface (CLI) + - Python API + - C++ API + - Possibly JAVA API + +## Concepts + +[daq concepts](daq/daq_concepts.md ':include') + +Software wise: + + - Can work with multiple databases (MySQL, SQLite, possibly others) + - Data queries + - Administration tools + - DAQ module tools + - Introspection tools + + +## Demo: + +One can visit HallD RCDB Web site as demo: +https://halldweb.jlab.org/rcdb/ + +Daily updated SQLite database is available here: +https://halldweb.jlab.org/dist/rcdb.sqlite + + +## Conditions + +Run conditions is the way to store information related to a run (which is identified by run_number everywhere). +From a simplistic point of view, run conditions are presented in RCDB as **name**-**value** pairs attached to a run number. For example, **event_count** = **1663** for run **100**. + +One of the major use cases of RCDB is searching for runs matching required conditions. It is done using simple, python-if-like queries. The result of ```event_count > 100000``` is all runs, that, obviously, have **event_count** more than **100000** + +Lets see how API-s would look for the examples above. + +Python: +```python +import rcdb + +# Open SQLite database connection +db = rcdb.RCDBProvider("sqlite:///path.to.file.db") + +# Read value for run 100 +event_count = db.get_condition(100, "event_count").value + +# Select runs +result = db.select_runs("event_count > 100000") +for run in result: + print run.number # Or do whatever you want with runs +``` + +CLI: + +```bash +export RCDB_CONNECTION=mysql://rcdb@localhost/rcdb +rcnd --help # Gives you self descriptive help +rcnd 1000 event_count # See exact value of 'event_count' for run 1000 +rcnd --write 1663 100 event_count # Write condition value to run 100 +rcnd --search "event_count > 500" # Select runs + +``` + + +What RCDB conditions are not designed for? - They are not designed for large data sets that change rarely (value is the same for many runs). +That is because each condition value is independently saved (and attached) for each run. + +In the case of bulk data, it is better to save it using other RCDB options. RCDB provides the files saving mechanism as example. + +[//]: # () diff --git a/doc/Select-runs-and-get-values.md b/doc/Select-runs-and-get-values.md new file mode 100644 index 00000000..326b1ce5 --- /dev/null +++ b/doc/Select-runs-and-get-values.md @@ -0,0 +1,175 @@ +**Contents:** +- [Selecting runs and getting values](#selecting-runs-and-getting-values) + + [Get values](#get-values) + + [Run range](#run-range) + + [No filtration](#no-filtration) + + [Sort order](#sort-order) +- [Iterating over runs and getting conditions](#iterating-over-runs-and-getting-conditions) + + [Getting runs](#getting-runs) + + [Get any condition of the run](#get-any-condition-of-the-run) +- [Performance](#performance) + +------- + +To experiment with the examples on this page, one can download daily recreated SQLite database: +https://halldweb.jlab.org/dist/rcdb.sqlite + +Using connection string: +``` +sqlite:////rcdb.sqlite +``` + +Or connect to readonly mysql: +``` +mysql://rcdb@hallddb.jlab.org/rcdb +``` + +
+
+ +### Selecting runs and getting values + +
+ +#### Get values + +Suppose, one wants to get all event_count-s and beam_current-s for production runs: + +```python +import rcdb + +# Connect to database +db = rcdb.RCDBProvider("mysql://rcdb@hallddb.jlab.org/rcdb") + +# Select runs and get values +table = db.select_runs("@is_production")\ + .get_values(['event_count', 'beam_current'], insert_run_number=True) +print(table) +``` + +As the result one gets something like: +``` +[ +[1023, 3984793, 0.145] +[1024, 4569873, 0.230] +... +] +``` +The first column is a run number (we set ```insert_run_number=True``` above). The other two columns are 'event_count' and 'beam_current' as we gave it above. + +If run number is not needed ```insert_run_number``` may be skipped: +```python +table = db.select_runs("@is_production") + .get_values(['event_count', 'beam_current']) +``` + +A nice way to iterate the values: + +```python +for row in table: + event_count, beam_current = tuple(row) + print(event_count, beam_current) +``` + +
+ +#### Run range + +If one wants to apply a run range, say for a particular run period: +```python +table = db.select_runs("@is_production", 10000, 20000)\ + .get_values(['event_count', 'beam_current'], True) +``` + +
+ +#### No filtration +To get values for all runs without filtration a search pattern may be skipped: +```python +table = db.select_runs(run_min=10000, run_max=20000)\ + .get_values(['event_count', 'beam_current'], insert_run_number=True) +``` + +(note that parameter names are used here, so the python could figure function parameters out) + + +
+ +#### Sort order +The table is always sorted by run number. It is just a 'feature' of getting runs DB query (that is under the hood). However, the order in with run numbers are sorted could be changed: +```python +table = db.select_runs(run_min=10000, run_max=20000, sort_desc=True)\ + .get_values(['event_count', 'beam_current'], insert_run_number=True) +``` + +```sort_desc=True``` - makes rows to be sorted by descending run_number + + +
+
+ +### Iterating over runs and getting conditions + +
+ +#### Getting runs +```select_runs``` function returns ```RunSelectionResult``` object that contains all selected runs and some other information about how the runs where selected. The RunSelectionResult implements ```list``` interface returning ```Run`-s. Thus one can do: + +```python +import rcdb +db = rcdb.RCDBProvider("mysql://rcdb@hallddb.jlab.org/rcdb") +result = db.select_runs("@is_production") +for run in result: + print run.number +``` + +As one could guess the selected run numbers are printed as the result. + +
+ +#### Get any condition of the run +```Run``` has the next useful functions: + +```python +def get_conditions_by_name(self): + # Get all conditions and returns dictionary of condition.name -> condition +def get_condition(self, condition_name): + # Gets Condition object by name if such name condition exist or None +def get_condition_value(self, condition_name): + # Gets the condition value if such condition exist or None +``` + +So one can iterate selected runs and get any desired condition: + +```python +import rcdb +db = rcdb.RCDBProvider("mysql://rcdb@hallddb.jlab.org/rcdb") +result = db.select_runs("@is_production") +for run in result: + print run.get_condition_value('event_count') +``` + + + +

+ +## Performance + +In the performance point of view, the fastest way to get values by using +``` +db.select_runs(...).get_values(...) +``` +Because ```get_values``` makes just a single database call to retrieve all values for selected runs. + +In case of iterating: +``` +result = db.select_runs("@is_production") +for run in result: + print run.get_condition_value('event_count') +``` +Database is queried on each get_condition_value + + + + + diff --git a/doc/Web-quick-query.md b/doc/Web-quick-query.md new file mode 100644 index 00000000..f988ab6e --- /dev/null +++ b/doc/Web-quick-query.md @@ -0,0 +1,10 @@ +[[images/web_quick_query_top.png]] + + +For query now is possible to use: + +1. ***Run nubmer*** (like ```4371```), the result is showing info for the run +2. ***Run range [from]-[to]*** (like ```3000-5000```), the result is showing runs in this range +3. ***All other***, the result is the same as just /runs page + +More options is upcoming \ No newline at end of file diff --git a/doc/_sidebar.md b/doc/_sidebar.md new file mode 100644 index 00000000..47a70e12 --- /dev/null +++ b/doc/_sidebar.md @@ -0,0 +1,30 @@ +- [Home](/) + +- Tutorials: + - [Installation](tutorials/Installation) + - [Select values tutorial](tutorials/Select-values) (python) + - [Query syntax](tutorials/Query-syntax) + - [Add data](tutorials/Python-basics)(python) + - [CLI Basics](tutorials/rcnd) + +- Design: + - [Connection](design/Connection) + - [DB and APIs structure](design/DB-and-API-structure) + - [Creating condition types](design/Creating-condition-types) + - [Adding condition values](design/Adding-condition-values) + - [SQLAlchemy](design/SQLAlchemy) + - [Logging](design/Logging) + - [Performance](design/Performance) + +- API + - [C++ API](api/Cpp) + - [Java API](java/Java) + - [Command line(rcnd)](design/rcnd) + +- [Deploy](Deployment) + +- DAQ: + - [Overview](DaqOverview) + - [DB Installation](Database-Installation) + - [Config parser](DaqConfigParser) + diff --git a/doc/api/Cpp.md b/doc/api/Cpp.md new file mode 100644 index 00000000..0e5886ac --- /dev/null +++ b/doc/api/Cpp.md @@ -0,0 +1,153 @@ +- [Installation](Cpp#installation) +- [Getting values](Cpp#getting-values) +- [Examples](Cpp#examples) + +## RCDB C++ API overview + +C++ API is a header only library that allows to read RCDB condition values for the run. The library doesn't provide possibility of run selection queries at this point. Also it requires C++11 to compile. + +C++ API code is located in [$RCDB_HOME/cpp](https://github.com/JeffersonLab/rcdb/tree/master/cpp) directory. + +
+
+ +## Installation + +TL; DR; version: + +**Just include headers and:** + +* define ```RCDB_MYSQL``` for MySQL, ```RCDB_SQLITE``` for SQLite +* Ensure libs and headers are included. + +Compile and run the simplest example for SQLite + +```bash +> gcc $RCDB_HOME/api/examples/simple.api -o simple -I$RCDB_HOME/api/include/ -std=c++11 -lstdc++ -lsqlite3 -DRCDB_SQLITE + +> ./simple sqlite:////path/to/db/rcdb.sqlite 10452 +``` + +with MySQL support: +``` +> gcc $RCDB_HOME/cpp/examples/simple.cpp -o simple -I$RCDB_HOME/cpp/include/ -std=c++11 -lstdc++ -DRCDB_MYSQL `mysql_config --libs --cflags --include` + +> ./simple mysql://rcdb@hallddb.jlab.org/rcdb 10452 +``` + +Combine both to have MySQL and SQLite working together + +--- + +RCDB C++ API is a header only since 0.03. Which means there is no more librcdb and separate step for RCDB. +That also means that MySQL and SQLite libraries should be linked to the application which includes RCDB headers. + +In order for your code to build ensure flags/configuration: + +* There is at lease C++11 support enabled and stdc++ library linked. This means that probably minimum GCC version to be used is 4.8: + + ```-std=c++11 -lstdc++``` + +* For MySQL: + + * Define ```RCDB_MYSQL``` + * Add mysql-connector includes and libs. There is useful ```mysql_config``` script: + + ``` -DRCDB_MYSQL `mysql_config --libs --cflags --include` ``` + +* For SQLite: + + * Define ```RCDB_SQLITE```: + * Link libsqlite3 + + ``` -DRCDB_SQLITE -lsqlite3 ``` + + +** Defining RCDB_MYSQL or RCDB_SQLITE ** + + +​* Code ```#define RCDB_MYSQL``` +* Compiler arguments ```-DRCDB_MYSQL``` +* Scons ```env.Append(CPPDEFINES=['RCDB_MYSQL', 'RCDB_SQLITE'])``` +* CMAKE ```add_definitions(-DRCDB_MYSQL)``` +* SMBS ```AddRcdb()``` in SConscript + + +### Dependencies + +#### Ubuntu + +* MySQL ```libmysqlclient-dev``` or ```libmariadbclient-dev``` +* SQLite ```libsqlite3-dev``` + +```sudo apt-get install libmariadbclient-dev libsqlite3-dev -y``` + +#### CentOS/Fedora + +... please add, somebody ... + +
+
+ +## Getting values + +The example shows how to get values from RCDB: + +```cpp +// Connect +Connection con("mysql://rcdb@hallddb/rcdb"); + +// Get event_count for run 10173 +auto cnd = prov.GetCondition(10173, "event_count"); + +// Check event_count has a value for the run +if(!cnd) { + std::cout<< "event_count condition is not set for the run"<ToInt(); +``` + +Here is the list of condition ToXXX functions and what values they are for: + +```cpp +int ToInt(); /// For int values +bool ToBool(); /// For bool or int in DB +double ToDouble(); /// For Double or int in DB +std::string ToString(); /// For Json, String or Blob +time_point ToTime(); /// For time value +rapidjson::Document ToJsonDocument(); /// For JSon document + +rcdb::ValueTypes GetValueType(); /// Returns the type enum +``` + +## Examples + +Examples are located in [$RCDB_HOME/cpp/examples](https://github.com/JeffersonLab/rcdb/tree/master/cpp/examples) folder. To build them use `with-examples=true` scons flag: + +```bash +cd $RCDB_HOME/api +scons with-examples=true #... +``` + +After examples are built they are located in `$RCDB_HOME/cpp/bin` directory named as `exmpl_<...>` + +
+ +**List of examples:** + +* [simple.cpp](https://github.com/JeffersonLab/rcdb/blob/master/cpp/examples/simple.cpp) - Simple condition readout +* [get_trigger_params.cpp](https://github.com/JeffersonLab/rcdb/blob/master/cpp/examples/get_trigger_params.cpp) - Versatile data readout example. It includes: + * Reading conditions + * Working with JSON serialized objects + * Getting RCDB stored files contents + * Working with config file parser +* [write_conditions.cpp](https://github.com/JeffersonLab/rcdb/blob/master/cpp/examples/write_conditions.cpp) - Writing conditions to RCDB from C++. It includes: + * Using WriteConnection + * Adding condition values of different types + + + + diff --git a/doc/api/Java.md b/doc/api/Java.md new file mode 100644 index 00000000..235280ca --- /dev/null +++ b/doc/api/Java.md @@ -0,0 +1,65 @@ +## RCDB Java API overview + +Java API allows one to read RCDB condition values for the run. It doesn't provide possibility of run selection queries at this point. + + +## Installation +Java API comes as sources (in Kotlin) and as ready to use Java precompiled .jar file + +* Java API is located in [$RCDB_HOME/java](https://github.com/JeffersonLab/rcdb/tree/master/java) directory. +* Precompiled jar is located in [$RCDB_HOME/java/out/artifacts](https://github.com/JeffersonLab/rcdb/tree/master/java/out/artifacts/rcdb_jar/) + +--- + + +## Getting values + +The example shows how to get values from RCDB: + +```java +import org.rcdb.*; +//... + // Connect to the database + // The real HallD database is going to be used for the example + JDBCProvider provider = RCDB.createProvider("mysql://rcdb@hallddb.jlab.org/rcdb"); + provider.connect(); + + // get event count as a long value for run number 31000 + long eventCount = provider.getCondition(31000, "event_count").toLong(); + System.out.println("event_count = " + eventCount); +``` + +Here is the list of condition to[Type] functions and what values they are for: + +```java +Long toLong(); /// For int values +Bool toBool(); /// For bool or int in DB +Double toDouble(); /// For Double or int in DB +String toString(); /// For Json, String or Blob +Date toDate(); /// For time value + +org.rcdb.ValueTypes /// type enum +``` + +## Examples + +Examples are located in [$RCDB_HOME/java/src/javaExamples](https://github.com/JeffersonLab/rcdb/tree/master/java/src/javaExamples) folder. + +
+ +#### List of examples: + +Simple example - shows how to read values from database and lists all condition types from DB + +[$RCDB_HOME/java/src/javaExamples/SimpleExample.java](https://github.com/JeffersonLab/rcdb/blob/master/java/src/javaExamples/SimpleExample.java) + +
+ +#### Kotlin + +There are also an example written in [$RCDB_HOME/java/src/kotlinExamples/main.kt](https://github.com/JeffersonLab/rcdb/blob/master/java/src/kotlinExamples/main.kt) + + + + + diff --git a/doc/daq/DaqConfigParser.md b/doc/daq/DaqConfigParser.md new file mode 100644 index 00000000..523f618e --- /dev/null +++ b/doc/daq/DaqConfigParser.md @@ -0,0 +1,18 @@ +The configuration file looks like + +``` +#! CONFIG FILE:: /home/hdops/CDAQ/daq_dev_v0.31/daq/config/hd_all/TRG_FCAL_BCAL_m8_b1bf1.conf +# (Re)Created:: on Wed Apr 29 12:06:51 EDT 2015 + +========================== + TRIGGER +========================== + +TS_TRIG_TYPE 6 + +# SSP SLOT FIBER_EN SUM_ENABLE +SSP_SLOT 8 0xFF 1 +... +``` + +Here ```TRIGGER``` is a section \ No newline at end of file diff --git a/doc/daq/DaqOverview.md b/doc/daq/DaqOverview.md new file mode 100644 index 00000000..b1d2c934 --- /dev/null +++ b/doc/daq/DaqOverview.md @@ -0,0 +1,7 @@ +## General concepts + +This chapter overviews of how run data is to be added to RCDB. +There are several ideological ideas behind of how RCDB works: + +[daq concepts](daq_concepts.md ':include') + diff --git a/doc/daq/daq_concepts.md b/doc/daq/daq_concepts.md new file mode 100644 index 00000000..e69de29b diff --git a/doc/design/Adding-condition-values.md b/doc/design/Adding-condition-values.md new file mode 100644 index 00000000..38c374f0 --- /dev/null +++ b/doc/design/Adding-condition-values.md @@ -0,0 +1,297 @@ +## Add values + +> A 'condition type' (defining name and type) must be created prior adding values. It is discussed in [previous chapter](Creating condition types) and is included in examples further in this chapters + +There are two functions to add condition values to DB. First one is: + +```python +def add_condition(run, name, value, replace=False) +``` + +There is a common situation when one has a collection of values (E.g. after parsing a file), for this case there is a handy function that allows to add many conditions values at one time. + +```python +def add_conditions(run, name_values, replace=False) +``` + +The ```name_values``` could be a dictionary or list of name-value pairs: + +```python +# dict +name_values = {"name1":value1, "name2":value2, ...} +# list of tuples +name_values = [("name1",value1), ("name2",value2), ...] +# list of lists +name_values = [["name1",value1], ["name2",value2], ...] +``` + +**(!) performance:** ```add_conditions``` tries to use as less transactions as possible to check and commit all values. So for it provides a big performance gain vs calling ```add_condition``` for each value separately + +### Replace values +What if the condition value for this run with this name already exists in the DB? + +In general, to replace value ```replace=True``` parameter should be passed to ```add_condition``` or ```add_conditions```. + +If run has this condition, with the same value and time, exception is not raised and function does nothing. + +Example: + +```python +db.add_condition(1, "event_count", 1000) # First addition to DB +db.add_condition(1, "event_count", 1000) # Ok. Do nothing, such value already exists +db.add_condition(1, "event_count", 2222) # Error. OverrideConditionValueError +db.add_condition(1, "event_count", 2222, replace=True) # Ok. Replacing existing value +print(db.get_condition(1, "event_count")) +# value: 2222 +# time: None +``` + +## Store different data types + +### Basic types: int, float, bool, string + +To store basic types one of the fields should be used: + +* ```ConditionType.STRING_FIELD``` +* ```ConditionType.INT_FIELD``` +* ```ConditionType.BOOL_FIELD``` +* ```ConditionType.FLOAT_FIELD``` + + +Lets example it: + +```python +# Create RCDBProvider provider object and connect it to DB +db = RCDBProvider("sqlite:///example.db") + +# Crete condition types +db.create_condition_type("int_val", ConditionType.INT_FIELD) +db.create_condition_type("float_val", ConditionType.FLOAT_FIELD) +db.create_condition_type("bool_val", ConditionType.BOOL_FIELD) +db.create_condition_type("string_val", ConditionType.STRING_FIELD) + +# Add values to run 1 +db.add_condition(1, "int_val", 1000) +db.add_condition(1, "float_val", 2.5) +db.add_condition(1, "bool_val", True) +db.add_condition(1, "string_val", "test test") + +# Read values for run 1 and use them + +condition = db.get_condition(1, "int_val") +print condition.value + +condition = db.get_condition(1, "float_val") +print condition.value + +condition = db.get_condition(1, "bool_val") +print condition.value + +condition = db.get_condition(1, "string_val") +print condition.value +``` + +The output: + +``` +1000 +2.5 +True +test test +``` + +### Time information + +ConditionType.TIME_FIELD is used for time fields. Standard python datetime is used for that: (Lets see the first example): + +```python +# Create condition type +db.create_condition_type("my_val", ConditionType.TIME_FIELD) + +# Add value and time information +db.add_condition(1, "my_val", datetime(2015, 10, 10, 15, 28, 12, 111111)) +``` + + + +### Arrays and dictionaries + +Best way to store arrays and dictionaries is serializing them to JSON. Use ConditionType.JSON_FIELD for that. +RCDB conditions API doesn't provide mechanisms of converting objects to JSON and from JSON at this point. +For arrays it is done easily by json module. + + +The example from [[https://docs.python.org/2/library/json.html python 2.7 documentation]]: + +``` +>>> import json +>>> json.dumps(['foo', {'bar': ('baz', None, 1.0, 2)}]) + '["foo", {"bar": ["baz", null, 1.0, 2]}]' + +>>> json.loads('["foo", {"bar":["baz", null, 1.0, 2]}]') + [u'foo', {u'bar': [u'baz', None, 1.0, 2]}] +``` + +So, serialization is on the users side. It is done to have a better control over serialization. +This means that ***if condition type is JSON_FIELD, ```add_condition``` function awaits string*** and ***after you get condition back, Condition.value contains string***. + + +Example: + +```python +import json +from rcdb.provider import RCDBProvider +from rcdb.model import ConditionType + +# Create RCDBProvider provider object and connect it to DB +db = RCDBProvider("sqlite:///example.db") + +# Create condition type +db.create_condition_type("list_data", ConditionType.JSON_FIELD) +db.create_condition_type("dict_data", ConditionType.JSON_FIELD) + +list_to_store = [1, 2, 3] +dict_to_store = {"x": 1, "y": 2, "z": 3} + +# Dump values to JSON and save it to DB to run 1 +db.add_condition(1, "list_data", json.dumps(list_to_store)) +db.add_condition(1, "dict_data", json.dumps(dict_to_store)) + +# Get condition from database +restored_list = json.loads(db.get_condition(1, "list_data").value) +restored_dict = json.loads(db.get_condition(1, "dict_data").value) + +print restored_list +print restored_dict + +print restored_dict["x"] +print restored_dict["y"] +print restored_dict["z"] +python + +The output is: + +``` +[1, 2, 3] +{u'y': 2, u'x': 1, u'z': 3} +1 +2 +3 +``` + + +The example is located at + +``` +$RCDB_HOME/python/example_conditions_store_array.py +``` + +and can be run as: +```bash +python $RCDB_HOME/python/create_empty_sqlite.py example.db +python $RCDB_HOME/python/example_conditions_store_array.py +``` + +As one can mention unicode string is returned as unicode after json deserialization (look at u"x" instead of just "x"). +It is not a problem if you just work with this array, because python acts seamlessly with unicode strings. +As you can see in example, we use usual string "x" in restored_dict["x"] and it just works. + +If it is a problem, there is a +[[http://stackoverflow.com/questions/956867/how-to-get-string-objects-instead-of-unicode-ones-from-json-in-python stackoverlow question on that]] + +Using pyYAML to deserialize to strings looks easy. + + + +### Custom python objects + +To save custom python objects to database, jsonpickle package could be used. It is an open source project available +via pip install. It is not shipped with RCDB at the moment. + +```python +from rcdb.provider import RCDBProvider +from rcdb.model import ConditionType +import jsonpickle + + +class Cat(object): + def __init__(self, name): + self.name = name + self.mice_eaten = 1230 + + +# Create RCDBProvider provider object and connect it to DB +db = RCDBProvider("sqlite:///example.db") + +# Create condition type +db.create_condition_type("cat", ConditionType.JSON_FIELD) + + +# Create a cat and store in in the DB for run 1 +cat = Cat('Alice') +db.add_condition(1, "cat", jsonpickle.encode(cat)) + +# Get condition from database for run 1 +condition = db.get_condition(1, "cat") +loaded_cat = jsonpickle.decode(condition.value) + +print "How cat is stored in DB:" +print condition.value +print "Deserialized cat:" +print "name:", loaded_cat.name +print "mice_eaten:", loaded_cat.mice_eaten +``` + +The result: + +``` +How cat is stored in DB: +{"py/object": "__main__.Cat", "name": "Alice", "mice_eaten": 1230} +Deserialized cat: +name: Alice +mice_eaten: 1230 +``` + + +[[http://jsonpickle.github.io jsonpickle Documentation]] + +jsonpickle installation: + +system level: + +``` +pip install jsonpickle +``` + +user level: + +``` +pip install --user jsonpickle +``` + + + +### STRING_FIELD vs. JSON_FIELD vs. BLOB_FIELD + +What if data doesn't fit into the string or JSON? There is ConditionType.BLOB_FIELD type. + +Concise instruction is much like JSON: + +* Set condition type as BLOB_FIELD +* You serialize object whatever you like +* Save it to DB as string +* Load from DB +* Deserialize whatever you like + + +But what is the difference between STRING_FIELD, JSON_FIELD and BLOB_FIELD? + + +There is no difference in terms of storing the data. A Condition class, same as a database table, has ''text_value'' +field where text/string data is stored. The ONLY difference is how this fields are treated and presented in GUI. + +* STRING_FIELD - is considered to be a human readable string. + +* '''JSON_FIELD''' - is considered to be JSON, which is colored and formatted accordingly + +* '''BLOB_FIELD''' - is considered to be neither very readable string nor JSON. But it is still should converted to some string. And I hope it will never be used. diff --git a/doc/design/Connection.md b/doc/design/Connection.md new file mode 100644 index 00000000..94c722ad --- /dev/null +++ b/doc/design/Connection.md @@ -0,0 +1,90 @@ + +In order to connect to data source, RCDB uses so called `connection strings`. The connection strings have the same +form for all API-s and the CLI tools. The general form is: + +``` +dialect://username:password@host:port/database +``` + +For MySQL and SQLite databases the connection strings are: + +``` +mysql://user_name:password@host:port/database +sqlite:///path_to_file +``` + +***(!)*** Note that because SQLite doesn't have user_name and password, it starts with three slashes ///. +And thus there are 4 (four) slashes `////` in an absolute path to file. +``` +sqlite:////home/user/example.db +``` + + +**HallD MySQL connection string** (as example) + +``` +mysql://rcdb@hallddb.jlab.org/rcdb +``` + +More about connection strings could be found in [SQLAlchemy documentation](http://docs.sqlalchemy.org/en/rel_0_9/core/engines.html#database-urls%20SQLAlchemy%20documentation) + +
+ +## CLI + +For CLI tools the standard is to have ```-c``` or ```--connection``` flag and/or +```RCDB_CONNECTION``` environment variable + +```bash +export CCDB_CONNECTION=mysql://user_name:password@host:port/database +rcnd +``` + +## Python + +```python +db = RCDBProvider("sqlite:///example.db") +``` + +RCDBProvider is an object that holds database session and provides connect/disconnect functions. It uses connection strings to pass database parameters to the class. It also also carry functions to manage run condition and other RCDB data. + +The functions usually return database model objects (described right in the next section). +Additional manipulations over this objects could be done with SQLAlchemy (described later). + +In the example above class constructor is used to connect to database. But there are more connection functions: + +```python +# Create provider without connecting +db = RCDBProvider() + +# Connect to database +db.connect("sqlite:///example.db") + +# check connection and get connection string from provider +if db.is_connected: + print "connected to:", db.connection_string + +#disconnect from DB +db.disconnect() +``` + +## C++ and Java + +C++ and Java have similar class structure. The examples are: + +[Java simple example](https://github.com/JeffersonLab/rcdb/blob/master/java/src/javaExamples/SimpleExample.java) + +[C++ simple example](https://github.com/JeffersonLab/rcdb/blob/master/cpp/examples/simple.cpp) + +```c++ +// Connect has RAII approach +Connection con("mysql://rcdb@hallddb/rcdb"); + +// Get event_count for run 10173 +auto cnd = prov.GetCondition(10173, "event_count"); +``` + +## All API: + +**(!)** In some cases the ```connect``` function doesn't really connect to database (for lazy initialization features). Thus, *connect* function raises exceptions if the connection string has wrong format or there is no required libraries in the system. But if there is no physical connection to MySQL or there is no such SQLite file, ***the function doesn't guarantees to raise errors***. The errors are raised on first data retrieval in such case. (The function is more or less the same for all APIs) + diff --git a/doc/design/Creating-condition-types.md b/doc/design/Creating-condition-types.md new file mode 100644 index 00000000..9134a3a4 --- /dev/null +++ b/doc/design/Creating-condition-types.md @@ -0,0 +1,39 @@ + +To save data in run conditions, a "condition type" should be created first. It is done once in a database lifetime. +Lets look ''create_condition_type'' from the example above (we add parameter names here): + +```python +db.create_condition_type(name="my_val", + value_type, + description) +``` + +**name** - The first parameter is condition name. When we say "event_count for run 100", "event_count" is that name. +Names are case sensitive. The API doesn't validate names for any name convension and there is no built in checking for +spaces. But spaces would definitely make problems so are not recommended. + +It is possible to have names like: + +```python +category/sub/name +category-sub-name +category-sub_name +``` + +Names are just strings. RCDB doesn't provide special treatment of slashes '/' or directories. + + +**value_type** - The second parameter defines type of the value. It can be one of: + +* ConditionType.STRING_FIELD +* ConditionType.INT_FIELD +* ConditionType.BOOL_FIELD +* ConditionType.FLOAT_FIELD +* ConditionType.TIME_FIELD +* ConditionType.JSON_FIELD +* ConditionType.BLOB_FIELD + +More examples of how to use types are presented in the next section + + +**description** - 255 chars max human readable description, that other users can see. It is optional but it is very good practice to fill it. \ No newline at end of file diff --git a/doc/design/DB-and-API-structure.md b/doc/design/DB-and-API-structure.md new file mode 100644 index 00000000..ca640827 --- /dev/null +++ b/doc/design/DB-and-API-structure.md @@ -0,0 +1,116 @@ +## Database structure explained + +![RCDB sql schema](images/schema.png) + +The essential database schema is pretty simple and could be split in four groups of tables: + +1. Runs. That is where a run numbers and run periods are stored. +2. File storage. Each file has many to many relationship with run numbers. +3. Conditions. Name-value pairs are stored there +4. Meta. Logs and SQL DB schema version + +While Run and File storage are pretty simple and self descriptive, Conditions storage requires +additional explanation. + +In terms of RCDB, ```Conditions``` are name-value pairs attached to runs. So it is like: + +``` +RUN -- NAME -- VALUE +``` + +The essential RCDB feature is that while all runs may have a common set of name-value pairs (e.g. event_count, run_type), some runs may have special name-value pairs, that are not relevant for other runs. For example one may have trigger study with some trigger specific values that doesn't make sense for physics runs. The same could be imagined for for calibration runs. + +With this feature in mind, it is not optimal to create just one table with all possible conditions as columns and all values as rows (huge rows). Instead, RCDB uses so called "hybrid approach to object-attribute-value model". The are two tables: ```conditions``` that stores actual values and ```condition_types``` that holds information of condition names and their real types. ```conditions``` table has several columns to store different types of values. + + +| Storage column| Data type | +|---------------|------------| +|text_value |strings, json, blobs, long texts | +|int_value |integers | +|float_value |floats | +|bool_value |booleans | +|time_value |date time values| + +***Why is it so?*** - because we would like to have queries like: *"give me runs where event_count > 100 000"* + +i.e., if we know that **event_count* is int, we would like database to treat it as int. At the same time we would like to store strings and more general data with blobs. + +If value is int, float, bool or time, it is stored in appropriate field, which allows to use its type when querying and searching over them. At the same time, more complex objects as JSON or blobs can be stored... to figure out them lately + +This approach adds some complexity for its flexibility. But those complexities are minimized by APIs, which automate type checks. So finally users work with just run-name-values, leaving the complexities under the hood of APIs. + +Lets look at python API as an example + + +
+ +## Python + +Python API data model classes resembles this structure. Most common python classes that you work with: + +* **Run** - represents run +* **Condition** - stores data for the run +* **ConditionType** - stores condition name, field type and other + + +All classes have properties to reference each other. The main properties for conditions management are: + +```python +class Run(ModelBase): + number # int - The run number + start_time # datetime - Run start time + end_time # datetime - Run end time + conditions # list[Condition] - Conditions associated with the run + + +class ConditionType(ModelBase): + name # str(max 255) - A name of condition + value_type # str(max 255) - Type name. One of XXX_FIELD below + values # query[Condition] - query to look condition values for runs + + # Constants, used for declaration of value_type + STRING_FIELD = "string" + INT_FIELD = "int" + BOOL_FIELD = "bool" + FLOAT_FIELD = "float" + JSON_FIELD = "json" + BLOB_FIELD = "blob" + TIME_FIELD = "time" + + +class Condition(ModelBase): + time # datetime - time related to condition (when it occurred in example) + run_number # int - the run number + + @property + value # int, float, bool or string - depending on type. The condition value + + text_value # holds data if type STRING_FIELD,JSON_FIELD or BLOB_FIELD + int_value # holds data if type INT_FIELD + float_value # holds data if type FLOAT_FIELD + bool_value # holds data if type BOOL_FIELD + + run # Run - Run object associated with the run_number + type # ConditionType - link to associated condition type + name # str - link to type.name. See ConditionType.name + value_type # str - link to type.value_type. See ConditionType.value_type +``` + + +#### How data is stored in the DB + +In general, one just uses Condition.value to get the right value for the condition. But what happens under the hood? + +As you may noticed from comments above, in reality data is stored in one of the fields: + +| Storage field | Value type | +|---------------|------------| +|text_value |STRING_FIELD, JSON_FIELD or BLOB_FIELD | +|int_value |INT_FIELD | +|float_value |FLOAT_FIELD | +|bool_value |BOOL_FIELD | +|time_value |TIME_FIELD | + + +When you call **Condition.value** property, Condition class checks for **type.value_type** and returns +an appropriate **xxx_value**. \ No newline at end of file diff --git a/doc/design/Logging.md b/doc/design/Logging.md new file mode 100644 index 00000000..43d2e3d9 --- /dev/null +++ b/doc/design/Logging.md @@ -0,0 +1,37 @@ +RCDB have a logging system which stores some information about what is going on in the same database in *'log_records'* +table. + + +Set '''RCDB_USER''' environment variable to have your name in logs (or set it manually in API as shown below) + + +* Creating condition types goes to log automatically +* All condition values manipulations are not logged + +It is done in assumption, that the database has many runs and each run has many condition values, +so if each condition value creation will have text log message, the database will be bloated with log records. + + +From the other point of view, when you do a series of operations with conditions it may be a good idea to left a +log message that could be seen by other users. + + +Custom data modification by SQLAlchemy, like creating or deleting objects manually with session.commit() is not +logged too, so log notification is left to user here too. + + +How to left a log record: + + +# set RCDB_USER environment variable to give RCDB you user name +# another option is to give it in constructor +db = RCDBProvider("sqlite:///example.db", user_name="john") + +# and one more option of setting user name +db.user_name = "john" + +# simplest log version +db.add_log_record(None, "Hello everybody! You'll see this message in logs on RCDB site", 0) + + +First None means there is no specific database object ID for this message. The last '0' means there is no specific run number for this message diff --git a/doc/design/Performance.md b/doc/design/Performance.md new file mode 100644 index 00000000..c1d0061c --- /dev/null +++ b/doc/design/Performance.md @@ -0,0 +1,81 @@ + + +=== Reusing objects === + + +Most of the API functions (like add_condition(...) or get_condition(...)) can accept model objects as +parameters: + + +# 1. Using run number and condition name +db.add_condition(1, "my_value", 10) + +# 2. Using model objects +run = db.get_run(1) +ct = db.get_condition_type("my_value") +db.add_condition(run, ct, 10) + + + +When you do db.add_condition(1, "my_value", 10) condition type and run are queried inside a function. If you do several actions with one object, like adding many conditions for one run or adding one condition to many runs, reusing the object could boost performance up to 30% each. + + + + + +=== Auto commit value addition=== +Performance study shows, that approximately 50% of the time spent in add_condition(...) is used to commit changes to DB. + +To speed up conditions addition add_condition(...) function has '''auto_commit''' optional argument. +By default it is '''True''', changes are committed to DB, if ''add_condition'' call is successful. +Setting ''auto_commit''='''False''' allows to defer commit, changes are pending in SQLAlchemy cache and can be committed +manually later. + + +''auto_commit''='''False''' purposes are: + +* Make a lot of changes and commit them at one time gaining performance +* Rollback changes + + +To commit changes, having db = RCDBProvider(...) you should call db.session.commit() + + + +""" Test auto_commit feature that allows to commit changes to DB later""" +ct = self.db.create_condition_type("ac", ConditionType.INT_FIELD) + +# Add condition to addition but don't commit changes +self.db.add_condition(1, ct, 10, auto_commit=False) + +# But the object is selectable already +val = self.db.get_condition(1, ct) +self.assertEqual(val.value, 10) + +# Commit session. Now "ac"=10 is stored in the DB +self.db.session.commit() + +# Now we deffer committing changes to DB. Object is in SQLAlchemy cache +self.db.add_condition(1, ct, 20, None, True, False) +self.db.add_condition(1, ct, 30, None, True, False) + +# If we select this object, SQLAlchemy gives us changed version +val = self.db.get_condition(1, ct) +self.assertEqual(val.value, 30) + +# Roll back changes +self.db.session.rollback() +val = self.db.get_condition(1, ct) +self.assertEqual(val.value, 10) + + + +The example is available in tests: + +
+$RCDB_HOME/python/tests/test_conditions.py
+
+ + +(!) note at the same time, that more complex scenarios with not committed objects haven't been tested. + diff --git a/doc/design/SQLAlchemy.md b/doc/design/SQLAlchemy.md new file mode 100644 index 00000000..38dc5ccc --- /dev/null +++ b/doc/design/SQLAlchemy.md @@ -0,0 +1,338 @@ +## SQLAlchemy + +SQLAlchemy makes link between python classes and related database tables. It loads data from DB to classes and when +objects are changed, can commit changes back to DB. Also SQLAlchemy glues the classes and makes it possible to +navigate between objects. + +Lets see a code example: + +```python +# open database +db = rcdb.RCDBProvider("sqlite:///example.db") + +# get Run object for the run number 1 +run = db.get_run(1) + +# now we have access to all conditions for that run as +run.conditions + +# get all condition names or all condition values + +names = [condition.name for condition in run.conditions] +values = [condition.values for condition in run.conditions] +``` + +SQLAlchemy makes queries to database if needed. So when you do `run = self.db.get_run(1)`, `Run.conditions` +collection is not yet loaded from DB. It actually isn't loaded even when we do like x=run.conditions. But first time +when a real value is needed, database is queried for all conditions for that run. + + + +### Editing or deleting objects + +Even if overriding of existing values are possible for RCDB, deleting data or editing existing condition types +considered to be avoided. But sometimes it is needed. Especially at the development/debugging phase. + + +To edit or delete things SQLAlchemy '''session''' object can be used. + + +#### Editing + +**Edit condition type** + +```python +# get condition type +condition_type = db.get_condition_type("my_var") + +# Change what you need +condition_type.value_type = ConditionType.JSON_FIELD + +# Calling session commit will save changes to database +db.session.commit() +``` + +**Rename condition** + +```python +# get condition type +condition_type = db.get_condition_type("my_var") + +# Change what you need +condition_type.name = "new_var" + +# Calling session commit will save changes to database +db.session.commit() +``` + +The magic is that all data for all runs are now accessible by '''new_var''' + + +### Deleting + +Deleting objects is done with session.delete function: + +```python +# Edit condition type +condition_type = db.get_condition_type("my_var") + +# mark the object for deletion +db.session.delete(condition_type) + +# Calling session commit will save changes to database +db.session.commit() +``` + +More about session and SQLAlchemy objects manipulation with it can be found in +[sql alchemy docs](http://docs.sqlalchemy.org/en/rel_0_9/orm/session_basics.html#basics-of-using-a-session SQLAlchemy documentation) + + + + + +## Database querying + + +### Working with runs + +If you ever want to get Run object by run_number here is how: + +```python +run = db.get_run(run_number) +print run.number +print run.start_time +print run.end_time +print run.conditions... # but it is written further +``` + +How to query runs is shown far below + + +### Get runs by number (or intruduction to SQLAlchemy queries) + +Lets select all runs with run_number < 100 using SQLAlchemy + +```python +# open database +db = rcdb.RCDBProvider("sqlite:///example.db") + +# create query +query = db.session.query(Run).filter(Run.number < 100) + +# get count of selected runs +print query.count() + +# get first run from selected +print query.first() + +# get all run that matches the creteria +print query.all() +``` + +What happened? + +'''db.session''' - gets SQLAlchemy ''session'' object + +'''.query(Run)''' - here we say, that we want Run objects to be returned. At the same time we say what table we want to query + +'''.filter(Run.number < 100)''' - filtering clause + +When we've got query ready, we can actually get objects by query.first() or query.all() +(there are actually more) or just count number of runs by query.count() + +We can use Run.conditions to get conditions for each run. Lets see more advanced example + +# open database +db = rcdb.RCDBProvider("sqlite:///example.db") + +# create query +query = db.session.query(Run) + .filter(Run.number.between(50,55) + .order_by(desc(Run.number)) + +# get all such runs +runs = query.all() +for run in runs: + event_count, = (condition.value for condition in run.conditions if condition.name=='event_count') + + +It works and looks easy. But there is one drawback, each selected run will call one SELECT QUERY to DB to get its +conditions. If might be OK for many cases. + + + +=== Raw SQLAlchemy queries === + +What if we want to select runs by conditions value? + + +First, lets say, that if RCDBProvider gives access to SQLAlchemy session, then it is possible to make use of full +power of SQLAlchemy queries. + + +Lets say, we want to get all runs with '''event_count''' > '''100 000''' + + +# open database +db = rcdb.RCDBProvider("sqlite:///example.db") + +# create query +query = db.session.query(Run).join(Run.conditions).join(Condition.type)\ + .filter(ConditionType.name == "event_count")\ + .filter(Condition.int_value > 100 000)\ + .order_by(Run.number) + + +# get count of selected runs +print query.count() + +# get first run from selected +print query.first() + +# get all run that matches the creteria +print query.all() + + + +What happened here. + +By first line: + +query = db.session.query(Run).join(Run.conditions).join(Condition.type)\ + + +we say, that we would like to select Run objects ('''.query(Run)'''), and also that we will use conditions +and condition types ('''.join(Run.conditions).join(Condition.type)'''). + + +Then we filter results (.'''filter(...)''') and ask results to by ordered by Run.number ('''.order_by(Run.number)''') + + +All these functions (join, filter, order_by, ...) returns Query object, that allows to stack them as many as needed. + + +Finally, to get the results, one of query.count(), query.first(), query.one() or query.all() is called. + + +But probably you already feel drawbacks of this approach: + +* First, you see that you have to use int_value to filter conditions. That by many means worse than using Condition.value property, that handles type automatically. +* Another drawback is that when you add more logic, the query becomes bulky. + + +Lets imagine next example. We look for run in range 1000 to 2000 with event_count > 10000, some data_value in range 1.2 and 2.4 + + +query = db.session.query(Run).join(Run.conditions).join(Condition.type)\ + .filter(Run.number.between(1000, 2000)\ + .filter(((ConditionType.name == "event_count") & (Condition.int_value > 10000)) | + ((ConditionType.name == "data_value") & (Condition.float_value.between(1.2, 2.4))))\ + .order_by(Run.number) + +print query.all() + + + +Note that instead of common '''&&''' and '''||''', '''&''' and '''|''' is used. +SQLAlchemy overloads this operators to use for comparison. + +Note also, that such expressions should be in parentheses. It is possible to use '''or_''' and '''and_''' functions +instead, but it doesn't improve the readability. + + + +=== Querying using RCDB helpers === + +RCDB ConditionType provide helpful properties to make querying easier. + + +# get condition type +t = db.get_condition_type("event_count") + +# select runs where event_count > 1000 +query = t.run_query.filter(t.value_field > 1000) + +print query.all() + + + +What happened? + +*'''run_query''' - returns query bootstrap that selects Run objects for given type. So it hides this thing from the raw query above: + + +....query(Run).join(Run.conditions).join(Condition.type) ... .filter(((ConditionType.name == "event_count") + + + +*'''value_field''' - returns the right Condition.xxx_value for a given type. When you put '''t.value_field > 1000''' here, ConditionType '''t''' looked at his '''value_type''' and selected the right Condition.int_value to compare + + +But there is a limitation. Each condition type should has its own query. But queries can be combined by '''union''' or +'''intersect''' methods later. + + +Lets look at the example, where we fill DB with dummy data and then query for runs using the helper properties. The same example can be found in $RCDB_HOME/python/example_conditions_query.py + + +# create in memory SQLite database +db = rcdb.RCDBProvider("sqlite://") +rcdb.model.Base.metadata.create_all(db.engine) + +# create conditions types +event_count_type = db.create_condition_type("event_count", ConditionType.INT_FIELD) +data_value_type = db.create_condition_type("data_value", ConditionType.FLOAT_FIELD) + +# create runs and fill values +for i in range(0, 100): + db.create_run(i) + db.add_condition(i, event_count_type, i + 950) #event_count in range 950 - 1049 + db.add_condition(i, data_value_type, (i/100.0) + 1) #data_value in 1 - 2 + + +""" Demonstrates ConditionType query helpers""" +event_count_type = db.get_condition_type("event_count") +data_value_type = db.get_condition_type("data_value") + +# select runs where event_count > 1000 +query = event_count_type.run_query.filter(event_count_type.value_field > 1000).filter(Run.number <=53) +print query.all() + +# select runs where 1.52 < data_value < 1.7 +query2 = data_value_type.run_query + .filter(data_value_type.value_field.between(1.52, 1.7))\ + .filter(Run.number < 55) +print query2.all() + +# combine results of this two queries +print "Results intersect:" +print query.intersect(query2).all() +print "Results union:" +print query.union(query2).all() + + +The output is: + +
+[, , ]
+[, , ]
+
+Results intersect:
+[, ]
+
+Results union:
+[, , , ]
+
+ + +More on SQLAlchemy queries in +[http://sqlalchemy.readthedocs.org/en/rel_0_9/orm/tutorial.html#querying SQLAlchemy querying tutorial] +[http://sqlalchemy.readthedocs.org/en/rel_0_9/orm/query.html SQLAlchemy Query API] + + +The example is available as + +python $RCDB_HOME/python/example_conditions_query.py + +(It creates inmemory database so there is no need in creaty_empty_sqlite.py) + diff --git a/doc/images/schema.png b/doc/images/schema.png new file mode 100644 index 0000000000000000000000000000000000000000..5c2e5fe645c36d6762055b8e45c2a96b933dadc1 GIT binary patch literal 108718 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℘}P86H5WJ(Rt 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/images/web_quick_query_top.png b/doc/images/web_quick_query_top.png new file mode 100644 index 0000000000000000000000000000000000000000..b2d7dd9c2723e834ab07478ddae47db17604db4e GIT binary patch literal 4230 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 cNzxepAFs_5c6?07*qoM6N<$g7*3|EdT%j literal 0 HcmV?d00001 diff --git a/doc/index.html b/doc/index.html new file mode 100644 index 00000000..7bd2f587 --- /dev/null +++ b/doc/index.html @@ -0,0 +1,129 @@ + + + + + Document + + + + + + + + + + + + + +
+ + + + + + + + + + + + diff --git a/doc/tutorials/Installation.md b/doc/tutorials/Installation.md new file mode 100644 index 00000000..40252556 --- /dev/null +++ b/doc/tutorials/Installation.md @@ -0,0 +1,104 @@ +#### 0. Get rcdb + +Clone this repository: + +``` +git clone https://github.com/JeffersonLab/rcdb.git +``` + +
+ +#### 1. Install python requirements + +``` +cd rcdb +pip install -r requirements.txt +``` + + +
+ +#### 2. Set environment + +There are *environment.\* scripts, which automatically set +environment variables for the of rcdb + +```bash +source environment.bash # for bash +``` + +The script sets ***$RCDB_HOME*** to RCDB root directory, appends ***$PYTHONPATH*** and ***$PATH***. The full list of variables set by the script and how to set them manually one can [read below](#setup-environment-manually). + +
+ +#### 3. Database connection + +One needs so called "connection string" in order to connect to database. For now we consider to use MySQL and SQLite databases. The connection strings for them are: + +***MySQL*** +``` +mysql://user_name:password@host:port/database +``` + + +***SQLite*** +``` +sqlite:///path_to_file +``` + +**(!)** Note that because SQLite doesn't have user_name and password, it starts with three slashes ///. +And thus there should be four slashes //// in absolute path to file. + +``` +sqlite:////home/user_name/rcdb.sqlite.db +``` + +
+ +***RCDB_CONNECTION*** + +The common way of different parts of RCDB to know the connection string is to set RCDB_CONNECTION environment variable: + +```bash +#bash: +export RCDB_CONNECTION="mysql://rcdb@hallddb.jlab.org/rcdb" +``` + +The other common way is to set `-c \` key + + +
+ +#### 4. Experimenting + +To experiment with RCDB and examples below, there is create_empty_sqlite.py script in $RCDB_HOME/python folder. +The script creates empty sqlite database. The usage is: + +```bash +python $RCDB_HOME/python/create_empty_sqlite.py path_to_database.db +``` + +One can download HALLD sqlite database (autogenerated daily) here: + +https://halldweb.jlab.org/dist/rcdb.sqlite + +
+
+
+ +---------------------- +### Setup environment manually + +If one needs to setup environment variables ***manually***, here is the list of variables, `environment.XXX` scripts set: + +* `RCDB_HOME` - set to the rcdb directory (where environment.* scripts are located) +* `PYTHONPATH` - add `$RCDB_HOME/python` - in order to import rcdb module from python +* `PATH` - add `"$RCDB_HOME":"$RCDB_HOME/bin":$PATH` + +If one wants to use C++ ***readout*** API +* `LD_LIBRARY_PATH` - add `$RCDB_HOME/cpp/lib` +* `CPLUS_INCLUDE_PATH` - add `$RCDB_HOME/cpp/include` +* `PATH` - add `"$RCDB_HOME/bin"` + + + diff --git a/doc/tutorials/Python-basics.md b/doc/tutorials/Python-basics.md new file mode 100644 index 00000000..d6f087f6 --- /dev/null +++ b/doc/tutorials/Python-basics.md @@ -0,0 +1,110 @@ +At least to start with RCDB conditions, to put values and to get them back: + + +#Python + +```python +from datetime import datetime +from rcdb.provider import RCDBProvider +from rcdb.model import ConditionType + +# 1. Create RCDBProvider object that connects to DB and provide most of the functions +db = RCDBProvider("sqlite:///example.db") + +# 2. Create condition type. It is done only once +db.create_condition_type("my_val", ConditionType.INT_FIELD, "This is my value") + +# 3. Add data to database +db.add_condition(1, "my_val", 1000) + +# Replace previous value +db.add_condition(1, "my_val", 2000, replace=True) + +# 4. Get condition from database +condition = db.get_condition(1, "my_val") + +print condition +print "value =", condition.value +print "name =", condition.name + +``` + +The script result: +``` + +value = 2000 +name = my_val +``` + + +More actions on objects: + +```python +# 5. Get all existing conditions names and their descriptions +for ct in db.get_condition_types(): + print ct.name, ':', ct.description +``` + + +The script result: +``` +my_val : This is my value +``` + + +```python +# 6. Get all values for the run 1 +run = db.get_run(1) +print "Conditions for run {}".format(run.number) +for condition in run.conditions: + print condition.name, '=', condition.value +``` + + +The script result: +
+my_val = 2000
+
+ + +The example also available as: + +```bash +$RCDB_HOME/python/example_conditions_basic.py +``` + + +It is assumed that 'example.db' is SQLite database, created by *create_empty_sqlite.py* script. To run it: + + +python $RCDB_HOME/python/create_empty_sqlite.py example.db +python $RCDB_HOME/python/example_conditions_basic.py + +'''(!)''' note that to run the script again you probably have to delete the database rm example.db + +The next sections will cover this example and give thorough explanation on what is here. + + + +## Command line tools +Command line tools provide less possibilities for data manipulation than python API at the moment. + +```bash +export RCDB_CONNECTION=mysql://rcdb@localhost/rcdb +rcnd --help # Gives you self descriptive help +rcnd -c mysql://rcdb@localhost/rcdb # -c flag sets connection string from command line instead of environment +rcnd # Gives database statistics, number of runs and conditions +rcnd 1000 # See all recorded values for run 1000 +rcnd 1000 event_count # See exact value of 'event_count' for run 1000 + +# Creating condition type (need to be done once) +rcnd --create my_value --type string --description "This is my value" + +# Write value for run 1000 for condition 'my_value' +rcnd --write "value to write" --replace 1000 my_value + +# See all condition names and types in DB +rcnd --list +``` + +More information and examples are in [[#Command line tools]] section below. diff --git a/doc/tutorials/Query-syntax.md b/doc/tutorials/Query-syntax.md new file mode 100644 index 00000000..2ac7ee52 --- /dev/null +++ b/doc/tutorials/Query-syntax.md @@ -0,0 +1,95 @@ +## Queries + +Queries allow to select runs using simple 'python if syntax'. Condition names and aliases are used as variables. Queries are implemented in web GUI, python API and CLI . + +**Example.** Query to get production runs with beam current around 100 uA and 'BCAL' in run_config ( here 'BCAL' is a detector / subsystem in HallD and run_config is a name of a configuration file): + +General/web site query: + +```bash +@is_production and 80 < beam_current < 120 and 'BCAL' in run_config +``` + +[The result of the query on HallD website](https://halldweb.jlab.org/rcdb/runs/search?runFrom=10000&runTo=20000&q=%40is_production+++and+++80+%3C+beam_current+%3C+120+++and+++%27BCAL%27+in+run_config) + +Queries syntax are the same across API-s (which supports queries at all) + +python: + +```python +runs = db.select_runs("@is_production and 80 < beam_current < 120 and 'BCAL' in run_config") +``` + +CLI: + +```bash +>>rcdb sel "@is_production and 80 < beam_current < 120 and 'BCAL' in run_config" +``` + + +## Syntax + +Queries use python 'if' syntax. The full python documentation is [here](https://docs.python.org/2/library/stdtypes.html). + +Concise version is: + +* ```<```, ```<=```, ```==```, ```!=```, ```=>```, ```>``` to compare values (same as in C++) + +* ```or```, ```and```, ```not``` for logic operators (||, &&, ! in C++) + +* ```in``` operator is to check a value or a subarray is present in the array, (arrays or lists in python can be given in square braces ```[]```): + + ```python + 5 in radiator_id + radiator_id in [5, 6, 12] + ``` + +* strings must be enclosed in ```'``` - single braces. ```==```, ```!=``` operators can be used to compare two strings, ```in``` operator works for substrings and letters: + + ```python + run_config == 'FCAL_BCAL_PS_m7.conf' + 'hd_all' in run_type + ``` + +## Aliases + +One may notice ```@is_production``` in the query example above. ```@``` means 'alias' - predefined set of conditions. For example for HallD ```@is_production``` alias is given as: + +```python +run_type in ['hd_all.tsg', 'hd_all.tsg_ps', 'hd_all.bcal_fcal_st.tsg'] and +beam_current > 2 and +event_count > 500000 and +solenoid_current > 100 and +collimator_diameter != 'Blocking' +``` + + +Aliases - are predefined set of filter expressions. The purpose of aliases is to shorten standard search expressions. Aliases starts with ```@``` sign. + +For example, +``` +@is_cosmic +``` + +Set to: + +```python +run_type == 'hd_all.tsg_cosmic' and 'COSMIC' in daq_run and beam_current < 10 +``` + +One can use it like: + +```python +@is_cosmic and magnet_current > 800 +``` + +When the query is executed, this expression will be expanded as: + +```python +(run_type == 'hd_all.tsg_cosmic' and 'COSMIC' in daq_run and beam_current < 10) and magnet_current > 800 +``` + + +### GlueX standard search aliases +[Awailable at GluEx wiki|] +[Wiki](https://halldweb.jlab.org/wiki/index.php/RCDB_Standard_Searches) \ No newline at end of file diff --git a/doc/tutorials/Select-values.md b/doc/tutorials/Select-values.md new file mode 100644 index 00000000..78d7b891 --- /dev/null +++ b/doc/tutorials/Select-values.md @@ -0,0 +1,144 @@ +- [TL;DR; aka Too long didn't read](#tl-dr) +- [Select and filter](#select-and-filter) +- [All options](#all-options) +- [Result details](#result-details) +- [Performance](#performance) +- [From shell](#from-shell) + +## TL; DR; + +Fastest way to select values in 3 lines: + +```python +# import RCDB +from rcdb.provider import RCDBProvider + +# connect to DB +db = RCDBProvider("mysql://rcdb@hallddb.jlab.org/rcdb") + +# select values with query +table = db.select_values(['polarization_angle','beam_current'], "@is_production", run_min=30000, run_max=30050) +``` + +```table``` will contain 3 columns ```run_number```, ```polarization_angle```, ```beam_current```. Like: + +```python +[[30044, -1.0, UNKNOWN], + [30045, 45.0, PARA ], +...] +``` + +
+ +## Select and filter + +The fastest designed way to get values from RCDB is by using ```select_values``` function. +The full example is here: +[$RCDB_HOME/python/example_select_values.py](https://github.com/JeffersonLab/rcdb/blob/master/python/example_select_values.py) + +The simplest usage is to put condition names and a run range: + +```python +from rcdb.provider import RCDBProvider + +db = RCDBProvider("mysql://rcdb@hallddb.jlab.org/rcdb") + +table = db.select_values(['polarization_angle','polarization_direction'], run_min=30000, run_max=30050) + +# Print results +print(" run_number, polarization_angle, polarization_direction") +for row in table: + print row[0], row[1], row[2] +``` + +output: +``` +30044 -1.0 UNKNOWN +30045 45.0 PARA +``` +By default, the result is a table with runs numbers in the first column and requested conditions values in other columns. + +
+ +### Filter +It is possible to put [a selection query](Query-syntax) as a second argument [as in the above example](#tl-dr): + +```python +table = db.select_values(['polarization_angle'], "@is_production", run_min=30000, run_max=30050) +``` + +### Exact run numbers +Instead of using run range one can specify exact run numbers using ```runs``` argument + +```python +table = db.select_values(['event_count'], "@is_production", runs=[30300,30298,30286]) +``` + +
+ +## All options + +```python + # Default value | Descrition + #---------------+------------------------------------ +table = db.select_values(val_names=['event_count'], # [] | List of conditions names to select, empty by default + search_str="@is_production and event_count>1000", # "" | Search pattern. + run_min=30200, # 0 | minimum run to search/select + run_max=30301, # sys.maxsize | maximum run to search/select + sort_desc=True, # False | if True result runs will by sorted descendant by run_number, ascendant if False + insert_run_number=True, # True | If True the first column of the result will be a run number + runs=None) # None | a list of runs to search from. In this case run_min and run_max are not used +``` + +Remarks: +1. ```val_names```. If ```val_names``` list is empty, run numbers will be selected (assuming that insert_run_number=True by default) + +2. ```search_str```. If ```search_str`` is empty, the function doesn't apply filters and just select values for all runs according to ['run_min' - 'run_max'] or 'runs' list + + +
+ +## Result details + +The result of ```select_values``` (called ```table``` in the examples) holds more information than just values. Here some useful fields: + +- table.selected_conditions - selected condition names (or call it column titles) +- table.performance['total'] - function execution time + +```python +table = db.select_values(['beam_current'], "@is_production", runs=[30300,30298,30286]) +print("We selected: " + ", ".join(table.selected_conditions)) +print("It took {:.2f} sec ".format(table.performance['total'])) +``` + +result: + +``` +We selected: run, beam_current +It took 0.14 sec +``` + +
+ +## Performance + +calling ```select_values``` is the fastest way to get such tables of values. Before RCDB had a chain of functions ```select_runs(...).get_values(...)``` to select values. This chain is still there but it is much slower. MUCH SLOWER. + +More info about select runs && get values +- [Select runs & get values](Select-runs-and-get-values) (python) + +
+ +## From shell +(Shell one liner) + +Suppose one wants to select values in a bash script but doesn't want to create a separate python script. +It is possible to call ```python -c "semicolon;separated;commands"``` + +Combining everything in such one-liner: + +```bash +python -c "import rcdb.provider;t=rcdb.provider.RCDBProvider('mysql://rcdb@hallddb.jlab.org/rcdb').select_values(['polarization_angle','polarization_direction'], run_min=30000, run_max=31000);print('\n'.join([' '.join(map(str, r)) for r in t]))" +``` + +Shouldn't be there an easier way? It was planned to have ```rcdb sel``` command doing it. But it hasn't been fully implemented yet. If you have a spare time (or student) to contribute, please, contact me (Dmitry) diff --git a/doc/tutorials/rcnd.md b/doc/tutorials/rcnd.md new file mode 100644 index 00000000..1c288255 --- /dev/null +++ b/doc/tutorials/rcnd.md @@ -0,0 +1,124 @@ +*(!) Important notice:* + +Initially it was planned that RCDB should have two commands: + +* ```rcdb``` - to select values and readout the database +* ```arcdb``` - which stands for 'admin rcdb' that allows to add data and manipulate the DB + +But because RCDB needed something from the beginning, ```rcnd``` tool was created which has some limited abilities to get data from the database and add new values. ```rcnd``` was planned as a temporary command while a/rcdb commands would be in development. In reality, the development has been slowed down and at this point we have: + +* ```rcnd``` - widely used command with pretty limited abilities for getting and adding data to DB +* ```rcdb``` - command that has VERY limited abilities and misleads users becuase of it. + +Solutions? Best would be to participate in RCDB development. + +## RCND + + +```bash +> export RCDB_CONNECTION=mysql://rcdb@localhost/rcdb +> rcnd --help # Gives you self descriptive help +> rcnd -c mysql://rcdb@localhost/rcdb # -c flag sets connection string from command line +> rcnd # Gives database statistics, number of runs and conditions +``` + +Output: + +``` +Runs total: 1387 +Last run : 2472 +Condition types total: 9 +Conditions: + + components + component_stats +... +``` + + + +### Getting condition names and info + +To get all conditions `-l` or `--list` flags are to be used. It shows condition names, types and descriptions (if exists): + +```bash +> rcnd -l +components (json) +component_stats (json) +event_count (int) - Run events count +event_rate (float) - Events per sec. +... +``` + + +To get names only use `--list-names`: + +``` +> rcnd --list-names +components +component_stats +event_count +event_rate +... +``` + +### Getting value by the run number +To see all conditions and values for a run: + +``` +> rcnd 1000 # See all recorded values for run 1000 +components = (json){"ROCBCAL2": "ROC", "ROCBCAL3": "ROC", "ROCBCAL1":... +component_stats = (json){"ROCBCAL2": {"evt-number": 487, "data-rate": 300.... +event_count = 487 +rtvs = (json){"%(CODA_ROL1)": "/home/hdops/CDAQ/daq_dev_v0.31/d... +run_config = 'pulser.conf' +run_type = 'hd_bcal_n.ti' +... +``` + + +Add name to get value of the only condition: + +``` +> rcnd 1000 event_count +487 + +> rcnd 1000 components +{"ROCBCAL2": "ROC", "ROCBCAL3": "ROC"} +``` + + + +### Writing data + +Creating condition type (need to be done once): + +``` +> rcnd --create my_value --type string --description "This is my value" +ConditionType created with name='my_value', type='string', is_many_per_run='False' +``` + +Where --type is: + +* bool, int, float, string - basic types. float is the default +* json - to store arrays or custom objects +* time - to store just time. (You can alwais add time information to any other type) +* blob - binary blob. Don't use it if possible + + +#### Naming policy (not strict at all): + +* Don't use spaces. Use '_' instead +* Full words are better. So 'event_count' is better than evt_cnt +* Max name is 255 character. But please, make them shorter + + + +Write value for run 1000 for condition 'my_value' + +``` +> rcnd --write "value to write" --replace 1000 my_value +Written 'my_value' to run number 1000 +``` + +Without `--replace` error is raised, if run 1000 already have different value for 'my_value' \ No newline at end of file From 3a0df13731f8f8c9d608bc7b99760be8710402d5 Mon Sep 17 00:00:00 2001 From: Dmitry Romanov Date: Wed, 14 Jun 2023 10:01:50 -0400 Subject: [PATCH 02/34] doc uypdate --- doc/README.md | 2 +- doc/daq/daq_concepts.md | 21 +++++++++++++++++++++ python/MANIFEST.in | 0 python/packaging.md | 0 python/profile.txt | 23 ----------------------- python/rcdb/__main__.py | 0 python/rcdb/rcdb_cli/db.py | 0 python/rcdb/rcdb_cli/mkdb.py | 27 +++++++++++++++++++++++++++ python/rcdb/version.py | 0 python/requirements.txt | 0 python/setup.py | 0 11 files changed, 49 insertions(+), 24 deletions(-) create mode 100644 python/MANIFEST.in create mode 100644 python/packaging.md delete mode 100644 python/profile.txt create mode 100644 python/rcdb/__main__.py create mode 100644 python/rcdb/rcdb_cli/db.py create mode 100644 python/rcdb/rcdb_cli/mkdb.py create mode 100644 python/rcdb/version.py create mode 100644 python/requirements.txt create mode 100644 python/setup.py diff --git a/doc/README.md b/doc/README.md index 2d79b51b..f85014c9 100644 --- a/doc/README.md +++ b/doc/README.md @@ -14,7 +14,7 @@ One can consider two main aspects of what conceptually RCDB is designed for: ## Concepts -[daq concepts](daq/daq_concepts.md ':include') +[daq_concepts](daq/daq_concepts.md ':include') Software wise: diff --git a/doc/daq/daq_concepts.md b/doc/daq/daq_concepts.md index e69de29b..1055efa6 100644 --- a/doc/daq/daq_concepts.md +++ b/doc/daq/daq_concepts.md @@ -0,0 +1,21 @@ +1. ***Automation and Minimal Human Intervention***: RCDB is designed to be an automated database, striving to gather + data with as little human intervention as possible. While there are certain types of data typically provided by human + operators (such as run comments), the goal is to minimize the number of interactions with + biological organisms. + +2. ***Continuous Updates***: RCDB is designed to be updated continuously throughout its runtime, without maintaining a + historical record by default (though logs are present, they serve a different purpose). The latest data is generally + assumed to be the most accurate, hence any new data will replace existing values. In cases where a complete record of + all values during a run is required, the data can be stored in an array or collection, which is then saved as JSON. + For instance, run statistics are updated every 10 seconds. This approach ensures that some data is preserved in the + event of a system crash before completion. + +3. ***Modularity and Extensibility***: RCDB goes beyond providing an API for data addition; it also offers a suite of + tools and modules that are as isolated as possible to provide ready-to-use DAQ writing functionality, such as CODA + integration. It further allows users to introduce their own methods, promoting a more customizable and extensible + system. + +4. ***Fault Isolation and Notification***: In the event of a module failure during an update, the issue should be logged + and human operators should be alerted. However, it's crucial that such failures do not impact the functionality of + other modules. This principle ensures that individual module failures do not cascade into system-wide disruptions, + thereby maintaining overall system stability. diff --git a/python/MANIFEST.in b/python/MANIFEST.in new file mode 100644 index 00000000..e69de29b diff --git a/python/packaging.md b/python/packaging.md new file mode 100644 index 00000000..e69de29b diff --git a/python/profile.txt b/python/profile.txt deleted file mode 100644 index 20c195f3..00000000 --- a/python/profile.txt +++ /dev/null @@ -1,23 +0,0 @@ - -== Packages == -pip install gprof2dot -apt-get install xdot - - -python -m cProfile -o foo.stats - - - - - +{% macro run_search_box(condition_types=[], run_from=-1, run_to=-1, search_query="") %} + {% set run_from_str = "" if run_from == -1 else run_from %} + {% set run_to_str = "" if run_to == -1 else run_to %} + +
+{% endmacro %} + +{% macro run_search_box_scripts(condition_types=[], run_from=-1, run_to=-1, search_query="") %} + + + + + + {% endmacro %} \ No newline at end of file diff --git a/rcdb_www/templates/runs/conditions.html b/rcdb_web/templates/runs/conditions.html similarity index 100% rename from rcdb_www/templates/runs/conditions.html rename to rcdb_web/templates/runs/conditions.html diff --git a/rcdb_www/templates/runs/custom_column.html b/rcdb_web/templates/runs/custom_column.html similarity index 100% rename from rcdb_www/templates/runs/custom_column.html rename to rcdb_web/templates/runs/custom_column.html diff --git a/rcdb_www/templates/runs/example.html b/rcdb_web/templates/runs/example.html similarity index 100% rename from rcdb_www/templates/runs/example.html rename to rcdb_web/templates/runs/example.html diff --git a/rcdb_www/templates/runs/index.html b/rcdb_web/templates/runs/index.html similarity index 100% rename from rcdb_www/templates/runs/index.html rename to rcdb_web/templates/runs/index.html diff --git a/rcdb_www/templates/runs/info.html b/rcdb_web/templates/runs/info.html similarity index 100% rename from rcdb_www/templates/runs/info.html rename to rcdb_web/templates/runs/info.html diff --git a/rcdb_www/templates/runs/not_found.html b/rcdb_web/templates/runs/not_found.html similarity index 100% rename from rcdb_www/templates/runs/not_found.html rename to rcdb_web/templates/runs/not_found.html diff --git a/rcdb_www/templates/settings/layout.html b/rcdb_web/templates/settings/layout.html similarity index 100% rename from rcdb_www/templates/settings/layout.html rename to rcdb_web/templates/settings/layout.html diff --git a/rcdb_www/templates/settings/password.html b/rcdb_web/templates/settings/password.html similarity index 100% rename from rcdb_www/templates/settings/password.html rename to rcdb_web/templates/settings/password.html diff --git a/rcdb_www/templates/settings/profile.html b/rcdb_web/templates/settings/profile.html similarity index 100% rename from rcdb_www/templates/settings/profile.html rename to rcdb_web/templates/settings/profile.html diff --git a/rcdb_www/templates/show_entries.html b/rcdb_web/templates/show_entries.html similarity index 96% rename from rcdb_www/templates/show_entries.html rename to rcdb_web/templates/show_entries.html index 7e80023e..982924ef 100644 --- a/rcdb_www/templates/show_entries.html +++ b/rcdb_web/templates/show_entries.html @@ -1,21 +1,21 @@ -{% extends "layout.html" %} -{% block body %} - {% if session.logged_in %} -
-
-
Title: -
-
Text: -
-
-
-
- {% endif %} -
    - {% for entry in entries %} -
  • {{ entry.title }}

    {{ entry.text|safe }} - {% else %} -
  • Unbelievable. No entries here so far - {% endfor %} -
+{% extends "layout.html" %} +{% block body %} + {% if session.logged_in %} +
+
+
Title: +
+
Text: +
+
+
+
+ {% endif %} +
    + {% for entry in entries %} +
  • {{ entry.title }}

    {{ entry.text|safe }} + {% else %} +
  • Unbelievable. No entries here so far + {% endfor %} +
{% endblock %} \ No newline at end of file diff --git a/rcdb_www/templates/statistics/index.html b/rcdb_web/templates/statistics/index.html similarity index 96% rename from rcdb_www/templates/statistics/index.html rename to rcdb_web/templates/statistics/index.html index 9bc99ae4..9321dc4b 100644 --- a/rcdb_www/templates/statistics/index.html +++ b/rcdb_web/templates/statistics/index.html @@ -1,58 +1,58 @@ -{% extends 'layouts/base.html' %} - -{% set page_title = 'Statistics' %} - -{% block container %} - -
- -

Database schema version

- - - - - {% for db_version in db_versions %} - - - - - - {% endfor %} - - - - -

Statistics

- -
VersionTime of updateComments
- {% if loop.first %} - {{ db_version.version }} < - {% else %} - {{ db_version.version }} - {% endif %} - - {{ db_version.created }} - - {{ db_version.comment }} -
- - - - - - - - - - - - - - -
Run count{{ run_count }}
Last run - {{ run_last.number }} -   Started: {{ run_last.start_time }} -
Boards in DB {{ boards_count }}
Crates in DB {{ crates_count }}
-
- +{% extends 'layouts/base.html' %} + +{% set page_title = 'Statistics' %} + +{% block container %} + +
+ +

Database schema version

+ + + + + {% for db_version in db_versions %} + + + + + + {% endfor %} + + + + +

Statistics

+ +
VersionTime of updateComments
+ {% if loop.first %} + {{ db_version.version }} < + {% else %} + {{ db_version.version }} + {% endif %} + + {{ db_version.created }} + + {{ db_version.comment }} +
+ + + + + + + + + + + + + + +
Run count{{ run_count }}
Last run + {{ run_last.number }} +   Started: {{ run_last.start_time }} +
Boards in DB {{ boards_count }}
Crates in DB {{ crates_count }}
+
+ {% endblock %} \ No newline at end of file diff --git a/rcdb_www/templates/user/index.html b/rcdb_web/templates/user/index.html similarity index 100% rename from rcdb_www/templates/user/index.html rename to rcdb_web/templates/user/index.html diff --git a/rcdb_www/templates/user/profile.html b/rcdb_web/templates/user/profile.html similarity index 100% rename from rcdb_www/templates/user/profile.html rename to rcdb_web/templates/user/profile.html diff --git a/start_www.py b/start_www.py index af1dd1da..be688cac 100644 --- a/start_www.py +++ b/start_www.py @@ -16,9 +16,9 @@ def get_this_folder(): sys.path.insert(0, python_folder) # import and start web site - import rcdb_www + import rcdb_web if "RCDB_CONNECTION" in os.environ.keys(): - rcdb_www.app.config["SQL_CONNECTION_STRING"] = os.environ["RCDB_CONNECTION"] + rcdb_web.app.config["SQL_CONNECTION_STRING"] = os.environ["RCDB_CONNECTION"] - rcdb_www.app.run() + rcdb_web.app.run() From 9d35fafd50888e311b0b30b9a63189f0f3abec26 Mon Sep 17 00:00:00 2001 From: Dmitry Romanov Date: Mon, 18 Sep 2023 15:24:13 -0400 Subject: [PATCH 13/34] Update scripts for db v2 --- python/rcdb/provider.py | 4 +- python/rcdb/rcdb_cli/db.py | 74 +++++++++++++++++++++---- python/requirements.txt | 8 ++- python/setup.py | 4 +- python/tests/test_sql_schema_version.py | 4 +- rcdb_web/run_table.py | 2 - sql/update_db_v1_to_v2.sql | 59 ++++++++++++++++++++ 7 files changed, 135 insertions(+), 20 deletions(-) create mode 100644 sql/update_db_v1_to_v2.sql diff --git a/python/rcdb/provider.py b/python/rcdb/provider.py index 9116749e..fa8878e6 100644 --- a/python/rcdb/provider.py +++ b/python/rcdb/provider.py @@ -67,7 +67,7 @@ def __init__(self, connection_string=None, user_name="", check_version=True): # ------------------------------------------------ # Check DB version # ------------------------------------------------ - def get_sql_schema_version(self): + def get_schema_version(self): """Check if connected SQL schema is of the right version""" schema_version, = self.session.query(SchemaVersion.version) \ @@ -112,7 +112,7 @@ def connect(self, connection_string="mysql+pymysql://rcdb@127.0.0.1/rcdb", check self._connection_string = connection_string if check_version: - db_version = self.get_sql_schema_version() + db_version = self.get_schema_version() if db_version != rcdb.SQL_SCHEMA_VERSION: message = "SQL schema version doesn't match. " \ "Retrieved DB version is {0}, required version is {1}" \ diff --git a/python/rcdb/rcdb_cli/db.py b/python/rcdb/rcdb_cli/db.py index 995033dc..df893239 100644 --- a/python/rcdb/rcdb_cli/db.py +++ b/python/rcdb/rcdb_cli/db.py @@ -5,7 +5,7 @@ import rcdb from rcdb import RCDBProvider -from rcdb.model import SchemaVersion +from rcdb.model import SchemaVersion, Alias, RunPeriod from rcdb.rcdb_cli.context import pass_rcdb_context from rcdb.provider import stamp_schema_version @@ -23,16 +23,68 @@ def db(ctx): # add a command to the 'db' group @db.command() -@click.option("--connection", help="The connection string for the database.") -@click.option("--connection", help="The connection string for the database.") -def update(connection): - """Update the database schema.""" - engine = create_engine(connection) - metadata = MetaData() - # Perform your database operations here... - # For example, load a table from the database or create it if it doesn't exist - users = Table('users', metadata, autoload_with=engine, extend_existing=True) - # ... +@pass_rcdb_context +def update(context): + provider = RCDBProvider(context.connection_str, check_version=False) + + # Check something exists + if not sqlalchemy.inspect(provider.engine).has_table(SchemaVersion.__tablename__): + print('The schema version table does not exists. It looks like RCDB v1.') + current_version = 1 + else: + print('Found schema version table') + # Check schema version + current_version = provider.get_schema_version() + + if current_version !=1: + print(f"Can't update schema version. Current version is: {current_version.version}. This command can update:") + print(f" DB v1 --> v2") + return + else: + print("Found DB v1. Will do v1 --> v2 update") + + # PRINTOUT PART + print("This command changes RCDB schema in DB") + click.echo(click.style('(!!!) NEVER EVER RUN THIS ON PRODUCTION DB WITHOUT PRIOR TESTING (!!!)', bold=True)) + print(" -This operation is not done in one transaction. If update fails in the middle, DB will be unusable") + print(" -Older versions of RCDB clients will not work with new DB schema") + print("\nDB: {}\n".format(context.connection_str)) + + # Double check user knows what will happen + if not click.confirm('Do you really want to continue?'): + return + + # TODO move next it to provider, create schema_update_v1_v2 function !!! + + # That we will need for DB + metadata = rcdb.model.Base.metadata + provider = RCDBProvider(context.connection_str, check_version=False) + + # Create alias table + Alias.__table__.create(provider.engine) + + # Create run periods table + RunPeriod.__table__.create(provider.engine) + + with provider.engine.connect() as conn: + conn.execute(""" + DROP TABLE IF EXISTS `trigger_thresholds` ; + DROP TABLE IF EXISTS `trigger_masks` ; + DROP TABLE IF EXISTS `readout_thresholds` ; + DROP TABLE IF EXISTS `readout_masks` ; + DROP TABLE IF EXISTS `dac_presets` ; + DROP TABLE IF EXISTS `crates` ; + DROP TABLE IF EXISTS `boards` ; + DROP TABLE IF EXISTS `board_installations_have_runs` ; + DROP TABLE IF EXISTS `board_installations` ; + DROP TABLE IF EXISTS `board_configurations_have_runs` ; + DROP TABLE IF EXISTS `board_configurations` ; + DROP TABLE IF EXISTS `alembic_version` ; + """) + + # Set correct version + version = stamp_schema_version(provider) + print("Stamped schema version: {} - '{}'".format(version.version, version.comment)) @db.command() diff --git a/python/requirements.txt b/python/requirements.txt index 8b137891..627c9b68 100644 --- a/python/requirements.txt +++ b/python/requirements.txt @@ -1 +1,7 @@ - +markupsafe>=2.1.3 +pymysql>=1.0.3 +ply>=3.11 +mako>=1.2.4 +# click 8.1 drops support of python 3.6 +click>=8.0.4 +sqlalchemy>=2 diff --git a/python/setup.py b/python/setup.py index 66dae010..4fbc67a3 100644 --- a/python/setup.py +++ b/python/setup.py @@ -37,8 +37,8 @@ ], packages=["rcdb"], include_package_data=True, - setup_requires=["click"], - install_requires=["click"], + setup_requires=["click", "ply", "pymysql"], + install_requires=["click", "ply", "pymysql"], entry_points={ "console_scripts": [ "rcdb=rcdb:run_rcdb_cli", diff --git a/python/tests/test_sql_schema_version.py b/python/tests/test_sql_schema_version.py index 6b1c89f8..a781b8e0 100644 --- a/python/tests/test_sql_schema_version.py +++ b/python/tests/test_sql_schema_version.py @@ -26,7 +26,7 @@ def test_right_schema_version(self): self.db.session.add(v) self.db.session.commit() - self.assertEqual(self.db.get_sql_schema_version(), rcdb.SQL_SCHEMA_VERSION) + self.assertEqual(self.db.get_schema_version(), rcdb.SQL_SCHEMA_VERSION) def test_no_schema_version(self): """Test of Run in db function""" @@ -44,7 +44,7 @@ def test_lower_schema_version(self): v.version = 0 self.db.session.add(v) self.db.session.commit() - self.assertFalse(self.db.get_sql_schema_version()) + self.assertFalse(self.db.get_schema_version()) diff --git a/rcdb_web/run_table.py b/rcdb_web/run_table.py index 051150d6..da9adf74 100644 --- a/rcdb_web/run_table.py +++ b/rcdb_web/run_table.py @@ -3,5 +3,3 @@ class RunTableData: def __init__(self): self.headers = [] self.rows = [] - - diff --git a/sql/update_db_v1_to_v2.sql b/sql/update_db_v1_to_v2.sql new file mode 100644 index 00000000..a6f3c0a7 --- /dev/null +++ b/sql/update_db_v1_to_v2.sql @@ -0,0 +1,59 @@ +-- MySQL Workbench Synchronization +-- Generated: 2023-09-14 11:46 +-- Model: New Model +-- Version: 1.0 +-- Project: Name of the project +-- Author: romanov + +SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0; +SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0; +SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION'; + +CREATE TABLE IF NOT EXISTS `rcdb`.`aliases` ( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `name` VARCHAR(255) NOT NULL, + `code` TEXT NOT NULL, + `description` VARCHAR(255) NULL DEFAULT NULL, + PRIMARY KEY (`id`)) +ENGINE = InnoDB; + +CREATE TABLE IF NOT EXISTS `rcdb`.`run_periods` ( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `name` VARCHAR(255) NOT NULL, + `description` VARCHAR(255) NULL DEFAULT NULL, + `start_date` DATE NULL DEFAULT NULL, + `end_date` DATE NULL DEFAULT NULL, + `run_min` INT(11) NOT NULL, + `run_max` INT(11) NOT NULL, + PRIMARY KEY (`id`)) +ENGINE = InnoDB; + +DROP TABLE IF EXISTS `rcdb`.`trigger_thresholds` ; + +DROP TABLE IF EXISTS `rcdb`.`trigger_masks` ; + +DROP TABLE IF EXISTS `rcdb`.`readout_thresholds` ; + +DROP TABLE IF EXISTS `rcdb`.`readout_masks` ; + +DROP TABLE IF EXISTS `rcdb`.`dac_presets` ; + +DROP TABLE IF EXISTS `rcdb`.`crates` ; + +DROP TABLE IF EXISTS `rcdb`.`boards` ; + +DROP TABLE IF EXISTS `rcdb`.`board_installations_have_runs` ; + +DROP TABLE IF EXISTS `rcdb`.`board_installations` ; + +DROP TABLE IF EXISTS `rcdb`.`board_configurations_have_runs` ; + +DROP TABLE IF EXISTS `rcdb`.`board_configurations` ; + +DROP TABLE IF EXISTS `rcdb`.`alembic_version` ; + + + +SET SQL_MODE=@OLD_SQL_MODE; +SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS; +SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS; From 37aaa2be70d41cb70862212ff5441f6d2cf57e4f Mon Sep 17 00:00:00 2001 From: "sqrd.max" Date: Wed, 4 Oct 2023 23:09:01 +0300 Subject: [PATCH 14/34] New blueprint for condition table --- rcdb_web/__init__.py | 2 + rcdb_web/runs/views.py | 38 +++++-------------- rcdb_web/templates/test_conditions/index.html | 10 +++++ rcdb_web/test_conditions/__init__.py | 0 rcdb_web/test_conditions/veiws.py | 17 +++++++++ 5 files changed, 39 insertions(+), 28 deletions(-) create mode 100644 rcdb_web/templates/test_conditions/index.html create mode 100644 rcdb_web/test_conditions/__init__.py create mode 100644 rcdb_web/test_conditions/veiws.py diff --git a/rcdb_web/__init__.py b/rcdb_web/__init__.py index 4e2958c6..20b7636a 100644 --- a/rcdb_web/__init__.py +++ b/rcdb_web/__init__.py @@ -13,6 +13,7 @@ from rcdb_web.files.views import mod as files_module from rcdb_web.statistics.views import mod as statistics_module from rcdb_web.conditions.views import mod as conditions_module +from rcdb_web.test_conditions.veiws import mod as test_conditions_module DEBUG = True SECRET_KEY = 'development key' @@ -88,6 +89,7 @@ def url_for_other_page(page): app.register_blueprint(files_module) app.register_blueprint(statistics_module) app.register_blueprint(conditions_module) +app.register_blueprint(test_conditions_module) if __name__ == '__main__': app.run() diff --git a/rcdb_web/runs/views.py b/rcdb_web/runs/views.py index babf7298..4615fc22 100644 --- a/rcdb_web/runs/views.py +++ b/rcdb_web/runs/views.py @@ -18,12 +18,13 @@ mod = Blueprint('runs', __name__, url_prefix='/runs') -_nsre=re.compile("([0-9]+)") +_nsre = re.compile("([0-9]+)") + def natural_sort_key(l): convert = lambda text: int(text) if text.isdigit() else text.lower() - alphanum_key = lambda key: [ convert(c) for c in re.split('([0-9]+)', key) ] - return sorted(l, key = alphanum_key) + alphanum_key = lambda key: [convert(c) for c in re.split('([0-9]+)', key)] + return sorted(l, key=alphanum_key) PER_PAGE = 200 @@ -33,7 +34,6 @@ def natural_sort_key(l): @mod.route('/page/', defaults={'run_from': -1, 'run_to': -1}) @mod.route('/-', defaults={'page': 1}) def index(page, run_from, run_to): - start_time_stamp = int(time() * 1000) preparation_sw = StopWatchTimer() @@ -50,7 +50,8 @@ def index(page, run_from, run_to): # Filter query and count results query = query.filter(Run.number >= run_min, Run.number <= run_max) - count = g.tdb.session.query(func.count(Run.number)).filter(Run.number >= run_min, Run.number <= run_max).scalar() + count = g.tdb.session.query(func.count(Run.number)).filter(Run.number >= run_min, + Run.number <= run_max).scalar() # we don't want pagination in this case, setting page size same/bigger than count per_page = run_max - run_min @@ -64,9 +65,9 @@ def index(page, run_from, run_to): preparation_sw.stop() query_sw = StopWatchTimer() # Get runs from query - runs = query.options(subqueryload(Run.conditions))\ - .order_by(Run.number.desc())\ - .slice(pagination.item_limit_from, pagination.item_limit_to)\ + runs = query.options(subqueryload(Run.conditions)) \ + .order_by(Run.number.desc()) \ + .slice(pagination.item_limit_from, pagination.item_limit_to) \ .all() query_sw.stop() performance = { @@ -150,7 +151,6 @@ def info(run_number): @mod.route('/elog/') def elog(run_number): - try: from urllib2 import urlopen, HTTPError except ImportError: @@ -158,7 +158,7 @@ def elog(run_number): from urllib.error import HTTPError try: elog_json = urlopen('https://logbooks.jlab.org/api/elog/entries?book=hdrun&title=Run_{}&limit=1' - .format(run_number)).read() + .format(run_number)).read() except HTTPError as e: return jsonify(stat=str(e.code)) @@ -218,7 +218,6 @@ def search(): if run_from_str or run_to_str: run_range = run_from_str + "-" + run_to_str - args = {} run_from, run_to = _parse_run_range(run_range) @@ -307,7 +306,6 @@ def search2(): else: run.is_active = False - return render_template("runs/custom_column.html", rows=result.get_values(columns, True), column_condition_types=column_condition_types, @@ -319,19 +317,3 @@ def search2(): performance=result.performance, columns=columns) - - - - - - - - - - - - - - - - diff --git a/rcdb_web/templates/test_conditions/index.html b/rcdb_web/templates/test_conditions/index.html new file mode 100644 index 00000000..711cc84d --- /dev/null +++ b/rcdb_web/templates/test_conditions/index.html @@ -0,0 +1,10 @@ + + + + + Title + + +hahah + + \ No newline at end of file diff --git a/rcdb_web/test_conditions/__init__.py b/rcdb_web/test_conditions/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/rcdb_web/test_conditions/veiws.py b/rcdb_web/test_conditions/veiws.py new file mode 100644 index 00000000..93db6343 --- /dev/null +++ b/rcdb_web/test_conditions/veiws.py @@ -0,0 +1,17 @@ +import json +import re +from flask import Blueprint, request, render_template, flash, g, session, redirect, url_for +# from werkzeug import check_password_hash, generate_password_hash +import rcdb +from collections import defaultdict +from rcdb.model import Run, Condition, ConditionType +from sqlalchemy.orm import subqueryload + +mod = Blueprint('test_conditions', __name__, url_prefix='/test_conditions') + + +@mod.route('/') +def index(): + conditions = g.tdb.session.query(ConditionType).order_by(ConditionType.name.asc()).all() + return render_template("test_conditions/index.html", conditions=conditions) + pass From 3c54b86016296910fe87a6105c7573ec7f924780 Mon Sep 17 00:00:00 2001 From: "sqrd.max" Date: Fri, 6 Oct 2023 03:51:52 +0300 Subject: [PATCH 15/34] runs doesn't work --- rcdb_web/templates/test_conditions/index.html | 127 ++++++++++++++++-- rcdb_web/test_conditions/veiws.py | 9 +- 2 files changed, 125 insertions(+), 11 deletions(-) diff --git a/rcdb_web/templates/test_conditions/index.html b/rcdb_web/templates/test_conditions/index.html index 711cc84d..a0157f74 100644 --- a/rcdb_web/templates/test_conditions/index.html +++ b/rcdb_web/templates/test_conditions/index.html @@ -1,10 +1,117 @@ - - - - - Title - - -hahah - - \ No newline at end of file +{% extends 'layouts/base.html' %} + +{% set page_title = 'Test Conditions' %} + +{% block container %} + +
+ +

Test Conditions

+ +{#
#} +{# #} +{#
#} +{# #} +{#
#} +{##} +{# #} +{#
#} +{# #} +{# #} +{# #} +{# #} +{# #} +{# #} +{# #} +{# #} +{# #} +{# #} +{#
Column 1Column 2
#} +{#
#} +{#
#} + +
+
+
+ +
+
+
+
+ +
+
+
+ + +
+
+ + + + + + + + {% for row in table %} + + + + {% endfor %} + +
Runs
{{ row.run_number }}
+
+ +
+ + + + + + + + + + +
Conditions
+
+
+ +
+ +{% endblock %} + +{% block js_btm %} + {{ super() }} + + + + + +{% endblock %} \ No newline at end of file diff --git a/rcdb_web/test_conditions/veiws.py b/rcdb_web/test_conditions/veiws.py index 93db6343..c86cb39f 100644 --- a/rcdb_web/test_conditions/veiws.py +++ b/rcdb_web/test_conditions/veiws.py @@ -7,11 +7,18 @@ from rcdb.model import Run, Condition, ConditionType from sqlalchemy.orm import subqueryload +from rcdb.provider import RCDBProvider + mod = Blueprint('test_conditions', __name__, url_prefix='/test_conditions') @mod.route('/') def index(): conditions = g.tdb.session.query(ConditionType).order_by(ConditionType.name.asc()).all() - return render_template("test_conditions/index.html", conditions=conditions) + + db = RCDBProvider("mysql://localhost/rcdb") + + table = db.select_values(['polarization_angle', 'polarization_direction'], run_min=30000, run_max=30050) + + return render_template("test_conditions/index.html", conditions=conditions, table=table) pass From 7b139fbeb77e686246c18ed19dcf64b149f434e2 Mon Sep 17 00:00:00 2001 From: "sqrd.max" Date: Thu, 26 Oct 2023 22:12:28 +0300 Subject: [PATCH 16/34] custom table works --- python/rcdb/provider.py | 5 +- rcdb_web/__init__.py | 4 +- rcdb_web/runs/views.py | 4 +- .../__init__.py | 0 rcdb_web/select_values/veiws.py | 69 +++++++++++ rcdb_web/templates/layouts/base.html | 3 + rcdb_web/templates/run_search_box.html | 8 +- rcdb_web/templates/runs/index.html | 3 +- rcdb_web/templates/select_values/index.html | 66 ++++++++++ rcdb_web/templates/test_conditions/index.html | 117 ------------------ rcdb_web/test_conditions/veiws.py | 24 ---- 11 files changed, 154 insertions(+), 149 deletions(-) rename rcdb_web/{test_conditions => select_values}/__init__.py (100%) create mode 100644 rcdb_web/select_values/veiws.py create mode 100644 rcdb_web/templates/select_values/index.html delete mode 100644 rcdb_web/templates/test_conditions/index.html delete mode 100644 rcdb_web/test_conditions/veiws.py diff --git a/python/rcdb/provider.py b/python/rcdb/provider.py index fa8878e6..a700bcb6 100644 --- a/python/rcdb/provider.py +++ b/python/rcdb/provider.py @@ -937,7 +937,10 @@ def select_values(self, val_names=None, search_str="", run_min=0, run_max=sys.ma if runs: result = self.session.connection().execute(sql) # runs are already in query else: - result = self.session.connection().execute(sql, run_max=run_max, run_min=run_min) + + #sql.bindparams(run_max=run_max, run_min=run_min) + #result = self.session.connection().execute(sql) + result = self.session.connection().execute(sql, parameters={"run_min": run_min, "run_max":run_max}) query_sw.stop() diff --git a/rcdb_web/__init__.py b/rcdb_web/__init__.py index 20b7636a..e4d64a99 100644 --- a/rcdb_web/__init__.py +++ b/rcdb_web/__init__.py @@ -13,7 +13,7 @@ from rcdb_web.files.views import mod as files_module from rcdb_web.statistics.views import mod as statistics_module from rcdb_web.conditions.views import mod as conditions_module -from rcdb_web.test_conditions.veiws import mod as test_conditions_module +from rcdb_web.select_values.veiws import mod as select_values_module DEBUG = True SECRET_KEY = 'development key' @@ -89,7 +89,7 @@ def url_for_other_page(page): app.register_blueprint(files_module) app.register_blueprint(statistics_module) app.register_blueprint(conditions_module) -app.register_blueprint(test_conditions_module) +app.register_blueprint(select_values_module) if __name__ == '__main__': app.run() diff --git a/rcdb_web/runs/views.py b/rcdb_web/runs/views.py index 4615fc22..09d01034 100644 --- a/rcdb_web/runs/views.py +++ b/rcdb_web/runs/views.py @@ -233,7 +233,7 @@ def search(): run_from = 0 if run_to is None: - run_to = sys.maxint + run_to = sys.maxsize try: result = g.tdb.select_runs(search_query, run_to, run_from, sort_desc=True) @@ -250,7 +250,7 @@ def search(): pagination=pagination, condition_types=condition_types, run_from=run_from, - run_to=run_to if run_to != sys.maxint else -1, + run_to=run_to if run_to != sys.maxsize else -1, search_query=search_query, performance=result.performance) diff --git a/rcdb_web/test_conditions/__init__.py b/rcdb_web/select_values/__init__.py similarity index 100% rename from rcdb_web/test_conditions/__init__.py rename to rcdb_web/select_values/__init__.py diff --git a/rcdb_web/select_values/veiws.py b/rcdb_web/select_values/veiws.py new file mode 100644 index 00000000..88796b3f --- /dev/null +++ b/rcdb_web/select_values/veiws.py @@ -0,0 +1,69 @@ +import json +import re +import sys + +from flask import Blueprint, request, render_template, flash, g, session, redirect, url_for +# from werkzeug import check_password_hash, generate_password_hash +import rcdb +from collections import defaultdict +from rcdb.model import Run, Condition, ConditionType +from sqlalchemy.orm import subqueryload + +from rcdb.provider import RCDBProvider +from runs.views import _parse_run_range + +mod = Blueprint('select_values', __name__, url_prefix='/select_values') + + +@mod.route('/') +def index(): + all_conditions = g.tdb.session.query(ConditionType).order_by(ConditionType.name.asc()).all() + # req_conditions_str = request.args.get('cnd', '') + # run_range = request.args.get('rr', '') + # req_conditions_str = ['beam_current', 'event_count', 'daq_run', 'run_config'] # Conditions that user requested + + + + # db = RCDBProvider("mysql://localhost/rcdb") + # (!) don't forget that if conditions are not found this will fail + # table = g.tdb.select_values(req_conditions_value, search_str="", run_min=30000, run_max=30050) + + return render_template("select_values/index.html", + all_conditions=all_conditions) + pass + + +@mod.route('/search', methods=['GET']) +def search(): + run_range = request.args.get('rr', '') + search_query = request.args.get('q', '') + req_conditions_str = request.args.get('cnd', '') + req_conditions_value = req_conditions_str.split(',') + + run_from_str = request.args.get('runFrom', '') + run_to_str = request.args.get('runTo', '') + + if run_from_str or run_to_str: + run_range = run_from_str + "-" + run_to_str + + args = {} + run_from, run_to = _parse_run_range(run_range) + + try: + table = g.tdb.select_values(val_names=req_conditions_value, search_str=search_query, run_min=run_from, run_max=run_to, sort_desc=True) + print(req_conditions_value, run_from, run_to) + except Exception as err: + flash("Error in performing request: {}".format(err), 'danger') + return redirect(url_for('select_values.index')) + + condition_types = g.tdb.get_condition_types() + print(run_to, run_from) + + return render_template("select_values/index.html", + run_range=run_range, + run_from=run_from, + run_to=run_to, + search_query=search_query, + req_conditions_value=req_conditions_value, + table=table) + diff --git a/rcdb_web/templates/layouts/base.html b/rcdb_web/templates/layouts/base.html index 5296a808..96d45ee2 100644 --- a/rcdb_web/templates/layouts/base.html +++ b/rcdb_web/templates/layouts/base.html @@ -63,6 +63,9 @@
  • Conditions
  • +
  • + Select values +
  • diff --git a/rcdb_web/templates/run_search_box.html b/rcdb_web/templates/run_search_box.html index 88c260fa..a4fe066c 100644 --- a/rcdb_web/templates/run_search_box.html +++ b/rcdb_web/templates/run_search_box.html @@ -1,11 +1,11 @@ -{% macro run_search_box(condition_types=[], run_from=-1, run_to=-1, search_query="") %} +{% macro run_search_box(condition_types=[], run_from=-1, run_to=-1, search_query="", form_url=url_for("runs.search")) %} {% set run_from_str = "" if run_from == -1 else run_from %} {% set run_to_str = "" if run_to == -1 else run_to %}
    -
    +
    @@ -77,6 +77,10 @@
    +
    + + +
    diff --git a/rcdb_web/templates/runs/index.html b/rcdb_web/templates/runs/index.html index d01d3e17..f6a08eee 100644 --- a/rcdb_web/templates/runs/index.html +++ b/rcdb_web/templates/runs/index.html @@ -1,8 +1,9 @@ +{% extends 'layouts/base.html' %} + {% import 'default_run_table.html' as table%} {% from 'render_pagination.html' import render_pagination %} {% import 'run_search_box.html' as search_box%} -{% extends 'layouts/base.html' %} {% set page_title = 'Runs' %} diff --git a/rcdb_web/templates/select_values/index.html b/rcdb_web/templates/select_values/index.html new file mode 100644 index 00000000..f953b3e4 --- /dev/null +++ b/rcdb_web/templates/select_values/index.html @@ -0,0 +1,66 @@ +{% extends 'layouts/base.html' %} +{% import 'run_search_box.html' as search_box%} + +{% set page_title = 'Test Conditions' %} + +{% block container %} + +{{ search_box.run_search_box(condition_types=all_conditions, run_from=run_from, run_to=run_to, search_query=search_query, form_url=url_for("select_values.search")) }} + +
    + +

    Custom table

    + + +
    +
    + + + + + {% for cnd_name in req_conditions_value %} + + {% endfor %} + + + + {% for row in table %} + + {% for col in row %} + + {% endfor %} + + {% endfor %} + +
    Run{{ cnd_name }}
    {{ col }}
    +
    +
    + +
    + +{% endblock %} + +{% block js_btm %} + {{ super() }} + + + + {{ search_box.run_search_box_scripts(condition_types) }} +{% endblock %} \ No newline at end of file diff --git a/rcdb_web/templates/test_conditions/index.html b/rcdb_web/templates/test_conditions/index.html deleted file mode 100644 index a0157f74..00000000 --- a/rcdb_web/templates/test_conditions/index.html +++ /dev/null @@ -1,117 +0,0 @@ -{% extends 'layouts/base.html' %} - -{% set page_title = 'Test Conditions' %} - -{% block container %} - -
    - -

    Test Conditions

    - -{#
    #} -{# #} -{#
    #} -{# #} -{#
    #} -{##} -{# #} -{#
    #} -{# #} -{# #} -{# #} -{# #} -{# #} -{# #} -{# #} -{# #} -{# #} -{# #} -{#
    Column 1Column 2
    #} -{#
    #} -{#
    #} - -
    -
    -
    - -
    -
    -
    -
    - -
    -
    -
    - - -
    -
    - - - - - - - - {% for row in table %} - - - - {% endfor %} - -
    Runs
    {{ row.run_number }}
    -
    - -
    - - - - - - - - - - -
    Conditions
    -
    -
    - -
    - -{% endblock %} - -{% block js_btm %} - {{ super() }} - - - - - -{% endblock %} \ No newline at end of file diff --git a/rcdb_web/test_conditions/veiws.py b/rcdb_web/test_conditions/veiws.py deleted file mode 100644 index c86cb39f..00000000 --- a/rcdb_web/test_conditions/veiws.py +++ /dev/null @@ -1,24 +0,0 @@ -import json -import re -from flask import Blueprint, request, render_template, flash, g, session, redirect, url_for -# from werkzeug import check_password_hash, generate_password_hash -import rcdb -from collections import defaultdict -from rcdb.model import Run, Condition, ConditionType -from sqlalchemy.orm import subqueryload - -from rcdb.provider import RCDBProvider - -mod = Blueprint('test_conditions', __name__, url_prefix='/test_conditions') - - -@mod.route('/') -def index(): - conditions = g.tdb.session.query(ConditionType).order_by(ConditionType.name.asc()).all() - - db = RCDBProvider("mysql://localhost/rcdb") - - table = db.select_values(['polarization_angle', 'polarization_direction'], run_min=30000, run_max=30050) - - return render_template("test_conditions/index.html", conditions=conditions, table=table) - pass From 56744beedfd7b53fd3a4c46cb461113bf623830e Mon Sep 17 00:00:00 2001 From: "sqrd.max" Date: Wed, 1 Nov 2023 23:16:55 +0200 Subject: [PATCH 17/34] need to fix query --- rcdb_web/select_values/veiws.py | 15 ++-- rcdb_web/templates/run_search_box.html | 31 ++++++++- .../templates/select_values/custom_table.html | 68 +++++++++++++++++++ rcdb_web/templates/select_values/index.html | 53 ++++++++++----- 4 files changed, 137 insertions(+), 30 deletions(-) create mode 100644 rcdb_web/templates/select_values/custom_table.html diff --git a/rcdb_web/select_values/veiws.py b/rcdb_web/select_values/veiws.py index 88796b3f..8375bee9 100644 --- a/rcdb_web/select_values/veiws.py +++ b/rcdb_web/select_values/veiws.py @@ -18,15 +18,6 @@ @mod.route('/') def index(): all_conditions = g.tdb.session.query(ConditionType).order_by(ConditionType.name.asc()).all() - # req_conditions_str = request.args.get('cnd', '') - # run_range = request.args.get('rr', '') - # req_conditions_str = ['beam_current', 'event_count', 'daq_run', 'run_config'] # Conditions that user requested - - - - # db = RCDBProvider("mysql://localhost/rcdb") - # (!) don't forget that if conditions are not found this will fail - # table = g.tdb.select_values(req_conditions_value, search_str="", run_min=30000, run_max=30050) return render_template("select_values/index.html", all_conditions=all_conditions) @@ -35,6 +26,7 @@ def index(): @mod.route('/search', methods=['GET']) def search(): + all_conditions = g.tdb.session.query(ConditionType).order_by(ConditionType.name.asc()).all() run_range = request.args.get('rr', '') search_query = request.args.get('q', '') req_conditions_str = request.args.get('cnd', '') @@ -50,7 +42,8 @@ def search(): run_from, run_to = _parse_run_range(run_range) try: - table = g.tdb.select_values(val_names=req_conditions_value, search_str=search_query, run_min=run_from, run_max=run_to, sort_desc=True) + table = g.tdb.select_values(val_names=req_conditions_value, search_str=search_query, run_min=run_from, + run_max=run_to, sort_desc=True) print(req_conditions_value, run_from, run_to) except Exception as err: flash("Error in performing request: {}".format(err), 'danger') @@ -60,10 +53,10 @@ def search(): print(run_to, run_from) return render_template("select_values/index.html", + all_conditions=all_conditions, run_range=run_range, run_from=run_from, run_to=run_to, search_query=search_query, req_conditions_value=req_conditions_value, table=table) - diff --git a/rcdb_web/templates/run_search_box.html b/rcdb_web/templates/run_search_box.html index a4fe066c..1107f294 100644 --- a/rcdb_web/templates/run_search_box.html +++ b/rcdb_web/templates/run_search_box.html @@ -1,6 +1,12 @@ {% macro run_search_box(condition_types=[], run_from=-1, run_to=-1, search_query="", form_url=url_for("runs.search")) %} {% set run_from_str = "" if run_from == -1 else run_from %} {% set run_to_str = "" if run_to == -1 else run_to %} +
    @@ -78,9 +84,16 @@
    - - -
    + +
    + +
    + +
    +
    +
    @@ -195,5 +208,17 @@ } }); }); + + const toggleButton = document.getElementById('toggleButton'); + const hiddenTable = document.querySelector('.hidden-table'); + const toggleIcon = document.getElementById('toggleIcon'); + + toggleButton.addEventListener('click', function(event) { + event.preventDefault(); + hiddenTable.classList.toggle('hidden'); + toggleIcon.style.transform = hiddenTable.classList.contains('hidden') ? 'rotate(0deg)' : 'rotate(180deg)'; + }); + + {% endmacro %} \ No newline at end of file diff --git a/rcdb_web/templates/select_values/custom_table.html b/rcdb_web/templates/select_values/custom_table.html new file mode 100644 index 00000000..28a4e28d --- /dev/null +++ b/rcdb_web/templates/select_values/custom_table.html @@ -0,0 +1,68 @@ +{% extends 'layouts/base.html' %} +{% import 'run_search_box.html' as search_box%} + +{% set page_title = 'Test Conditions' %} + +{% block container %} + +{{ search_box.run_search_box(condition_types=all_conditions, run_from=run_from, run_to=run_to, search_query=search_query, form_url=url_for("select_values.search")) }} + +
    + +

    Custom table

    + + +
    +
    + + + + + {% for cnd_name in req_conditions_value %} + + {% endfor %} + + + + {% for row in table %} + + {% for col in row %} + + {% endfor %} + + {% endfor %} + +
    Run{{ cnd_name }}
    {{ col }}
    + + +
    +
    + +
    + +{% endblock %} + +{% block js_btm %} + {{ super() }} + + + + {{ search_box.run_search_box_scripts(condition_types) }} +{% endblock %} \ No newline at end of file diff --git a/rcdb_web/templates/select_values/index.html b/rcdb_web/templates/select_values/index.html index f953b3e4..e4b63649 100644 --- a/rcdb_web/templates/select_values/index.html +++ b/rcdb_web/templates/select_values/index.html @@ -1,7 +1,7 @@ {% extends 'layouts/base.html' %} {% import 'run_search_box.html' as search_box%} -{% set page_title = 'Test Conditions' %} +{% set page_title = 'Select values' %} {% block container %} @@ -10,8 +10,29 @@

    Custom table

    +
    +
    + + + + + + + - + {% for condition in all_conditions %} + + + + + + + {% endfor %} +
    TypeNameDescription
    {{ condition.value_type }}{{ condition.name }}{{ condition.description }}
    + + +
    +
    @@ -35,7 +56,6 @@
    -
    {% endblock %} @@ -45,22 +65,23 @@ {{ search_box.run_search_box_scripts(condition_types) }} {% endblock %} \ No newline at end of file From 4f1a012c8788456275da188b331ae40fb13bc9b7 Mon Sep 17 00:00:00 2001 From: "sqrd.max" Date: Sat, 18 Nov 2023 00:53:07 +0200 Subject: [PATCH 18/34] local storage works --- rcdb_web/requirements.txt | 3 +- rcdb_web/select_values/veiws.py | 26 +++--- rcdb_web/templates/run_search_box.html | 80 ++++++++++++++----- .../templates/select_values/custom_table.html | 68 ---------------- rcdb_web/templates/select_values/index.html | 45 +++++++++-- 5 files changed, 116 insertions(+), 106 deletions(-) delete mode 100644 rcdb_web/templates/select_values/custom_table.html diff --git a/rcdb_web/requirements.txt b/rcdb_web/requirements.txt index 7c12b958..46be1c38 100644 --- a/rcdb_web/requirements.txt +++ b/rcdb_web/requirements.txt @@ -1,2 +1,3 @@ flask -urllib2 \ No newline at end of file +#urllib2 +jinja2 \ No newline at end of file diff --git a/rcdb_web/select_values/veiws.py b/rcdb_web/select_values/veiws.py index 8375bee9..4aa812fd 100644 --- a/rcdb_web/select_values/veiws.py +++ b/rcdb_web/select_values/veiws.py @@ -18,10 +18,19 @@ @mod.route('/') def index(): all_conditions = g.tdb.session.query(ConditionType).order_by(ConditionType.name.asc()).all() + run_from_str = request.args.get('runFrom', '') + run_to_str = request.args.get('runTo', '') + search_query = request.args.get('q', '') + req_conditions_str = request.args.get('cnd', '') + + print("search" + search_query) return render_template("select_values/index.html", - all_conditions=all_conditions) - pass + all_conditions=all_conditions, + run_from_str=run_from_str, + run_to_str=run_to_str, + search_query=search_query, + req_conditions_str=req_conditions_str) @mod.route('/search', methods=['GET']) @@ -30,7 +39,7 @@ def search(): run_range = request.args.get('rr', '') search_query = request.args.get('q', '') req_conditions_str = request.args.get('cnd', '') - req_conditions_value = req_conditions_str.split(',') + req_conditions_values = req_conditions_str.split(',') run_from_str = request.args.get('runFrom', '') run_to_str = request.args.get('runTo', '') @@ -38,19 +47,17 @@ def search(): if run_from_str or run_to_str: run_range = run_from_str + "-" + run_to_str - args = {} run_from, run_to = _parse_run_range(run_range) try: - table = g.tdb.select_values(val_names=req_conditions_value, search_str=search_query, run_min=run_from, + table = g.tdb.select_values(val_names=req_conditions_values, search_str=search_query, run_min=run_from, run_max=run_to, sort_desc=True) - print(req_conditions_value, run_from, run_to) + print(req_conditions_values, run_from, run_to) except Exception as err: flash("Error in performing request: {}".format(err), 'danger') return redirect(url_for('select_values.index')) - condition_types = g.tdb.get_condition_types() - print(run_to, run_from) + print(search_query) return render_template("select_values/index.html", all_conditions=all_conditions, @@ -58,5 +65,6 @@ def search(): run_from=run_from, run_to=run_to, search_query=search_query, - req_conditions_value=req_conditions_value, + req_conditions_str=req_conditions_str, + req_conditions_values=req_conditions_values, table=table) diff --git a/rcdb_web/templates/run_search_box.html b/rcdb_web/templates/run_search_box.html index 1107f294..5bb6c906 100644 --- a/rcdb_web/templates/run_search_box.html +++ b/rcdb_web/templates/run_search_box.html @@ -1,17 +1,11 @@ -{% macro run_search_box(condition_types=[], run_from=-1, run_to=-1, search_query="", form_url=url_for("runs.search")) %} +{% macro run_search_box(condition_types=[], run_from=-1, run_to=-1, search_query="", req_conditions_str="", form_url=url_for("runs.search")) %} {% set run_from_str = "" if run_from == -1 else run_from %} {% set run_to_str = "" if run_to == -1 else run_to %} -
    -
    +
    @@ -61,8 +55,8 @@
    -
    - +
    +
    @@ -83,17 +77,18 @@
    -
    - -
    - -
    - -
    -
    -
    +
    + +
    + +
    + +
    +
    +
    +
    @@ -133,14 +128,35 @@
    {% endmacro %} -{% macro run_search_box_scripts(condition_types=[], run_from=-1, run_to=-1, search_query="") %} +{% macro run_search_box_scripts(condition_types=[], run_from=-1, run_to=-1, search_query="", req_conditions="") %} {% endmacro %} \ No newline at end of file diff --git a/rcdb_web/templates/select_values/custom_table.html b/rcdb_web/templates/select_values/custom_table.html deleted file mode 100644 index 28a4e28d..00000000 --- a/rcdb_web/templates/select_values/custom_table.html +++ /dev/null @@ -1,68 +0,0 @@ -{% extends 'layouts/base.html' %} -{% import 'run_search_box.html' as search_box%} - -{% set page_title = 'Test Conditions' %} - -{% block container %} - -{{ search_box.run_search_box(condition_types=all_conditions, run_from=run_from, run_to=run_to, search_query=search_query, form_url=url_for("select_values.search")) }} - -
    - -

    Custom table

    - - -
    -
    - - - - - {% for cnd_name in req_conditions_value %} - - {% endfor %} - - - - {% for row in table %} - - {% for col in row %} - - {% endfor %} - - {% endfor %} - -
    Run{{ cnd_name }}
    {{ col }}
    - - -
    -
    - -
    - -{% endblock %} - -{% block js_btm %} - {{ super() }} - - - - {{ search_box.run_search_box_scripts(condition_types) }} -{% endblock %} \ No newline at end of file diff --git a/rcdb_web/templates/select_values/index.html b/rcdb_web/templates/select_values/index.html index e4b63649..53b1e860 100644 --- a/rcdb_web/templates/select_values/index.html +++ b/rcdb_web/templates/select_values/index.html @@ -5,7 +5,7 @@ {% block container %} -{{ search_box.run_search_box(condition_types=all_conditions, run_from=run_from, run_to=run_to, search_query=search_query, form_url=url_for("select_values.search")) }} +{{ search_box.run_search_box(condition_types=all_conditions, run_from=run_from, run_to=run_to, search_query=search_query, req_conditions_str=req_conditions_str, form_url=url_for("select_values.search")) }}
    @@ -22,15 +22,13 @@ {% for condition in all_conditions %} - + {{ condition.value_type }} {{ condition.name }} {{ condition.description }} {% endfor %} - -
    @@ -39,7 +37,7 @@ Run - {% for cnd_name in req_conditions_value %} + {% for cnd_name in req_conditions_values %} {{ cnd_name }} {% endfor %} @@ -77,9 +75,44 @@ searchInput.value += `,${conditionName}`; } } else { - searchInput.value = searchInput.value.replace(`${conditionName},`, '').replace(conditionName, ''); + searchInput.value = searchInput.value.replace(`${conditionName},`, '').replace(`,${conditionName}`, '').replace(conditionName, ''); } } + + function updateLocalStorage() { + var checkboxes = document.querySelectorAll('input[type="checkbox"]'); + + checkboxes.forEach(function(checkbox) { + localStorage.setItem(checkbox.value, checkbox.checked); + }); + } + + function loadCheckboxState() { + var checkboxState = {}; + + for (var i = 0; i < localStorage.length; i++) { + var key = localStorage.key(i); + checkboxState[key] = localStorage.getItem(key) === 'true'; + } + + var checkboxes = document.querySelectorAll('input[type="checkbox"]'); + checkboxes.forEach(function(checkbox) { + checkbox.checked = checkboxState[checkbox.value] || false; + }); + } + + document.addEventListener('DOMContentLoaded', function() { + loadCheckboxState(); + }); + + + var checkboxes = document.querySelectorAll('input[type="checkbox"]'); + checkboxes.forEach(function(checkbox) { + checkbox.addEventListener('change', function() { + updateLocalStorage(); + }); + }); + From fcc3e4dec8eeefcd32cf2e3246ab5da611b9e724 Mon Sep 17 00:00:00 2001 From: Dmitry Romanov Date: Fri, 17 Nov 2023 18:36:52 -0500 Subject: [PATCH 19/34] Better error handling in select_values --- python/rcdb/provider.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/python/rcdb/provider.py b/python/rcdb/provider.py index a700bcb6..b9b358fd 100644 --- a/python/rcdb/provider.py +++ b/python/rcdb/provider.py @@ -788,8 +788,16 @@ def select_runs(self, search_str="", run_min=0, run_max=sys.maxsize, sort_desc=F if isinstance(value, Condition): value = (value,) run = value[0].run - if eval(compiled_search_eval): - sel_runs.append(run) + try: + if eval(compiled_search_eval): + sel_runs.append(run) + except Exception as ex: + message = 'Error evaluating search query.\n' \ + + ' Query: <<"{}">>, \n'.format(search_eval) \ + + ' Names: {}, \n'.format(names) \ + + ' Values: {} \n'.format(values) \ + + ' Error ({}): {}'.format(type(ex), ex) + raise QueryEvaluationError(msg=message) selection_sw.stop() result = RunSelectionResult(sel_runs, self) @@ -971,7 +979,7 @@ def select_values(self, val_names=None, search_str="", run_min=0, run_max=sys.ma + ' Query: <<"{}">>, \n'.format(search_eval) \ + ' Names: {}, \n'.format(names) \ + ' Values: {} \n'.format(values) \ - + ' Error: {}'.format(ex) + + ' Error ({}): {}'.format(type(ex), ex) raise QueryEvaluationError(msg=message) selection_sw.stop() From 2f65ce24e67c86e1a0f0f76119e0bb863b6780d1 Mon Sep 17 00:00:00 2001 From: "sqrd.max" Date: Sat, 18 Nov 2023 01:53:37 +0200 Subject: [PATCH 20/34] query works --- python/rcdb/provider.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/python/rcdb/provider.py b/python/rcdb/provider.py index b9b358fd..2683671f 100644 --- a/python/rcdb/provider.py +++ b/python/rcdb/provider.py @@ -975,6 +975,11 @@ def select_values(self, val_names=None, search_str="", run_min=0, run_max=sys.ma result_row.append(val) result_table.append(result_row) except Exception as ex: + # Condition value might be NoneType if it's not added to a run + # TypeError arises when comparing none with value and it's OK + if isinstance(ex, TypeError) and "NoneType" in str(ex): + continue + message = 'Error evaluating search query.\n' \ + ' Query: <<"{}">>, \n'.format(search_eval) \ + ' Names: {}, \n'.format(names) \ From b2d7147216f92722c4c8dfaef025b9af10f0bdff Mon Sep 17 00:00:00 2001 From: Dmitry Romanov Date: Fri, 17 Nov 2023 18:57:22 -0500 Subject: [PATCH 21/34] Disable req_conditions input from Runs box --- rcdb_web/templates/run_search_box.html | 5 +++-- rcdb_web/templates/select_values/index.html | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/rcdb_web/templates/run_search_box.html b/rcdb_web/templates/run_search_box.html index 5bb6c906..2528d264 100644 --- a/rcdb_web/templates/run_search_box.html +++ b/rcdb_web/templates/run_search_box.html @@ -1,4 +1,4 @@ -{% macro run_search_box(condition_types=[], run_from=-1, run_to=-1, search_query="", req_conditions_str="", form_url=url_for("runs.search")) %} +{% macro run_search_box(condition_types=[], run_from=-1, run_to=-1, search_query="", form_url=url_for("runs.search"), req_conditions_str="", show_req_conditions=False) %} {% set run_from_str = "" if run_from == -1 else run_from %} {% set run_to_str = "" if run_to == -1 else run_to %} @@ -77,6 +77,7 @@
    + {% if show_req_conditions %}
    @@ -88,7 +89,7 @@
    - + {% endif %} diff --git a/rcdb_web/templates/select_values/index.html b/rcdb_web/templates/select_values/index.html index 53b1e860..111378f8 100644 --- a/rcdb_web/templates/select_values/index.html +++ b/rcdb_web/templates/select_values/index.html @@ -5,7 +5,7 @@ {% block container %} -{{ search_box.run_search_box(condition_types=all_conditions, run_from=run_from, run_to=run_to, search_query=search_query, req_conditions_str=req_conditions_str, form_url=url_for("select_values.search")) }} +{{ search_box.run_search_box(condition_types=all_conditions, run_from=run_from, run_to=run_to, search_query=search_query, req_conditions_str=req_conditions_str, form_url=url_for("select_values.search"), show_req_conditions=True) }}
    From be58e33fc5eeadba5c0dd57563340e4c372043e6 Mon Sep 17 00:00:00 2001 From: "sqrd.max" Date: Wed, 22 Nov 2023 21:31:44 +0200 Subject: [PATCH 22/34] run periods from db --- python/rcdb/model.py | 20 ++++++++++---------- python/rcdb/provider.py | 27 +++++++++++++-------------- python/tests/test_get_run_periods.py | 19 +++++++++++++++++++ 3 files changed, 42 insertions(+), 24 deletions(-) create mode 100644 python/tests/test_get_run_periods.py diff --git a/python/rcdb/model.py b/python/rcdb/model.py index 9da7839e..dabc1db4 100644 --- a/python/rcdb/model.py +++ b/python/rcdb/model.py @@ -441,16 +441,16 @@ def __repr__(self): return "".format(self.id, self.name) -run_periods = { - "2017-01": (30000, 39999, "23 Jan 2017 - 13 Mar 2017 12 GeV e-"), - "2016-10": (20000, 29999, "15 Sep 2016 - 21 Dec 2016 12 GeV e-"), - "2016-02": (10000, 19999, "28 Jan 2016 - 24 Apr 2016 Commissioning, 12 GeV e-"), - "2015-12": (3939, 4807, "01 Dec 2015 - 28 Jan 2016 Commissioning, 12 GeV e-, Cosmics"), - "2015-06": (3386, 3938, "29 May 2015 - 01 Dec 2015 Cosmics"), - "2015-03": (2607, 3385, "11 Mar 2015 - 29 May 2015 Commissioning, 5.5 GeV e-"), - "2015-01": (2440, 2606, "06 Feb 2015 - 11 Mar 2015 Cosmics"), - "2014-10": (630, 2439, "28 Oct 2014 - 21 Dec 2014 Commissioning, 10 GeV e-"), -} +# run_periods = { +# "2017-01": (30000, 39999, "23 Jan 2017 - 13 Mar 2017 12 GeV e-"), +# "2016-10": (20000, 29999, "15 Sep 2016 - 21 Dec 2016 12 GeV e-"), +# "2016-02": (10000, 19999, "28 Jan 2016 - 24 Apr 2016 Commissioning, 12 GeV e-"), +# "2015-12": (3939, 4807, "01 Dec 2015 - 28 Jan 2016 Commissioning, 12 GeV e-, Cosmics"), +# "2015-06": (3386, 3938, "29 May 2015 - 01 Dec 2015 Cosmics"), +# "2015-03": (2607, 3385, "11 Mar 2015 - 29 May 2015 Commissioning, 5.5 GeV e-"), +# "2015-01": (2440, 2606, "06 Feb 2015 - 11 Mar 2015 Cosmics"), +# "2014-10": (630, 2439, "28 Oct 2014 - 21 Dec 2014 Commissioning, 10 GeV e-"), +# } # ------------------------------------------------- diff --git a/python/rcdb/provider.py b/python/rcdb/provider.py index 2683671f..b55443ea 100644 --- a/python/rcdb/provider.py +++ b/python/rcdb/provider.py @@ -52,6 +52,7 @@ def __init__(self, connection_string=None, user_name="", check_version=True): self._cnd_types_cache = None self._cnd_types_by_name = None self.aliases = default_aliases + self._run_periods_cache = None # username for record self.user_name = user_name @@ -298,24 +299,22 @@ def create_run(self, run_number): return run # ------------------------------------------------ - # Get run periods + # Returns run periods # ------------------------------------------------ - def get_run_periods(self): - """Returns dict with run-periods - :return: dict with {"name":(run_min, run_max, description)} - """ - return rcdb.model.run_periods + def get_run_periods(self): + """Gets all run periods as a list of RunPeriod objects - # ------------------------------------------------ - # Get run periods - # ------------------------------------------------ - def get_run_period(self, name): - """Returns dict with run-periods - :param name: Run period name in form of YYYY-MM, like 2016-02 - :return: dict with {"name":(run_min, run_max, description)} + :return: all RunPeriods in db + :rtype: list, [RunPeriod] """ - return rcdb.model.run_periods[str(name)] + if self._run_periods_cache is not None: + return self._run_periods_cache + try: + self._run_periods_cache = self.session.query(RunPeriod).all() + return self._run_periods_cache + except NoResultFound: + return [] # ------------------------------------------------ # Returns condition type diff --git a/python/tests/test_get_run_periods.py b/python/tests/test_get_run_periods.py new file mode 100644 index 00000000..139d20ea --- /dev/null +++ b/python/tests/test_get_run_periods.py @@ -0,0 +1,19 @@ +import unittest + +import rcdb +import rcdb.model +from rcdb.model import RunPeriod + + +class TestRunPeriod(unittest.TestCase): + def setUp(self): + self.db = rcdb.RCDBProvider("sqlite://", check_version=False) + rcdb.provider.destroy_all_create_schema(self.db) + + + def tearDown(self): + self.db.disconnect() + + +if __name__ == '__main__': + unittest.main() From 002de606f6c852ce1c043b3b1a43f57cf75d026a Mon Sep 17 00:00:00 2001 From: Dmitry Romanov Date: Wed, 22 Nov 2023 15:38:33 -0500 Subject: [PATCH 23/34] create_run_period with unit testing --- python/rcdb/provider.py | 54 ++++++++++++++++++++++++++++ python/tests/test_get_run_periods.py | 51 +++++++++++++++++++++++++- 2 files changed, 104 insertions(+), 1 deletion(-) diff --git a/python/rcdb/provider.py b/python/rcdb/provider.py index b55443ea..e842b2a2 100644 --- a/python/rcdb/provider.py +++ b/python/rcdb/provider.py @@ -96,6 +96,9 @@ def connect(self, connection_string="mysql+pymysql://rcdb@127.0.0.1/rcdb", check :type connection_string: str """ + if not connection_string: + raise ValueError("Connection string is whitespace or empty. Provide proper connection string for DB") + try: self.engine = sqlalchemy.create_engine(connection_string) except ImportError as err: @@ -316,6 +319,57 @@ def get_run_periods(self): except NoResultFound: return [] + # ------------------------------------------------ + # Creates run period + # ------------------------------------------------ + def create_run_period(self, name, description, run_min, run_max, start_date, end_date): + """ + Creates run period + + :param name: Short name or run period e.g. Gluex Spring 2018 + :type name: str + + :param description: More detailed description if needed + :type description: str + + :return: ConditionType object that corresponds to created DB record + :rtype: ConditionType + """ + + query = self.session.query(RunPeriod).filter(RunPeriod.run_min == run_min, RunPeriod.run_max == run_max) + + if query.count(): + # we've found a run period with this run_max and run_min + rp = query.first() + assert isinstance(rp, RunPeriod) + + message = f"Run period with run_min={run_min} and run_max={run_max} already exists in DB:" \ + f"name={rp.name}, descr.={rp.description}, start_date={rp.start_date}, end_date={rp.end_date}" + + raise ValueError(message) + else: + # no such ConditionType found in the database + rp = RunPeriod() + rp.name = name + rp.description = description + rp.start_date = start_date + rp.end_date = end_date + rp.run_min = run_min + rp.run_max = run_max + + try: + self.session.add(rp) + self.session.commit() + # clear cache + self._run_periods_cache = None + except: + self.session.rollback() + raise + + log_desc = f"RunPeriod created with name='{name}', run_min='{run_min}' run_max='{run_max}'" + self.add_log_record(rp, log_desc, 0) + return rp + # ------------------------------------------------ # Returns condition type # ------------------------------------------------ diff --git a/python/tests/test_get_run_periods.py b/python/tests/test_get_run_periods.py index 139d20ea..7b6b8318 100644 --- a/python/tests/test_get_run_periods.py +++ b/python/tests/test_get_run_periods.py @@ -3,17 +3,66 @@ import rcdb import rcdb.model from rcdb.model import RunPeriod +import datetime +""" +Example of run periods table used in this unit tests: +Yet another data example is here: +https://halldweb.jlab.org/wiki/index.php/Run_Periods + +run_periods = { + "2017-01": (30000, 39999, "23 Jan 2017 - 13 Mar 2017 12 GeV e-"), + "2016-10": (20000, 29999, "15 Sep 2016 - 21 Dec 2016 12 GeV e-"), + "2016-02": (10000, 19999, "28 Jan 2016 - 24 Apr 2016 Commissioning, 12 GeV e-"), + "2015-12": (3939, 4807, "01 Dec 2015 - 28 Jan 2016 Commissioning, 12 GeV e-, Cosmics"), + "2015-06": (3386, 3938, "29 May 2015 - 01 Dec 2015 Cosmics"), + "2015-03": (2607, 3385, "11 Mar 2015 - 29 May 2015 Commissioning, 5.5 GeV e-"), + "2015-01": (2440, 2606, "06 Feb 2015 - 11 Mar 2015 Cosmics"), + "2014-10": (630, 2439, "28 Oct 2014 - 21 Dec 2014 Commissioning, 10 GeV e-"), +} +""" class TestRunPeriod(unittest.TestCase): def setUp(self): self.db = rcdb.RCDBProvider("sqlite://", check_version=False) rcdb.provider.destroy_all_create_schema(self.db) - def tearDown(self): self.db.disconnect() + def test_create_run_periods(self): + """Test of Run in db function""" + + rp = self.db.create_run_period("Gluex 2017-01", + "12 GeV e-", + 30000, + 39999, + datetime.date(2017,1, 23), + datetime.date(2017, 3, 13)) + self.assertEqual(rp.name, "Gluex 2017-01") + self.assertEqual(rp.description, "12 GeV e-") + self.assertEqual(rp.start_date, datetime.date(2017,1, 23)) + self.assertEqual(rp.end_date, datetime.date(2017, 3, 13)) + self.assertEqual(rp.run_min, 30000) + self.assertEqual(rp.run_max, 39999) + + def test_get_run_periods(self): + self.db.create_run_period("Gluex 2017-01", + "12 GeV e-", + 30000, + 39999, + datetime.date(2017,1, 23), + datetime.date(2017, 3, 13)) + + self.db.create_run_period("Gluex 2016-02", + "End of GlueX phase", + 20000, + 29999, + datetime.date(2016,9, 15), + datetime.date(2016, 12, 21)) + rps = self.db.get_run_periods() + self.assertEqual(len(rps), 2) + if __name__ == '__main__': unittest.main() From 5761d845f479b9af01235284f7f7c28a3b748211 Mon Sep 17 00:00:00 2001 From: Dmitry Romanov Date: Wed, 22 Nov 2023 15:39:09 -0500 Subject: [PATCH 24/34] Check connection string is set in rcdb cli 'db' command --- python/rcdb/rcdb_cli/__main__.py | 1 - python/rcdb/rcdb_cli/app.py | 3 +++ python/rcdb/rcdb_cli/db.py | 12 ++++++++++-- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/python/rcdb/rcdb_cli/__main__.py b/python/rcdb/rcdb_cli/__main__.py index bc4e9c96..fadbe93d 100644 --- a/python/rcdb/rcdb_cli/__main__.py +++ b/python/rcdb/rcdb_cli/__main__.py @@ -1,4 +1,3 @@ from rcdb.rcdb_cli.app import rcdb_cli rcdb_cli(prog_name="rcdb") - diff --git a/python/rcdb/rcdb_cli/app.py b/python/rcdb/rcdb_cli/app.py index 21e63325..bd34650c 100644 --- a/python/rcdb/rcdb_cli/app.py +++ b/python/rcdb/rcdb_cli/app.py @@ -42,6 +42,9 @@ def rcdb_cli(ctx, user_config, connection, config, verbose): # Create a rcdb_app_context object and remember it as the context object. From # this point onwards other commands can refer to it by using the # @pass_rcdb_context decorator. + if not connection: + print("(!)WARNING no connection provided! " + "Provide DB connection string via --connection/-c or RCDB_CONNECTION environment variable.") ctx.obj = RcdbApplicationContext(os.path.abspath(user_config), connection) ctx.obj.verbose = verbose for key, value in config: diff --git a/python/rcdb/rcdb_cli/db.py b/python/rcdb/rcdb_cli/db.py index df893239..aa194d8b 100644 --- a/python/rcdb/rcdb_cli/db.py +++ b/python/rcdb/rcdb_cli/db.py @@ -16,8 +16,16 @@ def db(ctx): """Database management commands.""" if ctx.invoked_subcommand is None: connection_str = ctx.obj.connection_str - provider = RCDBProvider(connection_str, check_version=False) - schema_version, = provider.session.execute(select(SchemaVersion).order_by(SchemaVersion.version.desc())).first() + + # We create provider manually and not using ctx.obj.db because we need check_version=False + # We separate class creation and connection because connection_str might be null + provider = RCDBProvider() + if not connection_str: + print("ERROR connection string is missing.") + exit(1) + provider.connect(connection_str, check_version=False) + query = select(SchemaVersion).order_by(SchemaVersion.version.desc()) + schema_version, = provider.session.execute(query).first() print("Schema version: {} - '{}'".format(schema_version.version, schema_version.comment)) From b952083d13f94b5714093dcbfa17d406444b1f39 Mon Sep 17 00:00:00 2001 From: "sqrd.max" Date: Wed, 29 Nov 2023 18:00:28 +0200 Subject: [PATCH 25/34] finished select value --- python/rcdb/model.py | 4 +- rcdb_web/__init__.py | 22 ++++++----- rcdb_web/select_values/veiws.py | 11 ++++-- rcdb_web/templates/run_search_box.html | 43 +++++++++------------ rcdb_web/templates/select_values/index.html | 4 +- 5 files changed, 42 insertions(+), 42 deletions(-) diff --git a/python/rcdb/model.py b/python/rcdb/model.py index dabc1db4..2263dc1b 100644 --- a/python/rcdb/model.py +++ b/python/rcdb/model.py @@ -119,10 +119,10 @@ class RunPeriod(ModelBase): id = Column(Integer, primary_key=True) name = Column(String(255), nullable=False) description = Column(String(255), nullable=True) - start_date = Column(Date, nullable=True) - end_date = Column(Date, nullable=True) run_min = Column(Integer, nullable=False) run_max = Column(Integer, nullable=False) + start_date = Column(Date, nullable=True) + end_date = Column(Date, nullable=True) def __repr__(self): return "".format(self.name, self.run_min, self.run_max) diff --git a/rcdb_web/__init__.py b/rcdb_web/__init__.py index e4d64a99..17bddf40 100644 --- a/rcdb_web/__init__.py +++ b/rcdb_web/__init__.py @@ -1,5 +1,5 @@ from rcdb.alias import get_default_aliases_by_name -from rcdb.model import Run +from rcdb.model import Run, RunPeriod from flask import Flask, render_template, g, request, url_for import rcdb from datetime import datetime @@ -46,22 +46,26 @@ def not_found(error): @app.route('/sample') def sample(): - return render_template('index.html') +@app.route('/run_periods') +def run_periods(): + run_periods = g.tdb.session.query(RunPeriod).all() + + @app.route('/') def index(): - # Select the last 50 runs and - runs = g.tdb.session\ - .query(Run)\ - .order_by(Run.number.desc())\ - .options(subqueryload(Run.conditions))\ + runs = g.tdb.session \ + .query(Run) \ + .order_by(Run.number.desc()) \ + .options(subqueryload(Run.conditions)) \ .limit(50) condition_types = g.tdb.get_condition_types() - return render_template("index.html", runs=runs, DefaultConditions=rcdb.DefaultConditions, condition_types=condition_types) + return render_template("index.html", runs=runs, DefaultConditions=rcdb.DefaultConditions, + condition_types=condition_types) @app.template_filter('remove_dot_conf') @@ -82,8 +86,6 @@ def url_for_other_page(page): app.jinja_env.globals['url_for_other_page'] = url_for_other_page app.jinja_env.globals['rcdb_default_alias'] = rcdb.alias.default_aliases - - app.register_blueprint(runs_module) app.register_blueprint(logs_module) app.register_blueprint(files_module) diff --git a/rcdb_web/select_values/veiws.py b/rcdb_web/select_values/veiws.py index 4aa812fd..dcd53b23 100644 --- a/rcdb_web/select_values/veiws.py +++ b/rcdb_web/select_values/veiws.py @@ -6,7 +6,7 @@ # from werkzeug import check_password_hash, generate_password_hash import rcdb from collections import defaultdict -from rcdb.model import Run, Condition, ConditionType +from rcdb.model import Run, Condition, ConditionType, RunPeriod from sqlalchemy.orm import subqueryload from rcdb.provider import RCDBProvider @@ -18,15 +18,17 @@ @mod.route('/') def index(): all_conditions = g.tdb.session.query(ConditionType).order_by(ConditionType.name.asc()).all() + run_periods = g.tdb.session.query(RunPeriod).all() run_from_str = request.args.get('runFrom', '') run_to_str = request.args.get('runTo', '') search_query = request.args.get('q', '') req_conditions_str = request.args.get('cnd', '') - print("search" + search_query) + print(run_periods) return render_template("select_values/index.html", all_conditions=all_conditions, + run_periods=run_periods, run_from_str=run_from_str, run_to_str=run_to_str, search_query=search_query, @@ -36,6 +38,7 @@ def index(): @mod.route('/search', methods=['GET']) def search(): all_conditions = g.tdb.session.query(ConditionType).order_by(ConditionType.name.asc()).all() + run_periods = g.tdb.session.query(RunPeriod).all() run_range = request.args.get('rr', '') search_query = request.args.get('q', '') req_conditions_str = request.args.get('cnd', '') @@ -52,15 +55,15 @@ def search(): try: table = g.tdb.select_values(val_names=req_conditions_values, search_str=search_query, run_min=run_from, run_max=run_to, sort_desc=True) - print(req_conditions_values, run_from, run_to) except Exception as err: flash("Error in performing request: {}".format(err), 'danger') return redirect(url_for('select_values.index')) - print(search_query) + return render_template("select_values/index.html", all_conditions=all_conditions, + run_periods=run_periods, run_range=run_range, run_from=run_from, run_to=run_to, diff --git a/rcdb_web/templates/run_search_box.html b/rcdb_web/templates/run_search_box.html index 2528d264..994eb47d 100644 --- a/rcdb_web/templates/run_search_box.html +++ b/rcdb_web/templates/run_search_box.html @@ -1,4 +1,4 @@ -{% macro run_search_box(condition_types=[], run_from=-1, run_to=-1, search_query="", form_url=url_for("runs.search"), req_conditions_str="", show_req_conditions=False) %} +{% macro run_search_box(condition_types=[], run_periods=[], run_from=-1, run_to=-1, search_query="", form_url=url_for("runs.search"), req_conditions_str="", show_req_conditions=False) %} {% set run_from_str = "" if run_from == -1 else run_from %} {% set run_to_str = "" if run_to == -1 else run_to %} @@ -18,15 +18,9 @@
    @@ -40,21 +34,25 @@ +{# Primex, GluexI, Gluex-II, CLASS-12, CSR,#} +{#
  • [70000+] 2019-01 2020
  • #} +{#
  • [60000+] 2019-01 2019-04 – DIRC-com/PrimEx
  • #} +{#
  • [50000+] 2018-08 2018-11 – 78B GlueX-I/PrimEx-Com
  • #} +{#
  • [40000+] 2018-01 2018-05 – 145B
  • #} +{#
  • [30000+] 2017-01 2017-04 – 50B 12 GeV e-
  • #} +{#
  • [20000+] 2016-10 2016-12 – 7B 12 GeV e-
  • #} +{#
  • [10000+] 2016-02
  • #} +{#
  • [3939, 10000) 2015-12
  • #} +{#
  • [3607, 3939]2015-06
  • #}
    @@ -93,7 +91,6 @@
    -