fairfetched 0.0.1.post20260304__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. fairfetched-0.0.1.post20260304/LICENSE +21 -0
  2. fairfetched-0.0.1.post20260304/PKG-INFO +48 -0
  3. fairfetched-0.0.1.post20260304/README.md +26 -0
  4. fairfetched-0.0.1.post20260304/fairfetched/__init__.py +0 -0
  5. fairfetched-0.0.1.post20260304/fairfetched/get/__init__.py +3 -0
  6. fairfetched-0.0.1.post20260304/fairfetched/get/api.py +127 -0
  7. fairfetched-0.0.1.post20260304/fairfetched/get/chembl.py +195 -0
  8. fairfetched-0.0.1.post20260304/fairfetched/get/papyrus.py +130 -0
  9. fairfetched-0.0.1.post20260304/fairfetched/utils/__init__.py +14 -0
  10. fairfetched-0.0.1.post20260304/fairfetched/utils/_optional.py +30 -0
  11. fairfetched-0.0.1.post20260304/fairfetched/utils/_track.py +31 -0
  12. fairfetched-0.0.1.post20260304/fairfetched/utils/ensure.py +30 -0
  13. fairfetched-0.0.1.post20260304/fairfetched/utils/files.py +30 -0
  14. fairfetched-0.0.1.post20260304/fairfetched/utils/polars.py +96 -0
  15. fairfetched-0.0.1.post20260304/fairfetched/utils/storage.py +16 -0
  16. fairfetched-0.0.1.post20260304/fairfetched/utils/typing.py +24 -0
  17. fairfetched-0.0.1.post20260304/fairfetched/vis.py +92 -0
  18. fairfetched-0.0.1.post20260304/fairfetched.egg-info/PKG-INFO +48 -0
  19. fairfetched-0.0.1.post20260304/fairfetched.egg-info/SOURCES.txt +46 -0
  20. fairfetched-0.0.1.post20260304/fairfetched.egg-info/dependency_links.txt +1 -0
  21. fairfetched-0.0.1.post20260304/fairfetched.egg-info/requires.txt +15 -0
  22. fairfetched-0.0.1.post20260304/fairfetched.egg-info/top_level.txt +4 -0
  23. fairfetched-0.0.1.post20260304/pyproject.toml +36 -0
  24. fairfetched-0.0.1.post20260304/scripts/chembl.py +7 -0
  25. fairfetched-0.0.1.post20260304/setup.cfg +4 -0
  26. fairfetched-0.0.1.post20260304/tests/__init__.py +1 -0
  27. fairfetched-0.0.1.post20260304/tests/test_ensure.py +148 -0
  28. fairfetched-0.0.1.post20260304/tests/test_integration_chembl.py +305 -0
  29. fairfetched-0.0.1.post20260304/tests/test_utils.py +128 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 lucina-may nollen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,48 @@
