fairfetched 0.0.1.dev202607271__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.
- fairfetched/__init__.py +0 -0
- fairfetched/get/__init__.py +9 -0
- fairfetched/get/chembl.py +219 -0
- fairfetched/get/dataset.py +167 -0
- fairfetched/get/papyrus.py +156 -0
- fairfetched/standardize/___mol_expr.pyi +23 -0
- fairfetched/standardize/__init__.py +0 -0
- fairfetched/standardize/_optional.py +56 -0
- fairfetched/standardize/compound_fns.py +340 -0
- fairfetched/standardize/mol_expr.py +302 -0
- fairfetched/standardize/mol_functions.py +156 -0
- fairfetched/standardize/pipe.py +432 -0
- fairfetched/standardize/pipeline.py +31 -0
- fairfetched/utils/__init__.py +18 -0
- fairfetched/utils/_optional.py +98 -0
- fairfetched/utils/_track.py +31 -0
- fairfetched/utils/ensure.py +44 -0
- fairfetched/utils/files.py +48 -0
- fairfetched/utils/polars.py +369 -0
- fairfetched/utils/storage.py +18 -0
- fairfetched/utils/typing.py +75 -0
- fairfetched/vis.py +92 -0
- fairfetched-0.0.1.dev202607271.dist-info/METADATA +153 -0
- fairfetched-0.0.1.dev202607271.dist-info/RECORD +34 -0
- fairfetched-0.0.1.dev202607271.dist-info/WHEEL +5 -0
- fairfetched-0.0.1.dev202607271.dist-info/licenses/LICENSE +21 -0
- fairfetched-0.0.1.dev202607271.dist-info/top_level.txt +3 -0
- scripts/chembl.py +7 -0
- tests/__init__.py +1 -0
- tests/test_ensure.py +159 -0
- tests/test_get.py +1085 -0
- tests/test_integration_chembl.py +285 -0
- tests/test_standardize.py +190 -0
- tests/test_utils.py +162 -0
fairfetched/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import polars as pl
|
|
6
|
+
|
|
7
|
+
from fairfetched.utils import (
|
|
8
|
+
BASE_DIR,
|
|
9
|
+
ensure_sqlite_db_to_parquets,
|
|
10
|
+
ensure_untarred_sqlite,
|
|
11
|
+
ensure_url,
|
|
12
|
+
file_suffix_from_url,
|
|
13
|
+
lowercase_columns,
|
|
14
|
+
)
|
|
15
|
+
from fairfetched.utils.typing import BioactivityDBViews
|
|
16
|
+
|
|
17
|
+
CHEMBL_DIR = BASE_DIR / "chembl"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _format_version(version: float | str) -> str:
|
|
21
|
+
if isinstance(version, int | float):
|
|
22
|
+
version = str(version)
|
|
23
|
+
if isinstance(version, Sequence) and not isinstance(version, str):
|
|
24
|
+
raise TypeError(f"invalid version type: {type(version)}")
|
|
25
|
+
if not isinstance(version, str):
|
|
26
|
+
try:
|
|
27
|
+
version = str(version)
|
|
28
|
+
except Exception:
|
|
29
|
+
raise TypeError(f"invalid version type: {type(version)}")
|
|
30
|
+
|
|
31
|
+
version = version.lstrip("0")
|
|
32
|
+
if "." in version:
|
|
33
|
+
version = version.split(".")[0].zfill(2) + "." + version.split(".")[1]
|
|
34
|
+
# for canonicalize the version number 22.1 and 24.1 and left pad with a zero if needed
|
|
35
|
+
return version.replace(".", "_").replace("_0", "").zfill(2)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _version_to_url(version: str):
|
|
39
|
+
base = "https://ftp.ebi.ac.uk/pub/databases/chembl/ChEMBLdb/releases"
|
|
40
|
+
return f"{base}/chembl_{version}/chembl_{version}_sqlite.tar.gz"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
CHEMBL_VERSIONS: dict[str, dict[str, str]] = {
|
|
44
|
+
version: {"sql_db": _version_to_url(version)}
|
|
45
|
+
for version in sorted(map(_format_version, list(range(1, 38)) + ["24_1", "22_1"]))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def available_versions() -> tuple[str, ...]:
|
|
50
|
+
return tuple(CHEMBL_VERSIONS.keys())
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def latest() -> str:
|
|
54
|
+
return available_versions()[-1]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def source_urls(version: str) -> dict[str, str]:
|
|
58
|
+
return CHEMBL_VERSIONS[str(version)]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def ensure_raw_files(
|
|
62
|
+
version: str, raw_dir: Path | str | None = None, force=False
|
|
63
|
+
) -> dict[str, Path]:
|
|
64
|
+
"""Download the original SQL database with its original name and compression."""
|
|
65
|
+
if raw_dir is None:
|
|
66
|
+
raw_dir = CHEMBL_DIR / version
|
|
67
|
+
raw_dir = Path(raw_dir)
|
|
68
|
+
return {
|
|
69
|
+
name: ensure_url(
|
|
70
|
+
url=url, path=raw_dir / f"{name}{file_suffix_from_url(url)}", force=force
|
|
71
|
+
)
|
|
72
|
+
for name, url in source_urls(version).items()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def ensure_parquet_tables(
|
|
77
|
+
raw_paths: dict[str, Path], table_dir: Path | str | Any | None = None
|
|
78
|
+
) -> dict[str, Path]:
|
|
79
|
+
sql_tar_gz_path = raw_paths["sql_db"]
|
|
80
|
+
if table_dir is None:
|
|
81
|
+
table_dir = Path(sql_tar_gz_path).parent / "extracted"
|
|
82
|
+
table_dir = Path(table_dir)
|
|
83
|
+
table_dir.mkdir(exist_ok=True, parents=True)
|
|
84
|
+
|
|
85
|
+
raw_sql = ensure_untarred_sqlite(sql_tar_gz_path)
|
|
86
|
+
# the untarred should also stay so that we have access.....
|
|
87
|
+
# #@TODO: perhaps make tables deterministic for chembl to circumvent
|
|
88
|
+
parquets = ensure_sqlite_db_to_parquets(raw_sql, cache_dir=table_dir, force=False)
|
|
89
|
+
|
|
90
|
+
return parquets
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def cleanly_scan_parquet(path_: Path | str) -> pl.LazyFrame:
|
|
94
|
+
"""scans parquet paths and lazily handles null value conversion to None"""
|
|
95
|
+
return (
|
|
96
|
+
pl.scan_parquet(path_)
|
|
97
|
+
.pipe(lowercase_columns)
|
|
98
|
+
.fill_nan(None)
|
|
99
|
+
.with_columns(
|
|
100
|
+
pl.col(pl.String).replace({"": None}),
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def cleanly_scan_parquet_tables(
|
|
106
|
+
parquet_paths: dict[str, Path],
|
|
107
|
+
) -> dict[str, pl.LazyFrame]:
|
|
108
|
+
"""scans parquet paths and lazily handles null value conversion to None"""
|
|
109
|
+
return {name: cleanly_scan_parquet(path_) for name, path_ in parquet_paths.items()}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# def build_views(lfs: dict[str, pl.LazyFrame]) -> ComposedLFDict:
|
|
113
|
+
def build_views(parquet_paths: dict[str, Path]) -> BioactivityDBViews:
|
|
114
|
+
"""Build joined domain views from the scanned source tables."""
|
|
115
|
+
return {
|
|
116
|
+
"bioactivity": _bioactivities(parquet_paths),
|
|
117
|
+
"compounds": _compounds(parquet_paths),
|
|
118
|
+
"proteins": cleanly_scan_parquet(parquet_paths["protein"]),
|
|
119
|
+
"components": _components(parquet_paths),
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _bioactivities(parquet_paths: dict[str, Path]) -> pl.LazyFrame:
|
|
124
|
+
lfs = cleanly_scan_parquet_tables(parquet_paths)
|
|
125
|
+
return (
|
|
126
|
+
# dfs["activity_properties"]
|
|
127
|
+
# .join(dfs["activities"], on="activity_id", how="left")
|
|
128
|
+
# .join(dfs["action_type"], on="action_type", how="left")
|
|
129
|
+
# .join
|
|
130
|
+
lfs["bioactivity"]
|
|
131
|
+
.join(
|
|
132
|
+
lfs["protein"],
|
|
133
|
+
on="target_id",
|
|
134
|
+
how="left",
|
|
135
|
+
maintain_order="left",
|
|
136
|
+
validate="m:1", # one unique protein only from right, can reoccur within compounds.
|
|
137
|
+
)
|
|
138
|
+
.join(lfs["action_type"], on="action_type", how="left", suffix="_action_type")
|
|
139
|
+
.join(
|
|
140
|
+
lfs["assays"], on="assay_id", how="left", suffix="_assay"
|
|
141
|
+
) # doc_ids to this
|
|
142
|
+
.join(lfs["assay_type"], on="assay_type", how="left", suffix="_assay_type")
|
|
143
|
+
# .join(lfs, on="bao_format")
|
|
144
|
+
# .join(dfs[])
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _components(parquet_paths: dict[str, Path]) -> pl.LazyFrame:
|
|
149
|
+
lfs = cleanly_scan_parquet_tables(parquet_paths)
|
|
150
|
+
return (
|
|
151
|
+
lfs["component_sequences"]
|
|
152
|
+
.join(
|
|
153
|
+
lfs["component_class"],
|
|
154
|
+
on="component_id",
|
|
155
|
+
how="left",
|
|
156
|
+
suffix="_class",
|
|
157
|
+
validate="1:m",
|
|
158
|
+
)
|
|
159
|
+
.join(
|
|
160
|
+
lfs["component_domains"].join(
|
|
161
|
+
lfs["domains"],
|
|
162
|
+
on="domain_id",
|
|
163
|
+
how="left",
|
|
164
|
+
suffix="_domains",
|
|
165
|
+
validate="m:1",
|
|
166
|
+
),
|
|
167
|
+
on="component_id",
|
|
168
|
+
how="left",
|
|
169
|
+
suffix="_domains",
|
|
170
|
+
validate="1:m",
|
|
171
|
+
)
|
|
172
|
+
# .join(
|
|
173
|
+
# dfs["component_synonyms"],
|
|
174
|
+
# on="component_id",
|
|
175
|
+
# how="left",
|
|
176
|
+
# suffix="_synonyms",
|
|
177
|
+
# validate="m:m",
|
|
178
|
+
# )
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _compounds(parquet_paths: dict[str, Path]) -> pl.LazyFrame:
|
|
183
|
+
lfs = cleanly_scan_parquet_tables(parquet_paths)
|
|
184
|
+
return (
|
|
185
|
+
lfs["molecule_dictionary"]
|
|
186
|
+
.join(
|
|
187
|
+
lfs["compound_properties"],
|
|
188
|
+
on="molregno",
|
|
189
|
+
how="left",
|
|
190
|
+
suffix="_compound_properties",
|
|
191
|
+
validate="1:1",
|
|
192
|
+
)
|
|
193
|
+
.join(
|
|
194
|
+
lfs["compound_structures"],
|
|
195
|
+
on="molregno",
|
|
196
|
+
how="left",
|
|
197
|
+
suffix="_compound_structures",
|
|
198
|
+
validate="1:1",
|
|
199
|
+
)
|
|
200
|
+
.join(
|
|
201
|
+
lfs["compound_records"].join(
|
|
202
|
+
lfs["docs"], on="doc_id", how="left", suffix="_doc", validate="m:1"
|
|
203
|
+
),
|
|
204
|
+
on="molregno",
|
|
205
|
+
how="left",
|
|
206
|
+
suffix="_compound_records",
|
|
207
|
+
validate="1:m",
|
|
208
|
+
)
|
|
209
|
+
.join(
|
|
210
|
+
lfs["compound_structural_alerts"],
|
|
211
|
+
# .join(
|
|
212
|
+
# dfs["docs"], on="doc_id", how="left", suffix="_doc",validate="m:1"
|
|
213
|
+
# )
|
|
214
|
+
on="molregno",
|
|
215
|
+
how="left",
|
|
216
|
+
suffix="_compound_structural_alerts",
|
|
217
|
+
validate="1:m",
|
|
218
|
+
)
|
|
219
|
+
)
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from functools import cached_property
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from polars import LazyFrame
|
|
6
|
+
|
|
7
|
+
from fairfetched.get import chembl, papyrus
|
|
8
|
+
from fairfetched.utils import BASE_DIR
|
|
9
|
+
from fairfetched.utils.typing import BioactivityDBViews, DatasetGetModule
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class _Base:
|
|
14
|
+
"""lightweight wrapper serving as the main point for a united API per database"""
|
|
15
|
+
|
|
16
|
+
version: str
|
|
17
|
+
raw_paths: dict[str, Path]
|
|
18
|
+
parquet_paths: dict[str, Path]
|
|
19
|
+
dir: Path
|
|
20
|
+
module: DatasetGetModule
|
|
21
|
+
|
|
22
|
+
def __str__(self) -> str:
|
|
23
|
+
return f"{self.name}_{self.version}"
|
|
24
|
+
|
|
25
|
+
def __repr__(self) -> str:
|
|
26
|
+
return f"<{self.name.capitalize()}_{self.version} at {self.dir}"
|
|
27
|
+
|
|
28
|
+
def __hash__(self):
|
|
29
|
+
return hash(
|
|
30
|
+
(self.version, str(self.sources), str(self.raw_paths), self.module.__name__)
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
@cached_property
|
|
34
|
+
def name(self) -> str:
|
|
35
|
+
return self.module.__name__.split(".")[-1]
|
|
36
|
+
|
|
37
|
+
@cached_property
|
|
38
|
+
def sources(self) -> dict[str, str]:
|
|
39
|
+
|
|
40
|
+
sources = self.module.source_urls(self.version)
|
|
41
|
+
return sources
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def lfs(self) -> dict[str, LazyFrame]:
|
|
45
|
+
return self.module.cleanly_scan_parquet_tables(self.parquet_paths)
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def views(self) -> BioactivityDBViews:
|
|
49
|
+
return self.module.build_views(self.parquet_paths)
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def bioactivity(self) -> LazyFrame:
|
|
53
|
+
return self.views["bioactivity"]
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def compounds(self) -> LazyFrame:
|
|
57
|
+
return self.views["compounds"]
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def available_versions(cls) -> tuple[str, ...]:
|
|
61
|
+
return cls.module.available_versions()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True)
|
|
65
|
+
class Chembl(_Base):
|
|
66
|
+
module: DatasetGetModule = chembl
|
|
67
|
+
|
|
68
|
+
@staticmethod
|
|
69
|
+
def get_available_versions():
|
|
70
|
+
return chembl.available_versions()
|
|
71
|
+
|
|
72
|
+
@cached_property
|
|
73
|
+
def activity(self) -> LazyFrame:
|
|
74
|
+
return self.views["bioactivity"]
|
|
75
|
+
|
|
76
|
+
@cached_property
|
|
77
|
+
def raw_sql_db_path(self) -> Path:
|
|
78
|
+
return self.raw_paths["sql_db"]
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def from_version(
|
|
82
|
+
cls,
|
|
83
|
+
version: str | int | float, # ruff: ignore[PYI041]
|
|
84
|
+
root_dir: Path | str = f"{BASE_DIR}/chembl",
|
|
85
|
+
force: bool = False,
|
|
86
|
+
) -> "Chembl":
|
|
87
|
+
"""Downloads Chembl for version if not yet present in the given cache directory"""
|
|
88
|
+
version = chembl._format_version(version)
|
|
89
|
+
dir = Path(root_dir) / version
|
|
90
|
+
|
|
91
|
+
raw_paths: dict[str, Path] = chembl.ensure_raw_files(
|
|
92
|
+
version, raw_dir=dir / "raw", force=force
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
parquet_paths = chembl.ensure_parquet_tables(
|
|
96
|
+
raw_paths, table_dir=dir / "parquet"
|
|
97
|
+
)
|
|
98
|
+
return Chembl(
|
|
99
|
+
version=version,
|
|
100
|
+
raw_paths=raw_paths,
|
|
101
|
+
parquet_paths=parquet_paths,
|
|
102
|
+
dir=dir,
|
|
103
|
+
module=cls.module,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def from_latest(
|
|
108
|
+
cls,
|
|
109
|
+
root_dir: Path | str = f"{BASE_DIR}/chembl",
|
|
110
|
+
force: bool = False,
|
|
111
|
+
) -> "Chembl":
|
|
112
|
+
return cls.from_version(version=chembl.latest(), root_dir=root_dir, force=force)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@dataclass(frozen=True)
|
|
116
|
+
class Papyrus(_Base):
|
|
117
|
+
module: DatasetGetModule = papyrus
|
|
118
|
+
|
|
119
|
+
@staticmethod
|
|
120
|
+
def get_available_versions():
|
|
121
|
+
return papyrus.available_versions()
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def proteins(self) -> LazyFrame:
|
|
125
|
+
return self.views["proteins"]
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def full_data(self) -> LazyFrame:
|
|
129
|
+
"""all data (bioactivity + protein data),
|
|
130
|
+
composed into one flat tabular format LazyFrame"""
|
|
131
|
+
return self.views["full"]
|
|
132
|
+
|
|
133
|
+
@classmethod
|
|
134
|
+
def from_version(
|
|
135
|
+
cls,
|
|
136
|
+
version: str,
|
|
137
|
+
root_dir: Path | str = f"{BASE_DIR}/papyrus",
|
|
138
|
+
) -> "Papyrus":
|
|
139
|
+
"""Downloads Chembl for version if not yet present in the given cache directory"""
|
|
140
|
+
dir = Path(root_dir) / version
|
|
141
|
+
raw_paths: dict[str, Path] = papyrus.ensure_raw_files(
|
|
142
|
+
version, raw_dir=dir / "raw"
|
|
143
|
+
)
|
|
144
|
+
parquet_paths: dict[str, Path] = papyrus.ensure_parquet_tables(
|
|
145
|
+
raw_paths, table_dir=dir / "parquet"
|
|
146
|
+
)
|
|
147
|
+
return Papyrus(
|
|
148
|
+
version=version,
|
|
149
|
+
raw_paths=raw_paths,
|
|
150
|
+
parquet_paths=parquet_paths,
|
|
151
|
+
dir=dir,
|
|
152
|
+
module=cls.module,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
@classmethod
|
|
156
|
+
def from_latest(
|
|
157
|
+
cls,
|
|
158
|
+
root_dir: Path | str = f"{BASE_DIR}/papyrus",
|
|
159
|
+
) -> "Papyrus":
|
|
160
|
+
return cls.from_version(version=papyrus.latest(), root_dir=root_dir)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
if __name__ == "__main__":
|
|
164
|
+
p = Papyrus.from_latest()
|
|
165
|
+
p.views["proteins"]
|
|
166
|
+
|
|
167
|
+
p.lfs["proteins"]
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Papyrus dataset utilities for downloading, cleaning, and joining bioactivity and protein data.
|
|
2
|
+
|
|
3
|
+
This module provides functions to ensure the presence of raw and cleaned Papyrus dataset files,
|
|
4
|
+
and defines the Papyrus_57 database configuration.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import polars as pl
|
|
11
|
+
|
|
12
|
+
from fairfetched.utils import (
|
|
13
|
+
BASE_DIR,
|
|
14
|
+
decompress_and_scan_tsvxz,
|
|
15
|
+
ensure_url,
|
|
16
|
+
file_suffix_from_url,
|
|
17
|
+
lowercase_columns,
|
|
18
|
+
)
|
|
19
|
+
from fairfetched.utils.typing import BioactivityDBViews
|
|
20
|
+
|
|
21
|
+
PAPYRUS_VERSIONS: dict[str, dict[str, str]] = {
|
|
22
|
+
"05.6": {
|
|
23
|
+
"bioactivity": "https://zenodo.org/records/7373214/files/05.6_combined_set_with_stereochemistry.tsv.xz",
|
|
24
|
+
# "bioactivity_nostereochemistry": "https://zenodo.org/records/7373214/files/05.6_combined_set_without_stereochemistry.tsv.xz",
|
|
25
|
+
"readme": "https://zenodo.org/records/7373214/files/README.txt",
|
|
26
|
+
"protein": "https://zenodo.org/records/7373214/files/05.6_combined_set_protein_targets.tsv.xz",
|
|
27
|
+
},
|
|
28
|
+
"05.7": {
|
|
29
|
+
"bioactivity": "https://zenodo.org/records/13987985/files/05.7_combined_set_with_stereochemistry.tsv.xz",
|
|
30
|
+
# "bioactivity_nostereochemistry": "https://zenodo.org/records/13987985/files/05.7_combined_set_without_stereochemistry.tsv.xz",
|
|
31
|
+
"readme": "https://zenodo.org/records/13987985/files/README.txt",
|
|
32
|
+
"protein": "https://zenodo.org/records/13987985/files/05.7_combined_set_protein_targets.tsv.xz",
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def available_versions() -> tuple[str, ...]:
|
|
38
|
+
return tuple(PAPYRUS_VERSIONS.keys())
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def latest() -> str:
|
|
42
|
+
return available_versions()[-1]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def source_urls(version: str) -> dict[str, str]:
|
|
46
|
+
return PAPYRUS_VERSIONS[str(version)]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def ensure_raw_files(
|
|
50
|
+
version: str, raw_dir: Path | str | Any | None = None
|
|
51
|
+
) -> dict[str, Path]:
|
|
52
|
+
"""Download if missing, return path to raw file."""
|
|
53
|
+
if raw_dir is None:
|
|
54
|
+
raw_dir = BASE_DIR / "papyrus" / version / "raw"
|
|
55
|
+
raw_dir = Path(raw_dir)
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
name: ensure_url(url=url, path=raw_dir / f"{name}{file_suffix_from_url(url)}")
|
|
59
|
+
for name, url in source_urls(version).items()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def ensure_parquet_tables(
|
|
64
|
+
raw_paths: dict[str, Path],
|
|
65
|
+
table_dir: Path | str | None = None,
|
|
66
|
+
) -> dict[str, Path]:
|
|
67
|
+
"""Downloads if missing, extracts to streamable parquets for lazy loading,
|
|
68
|
+
returns paths to raw files and parquet files. Ignores README.txt file
|
|
69
|
+
|
|
70
|
+
parquet_dir default is raw_filepath_dir.parent / "parquet"
|
|
71
|
+
|
|
72
|
+
"""
|
|
73
|
+
if table_dir is None:
|
|
74
|
+
table_dir = next(iter(raw_paths.values())).parent.parent / "parquet"
|
|
75
|
+
table_dir = Path(table_dir)
|
|
76
|
+
table_dir.mkdir(exist_ok=True, parents=True)
|
|
77
|
+
schema_overrides = {
|
|
78
|
+
"Year": pl.Int32,
|
|
79
|
+
"pchembl_value_Mean": pl.Float64,
|
|
80
|
+
"pchembl_value_StdDev": pl.Float64,
|
|
81
|
+
"pchembl_value_SEM": pl.Float64,
|
|
82
|
+
# because of stray floats in pchembl_value_N, we do float -> int
|
|
83
|
+
"pchembl_value_N": pl.Float64,
|
|
84
|
+
"pchembl_value_Median": pl.Float64,
|
|
85
|
+
"pchembl_value_MAD": pl.Float64,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
filepath_dict = {}
|
|
89
|
+
for name, path_ in raw_paths.items():
|
|
90
|
+
if path_.name.lower() == "readme.txt":
|
|
91
|
+
continue
|
|
92
|
+
new_path = table_dir / f"{path_.stem.split('.')[0]}.parquet"
|
|
93
|
+
filepath_dict[name] = new_path
|
|
94
|
+
if new_path.exists():
|
|
95
|
+
continue
|
|
96
|
+
decompress_and_scan_tsvxz(
|
|
97
|
+
path_,
|
|
98
|
+
separator="\t",
|
|
99
|
+
infer_schema=False,
|
|
100
|
+
schema_overrides=schema_overrides,
|
|
101
|
+
null_values=["NA", ""], # which values to take as None
|
|
102
|
+
).cast(
|
|
103
|
+
{"pchembl_value_N": pl.Int64} if name == "bioactivity" else {}
|
|
104
|
+
).sink_parquet(new_path)
|
|
105
|
+
return filepath_dict
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def cleanly_scan_parquet_tables(
|
|
109
|
+
parquet_paths: dict[str, Path],
|
|
110
|
+
) -> dict[str, pl.LazyFrame]:
|
|
111
|
+
"""Scan table Parquet files and apply dataset-specific column cleanup."""
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
"protein": (
|
|
115
|
+
pl.scan_parquet(parquet_paths["protein"])
|
|
116
|
+
.pipe(lowercase_columns)
|
|
117
|
+
.rename({"uniprotid": "uniprot_id"})
|
|
118
|
+
),
|
|
119
|
+
"bioactivity": (
|
|
120
|
+
pl.scan_parquet(parquet_paths["bioactivity"]).pipe(lowercase_columns)
|
|
121
|
+
),
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def build_views(parquet_paths: dict[str, Path]) -> BioactivityDBViews:
|
|
126
|
+
"""Build joined domain views from the scanned source tables."""
|
|
127
|
+
lfs = cleanly_scan_parquet_tables(parquet_paths)
|
|
128
|
+
return {
|
|
129
|
+
"bioactivity": lfs["bioactivity"],
|
|
130
|
+
"compounds": lfs["bioactivity"]
|
|
131
|
+
.drop(
|
|
132
|
+
"activity_id",
|
|
133
|
+
)
|
|
134
|
+
.unique(("connectivity", "inchikey", "inchi")),
|
|
135
|
+
"full": lfs["bioactivity"].join(
|
|
136
|
+
lfs["protein"],
|
|
137
|
+
on="target_id",
|
|
138
|
+
how="left",
|
|
139
|
+
maintain_order="left",
|
|
140
|
+
validate="m:1", # one unique protein only from right, can reoccur within compounds.
|
|
141
|
+
),
|
|
142
|
+
"proteins": lfs["protein"],
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def help() -> None:
|
|
147
|
+
"""prints out example usage"""
|
|
148
|
+
print("""
|
|
149
|
+
Example usage:
|
|
150
|
+
```
|
|
151
|
+
raw = ensure_raw_files("05.7")
|
|
152
|
+
tables = cleanly_scan_parquet_tables(raw)
|
|
153
|
+
views = build_views(tables)
|
|
154
|
+
views["full"].sink_parquet("output.parquet")
|
|
155
|
+
```
|
|
156
|
+
""")
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# mol_expr.pyi
|
|
2
|
+
import polars as pl
|
|
3
|
+
|
|
4
|
+
from .mol_expr import MolExprs, MolFn
|
|
5
|
+
|
|
6
|
+
# class MolExpr:
|
|
7
|
+
# def __init__(self, expr: pl.Expr) -> None: ...
|
|
8
|
+
# def from_smiles(self) -> pl.Expr: ...
|
|
9
|
+
# def from_inchi(self) -> pl.Expr: ...
|
|
10
|
+
# def to_smiles(self) -> pl.Expr: ...
|
|
11
|
+
# def to_inchi(self) -> pl.Expr: ...
|
|
12
|
+
# def to_inchikey(self) -> pl.Expr: ...
|
|
13
|
+
# def to_kekulised_smiles(self) -> pl.Expr: ...
|
|
14
|
+
# def num_atoms(self) -> pl.Expr: ...
|
|
15
|
+
# def num_heavy_atoms(self) -> pl.Expr: ...
|
|
16
|
+
# def standardise(self, *steps: MolFn) -> pl.Expr: ...
|
|
17
|
+
|
|
18
|
+
# teach ty that pl.Expr has a .mol accessor
|
|
19
|
+
class _ExprMolAccessor:
|
|
20
|
+
mol: MolExprs
|
|
21
|
+
|
|
22
|
+
# ty picks this up via the __getattr__ extension pattern
|
|
23
|
+
def __getattr__(name: str) -> MolExprs: ...
|
|
File without changes
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import logging as lg
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
from chembl_structure_pipeline import ( # ty:ignore[unresolved-import] #ty:ignore[unused-ignore-comment] #ty:ignore[unused-ignore-comment]
|
|
5
|
+
get_parent_mol as chembl_get_parent_mol,
|
|
6
|
+
)
|
|
7
|
+
from chembl_structure_pipeline import ( # ty:ignore[unresolved-import] #ty:ignore[unused-ignore-comment] #ty:ignore[unused-ignore-comment]
|
|
8
|
+
standardize_mol as chembl_standardize,
|
|
9
|
+
)
|
|
10
|
+
except ImportError as e:
|
|
11
|
+
lg.warning("""
|
|
12
|
+
you should install chembl_structure_pipeline if you want
|
|
13
|
+
to use chembl standardisation
|
|
14
|
+
""")
|
|
15
|
+
CHEMBL_IMPORT_ERROR = e
|
|
16
|
+
|
|
17
|
+
def chembl_get_parent_mol(mol):
|
|
18
|
+
"""placeholder for chembl_structure_pipeline.get_parent_mol in case
|
|
19
|
+
the import doesn't work.
|
|
20
|
+
raises respective import error when called"""
|
|
21
|
+
lg.warning("""
|
|
22
|
+
please install `chembl_structure_pipeline` if you want to use
|
|
23
|
+
ChEMBL standardisation
|
|
24
|
+
""")
|
|
25
|
+
raise CHEMBL_IMPORT_ERROR
|
|
26
|
+
|
|
27
|
+
def chembl_standardize(mol):
|
|
28
|
+
"""placeholder for chembl_structure_pipeline.chembl_standardize in case
|
|
29
|
+
the import doesn't work.
|
|
30
|
+
|
|
31
|
+
raises respective import error when called.
|
|
32
|
+
"""
|
|
33
|
+
lg.warning("""
|
|
34
|
+
please install `chembl_structure_pipeline` if you want to use
|
|
35
|
+
ChEMBL standardisation
|
|
36
|
+
""")
|
|
37
|
+
raise CHEMBL_IMPORT_ERROR
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
from papyrus_structure_pipeline import ( # ty:ignore[unresolved-import] #ty:ignore[unused-ignore-comment] #ty:ignore[unused-ignore-comment]
|
|
42
|
+
standardize as _papyrus_standardize,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
except ImportError as e:
|
|
47
|
+
PAPYRUS_IMPORT_ERROR = e
|
|
48
|
+
|
|
49
|
+
def _papyrus_standardize(mol, **kwargs):
|
|
50
|
+
"""placeholder for papyrus_standardize in case imports dont work.
|
|
51
|
+
raises respective import error when called"""
|
|
52
|
+
lg.warning("""
|
|
53
|
+
please install `papyrus_structure_pipeline` if you want to use
|
|
54
|
+
Papyrus standardisation
|
|
55
|
+
""")
|
|
56
|
+
raise PAPYRUS_IMPORT_ERROR
|