scigantic-bindingdb 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- scigantic_bindingdb-0.2.0/LICENSE +16 -0
- scigantic_bindingdb-0.2.0/PKG-INFO +153 -0
- scigantic_bindingdb-0.2.0/README.md +122 -0
- scigantic_bindingdb-0.2.0/pyproject.toml +53 -0
- scigantic_bindingdb-0.2.0/setup.cfg +4 -0
- scigantic_bindingdb-0.2.0/src/scigantic_bindingdb/__init__.py +41 -0
- scigantic_bindingdb-0.2.0/src/scigantic_bindingdb/__main__.py +8 -0
- scigantic_bindingdb-0.2.0/src/scigantic_bindingdb/_constants.py +7 -0
- scigantic_bindingdb-0.2.0/src/scigantic_bindingdb/cache.py +113 -0
- scigantic_bindingdb-0.2.0/src/scigantic_bindingdb/chembl_bridge.py +87 -0
- scigantic_bindingdb-0.2.0/src/scigantic_bindingdb/cli.py +55 -0
- scigantic_bindingdb-0.2.0/src/scigantic_bindingdb/connection.py +84 -0
- scigantic_bindingdb-0.2.0/src/scigantic_bindingdb/dti_pairs.py +74 -0
- scigantic_bindingdb-0.2.0/src/scigantic_bindingdb/measurements.py +79 -0
- scigantic_bindingdb-0.2.0/src/scigantic_bindingdb/py.typed +0 -0
- scigantic_bindingdb-0.2.0/src/scigantic_bindingdb/releases.py +105 -0
- scigantic_bindingdb-0.2.0/src/scigantic_bindingdb.egg-info/PKG-INFO +153 -0
- scigantic_bindingdb-0.2.0/src/scigantic_bindingdb.egg-info/SOURCES.txt +27 -0
- scigantic_bindingdb-0.2.0/src/scigantic_bindingdb.egg-info/dependency_links.txt +1 -0
- scigantic_bindingdb-0.2.0/src/scigantic_bindingdb.egg-info/entry_points.txt +2 -0
- scigantic_bindingdb-0.2.0/src/scigantic_bindingdb.egg-info/requires.txt +7 -0
- scigantic_bindingdb-0.2.0/src/scigantic_bindingdb.egg-info/top_level.txt +1 -0
- scigantic_bindingdb-0.2.0/tests/test_cache.py +107 -0
- scigantic_bindingdb-0.2.0/tests/test_chembl_bridge.py +31 -0
- scigantic_bindingdb-0.2.0/tests/test_cli.py +16 -0
- scigantic_bindingdb-0.2.0/tests/test_connection.py +33 -0
- scigantic_bindingdb-0.2.0/tests/test_dti_pairs.py +60 -0
- scigantic_bindingdb-0.2.0/tests/test_measurements.py +48 -0
- scigantic_bindingdb-0.2.0/tests/test_releases.py +57 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
MIT No Attribution
|
|
2
|
+
|
|
3
|
+
Copyright 2026 Scigantic
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
|
6
|
+
software and associated documentation files (the "Software"), to deal in the Software
|
|
7
|
+
without restriction, including without limitation the rights to use, copy, modify,
|
|
8
|
+
merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
|
|
9
|
+
permit persons to whom the Software is furnished to do so.
|
|
10
|
+
|
|
11
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
|
12
|
+
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
|
13
|
+
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
14
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
|
|
15
|
+
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
|
16
|
+
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: scigantic-bindingdb
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Query BindingDB directly from a public S3 mirror with DuckDB, including a ChEMBL cross-reference bridge table and a ready drug-target-interaction training table.
|
|
5
|
+
Author: Scigantic
|
|
6
|
+
License: MIT-0
|
|
7
|
+
Project-URL: Homepage, https://scigantic.com
|
|
8
|
+
Project-URL: Repository, https://github.com/Scigantic/scigantic-bindingdb
|
|
9
|
+
Project-URL: Issues, https://github.com/Scigantic/scigantic-bindingdb/issues
|
|
10
|
+
Keywords: bindingdb,cheminformatics,duckdb,drug-discovery,binding-affinity
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT No Attribution License (MIT-0)
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Chemistry
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: duckdb>=0.10
|
|
25
|
+
Requires-Dist: pandas>=1.5
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
28
|
+
Requires-Dist: mypy>=1.10; extra == "dev"
|
|
29
|
+
Requires-Dist: pandas-stubs; extra == "dev"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
<h1 align="center">scigantic-bindingdb</h1>
|
|
33
|
+
|
|
34
|
+
<p align="center">
|
|
35
|
+
<a href="https://github.com/Scigantic/scigantic-bindingdb/actions/workflows/ci.yml">
|
|
36
|
+
<img alt="CI" src="https://github.com/Scigantic/scigantic-bindingdb/actions/workflows/ci.yml/badge.svg" /></a>
|
|
37
|
+
<a href="https://pypi.org/project/scigantic-bindingdb/">
|
|
38
|
+
<img alt="PyPI" src="https://img.shields.io/pypi/v/scigantic-bindingdb" /></a>
|
|
39
|
+
<a href="https://pypi.org/project/scigantic-bindingdb/">
|
|
40
|
+
<img alt="PyPI - Python Version" src="https://img.shields.io/pypi/pyversions/scigantic-bindingdb" /></a>
|
|
41
|
+
<a href="https://github.com/Scigantic/scigantic-bindingdb/blob/main/LICENSE">
|
|
42
|
+
<img alt="License" src="https://img.shields.io/github/license/Scigantic/scigantic-bindingdb" /></a>
|
|
43
|
+
</p>
|
|
44
|
+
|
|
45
|
+
Query BindingDB directly from a public S3 mirror with DuckDB.
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
import scigantic_bindingdb as bindingdb
|
|
49
|
+
|
|
50
|
+
df = bindingdb.query("""
|
|
51
|
+
SELECT reactant_set_id, ligand_smiles, ki_nm_value
|
|
52
|
+
FROM measurements
|
|
53
|
+
WHERE ki_nm_value IS NOT NULL
|
|
54
|
+
LIMIT 5
|
|
55
|
+
""")
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
That query runs against `s3://scigantic-bindingdb` over DuckDB's httpfs extension. Nothing is downloaded first, and there's no local database file sitting on disk afterward.
|
|
59
|
+
|
|
60
|
+
## Installation
|
|
61
|
+
|
|
62
|
+
```console
|
|
63
|
+
$ pip install scigantic-bindingdb
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## What's different about BindingDB
|
|
67
|
+
|
|
68
|
+
Every row is one binding measurement (Ki, IC50, Kd or EC50) between one ligand and one protein target, rather than ChEMBL's assay-centric bioactivity record. BindingDB ships no relational database, just a flat TSV; the mirror normalizes it into `measurements` (one row per measurement), `target_chains` (one row per protein chain of the target, since BindingDB's raw format repeats a column block once per chain in a multimer) and `target_chain_names`.
|
|
69
|
+
|
|
70
|
+
## Measurements, filtered the way that avoids the two sharp edges
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
df = bindingdb.measurements(uniprot_id="P00533", endpoint="ki") # EGFR
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Two things this does that a raw query on `measurements` doesn't do for you:
|
|
77
|
+
|
|
78
|
+
**Censored values stay out unless you ask for them.** Ki/IC50/Kd/EC50 are occasionally reported as `>X` or `<X` rather than an exact value, the same idea as ChEMBL's `standard_relation`. `exact_only=True`, the default, keeps only rows where that endpoint's qualifier is `=`:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
df = bindingdb.measurements(uniprot_id="P00533", endpoint="ic50", exact_only=False) # include censored bounds too
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**Target filtering doesn't fan out on multichain complexes.** `uniprot_id` matches against `target_chains` with an `EXISTS` check, not a `JOIN`, so a target whose accession appears on more than one chain of the same complex still returns each measurement once.
|
|
85
|
+
|
|
86
|
+
`bindingdb.query()` still reaches the raw tables directly for anything this leaves out.
|
|
87
|
+
|
|
88
|
+
## Cross-referencing ChEMBL
|
|
89
|
+
|
|
90
|
+
BindingDB ingests ChEMBL as one of its own curated source feeds (51.3% of measurements in the 202608 release), and ChEMBL separately absorbs some BindingDB patent-derived bioactivity data, so the two archives are not independent corpora. `derived/bindingdb_chembl_bridge.parquet`, built once at mirror time, joins measurements to [scigantic-chembl](https://github.com/Scigantic/scigantic-chembl) by BindingDB's own `chembl_id` column where present (authoritative) and falls back to an exact InChIKey match where it's missing:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
df = bindingdb.chembl_bridge(reactant_set_id="50000001")
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`with_names=True` (the default) also reaches into the live `scigantic-chembl` mirror for the matched compound's ChEMBL preferred name, a second public-bucket read over the same connection: not a mount-level dependency between the two archives, just a query-time join across two buckets that are both public and read-only here.
|
|
97
|
+
|
|
98
|
+
## Drug-target-interaction pairs
|
|
99
|
+
|
|
100
|
+
BindingDB is the dataset most DTI/proteochemometric tooling (like [DeepPurpose](https://github.com/kexinhuang12345/DeepPurpose)) is built around, specifically because it ships a full protein sequence alongside every affinity measurement, something ChEMBL's bioactivity tables don't do as directly. `derived/dti_pairs.parquet` is BindingDB reshaped into the (ligand, target, affinity) triples a model trains on, done once rather than re-derived by every caller:
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
df = bindingdb.dti_pairs(endpoint="ki", single_chain_only=True)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
reactant_set_id ligand_smiles target_sequence uniprot_id endpoint affinity_nm p_affinity
|
|
108
|
+
764556 Cc1ncoc1-c1nnc... MASLSQLSSHLN... P35462 ki 1.74 8.759451
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Only exact measurements (never a censored `>X`/`<X` bound treated as a real label), and `p_affinity` is already computed as `-log10(affinity_nm * 1e-9)`, the same transform as ChEMBL's `pchembl_value`. 2,589,053 pairs across the four endpoints in the 202608 release, 1,163,672 distinct ligands, 9,219 distinct UniProt targets.
|
|
112
|
+
|
|
113
|
+
Multichain targets are represented by chain 1's sequence only, standard practice for DTI benchmarks. Pass `single_chain_only=True` to drop the 5.7% of rows where that simplifies an actual multi-protein complex, if single-chain purity matters for your model. See `derived/DTI_README.md` in the mirror for the exact filters applied.
|
|
114
|
+
|
|
115
|
+
## Working offline
|
|
116
|
+
|
|
117
|
+
Off by default, since zero setup is the whole point. Turn it on to run the same queries repeatedly without re-fetching from S3:
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
import scigantic_bindingdb as bindingdb
|
|
121
|
+
|
|
122
|
+
bindingdb.enable_cache()
|
|
123
|
+
df = bindingdb.chembl_bridge() # downloads the bridge table once, then reads from disk
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`chembl_bridge()` and `dti_pairs()` each need exactly one derived file, so caching downloads that one file to `~/.cache/scigantic-bindingdb` (override with `enable_cache(cache_dir=...)` or the `SCIGANTIC_BINDINGDB_CACHE` environment variable) and reuses it after that.
|
|
127
|
+
|
|
128
|
+
`connect()`, `query()` and `measurements()` don't participate in this: `connect()` registers five core tables as views on every call, so caching them there would mean any call eagerly downloads everything regardless of what the query actually touches. Cache one table yourself if you want it locally: `bindingdb.cache_resolve("202608/parquet/measurements.parquet")` downloads it and returns the local path, usable directly in `read_parquet(...)`.
|
|
129
|
+
|
|
130
|
+
## What's mirrored
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
bindingdb.releases()
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
| release | raw tables | ChEMBL bridge | DTI pairs |
|
|
137
|
+
|---|---|---|---|
|
|
138
|
+
| 202608 | yes | yes | yes |
|
|
139
|
+
|
|
140
|
+
This table isn't hardcoded. `releases()` reads a small manifest published alongside each mirror run. If it can't be reached, calls fall back to the snapshot shipped with whatever version you have installed and print a warning, rather than failing outright.
|
|
141
|
+
|
|
142
|
+
Not mirrored yet: BindingDB's 3D SDF structures and precomputed similarity/substructure search (no fingerprint corpus has been built for this archive). `bindingdb.query()` still reaches every raw table the mirror carries.
|
|
143
|
+
|
|
144
|
+
## Command line
|
|
145
|
+
|
|
146
|
+
```console
|
|
147
|
+
$ scigantic-bindingdb info
|
|
148
|
+
$ scigantic-bindingdb query "SELECT count(*) FROM measurements" --release 202608
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## License
|
|
152
|
+
|
|
153
|
+
MIT-0. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
<h1 align="center">scigantic-bindingdb</h1>
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<a href="https://github.com/Scigantic/scigantic-bindingdb/actions/workflows/ci.yml">
|
|
5
|
+
<img alt="CI" src="https://github.com/Scigantic/scigantic-bindingdb/actions/workflows/ci.yml/badge.svg" /></a>
|
|
6
|
+
<a href="https://pypi.org/project/scigantic-bindingdb/">
|
|
7
|
+
<img alt="PyPI" src="https://img.shields.io/pypi/v/scigantic-bindingdb" /></a>
|
|
8
|
+
<a href="https://pypi.org/project/scigantic-bindingdb/">
|
|
9
|
+
<img alt="PyPI - Python Version" src="https://img.shields.io/pypi/pyversions/scigantic-bindingdb" /></a>
|
|
10
|
+
<a href="https://github.com/Scigantic/scigantic-bindingdb/blob/main/LICENSE">
|
|
11
|
+
<img alt="License" src="https://img.shields.io/github/license/Scigantic/scigantic-bindingdb" /></a>
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
Query BindingDB directly from a public S3 mirror with DuckDB.
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
import scigantic_bindingdb as bindingdb
|
|
18
|
+
|
|
19
|
+
df = bindingdb.query("""
|
|
20
|
+
SELECT reactant_set_id, ligand_smiles, ki_nm_value
|
|
21
|
+
FROM measurements
|
|
22
|
+
WHERE ki_nm_value IS NOT NULL
|
|
23
|
+
LIMIT 5
|
|
24
|
+
""")
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
That query runs against `s3://scigantic-bindingdb` over DuckDB's httpfs extension. Nothing is downloaded first, and there's no local database file sitting on disk afterward.
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```console
|
|
32
|
+
$ pip install scigantic-bindingdb
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## What's different about BindingDB
|
|
36
|
+
|
|
37
|
+
Every row is one binding measurement (Ki, IC50, Kd or EC50) between one ligand and one protein target, rather than ChEMBL's assay-centric bioactivity record. BindingDB ships no relational database, just a flat TSV; the mirror normalizes it into `measurements` (one row per measurement), `target_chains` (one row per protein chain of the target, since BindingDB's raw format repeats a column block once per chain in a multimer) and `target_chain_names`.
|
|
38
|
+
|
|
39
|
+
## Measurements, filtered the way that avoids the two sharp edges
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
df = bindingdb.measurements(uniprot_id="P00533", endpoint="ki") # EGFR
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Two things this does that a raw query on `measurements` doesn't do for you:
|
|
46
|
+
|
|
47
|
+
**Censored values stay out unless you ask for them.** Ki/IC50/Kd/EC50 are occasionally reported as `>X` or `<X` rather than an exact value, the same idea as ChEMBL's `standard_relation`. `exact_only=True`, the default, keeps only rows where that endpoint's qualifier is `=`:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
df = bindingdb.measurements(uniprot_id="P00533", endpoint="ic50", exact_only=False) # include censored bounds too
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Target filtering doesn't fan out on multichain complexes.** `uniprot_id` matches against `target_chains` with an `EXISTS` check, not a `JOIN`, so a target whose accession appears on more than one chain of the same complex still returns each measurement once.
|
|
54
|
+
|
|
55
|
+
`bindingdb.query()` still reaches the raw tables directly for anything this leaves out.
|
|
56
|
+
|
|
57
|
+
## Cross-referencing ChEMBL
|
|
58
|
+
|
|
59
|
+
BindingDB ingests ChEMBL as one of its own curated source feeds (51.3% of measurements in the 202608 release), and ChEMBL separately absorbs some BindingDB patent-derived bioactivity data, so the two archives are not independent corpora. `derived/bindingdb_chembl_bridge.parquet`, built once at mirror time, joins measurements to [scigantic-chembl](https://github.com/Scigantic/scigantic-chembl) by BindingDB's own `chembl_id` column where present (authoritative) and falls back to an exact InChIKey match where it's missing:
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
df = bindingdb.chembl_bridge(reactant_set_id="50000001")
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`with_names=True` (the default) also reaches into the live `scigantic-chembl` mirror for the matched compound's ChEMBL preferred name, a second public-bucket read over the same connection: not a mount-level dependency between the two archives, just a query-time join across two buckets that are both public and read-only here.
|
|
66
|
+
|
|
67
|
+
## Drug-target-interaction pairs
|
|
68
|
+
|
|
69
|
+
BindingDB is the dataset most DTI/proteochemometric tooling (like [DeepPurpose](https://github.com/kexinhuang12345/DeepPurpose)) is built around, specifically because it ships a full protein sequence alongside every affinity measurement, something ChEMBL's bioactivity tables don't do as directly. `derived/dti_pairs.parquet` is BindingDB reshaped into the (ligand, target, affinity) triples a model trains on, done once rather than re-derived by every caller:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
df = bindingdb.dti_pairs(endpoint="ki", single_chain_only=True)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
reactant_set_id ligand_smiles target_sequence uniprot_id endpoint affinity_nm p_affinity
|
|
77
|
+
764556 Cc1ncoc1-c1nnc... MASLSQLSSHLN... P35462 ki 1.74 8.759451
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Only exact measurements (never a censored `>X`/`<X` bound treated as a real label), and `p_affinity` is already computed as `-log10(affinity_nm * 1e-9)`, the same transform as ChEMBL's `pchembl_value`. 2,589,053 pairs across the four endpoints in the 202608 release, 1,163,672 distinct ligands, 9,219 distinct UniProt targets.
|
|
81
|
+
|
|
82
|
+
Multichain targets are represented by chain 1's sequence only, standard practice for DTI benchmarks. Pass `single_chain_only=True` to drop the 5.7% of rows where that simplifies an actual multi-protein complex, if single-chain purity matters for your model. See `derived/DTI_README.md` in the mirror for the exact filters applied.
|
|
83
|
+
|
|
84
|
+
## Working offline
|
|
85
|
+
|
|
86
|
+
Off by default, since zero setup is the whole point. Turn it on to run the same queries repeatedly without re-fetching from S3:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
import scigantic_bindingdb as bindingdb
|
|
90
|
+
|
|
91
|
+
bindingdb.enable_cache()
|
|
92
|
+
df = bindingdb.chembl_bridge() # downloads the bridge table once, then reads from disk
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`chembl_bridge()` and `dti_pairs()` each need exactly one derived file, so caching downloads that one file to `~/.cache/scigantic-bindingdb` (override with `enable_cache(cache_dir=...)` or the `SCIGANTIC_BINDINGDB_CACHE` environment variable) and reuses it after that.
|
|
96
|
+
|
|
97
|
+
`connect()`, `query()` and `measurements()` don't participate in this: `connect()` registers five core tables as views on every call, so caching them there would mean any call eagerly downloads everything regardless of what the query actually touches. Cache one table yourself if you want it locally: `bindingdb.cache_resolve("202608/parquet/measurements.parquet")` downloads it and returns the local path, usable directly in `read_parquet(...)`.
|
|
98
|
+
|
|
99
|
+
## What's mirrored
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
bindingdb.releases()
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
| release | raw tables | ChEMBL bridge | DTI pairs |
|
|
106
|
+
|---|---|---|---|
|
|
107
|
+
| 202608 | yes | yes | yes |
|
|
108
|
+
|
|
109
|
+
This table isn't hardcoded. `releases()` reads a small manifest published alongside each mirror run. If it can't be reached, calls fall back to the snapshot shipped with whatever version you have installed and print a warning, rather than failing outright.
|
|
110
|
+
|
|
111
|
+
Not mirrored yet: BindingDB's 3D SDF structures and precomputed similarity/substructure search (no fingerprint corpus has been built for this archive). `bindingdb.query()` still reaches every raw table the mirror carries.
|
|
112
|
+
|
|
113
|
+
## Command line
|
|
114
|
+
|
|
115
|
+
```console
|
|
116
|
+
$ scigantic-bindingdb info
|
|
117
|
+
$ scigantic-bindingdb query "SELECT count(*) FROM measurements" --release 202608
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## License
|
|
121
|
+
|
|
122
|
+
MIT-0. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "scigantic-bindingdb"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Query BindingDB directly from a public S3 mirror with DuckDB, including a ChEMBL cross-reference bridge table and a ready drug-target-interaction training table."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT-0" }
|
|
12
|
+
authors = [{ name = "Scigantic" }]
|
|
13
|
+
keywords = ["bindingdb", "cheminformatics", "duckdb", "drug-discovery", "binding-affinity"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"License :: OSI Approved :: MIT No Attribution License (MIT-0)",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.10",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Programming Language :: Python :: 3.13",
|
|
21
|
+
"Programming Language :: Python :: 3.14",
|
|
22
|
+
"Typing :: Typed",
|
|
23
|
+
"Topic :: Scientific/Engineering :: Bio-Informatics",
|
|
24
|
+
"Topic :: Scientific/Engineering :: Chemistry",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
dependencies = [
|
|
28
|
+
"duckdb>=0.10",
|
|
29
|
+
"pandas>=1.5",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
dev = ["pytest>=7", "mypy>=1.10", "pandas-stubs"]
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://scigantic.com"
|
|
37
|
+
Repository = "https://github.com/Scigantic/scigantic-bindingdb"
|
|
38
|
+
Issues = "https://github.com/Scigantic/scigantic-bindingdb/issues"
|
|
39
|
+
|
|
40
|
+
[project.scripts]
|
|
41
|
+
scigantic-bindingdb = "scigantic_bindingdb.cli:main"
|
|
42
|
+
|
|
43
|
+
[tool.setuptools.packages.find]
|
|
44
|
+
where = ["src"]
|
|
45
|
+
|
|
46
|
+
[tool.setuptools.package-data]
|
|
47
|
+
scigantic_bindingdb = ["py.typed"]
|
|
48
|
+
|
|
49
|
+
[tool.pytest.ini_options]
|
|
50
|
+
testpaths = ["tests"]
|
|
51
|
+
|
|
52
|
+
[tool.mypy]
|
|
53
|
+
strict = true
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Query BindingDB directly from a public S3 mirror. No download, no local database."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version as _version
|
|
4
|
+
|
|
5
|
+
from .cache import cache_dir, disable_cache, enable_cache, is_cache_enabled
|
|
6
|
+
from .cache import resolve as cache_resolve
|
|
7
|
+
from .chembl_bridge import chembl_bridge
|
|
8
|
+
from .connection import connect, query
|
|
9
|
+
from .dti_pairs import dti_pairs
|
|
10
|
+
from .measurements import measurements
|
|
11
|
+
from .releases import (
|
|
12
|
+
ReleaseCapabilityError,
|
|
13
|
+
ReleaseInfo,
|
|
14
|
+
UnknownReleaseError,
|
|
15
|
+
latest,
|
|
16
|
+
releases,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
__version__ = _version("scigantic-bindingdb")
|
|
21
|
+
except PackageNotFoundError:
|
|
22
|
+
# Running from a source checkout with no install (editable or not).
|
|
23
|
+
__version__ = "0.0.0"
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"chembl_bridge",
|
|
27
|
+
"connect",
|
|
28
|
+
"query",
|
|
29
|
+
"measurements",
|
|
30
|
+
"dti_pairs",
|
|
31
|
+
"releases",
|
|
32
|
+
"latest",
|
|
33
|
+
"enable_cache",
|
|
34
|
+
"disable_cache",
|
|
35
|
+
"is_cache_enabled",
|
|
36
|
+
"cache_dir",
|
|
37
|
+
"cache_resolve",
|
|
38
|
+
"ReleaseInfo",
|
|
39
|
+
"ReleaseCapabilityError",
|
|
40
|
+
"UnknownReleaseError",
|
|
41
|
+
]
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Optional local caching: download once, then work with no network.
|
|
2
|
+
|
|
3
|
+
Off by default. This package's whole pitch is zero setup, so caching stays
|
|
4
|
+
opt-in rather than something that changes the default behavior:
|
|
5
|
+
|
|
6
|
+
import scigantic_bindingdb as bindingdb
|
|
7
|
+
bindingdb.enable_cache()
|
|
8
|
+
|
|
9
|
+
chembl_bridge() then reads its one derived file from a local cache directory
|
|
10
|
+
instead of S3, downloading it the first time it's needed and reusing it
|
|
11
|
+
after that.
|
|
12
|
+
|
|
13
|
+
connect() / query() / measurements() deliberately do NOT use this:
|
|
14
|
+
connect() registers five core tables as views on every call, so caching
|
|
15
|
+
them there would mean any call eagerly downloads everything regardless of
|
|
16
|
+
what the query actually touches. Cache a specific table yourself if you
|
|
17
|
+
want it locally: cache_resolve("<release>/parquet/<table>.parquet")
|
|
18
|
+
downloads it and returns the local path, usable directly in
|
|
19
|
+
read_parquet(...).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import os
|
|
25
|
+
import sys
|
|
26
|
+
import urllib.request
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
from ._constants import BUCKET, REGION
|
|
30
|
+
|
|
31
|
+
_enabled = False
|
|
32
|
+
_cache_dir: Path | None = None
|
|
33
|
+
|
|
34
|
+
_CHUNK_BYTES = 1024 * 1024
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _default_cache_dir() -> Path:
|
|
38
|
+
if sys.platform == "win32":
|
|
39
|
+
base = os.environ.get("LOCALAPPDATA") or str(Path.home() / "AppData" / "Local")
|
|
40
|
+
elif sys.platform == "darwin":
|
|
41
|
+
base = str(Path.home() / "Library" / "Caches")
|
|
42
|
+
else:
|
|
43
|
+
base = os.environ.get("XDG_CACHE_HOME") or str(Path.home() / ".cache")
|
|
44
|
+
return Path(base) / "scigantic-bindingdb"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def enable_cache(cache_dir: str | None = None) -> Path:
|
|
48
|
+
"""Turn on local caching for every function in this package that uses it.
|
|
49
|
+
|
|
50
|
+
Cache location: `cache_dir` if given, else the
|
|
51
|
+
SCIGANTIC_BINDINGDB_CACHE environment variable, else a
|
|
52
|
+
platform-appropriate user cache directory. Returns the resolved
|
|
53
|
+
directory.
|
|
54
|
+
"""
|
|
55
|
+
global _enabled, _cache_dir
|
|
56
|
+
if cache_dir is not None:
|
|
57
|
+
resolved = Path(cache_dir)
|
|
58
|
+
elif os.environ.get("SCIGANTIC_BINDINGDB_CACHE"):
|
|
59
|
+
resolved = Path(os.environ["SCIGANTIC_BINDINGDB_CACHE"])
|
|
60
|
+
else:
|
|
61
|
+
resolved = _default_cache_dir()
|
|
62
|
+
resolved.mkdir(parents=True, exist_ok=True)
|
|
63
|
+
_cache_dir = resolved
|
|
64
|
+
_enabled = True
|
|
65
|
+
return resolved
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def disable_cache() -> None:
|
|
69
|
+
"""Turn caching back off. Later calls go straight to S3 again.
|
|
70
|
+
|
|
71
|
+
Anything already downloaded stays on disk; this only stops using it.
|
|
72
|
+
"""
|
|
73
|
+
global _enabled
|
|
74
|
+
_enabled = False
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def is_cache_enabled() -> bool:
|
|
78
|
+
return _enabled
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def cache_dir() -> Path | None:
|
|
82
|
+
"""The resolved cache directory, or None if caching has never been enabled."""
|
|
83
|
+
return _cache_dir
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def resolve(key: str) -> str:
|
|
87
|
+
"""An S3 URL, or a local cached file path if caching is on.
|
|
88
|
+
|
|
89
|
+
`key` is a path relative to the bucket root, e.g.
|
|
90
|
+
"202608/derived/bindingdb_chembl_bridge.parquet". Downloads to the
|
|
91
|
+
cache on first access; later calls for the same key reuse the local
|
|
92
|
+
file without touching the network.
|
|
93
|
+
"""
|
|
94
|
+
if not _enabled:
|
|
95
|
+
return f"s3://{BUCKET}/{key}"
|
|
96
|
+
|
|
97
|
+
assert _cache_dir is not None
|
|
98
|
+
local_path = _cache_dir / key
|
|
99
|
+
if local_path.exists():
|
|
100
|
+
return str(local_path)
|
|
101
|
+
|
|
102
|
+
local_path.parent.mkdir(parents=True, exist_ok=True)
|
|
103
|
+
url = f"https://{BUCKET}.s3.{REGION}.amazonaws.com/{key}"
|
|
104
|
+
# Download to a sibling temp file and rename into place atomically, so a
|
|
105
|
+
# download killed partway through never leaves a file that looks cached
|
|
106
|
+
# but isn't.
|
|
107
|
+
tmp_path = local_path.with_name(local_path.name + ".part")
|
|
108
|
+
print(f"scigantic-bindingdb: caching {key} ...", file=sys.stderr, flush=True)
|
|
109
|
+
with urllib.request.urlopen(url) as response, open(tmp_path, "wb") as fh:
|
|
110
|
+
while chunk := response.read(_CHUNK_BYTES):
|
|
111
|
+
fh.write(chunk)
|
|
112
|
+
os.replace(tmp_path, local_path)
|
|
113
|
+
return str(local_path)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Cross-reference BindingDB measurements to ChEMBL, through the precomputed
|
|
2
|
+
bridge table rather than re-deriving the join yourself.
|
|
3
|
+
|
|
4
|
+
BindingDB ingests ChEMBL as one of its own curated source feeds (51.3% of
|
|
5
|
+
measurements in the 202608 release), and ChEMBL separately absorbs some
|
|
6
|
+
BindingDB patent-derived bioactivity data, so the two archives are not
|
|
7
|
+
independent corpora. derived/bindingdb_chembl_bridge.parquet, built once at
|
|
8
|
+
mirror time, joins measurements to s3://scigantic-chembl by BindingDB's own
|
|
9
|
+
chembl_id column where present (authoritative) and falls back to an exact
|
|
10
|
+
InChIKey match where it's missing. This module wraps that file, and
|
|
11
|
+
optionally reaches into the live scigantic-chembl mirror over the same
|
|
12
|
+
DuckDB connection for compound/target names: a read-only cross-bucket SQL
|
|
13
|
+
join at query time, not a mount-level dependency between the two archives.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
19
|
+
|
|
20
|
+
from ._constants import CHEMBL_BUCKET
|
|
21
|
+
from .cache import resolve as _resolve
|
|
22
|
+
from .connection import connect
|
|
23
|
+
from .releases import _require, latest
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
import pandas as pd
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def chembl_bridge(
|
|
30
|
+
release: str | None = None,
|
|
31
|
+
chembl_release: str = "chembl_37",
|
|
32
|
+
reactant_set_id: str | None = None,
|
|
33
|
+
with_names: bool = True,
|
|
34
|
+
limit: int | None = None,
|
|
35
|
+
) -> "pd.DataFrame":
|
|
36
|
+
"""BindingDB measurements joined to their ChEMBL cross-reference.
|
|
37
|
+
|
|
38
|
+
with_names (default True) also pulls in compound_chembl_id's pref_name
|
|
39
|
+
from s3://scigantic-chembl/<chembl_release>, a second public bucket read
|
|
40
|
+
over the same connection. Set False to skip that join and get just the
|
|
41
|
+
bridge table's own columns (reactant_set_id, chembl_molregno, chembl_id,
|
|
42
|
+
match_method) plus the measurement's SMILES.
|
|
43
|
+
|
|
44
|
+
release defaults to the manifest's current latest(). Only that release
|
|
45
|
+
is guaranteed to carry the bridge table; call releases() to check.
|
|
46
|
+
"""
|
|
47
|
+
release = release or latest()
|
|
48
|
+
_require(release, "chembl_bridge")
|
|
49
|
+
con = connect(release)
|
|
50
|
+
try:
|
|
51
|
+
bridge_path = _resolve(f"{release}/derived/bindingdb_chembl_bridge.parquet")
|
|
52
|
+
con.execute(
|
|
53
|
+
"CREATE OR REPLACE VIEW chembl_bridge AS "
|
|
54
|
+
f"SELECT * FROM read_parquet('{bridge_path}')"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
where: list[str] = []
|
|
58
|
+
params: list[str | int] = []
|
|
59
|
+
if reactant_set_id is not None:
|
|
60
|
+
where.append("b.reactant_set_id = ?")
|
|
61
|
+
params.append(reactant_set_id)
|
|
62
|
+
clause = f"WHERE {' AND '.join(where)}" if where else ""
|
|
63
|
+
|
|
64
|
+
if with_names:
|
|
65
|
+
mol_dict = f"s3://{CHEMBL_BUCKET}/{chembl_release}/parquet/molecule_dictionary.parquet"
|
|
66
|
+
sql = f"""
|
|
67
|
+
SELECT b.reactant_set_id, b.chembl_molregno, b.chembl_id, b.match_method,
|
|
68
|
+
m.ligand_smiles, d.pref_name AS chembl_pref_name
|
|
69
|
+
FROM chembl_bridge b
|
|
70
|
+
JOIN measurements m ON m.reactant_set_id = b.reactant_set_id
|
|
71
|
+
LEFT JOIN read_parquet('{mol_dict}') d ON d.molregno = b.chembl_molregno
|
|
72
|
+
{clause}
|
|
73
|
+
"""
|
|
74
|
+
else:
|
|
75
|
+
sql = f"""
|
|
76
|
+
SELECT b.reactant_set_id, b.chembl_molregno, b.chembl_id, b.match_method,
|
|
77
|
+
m.ligand_smiles
|
|
78
|
+
FROM chembl_bridge b
|
|
79
|
+
JOIN measurements m ON m.reactant_set_id = b.reactant_set_id
|
|
80
|
+
{clause}
|
|
81
|
+
"""
|
|
82
|
+
if limit is not None:
|
|
83
|
+
sql += " LIMIT ?"
|
|
84
|
+
params.append(int(limit))
|
|
85
|
+
return con.execute(sql, params).df()
|
|
86
|
+
finally:
|
|
87
|
+
con.close()
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Command-line interface: `scigantic-bindingdb info` and `scigantic-bindingdb query`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from typing import Sequence, cast
|
|
8
|
+
|
|
9
|
+
from .connection import query as run_query
|
|
10
|
+
from .releases import releases
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _cmd_info(_args: argparse.Namespace) -> int:
|
|
14
|
+
for info in releases():
|
|
15
|
+
derived = [
|
|
16
|
+
name
|
|
17
|
+
for name, present in (
|
|
18
|
+
("chembl_bridge", info.chembl_bridge),
|
|
19
|
+
("dti_pairs", info.dti_pairs),
|
|
20
|
+
)
|
|
21
|
+
if present
|
|
22
|
+
]
|
|
23
|
+
suffix = f" + {', '.join(derived)}" if derived else " (raw tables only)"
|
|
24
|
+
print(f"{info.release}{suffix}")
|
|
25
|
+
return 0
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _cmd_query(args: argparse.Namespace) -> int:
|
|
29
|
+
df = run_query(args.sql, release=args.release)
|
|
30
|
+
print(df.to_csv(sep="\t", index=False), end="")
|
|
31
|
+
return 0
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
35
|
+
parser = argparse.ArgumentParser(prog="scigantic-bindingdb")
|
|
36
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
37
|
+
|
|
38
|
+
info_parser = subparsers.add_parser(
|
|
39
|
+
"info", help="list mirrored releases and what each one supports"
|
|
40
|
+
)
|
|
41
|
+
info_parser.set_defaults(func=_cmd_info)
|
|
42
|
+
|
|
43
|
+
query_parser = subparsers.add_parser(
|
|
44
|
+
"query", help="run SQL against a release and print tab-separated output"
|
|
45
|
+
)
|
|
46
|
+
query_parser.add_argument("sql")
|
|
47
|
+
query_parser.add_argument("--release", default=None, help="defaults to the current release")
|
|
48
|
+
query_parser.set_defaults(func=_cmd_query)
|
|
49
|
+
|
|
50
|
+
args = parser.parse_args(argv)
|
|
51
|
+
return cast(int, args.func(args))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
if __name__ == "__main__":
|
|
55
|
+
sys.exit(main())
|