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.
Files changed (42) hide show
  1. plasmid_oracle/__init__.py +96 -0
  2. plasmid_oracle/__main__.py +3 -0
  3. plasmid_oracle/_immutability.py +22 -0
  4. plasmid_oracle/api.py +109 -0
  5. plasmid_oracle/cli.py +214 -0
  6. plasmid_oracle/databases/__init__.py +3 -0
  7. plasmid_oracle/databases/setup.py +97 -0
  8. plasmid_oracle/errors.py +49 -0
  9. plasmid_oracle/execution/__init__.py +15 -0
  10. plasmid_oracle/execution/process.py +139 -0
  11. plasmid_oracle/model/__init__.py +43 -0
  12. plasmid_oracle/model/annotation.py +77 -0
  13. plasmid_oracle/model/characterization.py +67 -0
  14. plasmid_oracle/model/location.py +115 -0
  15. plasmid_oracle/model/manifest.py +50 -0
  16. plasmid_oracle/model/plasmid.py +116 -0
  17. plasmid_oracle/model/resolution.py +74 -0
  18. plasmid_oracle/model/sequence.py +152 -0
  19. plasmid_oracle/pipeline/__init__.py +24 -0
  20. plasmid_oracle/pipeline/cache.py +166 -0
  21. plasmid_oracle/pipeline/diagnostics.py +76 -0
  22. plasmid_oracle/pipeline/provider.py +63 -0
  23. plasmid_oracle/pipeline/runner.py +301 -0
  24. plasmid_oracle/providers/__init__.py +17 -0
  25. plasmid_oracle/providers/_external.py +45 -0
  26. plasmid_oracle/providers/_parsing.py +57 -0
  27. plasmid_oracle/providers/amrfinder.py +292 -0
  28. plasmid_oracle/providers/builtin.py +49 -0
  29. plasmid_oracle/providers/mob_typer.py +300 -0
  30. plasmid_oracle/providers/plannotate.py +247 -0
  31. plasmid_oracle/providers/pyrodigal.py +214 -0
  32. plasmid_oracle/py.typed +1 -0
  33. plasmid_oracle/reporting.py +118 -0
  34. plasmid_oracle/resolution.py +324 -0
  35. plasmid_oracle/serialization/__init__.py +3 -0
  36. plasmid_oracle/serialization/json.py +621 -0
  37. plasmid_oracle-0.2.0a0.dist-info/METADATA +337 -0
  38. plasmid_oracle-0.2.0a0.dist-info/RECORD +42 -0
  39. plasmid_oracle-0.2.0a0.dist-info/WHEEL +5 -0
  40. plasmid_oracle-0.2.0a0.dist-info/entry_points.txt +2 -0
  41. plasmid_oracle-0.2.0a0.dist-info/licenses/LICENSE +674 -0
  42. 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,3 @@
1
+ from plasmid_oracle.cli import main
2
+
3
+ raise SystemExit(main())
@@ -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,3 @@
1
+ from plasmid_oracle.databases.setup import DatabaseSetupResult, setup
2
+
3
+ __all__ = ["DatabaseSetupResult", "setup"]
@@ -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")
@@ -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
+ ]