dnakit 0.1.0.dev0__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.
- dnakit/__init__.py +108 -0
- dnakit/_version.py +5 -0
- dnakit/alignment/__init__.py +6 -0
- dnakit/alignment/pairwise.py +512 -0
- dnakit/alignment/results.py +188 -0
- dnakit/backends/__init__.py +80 -0
- dnakit/backends/external.py +517 -0
- dnakit/backends/registry.py +207 -0
- dnakit/batch.py +452 -0
- dnakit/cache/__init__.py +5 -0
- dnakit/cache/store.py +424 -0
- dnakit/chunking.py +920 -0
- dnakit/cli/__init__.py +5 -0
- dnakit/cli/__main__.py +6 -0
- dnakit/cli/app.py +814 -0
- dnakit/cli/workflow.py +94 -0
- dnakit/config/__init__.py +5 -0
- dnakit/config/loader.py +381 -0
- dnakit/core/__init__.py +80 -0
- dnakit/core/_json.py +206 -0
- dnakit/core/backend_info.py +70 -0
- dnakit/core/collection.py +163 -0
- dnakit/core/coordinates.py +295 -0
- dnakit/core/enums.py +95 -0
- dnakit/core/facade.py +719 -0
- dnakit/core/feature.py +90 -0
- dnakit/core/gap.py +82 -0
- dnakit/core/issues.py +62 -0
- dnakit/core/provenance.py +305 -0
- dnakit/core/record.py +161 -0
- dnakit/core/results.py +134 -0
- dnakit/core/sequence.py +327 -0
- dnakit/datasets/__init__.py +100 -0
- dnakit/datasets/_advanced_shared.py +198 -0
- dnakit/datasets/_metadata.py +33 -0
- dnakit/datasets/advanced_deduplicate.py +183 -0
- dnakit/datasets/advanced_split.py +302 -0
- dnakit/datasets/clustering.py +286 -0
- dnakit/datasets/config.py +531 -0
- dnakit/datasets/deduplicate.py +303 -0
- dnakit/datasets/evaluation.py +244 -0
- dnakit/datasets/filter.py +135 -0
- dnakit/datasets/neural_clustering.py +221 -0
- dnakit/datasets/results.py +520 -0
- dnakit/datasets/split.py +450 -0
- dnakit/descriptors/__init__.py +102 -0
- dnakit/descriptors/_dinucleotide.py +345 -0
- dnakit/descriptors/_shared.py +180 -0
- dnakit/descriptors/all.py +917 -0
- dnakit/descriptors/basic.py +193 -0
- dnakit/descriptors/codon.py +195 -0
- dnakit/descriptors/complexity.py +201 -0
- dnakit/descriptors/entropy.py +101 -0
- dnakit/descriptors/homopolymer.py +105 -0
- dnakit/descriptors/kmer.py +88 -0
- dnakit/descriptors/results.py +286 -0
- dnakit/descriptors/schema.py +414 -0
- dnakit/descriptors/window.py +203 -0
- dnakit/download/__init__.py +54 -0
- dnakit/download/catalogs.py +166 -0
- dnakit/download/ena.py +131 -0
- dnakit/download/encode.py +103 -0
- dnakit/download/files.py +469 -0
- dnakit/download/indexes.py +233 -0
- dnakit/download/metadata.py +164 -0
- dnakit/download/models.py +227 -0
- dnakit/download/ncbi.py +333 -0
- dnakit/download/sequence.py +133 -0
- dnakit/download/ucsc.py +99 -0
- dnakit/evaluation/__init__.py +81 -0
- dnakit/evaluation/_shared.py +288 -0
- dnakit/evaluation/collection.py +270 -0
- dnakit/evaluation/config.py +591 -0
- dnakit/evaluation/distribution.py +287 -0
- dnakit/evaluation/frechet.py +223 -0
- dnakit/evaluation/generative.py +381 -0
- dnakit/evaluation/reference.py +409 -0
- dnakit/evaluation/results.py +137 -0
- dnakit/evaluation/scorecard.py +211 -0
- dnakit/evaluation/sequence.py +403 -0
- dnakit/evaluation/synthesis.py +249 -0
- dnakit/exceptions.py +189 -0
- dnakit/fingerprints/__init__.py +109 -0
- dnakit/fingerprints/_shared.py +116 -0
- dnakit/fingerprints/advanced.py +829 -0
- dnakit/fingerprints/bit.py +229 -0
- dnakit/fingerprints/encoding.py +351 -0
- dnakit/fingerprints/kmer.py +170 -0
- dnakit/fingerprints/results.py +531 -0
- dnakit/fingerprints/sketch.py +246 -0
- dnakit/io/__init__.py +112 -0
- dnakit/io/_advanced_common.py +105 -0
- dnakit/io/_formats.py +994 -0
- dnakit/io/annotations.py +1179 -0
- dnakit/io/api.py +759 -0
- dnakit/io/config.py +315 -0
- dnakit/io/genbank.py +523 -0
- dnakit/io/indexed.py +1146 -0
- dnakit/io/metadata.py +452 -0
- dnakit/io/parquet.py +56 -0
- dnakit/io/results.py +131 -0
- dnakit/io/source.py +138 -0
- dnakit/io/tables.py +1237 -0
- dnakit/molbio/__init__.py +101 -0
- dnakit/molbio/_shared.py +266 -0
- dnakit/molbio/assembly.py +279 -0
- dnakit/molbio/crispr.py +181 -0
- dnakit/molbio/optimization.py +897 -0
- dnakit/molbio/primer3_design.py +667 -0
- dnakit/molbio/primers.py +899 -0
- dnakit/molbio/restriction.py +573 -0
- dnakit/molbio/results.py +583 -0
- dnakit/ops/__init__.py +588 -0
- dnakit/ops/_common.py +201 -0
- dnakit/ops/circular.py +193 -0
- dnakit/ops/concat.py +204 -0
- dnakit/ops/direction.py +27 -0
- dnakit/ops/edit.py +443 -0
- dnakit/ops/evolution.py +991 -0
- dnakit/ops/mutation.py +151 -0
- dnakit/ops/records.py +1012 -0
- dnakit/ops/translation.py +257 -0
- dnakit/patterns/__init__.py +81 -0
- dnakit/patterns/_shared.py +282 -0
- dnakit/patterns/coding.py +360 -0
- dnakit/patterns/crispr.py +337 -0
- dnakit/patterns/motif.py +721 -0
- dnakit/patterns/regions.py +341 -0
- dnakit/patterns/repeats.py +480 -0
- dnakit/patterns/restriction.py +281 -0
- dnakit/patterns/results.py +411 -0
- dnakit/py.typed +1 -0
- dnakit/references/__init__.py +27 -0
- dnakit/references/models.py +155 -0
- dnakit/references/ncbi.py +785 -0
- dnakit/representations/__init__.py +29 -0
- dnakit/representations/backends.py +928 -0
- dnakit/representations/checkpoints.py +356 -0
- dnakit/representations/extraction.py +220 -0
- dnakit/representations/models.py +395 -0
- dnakit/representations/results.py +79 -0
- dnakit/search/__init__.py +132 -0
- dnakit/search/_http.py +271 -0
- dnakit/search/_shared.py +108 -0
- dnakit/search/blast.py +516 -0
- dnakit/search/ena.py +245 -0
- dnakit/search/encode.py +102 -0
- dnakit/search/ensembl.py +874 -0
- dnakit/search/models.py +159 -0
- dnakit/search/ncbi.py +756 -0
- dnakit/search/ucsc.py +388 -0
- dnakit/secondary_structure/__init__.py +38 -0
- dnakit/secondary_structure/dotbracket.py +410 -0
- dnakit/secondary_structure/nupack.py +589 -0
- dnakit/secondary_structure/results.py +142 -0
- dnakit/similarity/__init__.py +125 -0
- dnakit/similarity/_shared.py +152 -0
- dnakit/similarity/approximate.py +258 -0
- dnakit/similarity/compare.py +162 -0
- dnakit/similarity/dashing.py +936 -0
- dnakit/similarity/distance.py +279 -0
- dnakit/similarity/index.py +365 -0
- dnakit/similarity/matrix.py +254 -0
- dnakit/similarity/results.py +676 -0
- dnakit/similarity/search.py +286 -0
- dnakit/similarity/sketch.py +96 -0
- dnakit/similarity/vector.py +662 -0
- dnakit/standardize/__init__.py +77 -0
- dnakit/standardize/_shared.py +100 -0
- dnakit/standardize/config.py +411 -0
- dnakit/standardize/gaps.py +1060 -0
- dnakit/standardize/normalize.py +565 -0
- dnakit/standardize/results.py +247 -0
- dnakit/standardize/validate.py +487 -0
- dnakit/structure3d/__init__.py +47 -0
- dnakit/structure3d/_shared.py +47 -0
- dnakit/structure3d/geometry.py +773 -0
- dnakit/structure3d/pdb.py +253 -0
- dnakit/structure3d/results.py +233 -0
- dnakit/structure3d/threedna.py +174 -0
- dnakit/thermodynamics/__init__.py +112 -0
- dnakit/thermodynamics/_shared.py +92 -0
- dnakit/thermodynamics/backends.py +653 -0
- dnakit/thermodynamics/calculations.py +837 -0
- dnakit/thermodynamics/config.py +175 -0
- dnakit/thermodynamics/equilibrium.py +340 -0
- dnakit/thermodynamics/optics.py +385 -0
- dnakit/thermodynamics/parameters.py +79 -0
- dnakit/thermodynamics/results.py +445 -0
- dnakit/visualization/__init__.py +43 -0
- dnakit/visualization/_svg.py +185 -0
- dnakit/visualization/advanced.py +314 -0
- dnakit/visualization/config.py +255 -0
- dnakit/visualization/export.py +349 -0
- dnakit/visualization/matrix.py +279 -0
- dnakit/visualization/report.py +431 -0
- dnakit/visualization/results.py +236 -0
- dnakit/visualization/sequence.py +459 -0
- dnakit/workflows/__init__.py +45 -0
- dnakit/workflows/manifest.py +365 -0
- dnakit/workflows/runner.py +1548 -0
- dnakit/workflows/schema.py +898 -0
- dnakit-0.1.0.dev0.dist-info/METADATA +126 -0
- dnakit-0.1.0.dev0.dist-info/RECORD +210 -0
- dnakit-0.1.0.dev0.dist-info/WHEEL +5 -0
- dnakit-0.1.0.dev0.dist-info/entry_points.txt +2 -0
- dnakit-0.1.0.dev0.dist-info/licenses/DISCLAIMER.md +26 -0
- dnakit-0.1.0.dev0.dist-info/licenses/LICENSE +21 -0
- dnakit-0.1.0.dev0.dist-info/licenses/THIRD_PARTY_NOTICES.md +73 -0
- dnakit-0.1.0.dev0.dist-info/top_level.txt +1 -0
dnakit/search/ncbi.py
ADDED
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
"""NCBI Datasets and Entrez E-utilities query adapters."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from collections.abc import Mapping, Sequence
|
|
7
|
+
from typing import Any, Literal
|
|
8
|
+
from urllib.parse import quote
|
|
9
|
+
|
|
10
|
+
from dnakit.exceptions import ConfigurationError, QueryError
|
|
11
|
+
|
|
12
|
+
from ._http import (
|
|
13
|
+
build_url,
|
|
14
|
+
limited_records,
|
|
15
|
+
mapping_records,
|
|
16
|
+
redact_url,
|
|
17
|
+
request_json,
|
|
18
|
+
require_text,
|
|
19
|
+
)
|
|
20
|
+
from ._shared import (
|
|
21
|
+
adapter_provenance,
|
|
22
|
+
query_values,
|
|
23
|
+
quoted_path_values,
|
|
24
|
+
resolved_config,
|
|
25
|
+
)
|
|
26
|
+
from ._shared import (
|
|
27
|
+
page_size as validate_page_size,
|
|
28
|
+
)
|
|
29
|
+
from .models import QueryResult, SearchConfig
|
|
30
|
+
|
|
31
|
+
DEFAULT_NCBI_DATASETS_URL = "https://api.ncbi.nlm.nih.gov/datasets/v2"
|
|
32
|
+
DEFAULT_NCBI_EUTILS_URL = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils"
|
|
33
|
+
_ASSEMBLY_ACCESSION = re.compile(r"^GC[AF]_\d+\.\d+$", flags=re.IGNORECASE)
|
|
34
|
+
_REFSEQ_ACCESSION = re.compile(r"^[A-Z]{1,6}_\d+(?:\.\d+)?$", flags=re.IGNORECASE)
|
|
35
|
+
_VIRUS_ACCESSION = re.compile(r"^[A-Z]{1,6}_?\d{3,}(?:\.\d+)?$", flags=re.IGNORECASE)
|
|
36
|
+
_ENTREZ_DATABASES = frozenset(
|
|
37
|
+
{
|
|
38
|
+
"assembly",
|
|
39
|
+
"bioproject",
|
|
40
|
+
"biosample",
|
|
41
|
+
"clinvar",
|
|
42
|
+
"gds",
|
|
43
|
+
"gene",
|
|
44
|
+
"geoprofiles",
|
|
45
|
+
"nuccore",
|
|
46
|
+
"protein",
|
|
47
|
+
"pubmed",
|
|
48
|
+
"snp",
|
|
49
|
+
"sra",
|
|
50
|
+
}
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _datasets_headers(config: SearchConfig) -> dict[str, str]:
|
|
55
|
+
return {} if config.api_key is None else {"api-key": config.api_key}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _page_metadata(payload: Mapping[str, Any]) -> tuple[int | None, str | None]:
|
|
59
|
+
raw_total = payload.get("total_count")
|
|
60
|
+
total = raw_total if isinstance(raw_total, int) and not isinstance(raw_total, bool) else None
|
|
61
|
+
token = payload.get("next_page_token")
|
|
62
|
+
next_token = token if isinstance(token, str) and token.strip() else None
|
|
63
|
+
return total, next_token
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def taxonomy(
|
|
67
|
+
query: str | int | Sequence[str | int],
|
|
68
|
+
*,
|
|
69
|
+
report: Literal["summary", "names", "related"] = "summary",
|
|
70
|
+
children: bool = False,
|
|
71
|
+
include_lineage: bool = True,
|
|
72
|
+
ranks: Sequence[str] = (),
|
|
73
|
+
page_size: int = 20,
|
|
74
|
+
page_token: str | None = None,
|
|
75
|
+
config: SearchConfig | None = None,
|
|
76
|
+
api_base_url: str = DEFAULT_NCBI_DATASETS_URL,
|
|
77
|
+
) -> QueryResult:
|
|
78
|
+
"""Query NCBI Taxonomy by TaxID, scientific name, common name, or alias.
|
|
79
|
+
|
|
80
|
+
Use ``report="names"`` for the complete names/aliases report. Use
|
|
81
|
+
``report="related"`` with one numeric TaxID for child nodes and lineage.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
resolved = resolved_config(config)
|
|
85
|
+
size = validate_page_size(page_size, resolved)
|
|
86
|
+
values = query_values(query, maximum=100)
|
|
87
|
+
if report not in {"summary", "names", "related"}:
|
|
88
|
+
raise ConfigurationError("Unsupported taxonomy report.", code="INVALID_QUERY")
|
|
89
|
+
params: list[tuple[str, object]] = [("page_size", size)]
|
|
90
|
+
if page_token is not None:
|
|
91
|
+
params.append(("page_token", require_text(page_token, "page_token")))
|
|
92
|
+
if ranks:
|
|
93
|
+
params.append(
|
|
94
|
+
("ranks", tuple(require_text(rank, "rank", max_length=128) for rank in ranks))
|
|
95
|
+
)
|
|
96
|
+
if report == "related":
|
|
97
|
+
if len(values) != 1 or not values[0].isdigit():
|
|
98
|
+
raise ConfigurationError(
|
|
99
|
+
"taxonomy report='related' requires one numeric TaxID.", code="INVALID_QUERY"
|
|
100
|
+
)
|
|
101
|
+
path = f"/taxonomy/taxon/{values[0]}/related_ids"
|
|
102
|
+
params.append(("include_lineage", include_lineage))
|
|
103
|
+
else:
|
|
104
|
+
suffix = "name_report" if report == "names" else "dataset_report"
|
|
105
|
+
path = f"/taxonomy/taxon/{quoted_path_values(values)}/{suffix}"
|
|
106
|
+
params.append(("children", children))
|
|
107
|
+
url = build_url(api_base_url, path, params)
|
|
108
|
+
payload = request_json(
|
|
109
|
+
url,
|
|
110
|
+
resolved,
|
|
111
|
+
provider="NCBI Datasets",
|
|
112
|
+
headers=_datasets_headers(resolved),
|
|
113
|
+
)
|
|
114
|
+
if not isinstance(payload, Mapping):
|
|
115
|
+
raise QueryError(
|
|
116
|
+
"NCBI Taxonomy returned an unexpected response.", code="QUERY_RESPONSE_ERROR"
|
|
117
|
+
)
|
|
118
|
+
records: tuple[dict[str, object], ...]
|
|
119
|
+
if report == "related":
|
|
120
|
+
raw_ids = payload.get("tax_ids")
|
|
121
|
+
records = (
|
|
122
|
+
tuple(
|
|
123
|
+
{"tax_id": item}
|
|
124
|
+
for item in raw_ids
|
|
125
|
+
if isinstance(item, int) and not isinstance(item, bool)
|
|
126
|
+
)
|
|
127
|
+
if isinstance(raw_ids, list)
|
|
128
|
+
else ()
|
|
129
|
+
)
|
|
130
|
+
else:
|
|
131
|
+
records = mapping_records(payload, key="reports")
|
|
132
|
+
records = limited_records(records, resolved)
|
|
133
|
+
total, next_token = _page_metadata(payload)
|
|
134
|
+
total = max(total, len(records)) if total is not None else len(records)
|
|
135
|
+
return QueryResult(
|
|
136
|
+
"taxonomy",
|
|
137
|
+
"NCBI Datasets",
|
|
138
|
+
redact_url(url),
|
|
139
|
+
records,
|
|
140
|
+
adapter_provenance(
|
|
141
|
+
"NCBI Taxonomy",
|
|
142
|
+
citation_url="https://www.ncbi.nlm.nih.gov/datasets/docs/v2/how-tos/taxonomy/taxonomy/",
|
|
143
|
+
filters={
|
|
144
|
+
"query": values,
|
|
145
|
+
"report": report,
|
|
146
|
+
"children": children,
|
|
147
|
+
"ranks": tuple(ranks),
|
|
148
|
+
},
|
|
149
|
+
),
|
|
150
|
+
total_count=total,
|
|
151
|
+
next_page_token=next_token,
|
|
152
|
+
metadata={"coordinate_system": None, "report": report},
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def assembly(
|
|
157
|
+
query: str | Sequence[str],
|
|
158
|
+
*,
|
|
159
|
+
by: Literal["auto", "accession", "taxon"] = "auto",
|
|
160
|
+
reference_only: bool = False,
|
|
161
|
+
assembly_source: Literal["all", "refseq", "genbank"] = "all",
|
|
162
|
+
current_only: bool = True,
|
|
163
|
+
page_size: int = 20,
|
|
164
|
+
page_token: str | None = None,
|
|
165
|
+
config: SearchConfig | None = None,
|
|
166
|
+
api_base_url: str = DEFAULT_NCBI_DATASETS_URL,
|
|
167
|
+
) -> QueryResult:
|
|
168
|
+
"""Query genome assemblies and their statistics from NCBI Datasets."""
|
|
169
|
+
|
|
170
|
+
resolved = resolved_config(config)
|
|
171
|
+
size = validate_page_size(page_size, resolved)
|
|
172
|
+
values = query_values(query, maximum=100)
|
|
173
|
+
if by == "auto":
|
|
174
|
+
accession_flags = tuple(
|
|
175
|
+
_ASSEMBLY_ACCESSION.fullmatch(value) is not None for value in values
|
|
176
|
+
)
|
|
177
|
+
if any(accession_flags) and not all(accession_flags):
|
|
178
|
+
raise ConfigurationError(
|
|
179
|
+
"Do not mix assembly accessions and taxon names in one query.", code="INVALID_QUERY"
|
|
180
|
+
)
|
|
181
|
+
by = "accession" if all(accession_flags) else "taxon"
|
|
182
|
+
if by not in {"accession", "taxon"}:
|
|
183
|
+
raise ConfigurationError(
|
|
184
|
+
"assembly by must be auto, accession, or taxon.", code="INVALID_QUERY"
|
|
185
|
+
)
|
|
186
|
+
path = f"/genome/{by}/{quoted_path_values(values)}/dataset_report"
|
|
187
|
+
params: list[tuple[str, object]] = [
|
|
188
|
+
("filters.reference_only", reference_only),
|
|
189
|
+
("filters.assembly_source", assembly_source),
|
|
190
|
+
("filters.assembly_version", "current" if current_only else "all_assemblies"),
|
|
191
|
+
("page_size", size),
|
|
192
|
+
]
|
|
193
|
+
if page_token is not None:
|
|
194
|
+
params.append(("page_token", require_text(page_token, "page_token")))
|
|
195
|
+
url = build_url(api_base_url, path, params)
|
|
196
|
+
payload = request_json(
|
|
197
|
+
url, resolved, provider="NCBI Datasets", headers=_datasets_headers(resolved)
|
|
198
|
+
)
|
|
199
|
+
if not isinstance(payload, Mapping):
|
|
200
|
+
raise QueryError(
|
|
201
|
+
"NCBI Assembly returned an unexpected response.", code="QUERY_RESPONSE_ERROR"
|
|
202
|
+
)
|
|
203
|
+
records = limited_records(mapping_records(payload, key="reports"), resolved)
|
|
204
|
+
total, next_token = _page_metadata(payload)
|
|
205
|
+
total = max(total, len(records)) if total is not None else len(records)
|
|
206
|
+
return QueryResult(
|
|
207
|
+
"assembly",
|
|
208
|
+
"NCBI Datasets",
|
|
209
|
+
redact_url(url),
|
|
210
|
+
records,
|
|
211
|
+
adapter_provenance(
|
|
212
|
+
"NCBI Genome",
|
|
213
|
+
citation_url="https://www.ncbi.nlm.nih.gov/datasets/docs/v2/how-tos/genomes/get-genome-metadata/",
|
|
214
|
+
filters={
|
|
215
|
+
"query": values,
|
|
216
|
+
"by": by,
|
|
217
|
+
"reference_only": reference_only,
|
|
218
|
+
"assembly_source": assembly_source,
|
|
219
|
+
"current_only": current_only,
|
|
220
|
+
},
|
|
221
|
+
),
|
|
222
|
+
total_count=total,
|
|
223
|
+
next_page_token=next_token,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def gene(
|
|
228
|
+
query: str | int | Sequence[str | int],
|
|
229
|
+
*,
|
|
230
|
+
taxon: str | int | None = None,
|
|
231
|
+
by: Literal["auto", "id", "accession", "symbol", "search"] = "auto",
|
|
232
|
+
report: Literal["gene", "product"] = "gene",
|
|
233
|
+
gene_types: Sequence[str] = (),
|
|
234
|
+
page_size: int = 20,
|
|
235
|
+
page_token: str | None = None,
|
|
236
|
+
config: SearchConfig | None = None,
|
|
237
|
+
api_base_url: str = DEFAULT_NCBI_DATASETS_URL,
|
|
238
|
+
) -> QueryResult:
|
|
239
|
+
"""Query genes by GeneID, RefSeq accession, symbol, alias, or name."""
|
|
240
|
+
|
|
241
|
+
resolved = resolved_config(config)
|
|
242
|
+
size = validate_page_size(page_size, resolved)
|
|
243
|
+
values = query_values(query, maximum=100)
|
|
244
|
+
taxon_text = None if taxon is None else require_text(str(taxon), "taxon", max_length=1_024)
|
|
245
|
+
if report not in {"gene", "product"}:
|
|
246
|
+
raise ConfigurationError("gene report must be gene or product.", code="INVALID_QUERY")
|
|
247
|
+
if by == "auto":
|
|
248
|
+
if all(value.isdigit() for value in values):
|
|
249
|
+
by = "id"
|
|
250
|
+
elif all(_REFSEQ_ACCESSION.fullmatch(value) is not None for value in values):
|
|
251
|
+
by = "accession"
|
|
252
|
+
elif taxon_text is not None:
|
|
253
|
+
by = "symbol"
|
|
254
|
+
else:
|
|
255
|
+
raise ConfigurationError(
|
|
256
|
+
"Gene symbols, aliases, and names require taxon; otherwise set by explicitly.",
|
|
257
|
+
code="INVALID_QUERY",
|
|
258
|
+
)
|
|
259
|
+
suffix = "product_report" if report == "product" else "dataset_report"
|
|
260
|
+
params: list[tuple[str, object]] = [("page_size", size)]
|
|
261
|
+
if page_token is not None:
|
|
262
|
+
params.append(("page_token", require_text(page_token, "page_token")))
|
|
263
|
+
if gene_types:
|
|
264
|
+
params.append(
|
|
265
|
+
("types", tuple(require_text(item, "gene_type", max_length=128) for item in gene_types))
|
|
266
|
+
)
|
|
267
|
+
if by == "id":
|
|
268
|
+
if not all(value.isdigit() for value in values):
|
|
269
|
+
raise ConfigurationError("GeneID queries must be numeric.", code="INVALID_QUERY")
|
|
270
|
+
path = f"/gene/id/{quoted_path_values(values)}/{suffix}"
|
|
271
|
+
elif by == "accession":
|
|
272
|
+
path = f"/gene/accession/{quoted_path_values(values)}/{suffix}"
|
|
273
|
+
elif by == "symbol":
|
|
274
|
+
if taxon_text is None:
|
|
275
|
+
raise ConfigurationError("Gene symbol queries require taxon.", code="INVALID_QUERY")
|
|
276
|
+
path = (
|
|
277
|
+
f"/gene/symbol/{quoted_path_values(values)}/taxon/{quote(taxon_text, safe='')}/{suffix}"
|
|
278
|
+
)
|
|
279
|
+
elif by == "search":
|
|
280
|
+
if taxon_text is None or len(values) != 1:
|
|
281
|
+
raise ConfigurationError(
|
|
282
|
+
"Gene alias/name search requires one query and taxon.", code="INVALID_QUERY"
|
|
283
|
+
)
|
|
284
|
+
path = f"/gene/taxon/{quote(taxon_text, safe='')}/{suffix}"
|
|
285
|
+
params.append(("query", values[0]))
|
|
286
|
+
else:
|
|
287
|
+
raise ConfigurationError("Unsupported gene query mode.", code="INVALID_QUERY")
|
|
288
|
+
url = build_url(api_base_url, path, params)
|
|
289
|
+
payload = request_json(
|
|
290
|
+
url, resolved, provider="NCBI Datasets", headers=_datasets_headers(resolved)
|
|
291
|
+
)
|
|
292
|
+
if not isinstance(payload, Mapping):
|
|
293
|
+
raise QueryError("NCBI Gene returned an unexpected response.", code="QUERY_RESPONSE_ERROR")
|
|
294
|
+
records = limited_records(mapping_records(payload, key="reports"), resolved)
|
|
295
|
+
total, next_token = _page_metadata(payload)
|
|
296
|
+
total = max(total, len(records)) if total is not None else len(records)
|
|
297
|
+
return QueryResult(
|
|
298
|
+
"gene",
|
|
299
|
+
"NCBI Datasets",
|
|
300
|
+
redact_url(url),
|
|
301
|
+
records,
|
|
302
|
+
adapter_provenance(
|
|
303
|
+
"NCBI Gene",
|
|
304
|
+
citation_url="https://www.ncbi.nlm.nih.gov/datasets/docs/v2/how-tos/genes/get-gene-metadata/",
|
|
305
|
+
filters={"query": values, "taxon": taxon_text, "by": by, "report": report},
|
|
306
|
+
),
|
|
307
|
+
total_count=total,
|
|
308
|
+
next_page_token=next_token,
|
|
309
|
+
metadata={"report": report},
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def ncbi_orthologs(
|
|
314
|
+
gene_id: int | str,
|
|
315
|
+
*,
|
|
316
|
+
returned_content: Literal["complete", "ids_only"] = "complete",
|
|
317
|
+
page_size: int = 20,
|
|
318
|
+
page_token: str | None = None,
|
|
319
|
+
config: SearchConfig | None = None,
|
|
320
|
+
api_base_url: str = DEFAULT_NCBI_DATASETS_URL,
|
|
321
|
+
) -> QueryResult:
|
|
322
|
+
"""Query the NCBI ortholog set for one numeric GeneID."""
|
|
323
|
+
|
|
324
|
+
resolved = resolved_config(config)
|
|
325
|
+
value = require_text(str(gene_id), "gene_id", max_length=32)
|
|
326
|
+
if not value.isdigit():
|
|
327
|
+
raise ConfigurationError("gene_id must be numeric.", code="INVALID_QUERY")
|
|
328
|
+
size = validate_page_size(page_size, resolved)
|
|
329
|
+
if returned_content not in {"complete", "ids_only"}:
|
|
330
|
+
raise ConfigurationError("Unsupported ortholog returned_content.", code="INVALID_QUERY")
|
|
331
|
+
params: list[tuple[str, object]] = [
|
|
332
|
+
("returned_content", returned_content.upper()),
|
|
333
|
+
("page_size", size),
|
|
334
|
+
]
|
|
335
|
+
if page_token is not None:
|
|
336
|
+
params.append(("page_token", require_text(page_token, "page_token")))
|
|
337
|
+
url = build_url(api_base_url, f"/gene/id/{value}/orthologs", params)
|
|
338
|
+
payload = request_json(
|
|
339
|
+
url, resolved, provider="NCBI Datasets", headers=_datasets_headers(resolved)
|
|
340
|
+
)
|
|
341
|
+
if not isinstance(payload, Mapping):
|
|
342
|
+
raise QueryError(
|
|
343
|
+
"NCBI Orthologs returned an unexpected response.", code="QUERY_RESPONSE_ERROR"
|
|
344
|
+
)
|
|
345
|
+
records = limited_records(mapping_records(payload, key="reports"), resolved)
|
|
346
|
+
total, next_token = _page_metadata(payload)
|
|
347
|
+
total = max(total, len(records)) if total is not None else len(records)
|
|
348
|
+
return QueryResult(
|
|
349
|
+
"homology",
|
|
350
|
+
"NCBI Datasets",
|
|
351
|
+
redact_url(url),
|
|
352
|
+
records,
|
|
353
|
+
adapter_provenance(
|
|
354
|
+
"NCBI Gene Orthologs",
|
|
355
|
+
citation_url="https://www.ncbi.nlm.nih.gov/datasets/docs/v2/how-tos/genes/download-ortholog-set/",
|
|
356
|
+
filters={"gene_id": value},
|
|
357
|
+
),
|
|
358
|
+
total_count=total,
|
|
359
|
+
next_page_token=next_token,
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def virus(
|
|
364
|
+
query: str | Sequence[str],
|
|
365
|
+
*,
|
|
366
|
+
by: Literal["auto", "accession", "taxon"] = "auto",
|
|
367
|
+
report: Literal["dataset", "annotation"] = "dataset",
|
|
368
|
+
refseq_only: bool = False,
|
|
369
|
+
annotated_only: bool = False,
|
|
370
|
+
complete_only: bool = False,
|
|
371
|
+
host: str | None = None,
|
|
372
|
+
geo_location: str | None = None,
|
|
373
|
+
released_since: str | None = None,
|
|
374
|
+
updated_since: str | None = None,
|
|
375
|
+
page_size: int = 20,
|
|
376
|
+
page_token: str | None = None,
|
|
377
|
+
config: SearchConfig | None = None,
|
|
378
|
+
api_base_url: str = DEFAULT_NCBI_DATASETS_URL,
|
|
379
|
+
) -> QueryResult:
|
|
380
|
+
"""Query NCBI Virus genome and annotation metadata by taxon or accession."""
|
|
381
|
+
|
|
382
|
+
resolved = resolved_config(config)
|
|
383
|
+
values = query_values(query, maximum=100)
|
|
384
|
+
size = validate_page_size(page_size, resolved)
|
|
385
|
+
if by == "auto":
|
|
386
|
+
accession_flags = tuple(_VIRUS_ACCESSION.fullmatch(value) is not None for value in values)
|
|
387
|
+
if any(accession_flags) and not all(accession_flags):
|
|
388
|
+
raise ConfigurationError(
|
|
389
|
+
"Do not mix virus accessions and taxon names.", code="INVALID_QUERY"
|
|
390
|
+
)
|
|
391
|
+
by = "accession" if all(accession_flags) else "taxon"
|
|
392
|
+
if by not in {"accession", "taxon"}:
|
|
393
|
+
raise ConfigurationError("virus by must be auto, accession, or taxon.")
|
|
394
|
+
if by == "taxon" and len(values) != 1:
|
|
395
|
+
raise ConfigurationError("Virus taxon queries accept one taxon at a time.")
|
|
396
|
+
if report not in {"dataset", "annotation"}:
|
|
397
|
+
raise ConfigurationError("virus report must be dataset or annotation.")
|
|
398
|
+
for name, value in (
|
|
399
|
+
("refseq_only", refseq_only),
|
|
400
|
+
("annotated_only", annotated_only),
|
|
401
|
+
("complete_only", complete_only),
|
|
402
|
+
):
|
|
403
|
+
if not isinstance(value, bool):
|
|
404
|
+
raise ConfigurationError(f"{name} must be boolean.")
|
|
405
|
+
suffix = "dataset_report" if report == "dataset" else "annotation_report"
|
|
406
|
+
path_values = values if by == "accession" else (values[0],)
|
|
407
|
+
path = f"/virus/{by}/{quoted_path_values(path_values)}/{suffix}"
|
|
408
|
+
params: list[tuple[str, object]] = [
|
|
409
|
+
("refseq_only", refseq_only),
|
|
410
|
+
("annotated_only", annotated_only),
|
|
411
|
+
("complete_only", complete_only),
|
|
412
|
+
("host", None if host is None else require_text(host, "host", max_length=256)),
|
|
413
|
+
(
|
|
414
|
+
"geo_location",
|
|
415
|
+
None
|
|
416
|
+
if geo_location is None
|
|
417
|
+
else require_text(geo_location, "geo_location", max_length=256),
|
|
418
|
+
),
|
|
419
|
+
(
|
|
420
|
+
"released_since",
|
|
421
|
+
None
|
|
422
|
+
if released_since is None
|
|
423
|
+
else require_text(released_since, "released_since", max_length=64),
|
|
424
|
+
),
|
|
425
|
+
(
|
|
426
|
+
"updated_since",
|
|
427
|
+
None
|
|
428
|
+
if updated_since is None
|
|
429
|
+
else require_text(updated_since, "updated_since", max_length=64),
|
|
430
|
+
),
|
|
431
|
+
("page_size", size),
|
|
432
|
+
]
|
|
433
|
+
if page_token is not None:
|
|
434
|
+
params.append(("page_token", require_text(page_token, "page_token")))
|
|
435
|
+
url = build_url(api_base_url, path, params)
|
|
436
|
+
payload = request_json(
|
|
437
|
+
url, resolved, provider="NCBI Datasets", headers=_datasets_headers(resolved)
|
|
438
|
+
)
|
|
439
|
+
if not isinstance(payload, Mapping):
|
|
440
|
+
raise QueryError("NCBI Virus returned an unexpected response.", code="QUERY_RESPONSE_ERROR")
|
|
441
|
+
records = limited_records(mapping_records(payload, key="reports"), resolved)
|
|
442
|
+
total, next_token = _page_metadata(payload)
|
|
443
|
+
total = max(total, len(records)) if total is not None else len(records)
|
|
444
|
+
return QueryResult(
|
|
445
|
+
"virus",
|
|
446
|
+
"NCBI Datasets",
|
|
447
|
+
redact_url(url),
|
|
448
|
+
records,
|
|
449
|
+
adapter_provenance(
|
|
450
|
+
"NCBI Virus",
|
|
451
|
+
citation_url="https://www.ncbi.nlm.nih.gov/datasets/docs/v2/how-tos/virus/virus-metadata/",
|
|
452
|
+
filters={
|
|
453
|
+
"query": values,
|
|
454
|
+
"by": by,
|
|
455
|
+
"report": report,
|
|
456
|
+
"refseq_only": refseq_only,
|
|
457
|
+
"annotated_only": annotated_only,
|
|
458
|
+
"complete_only": complete_only,
|
|
459
|
+
"host": host,
|
|
460
|
+
"geo_location": geo_location,
|
|
461
|
+
"released_since": released_since,
|
|
462
|
+
"updated_since": updated_since,
|
|
463
|
+
},
|
|
464
|
+
),
|
|
465
|
+
total_count=total,
|
|
466
|
+
next_page_token=next_token,
|
|
467
|
+
metadata={"report": report},
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def _eutils_parameters(config: SearchConfig) -> list[tuple[str, object]]:
|
|
472
|
+
parameters: list[tuple[str, object]] = [("tool", config.tool)]
|
|
473
|
+
if config.email is not None:
|
|
474
|
+
parameters.append(("email", config.email))
|
|
475
|
+
if config.api_key is not None:
|
|
476
|
+
parameters.append(("api_key", config.api_key))
|
|
477
|
+
return parameters
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def entrez(
|
|
481
|
+
database: str,
|
|
482
|
+
term: str,
|
|
483
|
+
*,
|
|
484
|
+
retmax: int = 20,
|
|
485
|
+
retstart: int = 0,
|
|
486
|
+
sort: str | None = None,
|
|
487
|
+
config: SearchConfig | None = None,
|
|
488
|
+
api_base_url: str = DEFAULT_NCBI_EUTILS_URL,
|
|
489
|
+
query_type: str | None = None,
|
|
490
|
+
) -> QueryResult:
|
|
491
|
+
"""Run a bounded ESearch followed by ESummary against an Entrez database."""
|
|
492
|
+
|
|
493
|
+
resolved = resolved_config(config)
|
|
494
|
+
db = require_text(database, "database", max_length=64).casefold()
|
|
495
|
+
if db not in _ENTREZ_DATABASES:
|
|
496
|
+
raise ConfigurationError(
|
|
497
|
+
"Unsupported Entrez database.",
|
|
498
|
+
code="INVALID_QUERY",
|
|
499
|
+
context={"database": db, "allowed": sorted(_ENTREZ_DATABASES)},
|
|
500
|
+
)
|
|
501
|
+
query = require_text(term, "term")
|
|
502
|
+
size = validate_page_size(retmax, resolved, maximum=10_000)
|
|
503
|
+
if isinstance(retstart, bool) or not isinstance(retstart, int) or retstart < 0:
|
|
504
|
+
raise ConfigurationError("retstart must be a non-negative integer.", code="INVALID_QUERY")
|
|
505
|
+
search_params: list[tuple[str, object]] = [
|
|
506
|
+
("db", db),
|
|
507
|
+
("term", query),
|
|
508
|
+
("retmode", "json"),
|
|
509
|
+
("retmax", size),
|
|
510
|
+
("retstart", retstart),
|
|
511
|
+
*_eutils_parameters(resolved),
|
|
512
|
+
]
|
|
513
|
+
if sort is not None:
|
|
514
|
+
search_params.append(("sort", require_text(sort, "sort", max_length=128)))
|
|
515
|
+
search_url = build_url(api_base_url, "/esearch.fcgi", search_params)
|
|
516
|
+
search_payload = request_json(search_url, resolved, provider="NCBI Entrez")
|
|
517
|
+
if not isinstance(search_payload, Mapping) or not isinstance(
|
|
518
|
+
search_payload.get("esearchresult"), Mapping
|
|
519
|
+
):
|
|
520
|
+
raise QueryError(
|
|
521
|
+
"NCBI ESearch returned an unexpected response.", code="QUERY_RESPONSE_ERROR"
|
|
522
|
+
)
|
|
523
|
+
search_result = search_payload["esearchresult"]
|
|
524
|
+
raw_ids = search_result.get("idlist")
|
|
525
|
+
ids = tuple(str(item) for item in raw_ids) if isinstance(raw_ids, list) else ()
|
|
526
|
+
if len(ids) > resolved.max_records:
|
|
527
|
+
raise QueryError("NCBI ESearch exceeded max_records.", code="QUERY_RECORD_LIMIT")
|
|
528
|
+
count_raw = search_result.get("count")
|
|
529
|
+
try:
|
|
530
|
+
total = int(count_raw)
|
|
531
|
+
except (TypeError, ValueError):
|
|
532
|
+
total = len(ids)
|
|
533
|
+
records: tuple[dict[str, object], ...] = ()
|
|
534
|
+
summary_url: str | None = None
|
|
535
|
+
if ids:
|
|
536
|
+
summary_url = build_url(
|
|
537
|
+
api_base_url,
|
|
538
|
+
"/esummary.fcgi",
|
|
539
|
+
[
|
|
540
|
+
("db", db),
|
|
541
|
+
("id", ",".join(ids)),
|
|
542
|
+
("retmode", "json"),
|
|
543
|
+
("version", "2.0"),
|
|
544
|
+
*_eutils_parameters(resolved),
|
|
545
|
+
],
|
|
546
|
+
)
|
|
547
|
+
summary_payload = request_json(summary_url, resolved, provider="NCBI Entrez")
|
|
548
|
+
result = summary_payload.get("result") if isinstance(summary_payload, Mapping) else None
|
|
549
|
+
if not isinstance(result, Mapping):
|
|
550
|
+
raise QueryError(
|
|
551
|
+
"NCBI ESummary returned an unexpected response.", code="QUERY_RESPONSE_ERROR"
|
|
552
|
+
)
|
|
553
|
+
uid_order = result.get("uids")
|
|
554
|
+
ordered = tuple(str(item) for item in uid_order) if isinstance(uid_order, list) else ids
|
|
555
|
+
records = tuple(
|
|
556
|
+
dict(item) for uid in ordered if isinstance((item := result.get(uid)), Mapping)
|
|
557
|
+
)
|
|
558
|
+
records = limited_records(records, resolved)
|
|
559
|
+
kind = query_type or db
|
|
560
|
+
return QueryResult(
|
|
561
|
+
kind,
|
|
562
|
+
"NCBI Entrez",
|
|
563
|
+
redact_url(search_url),
|
|
564
|
+
records,
|
|
565
|
+
adapter_provenance(
|
|
566
|
+
f"NCBI Entrez {db}",
|
|
567
|
+
citation_url="https://www.ncbi.nlm.nih.gov/books/NBK25501/",
|
|
568
|
+
filters={"database": db, "term": query, "retstart": retstart, "retmax": size},
|
|
569
|
+
),
|
|
570
|
+
total_count=max(total, len(records)),
|
|
571
|
+
metadata={
|
|
572
|
+
"database": db,
|
|
573
|
+
"uids": ids,
|
|
574
|
+
"summary_url": None if summary_url is None else redact_url(summary_url),
|
|
575
|
+
},
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def accession(
|
|
580
|
+
identifier: str,
|
|
581
|
+
*,
|
|
582
|
+
database: Literal["nuccore", "protein", "assembly", "sra"] = "nuccore",
|
|
583
|
+
config: SearchConfig | None = None,
|
|
584
|
+
api_base_url: str = DEFAULT_NCBI_EUTILS_URL,
|
|
585
|
+
) -> QueryResult:
|
|
586
|
+
"""Check whether an accession exists and retrieve its current summary/version."""
|
|
587
|
+
|
|
588
|
+
value = require_text(identifier, "identifier", max_length=256)
|
|
589
|
+
return entrez(
|
|
590
|
+
database,
|
|
591
|
+
f'"{value}"[Accession]',
|
|
592
|
+
retmax=20,
|
|
593
|
+
config=config,
|
|
594
|
+
api_base_url=api_base_url,
|
|
595
|
+
query_type="accession",
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def project(
|
|
600
|
+
term: str,
|
|
601
|
+
*,
|
|
602
|
+
retmax: int = 20,
|
|
603
|
+
config: SearchConfig | None = None,
|
|
604
|
+
api_base_url: str = DEFAULT_NCBI_EUTILS_URL,
|
|
605
|
+
) -> QueryResult:
|
|
606
|
+
"""Search BioProject metadata with the Entrez query language."""
|
|
607
|
+
|
|
608
|
+
return entrez(
|
|
609
|
+
"bioproject",
|
|
610
|
+
term,
|
|
611
|
+
retmax=retmax,
|
|
612
|
+
config=config,
|
|
613
|
+
api_base_url=api_base_url,
|
|
614
|
+
query_type="project",
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
def sample(
|
|
619
|
+
term: str,
|
|
620
|
+
*,
|
|
621
|
+
retmax: int = 20,
|
|
622
|
+
config: SearchConfig | None = None,
|
|
623
|
+
api_base_url: str = DEFAULT_NCBI_EUTILS_URL,
|
|
624
|
+
) -> QueryResult:
|
|
625
|
+
"""Search BioSample metadata with the Entrez query language."""
|
|
626
|
+
|
|
627
|
+
return entrez(
|
|
628
|
+
"biosample",
|
|
629
|
+
term,
|
|
630
|
+
retmax=retmax,
|
|
631
|
+
config=config,
|
|
632
|
+
api_base_url=api_base_url,
|
|
633
|
+
query_type="sample",
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def clinical_variant(
|
|
638
|
+
term: str,
|
|
639
|
+
*,
|
|
640
|
+
retmax: int = 20,
|
|
641
|
+
config: SearchConfig | None = None,
|
|
642
|
+
api_base_url: str = DEFAULT_NCBI_EUTILS_URL,
|
|
643
|
+
) -> QueryResult:
|
|
644
|
+
"""Search ClinVar by rsID, HGVS, gene, condition, or Entrez expression."""
|
|
645
|
+
|
|
646
|
+
return entrez(
|
|
647
|
+
"clinvar",
|
|
648
|
+
term,
|
|
649
|
+
retmax=retmax,
|
|
650
|
+
config=config,
|
|
651
|
+
api_base_url=api_base_url,
|
|
652
|
+
query_type="clinical_variant",
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def expression(
|
|
657
|
+
term: str,
|
|
658
|
+
*,
|
|
659
|
+
retmax: int = 20,
|
|
660
|
+
config: SearchConfig | None = None,
|
|
661
|
+
api_base_url: str = DEFAULT_NCBI_EUTILS_URL,
|
|
662
|
+
) -> QueryResult:
|
|
663
|
+
"""Search GEO DataSets/Series metadata through NCBI E-utilities."""
|
|
664
|
+
|
|
665
|
+
return entrez(
|
|
666
|
+
"gds",
|
|
667
|
+
term,
|
|
668
|
+
retmax=retmax,
|
|
669
|
+
config=config,
|
|
670
|
+
api_base_url=api_base_url,
|
|
671
|
+
query_type="expression",
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
def literature(
|
|
676
|
+
term: str,
|
|
677
|
+
*,
|
|
678
|
+
retmax: int = 20,
|
|
679
|
+
sort: str = "relevance",
|
|
680
|
+
config: SearchConfig | None = None,
|
|
681
|
+
api_base_url: str = DEFAULT_NCBI_EUTILS_URL,
|
|
682
|
+
) -> QueryResult:
|
|
683
|
+
"""Search linked biological literature in PubMed."""
|
|
684
|
+
|
|
685
|
+
resolved = resolved_config(config)
|
|
686
|
+
return entrez(
|
|
687
|
+
"pubmed",
|
|
688
|
+
term,
|
|
689
|
+
retmax=retmax,
|
|
690
|
+
sort=sort,
|
|
691
|
+
config=resolved,
|
|
692
|
+
api_base_url=api_base_url,
|
|
693
|
+
query_type="literature",
|
|
694
|
+
)
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
def database_version(
|
|
698
|
+
database: str,
|
|
699
|
+
*,
|
|
700
|
+
config: SearchConfig | None = None,
|
|
701
|
+
api_base_url: str = DEFAULT_NCBI_EUTILS_URL,
|
|
702
|
+
) -> QueryResult:
|
|
703
|
+
"""Return Entrez database record counts, indexed fields, and last-update metadata."""
|
|
704
|
+
|
|
705
|
+
resolved = resolved_config(config)
|
|
706
|
+
db = require_text(database, "database", max_length=64).casefold()
|
|
707
|
+
if db not in _ENTREZ_DATABASES:
|
|
708
|
+
raise ConfigurationError("Unsupported Entrez database.", code="INVALID_QUERY")
|
|
709
|
+
url = build_url(
|
|
710
|
+
api_base_url,
|
|
711
|
+
"/einfo.fcgi",
|
|
712
|
+
[("db", db), ("retmode", "json"), ("version", "2.0"), *_eutils_parameters(resolved)],
|
|
713
|
+
)
|
|
714
|
+
payload = request_json(url, resolved, provider="NCBI Entrez")
|
|
715
|
+
if not isinstance(payload, Mapping):
|
|
716
|
+
raise QueryError("NCBI EInfo returned an unexpected response.", code="QUERY_RESPONSE_ERROR")
|
|
717
|
+
root = payload.get("einforesult")
|
|
718
|
+
raw_info = root.get("dbinfo") if isinstance(root, Mapping) else None
|
|
719
|
+
if isinstance(raw_info, list):
|
|
720
|
+
records = mapping_records(raw_info)
|
|
721
|
+
elif isinstance(raw_info, Mapping):
|
|
722
|
+
records = (dict(raw_info),)
|
|
723
|
+
else:
|
|
724
|
+
records = ()
|
|
725
|
+
records = limited_records(records, resolved)
|
|
726
|
+
return QueryResult(
|
|
727
|
+
"database_version",
|
|
728
|
+
"NCBI Entrez",
|
|
729
|
+
redact_url(url),
|
|
730
|
+
records,
|
|
731
|
+
adapter_provenance(
|
|
732
|
+
f"NCBI Entrez {db}",
|
|
733
|
+
citation_url="https://www.ncbi.nlm.nih.gov/books/NBK25499/#chapter4.EInfo",
|
|
734
|
+
filters={"database": db},
|
|
735
|
+
),
|
|
736
|
+
total_count=len(records),
|
|
737
|
+
)
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
__all__ = [
|
|
741
|
+
"DEFAULT_NCBI_DATASETS_URL",
|
|
742
|
+
"DEFAULT_NCBI_EUTILS_URL",
|
|
743
|
+
"accession",
|
|
744
|
+
"assembly",
|
|
745
|
+
"clinical_variant",
|
|
746
|
+
"database_version",
|
|
747
|
+
"entrez",
|
|
748
|
+
"expression",
|
|
749
|
+
"gene",
|
|
750
|
+
"literature",
|
|
751
|
+
"ncbi_orthologs",
|
|
752
|
+
"project",
|
|
753
|
+
"sample",
|
|
754
|
+
"taxonomy",
|
|
755
|
+
"virus",
|
|
756
|
+
]
|