seqevi 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- seqevi/__init__.py +7 -0
- seqevi/__main__.py +6 -0
- seqevi/adapters/__init__.py +41 -0
- seqevi/adapters/base.py +131 -0
- seqevi/adapters/dbcan_cazyme.py +588 -0
- seqevi/adapters/eggnog.py +674 -0
- seqevi/adapters/interpro_pfam.py +757 -0
- seqevi/adapters/registry.py +68 -0
- seqevi/annotate.py +413 -0
- seqevi/api.py +390 -0
- seqevi/cli.py +610 -0
- seqevi/distribution/__init__.py +13 -0
- seqevi/distribution/manifest.py +199 -0
- seqevi/distribution/oci.py +490 -0
- seqevi/distribution/setup.py +752 -0
- seqevi/errors.py +73 -0
- seqevi/evidence.py +295 -0
- seqevi/execution_profile.py +526 -0
- seqevi/hashing.py +13 -0
- seqevi/kits/__init__.py +1 -0
- seqevi/kits/dbcan-cazyme.toml +35 -0
- seqevi/resource_lock.py +438 -0
- seqevi/result.py +682 -0
- seqevi/runner.py +163 -0
- seqevi/runtime_identity.py +104 -0
- seqevi/sequence.py +383 -0
- seqevi/service/__init__.py +11 -0
- seqevi/service/app.py +213 -0
- seqevi/service/config.py +38 -0
- seqevi/service/persistence.py +360 -0
- seqevi/store/__init__.py +14 -0
- seqevi/store/artifact.py +225 -0
- seqevi/store/client.py +311 -0
- seqevi/store/contract.py +33 -0
- seqevi/store/factory.py +38 -0
- seqevi/store/local.py +479 -0
- seqevi/store/migration.py +62 -0
- seqevi/store/migrations/__init__.py +1 -0
- seqevi/store/migrations/env.py +30 -0
- seqevi/store/migrations/versions/0001_initial_store.py +103 -0
- seqevi/store/migrations/versions/0002_artifact_byte_size_bigint.py +40 -0
- seqevi/store/migrations/versions/__init__.py +1 -0
- seqevi/store/schema.py +86 -0
- seqevi/store/transport.py +224 -0
- seqevi-0.2.0.dist-info/METADATA +333 -0
- seqevi-0.2.0.dist-info/RECORD +49 -0
- seqevi-0.2.0.dist-info/WHEEL +4 -0
- seqevi-0.2.0.dist-info/entry_points.txt +5 -0
- seqevi-0.2.0.dist-info/licenses/LICENSE +21 -0
seqevi/api.py
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
"""Public Python application boundary for SeqEvi annotation results."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import duckdb
|
|
11
|
+
|
|
12
|
+
from . import __version__
|
|
13
|
+
from .adapters import (
|
|
14
|
+
AdapterConfiguration,
|
|
15
|
+
AdapterName,
|
|
16
|
+
AnnotationAdapter,
|
|
17
|
+
create_adapter,
|
|
18
|
+
)
|
|
19
|
+
from .annotate import AnnotationSummary, run_annotation
|
|
20
|
+
from .distribution.oci import run_oci_annotation
|
|
21
|
+
from .errors import AnnotationError
|
|
22
|
+
from .execution_profile import (
|
|
23
|
+
ExecutionProfile,
|
|
24
|
+
load_execution_profile,
|
|
25
|
+
load_named_profile,
|
|
26
|
+
)
|
|
27
|
+
from .result import RESULT_FORMAT_VERSION, scan_annotations as _scan_annotations
|
|
28
|
+
from .store import open_evidence_store
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True, slots=True)
|
|
32
|
+
class ResolvedAnnotationInputs:
|
|
33
|
+
"""Concrete inputs after profile/config resolution.
|
|
34
|
+
|
|
35
|
+
For a managed v2 profile, ``executable`` is ``None`` and ``profile`` holds
|
|
36
|
+
the immutable OCI runtime. The application boundary dispatches that case
|
|
37
|
+
without exposing Docker branches to scientific adapters.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
adapter: AdapterName
|
|
41
|
+
executable: Path | None
|
|
42
|
+
resource: Path
|
|
43
|
+
store: str | Path | None
|
|
44
|
+
threads: int
|
|
45
|
+
timeout_seconds: float | None
|
|
46
|
+
environment: tuple[tuple[str, str], ...] = ()
|
|
47
|
+
profile: ExecutionProfile | None = None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True, slots=True)
|
|
51
|
+
class AnnotationInvocation:
|
|
52
|
+
"""Internal shared application result used by API and CLI presenters."""
|
|
53
|
+
|
|
54
|
+
relation: duckdb.DuckDBPyRelation
|
|
55
|
+
summary: AnnotationSummary
|
|
56
|
+
adapter: str
|
|
57
|
+
result_schema_id: str
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def annotate(
|
|
61
|
+
fasta: str | Path,
|
|
62
|
+
*,
|
|
63
|
+
output: str | Path,
|
|
64
|
+
profile: str | None = None,
|
|
65
|
+
config: str | Path | None = None,
|
|
66
|
+
adapter: AdapterName | str | None = None,
|
|
67
|
+
executable: str | Path | None = None,
|
|
68
|
+
resource: str | Path | None = None,
|
|
69
|
+
store: str | Path | None = None,
|
|
70
|
+
threads: int | None = None,
|
|
71
|
+
timeout_seconds: float | None = None,
|
|
72
|
+
) -> duckdb.DuckDBPyRelation:
|
|
73
|
+
"""Annotate a protein FASTA and return its native DuckDB relation.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
fasta: Input protein FASTA path.
|
|
77
|
+
output: New `.duckdb` result path. Existing paths are rejected.
|
|
78
|
+
profile: Named profile under the user's SeqEvi configuration directory.
|
|
79
|
+
config: Explicit execution-profile TOML path.
|
|
80
|
+
adapter: Official adapter name for complete explicit mode.
|
|
81
|
+
executable: External adapter launcher path or command name.
|
|
82
|
+
resource: Native upstream annotation resource directory.
|
|
83
|
+
store: Local Store path or shared Store URL.
|
|
84
|
+
threads: Operational worker-thread override.
|
|
85
|
+
timeout_seconds: Optional external-tool timeout.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
The `main.annotations` DuckDB relation. It keeps the read-only result
|
|
89
|
+
connection alive after this function returns.
|
|
90
|
+
|
|
91
|
+
Raises:
|
|
92
|
+
AnnotationError: If configuration, annotation, or result publication
|
|
93
|
+
fails. The function does not translate errors into CLI exit codes.
|
|
94
|
+
|
|
95
|
+
Examples:
|
|
96
|
+
>>> relation = annotate(
|
|
97
|
+
... "proteins.faa",
|
|
98
|
+
... output="annotations.duckdb",
|
|
99
|
+
... profile="interpro-pfam",
|
|
100
|
+
... )
|
|
101
|
+
>>> "InputID" in relation.columns
|
|
102
|
+
True
|
|
103
|
+
|
|
104
|
+
Notes:
|
|
105
|
+
The Store remains the incremental evidence cache. The returned file is
|
|
106
|
+
a complete immutable snapshot for this invocation, including cached
|
|
107
|
+
sequences and newly computed misses.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
invocation = _run_annotation_application(
|
|
111
|
+
fasta=Path(fasta),
|
|
112
|
+
output=Path(output),
|
|
113
|
+
profile=profile,
|
|
114
|
+
config=config,
|
|
115
|
+
adapter=adapter,
|
|
116
|
+
executable=executable,
|
|
117
|
+
resource=resource,
|
|
118
|
+
store=store,
|
|
119
|
+
threads=threads,
|
|
120
|
+
timeout_seconds=timeout_seconds,
|
|
121
|
+
)
|
|
122
|
+
return invocation.relation
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def scan_annotations(path: str | Path) -> duckdb.DuckDBPyRelation:
|
|
126
|
+
"""Open an existing SeqEvi result read-only as a native relation.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
path: Published `.duckdb` result path.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
The validated `main.annotations` relation. The relation remains usable
|
|
133
|
+
after this function returns and exposes DuckDB's native `select`,
|
|
134
|
+
`filter`, Polars, and Arrow reader methods.
|
|
135
|
+
|
|
136
|
+
Raises:
|
|
137
|
+
AnnotationError: If the path is missing or fails the result contract.
|
|
138
|
+
|
|
139
|
+
Examples:
|
|
140
|
+
>>> relation = scan_annotations("annotations.duckdb")
|
|
141
|
+
>>> relation.columns[:2]
|
|
142
|
+
['InputOrder', 'InputID']
|
|
143
|
+
|
|
144
|
+
Notes:
|
|
145
|
+
This function opens the database read-only and never upgrades storage,
|
|
146
|
+
writes a WAL, or changes the `_seqevi` catalog.
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
return _scan_annotations(Path(path))
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _run_annotation_application(
|
|
153
|
+
*,
|
|
154
|
+
fasta: Path,
|
|
155
|
+
output: Path,
|
|
156
|
+
profile: str | None,
|
|
157
|
+
config: str | Path | None,
|
|
158
|
+
adapter: AdapterName | str | None,
|
|
159
|
+
executable: str | Path | None,
|
|
160
|
+
resource: str | Path | None,
|
|
161
|
+
store: str | Path | None,
|
|
162
|
+
threads: int | None,
|
|
163
|
+
timeout_seconds: float | None,
|
|
164
|
+
adapter_factory: Callable[[AdapterConfiguration], AnnotationAdapter] | None = None,
|
|
165
|
+
) -> AnnotationInvocation:
|
|
166
|
+
inputs = resolve_annotation_inputs(
|
|
167
|
+
profile=profile,
|
|
168
|
+
config=config,
|
|
169
|
+
adapter=adapter,
|
|
170
|
+
executable=executable,
|
|
171
|
+
resource=resource,
|
|
172
|
+
store=store,
|
|
173
|
+
threads=threads,
|
|
174
|
+
timeout_seconds=timeout_seconds,
|
|
175
|
+
)
|
|
176
|
+
if inputs.profile is not None and inputs.profile.version == 2:
|
|
177
|
+
managed = run_oci_annotation(
|
|
178
|
+
fasta=fasta.expanduser().resolve(),
|
|
179
|
+
output=output.expanduser().resolve(),
|
|
180
|
+
profile=inputs.profile,
|
|
181
|
+
store=inputs.store,
|
|
182
|
+
threads=inputs.threads,
|
|
183
|
+
timeout_seconds=inputs.timeout_seconds,
|
|
184
|
+
)
|
|
185
|
+
return AnnotationInvocation(
|
|
186
|
+
relation=_scan_annotations(managed.output),
|
|
187
|
+
summary=managed.summary,
|
|
188
|
+
adapter=managed.adapter,
|
|
189
|
+
result_schema_id=managed.result_schema_id,
|
|
190
|
+
)
|
|
191
|
+
if inputs.executable is None:
|
|
192
|
+
raise AnnotationError("local annotation requires an executable")
|
|
193
|
+
factory = create_adapter if adapter_factory is None else adapter_factory
|
|
194
|
+
configured_adapter = factory(
|
|
195
|
+
AdapterConfiguration(
|
|
196
|
+
name=inputs.adapter,
|
|
197
|
+
executable=inputs.executable,
|
|
198
|
+
database=inputs.resource,
|
|
199
|
+
environment=inputs.environment,
|
|
200
|
+
)
|
|
201
|
+
)
|
|
202
|
+
metadata = _result_metadata_template(configured_adapter)
|
|
203
|
+
with open_evidence_store(inputs.store) as evidence_store:
|
|
204
|
+
summary = run_annotation(
|
|
205
|
+
fasta_path=fasta.expanduser().resolve(),
|
|
206
|
+
output_dir=output.expanduser().resolve(),
|
|
207
|
+
adapter=configured_adapter,
|
|
208
|
+
store=evidence_store,
|
|
209
|
+
timeout_seconds=inputs.timeout_seconds,
|
|
210
|
+
threads=inputs.threads,
|
|
211
|
+
output_format="duckdb",
|
|
212
|
+
result_metadata=metadata,
|
|
213
|
+
)
|
|
214
|
+
return AnnotationInvocation(
|
|
215
|
+
relation=_scan_annotations(summary.output_dir),
|
|
216
|
+
summary=summary,
|
|
217
|
+
adapter=metadata["Adapter"],
|
|
218
|
+
result_schema_id=metadata["ResultSchemaID"],
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def resolve_annotation_inputs(
|
|
223
|
+
*,
|
|
224
|
+
profile: str | None,
|
|
225
|
+
config: str | Path | None,
|
|
226
|
+
adapter: AdapterName | str | None,
|
|
227
|
+
executable: str | Path | None,
|
|
228
|
+
resource: str | Path | None,
|
|
229
|
+
store: str | Path | None,
|
|
230
|
+
threads: int | None,
|
|
231
|
+
timeout_seconds: float | None,
|
|
232
|
+
) -> ResolvedAnnotationInputs:
|
|
233
|
+
"""Resolve one profile, config file, or complete explicit identity set."""
|
|
234
|
+
|
|
235
|
+
if profile is not None and config is not None:
|
|
236
|
+
raise AnnotationError("--profile and --config cannot be used together")
|
|
237
|
+
selected_profile: ExecutionProfile | None = None
|
|
238
|
+
if profile is not None:
|
|
239
|
+
selected_profile = load_named_profile(profile)
|
|
240
|
+
elif config is not None:
|
|
241
|
+
selected_profile = load_execution_profile(Path(config))
|
|
242
|
+
|
|
243
|
+
explicit_identity = (adapter, executable, resource)
|
|
244
|
+
if selected_profile is not None:
|
|
245
|
+
if any(value is not None for value in explicit_identity):
|
|
246
|
+
raise AnnotationError(
|
|
247
|
+
"--adapter, --executable and --resource cannot be combined with "
|
|
248
|
+
"--profile or --config"
|
|
249
|
+
)
|
|
250
|
+
return _resolved_from_profile(
|
|
251
|
+
selected_profile,
|
|
252
|
+
store=store,
|
|
253
|
+
threads=threads,
|
|
254
|
+
timeout_seconds=timeout_seconds,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
missing = [
|
|
258
|
+
option
|
|
259
|
+
for option, value in (
|
|
260
|
+
("--adapter", adapter),
|
|
261
|
+
("--executable", executable),
|
|
262
|
+
("--resource", resource),
|
|
263
|
+
)
|
|
264
|
+
if value is None
|
|
265
|
+
]
|
|
266
|
+
if missing:
|
|
267
|
+
raise AnnotationError(
|
|
268
|
+
"explicit mode requires "
|
|
269
|
+
+ ", ".join(missing)
|
|
270
|
+
+ "; alternatively select --profile or --config"
|
|
271
|
+
)
|
|
272
|
+
assert adapter is not None
|
|
273
|
+
assert executable is not None
|
|
274
|
+
assert resource is not None
|
|
275
|
+
return ResolvedAnnotationInputs(
|
|
276
|
+
adapter=_coerce_adapter_name(adapter),
|
|
277
|
+
executable=_resolve_executable(executable),
|
|
278
|
+
resource=_resolve_resource(resource),
|
|
279
|
+
store=store,
|
|
280
|
+
threads=threads if threads is not None else 1,
|
|
281
|
+
timeout_seconds=timeout_seconds,
|
|
282
|
+
profile=None,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _resolved_from_profile(
|
|
287
|
+
profile: ExecutionProfile,
|
|
288
|
+
*,
|
|
289
|
+
store: str | Path | None,
|
|
290
|
+
threads: int | None,
|
|
291
|
+
timeout_seconds: float | None,
|
|
292
|
+
) -> ResolvedAnnotationInputs:
|
|
293
|
+
if profile.version == 2:
|
|
294
|
+
return ResolvedAnnotationInputs(
|
|
295
|
+
adapter=profile.adapter,
|
|
296
|
+
executable=None,
|
|
297
|
+
resource=profile.resource,
|
|
298
|
+
store=store if store is not None else profile.store,
|
|
299
|
+
threads=(
|
|
300
|
+
threads
|
|
301
|
+
if threads is not None
|
|
302
|
+
else profile.threads
|
|
303
|
+
if profile.threads is not None
|
|
304
|
+
else 1
|
|
305
|
+
),
|
|
306
|
+
timeout_seconds=(
|
|
307
|
+
timeout_seconds
|
|
308
|
+
if timeout_seconds is not None
|
|
309
|
+
else profile.timeout_seconds
|
|
310
|
+
),
|
|
311
|
+
profile=profile,
|
|
312
|
+
)
|
|
313
|
+
if profile.executable is None:
|
|
314
|
+
raise AnnotationError("local execution profile has no executable")
|
|
315
|
+
return ResolvedAnnotationInputs(
|
|
316
|
+
adapter=profile.adapter,
|
|
317
|
+
executable=profile.executable,
|
|
318
|
+
resource=profile.resource,
|
|
319
|
+
store=store if store is not None else profile.store,
|
|
320
|
+
threads=(
|
|
321
|
+
threads
|
|
322
|
+
if threads is not None
|
|
323
|
+
else profile.threads
|
|
324
|
+
if profile.threads is not None
|
|
325
|
+
else 1
|
|
326
|
+
),
|
|
327
|
+
timeout_seconds=(
|
|
328
|
+
timeout_seconds if timeout_seconds is not None else profile.timeout_seconds
|
|
329
|
+
),
|
|
330
|
+
environment=profile.environment,
|
|
331
|
+
profile=profile,
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _coerce_adapter_name(value: AdapterName | str) -> AdapterName:
|
|
336
|
+
try:
|
|
337
|
+
return value if isinstance(value, AdapterName) else AdapterName(value)
|
|
338
|
+
except ValueError as error:
|
|
339
|
+
raise AnnotationError(f"unknown adapter: {value}") from error
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _resolve_executable(value: str | Path) -> Path:
|
|
343
|
+
value_text = str(value)
|
|
344
|
+
candidate = (
|
|
345
|
+
value_text.replace("~", str(Path.home()), 1)
|
|
346
|
+
if value_text.startswith("~")
|
|
347
|
+
else value_text
|
|
348
|
+
)
|
|
349
|
+
resolved = shutil.which(candidate)
|
|
350
|
+
if resolved is None:
|
|
351
|
+
raise AnnotationError(f"executable not found or not executable: {value}")
|
|
352
|
+
return Path(resolved).resolve()
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _resolve_resource(value: str | Path) -> Path:
|
|
356
|
+
resolved = Path(value).expanduser().resolve()
|
|
357
|
+
if not resolved.is_dir():
|
|
358
|
+
raise AnnotationError(f"annotation resource is not a directory: {resolved}")
|
|
359
|
+
return resolved
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _result_metadata_template(adapter: AnnotationAdapter) -> dict[str, str]:
|
|
363
|
+
name = adapter.contract.name
|
|
364
|
+
if name == "eggnog":
|
|
365
|
+
upstream_tool = "eggNOG-mapper"
|
|
366
|
+
upstream_version = str(getattr(adapter, "tool_version", "unknown"))
|
|
367
|
+
schema_id = "eggnog-mapper/2"
|
|
368
|
+
elif name == "interpro-pfam":
|
|
369
|
+
upstream_tool = "InterProScan"
|
|
370
|
+
upstream_version = str(getattr(adapter, "interproscan_version", "unknown"))
|
|
371
|
+
schema_id = "interproscan-pfam/5"
|
|
372
|
+
elif name == "dbcan-cazyme":
|
|
373
|
+
upstream_tool = "dbCAN"
|
|
374
|
+
upstream_version = str(getattr(adapter, "dbcan_version", "unknown"))
|
|
375
|
+
schema_id = "dbcan-cazyme/5"
|
|
376
|
+
else:
|
|
377
|
+
upstream_tool = name
|
|
378
|
+
upstream_version = "unknown"
|
|
379
|
+
schema_id = f"{name}/1"
|
|
380
|
+
return {
|
|
381
|
+
"ResultFormatVersion": RESULT_FORMAT_VERSION,
|
|
382
|
+
"ResultSchemaID": schema_id,
|
|
383
|
+
"SeqEviVersion": __version__,
|
|
384
|
+
"Adapter": adapter.contract.name,
|
|
385
|
+
"AdapterContractVersion": adapter.contract.version,
|
|
386
|
+
"UpstreamTool": upstream_tool,
|
|
387
|
+
"UpstreamToolVersion": upstream_version,
|
|
388
|
+
"ToolRuntimeDigest": adapter.contract.tool_runtime_digest,
|
|
389
|
+
"ResourceID": adapter.contract.resource_id,
|
|
390
|
+
}
|