virtool-workflow 6.0.0a7__tar.gz → 6.0.0a9__tar.gz
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.
- {virtool_workflow-6.0.0a7 → virtool_workflow-6.0.0a9}/PKG-INFO +2 -2
- {virtool_workflow-6.0.0a7 → virtool_workflow-6.0.0a9}/pyproject.toml +3 -11
- {virtool_workflow-6.0.0a7 → virtool_workflow-6.0.0a9}/virtool_workflow/analysis/fastqc.py +15 -18
- {virtool_workflow-6.0.0a7 → virtool_workflow-6.0.0a9}/virtool_workflow/analysis/skewer.py +14 -21
- {virtool_workflow-6.0.0a7 → virtool_workflow-6.0.0a9}/virtool_workflow/analysis/trimming.py +3 -6
- {virtool_workflow-6.0.0a7 → virtool_workflow-6.0.0a9}/virtool_workflow/api/acquire.py +4 -6
- {virtool_workflow-6.0.0a7 → virtool_workflow-6.0.0a9}/virtool_workflow/api/client.py +11 -15
- {virtool_workflow-6.0.0a7 → virtool_workflow-6.0.0a9}/virtool_workflow/api/utils.py +9 -12
- {virtool_workflow-6.0.0a7 → virtool_workflow-6.0.0a9}/virtool_workflow/data/analyses.py +4 -9
- {virtool_workflow-6.0.0a7 → virtool_workflow-6.0.0a9}/virtool_workflow/data/hmms.py +5 -10
- {virtool_workflow-6.0.0a7 → virtool_workflow-6.0.0a9}/virtool_workflow/data/indexes.py +12 -21
- {virtool_workflow-6.0.0a7 → virtool_workflow-6.0.0a9}/virtool_workflow/data/jobs.py +2 -2
- {virtool_workflow-6.0.0a7 → virtool_workflow-6.0.0a9}/virtool_workflow/data/ml.py +16 -8
- {virtool_workflow-6.0.0a7 → virtool_workflow-6.0.0a9}/virtool_workflow/data/samples.py +8 -12
- {virtool_workflow-6.0.0a7 → virtool_workflow-6.0.0a9}/virtool_workflow/data/subtractions.py +11 -17
- {virtool_workflow-6.0.0a7 → virtool_workflow-6.0.0a9}/virtool_workflow/data/uploads.py +2 -3
- {virtool_workflow-6.0.0a7 → virtool_workflow-6.0.0a9}/virtool_workflow/decorators.py +2 -5
- {virtool_workflow-6.0.0a7 → virtool_workflow-6.0.0a9}/virtool_workflow/errors.py +1 -9
- {virtool_workflow-6.0.0a7 → virtool_workflow-6.0.0a9}/virtool_workflow/hooks.py +2 -4
- {virtool_workflow-6.0.0a7 → virtool_workflow-6.0.0a9}/virtool_workflow/pytest_plugin/data.py +8 -4
- {virtool_workflow-6.0.0a7 → virtool_workflow-6.0.0a9}/virtool_workflow/pytest_plugin/subprocess.py +1 -1
- {virtool_workflow-6.0.0a7 → virtool_workflow-6.0.0a9}/virtool_workflow/runtime/discover.py +5 -9
- {virtool_workflow-6.0.0a7 → virtool_workflow-6.0.0a9}/virtool_workflow/runtime/hook.py +4 -7
- {virtool_workflow-6.0.0a7 → virtool_workflow-6.0.0a9}/virtool_workflow/runtime/ping.py +1 -2
- {virtool_workflow-6.0.0a7 → virtool_workflow-6.0.0a9}/virtool_workflow/runtime/run.py +13 -16
- {virtool_workflow-6.0.0a7 → virtool_workflow-6.0.0a9}/virtool_workflow/runtime/run_subprocess.py +7 -10
- {virtool_workflow-6.0.0a7 → virtool_workflow-6.0.0a9}/virtool_workflow/runtime/sentry.py +2 -3
- {virtool_workflow-6.0.0a7 → virtool_workflow-6.0.0a9}/virtool_workflow/utils.py +12 -1
- {virtool_workflow-6.0.0a7 → virtool_workflow-6.0.0a9}/virtool_workflow/workflow.py +6 -10
- {virtool_workflow-6.0.0a7 → virtool_workflow-6.0.0a9}/LICENSE +0 -0
- {virtool_workflow-6.0.0a7 → virtool_workflow-6.0.0a9}/README.md +0 -0
- {virtool_workflow-6.0.0a7 → virtool_workflow-6.0.0a9}/virtool_workflow/__init__.py +0 -0
- {virtool_workflow-6.0.0a7 → virtool_workflow-6.0.0a9}/virtool_workflow/analysis/__init__.py +0 -0
- {virtool_workflow-6.0.0a7 → virtool_workflow-6.0.0a9}/virtool_workflow/analysis/utils.py +0 -0
- {virtool_workflow-6.0.0a7 → virtool_workflow-6.0.0a9}/virtool_workflow/api/__init__.py +0 -0
- {virtool_workflow-6.0.0a7 → virtool_workflow-6.0.0a9}/virtool_workflow/cli.py +0 -0
- {virtool_workflow-6.0.0a7 → virtool_workflow-6.0.0a9}/virtool_workflow/data/__init__.py +0 -0
- {virtool_workflow-6.0.0a7 → virtool_workflow-6.0.0a9}/virtool_workflow/files.py +0 -0
- {virtool_workflow-6.0.0a7 → virtool_workflow-6.0.0a9}/virtool_workflow/pytest_plugin/__init__.py +0 -0
- {virtool_workflow-6.0.0a7 → virtool_workflow-6.0.0a9}/virtool_workflow/pytest_plugin/utils.py +0 -0
- {virtool_workflow-6.0.0a7 → virtool_workflow-6.0.0a9}/virtool_workflow/runtime/__init__.py +0 -0
- {virtool_workflow-6.0.0a7 → virtool_workflow-6.0.0a9}/virtool_workflow/runtime/config.py +0 -0
- {virtool_workflow-6.0.0a7 → virtool_workflow-6.0.0a9}/virtool_workflow/runtime/events.py +0 -0
- {virtool_workflow-6.0.0a7 → virtool_workflow-6.0.0a9}/virtool_workflow/runtime/path.py +1 -1
- {virtool_workflow-6.0.0a7 → virtool_workflow-6.0.0a9}/virtool_workflow/runtime/redis.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: virtool-workflow
|
3
|
-
Version: 6.0.
|
3
|
+
Version: 6.0.0a9
|
4
4
|
Summary: A framework for developing bioinformatics workflows for Virtool.
|
5
5
|
Home-page: https://github.com/virtool/virtool-workflow
|
6
6
|
License: MIT
|
@@ -19,7 +19,7 @@ Requires-Dist: orjson (>=3.9.9,<4.0.0)
|
|
19
19
|
Requires-Dist: pydantic-factories (>=1.17.3,<2.0.0)
|
20
20
|
Requires-Dist: pyfixtures (>=1.0.0,<2.0.0)
|
21
21
|
Requires-Dist: sentry-sdk (>=1.5.7,<2.0.0)
|
22
|
-
Requires-Dist: virtool-core (>=
|
22
|
+
Requires-Dist: virtool-core (>=11.0.0,<12.0.0)
|
23
23
|
Project-URL: Repository, https://github.com/virtool/virtool-workflow
|
24
24
|
Description-Content-Type: text/markdown
|
25
25
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[tool.poetry]
|
2
2
|
name = "virtool-workflow"
|
3
|
-
version = "6.0.0-alpha.
|
3
|
+
version = "6.0.0-alpha.9"
|
4
4
|
description = "A framework for developing bioinformatics workflows for Virtool."
|
5
5
|
authors = ["Ian Boyes", "Blake Smith", "Ryan Fang"]
|
6
6
|
license = "MIT"
|
@@ -19,7 +19,7 @@ python = "~3.10"
|
|
19
19
|
click = "^8.1.7"
|
20
20
|
aiohttp = "^3.8.1"
|
21
21
|
aiofiles = "^0.7.0"
|
22
|
-
virtool-core = "^
|
22
|
+
virtool-core = "^11.0.0"
|
23
23
|
aioredis = "1.3.1"
|
24
24
|
sentry-sdk = "^1.5.7"
|
25
25
|
pyfixtures = "^1.0.0"
|
@@ -58,17 +58,9 @@ exclude = [
|
|
58
58
|
".ruff_cache",
|
59
59
|
"__pypackages__",
|
60
60
|
]
|
61
|
-
indent-width = 4
|
62
|
-
line-length = 88
|
63
|
-
target-version = "py310"
|
64
61
|
|
65
62
|
[tool.ruff.lint]
|
66
|
-
|
67
|
-
|
68
|
-
[tool.ruff.format]
|
69
|
-
indent-style = "space"
|
70
|
-
line-ending = "auto"
|
71
|
-
quote-style = "double"
|
63
|
+
select = ["ALL"]
|
72
64
|
|
73
65
|
[build-system]
|
74
66
|
requires = ["poetry-core>=1.0.0"]
|
@@ -2,12 +2,12 @@
|
|
2
2
|
from __future__ import annotations
|
3
3
|
|
4
4
|
import asyncio
|
5
|
-
import statistics
|
6
5
|
import shutil
|
6
|
+
import statistics
|
7
7
|
import tempfile
|
8
8
|
from dataclasses import dataclass
|
9
9
|
from pathlib import Path
|
10
|
-
from typing import Protocol, TextIO
|
10
|
+
from typing import IO, Protocol, TextIO
|
11
11
|
|
12
12
|
from pyfixtures import fixture
|
13
13
|
|
@@ -49,16 +49,16 @@ class BaseQualityParser:
|
|
49
49
|
mean=statistics.mean([this.mean, other.mean]),
|
50
50
|
median=statistics.mean([this.median, other.median]),
|
51
51
|
lower_quartile=statistics.mean(
|
52
|
-
[this.lower_quartile, other.lower_quartile]
|
52
|
+
[this.lower_quartile, other.lower_quartile],
|
53
53
|
),
|
54
54
|
upper_quartile=statistics.mean(
|
55
|
-
[this.upper_quartile, other.upper_quartile]
|
55
|
+
[this.upper_quartile, other.upper_quartile],
|
56
56
|
),
|
57
57
|
tenth_percentile=statistics.mean(
|
58
|
-
[this.tenth_percentile, other.tenth_percentile]
|
58
|
+
[this.tenth_percentile, other.tenth_percentile],
|
59
59
|
),
|
60
60
|
ninetieth_percentile=statistics.mean(
|
61
|
-
[this.ninetieth_percentile, other.ninetieth_percentile]
|
61
|
+
[this.ninetieth_percentile, other.ninetieth_percentile],
|
62
62
|
),
|
63
63
|
)
|
64
64
|
for this, other in zip(self.data, parser.data)
|
@@ -109,7 +109,7 @@ class BaseQualityParser:
|
|
109
109
|
upper_quartile=upper_quartile,
|
110
110
|
tenth_percentile=tenth_percentile,
|
111
111
|
ninetieth_percentile=ninetieth_percentile,
|
112
|
-
)
|
112
|
+
),
|
113
113
|
)
|
114
114
|
|
115
115
|
if i - max_index != 1:
|
@@ -208,7 +208,7 @@ class NucleotideCompositionParser:
|
|
208
208
|
split = line.split()
|
209
209
|
|
210
210
|
try:
|
211
|
-
g, a, t, c =
|
211
|
+
g, a, t, c = (float(value) for value in split[1:])
|
212
212
|
except ValueError as err:
|
213
213
|
if "NaN" not in str(err):
|
214
214
|
raise
|
@@ -276,8 +276,7 @@ def _calculate_index_range(base: str) -> range:
|
|
276
276
|
|
277
277
|
|
278
278
|
def _handle_base_quality_nan(split_line: list) -> list:
|
279
|
-
"""
|
280
|
-
Parse a per-base quality line from FastQC containing NaN values.
|
279
|
+
"""Parse a per-base quality line from FastQC containing NaN values.
|
281
280
|
|
282
281
|
:param split_line: the quality line split into a :class:`.List`
|
283
282
|
:return: replacement values
|
@@ -301,8 +300,7 @@ def _handle_base_quality_nan(split_line: list) -> list:
|
|
301
300
|
|
302
301
|
|
303
302
|
def _parse_fastqc(fastqc_path: Path, output_path: Path) -> dict:
|
304
|
-
"""
|
305
|
-
Parse the FastQC results at `fastqc_path`.
|
303
|
+
"""Parse the FastQC results at `fastqc_path`.
|
306
304
|
|
307
305
|
All FastQC data except the textual data file are removed.
|
308
306
|
|
@@ -333,7 +331,7 @@ def _parse_fastqc(fastqc_path: Path, output_path: Path) -> dict:
|
|
333
331
|
nucleotide_composition = NucleotideCompositionParser()
|
334
332
|
sequence_quality = SequenceQualityParser()
|
335
333
|
|
336
|
-
with open(new_path
|
334
|
+
with open(new_path) as f:
|
337
335
|
while True:
|
338
336
|
line = f.readline()
|
339
337
|
|
@@ -358,7 +356,7 @@ def _parse_fastqc(fastqc_path: Path, output_path: Path) -> dict:
|
|
358
356
|
basic_statistics=basic_statistics,
|
359
357
|
nucleotide_composition=nucleotide_composition,
|
360
358
|
sequence_quality=sequence_quality,
|
361
|
-
)
|
359
|
+
),
|
362
360
|
)
|
363
361
|
|
364
362
|
if len(sides) == 1:
|
@@ -412,7 +410,7 @@ def _parse_fastqc(fastqc_path: Path, output_path: Path) -> dict:
|
|
412
410
|
"composition": [
|
413
411
|
[round(n, 1) for n in [point.g, point.a, point.t, point.c]]
|
414
412
|
for point in left.nucleotide_composition.composite(
|
415
|
-
right.nucleotide_composition
|
413
|
+
right.nucleotide_composition,
|
416
414
|
).data
|
417
415
|
],
|
418
416
|
"count": basic.count,
|
@@ -432,14 +430,13 @@ class FastQCRunner(Protocol):
|
|
432
430
|
|
433
431
|
@fixture
|
434
432
|
async def fastqc(run_subprocess: RunSubprocess):
|
435
|
-
"""
|
436
|
-
Provides an asynchronous function that can run FastQC as a subprocess.
|
433
|
+
"""Provides an asynchronous function that can run FastQC as a subprocess.
|
437
434
|
|
438
435
|
The function takes a one or two paths to FASTQ read files (:class:`.ReadPaths`) in
|
439
436
|
a tuple.
|
440
437
|
|
441
438
|
Example:
|
442
|
-
|
439
|
+
-------
|
443
440
|
.. code-block:: python
|
444
441
|
|
445
442
|
@step
|
@@ -1,5 +1,5 @@
|
|
1
|
-
"""
|
2
|
-
|
1
|
+
"""Utilities and a fixture for using `Skewer <https://github.com/relipmoc/skewer>`_ to
|
2
|
+
trim reads.
|
3
3
|
"""
|
4
4
|
import asyncio
|
5
5
|
import os
|
@@ -85,18 +85,16 @@ class SkewerResult:
|
|
85
85
|
|
86
86
|
@property
|
87
87
|
def left(self) -> Path:
|
88
|
-
"""
|
89
|
-
|
90
|
-
|
91
|
-
- the FASTA trimming result for the left reads of a paired Illumina dataset
|
88
|
+
"""The path to one of:
|
89
|
+
- the FASTQ trimming result for an unpaired Illumina dataset
|
90
|
+
- the FASTA trimming result for the left reads of a paired Illumina dataset
|
92
91
|
|
93
92
|
"""
|
94
93
|
return self.read_paths[0]
|
95
94
|
|
96
95
|
@property
|
97
96
|
def right(self) -> Path | None:
|
98
|
-
"""
|
99
|
-
The path to the rights reads of a paired Illumina dataset.
|
97
|
+
"""The path to the rights reads of a paired Illumina dataset.
|
100
98
|
|
101
99
|
``None`` if the dataset in unpaired.
|
102
100
|
|
@@ -110,16 +108,14 @@ class SkewerResult:
|
|
110
108
|
|
111
109
|
|
112
110
|
def calculate_skewer_trimming_parameters(
|
113
|
-
sample: WFSample, min_read_length: int
|
111
|
+
sample: WFSample, min_read_length: int,
|
114
112
|
) -> SkewerConfiguration:
|
115
|
-
"""
|
116
|
-
Calculates trimming parameters based on the library type, and minimum allowed trim length.
|
113
|
+
"""Calculates trimming parameters based on the library type, and minimum allowed trim length.
|
117
114
|
|
118
115
|
:param sample: The sample to calculate trimming parameters for.
|
119
116
|
:param min_read_length: The minimum length of a read before it is discarded.
|
120
117
|
:return: the trimming parameters
|
121
118
|
"""
|
122
|
-
|
123
119
|
config = SkewerConfiguration(
|
124
120
|
min_length=min_read_length,
|
125
121
|
mode=SkewerMode.PAIRED_END if sample.paired else SkewerMode.SINGLE_END,
|
@@ -145,15 +141,14 @@ class SkewerRunner(Protocol):
|
|
145
141
|
"""A protocol describing callables that can be used to run Skewer."""
|
146
142
|
|
147
143
|
async def __call__(
|
148
|
-
self, config: SkewerConfiguration, paths: ReadPaths, output_path: Path
|
144
|
+
self, config: SkewerConfiguration, paths: ReadPaths, output_path: Path,
|
149
145
|
) -> SkewerResult:
|
150
146
|
...
|
151
147
|
|
152
148
|
|
153
149
|
@fixture
|
154
150
|
def skewer(proc: int, run_subprocess: RunSubprocess) -> SkewerRunner:
|
155
|
-
"""
|
156
|
-
Provides an asynchronous function that can run skewer.
|
151
|
+
"""Provides an asynchronous function that can run skewer.
|
157
152
|
|
158
153
|
The provided function takes a :class:`.SkewerConfiguration` and a tuple of paths to
|
159
154
|
the left and right reads to trim. If a single member tuple is provided, the dataset
|
@@ -163,7 +158,7 @@ def skewer(proc: int, run_subprocess: RunSubprocess) -> SkewerRunner:
|
|
163
158
|
for the workflow run.
|
164
159
|
|
165
160
|
Example:
|
166
|
-
|
161
|
+
-------
|
167
162
|
.. code-block:: python
|
168
163
|
|
169
164
|
@step
|
@@ -183,7 +178,7 @@ def skewer(proc: int, run_subprocess: RunSubprocess) -> SkewerRunner:
|
|
183
178
|
raise RuntimeError("skewer is not installed.")
|
184
179
|
|
185
180
|
async def func(
|
186
|
-
config: SkewerConfiguration, read_paths: ReadPaths, output_path: Path
|
181
|
+
config: SkewerConfiguration, read_paths: ReadPaths, output_path: Path,
|
187
182
|
):
|
188
183
|
temp_path = Path(await asyncio.to_thread(mkdtemp, suffix="_virtool_skewer"))
|
189
184
|
|
@@ -224,7 +219,7 @@ def skewer(proc: int, run_subprocess: RunSubprocess) -> SkewerRunner:
|
|
224
219
|
)
|
225
220
|
|
226
221
|
read_paths = await asyncio.to_thread(
|
227
|
-
_rename_trimming_results, temp_path, output_path
|
222
|
+
_rename_trimming_results, temp_path, output_path,
|
228
223
|
)
|
229
224
|
|
230
225
|
return SkewerResult(command, output_path, process, read_paths)
|
@@ -233,12 +228,10 @@ def skewer(proc: int, run_subprocess: RunSubprocess) -> SkewerRunner:
|
|
233
228
|
|
234
229
|
|
235
230
|
def _rename_trimming_results(temp_path: Path, output_path: Path) -> ReadPaths:
|
236
|
-
"""
|
237
|
-
Rename Skewer output to a simple name used in Virtool.
|
231
|
+
"""Rename Skewer output to a simple name used in Virtool.
|
238
232
|
|
239
233
|
:param path: The path containing the results from Skewer
|
240
234
|
"""
|
241
|
-
|
242
235
|
shutil.move(
|
243
236
|
temp_path / "reads-trimmed.log",
|
244
237
|
output_path / "trim.log",
|
@@ -8,10 +8,9 @@ from virtool_workflow.data.samples import WFSample
|
|
8
8
|
|
9
9
|
|
10
10
|
def calculate_trimming_cache_key(
|
11
|
-
sample_id: str, trimming_parameters: dict, program: str = "skewer"
|
11
|
+
sample_id: str, trimming_parameters: dict, program: str = "skewer",
|
12
12
|
):
|
13
|
-
"""
|
14
|
-
Compute a unique cache key.
|
13
|
+
"""Compute a unique cache key.
|
15
14
|
|
16
15
|
**This is not currently used.**
|
17
16
|
|
@@ -21,7 +20,6 @@ def calculate_trimming_cache_key(
|
|
21
20
|
:return: A unique cache key.
|
22
21
|
|
23
22
|
"""
|
24
|
-
|
25
23
|
raw_key = "reads-" + json.dumps(
|
26
24
|
{
|
27
25
|
"id": sample_id,
|
@@ -35,8 +33,7 @@ def calculate_trimming_cache_key(
|
|
35
33
|
|
36
34
|
|
37
35
|
def calculate_trimming_min_length(sample: WFSample) -> int:
|
38
|
-
"""
|
39
|
-
Calculate the minimum trimming length that should be used for the passed sample.
|
36
|
+
"""Calculate the minimum trimming length that should be used for the passed sample.
|
40
37
|
|
41
38
|
This takes into account the library type (:class:`.LibraryType`) and the maximum
|
42
39
|
observed read length in the sample.
|
@@ -1,10 +1,10 @@
|
|
1
1
|
import asyncio
|
2
2
|
|
3
|
-
from aiohttp import ClientSession, TCPConnector
|
3
|
+
from aiohttp import ClientConnectionError, ClientSession, TCPConnector
|
4
4
|
from structlog import get_logger
|
5
5
|
from virtool_core.models.job import JobAcquired
|
6
6
|
|
7
|
-
from virtool_workflow.errors import JobAlreadyAcquired,
|
7
|
+
from virtool_workflow.errors import JobAlreadyAcquired, JobsAPIError, JobsAPIServerError
|
8
8
|
|
9
9
|
logger = get_logger("api")
|
10
10
|
|
@@ -13,16 +13,14 @@ async def acquire_job_by_id(
|
|
13
13
|
jobs_api_connection_string: str,
|
14
14
|
job_id: str,
|
15
15
|
) -> JobAcquired:
|
16
|
-
"""
|
17
|
-
Acquire the job with a given ID via the API.
|
16
|
+
"""Acquire the job with a given ID via the API.
|
18
17
|
|
19
18
|
:param jobs_api_connection_string: The url for the jobs API.
|
20
19
|
:param job_id: The id of the job to acquire
|
21
20
|
:return: a job including its API key
|
22
21
|
"""
|
23
|
-
|
24
22
|
async with ClientSession(
|
25
|
-
connector=TCPConnector(force_close=True, limit=100)
|
23
|
+
connector=TCPConnector(force_close=True, limit=100),
|
26
24
|
) as session:
|
27
25
|
attempts = 4
|
28
26
|
|
@@ -2,14 +2,14 @@ from contextlib import asynccontextmanager
|
|
2
2
|
from pathlib import Path
|
3
3
|
|
4
4
|
import aiofiles
|
5
|
-
from aiohttp import
|
5
|
+
from aiohttp import BasicAuth, ClientSession
|
6
6
|
|
7
7
|
from virtool_workflow.api.utils import (
|
8
|
-
raise_exception_by_status_code,
|
9
8
|
decode_json_response,
|
9
|
+
raise_exception_by_status_code,
|
10
10
|
)
|
11
|
-
from virtool_workflow.files import VirtoolFileFormat
|
12
11
|
from virtool_workflow.errors import JobsAPIError
|
12
|
+
from virtool_workflow.files import VirtoolFileFormat
|
13
13
|
|
14
14
|
CHUNK_SIZE = 1024 * 1024 * 2
|
15
15
|
|
@@ -21,19 +21,17 @@ class APIClient:
|
|
21
21
|
|
22
22
|
async def get_json(self, path: str) -> dict:
|
23
23
|
"""Get the JSON response from the provided API ``path``."""
|
24
|
-
|
25
24
|
async with self.http.get(f"{self.jobs_api_connection_string}{path}") as resp:
|
26
25
|
await raise_exception_by_status_code(resp)
|
27
26
|
return await decode_json_response(resp)
|
28
27
|
|
29
28
|
async def get_file(self, path: str, target_path: Path):
|
30
|
-
"""
|
31
|
-
Download the file at URL ``path`` to the local filesystem path ``target_path``.
|
29
|
+
"""Download the file at URL ``path`` to the local filesystem path ``target_path``.
|
32
30
|
"""
|
33
31
|
async with self.http.get(f"{self.jobs_api_connection_string}{path}") as resp:
|
34
32
|
if resp.status != 200:
|
35
33
|
raise JobsAPIError(
|
36
|
-
f"Encountered {resp.status} while downloading '{path}'"
|
34
|
+
f"Encountered {resp.status} while downloading '{path}'",
|
37
35
|
)
|
38
36
|
async with aiofiles.open(target_path, "wb") as f:
|
39
37
|
async for chunk in resp.content.iter_chunked(CHUNK_SIZE):
|
@@ -42,8 +40,7 @@ class APIClient:
|
|
42
40
|
return target_path
|
43
41
|
|
44
42
|
async def patch_json(self, path: str, data: dict) -> dict:
|
45
|
-
"""
|
46
|
-
Make a patch request against the provided API ``path`` and return the response
|
43
|
+
"""Make a patch request against the provided API ``path`` and return the response
|
47
44
|
as a dictionary of decoded JSON.
|
48
45
|
|
49
46
|
:param path: the API path to make the request against
|
@@ -51,7 +48,7 @@ class APIClient:
|
|
51
48
|
:return: the response as a dictionary of decoded JSON
|
52
49
|
"""
|
53
50
|
async with self.http.patch(
|
54
|
-
f"{self.jobs_api_connection_string}{path}", json=data
|
51
|
+
f"{self.jobs_api_connection_string}{path}", json=data,
|
55
52
|
) as resp:
|
56
53
|
await raise_exception_by_status_code(resp)
|
57
54
|
return await decode_json_response(resp)
|
@@ -78,7 +75,7 @@ class APIClient:
|
|
78
75
|
|
79
76
|
async def post_json(self, path: str, data: dict) -> dict:
|
80
77
|
async with self.http.post(
|
81
|
-
f"{self.jobs_api_connection_string}{path}", json=data
|
78
|
+
f"{self.jobs_api_connection_string}{path}", json=data,
|
82
79
|
) as resp:
|
83
80
|
await raise_exception_by_status_code(resp)
|
84
81
|
return await decode_json_response(resp)
|
@@ -105,7 +102,7 @@ class APIClient:
|
|
105
102
|
|
106
103
|
async def put_json(self, path: str, data: dict) -> dict:
|
107
104
|
async with self.http.put(
|
108
|
-
f"{self.jobs_api_connection_string}{path}", json=data
|
105
|
+
f"{self.jobs_api_connection_string}{path}", json=data,
|
109
106
|
) as resp:
|
110
107
|
await raise_exception_by_status_code(resp)
|
111
108
|
return await decode_json_response(resp)
|
@@ -127,10 +124,9 @@ async def api_client(
|
|
127
124
|
job_id: str,
|
128
125
|
key: str,
|
129
126
|
):
|
130
|
-
"""
|
131
|
-
An authenticated :class:``APIClient`` to make requests against the jobs API.
|
127
|
+
"""An authenticated :class:``APIClient`` to make requests against the jobs API.
|
132
128
|
"""
|
133
129
|
async with ClientSession(
|
134
|
-
auth=BasicAuth(login=f"job-{job_id}", password=key)
|
130
|
+
auth=BasicAuth(login=f"job-{job_id}", password=key),
|
135
131
|
) as http:
|
136
132
|
yield APIClient(http, jobs_api_connection_string)
|
@@ -2,27 +2,26 @@ import asyncio
|
|
2
2
|
import functools
|
3
3
|
|
4
4
|
from aiohttp import (
|
5
|
-
ServerDisconnectedError,
|
6
5
|
ClientConnectorError,
|
7
6
|
ClientResponse,
|
8
7
|
ContentTypeError,
|
8
|
+
ServerDisconnectedError,
|
9
9
|
)
|
10
10
|
from structlog import get_logger
|
11
11
|
|
12
12
|
from virtool_workflow.errors import (
|
13
|
-
JobsAPIServerError,
|
14
|
-
JobsAPINotFound,
|
15
13
|
JobsAPIBadRequest,
|
16
|
-
JobsAPIForbidden,
|
17
14
|
JobsAPIConflict,
|
15
|
+
JobsAPIForbidden,
|
16
|
+
JobsAPINotFound,
|
17
|
+
JobsAPIServerError,
|
18
18
|
)
|
19
19
|
|
20
20
|
logger = get_logger("api")
|
21
21
|
|
22
22
|
|
23
23
|
def retry(func):
|
24
|
-
"""
|
25
|
-
Retry an API call five times when encountering the following exceptions:
|
24
|
+
"""Retry an API call five times when encountering the following exceptions:
|
26
25
|
* ``ConnectionRefusedError``.
|
27
26
|
* ``ClientConnectorError``.
|
28
27
|
* ``ServerDisconnectedError``.
|
@@ -47,7 +46,7 @@ def retry(func):
|
|
47
46
|
|
48
47
|
attempts += 1
|
49
48
|
get_logger("runtime").info(
|
50
|
-
f"Encountered {type(err).__name__}. Retrying in 5 seconds."
|
49
|
+
f"Encountered {type(err).__name__}. Retrying in 5 seconds.",
|
51
50
|
)
|
52
51
|
await asyncio.sleep(5)
|
53
52
|
|
@@ -57,8 +56,7 @@ def retry(func):
|
|
57
56
|
|
58
57
|
|
59
58
|
async def decode_json_response(resp: ClientResponse) -> dict | list | None:
|
60
|
-
"""
|
61
|
-
Decode a JSON response from a :class:``ClientResponse``.
|
59
|
+
"""Decode a JSON response from a :class:``ClientResponse``.
|
62
60
|
|
63
61
|
Raise a :class:`ValueError` if the response is not JSON.
|
64
62
|
|
@@ -72,8 +70,7 @@ async def decode_json_response(resp: ClientResponse) -> dict | list | None:
|
|
72
70
|
|
73
71
|
|
74
72
|
async def raise_exception_by_status_code(resp: ClientResponse):
|
75
|
-
"""
|
76
|
-
Raise an exception based on the status code of the response.
|
73
|
+
"""Raise an exception based on the status code of the response.
|
77
74
|
|
78
75
|
:param resp: the response to check
|
79
76
|
:raise JobsAPIBadRequest: the response status code is 400
|
@@ -108,5 +105,5 @@ async def raise_exception_by_status_code(resp: ClientResponse):
|
|
108
105
|
raise status_exception_map[resp.status](message)
|
109
106
|
else:
|
110
107
|
raise ValueError(
|
111
|
-
f"Status code {resp.status} not handled for response\n {resp}"
|
108
|
+
f"Status code {resp.status} not handled for response\n {resp}",
|
112
109
|
)
|
@@ -52,17 +52,14 @@ class WFAnalysis:
|
|
52
52
|
"""The workflow being run to populate the analysis."""
|
53
53
|
|
54
54
|
async def delete(self):
|
55
|
-
"""
|
56
|
-
Delete the analysis.
|
55
|
+
"""Delete the analysis.
|
57
56
|
|
58
57
|
This method should be called if the workflow fails before a result is uploaded.
|
59
58
|
"""
|
60
|
-
|
61
59
|
await self._api.delete(f"/analyses/{self.id}")
|
62
60
|
|
63
61
|
async def upload_file(self, path: Path, fmt: VirtoolFileFormat = "unknown"):
|
64
|
-
"""
|
65
|
-
Upload files in the workflow environment that should be associated with the
|
62
|
+
"""Upload files in the workflow environment that should be associated with the
|
66
63
|
current analysis.
|
67
64
|
|
68
65
|
:param path: the path to the file to upload
|
@@ -76,8 +73,7 @@ class WFAnalysis:
|
|
76
73
|
)
|
77
74
|
|
78
75
|
async def upload_result(self, results: dict[str, Any]):
|
79
|
-
"""
|
80
|
-
Upload the results dict for the analysis.
|
76
|
+
"""Upload the results dict for the analysis.
|
81
77
|
|
82
78
|
:param results: the analysis results
|
83
79
|
"""
|
@@ -89,8 +85,7 @@ async def analysis(
|
|
89
85
|
_api: APIClient,
|
90
86
|
job: Job,
|
91
87
|
) -> WFAnalysis:
|
92
|
-
"""
|
93
|
-
A :class:`.WFAnalysis` object that represents the analysis associated with the running
|
88
|
+
"""A :class:`.WFAnalysis` object that represents the analysis associated with the running
|
94
89
|
workflow.
|
95
90
|
"""
|
96
91
|
id_ = job.args["analysis_id"]
|
@@ -17,8 +17,7 @@ from virtool_workflow.runtime.run_subprocess import RunSubprocess
|
|
17
17
|
|
18
18
|
@dataclass
|
19
19
|
class WFHMMs:
|
20
|
-
"""
|
21
|
-
A class that exposes:
|
20
|
+
"""A class that exposes:
|
22
21
|
|
23
22
|
1. A :class:`dict` the links `HMMER <http://hmmer.org/>`_ cluster IDs to Virtool
|
24
23
|
annotation IDs.
|
@@ -37,24 +36,21 @@ class WFHMMs:
|
|
37
36
|
|
38
37
|
@cached_property
|
39
38
|
def cluster_annotation_map(self) -> dict[int, str]:
|
40
|
-
"""
|
41
|
-
A :class:`dict` that maps cluster IDs used to identify HMMs in
|
39
|
+
"""A :class:`dict` that maps cluster IDs used to identify HMMs in
|
42
40
|
`HMMER <http://hmmer.org/>`_ to annotation IDs used in Virtool.
|
43
41
|
"""
|
44
42
|
return {hmm.cluster: hmm.id for hmm in self.annotations}
|
45
43
|
|
46
44
|
@property
|
47
45
|
def profiles_path(self):
|
48
|
-
"""
|
49
|
-
The path to the ``profiles.hmm`` file.
|
46
|
+
"""The path to the ``profiles.hmm`` file.
|
50
47
|
|
51
48
|
It can be provided directly to HMMER.
|
52
49
|
"""
|
53
50
|
return self.path / "profiles.hmm"
|
54
51
|
|
55
52
|
def get_id_by_cluster(self, cluster: int) -> str:
|
56
|
-
"""
|
57
|
-
Get the Virtool HMM annotation ID for a given cluster ID.
|
53
|
+
"""Get the Virtool HMM annotation ID for a given cluster ID.
|
58
54
|
|
59
55
|
:param cluster: a cluster ID
|
60
56
|
:return: the corresponding annotation ID
|
@@ -69,8 +65,7 @@ async def hmms(
|
|
69
65
|
run_subprocess: RunSubprocess,
|
70
66
|
work_path: Path,
|
71
67
|
):
|
72
|
-
"""
|
73
|
-
A fixture for accessing HMM data.
|
68
|
+
"""A fixture for accessing HMM data.
|
74
69
|
|
75
70
|
The ``*.hmm`` file is copied from the data directory and ``hmmpress`` is run to
|
76
71
|
create all the HMM files.
|