scmora-db 0.1.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.
scmora_db/__init__.py ADDED
@@ -0,0 +1,51 @@
1
+ """Search, download, and load SCMORA .h5mu datasets."""
2
+
3
+ from ._version import __version__
4
+ from .catalog import (
5
+ DEFAULT_REPO_ID,
6
+ MatchResult,
7
+ list_dataset_ids,
8
+ list_detail_sources,
9
+ list_detailed_conditions,
10
+ list_usage_tags,
11
+ list_values,
12
+ load_catalog,
13
+ resolve_matches,
14
+ search_datasets,
15
+ )
16
+ from .exceptions import AmbiguousDatasetError, ScmoraDbError, TooManyMatchesError
17
+
18
+ __all__ = [
19
+ "AmbiguousDatasetError",
20
+ "DEFAULT_REPO_ID",
21
+ "MatchResult",
22
+ "ScmoraDbError",
23
+ "TooManyMatchesError",
24
+ "__version__",
25
+ "download_datasets",
26
+ "list_dataset_ids",
27
+ "list_detail_sources",
28
+ "list_detailed_conditions",
29
+ "list_usage_tags",
30
+ "list_values",
31
+ "load_catalog",
32
+ "load_datasets",
33
+ "resolve_matches",
34
+ "search_datasets",
35
+ ]
36
+
37
+
38
+ def download_datasets(*args, **kwargs):
39
+ """Download matching .h5mu files from Hugging Face."""
40
+
41
+ from .download import download_datasets as _download_datasets
42
+
43
+ return _download_datasets(*args, **kwargs)
44
+
45
+
46
+ def load_datasets(*args, **kwargs):
47
+ """Download and load matching .h5mu files with mudata.read_h5mu."""
48
+
49
+ from .io import load_datasets as _load_datasets
50
+
51
+ return _load_datasets(*args, **kwargs)
scmora_db/_version.py ADDED
@@ -0,0 +1,3 @@
1
+ """Package version."""
2
+
3
+ __version__ = "0.1.0"
scmora_db/catalog.py ADDED
@@ -0,0 +1,288 @@
1
+ """Metadata catalog access and filtering."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Iterable, List, Optional, Union
8
+
9
+ import pandas as pd
10
+
11
+ from .exceptions import AmbiguousDatasetError, TooManyMatchesError
12
+
13
+ DEFAULT_REPO_ID = "shiny321/genome-db"
14
+ DEFAULT_REPO_TYPE = "dataset"
15
+ DEFAULT_METADATA_FILENAME = "metadata.csv"
16
+ DEFAULT_MAX_AUTO_MATCHES = 5
17
+ LISTABLE_FIELDS = {
18
+ "condition": "condition",
19
+ "dataset-id": "dataset_id",
20
+ "dataset-ids": "dataset_id",
21
+ "dataset-uid": "dataset_uid",
22
+ "dataset-uids": "dataset_uid",
23
+ "detailed-condition": "detailed_condition",
24
+ "detailed-conditions": "detailed_condition",
25
+ "detail-source": "detail_source",
26
+ "detail-sources": "detail_source",
27
+ "group": "group",
28
+ "groups": "group",
29
+ "gse-id": "gse_id",
30
+ "gse-ids": "gse_id",
31
+ "reference": "reference",
32
+ "references": "reference",
33
+ "sample-type": "sample_type",
34
+ "sample-types": "sample_type",
35
+ "species": "species",
36
+ "usage-primary": "usage_primary",
37
+ "usage-primaries": "usage_primary",
38
+ "usage-tag": "usage_tags",
39
+ "usage-tags": "usage_tags",
40
+ }
41
+
42
+
43
+ @dataclass(frozen=True)
44
+ class MatchResult:
45
+ """A resolved query and the matching rows."""
46
+
47
+ rows: pd.DataFrame
48
+ matched_ids: List[str]
49
+
50
+ @property
51
+ def count(self) -> int:
52
+ return len(self.rows)
53
+
54
+ @property
55
+ def is_single(self) -> bool:
56
+ return self.count == 1
57
+
58
+
59
+ def load_catalog(
60
+ repo_id: str = DEFAULT_REPO_ID,
61
+ *,
62
+ revision: Optional[str] = None,
63
+ token: Optional[Union[str, bool]] = None,
64
+ cache_dir: Optional[Union[str, Path]] = None,
65
+ metadata_path: Optional[Union[str, Path]] = None,
66
+ prefer_remote: bool = False,
67
+ ) -> pd.DataFrame:
68
+ """Load the dataset metadata catalog.
69
+
70
+ By default this reads the metadata bundled with the package. Set
71
+ ``prefer_remote=True`` to download ``metadata.csv`` from Hugging Face first.
72
+ """
73
+
74
+ if metadata_path is not None:
75
+ path = Path(metadata_path)
76
+ elif prefer_remote:
77
+ from huggingface_hub import hf_hub_download
78
+
79
+ path = Path(
80
+ hf_hub_download(
81
+ repo_id=repo_id,
82
+ repo_type=DEFAULT_REPO_TYPE,
83
+ filename=DEFAULT_METADATA_FILENAME,
84
+ revision=revision,
85
+ token=token,
86
+ cache_dir=cache_dir,
87
+ )
88
+ )
89
+ else:
90
+ path = Path(__file__).with_name(DEFAULT_METADATA_FILENAME)
91
+
92
+ return _normalize_catalog(_read_metadata_csv(path))
93
+
94
+
95
+ def search_datasets(
96
+ *,
97
+ dataset_id: Optional[Union[str, Iterable[str]]] = None,
98
+ dataset_uid: Optional[Union[str, Iterable[str]]] = None,
99
+ gse_id: Optional[Union[str, Iterable[str]]] = None,
100
+ detailed_condition: Optional[Union[str, Iterable[str]]] = None,
101
+ usage_tag: Optional[Union[str, Iterable[str]]] = None,
102
+ detail_source: Optional[Union[str, Iterable[str]]] = None,
103
+ condition: Optional[Union[str, Iterable[str]]] = None,
104
+ sample_type: Optional[Union[str, Iterable[str]]] = None,
105
+ species: Optional[Union[str, Iterable[str]]] = None,
106
+ reference: Optional[Union[str, Iterable[str]]] = None,
107
+ repo_id: str = DEFAULT_REPO_ID,
108
+ revision: Optional[str] = None,
109
+ token: Optional[Union[str, bool]] = None,
110
+ cache_dir: Optional[Union[str, Path]] = None,
111
+ metadata_path: Optional[Union[str, Path]] = None,
112
+ prefer_remote: bool = False,
113
+ case_sensitive: bool = False,
114
+ ) -> pd.DataFrame:
115
+ """Search datasets by metadata fields.
116
+
117
+ ``usage_tag`` matches individual semicolon-separated tags in ``usage_tags``.
118
+ Other text filters use exact matching by default, case-insensitively.
119
+ """
120
+
121
+ df = load_catalog(
122
+ repo_id=repo_id,
123
+ revision=revision,
124
+ token=token,
125
+ cache_dir=cache_dir,
126
+ metadata_path=metadata_path,
127
+ prefer_remote=prefer_remote,
128
+ )
129
+
130
+ filters = {
131
+ "dataset_uid": dataset_uid,
132
+ "dataset_id": dataset_id,
133
+ "gse_id": gse_id,
134
+ "detailed_condition": detailed_condition,
135
+ "detail_source": detail_source,
136
+ "condition": condition,
137
+ "sample_type": sample_type,
138
+ "species": species,
139
+ "reference": reference,
140
+ }
141
+ for column, values in filters.items():
142
+ if values is not None:
143
+ df = df[_isin(df[column], values, case_sensitive=case_sensitive)]
144
+
145
+ if usage_tag is not None:
146
+ df = df[_has_usage_tag(df["usage_tags"], usage_tag, case_sensitive=case_sensitive)]
147
+
148
+ return df.reset_index(drop=True)
149
+
150
+
151
+ def resolve_matches(
152
+ *,
153
+ max_auto_matches: int = DEFAULT_MAX_AUTO_MATCHES,
154
+ require_unique_dataset_id: bool = False,
155
+ **search_kwargs,
156
+ ) -> MatchResult:
157
+ """Resolve a query for download/load operations."""
158
+
159
+ rows = search_datasets(**search_kwargs)
160
+ matched_ids = rows["dataset_uid"].astype(str).tolist()
161
+
162
+ if rows.empty:
163
+ return MatchResult(rows=rows, matched_ids=matched_ids)
164
+
165
+ if require_unique_dataset_id and search_kwargs.get("dataset_id") is not None:
166
+ if len(rows) > 1 and search_kwargs.get("dataset_uid") is None and search_kwargs.get("gse_id") is None:
167
+ raise AmbiguousDatasetError(search_kwargs["dataset_id"], matched_ids)
168
+
169
+ if len(rows) > max_auto_matches:
170
+ raise TooManyMatchesError(len(rows), matched_ids, max_auto_matches)
171
+
172
+ return MatchResult(rows=rows, matched_ids=matched_ids)
173
+
174
+
175
+ def list_dataset_ids(**catalog_kwargs) -> List[str]:
176
+ """Return sorted dataset IDs."""
177
+
178
+ return list_values("dataset-id", **catalog_kwargs)
179
+
180
+
181
+ def list_detailed_conditions(**catalog_kwargs) -> List[str]:
182
+ """Return sorted detailed conditions."""
183
+
184
+ return list_values("detailed-condition", **catalog_kwargs)
185
+
186
+
187
+ def list_detail_sources(**catalog_kwargs) -> List[str]:
188
+ """Return sorted detail sources."""
189
+
190
+ return list_values("detail-source", **catalog_kwargs)
191
+
192
+
193
+ def list_usage_tags(**catalog_kwargs) -> List[str]:
194
+ """Return sorted individual usage tags."""
195
+
196
+ return list_values("usage-tag", **catalog_kwargs)
197
+
198
+
199
+ def list_values(field: str, **catalog_kwargs) -> List[str]:
200
+ """Return sorted unique values for a metadata field.
201
+
202
+ ``field`` accepts CLI-style names such as ``usage-tags`` and metadata column
203
+ names such as ``usage_tags``.
204
+ """
205
+
206
+ normalized = field.strip().replace("_", "-")
207
+ column = LISTABLE_FIELDS.get(normalized, field.strip())
208
+ df = load_catalog(**catalog_kwargs)
209
+
210
+ if column not in df.columns:
211
+ choices = ", ".join(sorted(LISTABLE_FIELDS))
212
+ raise ValueError(f"Unknown list field {field!r}. Available fields: {choices}")
213
+
214
+ if column != "usage_tags":
215
+ return sorted(value for value in df[column].dropna().astype(str).unique() if value)
216
+
217
+ tags = set()
218
+ for value in df["usage_tags"].dropna().astype(str):
219
+ tags.update(tag.strip() for tag in value.split(";") if tag.strip())
220
+ return sorted(tags)
221
+
222
+
223
+ def _normalize_catalog(df: pd.DataFrame) -> pd.DataFrame:
224
+ df = df.copy()
225
+ df.columns = [str(column).strip() for column in df.columns]
226
+
227
+ required = {
228
+ "dataset_uid",
229
+ "dataset_id",
230
+ "gse_id",
231
+ "file_path",
232
+ "usage_tags",
233
+ "detail_source",
234
+ "detailed_condition",
235
+ }
236
+ missing = sorted(required - set(df.columns))
237
+ if missing:
238
+ raise ValueError(f"Metadata catalog is missing required columns: {', '.join(missing)}")
239
+
240
+ for column in df.columns:
241
+ if pd.api.types.is_object_dtype(df[column]):
242
+ df[column] = df[column].fillna("").astype(str).str.strip()
243
+
244
+ return df
245
+
246
+
247
+ def _read_metadata_csv(path: Union[str, Path]) -> pd.DataFrame:
248
+ with Path(path).open("r", encoding="utf-8", newline="") as handle:
249
+ return pd.read_csv(handle, engine="python")
250
+
251
+
252
+ def _as_list(values: Union[str, Iterable[str]]) -> List[str]:
253
+ if isinstance(values, str):
254
+ return [values]
255
+ return [str(value) for value in values]
256
+
257
+
258
+ def _normalize_value(value: str, *, case_sensitive: bool) -> str:
259
+ value = str(value).strip()
260
+ return value if case_sensitive else value.casefold()
261
+
262
+
263
+ def _isin(series: pd.Series, values: Union[str, Iterable[str]], *, case_sensitive: bool) -> pd.Series:
264
+ normalized_values = {
265
+ _normalize_value(value, case_sensitive=case_sensitive)
266
+ for value in _as_list(values)
267
+ }
268
+ normalized_series = series.astype(str).map(
269
+ lambda value: _normalize_value(value, case_sensitive=case_sensitive)
270
+ )
271
+ return normalized_series.isin(normalized_values)
272
+
273
+
274
+ def _has_usage_tag(series: pd.Series, values: Union[str, Iterable[str]], *, case_sensitive: bool) -> pd.Series:
275
+ wanted = {
276
+ _normalize_value(value, case_sensitive=case_sensitive)
277
+ for value in _as_list(values)
278
+ }
279
+
280
+ def has_tag(value: str) -> bool:
281
+ tags = {
282
+ _normalize_value(tag, case_sensitive=case_sensitive)
283
+ for tag in str(value).split(";")
284
+ if tag.strip()
285
+ }
286
+ return bool(tags & wanted)
287
+
288
+ return series.astype(str).map(has_tag)
scmora_db/cli.py ADDED
@@ -0,0 +1,204 @@
1
+ """Command-line interface for scmora-db."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+ from pathlib import Path
8
+ from typing import List, Optional, Union
9
+
10
+ from ._version import __version__
11
+ from .catalog import (
12
+ DEFAULT_MAX_AUTO_MATCHES,
13
+ DEFAULT_REPO_ID,
14
+ LISTABLE_FIELDS,
15
+ list_values,
16
+ search_datasets,
17
+ )
18
+ from .download import download_datasets
19
+ from .exceptions import AmbiguousDatasetError, TooManyMatchesError
20
+
21
+
22
+ def main(argv: Optional[List[str]] = None) -> int:
23
+ parser = _build_parser()
24
+ args = parser.parse_args(argv)
25
+
26
+ try:
27
+ return args.func(args)
28
+ except TooManyMatchesError as exc:
29
+ print(str(exc), file=sys.stderr)
30
+ for dataset_uid in exc.dataset_uids:
31
+ print(dataset_uid)
32
+ return 2
33
+ except AmbiguousDatasetError as exc:
34
+ print(str(exc), file=sys.stderr)
35
+ for dataset_uid in exc.matches:
36
+ print(dataset_uid)
37
+ return 2
38
+
39
+
40
+ def _build_parser() -> argparse.ArgumentParser:
41
+ parser = argparse.ArgumentParser(
42
+ prog="scmora-db",
43
+ description="Search, download, and load SCMORA .h5mu datasets.",
44
+ )
45
+ parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
46
+ parser.add_argument("--repo-id", default=DEFAULT_REPO_ID, help="Hugging Face dataset repo ID.")
47
+ parser.add_argument("--revision", default=None, help="Hugging Face revision, branch, or commit.")
48
+ parser.add_argument("--token", default=None, help="Hugging Face token for private datasets.")
49
+ parser.add_argument("--cache-dir", default=None, help="Hugging Face cache directory.")
50
+ parser.add_argument("--metadata-path", default=None, help="Use a local metadata CSV.")
51
+ parser.add_argument(
52
+ "--prefer-remote",
53
+ action="store_true",
54
+ help="Download metadata.csv from Hugging Face instead of using bundled metadata.",
55
+ )
56
+
57
+ subparsers = parser.add_subparsers(dest="command", required=True)
58
+
59
+ search_parser = subparsers.add_parser("search", help="Search metadata without downloading .h5mu files.")
60
+ _add_filter_args(search_parser)
61
+ search_parser.add_argument("--columns", default=None, help="Comma-separated columns to print.")
62
+ search_parser.add_argument("--limit", type=int, default=None, help="Maximum rows to print.")
63
+ search_parser.set_defaults(func=_cmd_search)
64
+
65
+ download_parser = subparsers.add_parser("download", help="Download matching .h5mu files.")
66
+ _add_filter_args(download_parser)
67
+ download_parser.add_argument("--local-dir", default=None, help="Optional local output directory.")
68
+ download_parser.add_argument("--force-download", action="store_true", help="Force redownload.")
69
+ download_parser.add_argument("--max-auto-matches", type=int, default=DEFAULT_MAX_AUTO_MATCHES)
70
+ download_parser.set_defaults(func=_cmd_download)
71
+
72
+ load_parser = subparsers.add_parser("load", help="Download and open matching .h5mu files.")
73
+ _add_filter_args(load_parser)
74
+ load_parser.add_argument("--local-dir", default=None, help="Optional local output directory.")
75
+ load_parser.add_argument("--force-download", action="store_true", help="Force redownload.")
76
+ load_parser.add_argument("--max-auto-matches", type=int, default=DEFAULT_MAX_AUTO_MATCHES)
77
+ load_parser.add_argument("--backed", default=None, help='MuData backed mode, for example "r".')
78
+ load_parser.set_defaults(func=_cmd_load)
79
+
80
+ list_parser = subparsers.add_parser("list", help="List available metadata values.")
81
+ list_parser.add_argument(
82
+ "field",
83
+ choices=sorted(LISTABLE_FIELDS),
84
+ )
85
+ list_parser.set_defaults(func=_cmd_list)
86
+
87
+ return parser
88
+
89
+
90
+ def _add_filter_args(parser: argparse.ArgumentParser) -> None:
91
+ parser.add_argument("--dataset-id", action="append", help="Filter by dataset_id. Can be repeated.")
92
+ parser.add_argument("--dataset-uid", action="append", help="Filter by unique gse_id/dataset_id.")
93
+ parser.add_argument("--gse-id", action="append", help="Filter by GSE ID. Can be repeated.")
94
+ parser.add_argument("--detailed-condition", action="append", help="Filter by detailed_condition.")
95
+ parser.add_argument("--usage-tag", action="append", help="Filter by one usage tag.")
96
+ parser.add_argument("--detail-source", action="append", help="Filter by detail_source.")
97
+ parser.add_argument("--condition", action="append", help="Filter by broad condition.")
98
+ parser.add_argument("--sample-type", action="append", help="Filter by sample_type.")
99
+ parser.add_argument("--species", action="append", help="Filter by species.")
100
+ parser.add_argument("--reference", action="append", help="Filter by reference genome.")
101
+
102
+
103
+ def _catalog_kwargs(args) -> dict:
104
+ return {
105
+ "repo_id": args.repo_id,
106
+ "revision": args.revision,
107
+ "token": args.token,
108
+ "cache_dir": args.cache_dir,
109
+ "metadata_path": args.metadata_path,
110
+ "prefer_remote": args.prefer_remote,
111
+ }
112
+
113
+
114
+ def _filter_kwargs(args) -> dict:
115
+ return {
116
+ "dataset_id": args.dataset_id,
117
+ "dataset_uid": args.dataset_uid,
118
+ "gse_id": args.gse_id,
119
+ "detailed_condition": args.detailed_condition,
120
+ "usage_tag": args.usage_tag,
121
+ "detail_source": args.detail_source,
122
+ "condition": args.condition,
123
+ "sample_type": args.sample_type,
124
+ "species": args.species,
125
+ "reference": args.reference,
126
+ }
127
+
128
+
129
+ def _cmd_search(args) -> int:
130
+ df = search_datasets(**_filter_kwargs(args), **_catalog_kwargs(args))
131
+ columns = [
132
+ "dataset_uid",
133
+ "dataset_id",
134
+ "gse_id",
135
+ "detailed_condition",
136
+ "usage_tags",
137
+ "detail_source",
138
+ "file_path",
139
+ ]
140
+ if args.columns:
141
+ columns = [column.strip() for column in args.columns.split(",") if column.strip()]
142
+ if args.limit is not None:
143
+ df = df.head(args.limit)
144
+ print(df.loc[:, columns].to_csv(index=False).rstrip())
145
+ return 0
146
+
147
+
148
+ def _cmd_download(args) -> int:
149
+ paths = download_datasets(
150
+ **_filter_kwargs(args),
151
+ **_catalog_kwargs(args),
152
+ max_auto_matches=args.max_auto_matches,
153
+ local_dir=args.local_dir,
154
+ force_download=args.force_download,
155
+ )
156
+ _print_paths(paths)
157
+ return 0
158
+
159
+
160
+ def _cmd_load(args) -> int:
161
+ from .io import load_datasets
162
+
163
+ objects = load_datasets(
164
+ **_filter_kwargs(args),
165
+ **_catalog_kwargs(args),
166
+ max_auto_matches=args.max_auto_matches,
167
+ local_dir=args.local_dir,
168
+ force_download=args.force_download,
169
+ backed=args.backed,
170
+ )
171
+ if not isinstance(objects, list):
172
+ objects = [objects]
173
+ for obj in objects:
174
+ print(_summarize_mudata(obj))
175
+ return 0
176
+
177
+
178
+ def _cmd_list(args) -> int:
179
+ kwargs = _catalog_kwargs(args)
180
+ values = list_values(args.field, **kwargs)
181
+
182
+ for value in values:
183
+ print(value)
184
+ return 0
185
+
186
+
187
+ def _print_paths(paths: Union[str, List[str]]) -> None:
188
+ if isinstance(paths, str):
189
+ print(paths)
190
+ return
191
+ for path in paths:
192
+ print(path)
193
+
194
+
195
+ def _summarize_mudata(obj) -> str:
196
+ n_obs = getattr(obj, "n_obs", "?")
197
+ n_vars = getattr(obj, "n_vars", "?")
198
+ mod = getattr(obj, "mod", {})
199
+ modalities = ",".join(mod.keys()) if hasattr(mod, "keys") else "?"
200
+ return f"MuData(n_obs={n_obs}, n_vars={n_vars}, modalities={modalities})"
201
+
202
+
203
+ if __name__ == "__main__":
204
+ raise SystemExit(main())
scmora_db/download.py ADDED
@@ -0,0 +1,86 @@
1
+ """Download .h5mu files from Hugging Face."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import List, Optional, Union
7
+
8
+ from .catalog import DEFAULT_MAX_AUTO_MATCHES, DEFAULT_REPO_ID, DEFAULT_REPO_TYPE, resolve_matches
9
+
10
+
11
+ def download_datasets(
12
+ *,
13
+ dataset_id: Optional[Union[str, List[str]]] = None,
14
+ dataset_uid: Optional[Union[str, List[str]]] = None,
15
+ gse_id: Optional[Union[str, List[str]]] = None,
16
+ detailed_condition: Optional[Union[str, List[str]]] = None,
17
+ usage_tag: Optional[Union[str, List[str]]] = None,
18
+ detail_source: Optional[Union[str, List[str]]] = None,
19
+ condition: Optional[Union[str, List[str]]] = None,
20
+ sample_type: Optional[Union[str, List[str]]] = None,
21
+ species: Optional[Union[str, List[str]]] = None,
22
+ reference: Optional[Union[str, List[str]]] = None,
23
+ repo_id: str = DEFAULT_REPO_ID,
24
+ revision: Optional[str] = None,
25
+ token: Optional[Union[str, bool]] = None,
26
+ cache_dir: Optional[Union[str, Path]] = None,
27
+ metadata_path: Optional[Union[str, Path]] = None,
28
+ prefer_remote: bool = False,
29
+ max_auto_matches: int = DEFAULT_MAX_AUTO_MATCHES,
30
+ local_dir: Optional[Union[str, Path]] = None,
31
+ force_download: bool = False,
32
+ ) -> Union[str, List[str]]:
33
+ """Download matching ``.h5mu`` files and return local paths.
34
+
35
+ If one dataset matches, a single path string is returned. If two to five
36
+ datasets match, a list of path strings is returned. More than five matches
37
+ raises ``TooManyMatchesError``.
38
+ """
39
+
40
+ result = resolve_matches(
41
+ dataset_id=dataset_id,
42
+ dataset_uid=dataset_uid,
43
+ gse_id=gse_id,
44
+ detailed_condition=detailed_condition,
45
+ usage_tag=usage_tag,
46
+ detail_source=detail_source,
47
+ condition=condition,
48
+ sample_type=sample_type,
49
+ species=species,
50
+ reference=reference,
51
+ repo_id=repo_id,
52
+ revision=revision,
53
+ token=token,
54
+ cache_dir=cache_dir,
55
+ metadata_path=metadata_path,
56
+ prefer_remote=prefer_remote,
57
+ max_auto_matches=max_auto_matches,
58
+ require_unique_dataset_id=True,
59
+ )
60
+
61
+ if result.rows.empty:
62
+ return []
63
+
64
+ try:
65
+ from huggingface_hub import hf_hub_download
66
+ except ImportError as exc:
67
+ raise ImportError(
68
+ "Downloading datasets requires the dependency 'huggingface_hub'. "
69
+ "Install scmora-db with its default dependencies: pip install scmora-db"
70
+ ) from exc
71
+
72
+ paths = []
73
+ for _, row in result.rows.iterrows():
74
+ path = hf_hub_download(
75
+ repo_id=repo_id,
76
+ repo_type=DEFAULT_REPO_TYPE,
77
+ filename=row["file_path"],
78
+ revision=revision,
79
+ token=token,
80
+ cache_dir=cache_dir,
81
+ local_dir=local_dir,
82
+ force_download=force_download,
83
+ )
84
+ paths.append(path)
85
+
86
+ return paths[0] if len(paths) == 1 else paths
@@ -0,0 +1,34 @@
1
+ """Custom exceptions for scmora-db."""
2
+
3
+
4
+ class ScmoraDbError(Exception):
5
+ """Base class for scmora-db errors."""
6
+
7
+
8
+ class AmbiguousDatasetError(ScmoraDbError):
9
+ """Raised when a dataset_id query matches multiple datasets."""
10
+
11
+ def __init__(self, dataset_id, matches):
12
+ self.dataset_id = dataset_id
13
+ self.matches = list(matches)
14
+ message = (
15
+ f"dataset_id {dataset_id!r} matched multiple datasets. "
16
+ "Use dataset_uid or add gse_id to choose one: "
17
+ + ", ".join(self.matches)
18
+ )
19
+ super().__init__(message)
20
+
21
+
22
+ class TooManyMatchesError(ScmoraDbError):
23
+ """Raised when a download or load query matches too many datasets."""
24
+
25
+ def __init__(self, count, dataset_uids, limit):
26
+ self.count = count
27
+ self.dataset_uids = list(dataset_uids)
28
+ self.limit = limit
29
+ ids = ", ".join(self.dataset_uids)
30
+ message = (
31
+ f"Query matched {count} datasets, which is more than the automatic "
32
+ f"limit of {limit}. Matched dataset_uids: {ids}"
33
+ )
34
+ super().__init__(message)