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.
Files changed (210) hide show
  1. dnakit/__init__.py +108 -0
  2. dnakit/_version.py +5 -0
  3. dnakit/alignment/__init__.py +6 -0
  4. dnakit/alignment/pairwise.py +512 -0
  5. dnakit/alignment/results.py +188 -0
  6. dnakit/backends/__init__.py +80 -0
  7. dnakit/backends/external.py +517 -0
  8. dnakit/backends/registry.py +207 -0
  9. dnakit/batch.py +452 -0
  10. dnakit/cache/__init__.py +5 -0
  11. dnakit/cache/store.py +424 -0
  12. dnakit/chunking.py +920 -0
  13. dnakit/cli/__init__.py +5 -0
  14. dnakit/cli/__main__.py +6 -0
  15. dnakit/cli/app.py +814 -0
  16. dnakit/cli/workflow.py +94 -0
  17. dnakit/config/__init__.py +5 -0
  18. dnakit/config/loader.py +381 -0
  19. dnakit/core/__init__.py +80 -0
  20. dnakit/core/_json.py +206 -0
  21. dnakit/core/backend_info.py +70 -0
  22. dnakit/core/collection.py +163 -0
  23. dnakit/core/coordinates.py +295 -0
  24. dnakit/core/enums.py +95 -0
  25. dnakit/core/facade.py +719 -0
  26. dnakit/core/feature.py +90 -0
  27. dnakit/core/gap.py +82 -0
  28. dnakit/core/issues.py +62 -0
  29. dnakit/core/provenance.py +305 -0
  30. dnakit/core/record.py +161 -0
  31. dnakit/core/results.py +134 -0
  32. dnakit/core/sequence.py +327 -0
  33. dnakit/datasets/__init__.py +100 -0
  34. dnakit/datasets/_advanced_shared.py +198 -0
  35. dnakit/datasets/_metadata.py +33 -0
  36. dnakit/datasets/advanced_deduplicate.py +183 -0
  37. dnakit/datasets/advanced_split.py +302 -0
  38. dnakit/datasets/clustering.py +286 -0
  39. dnakit/datasets/config.py +531 -0
  40. dnakit/datasets/deduplicate.py +303 -0
  41. dnakit/datasets/evaluation.py +244 -0
  42. dnakit/datasets/filter.py +135 -0
  43. dnakit/datasets/neural_clustering.py +221 -0
  44. dnakit/datasets/results.py +520 -0
  45. dnakit/datasets/split.py +450 -0
  46. dnakit/descriptors/__init__.py +102 -0
  47. dnakit/descriptors/_dinucleotide.py +345 -0
  48. dnakit/descriptors/_shared.py +180 -0
  49. dnakit/descriptors/all.py +917 -0
  50. dnakit/descriptors/basic.py +193 -0
  51. dnakit/descriptors/codon.py +195 -0
  52. dnakit/descriptors/complexity.py +201 -0
  53. dnakit/descriptors/entropy.py +101 -0
  54. dnakit/descriptors/homopolymer.py +105 -0
  55. dnakit/descriptors/kmer.py +88 -0
  56. dnakit/descriptors/results.py +286 -0
  57. dnakit/descriptors/schema.py +414 -0
  58. dnakit/descriptors/window.py +203 -0
  59. dnakit/download/__init__.py +54 -0
  60. dnakit/download/catalogs.py +166 -0
  61. dnakit/download/ena.py +131 -0
  62. dnakit/download/encode.py +103 -0
  63. dnakit/download/files.py +469 -0
  64. dnakit/download/indexes.py +233 -0
  65. dnakit/download/metadata.py +164 -0
  66. dnakit/download/models.py +227 -0
  67. dnakit/download/ncbi.py +333 -0
  68. dnakit/download/sequence.py +133 -0
  69. dnakit/download/ucsc.py +99 -0
  70. dnakit/evaluation/__init__.py +81 -0
  71. dnakit/evaluation/_shared.py +288 -0
  72. dnakit/evaluation/collection.py +270 -0
  73. dnakit/evaluation/config.py +591 -0
  74. dnakit/evaluation/distribution.py +287 -0
  75. dnakit/evaluation/frechet.py +223 -0
  76. dnakit/evaluation/generative.py +381 -0
  77. dnakit/evaluation/reference.py +409 -0
  78. dnakit/evaluation/results.py +137 -0
  79. dnakit/evaluation/scorecard.py +211 -0
  80. dnakit/evaluation/sequence.py +403 -0
  81. dnakit/evaluation/synthesis.py +249 -0
  82. dnakit/exceptions.py +189 -0
  83. dnakit/fingerprints/__init__.py +109 -0
  84. dnakit/fingerprints/_shared.py +116 -0
  85. dnakit/fingerprints/advanced.py +829 -0
  86. dnakit/fingerprints/bit.py +229 -0
  87. dnakit/fingerprints/encoding.py +351 -0
  88. dnakit/fingerprints/kmer.py +170 -0
  89. dnakit/fingerprints/results.py +531 -0
  90. dnakit/fingerprints/sketch.py +246 -0
  91. dnakit/io/__init__.py +112 -0
  92. dnakit/io/_advanced_common.py +105 -0
  93. dnakit/io/_formats.py +994 -0
  94. dnakit/io/annotations.py +1179 -0
  95. dnakit/io/api.py +759 -0
  96. dnakit/io/config.py +315 -0
  97. dnakit/io/genbank.py +523 -0
  98. dnakit/io/indexed.py +1146 -0
  99. dnakit/io/metadata.py +452 -0
  100. dnakit/io/parquet.py +56 -0
  101. dnakit/io/results.py +131 -0
  102. dnakit/io/source.py +138 -0
  103. dnakit/io/tables.py +1237 -0
  104. dnakit/molbio/__init__.py +101 -0
  105. dnakit/molbio/_shared.py +266 -0
  106. dnakit/molbio/assembly.py +279 -0
  107. dnakit/molbio/crispr.py +181 -0
  108. dnakit/molbio/optimization.py +897 -0
  109. dnakit/molbio/primer3_design.py +667 -0
  110. dnakit/molbio/primers.py +899 -0
  111. dnakit/molbio/restriction.py +573 -0
  112. dnakit/molbio/results.py +583 -0
  113. dnakit/ops/__init__.py +588 -0
  114. dnakit/ops/_common.py +201 -0
  115. dnakit/ops/circular.py +193 -0
  116. dnakit/ops/concat.py +204 -0
  117. dnakit/ops/direction.py +27 -0
  118. dnakit/ops/edit.py +443 -0
  119. dnakit/ops/evolution.py +991 -0
  120. dnakit/ops/mutation.py +151 -0
  121. dnakit/ops/records.py +1012 -0
  122. dnakit/ops/translation.py +257 -0
  123. dnakit/patterns/__init__.py +81 -0
  124. dnakit/patterns/_shared.py +282 -0
  125. dnakit/patterns/coding.py +360 -0
  126. dnakit/patterns/crispr.py +337 -0
  127. dnakit/patterns/motif.py +721 -0
  128. dnakit/patterns/regions.py +341 -0
  129. dnakit/patterns/repeats.py +480 -0
  130. dnakit/patterns/restriction.py +281 -0
  131. dnakit/patterns/results.py +411 -0
  132. dnakit/py.typed +1 -0
  133. dnakit/references/__init__.py +27 -0
  134. dnakit/references/models.py +155 -0
  135. dnakit/references/ncbi.py +785 -0
  136. dnakit/representations/__init__.py +29 -0
  137. dnakit/representations/backends.py +928 -0
  138. dnakit/representations/checkpoints.py +356 -0
  139. dnakit/representations/extraction.py +220 -0
  140. dnakit/representations/models.py +395 -0
  141. dnakit/representations/results.py +79 -0
  142. dnakit/search/__init__.py +132 -0
  143. dnakit/search/_http.py +271 -0
  144. dnakit/search/_shared.py +108 -0
  145. dnakit/search/blast.py +516 -0
  146. dnakit/search/ena.py +245 -0
  147. dnakit/search/encode.py +102 -0
  148. dnakit/search/ensembl.py +874 -0
  149. dnakit/search/models.py +159 -0
  150. dnakit/search/ncbi.py +756 -0
  151. dnakit/search/ucsc.py +388 -0
  152. dnakit/secondary_structure/__init__.py +38 -0
  153. dnakit/secondary_structure/dotbracket.py +410 -0
  154. dnakit/secondary_structure/nupack.py +589 -0
  155. dnakit/secondary_structure/results.py +142 -0
  156. dnakit/similarity/__init__.py +125 -0
  157. dnakit/similarity/_shared.py +152 -0
  158. dnakit/similarity/approximate.py +258 -0
  159. dnakit/similarity/compare.py +162 -0
  160. dnakit/similarity/dashing.py +936 -0
  161. dnakit/similarity/distance.py +279 -0
  162. dnakit/similarity/index.py +365 -0
  163. dnakit/similarity/matrix.py +254 -0
  164. dnakit/similarity/results.py +676 -0
  165. dnakit/similarity/search.py +286 -0
  166. dnakit/similarity/sketch.py +96 -0
  167. dnakit/similarity/vector.py +662 -0
  168. dnakit/standardize/__init__.py +77 -0
  169. dnakit/standardize/_shared.py +100 -0
  170. dnakit/standardize/config.py +411 -0
  171. dnakit/standardize/gaps.py +1060 -0
  172. dnakit/standardize/normalize.py +565 -0
  173. dnakit/standardize/results.py +247 -0
  174. dnakit/standardize/validate.py +487 -0
  175. dnakit/structure3d/__init__.py +47 -0
  176. dnakit/structure3d/_shared.py +47 -0
  177. dnakit/structure3d/geometry.py +773 -0
  178. dnakit/structure3d/pdb.py +253 -0
  179. dnakit/structure3d/results.py +233 -0
  180. dnakit/structure3d/threedna.py +174 -0
  181. dnakit/thermodynamics/__init__.py +112 -0
  182. dnakit/thermodynamics/_shared.py +92 -0
  183. dnakit/thermodynamics/backends.py +653 -0
  184. dnakit/thermodynamics/calculations.py +837 -0
  185. dnakit/thermodynamics/config.py +175 -0
  186. dnakit/thermodynamics/equilibrium.py +340 -0
  187. dnakit/thermodynamics/optics.py +385 -0
  188. dnakit/thermodynamics/parameters.py +79 -0
  189. dnakit/thermodynamics/results.py +445 -0
  190. dnakit/visualization/__init__.py +43 -0
  191. dnakit/visualization/_svg.py +185 -0
  192. dnakit/visualization/advanced.py +314 -0
  193. dnakit/visualization/config.py +255 -0
  194. dnakit/visualization/export.py +349 -0
  195. dnakit/visualization/matrix.py +279 -0
  196. dnakit/visualization/report.py +431 -0
  197. dnakit/visualization/results.py +236 -0
  198. dnakit/visualization/sequence.py +459 -0
  199. dnakit/workflows/__init__.py +45 -0
  200. dnakit/workflows/manifest.py +365 -0
  201. dnakit/workflows/runner.py +1548 -0
  202. dnakit/workflows/schema.py +898 -0
  203. dnakit-0.1.0.dev0.dist-info/METADATA +126 -0
  204. dnakit-0.1.0.dev0.dist-info/RECORD +210 -0
  205. dnakit-0.1.0.dev0.dist-info/WHEEL +5 -0
  206. dnakit-0.1.0.dev0.dist-info/entry_points.txt +2 -0
  207. dnakit-0.1.0.dev0.dist-info/licenses/DISCLAIMER.md +26 -0
  208. dnakit-0.1.0.dev0.dist-info/licenses/LICENSE +21 -0
  209. dnakit-0.1.0.dev0.dist-info/licenses/THIRD_PARTY_NOTICES.md +73 -0
  210. dnakit-0.1.0.dev0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,333 @@
