plasmid-oracle 0.2.0a0__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.
- plasmid_oracle/__init__.py +96 -0
- plasmid_oracle/__main__.py +3 -0
- plasmid_oracle/_immutability.py +22 -0
- plasmid_oracle/api.py +109 -0
- plasmid_oracle/cli.py +214 -0
- plasmid_oracle/databases/__init__.py +3 -0
- plasmid_oracle/databases/setup.py +97 -0
- plasmid_oracle/errors.py +49 -0
- plasmid_oracle/execution/__init__.py +15 -0
- plasmid_oracle/execution/process.py +139 -0
- plasmid_oracle/model/__init__.py +43 -0
- plasmid_oracle/model/annotation.py +77 -0
- plasmid_oracle/model/characterization.py +67 -0
- plasmid_oracle/model/location.py +115 -0
- plasmid_oracle/model/manifest.py +50 -0
- plasmid_oracle/model/plasmid.py +116 -0
- plasmid_oracle/model/resolution.py +74 -0
- plasmid_oracle/model/sequence.py +152 -0
- plasmid_oracle/pipeline/__init__.py +24 -0
- plasmid_oracle/pipeline/cache.py +166 -0
- plasmid_oracle/pipeline/diagnostics.py +76 -0
- plasmid_oracle/pipeline/provider.py +63 -0
- plasmid_oracle/pipeline/runner.py +301 -0
- plasmid_oracle/providers/__init__.py +17 -0
- plasmid_oracle/providers/_external.py +45 -0
- plasmid_oracle/providers/_parsing.py +57 -0
- plasmid_oracle/providers/amrfinder.py +292 -0
- plasmid_oracle/providers/builtin.py +49 -0
- plasmid_oracle/providers/mob_typer.py +300 -0
- plasmid_oracle/providers/plannotate.py +247 -0
- plasmid_oracle/providers/pyrodigal.py +214 -0
- plasmid_oracle/py.typed +1 -0
- plasmid_oracle/reporting.py +118 -0
- plasmid_oracle/resolution.py +324 -0
- plasmid_oracle/serialization/__init__.py +3 -0
- plasmid_oracle/serialization/json.py +621 -0
- plasmid_oracle-0.2.0a0.dist-info/METADATA +337 -0
- plasmid_oracle-0.2.0a0.dist-info/RECORD +42 -0
- plasmid_oracle-0.2.0a0.dist-info/WHEEL +5 -0
- plasmid_oracle-0.2.0a0.dist-info/entry_points.txt +2 -0
- plasmid_oracle-0.2.0a0.dist-info/licenses/LICENSE +674 -0
- plasmid_oracle-0.2.0a0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Evidence-first plasmid annotation and characterization."""
|
|
2
|
+
|
|
3
|
+
from plasmid_oracle.api import annotate, annotate_async, plasmid
|
|
4
|
+
from plasmid_oracle.databases import DatabaseSetupResult, setup
|
|
5
|
+
from plasmid_oracle.errors import (
|
|
6
|
+
InvalidLocationError,
|
|
7
|
+
InvalidProviderResultError,
|
|
8
|
+
InvalidSequenceError,
|
|
9
|
+
InvalidSerializedPlasmidError,
|
|
10
|
+
PlasmidOracleError,
|
|
11
|
+
ProviderExecutionError,
|
|
12
|
+
ProviderUnavailableError,
|
|
13
|
+
)
|
|
14
|
+
from plasmid_oracle.model import (
|
|
15
|
+
AnalysisManifest,
|
|
16
|
+
Annotation,
|
|
17
|
+
AnnotationSource,
|
|
18
|
+
Characterization,
|
|
19
|
+
CharacterizationCall,
|
|
20
|
+
EvidenceMetrics,
|
|
21
|
+
Integrity,
|
|
22
|
+
Location,
|
|
23
|
+
Plasmid,
|
|
24
|
+
ProviderRun,
|
|
25
|
+
ProviderStatus,
|
|
26
|
+
QualityFlag,
|
|
27
|
+
ResolutionConflict,
|
|
28
|
+
ResolutionStatus,
|
|
29
|
+
ResolvedAnnotation,
|
|
30
|
+
SequenceInfo,
|
|
31
|
+
SequenceWarning,
|
|
32
|
+
Span,
|
|
33
|
+
Strand,
|
|
34
|
+
Topology,
|
|
35
|
+
)
|
|
36
|
+
from plasmid_oracle.pipeline import (
|
|
37
|
+
AnnotationProvider,
|
|
38
|
+
DoctorReport,
|
|
39
|
+
ProviderContext,
|
|
40
|
+
ProviderDiagnostic,
|
|
41
|
+
ProviderResult,
|
|
42
|
+
ProviderSpec,
|
|
43
|
+
doctor,
|
|
44
|
+
)
|
|
45
|
+
from plasmid_oracle.resolution import resolve_annotations
|
|
46
|
+
from plasmid_oracle.serialization import from_dict, from_json, to_dict, to_json
|
|
47
|
+
|
|
48
|
+
__version__ = "0.2.0a0"
|
|
49
|
+
|
|
50
|
+
__all__ = [
|
|
51
|
+
"AnalysisManifest",
|
|
52
|
+
"Annotation",
|
|
53
|
+
"AnnotationProvider",
|
|
54
|
+
"AnnotationSource",
|
|
55
|
+
"Characterization",
|
|
56
|
+
"CharacterizationCall",
|
|
57
|
+
"DatabaseSetupResult",
|
|
58
|
+
"DoctorReport",
|
|
59
|
+
"EvidenceMetrics",
|
|
60
|
+
"Integrity",
|
|
61
|
+
"InvalidLocationError",
|
|
62
|
+
"InvalidProviderResultError",
|
|
63
|
+
"InvalidSerializedPlasmidError",
|
|
64
|
+
"InvalidSequenceError",
|
|
65
|
+
"Location",
|
|
66
|
+
"Plasmid",
|
|
67
|
+
"PlasmidOracleError",
|
|
68
|
+
"ProviderContext",
|
|
69
|
+
"ProviderDiagnostic",
|
|
70
|
+
"ProviderExecutionError",
|
|
71
|
+
"ProviderUnavailableError",
|
|
72
|
+
"ProviderResult",
|
|
73
|
+
"ProviderRun",
|
|
74
|
+
"ProviderSpec",
|
|
75
|
+
"ProviderStatus",
|
|
76
|
+
"QualityFlag",
|
|
77
|
+
"ResolutionConflict",
|
|
78
|
+
"ResolutionStatus",
|
|
79
|
+
"ResolvedAnnotation",
|
|
80
|
+
"SequenceInfo",
|
|
81
|
+
"SequenceWarning",
|
|
82
|
+
"Span",
|
|
83
|
+
"Strand",
|
|
84
|
+
"Topology",
|
|
85
|
+
"__version__",
|
|
86
|
+
"annotate",
|
|
87
|
+
"annotate_async",
|
|
88
|
+
"doctor",
|
|
89
|
+
"from_dict",
|
|
90
|
+
"from_json",
|
|
91
|
+
"plasmid",
|
|
92
|
+
"resolve_annotations",
|
|
93
|
+
"setup",
|
|
94
|
+
"to_dict",
|
|
95
|
+
"to_json",
|
|
96
|
+
]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from types import MappingProxyType
|
|
5
|
+
from typing import Any, cast
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def freeze_value(value: Any) -> Any:
|
|
9
|
+
"""Recursively freeze common mutable container values."""
|
|
10
|
+
if isinstance(value, Mapping):
|
|
11
|
+
return MappingProxyType({key: freeze_value(item) for key, item in value.items()})
|
|
12
|
+
if isinstance(value, list | tuple):
|
|
13
|
+
return tuple(freeze_value(item) for item in value)
|
|
14
|
+
if isinstance(value, set | frozenset):
|
|
15
|
+
return frozenset(freeze_value(item) for item in value)
|
|
16
|
+
return value
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def freeze_mapping(value: Mapping[str, object] | None) -> Mapping[str, object]:
|
|
20
|
+
if value is None:
|
|
21
|
+
return MappingProxyType({})
|
|
22
|
+
return cast(Mapping[str, object], freeze_value(value))
|
plasmid_oracle/api.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from collections.abc import Iterable, Mapping
|
|
5
|
+
from functools import partial
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from plasmid_oracle.model import (
|
|
9
|
+
AnalysisManifest,
|
|
10
|
+
Characterization,
|
|
11
|
+
Plasmid,
|
|
12
|
+
SequenceInfo,
|
|
13
|
+
Topology,
|
|
14
|
+
)
|
|
15
|
+
from plasmid_oracle.pipeline import (
|
|
16
|
+
PIPELINE_VERSION,
|
|
17
|
+
AnnotationProvider,
|
|
18
|
+
run_pipeline,
|
|
19
|
+
)
|
|
20
|
+
from plasmid_oracle.providers.builtin import providers_for_mode
|
|
21
|
+
|
|
22
|
+
_VALID_MODES = frozenset({"minimal", "fast", "standard", "deep"})
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def plasmid(
|
|
26
|
+
*,
|
|
27
|
+
seq: str,
|
|
28
|
+
topology: Topology | str = Topology.CIRCULAR,
|
|
29
|
+
source_metadata: Mapping[str, object] | None = None,
|
|
30
|
+
) -> Plasmid:
|
|
31
|
+
return Plasmid(
|
|
32
|
+
sequence=SequenceInfo.from_raw(seq, topology=topology),
|
|
33
|
+
evidence=(),
|
|
34
|
+
annotations=(),
|
|
35
|
+
characterization=Characterization(),
|
|
36
|
+
source_metadata=source_metadata or {},
|
|
37
|
+
analysis=AnalysisManifest(
|
|
38
|
+
pipeline_version=PIPELINE_VERSION,
|
|
39
|
+
mode="none",
|
|
40
|
+
),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def annotate(
|
|
45
|
+
*,
|
|
46
|
+
seq: str,
|
|
47
|
+
topology: Topology | str = Topology.CIRCULAR,
|
|
48
|
+
mode: str = "minimal",
|
|
49
|
+
providers: Iterable[AnnotationProvider] | None = None,
|
|
50
|
+
source_metadata: Mapping[str, object] | None = None,
|
|
51
|
+
strict: bool = True,
|
|
52
|
+
threads: int = 1,
|
|
53
|
+
timeout_seconds: float = 600.0,
|
|
54
|
+
cache: bool = False,
|
|
55
|
+
cache_dir: Path | None = None,
|
|
56
|
+
provider_workers: int = 1,
|
|
57
|
+
) -> Plasmid:
|
|
58
|
+
if mode not in _VALID_MODES:
|
|
59
|
+
choices = ", ".join(sorted(_VALID_MODES))
|
|
60
|
+
raise ValueError(f"Unsupported annotation mode {mode!r}; expected one of: {choices}")
|
|
61
|
+
|
|
62
|
+
normalized = plasmid(
|
|
63
|
+
seq=seq,
|
|
64
|
+
topology=topology,
|
|
65
|
+
source_metadata=source_metadata,
|
|
66
|
+
)
|
|
67
|
+
selected_providers = providers_for_mode(mode) if providers is None else tuple(providers)
|
|
68
|
+
return run_pipeline(
|
|
69
|
+
normalized,
|
|
70
|
+
mode=mode,
|
|
71
|
+
providers=selected_providers,
|
|
72
|
+
strict=strict,
|
|
73
|
+
threads=threads,
|
|
74
|
+
timeout_seconds=timeout_seconds,
|
|
75
|
+
cache=cache,
|
|
76
|
+
cache_dir=cache_dir,
|
|
77
|
+
provider_workers=provider_workers,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
async def annotate_async(
|
|
82
|
+
*,
|
|
83
|
+
seq: str,
|
|
84
|
+
topology: Topology | str = Topology.CIRCULAR,
|
|
85
|
+
mode: str = "minimal",
|
|
86
|
+
providers: Iterable[AnnotationProvider] | None = None,
|
|
87
|
+
source_metadata: Mapping[str, object] | None = None,
|
|
88
|
+
strict: bool = True,
|
|
89
|
+
threads: int = 1,
|
|
90
|
+
timeout_seconds: float = 600.0,
|
|
91
|
+
cache: bool = False,
|
|
92
|
+
cache_dir: Path | None = None,
|
|
93
|
+
provider_workers: int = 1,
|
|
94
|
+
) -> Plasmid:
|
|
95
|
+
operation = partial(
|
|
96
|
+
annotate,
|
|
97
|
+
seq=seq,
|
|
98
|
+
topology=topology,
|
|
99
|
+
mode=mode,
|
|
100
|
+
providers=providers,
|
|
101
|
+
source_metadata=source_metadata,
|
|
102
|
+
strict=strict,
|
|
103
|
+
threads=threads,
|
|
104
|
+
timeout_seconds=timeout_seconds,
|
|
105
|
+
cache=cache,
|
|
106
|
+
cache_dir=cache_dir,
|
|
107
|
+
provider_workers=provider_workers,
|
|
108
|
+
)
|
|
109
|
+
return await asyncio.to_thread(operation)
|
plasmid_oracle/cli.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import tempfile
|
|
8
|
+
from collections.abc import Sequence
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from plasmid_oracle import __version__
|
|
12
|
+
from plasmid_oracle.api import annotate
|
|
13
|
+
from plasmid_oracle.databases import setup as setup_database
|
|
14
|
+
from plasmid_oracle.errors import PlasmidOracleError
|
|
15
|
+
from plasmid_oracle.pipeline import DoctorReport, doctor
|
|
16
|
+
from plasmid_oracle.reporting import render_text
|
|
17
|
+
from plasmid_oracle.serialization import to_json
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _single_fasta(path: Path) -> tuple[str, str]:
|
|
21
|
+
identifier: str | None = None
|
|
22
|
+
sequence_lines: list[str] = []
|
|
23
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
24
|
+
if line.startswith(">"):
|
|
25
|
+
if identifier is not None:
|
|
26
|
+
raise ValueError("FASTA input must contain exactly one sequence")
|
|
27
|
+
identifier = line[1:].split(maxsplit=1)[0] or path.stem
|
|
28
|
+
elif line.strip():
|
|
29
|
+
if identifier is None:
|
|
30
|
+
raise ValueError("FASTA sequence data appeared before its header")
|
|
31
|
+
sequence_lines.append(line.strip())
|
|
32
|
+
if identifier is None or not sequence_lines:
|
|
33
|
+
raise ValueError("FASTA input does not contain a sequence")
|
|
34
|
+
return identifier, "".join(sequence_lines)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _doctor_payload(report: DoctorReport) -> dict[str, object]:
|
|
38
|
+
return {
|
|
39
|
+
"mode": report.mode,
|
|
40
|
+
"ready": report.ready,
|
|
41
|
+
"providers": [
|
|
42
|
+
{
|
|
43
|
+
"name": provider.name,
|
|
44
|
+
"available": provider.available,
|
|
45
|
+
"provider_version": provider.provider_version,
|
|
46
|
+
"tool_version": provider.tool_version,
|
|
47
|
+
"database_versions": dict(provider.database_versions),
|
|
48
|
+
"issues": list(provider.issues),
|
|
49
|
+
}
|
|
50
|
+
for provider in report.providers
|
|
51
|
+
],
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _parser() -> argparse.ArgumentParser:
|
|
56
|
+
parser = argparse.ArgumentParser(
|
|
57
|
+
prog="plasmid-oracle",
|
|
58
|
+
description="Evidence-first plasmid annotation and characterization",
|
|
59
|
+
)
|
|
60
|
+
parser.add_argument("--version", action="version", version=__version__)
|
|
61
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
62
|
+
|
|
63
|
+
annotate_parser = commands.add_parser("annotate", help="Annotate one plasmid")
|
|
64
|
+
source = annotate_parser.add_mutually_exclusive_group(required=True)
|
|
65
|
+
source.add_argument("--sequence", help="Raw DNA sequence")
|
|
66
|
+
source.add_argument("--fasta", type=Path, help="Single-record FASTA file")
|
|
67
|
+
annotate_parser.add_argument(
|
|
68
|
+
"--topology",
|
|
69
|
+
choices=("circular", "linear"),
|
|
70
|
+
default="circular",
|
|
71
|
+
)
|
|
72
|
+
annotate_parser.add_argument(
|
|
73
|
+
"--mode",
|
|
74
|
+
choices=("minimal", "fast", "standard", "deep"),
|
|
75
|
+
default="minimal",
|
|
76
|
+
)
|
|
77
|
+
annotate_parser.add_argument("--threads", type=int, default=1)
|
|
78
|
+
annotate_parser.add_argument(
|
|
79
|
+
"--provider-workers",
|
|
80
|
+
type=int,
|
|
81
|
+
default=1,
|
|
82
|
+
help="Maximum providers to run concurrently within the total thread budget",
|
|
83
|
+
)
|
|
84
|
+
annotate_parser.add_argument("--timeout", type=float, default=600.0)
|
|
85
|
+
annotate_parser.add_argument(
|
|
86
|
+
"--cache",
|
|
87
|
+
action="store_true",
|
|
88
|
+
help="Reuse results with matching sequence, tool, database, and parameter identity",
|
|
89
|
+
)
|
|
90
|
+
annotate_parser.add_argument("--cache-dir", type=Path)
|
|
91
|
+
annotate_parser.add_argument(
|
|
92
|
+
"--tolerant",
|
|
93
|
+
action="store_true",
|
|
94
|
+
help="Return partial results and record unavailable or failed providers",
|
|
95
|
+
)
|
|
96
|
+
annotate_parser.add_argument("--output", type=Path)
|
|
97
|
+
annotate_parser.add_argument(
|
|
98
|
+
"--format",
|
|
99
|
+
choices=("text", "json"),
|
|
100
|
+
help="Output format; defaults to text on stdout and JSON for files",
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
doctor_parser = commands.add_parser("doctor", help="Check provider readiness")
|
|
104
|
+
doctor_parser.add_argument(
|
|
105
|
+
"--mode",
|
|
106
|
+
choices=("minimal", "fast", "standard", "deep"),
|
|
107
|
+
default="standard",
|
|
108
|
+
)
|
|
109
|
+
doctor_parser.add_argument("--json", action="store_true")
|
|
110
|
+
doctor_parser.add_argument("--threads", type=int, default=1)
|
|
111
|
+
|
|
112
|
+
setup_parser = commands.add_parser(
|
|
113
|
+
"setup",
|
|
114
|
+
help="Install or update an external provider database",
|
|
115
|
+
)
|
|
116
|
+
setup_parser.add_argument(
|
|
117
|
+
"component",
|
|
118
|
+
choices=("plannotate", "amrfinderplus", "mob-suite"),
|
|
119
|
+
)
|
|
120
|
+
setup_parser.add_argument("--force", action="store_true")
|
|
121
|
+
setup_parser.add_argument(
|
|
122
|
+
"--mob-database",
|
|
123
|
+
type=Path,
|
|
124
|
+
help="MOB-suite database directory",
|
|
125
|
+
)
|
|
126
|
+
setup_parser.add_argument("--timeout", type=float, default=7200.0)
|
|
127
|
+
setup_parser.add_argument("--json", action="store_true")
|
|
128
|
+
return parser
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
132
|
+
arguments = _parser().parse_args(argv)
|
|
133
|
+
try:
|
|
134
|
+
if arguments.command == "doctor":
|
|
135
|
+
report = doctor(mode=arguments.mode, threads=arguments.threads)
|
|
136
|
+
payload = _doctor_payload(report)
|
|
137
|
+
if arguments.json:
|
|
138
|
+
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
139
|
+
else:
|
|
140
|
+
print(f"Mode: {report.mode} ({'ready' if report.ready else 'not ready'})")
|
|
141
|
+
for provider in report.providers:
|
|
142
|
+
state = "ready" if provider.available else "unavailable"
|
|
143
|
+
print(f"{provider.name}: {state}")
|
|
144
|
+
for issue in provider.issues:
|
|
145
|
+
print(f" {issue}")
|
|
146
|
+
return 0 if report.ready else 1
|
|
147
|
+
|
|
148
|
+
if arguments.command == "setup":
|
|
149
|
+
result = setup_database(
|
|
150
|
+
arguments.component,
|
|
151
|
+
force=arguments.force,
|
|
152
|
+
mob_database_dir=arguments.mob_database,
|
|
153
|
+
timeout_seconds=arguments.timeout,
|
|
154
|
+
)
|
|
155
|
+
payload = {
|
|
156
|
+
"component": result.component,
|
|
157
|
+
"path": str(result.path) if result.path is not None else None,
|
|
158
|
+
"detail": result.detail,
|
|
159
|
+
}
|
|
160
|
+
if arguments.json:
|
|
161
|
+
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
162
|
+
else:
|
|
163
|
+
print(result.detail)
|
|
164
|
+
if result.path is not None:
|
|
165
|
+
print(f"Path: {result.path}")
|
|
166
|
+
return 0
|
|
167
|
+
|
|
168
|
+
metadata: dict[str, object] = {}
|
|
169
|
+
sequence = arguments.sequence
|
|
170
|
+
if arguments.fasta is not None:
|
|
171
|
+
identifier, sequence = _single_fasta(arguments.fasta)
|
|
172
|
+
metadata["id"] = identifier
|
|
173
|
+
metadata["source"] = str(arguments.fasta)
|
|
174
|
+
assert sequence is not None
|
|
175
|
+
plasmid = annotate(
|
|
176
|
+
seq=sequence,
|
|
177
|
+
topology=arguments.topology,
|
|
178
|
+
mode=arguments.mode,
|
|
179
|
+
source_metadata=metadata,
|
|
180
|
+
strict=not arguments.tolerant,
|
|
181
|
+
threads=arguments.threads,
|
|
182
|
+
timeout_seconds=arguments.timeout,
|
|
183
|
+
cache=arguments.cache,
|
|
184
|
+
cache_dir=arguments.cache_dir,
|
|
185
|
+
provider_workers=arguments.provider_workers,
|
|
186
|
+
)
|
|
187
|
+
output_format = arguments.format or ("json" if arguments.output is not None else "text")
|
|
188
|
+
output = to_json(plasmid) if output_format == "json" else render_text(plasmid)
|
|
189
|
+
if arguments.output is None:
|
|
190
|
+
print(output)
|
|
191
|
+
else:
|
|
192
|
+
arguments.output.parent.mkdir(parents=True, exist_ok=True)
|
|
193
|
+
descriptor, temporary_name = tempfile.mkstemp(
|
|
194
|
+
prefix=f".{arguments.output.name}.",
|
|
195
|
+
dir=arguments.output.parent,
|
|
196
|
+
text=True,
|
|
197
|
+
)
|
|
198
|
+
try:
|
|
199
|
+
with os.fdopen(descriptor, "w", encoding="utf-8") as temporary:
|
|
200
|
+
temporary.write(output + "\n")
|
|
201
|
+
temporary.flush()
|
|
202
|
+
os.fsync(temporary.fileno())
|
|
203
|
+
os.replace(temporary_name, arguments.output)
|
|
204
|
+
except BaseException:
|
|
205
|
+
Path(temporary_name).unlink(missing_ok=True)
|
|
206
|
+
raise
|
|
207
|
+
return 0
|
|
208
|
+
except (PlasmidOracleError, ValueError, OSError) as error:
|
|
209
|
+
print(f"plasmid-oracle: {error}", file=sys.stderr)
|
|
210
|
+
return 2
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
if __name__ == "__main__":
|
|
214
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
import importlib.util
|
|
5
|
+
import os
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from plasmid_oracle.errors import ProviderUnavailableError
|
|
10
|
+
from plasmid_oracle.execution import ProcessRunner, SubprocessRunner
|
|
11
|
+
from plasmid_oracle.providers._external import (
|
|
12
|
+
ExecutableResolver,
|
|
13
|
+
default_executable_resolver,
|
|
14
|
+
require_executable,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True, slots=True)
|
|
19
|
+
class DatabaseSetupResult:
|
|
20
|
+
component: str
|
|
21
|
+
path: Path | None
|
|
22
|
+
detail: str
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _setup_plannotate(*, force: bool) -> DatabaseSetupResult:
|
|
26
|
+
if importlib.util.find_spec("plannotate") is None:
|
|
27
|
+
raise ProviderUnavailableError(
|
|
28
|
+
"pLannotate is not installed; install plasmid-oracle[plannotate]"
|
|
29
|
+
)
|
|
30
|
+
resources = importlib.import_module("plannotate.resources")
|
|
31
|
+
auto_download = os.environ.get("PLANNOTATE_AUTO_DOWNLOAD")
|
|
32
|
+
os.environ["PLANNOTATE_AUTO_DOWNLOAD"] = "1"
|
|
33
|
+
try:
|
|
34
|
+
installed = Path(resources.download_db(force=force))
|
|
35
|
+
finally:
|
|
36
|
+
if auto_download is None:
|
|
37
|
+
os.environ.pop("PLANNOTATE_AUTO_DOWNLOAD", None)
|
|
38
|
+
else:
|
|
39
|
+
os.environ["PLANNOTATE_AUTO_DOWNLOAD"] = auto_download
|
|
40
|
+
return DatabaseSetupResult(
|
|
41
|
+
component="plannotate",
|
|
42
|
+
path=installed,
|
|
43
|
+
detail="pLannotate database installed and checksum-verified by pLannotate",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def setup(
|
|
48
|
+
component: str,
|
|
49
|
+
*,
|
|
50
|
+
force: bool = False,
|
|
51
|
+
mob_database_dir: Path | None = None,
|
|
52
|
+
runner: ProcessRunner | None = None,
|
|
53
|
+
executable_resolver: ExecutableResolver = default_executable_resolver,
|
|
54
|
+
timeout_seconds: float = 7200,
|
|
55
|
+
) -> DatabaseSetupResult:
|
|
56
|
+
normalized = component.strip().lower().replace("-", "_")
|
|
57
|
+
process_runner = runner or SubprocessRunner(default_timeout_seconds=timeout_seconds)
|
|
58
|
+
|
|
59
|
+
if normalized == "plannotate":
|
|
60
|
+
return _setup_plannotate(force=force)
|
|
61
|
+
|
|
62
|
+
if normalized in {"amrfinder", "amrfinderplus"}:
|
|
63
|
+
executable = require_executable("amrfinder", executable_resolver)
|
|
64
|
+
command = [executable, "--force_update" if force else "--update"]
|
|
65
|
+
process_runner.run(command, timeout_seconds=timeout_seconds)
|
|
66
|
+
return DatabaseSetupResult(
|
|
67
|
+
component="amrfinderplus",
|
|
68
|
+
path=None,
|
|
69
|
+
detail="AMRFinderPlus database updated in its upstream default directory",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
if normalized in {"mob", "mob_suite"}:
|
|
73
|
+
database_dir = mob_database_dir
|
|
74
|
+
if database_dir is None:
|
|
75
|
+
configured = os.environ.get("PLASMID_ORACLE_MOB_DATABASE")
|
|
76
|
+
database_dir = Path(configured) if configured else None
|
|
77
|
+
if database_dir is None:
|
|
78
|
+
raise ValueError(
|
|
79
|
+
"MOB-suite setup requires mob_database_dir or PLASMID_ORACLE_MOB_DATABASE"
|
|
80
|
+
)
|
|
81
|
+
database_dir = database_dir.expanduser()
|
|
82
|
+
executable = require_executable("mob_init", executable_resolver)
|
|
83
|
+
process_runner.run(
|
|
84
|
+
(
|
|
85
|
+
executable,
|
|
86
|
+
"--database_directory",
|
|
87
|
+
os.fspath(database_dir),
|
|
88
|
+
),
|
|
89
|
+
timeout_seconds=timeout_seconds,
|
|
90
|
+
)
|
|
91
|
+
return DatabaseSetupResult(
|
|
92
|
+
component="mob_suite",
|
|
93
|
+
path=database_dir,
|
|
94
|
+
detail="MOB-suite database initialized",
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
raise ValueError("Unknown database component; expected plannotate, amrfinderplus, or mob_suite")
|
plasmid_oracle/errors.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from plasmid_oracle.model import ProviderRun
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PlasmidOracleError(Exception):
|
|
11
|
+
"""Base exception for Plasmid Oracle."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class InvalidSequenceError(PlasmidOracleError, ValueError):
|
|
15
|
+
"""Raised when DNA cannot be normalized without changing its meaning."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class InvalidLocationError(PlasmidOracleError, ValueError):
|
|
19
|
+
"""Raised when sequence coordinates do not form a valid location."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class InvalidProviderResultError(PlasmidOracleError, ValueError):
|
|
23
|
+
"""Raised when a provider returns evidence outside the canonical contract."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class InvalidSerializedPlasmidError(PlasmidOracleError, ValueError):
|
|
27
|
+
"""Raised when serialized data violates the versioned plasmid schema."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ProviderUnavailableError(PlasmidOracleError, RuntimeError):
|
|
31
|
+
"""Raised when a provider dependency or database is not installed."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ProviderExecutionError(PlasmidOracleError):
|
|
35
|
+
"""Raised when a requested provider does not complete."""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
provider_name: str,
|
|
40
|
+
provider_runs: Sequence[ProviderRun],
|
|
41
|
+
) -> None:
|
|
42
|
+
self.provider_name = provider_name
|
|
43
|
+
self.provider_runs = tuple(provider_runs)
|
|
44
|
+
failed_run = next(
|
|
45
|
+
(run for run in reversed(self.provider_runs) if run.name == provider_name),
|
|
46
|
+
self.provider_runs[-1],
|
|
47
|
+
)
|
|
48
|
+
detail = failed_run.error or "unknown provider error"
|
|
49
|
+
super().__init__(f"Provider {provider_name!r} failed: {detail}")
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from plasmid_oracle.execution.process import (
|
|
2
|
+
CommandFailedError,
|
|
3
|
+
CommandTimeoutError,
|
|
4
|
+
ProcessResult,
|
|
5
|
+
ProcessRunner,
|
|
6
|
+
SubprocessRunner,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"CommandFailedError",
|
|
11
|
+
"CommandTimeoutError",
|
|
12
|
+
"ProcessResult",
|
|
13
|
+
"ProcessRunner",
|
|
14
|
+
"SubprocessRunner",
|
|
15
|
+
]
|