1
+ Metadata-Version: 2.4
2
+ Name: fairfetched
3
+ Version: 0.0.1.post20260304
4
+ Summary: data APIs for reproducible data fetching in cheminformatics
5
+ Project-URL: Homepage, https://github.com/lucinamay/fairfetched
6
+ Requires-Python: >=3.13
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: polars>=1.38.1
10
+ Requires-Dist: pystow>=0.7.28
11
+ Requires-Dist: rich>=14.3.3
12
+ Provides-Extra: rdkit
13
+ Requires-Dist: rdkit>=2025.9.6; extra == "rdkit"
14
+ Provides-Extra: standardise
15
+ Requires-Dist: fairfetched[rdkit]; extra == "standardise"
16
+ Requires-Dist: papyrus-structure-pipeline>=0.0.5; extra == "standardise"
17
+ Provides-Extra: dev
18
+ Requires-Dist: pytest>=9.0.2; extra == "dev"
19
+ Requires-Dist: sqlalchemy>=2.0.46; extra == "dev"
20
+ Requires-Dist: fairfetched[standardise]; extra == "dev"
21
+ Dynamic: license-file
22
+
23
+ # fairfetched
24
+ data APIs for reproducible data fetching in cheminformatics in line with FAIR principles
25
+
26
+ # installation
27
+ you can install this package through
28
+ `uv add fairfetched` (recommended)
29
+
30
+ or if you do not use the uv package manager:
31
+ `pip install fairfetched`
32
+
33
+
34
+ # roadmap
35
+ - [ ] papyrus database support
36
+ - [x] papyrus latest version download
37
+ - [x] simple nested filtering
38
+ - [ ] efficient nested filtering
39
+ - [ ] all-version support
40
+ - [ ] built-in pivots
41
+ - [ ] chembl database support
42
+ - [x] database to tables
43
+ - [ ] intuitive pre-merged flat files
44
+ - [ ] database visualisation
45
+ - [ ] reproducible chembl and papyrus standardisation
46
+ - [ ] automated time-url logging and manifest files
47
+ - [ ] minimisation of dependencies
48
+ - [ ] support for other databases
@@ -0,0 +1,26 @@
1
+ # fairfetched
2
+ data APIs for reproducible data fetching in cheminformatics in line with FAIR principles
3
+
4
+ # installation
5
+ you can install this package through
6
+ `uv add fairfetched` (recommended)
7
+
8
+ or if you do not use the uv package manager:
9
+ `pip install fairfetched`
10
+
11
+
12
+ # roadmap
13
+ - [ ] papyrus database support
14
+ - [x] papyrus latest version download
15
+ - [x] simple nested filtering
16
+ - [ ] efficient nested filtering
17
+ - [ ] all-version support
18
+ - [ ] built-in pivots
19
+ - [ ] chembl database support
20
+ - [x] database to tables
21
+ - [ ] intuitive pre-merged flat files
22
+ - [ ] database visualisation
23
+ - [ ] reproducible chembl and papyrus standardisation
24
+ - [ ] automated time-url logging and manifest files
25
+ - [ ] minimisation of dependencies
26
+ - [ ] support for other databases
File without changes
@@ -0,0 +1,3 @@
1
+ from .api import Chembl, Papyrus
2
+
3
+ __all__ = ["Chembl", "Papyrus"]
@@ -0,0 +1,127 @@
1
+ from dataclasses import dataclass
2
+ from functools import cached_property
3
+ from pathlib import Path
4
+ from types import ModuleType
5
+ from typing import Tuple
6
+
7
+ from polars import LazyFrame
8
+
9
+ import fairfetched.get.chembl as chembl
10
+ import fairfetched.get.papyrus as papyrus
11
+ from fairfetched.utils import BASE_DIR
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class _Base:
16
+ version: str
17
+ raw_paths: dict[str, Path]
18
+ lfs: dict[str, LazyFrame]
19
+ dir: Path
20
+ module: ModuleType
21
+
22
+ def __hash__(self):
23
+ return hash(
24
+ (self.version, str(self.sources), str(self.raw_paths), self.module.__name__)
25
+ )
26
+
27
+ @cached_property
28
+ def name(self) -> str:
29
+ return self.module.__name__.split(".")[-1]
30
+
31
+ @cached_property
32
+ def sources(self) -> dict[str, str]:
33
+ return self.module.get_sources(self.version)
34
+
35
+ @property
36
+ def composed(self) -> dict[str, LazyFrame]:
37
+ lfs = self.module.compose(self.lfs)
38
+ return lfs
39
+
40
+ @property
41
+ def bioactivity(self) -> LazyFrame:
42
+ return self.composed["bioactivity"]
43
+
44
+ @property
45
+ def compounds(self) -> LazyFrame:
46
+ return self.composed["compounds"]
47
+
48
+ @classmethod
49
+ @cached_property
50
+ def available_versions(cls) -> Tuple[str]:
51
+ return cls.module.available_versions()
52
+
53
+
54
+ @dataclass(frozen=True)
55
+ class Chembl(_Base):
56
+ module: ModuleType = chembl
57
+ extracted_table_paths: dict[str, Path] | None = None
58
+
59
+ @cached_property
60
+ def activity(self) -> LazyFrame:
61
+ return self.composed["activity"]
62
+
63
+ @cached_property
64
+ def raw_sql_db_path(self) -> Path:
65
+ return self.raw_paths["sql_db"]
66
+
67
+ @classmethod
68
+ def from_version(
69
+ cls,
70
+ version: str | int | float,
71
+ root_dir: Path | str = f"{BASE_DIR}/chembl",
72
+ ):
73
+ """Downloads Chembl for version if not yet present in the given cache directory"""
74
+ version = chembl.__version_formatter(version)
75
+ dir = Path(root_dir) / version
76
+
77
+ raw_paths: dict[str, Path] = cls.module.ensure_raw(
78
+ version, cache_dir=dir / "raw"
79
+ )
80
+ extracted_table_paths = cls.module.extract_sqlite(
81
+ raw_paths["sql_db"], cache_dir=dir / "extracted"
82
+ )
83
+ lfs = cls.module.clean(extracted_table_paths)
84
+ return Chembl(
85
+ version=version,
86
+ raw_paths=raw_paths,
87
+ lfs=lfs,
88
+ dir=dir,
89
+ module=cls.module,
90
+ extracted_table_paths=extracted_table_paths,
91
+ )
92
+
93
+ @classmethod
94
+ def from_latest(cls, root_dir):
95
+ return cls.from_version(version=cls.module.latest(), root_dir=root_dir)
96
+
97
+
98
+ @dataclass(frozen=True)
99
+ class Papyrus(_Base):
100
+ module: ModuleType = papyrus
101
+
102
+ @classmethod
103
+ def from_version(
104
+ cls,
105
+ version: str,
106
+ root_dir: Path | str = f"{BASE_DIR}/papyrus",
107
+ ):
108
+ """Downloads Chembl for version if not yet present in the given cache directory"""
109
+ dir = Path(root_dir) / version
110
+ raw_paths: dict[str, Path] = cls.module.ensure_raw(
111
+ version, cache_dir=dir / "raw"
112
+ )
113
+ lfs = cls.module.clean(raw_paths)
114
+ return Papyrus(
115
+ version=version,
116
+ raw_paths=raw_paths,
117
+ lfs=lfs,
118
+ dir=dir,
119
+ module=cls.module,
120
+ )
121
+
122
+ @classmethod
123
+ def from_latest(
124
+ cls,
125
+ root_dir: Path | str = f"{BASE_DIR}/papyrus",
126
+ ):
127
+ return cls.from_version(version=cls.module.latest(), root_dir=root_dir)
@@ -0,0 +1,195 @@
1
+ from pathlib import Path
2
+ from typing import Any
3
+
4
+ import polars as pl
5
+
6
+ from fairfetched.utils import (
7
+ BASE_DIR,
8
+ ensure_url,
9
+ file_suffix_from_url,
10
+ lowercase_columns,
11
+ sqlite_db_to_parquets,
12
+ untar_sqlite,
13
+ )
14
+ from fairfetched.utils.typing import ComposedLFDict
15
+
16
+ CHEMBL_DIR = BASE_DIR / "chembl"
17
+
18
+
19
+ def __version_formatter(version: int | float | str) -> str:
20
+ if isinstance(version, int | float):
21
+ version = str(version)
22
+ if not isinstance(version, str):
23
+ try:
24
+ version = str(version)
25
+ finally:
26
+ raise TypeError(f"invalid version type: {type(version)}")
27
+
28
+ version = version.lstrip("0")
29
+ # for canonicalize the version number 22.1 and 24.1 and left pad with a zero if needed
30
+ return version.replace(".", "_").zfill(2)
31
+
32
+
33
+ def _version_to_url(version: str):
34
+ base = "https://ftp.ebi.ac.uk/pub/databases/chembl/ChEMBLdb/releases"
35
+ return f"{base}/chembl_{version}/chembl_{version}"
36
+
37
+
38
+ CHEMBL_VERSIONS: dict[str, dict[str, str]] = {
39
+ version: {"sql_db": _version_to_url(version)}
40
+ for version in sorted(
41
+ map(__version_formatter, list(range(1, 37)) + ["24_1", "22_1"])
42
+ )
43
+ }
44
+
45
+
46
+ def available_versions() -> tuple[str, ...]:
47
+ return tuple(CHEMBL_VERSIONS.keys())
48
+
49
+
50
+ def latest() -> str:
51
+ return available_versions()[-1]
52
+
53
+
54
+ def get_sources(version: str) -> dict[str, str]:
55
+ return CHEMBL_VERSIONS[str(version)]
56
+
57
+
58
+ def ensure_raw(version: str, cache_dir: Path | str | None = None) -> dict[str, Path]:
59
+ """downloads the original sql database, with its original name and compression"""
60
+ if cache_dir is None:
61
+ cache_dir = CHEMBL_DIR / version
62
+ cache_dir = Path(cache_dir)
63
+ return {
64
+ name: ensure_url(url=url, path=cache_dir / f"{file_suffix_from_url(url)}")
65
+ for name, url in get_sources(version).items()
66
+ }
67
+
68
+
69
+ def extract_sqlite(
70
+ sql_tar_gz_path: Path | str, cache_dir: Path | str | Any | None = None
71
+ ) -> dict[str, Path]:
72
+ if cache_dir is None:
73
+ cache_dir = Path(sql_tar_gz_path).parent / "extracted"
74
+
75
+ raw_sql = untar_sqlite(sql_tar_gz_path)
76
+ parquets = sqlite_db_to_parquets(raw_sql, cache_dir=cache_dir)
77
+ return parquets
78
+
79
+
80
+ def clean(extracted_sqlite_parquet_paths: dict[str, Path]) -> dict[str, pl.LazyFrame]:
81
+ return {
82
+ name: pl.scan_parquet(path_)
83
+ .pipe(lowercase_columns)
84
+ .fill_nan(None)
85
+ .with_columns(
86
+ pl.col(pl.String).replace({"": None}),
87
+ )
88
+ for name, path_ in extracted_sqlite_parquet_paths.items()
89
+ }
90
+
91
+
92
+ def compose(lfs: dict[str, pl.LazyFrame]) -> ComposedLFDict:
93
+ """Join/combine lazy frames. Optional, returns single LF."""
94
+ return {
95
+ "bioactivity": _bioactivities(lfs),
96
+ "compounds": _compounds(lfs),
97
+ "proteins": lfs["protein"],
98
+ "components": _components(lfs),
99
+ }
100
+
101
+
102
+ def _bioactivities(lfs) -> pl.LazyFrame:
103
+ return (
104
+ # dfs["activity_properties"]
105
+ # .join(dfs["activities"], on="activity_id", how="left")
106
+ # .join(dfs["action_type"], on="action_type", how="left")
107
+ # .join
108
+ lfs["bioactivity"]
109
+ .join(
110
+ lfs["protein"],
111
+ on="target_id",
112
+ how="left",
113
+ maintain_order="left",
114
+ validate="m:1", # one unique protein only from right, can reoccur within compounds.
115
+ )
116
+ .join(lfs["action_type"], on="action_type", how="left", suffix="_action_type")
117
+ .join(
118
+ lfs["assays"], on="assay_id", how="left", suffix="_assay"
119
+ ) # doc_ids to this
120
+ .join(lfs["assay_type"], on="assay_type", how="left", suffix="_assay_type")
121
+ .join(lfs, on="bao_format")
122
+ # .join(dfs[])
123
+ )
124
+
125
+
126
+ def _components(lfs) -> pl.LazyFrame:
127
+ return (
128
+ lfs["component_sequences"]
129
+ .join(
130
+ lfs["component_class"],
131
+ on="component_id",
132
+ how="left",
133
+ suffix="_class",
134
+ validate="1:m",
135
+ )
136
+ .join(
137
+ lfs["component_domains"].join(
138
+ lfs["domains"],
139
+ on="domain_id",
140
+ how="left",
141
+ suffix="_domains",
142
+ validate="m:1",
143
+ ),
144
+ on="component_id",
145
+ how="left",
146
+ suffix="_domains",
147
+ validate="1:m",
148
+ )
149
+ # .join(
150
+ # dfs["component_synonyms"],
151
+ # on="component_id",
152
+ # how="left",
153
+ # suffix="_synonyms",
154
+ # validate="m:m",
155
+ # )
156
+ )
157
+
158
+
159
+ def _compounds(lfs) -> pl.LazyFrame:
160
+ return (
161
+ lfs["molecule_dictionary"]
162
+ .join(
163
+ lfs["compound_properties"],
164
+ on="molregno",
165
+ how="left",
166
+ suffix="_compound_properties",
167
+ validate="1:1",
168
+ )
169
+ .join(
170
+ lfs["compound_structures"],
171
+ on="molregno",
172
+ how="left",
173
+ suffix="_compound_structures",
174
+ validate="1:1",
175
+ )
176
+ .join(
177
+ lfs["compound_records"].join(
178
+ lfs["docs"], on="doc_id", how="left", suffix="_doc", validate="m:1"
179
+ ),
180
+ on="molregno",
181
+ how="left",
182
+ suffix="_compound_records",
183
+ validate="1:m",
184
+ )
185
+ .join(
186
+ lfs["compound_structural_alerts"],
187
+ # .join(
188
+ # dfs["docs"], on="doc_id", how="left", suffix="_doc",validate="m:1"
189
+ # )
190
+ on="molregno",
191
+ how="left",
192
+ suffix="_compound_structural_alerts",
193
+ validate="1:m",
194
+ )
195
+ )
@@ -0,0 +1,130 @@
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
+ ensure_url,
15
+ file_suffix_from_url,
16
+ lowercase_columns,
17
+ scan_tsvxz,
18
+ )
19
+ from fairfetched.utils.typing import ComposedLFDict
20
+
21
+ PAPYRUS_VERSIONS: dict[str, dict[str, str]] = {
22
+ "05.7": {
23
+ "bioactivity": "https://zenodo.org/records/13987985/files/05.7_combined_set_with_stereochemistry.tsv.xz?download=1",
24
+ # "bioactivity_nostereochemistry": "https://zenodo.org/records/13987985/files/05.7_combined_set_without_stereochemistry.tsv.xz?download=1",
25
+ "readme": "https://zenodo.org/records/13987985/files/README.txt?download=1",
26
+ "protein": "https://zenodo.org/records/13987985/files/05.7_combined_set_protein_targets.tsv.xz?download=1",
27
+ },
28
+ "05.6": {
29
+ "bioactivity": "https://zenodo.org/records/7373214/files/05.6_combined_set_with_stereochemistry.tsv.xz?download=1",
30
+ # "bioactivity_nostereochemistry": "https://zenodo.org/records/7373214/files/05.6_combined_set_without_stereochemistry.tsv.xz?download=1",
31
+ "readme": "https://zenodo.org/records/7373214/files/README.txt?download=1",
32
+ "protein": "https://zenodo.org/records/7373214/files/05.6_combined_set_protein_targets.tsv.xz?download=1",
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 get_sources(version: str) -> dict[str, str]:
46
+ return PAPYRUS_VERSIONS[str(version)]
47
+
48
+
49
+ def ensure_raw(
50
+ version: str, cache_dir: Path | str | Any | None = None
51
+ ) -> dict[str, Path]:
52
+ """Download if missing, return path to raw file."""
53
+ if cache_dir is None:
54
+ cache_dir = BASE_DIR / "papyrus" / version
55
+ cache_dir = Path(cache_dir)
56
+
57
+ return {
58
+ name: ensure_url(url=url, path=cache_dir / f"{file_suffix_from_url(url)}")
59
+ for name, url in get_sources(version).items()
60
+ }
61
+
62
+
63
+ def clean(raw_filepath_dict: dict[str, Path]) -> dict[str, pl.LazyFrame]:
64
+ """Transform raw → clean, return lazy frames. No I/O."""
65
+ schema_overrides = {
66
+ "Year": pl.Int32,
67
+ "pchembl_value_Mean": pl.Float64,
68
+ "pchembl_value_StdDev": pl.Float64,
69
+ "pchembl_value_SEM": pl.Float64,
70
+ "pchembl_value_N": pl.Float64,
71
+ "pchembl_value_Median": pl.Float64,
72
+ "pchembl_value_MAD": pl.Float64,
73
+ }
74
+
75
+ return {
76
+ "protein": (
77
+ scan_tsvxz(raw_filepath_dict["protein"], separator="\t")
78
+ .pipe(lowercase_columns)
79
+ .rename({"uniprotid": "uniprot_id"})
80
+ ),
81
+ "bioactivity": (
82
+ scan_tsvxz(
83
+ raw_filepath_dict["bioactivity"],
84
+ separator="\t",
85
+ infer_schema=False,
86
+ schema_overrides=schema_overrides,
87
+ null_values="NA", # which values to take as None
88
+ )
89
+ .pipe(lowercase_columns)
90
+ .cast({"pchembl_value_n": pl.Int64})
91
+ ),
92
+ }
93
+
94
+
95
+ def compose(lfs: dict[str, pl.LazyFrame]) -> ComposedLFDict:
96
+ """Join/combine lazy frames. Optional, returns single LF."""
97
+ return {
98
+ "bioactivity": lfs["bioactivity"].join(
99
+ lfs["protein"],
100
+ on="target_id",
101
+ how="left",
102
+ maintain_order="left",
103
+ validate="m:1", # one unique protein only from right, can reoccur within compounds.
104
+ ),
105
+ "compounds": lfs["bioactivity"]
106
+ .drop(
107
+ "activity_id",
108
+ )
109
+ .unique(("connectivity", "inchikey", "inchi")),
110
+ "full": lfs["bioactivity"].join(
111
+ lfs["protein"],
112
+ on="target_id",
113
+ how="left",
114
+ maintain_order="left",
115
+ validate="m:1", # one unique protein only from right, can reoccur within compounds.
116
+ ),
117
+ }
118
+
119
+
120
+ def help() -> None:
121
+ """prints out example usage"""
122
+ print("""
123
+ Example usage:
124
+ ```
125
+ raw = ensure_raw("05.7")
126
+ lfs = clean(raw)
127
+ final_lf = compose(lfs)
128
+ final_lf.sink_parquet("output.parquet")
129
+ ```
130
+ """)
@@ -0,0 +1,14 @@
1
+ from .ensure import ensure_url
2
+ from .files import file_suffix_from_url, untar_sqlite
3
+ from .polars import lowercase_columns, scan_tsvxz, sqlite_db_to_parquets
4
+ from .storage import BASE_DIR
5
+
6
+ __all__ = [
7
+ "BASE_DIR",
8
+ ensure_url,
9
+ lowercase_columns,
10
+ scan_tsvxz,
11
+ file_suffix_from_url,
12
+ sqlite_db_to_parquets,
13
+ untar_sqlite,
14
+ ]
@@ -0,0 +1,30 @@
1
+ try:
2
+ from tqdm import ( # ty: ignore[unresolved-import,unused-ignore-comment] # ty: ignore[unused-ignore-comment]
3
+ tqdm as tqdm_track,
4
+ )
5
+ except ImportError:
6
+
7
+ def tqdm_track(iterable, *args, **kwargs):
8
+ return iter(iterable)
9
+
10
+
11
+ try:
12
+ from rich.progress import ( # ty: ignore[unresolved-import,unused-ignore-comment] # ty: ignore[unused-ignore-comment]
13
+ track as rich_track,
14
+ )
15
+
16
+ except ImportError:
17
+
18
+ def rich_track(iterable, *args, **kwargs):
19
+ return iter(iterable)
20
+
21
+
22
+ try:
23
+ from marimo import ( # ty: ignore[unresolved-import,unused-ignore-comment] # ty: ignore[unused-ignore-comment]
24
+ running_in_notebook as in_marimo,
25
+ )
26
+
27
+ except ImportError:
28
+
29
+ def in_marimo():
30
+ return False
@@ -0,0 +1,31 @@
1
+ from typing import Iterable
2
+
3
+ from _optional import in_marimo, rich_track, tqdm_track
4
+
5
+
6
+ def track(iterable: Iterable, desc: str = "", total: int | None = None):
7
+ """Progress bar that adapts to the environment (marimo or terminal)."""
8
+ if not total:
9
+ try:
10
+ total = len(iterable) # ty: ignore[invalid-argument-type]
11
+ except TypeError:
12
+ pass
13
+ if in_marimo():
14
+ # Use tqdm with thin bar characters for a slimmer look
15
+ return tqdm_track(
16
+ iterable,
17
+ desc=desc,
18
+ total=total,
19
+ bar_format="{desc} {bar:20} {percentage:3.0f}% {n_fmt}/{total_fmt} [{elapsed}<{remaining}, {rate_fmt}]",
20
+ ascii="─━",
21
+ leave=True,
22
+ )
23
+ return rich_track(
24
+ iterable,
25
+ description=desc,
26
+ total=total,
27
+ transient=False,
28
+ style="green",
29
+ complete_style="bold green",
30
+ console=None,
31
+ )
@@ -0,0 +1,30 @@
1
+ import datetime
2
+ import logging as lg
3
+ from pathlib import Path
4
+
5
+ import requests
6
+
7
+ from ._track import track
8
+
9
+
10
+ def ensure_url(url: str, path: Path, force: bool = False) -> Path:
11
+ """Downloads url to path if not already existing. Makes path dirs if not existing"""
12
+ path.parent.mkdir(parents=True, exist_ok=True)
13
+
14
+ if path.exists() and not force:
15
+ lg.debug(f"File already exists at {path}. Skipping download.")
16
+ return path
17
+
18
+ response = requests.get(url)
19
+ response.raise_for_status()
20
+ with open(path, "wb") as f:
21
+ total = int(response.headers.get("content-length", 0))
22
+ chunk_size = 8192
23
+ for chunk in track(
24
+ response.iter_content(chunk_size=chunk_size),
25
+ total=(total // chunk_size) + int(total % chunk_size != 0),
26
+ desc=f"downloading {url.split('/')[-1]}",
27
+ ):
28
+ f.write(chunk)
29
+ lg.info(f"Downloaded {url} to {path} on {datetime.datetime.now()}")
30
+ return path