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.
Files changed (29) hide show
  1. scigantic_bindingdb-0.2.0/LICENSE +16 -0
  2. scigantic_bindingdb-0.2.0/PKG-INFO +153 -0
  3. scigantic_bindingdb-0.2.0/README.md +122 -0
  4. scigantic_bindingdb-0.2.0/pyproject.toml +53 -0
  5. scigantic_bindingdb-0.2.0/setup.cfg +4 -0
  6. scigantic_bindingdb-0.2.0/src/scigantic_bindingdb/__init__.py +41 -0
  7. scigantic_bindingdb-0.2.0/src/scigantic_bindingdb/__main__.py +8 -0
  8. scigantic_bindingdb-0.2.0/src/scigantic_bindingdb/_constants.py +7 -0
  9. scigantic_bindingdb-0.2.0/src/scigantic_bindingdb/cache.py +113 -0
  10. scigantic_bindingdb-0.2.0/src/scigantic_bindingdb/chembl_bridge.py +87 -0
  11. scigantic_bindingdb-0.2.0/src/scigantic_bindingdb/cli.py +55 -0
  12. scigantic_bindingdb-0.2.0/src/scigantic_bindingdb/connection.py +84 -0
  13. scigantic_bindingdb-0.2.0/src/scigantic_bindingdb/dti_pairs.py +74 -0
  14. scigantic_bindingdb-0.2.0/src/scigantic_bindingdb/measurements.py +79 -0
  15. scigantic_bindingdb-0.2.0/src/scigantic_bindingdb/py.typed +0 -0
  16. scigantic_bindingdb-0.2.0/src/scigantic_bindingdb/releases.py +105 -0
  17. scigantic_bindingdb-0.2.0/src/scigantic_bindingdb.egg-info/PKG-INFO +153 -0
  18. scigantic_bindingdb-0.2.0/src/scigantic_bindingdb.egg-info/SOURCES.txt +27 -0
  19. scigantic_bindingdb-0.2.0/src/scigantic_bindingdb.egg-info/dependency_links.txt +1 -0
  20. scigantic_bindingdb-0.2.0/src/scigantic_bindingdb.egg-info/entry_points.txt +2 -0
  21. scigantic_bindingdb-0.2.0/src/scigantic_bindingdb.egg-info/requires.txt +7 -0
  22. scigantic_bindingdb-0.2.0/src/scigantic_bindingdb.egg-info/top_level.txt +1 -0
  23. scigantic_bindingdb-0.2.0/tests/test_cache.py +107 -0
  24. scigantic_bindingdb-0.2.0/tests/test_chembl_bridge.py +31 -0
  25. scigantic_bindingdb-0.2.0/tests/test_cli.py +16 -0
  26. scigantic_bindingdb-0.2.0/tests/test_connection.py +33 -0
  27. scigantic_bindingdb-0.2.0/tests/test_dti_pairs.py +60 -0
  28. scigantic_bindingdb-0.2.0/tests/test_measurements.py +48 -0
  29. 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,8 @@
1
+ """Enables `python -m scigantic_bindingdb`, same commands as the `scigantic-bindingdb` console script."""
2
+
3
+ import sys
4
+
5
+ from .cli import main
6
+
7
+ if __name__ == "__main__":
8
+ sys.exit(main())
@@ -0,0 +1,7 @@
1
+ """Shared constants: where the mirror lives."""
2
+
3
+ BUCKET = "scigantic-bindingdb"
4
+ REGION = "us-east-1"
5
+
6
+ # The scigantic-chembl mirror this package's chembl_bridge() cross-references.
7
+ CHEMBL_BUCKET = "scigantic-chembl"
@@ -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())