1
+ """NCBI Datasets data-package downloads."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from collections.abc import Callable, Sequence
7
+ from pathlib import Path
8
+ from typing import Literal
9
+
10
+ from dnakit.exceptions import ConfigurationError
11
+ from dnakit.references import download_genome as genome
12
+ from dnakit.search._http import build_url, require_text
13
+ from dnakit.search._shared import query_values, quoted_path_values
14
+ from dnakit.search.ncbi import DEFAULT_NCBI_DATASETS_URL
15
+
16
+ from .files import dataset, resolved_config
17
+ from .models import DatasetDownloadResult, DownloadConfig, DownloadProgress, RemoteFile
18
+
19
+ _GENOME_INCLUDE = frozenset(
20
+ {
21
+ "CDS_FASTA",
22
+ "GENOME_FASTA",
23
+ "GENOME_GBFF",
24
+ "GENOME_GFF",
25
+ "GENOME_GTF",
26
+ "NONE",
27
+ "PROT_FASTA",
28
+ "RNA_FASTA",
29
+ "SEQUENCE_REPORT",
30
+ }
31
+ )
32
+ _GENE_INCLUDE = frozenset(
33
+ {"FASTA_3P_UTR", "FASTA_5P_UTR", "FASTA_CDS", "FASTA_GENE", "FASTA_PROTEIN", "FASTA_RNA"}
34
+ )
35
+ _VIRUS_INCLUDE = frozenset({"GENOME", "CDS", "PROTEIN", "NONE"})
36
+ _VIRUS_AUX = frozenset({"ANNOTATION", "BIOSAMPLE_REPORT"})
37
+ _VIRUS_ACCESSION = re.compile(r"^[A-Z]{1,6}_?\d{3,}(?:\.\d+)?$", flags=re.IGNORECASE)
38
+
39
+
40
+ def _validate_include(values: Sequence[str], allowed: frozenset[str]) -> tuple[str, ...]:
41
+ resolved = tuple(value.upper() for value in values)
42
+ if not resolved or any(value not in allowed for value in resolved):
43
+ raise ConfigurationError(
44
+ "include contains an unsupported NCBI package file type.",
45
+ code="INVALID_DOWNLOAD_CONFIG",
46
+ context={"allowed": sorted(allowed)},
47
+ )
48
+ return resolved
49
+
50
+
51
+ def _slug(values: Sequence[str]) -> str:
52
+ joined = "_".join(values[:3])
53
+ if len(values) > 3:
54
+ joined += f"_plus_{len(values) - 3}"
55
+ return re.sub(r"[^A-Za-z0-9_.-]+", "_", joined)
56
+
57
+
58
+ def genome_package(
59
+ accessions: str | Sequence[str],
60
+ output_dir: str | Path,
61
+ *,
62
+ include: Sequence[str] = ("GENOME_FASTA",),
63
+ chromosomes: Sequence[str] = (),
64
+ hydrated: Literal["FULLY_HYDRATED", "DATA_REPORT_ONLY"] = "FULLY_HYDRATED",
65
+ config: DownloadConfig | None = None,
66
+ progress: Callable[[DownloadProgress], None] | None = None,
67
+ api_base_url: str = DEFAULT_NCBI_DATASETS_URL,
68
+ ) -> DatasetDownloadResult:
69
+ """Download an NCBI genome ZIP with selected sequences, annotations, and reports."""
70
+
71
+ resolved = resolved_config(config)
72
+ values = query_values(accessions, name="accessions", maximum=100)
73
+ if any(
74
+ re.fullmatch(r"GC[AF]_\d+\.\d+", value, flags=re.IGNORECASE) is None for value in values
75
+ ):
76
+ raise ConfigurationError(
77
+ "genome_package requires versioned GCA_/GCF_ accessions.",
78
+ code="INVALID_ASSEMBLY_ACCESSION",
79
+ )
80
+ includes = _validate_include(include, _GENOME_INCLUDE)
81
+ if hydrated not in {"FULLY_HYDRATED", "DATA_REPORT_ONLY"}:
82
+ raise ConfigurationError("Unsupported NCBI hydration mode.")
83
+ chromosome_values = tuple(str(value).strip() for value in chromosomes)
84
+ if any(not value or len(value) > 128 for value in chromosome_values):
85
+ raise ConfigurationError("chromosomes contains an invalid value.")
86
+ url = build_url(
87
+ api_base_url,
88
+ f"/genome/accession/{quoted_path_values(values)}/download",
89
+ (
90
+ ("include_annotation_type", includes),
91
+ ("chromosomes", chromosome_values),
92
+ ("hydrated", hydrated),
93
+ ),
94
+ )
95
+ filename = f"ncbi_genome_{_slug(values)}.zip"
96
+ headers = {} if resolved.api_key is None else {"api-key": resolved.api_key}
97
+ return dataset(
98
+ (RemoteFile(url, filename=filename),),
99
+ output_dir,
100
+ kind="ncbi_genome_package",
101
+ source="NCBI Datasets Genome",
102
+ metadata={
103
+ "accessions": values,
104
+ "include": includes,
105
+ "chromosomes": chromosome_values,
106
+ "hydrated": hydrated,
107
+ },
108
+ config=resolved,
109
+ progress=progress,
110
+ headers=headers,
111
+ )
112
+
113
+
114
+ def annotation(
115
+ accession: str,
116
+ output_dir: str | Path,
117
+ *,
118
+ formats: Sequence[str] = ("GENOME_GFF", "GENOME_GTF", "GENOME_GBFF", "SEQUENCE_REPORT"),
119
+ chromosomes: Sequence[str] = (),
120
+ config: DownloadConfig | None = None,
121
+ progress: Callable[[DownloadProgress], None] | None = None,
122
+ api_base_url: str = DEFAULT_NCBI_DATASETS_URL,
123
+ ) -> DatasetDownloadResult:
124
+ """Download an NCBI genome-annotation package (GFF3/GTF/GBFF/reports)."""
125
+
126
+ return genome_package(
127
+ accession,
128
+ output_dir,
129
+ include=formats,
130
+ chromosomes=chromosomes,
131
+ config=config,
132
+ progress=progress,
133
+ api_base_url=api_base_url,
134
+ )
135
+
136
+
137
+ def gene(
138
+ gene_ids: int | str | Sequence[int | str],
139
+ output_dir: str | Path,
140
+ *,
141
+ include: Sequence[str] = (
142
+ "FASTA_GENE",
143
+ "FASTA_RNA",
144
+ "FASTA_CDS",
145
+ "FASTA_PROTEIN",
146
+ "FASTA_5P_UTR",
147
+ "FASTA_3P_UTR",
148
+ ),
149
+ accession_filter: Sequence[str] = (),
150
+ include_product_report: bool = True,
151
+ config: DownloadConfig | None = None,
152
+ progress: Callable[[DownloadProgress], None] | None = None,
153
+ api_base_url: str = DEFAULT_NCBI_DATASETS_URL,
154
+ ) -> DatasetDownloadResult:
155
+ """Download gene, transcript, CDS, UTR, and protein FASTA from NCBI."""
156
+
157
+ resolved = resolved_config(config)
158
+ values = query_values(gene_ids, name="gene_ids", maximum=100)
159
+ if any(not value.isdigit() for value in values):
160
+ raise ConfigurationError("gene requires numeric NCBI GeneIDs.", code="INVALID_QUERY")
161
+ includes = _validate_include(include, _GENE_INCLUDE)
162
+ accessions = tuple(str(value).strip() for value in accession_filter)
163
+ if any(not value or len(value) > 256 for value in accessions):
164
+ raise ConfigurationError("accession_filter contains an invalid value.")
165
+ url = build_url(
166
+ api_base_url,
167
+ f"/gene/id/{quoted_path_values(values)}/download",
168
+ (
169
+ ("include_annotation_type", includes),
170
+ ("accession_filter", accessions),
171
+ ("aux_report", "PRODUCT_REPORT" if include_product_report else None),
172
+ (
173
+ "tabular_reports",
174
+ ("DATASET_REPORT", "PRODUCT_REPORT")
175
+ if include_product_report
176
+ else ("DATASET_REPORT",),
177
+ ),
178
+ ),
179
+ )
180
+ headers = {} if resolved.api_key is None else {"api-key": resolved.api_key}
181
+ return dataset(
182
+ (RemoteFile(url, filename=f"ncbi_gene_{_slug(values)}.zip"),),
183
+ output_dir,
184
+ kind="ncbi_gene_package",
185
+ source="NCBI Datasets Gene",
186
+ metadata={"gene_ids": values, "include": includes, "accession_filter": accessions},
187
+ config=resolved,
188
+ progress=progress,
189
+ headers=headers,
190
+ )
191
+
192
+
193
+ def taxonomy(
194
+ tax_ids: int | str | Sequence[int | str],
195
+ output_dir: str | Path,
196
+ *,
197
+ include_names: bool = True,
198
+ include_summary: bool = True,
199
+ config: DownloadConfig | None = None,
200
+ progress: Callable[[DownloadProgress], None] | None = None,
201
+ api_base_url: str = DEFAULT_NCBI_DATASETS_URL,
202
+ ) -> DatasetDownloadResult:
203
+ """Download taxonomy JSONL, lineage/names, and optional summary TSV."""
204
+
205
+ resolved = resolved_config(config)
206
+ values = query_values(tax_ids, name="tax_ids", maximum=100)
207
+ if any(not value.isdigit() for value in values):
208
+ raise ConfigurationError("taxonomy requires numeric NCBI TaxIDs.", code="INVALID_QUERY")
209
+ reports: list[str] = []
210
+ if include_names:
211
+ reports.append("NAMES_REPORT")
212
+ if include_summary:
213
+ reports.append("TAXONOMY_SUMMARY")
214
+ url = build_url(
215
+ api_base_url,
216
+ f"/taxonomy/taxon/{quoted_path_values(values)}/download",
217
+ (("aux_reports", tuple(reports)),),
218
+ )
219
+ headers = {} if resolved.api_key is None else {"api-key": resolved.api_key}
220
+ return dataset(
221
+ (RemoteFile(url, filename=f"ncbi_taxonomy_{_slug(values)}.zip"),),
222
+ output_dir,
223
+ kind="ncbi_taxonomy_package",
224
+ source="NCBI Datasets Taxonomy",
225
+ metadata={"tax_ids": values, "aux_reports": tuple(reports)},
226
+ config=resolved,
227
+ progress=progress,
228
+ headers=headers,
229
+ )
230
+
231
+
232
+ def virus_package(
233
+ query: str | Sequence[str],
234
+ output_dir: str | Path,
235
+ *,
236
+ by: Literal["auto", "accession", "taxon"] = "auto",
237
+ include: Sequence[str] = ("GENOME", "CDS", "PROTEIN"),
238
+ aux_reports: Sequence[str] = ("ANNOTATION", "BIOSAMPLE_REPORT"),
239
+ refseq_only: bool = False,
240
+ annotated_only: bool = False,
241
+ complete_only: bool = False,
242
+ host: str | None = None,
243
+ geo_location: str | None = None,
244
+ released_since: str | None = None,
245
+ updated_since: str | None = None,
246
+ config: DownloadConfig | None = None,
247
+ progress: Callable[[DownloadProgress], None] | None = None,
248
+ api_base_url: str = DEFAULT_NCBI_DATASETS_URL,
249
+ ) -> DatasetDownloadResult:
250
+ """Download an NCBI Virus genome, CDS, protein, annotation, and metadata ZIP."""
251
+
252
+ resolved = resolved_config(config)
253
+ values = query_values(query, name="query", maximum=100)
254
+ if by == "auto":
255
+ flags = tuple(_VIRUS_ACCESSION.fullmatch(value) is not None for value in values)
256
+ if any(flags) and not all(flags):
257
+ raise ConfigurationError("Do not mix virus accessions and taxon names.")
258
+ by = "accession" if all(flags) else "taxon"
259
+ if by not in {"accession", "taxon"}:
260
+ raise ConfigurationError("virus_package by must be auto, accession, or taxon.")
261
+ if by == "taxon" and len(values) != 1:
262
+ raise ConfigurationError("Virus taxon downloads accept one taxon at a time.")
263
+ includes = _validate_include(include, _VIRUS_INCLUDE)
264
+ reports = tuple(value.upper() for value in aux_reports)
265
+ if any(value not in _VIRUS_AUX for value in reports):
266
+ raise ConfigurationError(
267
+ "aux_reports contains an unsupported virus report.",
268
+ context={"allowed": sorted(_VIRUS_AUX)},
269
+ )
270
+ for name, value in (
271
+ ("refseq_only", refseq_only),
272
+ ("annotated_only", annotated_only),
273
+ ("complete_only", complete_only),
274
+ ):
275
+ if not isinstance(value, bool):
276
+ raise ConfigurationError(f"{name} must be boolean.")
277
+ path_values = values if by == "accession" else (values[0],)
278
+ url = build_url(
279
+ api_base_url,
280
+ f"/virus/{by}/{quoted_path_values(path_values)}/genome/download",
281
+ (
282
+ ("include_sequence", includes),
283
+ ("aux_report", reports),
284
+ ("refseq_only", refseq_only),
285
+ ("annotated_only", annotated_only),
286
+ ("complete_only", complete_only),
287
+ ("host", None if host is None else require_text(host, "host", max_length=256)),
288
+ (
289
+ "geo_location",
290
+ None
291
+ if geo_location is None
292
+ else require_text(geo_location, "geo_location", max_length=256),
293
+ ),
294
+ (
295
+ "released_since",
296
+ None
297
+ if released_since is None
298
+ else require_text(released_since, "released_since", max_length=64),
299
+ ),
300
+ (
301
+ "updated_since",
302
+ None
303
+ if updated_since is None
304
+ else require_text(updated_since, "updated_since", max_length=64),
305
+ ),
306
+ ),
307
+ )
308
+ headers = {} if resolved.api_key is None else {"api-key": resolved.api_key}
309
+ return dataset(
310
+ (RemoteFile(url, filename=f"ncbi_virus_{_slug(values)}.zip"),),
311
+ output_dir,
312
+ kind="ncbi_virus_package",
313
+ source="NCBI Datasets Virus",
314
+ metadata={
315
+ "query": values,
316
+ "by": by,
317
+ "include": includes,
318
+ "aux_reports": reports,
319
+ "refseq_only": refseq_only,
320
+ "annotated_only": annotated_only,
321
+ "complete_only": complete_only,
322
+ "host": host,
323
+ "geo_location": geo_location,
324
+ "released_since": released_since,
325
+ "updated_since": updated_since,
326
+ },
327
+ config=resolved,
328
+ progress=progress,
329
+ headers=headers,
330
+ )
331
+
332
+
333
+ __all__ = ["annotation", "gene", "genome", "genome_package", "taxonomy", "virus_package"]
@@ -0,0 +1,133 @@
1
+ """Persist coordinate sequences returned by Ensembl as auditable FASTA."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import os
7
+ import re
8
+ import tempfile
9
+ from collections.abc import Callable, Sequence
10
+ from pathlib import Path
11
+
12
+ from dnakit.exceptions import DownloadError
13
+ from dnakit.search import QueryProgress, SearchConfig
14
+ from dnakit.search.ensembl import DEFAULT_ENSEMBL_REST_URL
15
+ from dnakit.search.ensembl import sequence as search_sequence
16
+
17
+ from .files import _install_staged_files, _write_manifest, resolved_config
18
+ from .models import DatasetDownloadResult, DownloadConfig, DownloadedFile
19
+
20
+
21
+ def _fasta_identifier(value: object, fallback: str) -> str:
22
+ raw = value if isinstance(value, str) and value.strip() else fallback
23
+ return re.sub(r"[^A-Za-z0-9_.:-]+", "_", str(raw))[:255]
24
+
25
+
26
+ def sequence(
27
+ species: str,
28
+ region: str | Sequence[str],
29
+ output_path: str | os.PathLike[str],
30
+ *,
31
+ strand: int = 1,
32
+ upstream: int = 0,
33
+ downstream: int = 0,
34
+ mask: str | None = None,
35
+ config: DownloadConfig | None = None,
36
+ query_progress: Callable[[QueryProgress], None] | None = None,
37
+ ensembl_api_base_url: str = DEFAULT_ENSEMBL_REST_URL,
38
+ ) -> DatasetDownloadResult:
39
+ """Fetch one or more 0-based half-open regions and atomically write FASTA."""
40
+
41
+ resolved = resolved_config(config)
42
+ target = Path(output_path).expanduser().resolve()
43
+ if target.exists() and not target.is_file():
44
+ raise IsADirectoryError(f"output_path must be a file: {target}")
45
+ target.parent.mkdir(parents=True, exist_ok=True)
46
+ manifest = target.with_name(f"{target.name}.manifest.json")
47
+ if manifest.exists() and not manifest.is_file():
48
+ raise IsADirectoryError(f"Sequence manifest target must be a file: {manifest}")
49
+ if not resolved.overwrite and (target.exists() or manifest.exists()):
50
+ raise FileExistsError("Refusing to overwrite existing FASTA or manifest.")
51
+ result = search_sequence(
52
+ species,
53
+ region,
54
+ strand=strand, # type: ignore[arg-type]
55
+ upstream=upstream,
56
+ downstream=downstream,
57
+ mask=mask, # type: ignore[arg-type]
58
+ progress=query_progress,
59
+ config=SearchConfig(
60
+ timeout=min(float(resolved.timeout), 300.0),
61
+ max_response_bytes=min(resolved.max_file_bytes, 100_000_000),
62
+ max_records=resolved.max_files,
63
+ ),
64
+ api_base_url=ensembl_api_base_url,
65
+ )
66
+ lines: list[str] = []
67
+ for index, record in enumerate(result.records, start=1):
68
+ symbols = record.get("seq")
69
+ if not isinstance(symbols, str) or not symbols:
70
+ raise DownloadError(
71
+ "Ensembl sequence response did not contain sequence text.",
72
+ code="QUERY_RESPONSE_ERROR",
73
+ )
74
+ identifier = _fasta_identifier(record.get("id"), f"region_{index}")
75
+ requested = record.get("requested_region")
76
+ description = f" requested={requested}" if isinstance(requested, str) else ""
77
+ lines.append(f">{identifier}{description}")
78
+ lines.extend(symbols[offset : offset + 80] for offset in range(0, len(symbols), 80))
79
+ payload = ("\n".join(lines) + "\n").encode("ascii")
80
+ if len(payload) > resolved.max_file_bytes:
81
+ raise DownloadError("FASTA output exceeds max_file_bytes.", code="DOWNLOAD_SIZE_LIMIT")
82
+ md5 = hashlib.md5(payload, usedforsecurity=False).hexdigest()
83
+ sha256 = hashlib.sha256(payload).hexdigest()
84
+ downloaded = DownloadedFile(
85
+ result.request_url,
86
+ str(target),
87
+ len(payload),
88
+ md5,
89
+ sha256,
90
+ False,
91
+ )
92
+ with tempfile.TemporaryDirectory(prefix=".dnakit-sequence-", dir=str(target.parent)) as raw:
93
+ stage = Path(raw)
94
+ staged_target = stage / target.name
95
+ staged_target.write_bytes(payload)
96
+ staged_manifest = stage / manifest.name
97
+ _write_manifest(
98
+ {
99
+ "kind": "coordinate_sequence",
100
+ "source": "Ensembl",
101
+ "query": result.to_dict(),
102
+ "file": {
103
+ "path": str(target),
104
+ "byte_size": len(payload),
105
+ "md5": md5,
106
+ "sha256": sha256,
107
+ },
108
+ },
109
+ staged_manifest,
110
+ overwrite=False,
111
+ )
112
+ _install_staged_files(
113
+ (staged_target, staged_manifest),
114
+ (target, manifest),
115
+ overwrite=resolved.overwrite,
116
+ )
117
+ return DatasetDownloadResult(
118
+ "coordinate_sequence",
119
+ "Ensembl",
120
+ str(target.parent),
121
+ (downloaded,),
122
+ str(manifest),
123
+ result.provenance,
124
+ metadata={
125
+ "species": species,
126
+ "region_count": len(result.records),
127
+ "input_coordinate_system": "0-based half-open",
128
+ "strand": strand,
129
+ },
130
+ )
131
+
132
+
133
+ __all__ = ["sequence"]
@@ -0,0 +1,99 @@
1
+ """Download files selected from the UCSC Genome Browser file catalog."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ from collections.abc import Callable
7
+ from pathlib import Path, PurePosixPath
8
+ from urllib.parse import quote, urlsplit
9
+
10
+ from dnakit.exceptions import ConfigurationError, DownloadError
11
+ from dnakit.search import QueryResult
12
+ from dnakit.search._http import require_https_base
13
+
14
+ from .files import dataset, resolved_config
15
+ from .models import DatasetDownloadResult, DownloadConfig, DownloadProgress, RemoteFile
16
+
17
+ DEFAULT_UCSC_DOWNLOAD_URL = "https://hgdownload.soe.ucsc.edu"
18
+
19
+
20
+ def _file_url(value: str, base_url: str) -> tuple[str, str]:
21
+ raw = value.strip()
22
+ parsed = urlsplit(raw)
23
+ if parsed.scheme:
24
+ if parsed.scheme != "https" or not parsed.netloc or parsed.fragment:
25
+ raise ConfigurationError("UCSC file URL must use HTTPS.", code="INVALID_DOWNLOAD_URL")
26
+ path = parsed.path
27
+ url = raw
28
+ else:
29
+ relative = raw.lstrip("/")
30
+ path = "/" + relative
31
+ url = f"{require_https_base(base_url)}/{quote(relative, safe='/._-')}"
32
+ parts = PurePosixPath(path).parts
33
+ if not parts or ".." in parts:
34
+ raise ConfigurationError("UCSC file path is unsafe.", code="INVALID_DOWNLOAD_URL")
35
+ basename = PurePosixPath(path).name
36
+ if not basename:
37
+ raise ConfigurationError("UCSC file path has no filename.", code="INVALID_DOWNLOAD_URL")
38
+ digest = hashlib.sha256(raw.encode("utf-8")).hexdigest()[:12]
39
+ maximum_basename = 240 - len(digest)
40
+ filename = f"{digest}_{basename[:maximum_basename]}"
41
+ return url, filename
42
+
43
+
44
+ def files(
45
+ query: QueryResult,
46
+ output_dir: str | Path,
47
+ *,
48
+ config: DownloadConfig | None = None,
49
+ progress: Callable[[DownloadProgress], None] | None = None,
50
+ download_base_url: str = DEFAULT_UCSC_DOWNLOAD_URL,
51
+ ) -> DatasetDownloadResult:
52
+ """Download a bounded ``dnakit.search.ucsc_files`` result with local checksums."""
53
+
54
+ if (
55
+ not isinstance(query, QueryResult)
56
+ or query.provider != "UCSC"
57
+ or query.query_type != "files"
58
+ ):
59
+ raise ConfigurationError("query must be a dnakit.search.ucsc_files() result.")
60
+ resolved = resolved_config(config)
61
+ resources: list[RemoteFile] = []
62
+ expected_total = 0
63
+ source_paths: dict[str, str] = {}
64
+ for record in query.records:
65
+ raw_url = record.get("url")
66
+ if not isinstance(raw_url, str) or not raw_url.strip():
67
+ raise DownloadError("UCSC file record has no URL.", code="QUERY_RESPONSE_ERROR")
68
+ url, filename = _file_url(raw_url, download_base_url)
69
+ raw_size = record.get("sizeBytes")
70
+ if isinstance(raw_size, int) and not isinstance(raw_size, bool) and raw_size >= 0:
71
+ if raw_size > resolved.max_file_bytes:
72
+ raise DownloadError("UCSC file exceeds max_file_bytes.", code="DOWNLOAD_SIZE_LIMIT")
73
+ expected_total += raw_size
74
+ resources.append(RemoteFile(url, filename=filename))
75
+ source_paths[filename] = raw_url
76
+ if not resources:
77
+ raise DownloadError(
78
+ "UCSC query selected no downloadable files.", code="UCSC_FILES_NOT_FOUND"
79
+ )
80
+ if len(resources) > resolved.max_files:
81
+ raise DownloadError("UCSC files exceed max_files.", code="DOWNLOAD_FILE_LIMIT")
82
+ if expected_total > resolved.max_total_bytes:
83
+ raise DownloadError("UCSC files exceed max_total_bytes.", code="DOWNLOAD_TOTAL_SIZE_LIMIT")
84
+ return dataset(
85
+ resources,
86
+ output_dir,
87
+ kind="ucsc_files",
88
+ source="UCSC Genome Browser Downloads",
89
+ metadata={
90
+ "catalog_url": query.request_url,
91
+ "expected_total_bytes": expected_total,
92
+ "source_paths": source_paths,
93
+ },
94
+ config=resolved,
95
+ progress=progress,
96
+ )
97
+
98
+
99
+ __all__ = ["DEFAULT_UCSC_DOWNLOAD_URL", "files"]
@@ -0,0 +1,81 @@
1
+ """Transparent, deterministic DNA sequence and dataset evaluation APIs."""
2
+
3
+ from dnakit.evaluation.collection import (
4
+ evaluate_diversity,
5
+ evaluate_redundancy,
6
+ evaluate_uniqueness,
7
+ )
8
+ from dnakit.evaluation.config import (
9
+ AmbiguityEvaluationConfig,
10
+ ComplexityEvaluationConfig,
11
+ DistributionEvaluationConfig,
12
+ DiversityEvaluationConfig,
13
+ EvaluationLimits,
14
+ FragmentSimilarityConfig,
15
+ FrechetDistanceConfig,
16
+ QualityEvaluationConfig,
17
+ ReferenceSearchConfig,
18
+ ScorecardConfig,
19
+ ScoreRule,
20
+ SNNConfig,
21
+ SynthesisRiskConfig,
22
+ UniquenessEvaluationConfig,
23
+ )
24
+ from dnakit.evaluation.distribution import evaluate_distribution_similarity
25
+ from dnakit.evaluation.frechet import evaluate_frechet_distance
26
+ from dnakit.evaluation.generative import evaluate_fragment_similarity, evaluate_snn
27
+ from dnakit.evaluation.reference import (
28
+ create_reference_library,
29
+ evaluate_memorization,
30
+ evaluate_novelty,
31
+ evaluate_reference_similarity,
32
+ nearest_reference,
33
+ )
34
+ from dnakit.evaluation.results import EvaluationEntry, EvaluationReport, ReferenceLibrary
35
+ from dnakit.evaluation.scorecard import ScoreInput, evaluate_scorecard
36
+ from dnakit.evaluation.sequence import (
37
+ evaluate_ambiguity,
38
+ evaluate_complexity,
39
+ evaluate_quality,
40
+ evaluate_validity,
41
+ )
42
+ from dnakit.evaluation.synthesis import evaluate_synthesis_risk
43
+
44
+ __all__ = [
45
+ "AmbiguityEvaluationConfig",
46
+ "ComplexityEvaluationConfig",
47
+ "DistributionEvaluationConfig",
48
+ "DiversityEvaluationConfig",
49
+ "EvaluationEntry",
50
+ "EvaluationLimits",
51
+ "EvaluationReport",
52
+ "FragmentSimilarityConfig",
53
+ "FrechetDistanceConfig",
54
+ "QualityEvaluationConfig",
55
+ "ReferenceLibrary",
56
+ "ReferenceSearchConfig",
57
+ "SNNConfig",
58
+ "ScoreInput",
59
+ "ScoreRule",
60
+ "ScorecardConfig",
61
+ "SynthesisRiskConfig",
62
+ "UniquenessEvaluationConfig",
63
+ "create_reference_library",
64
+ "evaluate_ambiguity",
65
+ "evaluate_complexity",
66
+ "evaluate_distribution_similarity",
67
+ "evaluate_diversity",
68
+ "evaluate_fragment_similarity",
69
+ "evaluate_frechet_distance",
70
+ "evaluate_memorization",
71
+ "evaluate_novelty",
72
+ "evaluate_quality",
73
+ "evaluate_redundancy",
74
+ "evaluate_reference_similarity",
75
+ "evaluate_scorecard",
76
+ "evaluate_snn",
77
+ "evaluate_synthesis_risk",
78
+ "evaluate_uniqueness",
79
+ "evaluate_validity",
80
+ "nearest_reference",
81
+ ]