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
dnakit/search/ucsc.py ADDED
@@ -0,0 +1,388 @@
1
+ """Bounded UCSC Genome Browser REST API adapters."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import fnmatch
6
+ import re
7
+ from collections.abc import Mapping
8
+ from typing import cast
9
+
10
+ from dnakit.core import Provenance
11
+ from dnakit.exceptions import ConfigurationError, QueryError
12
+
13
+ from ._http import build_url, limited_records, redact_url, request_json, require_text
14
+ from ._shared import adapter_provenance, resolved_config
15
+ from .models import QueryResult, SearchConfig
16
+
17
+ DEFAULT_UCSC_API_URL = "https://api.genome.ucsc.edu"
18
+ _REGION = re.compile(r"^(?P<chrom>[^:\s]+):(?P<start>\d+)-(?P<end>\d+)$")
19
+
20
+
21
+ def _name(value: str, field: str) -> str:
22
+ return require_text(value, field, max_length=256)
23
+
24
+
25
+ def _region(value: str, *, max_span: int) -> tuple[str, int, int]:
26
+ text = require_text(value, "region", max_length=512)
27
+ match = _REGION.fullmatch(text)
28
+ if match is None:
29
+ raise ConfigurationError(
30
+ "region must use 'chromosome:start-end' with 0-based half-open coordinates.",
31
+ code="INVALID_REGION_QUERY",
32
+ )
33
+ chrom = match.group("chrom")
34
+ start = int(match.group("start"))
35
+ end = int(match.group("end"))
36
+ if end <= start or end - start > max_span:
37
+ raise ConfigurationError(
38
+ "region is empty or exceeds the endpoint span limit.",
39
+ code="REGION_QUERY_LIMIT",
40
+ context={"span": end - start, "max_span": max_span},
41
+ )
42
+ return chrom, start, end
43
+
44
+
45
+ def _provenance(filters: Mapping[str, object], *, version: str | None = None) -> Provenance:
46
+ return adapter_provenance(
47
+ "UCSC Genome Browser REST API",
48
+ citation_url="https://genome.ucsc.edu/goldenPath/help/api.html",
49
+ filters=filters,
50
+ version=version,
51
+ )
52
+
53
+
54
+ def genomes(
55
+ *,
56
+ contains: str | None = None,
57
+ config: SearchConfig | None = None,
58
+ api_base_url: str = DEFAULT_UCSC_API_URL,
59
+ ) -> QueryResult:
60
+ """List UCSC browser assemblies, optionally filtering their text fields."""
61
+
62
+ resolved = resolved_config(config)
63
+ needle = None if contains is None else require_text(contains, "contains").casefold()
64
+ url = build_url(api_base_url, "/list/ucscGenomes")
65
+ payload = request_json(url, resolved, provider="UCSC")
66
+ root = payload.get("ucscGenomes") if isinstance(payload, Mapping) else None
67
+ if not isinstance(root, Mapping):
68
+ raise QueryError("UCSC returned an unexpected genome list.", code="QUERY_RESPONSE_ERROR")
69
+ records: list[dict[str, object]] = []
70
+ for identifier, raw in root.items():
71
+ if not isinstance(identifier, str) or not isinstance(raw, Mapping):
72
+ continue
73
+ record: dict[str, object] = {"genome": identifier, **dict(raw)}
74
+ if needle is None or needle in " ".join(str(value) for value in record.values()).casefold():
75
+ records.append(record)
76
+ records.sort(key=lambda item: str(item["genome"]))
77
+ bounded = limited_records(records, resolved)
78
+ return QueryResult(
79
+ "assembly",
80
+ "UCSC",
81
+ redact_url(url),
82
+ bounded,
83
+ _provenance({"contains": contains}),
84
+ total_count=len(bounded),
85
+ metadata={"filter_applied_client_side": needle is not None},
86
+ )
87
+
88
+
89
+ def chromosomes(
90
+ genome: str,
91
+ *,
92
+ track: str | None = None,
93
+ config: SearchConfig | None = None,
94
+ api_base_url: str = DEFAULT_UCSC_API_URL,
95
+ ) -> QueryResult:
96
+ """List chromosome, contig, scaffold, and organelle names and lengths."""
97
+
98
+ resolved = resolved_config(config)
99
+ assembly = _name(genome, "genome")
100
+ track_name = None if track is None else _name(track, "track")
101
+ url = build_url(
102
+ api_base_url,
103
+ "/list/chromosomes",
104
+ (("genome", assembly), ("track", track_name)),
105
+ )
106
+ payload = request_json(url, resolved, provider="UCSC")
107
+ raw = payload.get("chromosomes") if isinstance(payload, Mapping) else None
108
+ if not isinstance(raw, Mapping):
109
+ raise QueryError(
110
+ "UCSC returned an unexpected chromosome list.", code="QUERY_RESPONSE_ERROR"
111
+ )
112
+ records = [
113
+ {"name": name, "length": length}
114
+ for name, length in raw.items()
115
+ if isinstance(name, str)
116
+ and isinstance(length, int)
117
+ and not isinstance(length, bool)
118
+ and length >= 0
119
+ ]
120
+ records.sort(key=lambda item: str(item["name"]))
121
+ bounded = limited_records(records, resolved)
122
+ data_time = payload.get("dataTime") if isinstance(payload, Mapping) else None
123
+ return QueryResult(
124
+ "chromosome",
125
+ "UCSC",
126
+ redact_url(url),
127
+ bounded,
128
+ _provenance(
129
+ {"genome": assembly, "track": track_name},
130
+ version=data_time if isinstance(data_time, str) else None,
131
+ ),
132
+ total_count=len(bounded),
133
+ metadata={"genome": assembly, "track": track_name},
134
+ )
135
+
136
+
137
+ def sequence(
138
+ genome: str,
139
+ region: str,
140
+ *,
141
+ reverse_complement: bool = False,
142
+ config: SearchConfig | None = None,
143
+ api_base_url: str = DEFAULT_UCSC_API_URL,
144
+ ) -> QueryResult:
145
+ """Return a bounded UCSC assembly sequence for a 0-based half-open region."""
146
+
147
+ resolved = resolved_config(config)
148
+ assembly = _name(genome, "genome")
149
+ chrom, start, end = _region(region, max_span=10_000_000)
150
+ if not isinstance(reverse_complement, bool):
151
+ raise ConfigurationError("reverse_complement must be boolean.", code="INVALID_QUERY")
152
+ url = build_url(
153
+ api_base_url,
154
+ "/getData/sequence",
155
+ (
156
+ ("genome", assembly),
157
+ ("chrom", chrom),
158
+ ("start", start),
159
+ ("end", end),
160
+ ("revComp", reverse_complement),
161
+ ),
162
+ )
163
+ payload = request_json(url, resolved, provider="UCSC")
164
+ dna = payload.get("dna") if isinstance(payload, Mapping) else None
165
+ if not isinstance(dna, str):
166
+ raise QueryError("UCSC sequence response has no DNA text.", code="QUERY_RESPONSE_ERROR")
167
+ data_time = payload.get("dataTime") if isinstance(payload, Mapping) else None
168
+ return QueryResult(
169
+ "sequence",
170
+ "UCSC",
171
+ redact_url(url),
172
+ (
173
+ {
174
+ "genome": assembly,
175
+ "chromosome": chrom,
176
+ "start_0based": start,
177
+ "end_0based": end,
178
+ "reverse_complement": reverse_complement,
179
+ "seq": dna,
180
+ },
181
+ ),
182
+ _provenance(
183
+ {
184
+ "genome": assembly,
185
+ "region": region,
186
+ "reverse_complement": reverse_complement,
187
+ },
188
+ version=data_time if isinstance(data_time, str) else None,
189
+ ),
190
+ total_count=1,
191
+ metadata={"input_coordinate_system": "0-based half-open"},
192
+ )
193
+
194
+
195
+ def tracks(
196
+ genome: str,
197
+ *,
198
+ contains: str | None = None,
199
+ leaves_only: bool = True,
200
+ config: SearchConfig | None = None,
201
+ api_base_url: str = DEFAULT_UCSC_API_URL,
202
+ ) -> QueryResult:
203
+ """List UCSC track definitions, with an optional client-side text filter."""
204
+
205
+ resolved = resolved_config(config)
206
+ assembly = _name(genome, "genome")
207
+ needle = None if contains is None else require_text(contains, "contains").casefold()
208
+ if not isinstance(leaves_only, bool):
209
+ raise ConfigurationError("leaves_only must be boolean.", code="INVALID_QUERY")
210
+ url = build_url(
211
+ api_base_url,
212
+ "/list/tracks",
213
+ (("genome", assembly), ("trackLeavesOnly", leaves_only)),
214
+ )
215
+ payload = request_json(url, resolved, provider="UCSC")
216
+ raw = payload.get(assembly) if isinstance(payload, Mapping) else None
217
+ if not isinstance(raw, Mapping):
218
+ raise QueryError("UCSC returned an unexpected track list.", code="QUERY_RESPONSE_ERROR")
219
+ records: list[dict[str, object]] = []
220
+ for track_name, definition in raw.items():
221
+ if not isinstance(track_name, str) or not isinstance(definition, Mapping):
222
+ continue
223
+ record: dict[str, object] = {"track": track_name, **dict(definition)}
224
+ if needle is None or needle in " ".join(str(value) for value in record.values()).casefold():
225
+ records.append(record)
226
+ records.sort(key=lambda item: str(item["track"]))
227
+ bounded = limited_records(records, resolved)
228
+ data_time = payload.get("dataTime") if isinstance(payload, Mapping) else None
229
+ return QueryResult(
230
+ "tracks",
231
+ "UCSC",
232
+ redact_url(url),
233
+ bounded,
234
+ _provenance(
235
+ {"genome": assembly, "contains": contains, "leaves_only": leaves_only},
236
+ version=data_time if isinstance(data_time, str) else None,
237
+ ),
238
+ total_count=len(bounded),
239
+ metadata={"filter_applied_client_side": needle is not None},
240
+ )
241
+
242
+
243
+ def track_data(
244
+ genome: str,
245
+ track: str,
246
+ region: str,
247
+ *,
248
+ max_items: int = 1_000,
249
+ config: SearchConfig | None = None,
250
+ api_base_url: str = DEFAULT_UCSC_API_URL,
251
+ ) -> QueryResult:
252
+ """Query bounded annotations, repeats, regulation, or signal from one UCSC track."""
253
+
254
+ resolved = resolved_config(config)
255
+ assembly = _name(genome, "genome")
256
+ track_name = _name(track, "track")
257
+ chrom, start, end = _region(region, max_span=5_000_000)
258
+ if (
259
+ isinstance(max_items, bool)
260
+ or not isinstance(max_items, int)
261
+ or not 1 <= max_items <= min(1_000_000, resolved.max_records)
262
+ ):
263
+ raise ConfigurationError(
264
+ "max_items exceeds SearchConfig.max_records.", code="QUERY_RECORD_LIMIT"
265
+ )
266
+ url = build_url(
267
+ api_base_url,
268
+ "/getData/track",
269
+ (
270
+ ("genome", assembly),
271
+ ("track", track_name),
272
+ ("chrom", chrom),
273
+ ("start", start),
274
+ ("end", end),
275
+ ("maxItemsOutput", max_items),
276
+ ),
277
+ )
278
+ payload = request_json(url, resolved, provider="UCSC")
279
+ raw = payload.get(track_name) if isinstance(payload, Mapping) else None
280
+ if not isinstance(raw, list):
281
+ raise QueryError("UCSC returned unexpected track data.", code="QUERY_RESPONSE_ERROR")
282
+ records: list[dict[str, object]] = []
283
+ for item in raw:
284
+ if not isinstance(item, Mapping):
285
+ continue
286
+ record = dict(item)
287
+ item_start = item.get("chromStart")
288
+ item_end = item.get("chromEnd")
289
+ if isinstance(item_start, int) and isinstance(item_end, int):
290
+ record["start_0based"] = item_start
291
+ record["end_0based"] = item_end
292
+ records.append(record)
293
+ bounded = limited_records(records, resolved)
294
+ data_time = payload.get("dataTime") if isinstance(payload, Mapping) else None
295
+ server_count = payload.get("itemsReturned") if isinstance(payload, Mapping) else None
296
+ total = (
297
+ server_count
298
+ if isinstance(server_count, int) and not isinstance(server_count, bool)
299
+ else len(bounded)
300
+ )
301
+ return QueryResult(
302
+ "annotation",
303
+ "UCSC",
304
+ redact_url(url),
305
+ bounded,
306
+ _provenance(
307
+ {"genome": assembly, "track": track_name, "region": region},
308
+ version=data_time if isinstance(data_time, str) else None,
309
+ ),
310
+ total_count=max(total, len(bounded)),
311
+ metadata={
312
+ "input_coordinate_system": "0-based half-open",
313
+ "track": track_name,
314
+ "track_type": payload.get("trackType") if isinstance(payload, Mapping) else None,
315
+ },
316
+ )
317
+
318
+
319
+ def files(
320
+ genome: str,
321
+ *,
322
+ pattern: str | None = None,
323
+ limit: int = 1_000,
324
+ config: SearchConfig | None = None,
325
+ api_base_url: str = DEFAULT_UCSC_API_URL,
326
+ ) -> QueryResult:
327
+ """List UCSC downloadable files, optionally filtering paths with a glob."""
328
+
329
+ resolved = resolved_config(config)
330
+ assembly = _name(genome, "genome")
331
+ if (
332
+ isinstance(limit, bool)
333
+ or not isinstance(limit, int)
334
+ or not 1 <= limit <= min(1_000_000, resolved.max_records)
335
+ ):
336
+ raise ConfigurationError(
337
+ "limit exceeds SearchConfig.max_records.", code="QUERY_RECORD_LIMIT"
338
+ )
339
+ glob = None if pattern is None else require_text(pattern, "pattern", max_length=1_024)
340
+ url = build_url(
341
+ api_base_url,
342
+ "/list/files",
343
+ (("genome", assembly), ("maxItemsOutput", limit)),
344
+ )
345
+ payload = request_json(url, resolved, provider="UCSC")
346
+ raw = payload.get("urlList") if isinstance(payload, Mapping) else None
347
+ if not isinstance(raw, list):
348
+ raise QueryError("UCSC returned an unexpected file list.", code="QUERY_RESPONSE_ERROR")
349
+ records = [
350
+ dict(item)
351
+ for item in raw
352
+ if isinstance(item, Mapping)
353
+ and isinstance(item.get("url"), str)
354
+ and (glob is None or fnmatch.fnmatch(cast(str, item["url"]), glob))
355
+ ]
356
+ bounded = limited_records(records, resolved)
357
+ data_time = payload.get("dataTime") if isinstance(payload, Mapping) else None
358
+ return QueryResult(
359
+ "files",
360
+ "UCSC",
361
+ redact_url(url),
362
+ bounded,
363
+ _provenance(
364
+ {"genome": assembly, "pattern": pattern, "server_limit": limit},
365
+ version=data_time if isinstance(data_time, str) else None,
366
+ ),
367
+ total_count=len(bounded),
368
+ metadata={
369
+ "filter_applied_client_side": glob is not None,
370
+ "server_items_returned": payload.get("itemsReturned")
371
+ if isinstance(payload, Mapping)
372
+ else None,
373
+ "server_limit_reached": payload.get("maxItemsLimit")
374
+ if isinstance(payload, Mapping)
375
+ else None,
376
+ },
377
+ )
378
+
379
+
380
+ __all__ = [
381
+ "DEFAULT_UCSC_API_URL",
382
+ "chromosomes",
383
+ "files",
384
+ "genomes",
385
+ "sequence",
386
+ "track_data",
387
+ "tracks",
388
+ ]
@@ -0,0 +1,38 @@
1
+ """Secondary-structure parsing and explicit NUPACK integration."""
2
+
3
+ from .dotbracket import (
4
+ analyze_dot_bracket,
5
+ ensemble_defect_from_probabilities,
6
+ pair_probability_metrics,
7
+ target_structure_probability,
8
+ )
9
+ from .nupack import NupackAdapter, probe_nupack
10
+ from .results import (
11
+ AccessibilityWindow,
12
+ ComplexConcentration,
13
+ NupackComplexResult,
14
+ NupackTubeResult,
15
+ PairProbabilityResult,
16
+ PredictedSecondaryStructure,
17
+ SecondaryBasePair,
18
+ SecondaryStem,
19
+ SecondaryStructureSummary,
20
+ )
21
+
22
+ __all__ = [
23
+ "AccessibilityWindow",
24
+ "ComplexConcentration",
25
+ "NupackAdapter",
26
+ "NupackComplexResult",
27
+ "NupackTubeResult",
28
+ "PairProbabilityResult",
29
+ "PredictedSecondaryStructure",
30
+ "SecondaryBasePair",
31
+ "SecondaryStem",
32
+ "SecondaryStructureSummary",
33
+ "analyze_dot_bracket",
34
+ "ensemble_defect_from_probabilities",
35
+ "pair_probability_metrics",
36
+ "probe_nupack",
37
+ "target_structure_probability",
38
+ ]