scigantic-bindingdb 0.2.0__py3-none-any.whl
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/__init__.py +41 -0
- scigantic_bindingdb/__main__.py +8 -0
- scigantic_bindingdb/_constants.py +7 -0
- scigantic_bindingdb/cache.py +113 -0
- scigantic_bindingdb/chembl_bridge.py +87 -0
- scigantic_bindingdb/cli.py +55 -0
- scigantic_bindingdb/connection.py +84 -0
- scigantic_bindingdb/dti_pairs.py +74 -0
- scigantic_bindingdb/measurements.py +79 -0
- scigantic_bindingdb/py.typed +0 -0
- scigantic_bindingdb/releases.py +105 -0
- scigantic_bindingdb-0.2.0.dist-info/METADATA +153 -0
- scigantic_bindingdb-0.2.0.dist-info/RECORD +17 -0
- scigantic_bindingdb-0.2.0.dist-info/WHEEL +5 -0
- scigantic_bindingdb-0.2.0.dist-info/entry_points.txt +2 -0
- scigantic_bindingdb-0.2.0.dist-info/licenses/LICENSE +16 -0
- scigantic_bindingdb-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -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())
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""DuckDB connection helpers. Queries run against the public S3 mirror over
|
|
2
|
+
httpfs, no download.
|
|
3
|
+
|
|
4
|
+
This module deliberately does not participate in enable_cache() (see
|
|
5
|
+
cache.py): connect() registers all five core tables as views on every call,
|
|
6
|
+
the largest over 200 MB, so caching them here would mean any call to
|
|
7
|
+
connect() or query() eagerly downloads everything regardless of what the
|
|
8
|
+
query actually touches. Caching applies to chembl_bridge() instead, which
|
|
9
|
+
needs exactly one known file.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import TYPE_CHECKING
|
|
15
|
+
|
|
16
|
+
from ._constants import BUCKET, REGION
|
|
17
|
+
from .releases import _validate_release, latest
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
import duckdb
|
|
21
|
+
import pandas as pd
|
|
22
|
+
|
|
23
|
+
# Registered as views on connect() so plain SQL can reference a table by
|
|
24
|
+
# name instead of a full read_parquet() path.
|
|
25
|
+
_CORE_TABLES = (
|
|
26
|
+
"measurements",
|
|
27
|
+
"target_chains",
|
|
28
|
+
"target_chain_names",
|
|
29
|
+
"assays",
|
|
30
|
+
"id_mappings",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def connect(release: str | None = None) -> "duckdb.DuckDBPyConnection":
|
|
35
|
+
"""Open a DuckDB connection against s3://scigantic-bindingdb.
|
|
36
|
+
|
|
37
|
+
`SELECT * FROM measurements` works directly; any other table under
|
|
38
|
+
`<release>/parquet/` is reachable with
|
|
39
|
+
`read_parquet('s3://scigantic-bindingdb/<release>/parquet/<table>.parquet')`.
|
|
40
|
+
|
|
41
|
+
release defaults to whatever the live manifest currently calls latest(),
|
|
42
|
+
resolved at call time rather than import time.
|
|
43
|
+
"""
|
|
44
|
+
import duckdb
|
|
45
|
+
|
|
46
|
+
release = release or latest()
|
|
47
|
+
_validate_release(release)
|
|
48
|
+
|
|
49
|
+
con = duckdb.connect()
|
|
50
|
+
# DuckDB auto-shows an ASCII progress bar for queries it estimates will
|
|
51
|
+
# take a while, on stdout, regardless of whether that's a real terminal.
|
|
52
|
+
# Surprising output for a library call in a notebook or script, so it's
|
|
53
|
+
# off here by default.
|
|
54
|
+
con.execute("SET enable_progress_bar=false")
|
|
55
|
+
con.execute("INSTALL httpfs")
|
|
56
|
+
con.execute("LOAD httpfs")
|
|
57
|
+
con.execute(f"SET s3_region='{REGION}'")
|
|
58
|
+
# The mirror is public-read. Without this, DuckDB looks for AWS
|
|
59
|
+
# credentials and fails on a machine that has none configured.
|
|
60
|
+
con.execute(
|
|
61
|
+
"CREATE OR REPLACE SECRET scigantic_bindingdb "
|
|
62
|
+
"(TYPE s3, PROVIDER config, KEY_ID '', SECRET '')"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
base = f"s3://{BUCKET}/{release}/parquet"
|
|
66
|
+
for table in _CORE_TABLES:
|
|
67
|
+
con.execute(
|
|
68
|
+
f"CREATE OR REPLACE VIEW {table} AS "
|
|
69
|
+
f"SELECT * FROM read_parquet('{base}/{table}.parquet')"
|
|
70
|
+
)
|
|
71
|
+
return con
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def query(sql: str, release: str | None = None) -> "pd.DataFrame":
|
|
75
|
+
"""Run SQL against a release and return a pandas DataFrame.
|
|
76
|
+
|
|
77
|
+
Opens a new connection per call. For several queries against the same
|
|
78
|
+
release, call connect() once and reuse it instead.
|
|
79
|
+
"""
|
|
80
|
+
con = connect(release)
|
|
81
|
+
try:
|
|
82
|
+
return con.execute(sql).df()
|
|
83
|
+
finally:
|
|
84
|
+
con.close()
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""(Ligand, target, affinity) triples, ready for a drug-target-interaction
|
|
2
|
+
or proteochemometric model to train on.
|
|
3
|
+
|
|
4
|
+
BindingDB is the dataset most DTI tooling (DeepPurpose and similar) is built
|
|
5
|
+
around, specifically because it ships a full protein sequence alongside
|
|
6
|
+
every affinity measurement, something ChEMBL's bioactivity tables don't
|
|
7
|
+
do as directly. This wraps derived/dti_pairs.parquet, the pre-filtered,
|
|
8
|
+
pre-transformed version of that pairing: exact measurements only (a
|
|
9
|
+
censored ">"/"<" bound is never a usable regression label), chain 1's
|
|
10
|
+
sequence required, and p_affinity already computed as -log10(affinity_nm *
|
|
11
|
+
1e-9), the same transform as ChEMBL's pchembl_value.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import TYPE_CHECKING, Literal
|
|
17
|
+
|
|
18
|
+
from .cache import resolve as _resolve
|
|
19
|
+
from .connection import connect
|
|
20
|
+
from .releases import _require, latest
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
import pandas as pd
|
|
24
|
+
|
|
25
|
+
Endpoint = Literal["ki", "ic50", "kd", "ec50"]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def dti_pairs(
|
|
29
|
+
release: str | None = None,
|
|
30
|
+
endpoint: Endpoint | None = None,
|
|
31
|
+
uniprot_id: str | None = None,
|
|
32
|
+
single_chain_only: bool = False,
|
|
33
|
+
limit: int | None = None,
|
|
34
|
+
) -> "pd.DataFrame":
|
|
35
|
+
"""Ligand/target/affinity triples for DTI or proteochemometric training.
|
|
36
|
+
|
|
37
|
+
endpoint filters to one of 'ki', 'ic50', 'kd', 'ec50'; omit to get all
|
|
38
|
+
four (a row's `endpoint` column says which).
|
|
39
|
+
|
|
40
|
+
single_chain_only=True drops rows whose target came from a complex with
|
|
41
|
+
more than one declared chain (n_chains_declared > 1). Those rows still
|
|
42
|
+
represent the interaction with chain 1's sequence only, which is fine
|
|
43
|
+
for most uses, but exclude them if single-chain purity matters for your
|
|
44
|
+
model.
|
|
45
|
+
|
|
46
|
+
release defaults to the manifest's current latest(). Only that release
|
|
47
|
+
is guaranteed to carry this file; call releases() to check.
|
|
48
|
+
"""
|
|
49
|
+
release = release or latest()
|
|
50
|
+
_require(release, "dti_pairs")
|
|
51
|
+
con = connect(release)
|
|
52
|
+
try:
|
|
53
|
+
path = _resolve(f"{release}/derived/dti_pairs.parquet")
|
|
54
|
+
con.execute(f"CREATE OR REPLACE VIEW dti_pairs AS SELECT * FROM read_parquet('{path}')")
|
|
55
|
+
|
|
56
|
+
where: list[str] = []
|
|
57
|
+
params: list[str | int] = []
|
|
58
|
+
if endpoint is not None:
|
|
59
|
+
where.append("endpoint = ?")
|
|
60
|
+
params.append(endpoint)
|
|
61
|
+
if uniprot_id is not None:
|
|
62
|
+
where.append("uniprot_id = ?")
|
|
63
|
+
params.append(uniprot_id)
|
|
64
|
+
if single_chain_only:
|
|
65
|
+
where.append("n_chains_declared = 1")
|
|
66
|
+
clause = f"WHERE {' AND '.join(where)}" if where else ""
|
|
67
|
+
|
|
68
|
+
sql = f"SELECT * FROM dti_pairs {clause} ORDER BY p_affinity DESC"
|
|
69
|
+
if limit is not None:
|
|
70
|
+
sql += " LIMIT ?"
|
|
71
|
+
params.append(int(limit))
|
|
72
|
+
return con.execute(sql, params).df()
|
|
73
|
+
finally:
|
|
74
|
+
con.close()
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Binding measurements, filtered the way that avoids BindingDB's two
|
|
2
|
+
sharpest edges: censored affinity values, and target joins that fan out on
|
|
3
|
+
multichain complexes.
|
|
4
|
+
|
|
5
|
+
Ki/IC50/Kd/EC50 are occasionally reported as ">X" or "<X" rather than an
|
|
6
|
+
exact value (an assay hit its ceiling or floor). exact_only=True, the
|
|
7
|
+
default, keeps only rows where that endpoint's qualifier is '=', so a
|
|
8
|
+
censored bound is never silently treated as a real affinity.
|
|
9
|
+
|
|
10
|
+
Filtering by target uses EXISTS against target_chains rather than a JOIN,
|
|
11
|
+
so a target whose UniProt ID appears on more than one chain of the same
|
|
12
|
+
complex still returns each measurement once, not once per matching chain.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import TYPE_CHECKING, Literal
|
|
18
|
+
|
|
19
|
+
from .connection import connect
|
|
20
|
+
from .releases import latest
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
import pandas as pd
|
|
24
|
+
|
|
25
|
+
Endpoint = Literal["ki", "ic50", "kd", "ec50"]
|
|
26
|
+
_ENDPOINTS: tuple[Endpoint, ...] = ("ki", "ic50", "kd", "ec50")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def measurements(
|
|
30
|
+
release: str | None = None,
|
|
31
|
+
uniprot_id: str | None = None,
|
|
32
|
+
endpoint: Endpoint = "ki",
|
|
33
|
+
exact_only: bool = True,
|
|
34
|
+
limit: int | None = None,
|
|
35
|
+
) -> "pd.DataFrame":
|
|
36
|
+
"""Binding measurements for one endpoint, most potent first.
|
|
37
|
+
|
|
38
|
+
uniprot_id matches either UniProt (SwissProt or TrEMBL) primary ID on
|
|
39
|
+
any chain of the target. A multichain complex needs the accession on
|
|
40
|
+
only one chain to match, and matches once regardless of how many chains
|
|
41
|
+
it appears on.
|
|
42
|
+
|
|
43
|
+
exact_only (default True) keeps only rows where this endpoint's
|
|
44
|
+
qualifier is '='. Set False to also see censored ">"/"<" bounds,
|
|
45
|
+
which come back with their qualifier column intact rather than a bare
|
|
46
|
+
number that looks exact but isn't.
|
|
47
|
+
|
|
48
|
+
release defaults to the manifest's current latest().
|
|
49
|
+
"""
|
|
50
|
+
if endpoint not in _ENDPOINTS:
|
|
51
|
+
raise ValueError(f"endpoint must be one of {_ENDPOINTS}, got {endpoint!r}")
|
|
52
|
+
release = release or latest()
|
|
53
|
+
value_col, qual_col = f"{endpoint}_nm_value", f"{endpoint}_nm_qualifier"
|
|
54
|
+
|
|
55
|
+
con = connect(release)
|
|
56
|
+
try:
|
|
57
|
+
where: list[str] = [f"{value_col} IS NOT NULL"]
|
|
58
|
+
params: list[str | int] = []
|
|
59
|
+
if exact_only:
|
|
60
|
+
where.append(f"{qual_col} = '='")
|
|
61
|
+
if uniprot_id is not None:
|
|
62
|
+
where.append(
|
|
63
|
+
"EXISTS (SELECT 1 FROM target_chains c WHERE "
|
|
64
|
+
"c.reactant_set_id = measurements.reactant_set_id AND "
|
|
65
|
+
"(c.uniprot_swissprot_primary_id = ? OR c.uniprot_trembl_primary_id = ?))"
|
|
66
|
+
)
|
|
67
|
+
params.extend([uniprot_id, uniprot_id])
|
|
68
|
+
|
|
69
|
+
sql = (
|
|
70
|
+
"SELECT * FROM measurements "
|
|
71
|
+
f"WHERE {' AND '.join(where)} "
|
|
72
|
+
f"ORDER BY {value_col} ASC"
|
|
73
|
+
)
|
|
74
|
+
if limit is not None:
|
|
75
|
+
sql += " LIMIT ?"
|
|
76
|
+
params.append(int(limit))
|
|
77
|
+
return con.execute(sql, params).df()
|
|
78
|
+
finally:
|
|
79
|
+
con.close()
|
|
File without changes
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Release metadata: what's mirrored, and what each release supports.
|
|
2
|
+
|
|
3
|
+
Reads a small manifest at s3://scigantic-bindingdb/_MANIFEST.json. Unlike
|
|
4
|
+
scigantic-chembl's manifest, this one is not yet regenerated by a cron --
|
|
5
|
+
BindingDB releases monthly and re-mirroring here is still a manual step, so
|
|
6
|
+
the manifest is hand-published alongside each mirror run. The mechanism is
|
|
7
|
+
kept identical to scigantic-chembl's on purpose, so a cron can be added later
|
|
8
|
+
without changing this module or anything that calls it.
|
|
9
|
+
|
|
10
|
+
The manifest is fetched once per process and cached. If it can't be fetched
|
|
11
|
+
(no network, a bucket hiccup), calls fall back to the snapshot below rather
|
|
12
|
+
than failing outright: worst case is that the package is only as fresh as
|
|
13
|
+
this package version, not broken.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import urllib.error
|
|
20
|
+
import urllib.request
|
|
21
|
+
import warnings
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from typing import Any, cast
|
|
24
|
+
|
|
25
|
+
from ._constants import BUCKET, REGION
|
|
26
|
+
|
|
27
|
+
_MANIFEST_URL = f"https://{BUCKET}.s3.{REGION}.amazonaws.com/_MANIFEST.json"
|
|
28
|
+
_TIMEOUT_SECONDS = 5
|
|
29
|
+
|
|
30
|
+
# Last-known-good snapshot, shipped with this package version. Only used if
|
|
31
|
+
# the live manifest can't be fetched.
|
|
32
|
+
_FALLBACK_LATEST = "202608"
|
|
33
|
+
_FALLBACK_RELEASES = {
|
|
34
|
+
"202608": {"raw": True, "chembl_bridge": True, "dti_pairs": True},
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class UnknownReleaseError(LookupError):
|
|
39
|
+
"""Raised when a release isn't mirrored at all."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ReleaseCapabilityError(LookupError):
|
|
43
|
+
"""Raised when a release doesn't carry the artifact a call asked for."""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class ReleaseInfo:
|
|
48
|
+
release: str
|
|
49
|
+
raw: bool
|
|
50
|
+
chembl_bridge: bool
|
|
51
|
+
dti_pairs: bool
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
_cache: dict[str, Any] | None = None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _manifest() -> dict[str, Any]:
|
|
58
|
+
global _cache
|
|
59
|
+
if _cache is not None:
|
|
60
|
+
return _cache
|
|
61
|
+
try:
|
|
62
|
+
with urllib.request.urlopen(_MANIFEST_URL, timeout=_TIMEOUT_SECONDS) as response:
|
|
63
|
+
_cache = json.loads(response.read())
|
|
64
|
+
except (urllib.error.URLError, TimeoutError, ValueError) as exc:
|
|
65
|
+
# HTTPError (a URLError subclass) carries an open response body; left
|
|
66
|
+
# unclosed it raises its own ResourceWarning on garbage collection,
|
|
67
|
+
# on top of the one we're about to emit deliberately below.
|
|
68
|
+
if isinstance(exc, urllib.error.HTTPError):
|
|
69
|
+
exc.close()
|
|
70
|
+
warnings.warn(
|
|
71
|
+
f"could not fetch the live release manifest ({exc!r}); falling back "
|
|
72
|
+
"to the snapshot shipped with this package version, which may be stale",
|
|
73
|
+
stacklevel=3,
|
|
74
|
+
)
|
|
75
|
+
_cache = {"latest": _FALLBACK_LATEST, "releases": _FALLBACK_RELEASES}
|
|
76
|
+
return _cache
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def releases() -> list[ReleaseInfo]:
|
|
80
|
+
"""List every release the mirror carries, and what each one supports."""
|
|
81
|
+
data = _manifest()
|
|
82
|
+
return [ReleaseInfo(release=name, **caps) for name, caps in data["releases"].items()]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def latest() -> str:
|
|
86
|
+
"""The release the archive treats as its current default."""
|
|
87
|
+
return cast(str, _manifest()["latest"])
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _validate_release(release: str) -> None:
|
|
91
|
+
data = _manifest()
|
|
92
|
+
if release not in data["releases"]:
|
|
93
|
+
known = ", ".join(data["releases"])
|
|
94
|
+
raise UnknownReleaseError(f"{release!r} is not mirrored. Known releases: {known}.")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _require(release: str, capability: str) -> None:
|
|
98
|
+
_validate_release(release)
|
|
99
|
+
data = _manifest()
|
|
100
|
+
if not data["releases"][release][capability]:
|
|
101
|
+
raise ReleaseCapabilityError(
|
|
102
|
+
f"{release!r} has no {capability.replace('_', ' ')}. Call releases() to "
|
|
103
|
+
"see what each release supports, or use query() against the raw "
|
|
104
|
+
"parquet tables instead."
|
|
105
|
+
)
|
|
@@ -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,17 @@
|
|
|
1
|
+
scigantic_bindingdb/__init__.py,sha256=Mi46LufFt4rdagj_ng8Y9tGMA8bffiAoDAu4-ad0tM4,1050
|
|
2
|
+
scigantic_bindingdb/__main__.py,sha256=rMnNg3rwz8DD2ZVM0ylfdeYfSLyHULSVg4aICcCN5eQ,190
|
|
3
|
+
scigantic_bindingdb/_constants.py,sha256=8EJUA6yIHAILlf1W8lnO45HkqW_qZwYX_OjiAUeUN88,216
|
|
4
|
+
scigantic_bindingdb/cache.py,sha256=2NPs0A_BCcgSYgSChrdw6kIrMLKZsxa7TP88-6awNnE,3802
|
|
5
|
+
scigantic_bindingdb/chembl_bridge.py,sha256=Oi2R1tLprKZ3jlx7igxHFJpKtjTGsSn6WMYENdDgiyw,3577
|
|
6
|
+
scigantic_bindingdb/cli.py,sha256=BOTUtDcFah2vE-m6JemcoGZmhMx0_ZPA6OQ3v2-_19A,1655
|
|
7
|
+
scigantic_bindingdb/connection.py,sha256=V4SlUJ0Ys_ZFBAH_G3ZTArsQCHNnTcSDlSo7nzNpc28,2844
|
|
8
|
+
scigantic_bindingdb/dti_pairs.py,sha256=Hurw3UQtTseVDClHoJjRmehyz_lGpg2vq4ibP9YSRCE,2790
|
|
9
|
+
scigantic_bindingdb/measurements.py,sha256=uOLOKHsKT9y6VJw3-sLoVd4a-e6EBBVdqwtbNWmjvp8,2897
|
|
10
|
+
scigantic_bindingdb/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
scigantic_bindingdb/releases.py,sha256=0kbGo2wTKB6hvBli5fQsmihqy59Nc8qEfBeUa0syi9k,3652
|
|
12
|
+
scigantic_bindingdb-0.2.0.dist-info/licenses/LICENSE,sha256=RiBhEtE0qZ65CeSf05VqMHWAhMh0ETETZAj4TEtscWE,904
|
|
13
|
+
scigantic_bindingdb-0.2.0.dist-info/METADATA,sha256=vymmP5sGoetAJeOg-l_GMA4slyq28WWfVaWa_nncBog,8419
|
|
14
|
+
scigantic_bindingdb-0.2.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
15
|
+
scigantic_bindingdb-0.2.0.dist-info/entry_points.txt,sha256=IuFxRQdzRACD7prBaS_gRgcfWFRK7RdJAynBc2eMU0o,69
|
|
16
|
+
scigantic_bindingdb-0.2.0.dist-info/top_level.txt,sha256=1FMpDQXbzsrl91ICHFlMrUKUH6iE-Cew56k6wt1VRKk,20
|
|
17
|
+
scigantic_bindingdb-0.2.0.dist-info/RECORD,,
|
|
@@ -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 @@
|
|
|
1
|
+
scigantic_bindingdb
|