Use indexed node lookup sidecar
Browse files- .env.example +1 -0
- README.md +2 -0
- backend/app/protein/config.py +14 -0
- backend/app/protein/tsv_loader.py +7 -3
- backend/scripts/generate_node_lookup.py +66 -0
.env.example
CHANGED
|
@@ -4,6 +4,7 @@ MUSPROT_DB_FILENAME=MuSProt.db
|
|
| 4 |
|
| 5 |
# Optional mounted-volume overrides
|
| 6 |
# MUSPROT_DB_PATH=/data/MuSProt.db
|
|
|
|
| 7 |
# MUSPROT_SUMMARY_PATH=/data/musprot_summary.json
|
| 8 |
# MUSPROT_DOCS_PATH=/data/MuSProt_documentation.md
|
| 9 |
# MUSPROT_PLOTS_DIR=/data/plots
|
|
|
|
| 4 |
|
| 5 |
# Optional mounted-volume overrides
|
| 6 |
# MUSPROT_DB_PATH=/data/MuSProt.db
|
| 7 |
+
# MUSPROT_NODE_DB_PATH=/data/MuSProt-node-lookup.db
|
| 8 |
# MUSPROT_SUMMARY_PATH=/data/musprot_summary.json
|
| 9 |
# MUSPROT_DOCS_PATH=/data/MuSProt_documentation.md
|
| 10 |
# MUSPROT_PLOTS_DIR=/data/plots
|
README.md
CHANGED
|
@@ -37,6 +37,7 @@ Recommended Dataset repository files:
|
|
| 37 |
|
| 38 |
```text
|
| 39 |
MuSProt.db
|
|
|
|
| 40 |
musprot_summary.json
|
| 41 |
MuSProt_documentation.md
|
| 42 |
plots/
|
|
@@ -46,6 +47,7 @@ Optional Space variables:
|
|
| 46 |
|
| 47 |
```text
|
| 48 |
MUSPROT_DB_PATH=/data/MuSProt.db
|
|
|
|
| 49 |
MUSPROT_SUMMARY_PATH=/data/musprot_summary.json
|
| 50 |
MUSPROT_DOCS_PATH=/data/MuSProt_documentation.md
|
| 51 |
MUSPROT_PLOTS_DIR=/data/plots
|
|
|
|
| 37 |
|
| 38 |
```text
|
| 39 |
MuSProt.db
|
| 40 |
+
MuSProt-node-lookup.db
|
| 41 |
musprot_summary.json
|
| 42 |
MuSProt_documentation.md
|
| 43 |
plots/
|
|
|
|
| 47 |
|
| 48 |
```text
|
| 49 |
MUSPROT_DB_PATH=/data/MuSProt.db
|
| 50 |
+
MUSPROT_NODE_DB_PATH=/data/MuSProt-node-lookup.db
|
| 51 |
MUSPROT_SUMMARY_PATH=/data/musprot_summary.json
|
| 52 |
MUSPROT_DOCS_PATH=/data/MuSProt_documentation.md
|
| 53 |
MUSPROT_PLOTS_DIR=/data/plots
|
backend/app/protein/config.py
CHANGED
|
@@ -66,6 +66,20 @@ def get_summary_path() -> Path | None:
|
|
| 66 |
)
|
| 67 |
|
| 68 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
def get_docs_path() -> Path | None:
|
| 70 |
"""Return the dataset documentation path when available."""
|
| 71 |
explicit_path = os.getenv("MUSPROT_DOCS_PATH")
|
|
|
|
| 66 |
)
|
| 67 |
|
| 68 |
|
| 69 |
+
def get_node_database_path() -> Path:
|
| 70 |
+
"""Return an optional indexed node lookup sidecar, falling back to the main DB."""
|
| 71 |
+
explicit_path = os.getenv("MUSPROT_NODE_DB_PATH")
|
| 72 |
+
if explicit_path:
|
| 73 |
+
path = Path(explicit_path).expanduser().resolve()
|
| 74 |
+
if path.exists():
|
| 75 |
+
return path
|
| 76 |
+
|
| 77 |
+
return (
|
| 78 |
+
_existing_path(DEFAULT_VOLUME_DIR / "MuSProt-node-lookup.db")
|
| 79 |
+
or get_database_path()
|
| 80 |
+
).resolve()
|
| 81 |
+
|
| 82 |
+
|
| 83 |
def get_docs_path() -> Path | None:
|
| 84 |
"""Return the dataset documentation path when available."""
|
| 85 |
explicit_path = os.getenv("MUSPROT_DOCS_PATH")
|
backend/app/protein/tsv_loader.py
CHANGED
|
@@ -8,10 +8,11 @@ key the first occurrence is kept.
|
|
| 8 |
import ast
|
| 9 |
import sqlite3
|
| 10 |
import logging
|
|
|
|
| 11 |
from functools import lru_cache
|
| 12 |
from typing import Dict, List, Optional, Tuple, Any
|
| 13 |
|
| 14 |
-
from app.protein.config import get_database_path
|
| 15 |
from app.protein.database import connect_readonly
|
| 16 |
|
| 17 |
logger = logging.getLogger(__name__)
|
|
@@ -81,7 +82,7 @@ def reset_index() -> None:
|
|
| 81 |
@lru_cache(maxsize=4096)
|
| 82 |
def _lookup_node_row(pdb_id: str, auth_asym_id: str) -> Optional[_Row]:
|
| 83 |
"""Read one node row without blocking on a full-table in-memory index."""
|
| 84 |
-
conn = connect_readonly(
|
| 85 |
try:
|
| 86 |
conn.row_factory = sqlite3.Row
|
| 87 |
row = conn.execute(
|
|
@@ -93,7 +94,10 @@ def _lookup_node_row(pdb_id: str, auth_asym_id: str) -> Optional[_Row]:
|
|
| 93 |
).fetchone()
|
| 94 |
if row is None:
|
| 95 |
return None
|
| 96 |
-
return {
|
|
|
|
|
|
|
|
|
|
| 97 |
finally:
|
| 98 |
conn.close()
|
| 99 |
|
|
|
|
| 8 |
import ast
|
| 9 |
import sqlite3
|
| 10 |
import logging
|
| 11 |
+
import zlib
|
| 12 |
from functools import lru_cache
|
| 13 |
from typing import Dict, List, Optional, Tuple, Any
|
| 14 |
|
| 15 |
+
from app.protein.config import get_database_path, get_node_database_path
|
| 16 |
from app.protein.database import connect_readonly
|
| 17 |
|
| 18 |
logger = logging.getLogger(__name__)
|
|
|
|
| 82 |
@lru_cache(maxsize=4096)
|
| 83 |
def _lookup_node_row(pdb_id: str, auth_asym_id: str) -> Optional[_Row]:
|
| 84 |
"""Read one node row without blocking on a full-table in-memory index."""
|
| 85 |
+
conn = connect_readonly(get_node_database_path())
|
| 86 |
try:
|
| 87 |
conn.row_factory = sqlite3.Row
|
| 88 |
row = conn.execute(
|
|
|
|
| 94 |
).fetchone()
|
| 95 |
if row is None:
|
| 96 |
return None
|
| 97 |
+
return {
|
| 98 |
+
key: zlib.decompress(value).decode("utf-8") if isinstance(value, bytes) else value or ""
|
| 99 |
+
for key, value in ((key, row[key]) for key in row.keys())
|
| 100 |
+
}
|
| 101 |
finally:
|
| 102 |
conn.close()
|
| 103 |
|
backend/scripts/generate_node_lookup.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Generate an indexed read-only node lookup sidecar from MuSProt.db."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import argparse
|
| 5 |
+
import sqlite3
|
| 6 |
+
import zlib
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
NODE_COLUMNS = (
|
| 11 |
+
"pdb_id", "auth_asym_id", "base_label", "sequence", "sequence_length",
|
| 12 |
+
"CATH_ID", "cath_superfamily", "Rosetta", "FoldX", "EvoEF2", "RM", "RM+",
|
| 13 |
+
"ranked_functions", "state_id", "experimental_method", "pH", "temp_K",
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def main() -> None:
|
| 18 |
+
parser = argparse.ArgumentParser()
|
| 19 |
+
parser.add_argument("source", type=Path)
|
| 20 |
+
parser.add_argument("-o", "--output", type=Path, default=Path("MuSProt-node-lookup.db"))
|
| 21 |
+
args = parser.parse_args()
|
| 22 |
+
|
| 23 |
+
output = args.output.resolve()
|
| 24 |
+
output.unlink(missing_ok=True)
|
| 25 |
+
|
| 26 |
+
conn = sqlite3.connect(output)
|
| 27 |
+
try:
|
| 28 |
+
conn.execute("PRAGMA journal_mode = OFF")
|
| 29 |
+
conn.execute("PRAGMA synchronous = OFF")
|
| 30 |
+
conn.execute(
|
| 31 |
+
'CREATE TABLE node (pdb_id, auth_asym_id, base_label, sequence BLOB,'
|
| 32 |
+
' sequence_length, CATH_ID, cath_superfamily, Rosetta, FoldX, EvoEF2,'
|
| 33 |
+
' RM, "RM+", ranked_functions BLOB, state_id, experimental_method, pH, temp_K)'
|
| 34 |
+
)
|
| 35 |
+
source = sqlite3.connect(f"file:{args.source.resolve()}?mode=ro", uri=True)
|
| 36 |
+
try:
|
| 37 |
+
select_columns = ", ".join(f'"{column}"' for column in NODE_COLUMNS)
|
| 38 |
+
rows = source.execute(f"SELECT {select_columns} FROM node")
|
| 39 |
+
placeholders = ", ".join("?" for _ in NODE_COLUMNS)
|
| 40 |
+
batch = []
|
| 41 |
+
for row in rows:
|
| 42 |
+
row = list(row)
|
| 43 |
+
for index in (3, 12):
|
| 44 |
+
row[index] = zlib.compress((row[index] or "").encode("utf-8"), 1)
|
| 45 |
+
batch.append(row)
|
| 46 |
+
if len(batch) == 1000:
|
| 47 |
+
conn.executemany(f"INSERT INTO node VALUES ({placeholders})", batch)
|
| 48 |
+
batch.clear()
|
| 49 |
+
if batch:
|
| 50 |
+
conn.executemany(f"INSERT INTO node VALUES ({placeholders})", batch)
|
| 51 |
+
finally:
|
| 52 |
+
source.close()
|
| 53 |
+
conn.execute(
|
| 54 |
+
"CREATE INDEX idx_node_chain "
|
| 55 |
+
"ON node(LOWER(pdb_id), UPPER(auth_asym_id))"
|
| 56 |
+
)
|
| 57 |
+
conn.execute("ANALYZE")
|
| 58 |
+
conn.commit()
|
| 59 |
+
finally:
|
| 60 |
+
conn.close()
|
| 61 |
+
|
| 62 |
+
print(f"Created {output} ({output.stat().st_size:,} bytes)")
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
if __name__ == "__main__":
|
| 66 |
+
main()
|