virtool-workflow 6.0.0a11__tar.gz → 7.1.0__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-7.1.0/PKG-INFO +74 -0
- virtool_workflow-7.1.0/README.md +49 -0
- {virtool_workflow-6.0.0a11 → virtool_workflow-7.1.0}/pyproject.toml +33 -14
- {virtool_workflow-6.0.0a11 → virtool_workflow-7.1.0}/virtool_workflow/api/acquire.py +6 -2
- {virtool_workflow-6.0.0a11 → virtool_workflow-7.1.0}/virtool_workflow/api/utils.py +8 -8
- {virtool_workflow-6.0.0a11 → virtool_workflow-7.1.0}/virtool_workflow/data/indexes.py +12 -9
- {virtool_workflow-6.0.0a11 → virtool_workflow-7.1.0}/virtool_workflow/data/samples.py +15 -7
- {virtool_workflow-6.0.0a11 → virtool_workflow-7.1.0}/virtool_workflow/data/subtractions.py +13 -6
- {virtool_workflow-6.0.0a11 → virtool_workflow-7.1.0}/virtool_workflow/decorators.py +7 -5
- virtool_workflow-7.1.0/virtool_workflow/errors.py +62 -0
- virtool_workflow-7.1.0/virtool_workflow/files.py +40 -0
- {virtool_workflow-6.0.0a11 → virtool_workflow-7.1.0}/virtool_workflow/hooks.py +2 -3
- {virtool_workflow-6.0.0a11 → virtool_workflow-7.1.0}/virtool_workflow/pytest_plugin/data.py +0 -1
- {virtool_workflow-6.0.0a11 → virtool_workflow-7.1.0}/virtool_workflow/runtime/redis.py +19 -14
- {virtool_workflow-6.0.0a11 → virtool_workflow-7.1.0}/virtool_workflow/runtime/run.py +5 -5
- {virtool_workflow-6.0.0a11 → virtool_workflow-7.1.0}/virtool_workflow/runtime/run_subprocess.py +35 -10
- {virtool_workflow-6.0.0a11 → virtool_workflow-7.1.0}/virtool_workflow/runtime/sentry.py +4 -4
- {virtool_workflow-6.0.0a11 → virtool_workflow-7.1.0}/virtool_workflow/utils.py +9 -0
- virtool_workflow-6.0.0a11/PKG-INFO +0 -119
- virtool_workflow-6.0.0a11/README.md +0 -93
- virtool_workflow-6.0.0a11/virtool_workflow/errors.py +0 -61
- virtool_workflow-6.0.0a11/virtool_workflow/files.py +0 -24
- {virtool_workflow-6.0.0a11 → virtool_workflow-7.1.0}/LICENSE +0 -0
- {virtool_workflow-6.0.0a11 → virtool_workflow-7.1.0}/virtool_workflow/__init__.py +0 -0
- {virtool_workflow-6.0.0a11 → virtool_workflow-7.1.0}/virtool_workflow/analysis/__init__.py +0 -0
- {virtool_workflow-6.0.0a11 → virtool_workflow-7.1.0}/virtool_workflow/analysis/fastqc.py +0 -0
- {virtool_workflow-6.0.0a11 → virtool_workflow-7.1.0}/virtool_workflow/analysis/skewer.py +0 -0
- {virtool_workflow-6.0.0a11 → virtool_workflow-7.1.0}/virtool_workflow/analysis/trimming.py +0 -0
- {virtool_workflow-6.0.0a11 → virtool_workflow-7.1.0}/virtool_workflow/analysis/utils.py +0 -0
- {virtool_workflow-6.0.0a11 → virtool_workflow-7.1.0}/virtool_workflow/api/__init__.py +0 -0
- {virtool_workflow-6.0.0a11 → virtool_workflow-7.1.0}/virtool_workflow/api/client.py +0 -0
- {virtool_workflow-6.0.0a11 → virtool_workflow-7.1.0}/virtool_workflow/cli.py +0 -0
- {virtool_workflow-6.0.0a11 → virtool_workflow-7.1.0}/virtool_workflow/data/__init__.py +0 -0
- {virtool_workflow-6.0.0a11 → virtool_workflow-7.1.0}/virtool_workflow/data/analyses.py +0 -0
- {virtool_workflow-6.0.0a11 → virtool_workflow-7.1.0}/virtool_workflow/data/hmms.py +0 -0
- {virtool_workflow-6.0.0a11 → virtool_workflow-7.1.0}/virtool_workflow/data/jobs.py +0 -0
- {virtool_workflow-6.0.0a11 → virtool_workflow-7.1.0}/virtool_workflow/data/ml.py +0 -0
- {virtool_workflow-6.0.0a11 → virtool_workflow-7.1.0}/virtool_workflow/data/uploads.py +0 -0
- {virtool_workflow-6.0.0a11 → virtool_workflow-7.1.0}/virtool_workflow/pytest_plugin/__init__.py +0 -0
- {virtool_workflow-6.0.0a11 → virtool_workflow-7.1.0}/virtool_workflow/pytest_plugin/utils.py +0 -0
- {virtool_workflow-6.0.0a11 → virtool_workflow-7.1.0}/virtool_workflow/runtime/__init__.py +0 -0
- {virtool_workflow-6.0.0a11 → virtool_workflow-7.1.0}/virtool_workflow/runtime/config.py +0 -0
- {virtool_workflow-6.0.0a11 → virtool_workflow-7.1.0}/virtool_workflow/runtime/discover.py +0 -0
- {virtool_workflow-6.0.0a11 → virtool_workflow-7.1.0}/virtool_workflow/runtime/events.py +0 -0
- {virtool_workflow-6.0.0a11 → virtool_workflow-7.1.0}/virtool_workflow/runtime/hook.py +0 -0
- {virtool_workflow-6.0.0a11 → virtool_workflow-7.1.0}/virtool_workflow/runtime/path.py +0 -0
- {virtool_workflow-6.0.0a11 → virtool_workflow-7.1.0}/virtool_workflow/runtime/ping.py +0 -0
- {virtool_workflow-6.0.0a11 → virtool_workflow-7.1.0}/virtool_workflow/workflow.py +0 -0
@@ -0,0 +1,74 @@
|
|
1
|
+
Metadata-Version: 2.1
|
2
|
+
Name: virtool-workflow
|
3
|
+
Version: 7.1.0
|
4
|
+
Summary: A framework for developing bioinformatics workflows for Virtool.
|
5
|
+
Home-page: https://github.com/virtool/virtool-workflow
|
6
|
+
License: MIT
|
7
|
+
Author: Ian Boyes
|
8
|
+
Maintainer: Ian Boyes
|
9
|
+
Requires-Python: >=3.12,<3.13
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
13
|
+
Requires-Dist: aiofiles (>=0.7.0,<0.8.0)
|
14
|
+
Requires-Dist: aiohttp (>=3.8.1,<4.0.0)
|
15
|
+
Requires-Dist: biopython (>=1.81,<2.0)
|
16
|
+
Requires-Dist: click (>=8.1.7,<9.0.0)
|
17
|
+
Requires-Dist: orjson (>=3.9.9,<4.0.0)
|
18
|
+
Requires-Dist: pydantic-factories (>=1.17.3,<2.0.0)
|
19
|
+
Requires-Dist: pyfixtures (>=1.0.0,<2.0.0)
|
20
|
+
Requires-Dist: sentry-sdk (>=2.3.1,<3.0.0)
|
21
|
+
Requires-Dist: virtool-core (>=14.0.0,<15.0.0)
|
22
|
+
Project-URL: Repository, https://github.com/virtool/virtool-workflow
|
23
|
+
Description-Content-Type: text/markdown
|
24
|
+
|
25
|
+
# Virtool Workflow
|
26
|
+
|
27
|
+

|
28
|
+
[](https://badge.fury.io/py/virtool-workflow)
|
29
|
+
|
30
|
+
A framework for developing bioinformatic workflows in Python.
|
31
|
+
|
32
|
+
```python
|
33
|
+
from virtool_workflow import step
|
34
|
+
|
35
|
+
|
36
|
+
@step
|
37
|
+
def step_function():
|
38
|
+
...
|
39
|
+
|
40
|
+
|
41
|
+
@step
|
42
|
+
def step_function_2():
|
43
|
+
...
|
44
|
+
```
|
45
|
+
|
46
|
+
## Contributing
|
47
|
+
|
48
|
+
### Commits
|
49
|
+
|
50
|
+
We require specific commit formatting. Any commit that does not follow the guidelines
|
51
|
+
will be squashed at our discretion.
|
52
|
+
|
53
|
+
Read our [commit and release](https://dev.virtool.ca/en/latest/commits_releases.html)
|
54
|
+
documentation for more information.
|
55
|
+
|
56
|
+
### Tests
|
57
|
+
|
58
|
+
Run tests with:
|
59
|
+
|
60
|
+
```shell
|
61
|
+
# Bring up Redis and the test container.
|
62
|
+
docker compose up -d
|
63
|
+
|
64
|
+
# Run tests in the test container.
|
65
|
+
docker compose exec test poetry run pytest
|
66
|
+
|
67
|
+
```
|
68
|
+
|
69
|
+
Run specific tests like:
|
70
|
+
|
71
|
+
```shell
|
72
|
+
docker compose exec test poetry run pytest tests/test_status.py
|
73
|
+
```
|
74
|
+
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# Virtool Workflow
|
2
|
+
|
3
|
+

|
4
|
+
[](https://badge.fury.io/py/virtool-workflow)
|
5
|
+
|
6
|
+
A framework for developing bioinformatic workflows in Python.
|
7
|
+
|
8
|
+
```python
|
9
|
+
from virtool_workflow import step
|
10
|
+
|
11
|
+
|
12
|
+
@step
|
13
|
+
def step_function():
|
14
|
+
...
|
15
|
+
|
16
|
+
|
17
|
+
@step
|
18
|
+
def step_function_2():
|
19
|
+
...
|
20
|
+
```
|
21
|
+
|
22
|
+
## Contributing
|
23
|
+
|
24
|
+
### Commits
|
25
|
+
|
26
|
+
We require specific commit formatting. Any commit that does not follow the guidelines
|
27
|
+
will be squashed at our discretion.
|
28
|
+
|
29
|
+
Read our [commit and release](https://dev.virtool.ca/en/latest/commits_releases.html)
|
30
|
+
documentation for more information.
|
31
|
+
|
32
|
+
### Tests
|
33
|
+
|
34
|
+
Run tests with:
|
35
|
+
|
36
|
+
```shell
|
37
|
+
# Bring up Redis and the test container.
|
38
|
+
docker compose up -d
|
39
|
+
|
40
|
+
# Run tests in the test container.
|
41
|
+
docker compose exec test poetry run pytest
|
42
|
+
|
43
|
+
```
|
44
|
+
|
45
|
+
Run specific tests like:
|
46
|
+
|
47
|
+
```shell
|
48
|
+
docker compose exec test poetry run pytest tests/test_status.py
|
49
|
+
```
|
@@ -1,32 +1,44 @@
|
|
1
1
|
[tool.poetry]
|
2
2
|
name = "virtool-workflow"
|
3
|
-
version = "
|
3
|
+
version = "7.1.0"
|
4
4
|
description = "A framework for developing bioinformatics workflows for Virtool."
|
5
|
-
authors = [
|
5
|
+
authors = [
|
6
|
+
"Ian Boyes",
|
7
|
+
"Blake Smith",
|
8
|
+
"Ryan Fang",
|
9
|
+
"Matt Curtis",
|
10
|
+
"Aman Monga",
|
11
|
+
"Bryce Davidson",
|
12
|
+
"Christine Wong Chong",
|
13
|
+
"Lilly Roberts",
|
14
|
+
"Markus Swoveland",
|
15
|
+
"Ryan Fang",
|
16
|
+
]
|
6
17
|
license = "MIT"
|
18
|
+
maintainers = [
|
19
|
+
"Ian Boyes",
|
20
|
+
"Reece Hoffman"
|
21
|
+
]
|
7
22
|
readme = "README.md"
|
8
23
|
repository = "https://github.com/virtool/virtool-workflow"
|
9
24
|
classifiers = [
|
10
|
-
"
|
11
|
-
"Programming Language :: Python :: 3.10"
|
25
|
+
"Programming Language :: Python :: 3.12",
|
12
26
|
]
|
13
27
|
packages = [
|
14
28
|
{ include = "virtool_workflow" },
|
15
29
|
]
|
16
30
|
|
17
31
|
[tool.poetry.dependencies]
|
18
|
-
python = "~3.
|
19
|
-
click = "^8.1.7"
|
20
|
-
aiohttp = "^3.8.1"
|
32
|
+
python = "~3.12"
|
21
33
|
aiofiles = "^0.7.0"
|
22
|
-
|
23
|
-
aioredis = "1.3.1"
|
24
|
-
sentry-sdk = "^1.5.7"
|
25
|
-
pyfixtures = "^1.0.0"
|
26
|
-
orjson = "^3.9.9"
|
34
|
+
aiohttp = "^3.8.1"
|
27
35
|
biopython = "^1.81"
|
36
|
+
click = "^8.1.7"
|
37
|
+
orjson = "^3.9.9"
|
28
38
|
pydantic-factories = "^1.17.3"
|
29
|
-
|
39
|
+
pyfixtures = "^1.0.0"
|
40
|
+
sentry-sdk = "^2.3.1"
|
41
|
+
virtool-core = "^14.0.0"
|
30
42
|
|
31
43
|
[tool.poetry.scripts]
|
32
44
|
run-workflow = "virtool_workflow.cli:cli_main"
|
@@ -36,13 +48,14 @@ pytest = "^7.4.2"
|
|
36
48
|
pytest-aiohttp = "^1.0.0"
|
37
49
|
pytest-asyncio = "^0.21.0"
|
38
50
|
pytest-mock = "^3.10.0"
|
39
|
-
pre-commit = "^2.18.1"
|
40
51
|
sphinx = "^4.0.2"
|
41
52
|
syrupy = "^3.0.5"
|
42
53
|
sphinx-autobuild = "^2021.3.14"
|
43
54
|
sphinx-nameko-theme = "^0.0.3"
|
44
55
|
pytest-structlog = "^0.6"
|
45
56
|
sphinx-toolbox = "^3.5.0"
|
57
|
+
ruff = "^0.4.6"
|
58
|
+
piccolo-theme = "^0.22.0"
|
46
59
|
|
47
60
|
[tool.pytest.ini_options]
|
48
61
|
asyncio_mode = "auto"
|
@@ -58,8 +71,14 @@ exclude = [
|
|
58
71
|
".ruff_cache",
|
59
72
|
"__pypackages__",
|
60
73
|
]
|
74
|
+
target-version = "py312"
|
61
75
|
|
62
76
|
[tool.ruff.lint]
|
77
|
+
ignore = [
|
78
|
+
"ANN101",
|
79
|
+
"D203",
|
80
|
+
"D213"
|
81
|
+
]
|
63
82
|
select = ["ALL"]
|
64
83
|
|
65
84
|
[build-system]
|
@@ -4,7 +4,11 @@ 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
|
7
|
+
from virtool_workflow.errors import (
|
8
|
+
JobAlreadyAcquiredError,
|
9
|
+
JobsAPIError,
|
10
|
+
JobsAPIServerError,
|
11
|
+
)
|
8
12
|
|
9
13
|
logger = get_logger("api")
|
10
14
|
|
@@ -39,7 +43,7 @@ async def acquire_job_by_id(
|
|
39
43
|
|
40
44
|
if resp.status == 400:
|
41
45
|
if "already acquired" in await resp.text():
|
42
|
-
raise
|
46
|
+
raise JobAlreadyAcquiredError(await resp.json())
|
43
47
|
|
44
48
|
logger.critical(
|
45
49
|
"unexpected api error during job acquisition",
|
@@ -10,10 +10,10 @@ from aiohttp import (
|
|
10
10
|
from structlog import get_logger
|
11
11
|
|
12
12
|
from virtool_workflow.errors import (
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
13
|
+
JobsAPIBadRequestError,
|
14
|
+
JobsAPIConflictError,
|
15
|
+
JobsAPIForbiddenError,
|
16
|
+
JobsAPINotFoundError,
|
17
17
|
JobsAPIServerError,
|
18
18
|
)
|
19
19
|
|
@@ -80,10 +80,10 @@ async def raise_exception_by_status_code(resp: ClientResponse):
|
|
80
80
|
:raise JobsAPIServerError: the response status code is 500
|
81
81
|
"""
|
82
82
|
status_exception_map = {
|
83
|
-
400:
|
84
|
-
403:
|
85
|
-
404:
|
86
|
-
409:
|
83
|
+
400: JobsAPIBadRequestError,
|
84
|
+
403: JobsAPIForbiddenError,
|
85
|
+
404: JobsAPINotFoundError,
|
86
|
+
409: JobsAPIConflictError,
|
87
87
|
500: JobsAPIServerError,
|
88
88
|
}
|
89
89
|
|
@@ -13,7 +13,7 @@ from virtool_core.models.reference import ReferenceNested
|
|
13
13
|
from virtool_core.utils import decompress_file
|
14
14
|
|
15
15
|
from virtool_workflow.api.client import APIClient
|
16
|
-
from virtool_workflow.errors import
|
16
|
+
from virtool_workflow.errors import MissingJobArgumentError
|
17
17
|
from virtool_workflow.files import VirtoolFileFormat
|
18
18
|
|
19
19
|
logger = get_logger("api")
|
@@ -43,9 +43,7 @@ class WFIndex:
|
|
43
43
|
|
44
44
|
@property
|
45
45
|
def bowtie_path(self) -> Path:
|
46
|
-
"""The path to the Bowtie2 index prefix for the Virtool index.
|
47
|
-
|
48
|
-
"""
|
46
|
+
"""The path to the Bowtie2 index prefix for the Virtool index."""
|
49
47
|
return self.path / "reference"
|
50
48
|
|
51
49
|
@property
|
@@ -151,7 +149,10 @@ class WFNewIndex:
|
|
151
149
|
await self._api.patch_json(f"/indexes/{self.id}", {})
|
152
150
|
|
153
151
|
async def upload(
|
154
|
-
self,
|
152
|
+
self,
|
153
|
+
path: Path,
|
154
|
+
fmt: VirtoolFileFormat = "fasta",
|
155
|
+
name: str | None = None,
|
155
156
|
):
|
156
157
|
"""Upload a file to associate with the index being built.
|
157
158
|
|
@@ -272,14 +273,16 @@ async def index(
|
|
272
273
|
|
273
274
|
@fixture
|
274
275
|
async def new_index(
|
275
|
-
_api: APIClient,
|
276
|
+
_api: APIClient,
|
277
|
+
job: Job,
|
278
|
+
proc: int,
|
279
|
+
work_path: Path,
|
276
280
|
) -> WFNewIndex:
|
277
|
-
"""The :class:`.WFNewIndex` for an index being created by the current job.
|
278
|
-
"""
|
281
|
+
"""The :class:`.WFNewIndex` for an index being created by the current job."""
|
279
282
|
try:
|
280
283
|
id_ = job.args["index_id"]
|
281
284
|
except KeyError:
|
282
|
-
raise
|
285
|
+
raise MissingJobArgumentError("Missing jobs args key 'index_id'")
|
283
286
|
|
284
287
|
log = logger.bind(resource="new_index", id=id_, job_id=job.id)
|
285
288
|
log.info("loading index")
|
@@ -12,7 +12,7 @@ from virtool_core.models.samples import Quality, Sample
|
|
12
12
|
from virtool_workflow.analysis.utils import ReadPaths
|
13
13
|
from virtool_workflow.api.client import APIClient
|
14
14
|
from virtool_workflow.data.uploads import WFUploads
|
15
|
-
from virtool_workflow.errors import
|
15
|
+
from virtool_workflow.errors import JobsAPINotFoundError
|
16
16
|
from virtool_workflow.files import VirtoolFileFormat
|
17
17
|
|
18
18
|
logger = get_logger("api")
|
@@ -83,7 +83,10 @@ class WFNewSample:
|
|
83
83
|
|
84
84
|
@fixture
|
85
85
|
async def sample(
|
86
|
-
_api: APIClient,
|
86
|
+
_api: APIClient,
|
87
|
+
job: Job,
|
88
|
+
uploads: WFUploads,
|
89
|
+
work_path: Path,
|
87
90
|
) -> WFSample:
|
88
91
|
"""The sample associated with the current job."""
|
89
92
|
id_ = job.args["sample_id"]
|
@@ -92,8 +95,8 @@ async def sample(
|
|
92
95
|
|
93
96
|
try:
|
94
97
|
sample_json = await _api.get_json(base_url_path)
|
95
|
-
except
|
96
|
-
raise
|
98
|
+
except JobsAPINotFoundError:
|
99
|
+
raise JobsAPINotFoundError("Sample not found")
|
97
100
|
|
98
101
|
sample = Sample(**sample_json)
|
99
102
|
|
@@ -101,7 +104,8 @@ async def sample(
|
|
101
104
|
await asyncio.to_thread(reads_path.mkdir, exist_ok=True, parents=True)
|
102
105
|
|
103
106
|
await _api.get_file(
|
104
|
-
f"{base_url_path}/reads/reads_1.fq.gz",
|
107
|
+
f"{base_url_path}/reads/reads_1.fq.gz",
|
108
|
+
reads_path / "reads_1.fq.gz",
|
105
109
|
)
|
106
110
|
|
107
111
|
if sample.paired:
|
@@ -110,7 +114,8 @@ async def sample(
|
|
110
114
|
reads_path / "reads_2.fq.gz",
|
111
115
|
)
|
112
116
|
await _api.get_file(
|
113
|
-
f"{base_url_path}/reads/reads_2.fq.gz",
|
117
|
+
f"{base_url_path}/reads/reads_2.fq.gz",
|
118
|
+
reads_path / "reads_2.fq.gz",
|
114
119
|
)
|
115
120
|
else:
|
116
121
|
read_paths = (reads_path / "reads_1.fq.gz",)
|
@@ -127,7 +132,10 @@ async def sample(
|
|
127
132
|
|
128
133
|
@fixture
|
129
134
|
async def new_sample(
|
130
|
-
_api: APIClient,
|
135
|
+
_api: APIClient,
|
136
|
+
job: Job,
|
137
|
+
uploads: WFUploads,
|
138
|
+
work_path: Path,
|
131
139
|
) -> WFNewSample:
|
132
140
|
"""The sample associated with the current job."""
|
133
141
|
id_ = job.args["sample_id"]
|
@@ -15,7 +15,7 @@ from virtool_core.models.subtraction import (
|
|
15
15
|
from virtool_workflow.api.client import APIClient
|
16
16
|
from virtool_workflow.data.analyses import WFAnalysis
|
17
17
|
from virtool_workflow.data.uploads import WFUploads
|
18
|
-
from virtool_workflow.errors import
|
18
|
+
from virtool_workflow.errors import MissingJobArgumentError
|
19
19
|
|
20
20
|
logger = get_logger("api")
|
21
21
|
|
@@ -118,7 +118,9 @@ class WFNewSubtraction:
|
|
118
118
|
|
119
119
|
@fixture
|
120
120
|
async def subtractions(
|
121
|
-
_api: APIClient,
|
121
|
+
_api: APIClient,
|
122
|
+
analysis: WFAnalysis,
|
123
|
+
work_path: Path,
|
122
124
|
) -> list[WFSubtraction]:
|
123
125
|
"""The subtractions to be used for the current analysis job."""
|
124
126
|
subtraction_work_path = work_path / "subtractions"
|
@@ -158,7 +160,10 @@ async def subtractions(
|
|
158
160
|
|
159
161
|
@fixture
|
160
162
|
async def new_subtraction(
|
161
|
-
_api: APIClient,
|
163
|
+
_api: APIClient,
|
164
|
+
job: Job,
|
165
|
+
uploads: WFUploads,
|
166
|
+
work_path: Path,
|
162
167
|
) -> WFNewSubtraction:
|
163
168
|
"""A new subtraction that will be created during the current job.
|
164
169
|
|
@@ -167,12 +172,12 @@ async def new_subtraction(
|
|
167
172
|
try:
|
168
173
|
id_ = job.args["subtraction_id"]
|
169
174
|
except KeyError:
|
170
|
-
raise
|
175
|
+
raise MissingJobArgumentError("subtraction_id")
|
171
176
|
|
172
177
|
try:
|
173
178
|
upload_id = job.args["files"][0]["id"]
|
174
179
|
except KeyError:
|
175
|
-
raise
|
180
|
+
raise MissingJobArgumentError("files")
|
176
181
|
|
177
182
|
subtraction_json = await _api.get_json(f"/subtractions/{id_}")
|
178
183
|
subtraction_ = Subtraction(**subtraction_json)
|
@@ -221,7 +226,9 @@ async def new_subtraction(
|
|
221
226
|
log.info("Uploading subtraction file")
|
222
227
|
|
223
228
|
await _api.put_file(
|
224
|
-
f"/subtractions/{subtraction_.id}/files/{filename}",
|
229
|
+
f"/subtractions/{subtraction_.id}/files/{filename}",
|
230
|
+
path,
|
231
|
+
"unknown",
|
225
232
|
)
|
226
233
|
|
227
234
|
log.info("Finished uploading subtraction file")
|
@@ -1,11 +1,13 @@
|
|
1
1
|
"""Create Workflows by decorating module scope functions."""
|
2
|
+
|
3
|
+
from collections.abc import Callable
|
2
4
|
from types import ModuleType
|
3
|
-
from typing import Callable
|
4
5
|
|
6
|
+
from virtool_workflow.errors import WorkflowStepsError
|
5
7
|
from virtool_workflow.workflow import Workflow
|
6
8
|
|
7
9
|
|
8
|
-
def step(func: Callable = None, *, name: str | None = None) -> Callable:
|
10
|
+
def step(func: Callable | None = None, *, name: str | None = None) -> Callable:
|
9
11
|
"""Mark a function as a workflow step function.
|
10
12
|
|
11
13
|
:param func: the workflow step function
|
@@ -16,7 +18,7 @@ def step(func: Callable = None, *, name: str | None = None) -> Callable:
|
|
16
18
|
return lambda _f: step(_f, name=name)
|
17
19
|
|
18
20
|
func.__workflow_marker__ = "step"
|
19
|
-
func.__workflow_step_props__ =
|
21
|
+
func.__workflow_step_props__ = {"name": name}
|
20
22
|
|
21
23
|
return func
|
22
24
|
|
@@ -39,7 +41,7 @@ def collect(module: ModuleType) -> Workflow:
|
|
39
41
|
if marked.__workflow_marker__ == "step":
|
40
42
|
workflow.step(marked, **marked.__workflow_step_props__)
|
41
43
|
|
42
|
-
if
|
43
|
-
raise
|
44
|
+
if not workflow.steps:
|
45
|
+
raise WorkflowStepsError(str(module))
|
44
46
|
|
45
47
|
return workflow
|
@@ -0,0 +1,62 @@
|
|
1
|
+
"""Custom exceptions for ``virtool_workflow``."""
|
2
|
+
|
3
|
+
from subprocess import SubprocessError
|
4
|
+
|
5
|
+
|
6
|
+
class JobAlreadyAcquiredError(Exception):
|
7
|
+
"""Raised when an attempt is made to reacquire a job."""
|
8
|
+
|
9
|
+
def __init__(self, job_id: str) -> None:
|
10
|
+
"""Initialize the exception with a message containing the job ID."""
|
11
|
+
super().__init__(
|
12
|
+
f"Job {job_id} is has already been acquired.",
|
13
|
+
)
|
14
|
+
|
15
|
+
|
16
|
+
class JobsAPIError(Exception):
|
17
|
+
"""A base exception for errors due to HTTP errors from the jobs API."""
|
18
|
+
|
19
|
+
|
20
|
+
class JobsAPIBadRequestError(JobsAPIError):
|
21
|
+
"""A ``400 Bad Request`` response was received from the jobs API."""
|
22
|
+
|
23
|
+
status = 400
|
24
|
+
|
25
|
+
|
26
|
+
class JobsAPIForbiddenError(JobsAPIError):
|
27
|
+
"""A ``403 Forbidden`` response was received from the jobs API."""
|
28
|
+
|
29
|
+
status = 403
|
30
|
+
|
31
|
+
|
32
|
+
class JobsAPINotFoundError(JobsAPIError):
|
33
|
+
"""A ``404 Not Found`` response was received from the jobs API."""
|
34
|
+
|
35
|
+
status = 404
|
36
|
+
|
37
|
+
|
38
|
+
class JobsAPIConflictError(JobsAPIError):
|
39
|
+
"""A ``409 Conflict`` response was received from the jobs API."""
|
40
|
+
|
41
|
+
status = 409
|
42
|
+
|
43
|
+
|
44
|
+
class JobsAPIServerError(JobsAPIError):
|
45
|
+
"""A ``500 Internal Server Error`` response was received from the jobs API."""
|
46
|
+
|
47
|
+
status = 500
|
48
|
+
|
49
|
+
|
50
|
+
class MissingJobArgumentError(ValueError):
|
51
|
+
"""The `job.args` dict is missing a required key for some funcionality."""
|
52
|
+
|
53
|
+
|
54
|
+
class WorkflowStepsError(Exception):
|
55
|
+
"""Raised when no workflow steps are found in a module."""
|
56
|
+
|
57
|
+
def __init__(self, module: str) -> None:
|
58
|
+
super().__init__(f"No workflow steps could be found in {module}")
|
59
|
+
|
60
|
+
|
61
|
+
class SubprocessFailedError(SubprocessError):
|
62
|
+
"""Subprocess exited with non-zero status during a workflow."""
|
@@ -0,0 +1,40 @@
|
|
1
|
+
"""Dataclasses for describing files uploaded to the Virtool server."""
|
2
|
+
|
3
|
+
import datetime
|
4
|
+
from dataclasses import dataclass
|
5
|
+
from typing import Literal
|
6
|
+
|
7
|
+
VirtoolFileFormat = Literal[
|
8
|
+
"sam",
|
9
|
+
"bam",
|
10
|
+
"fasta",
|
11
|
+
"fastq",
|
12
|
+
"csv",
|
13
|
+
"tsv",
|
14
|
+
"json",
|
15
|
+
"unknown",
|
16
|
+
]
|
17
|
+
"""A literal type hint for the format of a :class:`.VirtoolFile`."""
|
18
|
+
|
19
|
+
|
20
|
+
@dataclass
|
21
|
+
class VirtoolFile:
|
22
|
+
"""A description of a file uploaded to the Virtool server."""
|
23
|
+
|
24
|
+
id: int
|
25
|
+
"""The unique ID for the file."""
|
26
|
+
|
27
|
+
name: str
|
28
|
+
"""The name of the file."""
|
29
|
+
|
30
|
+
size: int
|
31
|
+
"""The size of the file in bytes."""
|
32
|
+
|
33
|
+
format: VirtoolFileFormat
|
34
|
+
"""The format of the file."""
|
35
|
+
|
36
|
+
name_on_disk: str | None = None
|
37
|
+
"""The actual name of the file on disk."""
|
38
|
+
|
39
|
+
uploaded_at: datetime.datetime | None = None
|
40
|
+
"""When the file was uploaded."""
|
@@ -1,5 +1,4 @@
|
|
1
|
-
"""Hooks
|
2
|
-
"""
|
1
|
+
"""Hooks do things when events happen during the workflow lifecycle."""
|
3
2
|
|
4
3
|
from virtool_workflow.runtime.hook import Hook
|
5
4
|
|
@@ -124,7 +123,7 @@ __all__ = [
|
|
124
123
|
]
|
125
124
|
|
126
125
|
|
127
|
-
def cleanup_builtin_status_hooks():
|
126
|
+
def cleanup_builtin_status_hooks() -> None:
|
128
127
|
"""Clear callbacks for built-in status hooks.
|
129
128
|
|
130
129
|
This prevents carryover of hooks between tests. Carryover won't be encountered in
|
@@ -2,8 +2,8 @@ import asyncio
|
|
2
2
|
from asyncio import CancelledError
|
3
3
|
from typing import Callable
|
4
4
|
|
5
|
-
from aioredis import Redis
|
6
5
|
from structlog import get_logger
|
6
|
+
from virtool_core.redis import Redis
|
7
7
|
|
8
8
|
logger = get_logger("redis")
|
9
9
|
|
@@ -11,10 +11,11 @@ CANCELLATION_CHANNEL = "channel:cancel"
|
|
11
11
|
|
12
12
|
|
13
13
|
async def get_next_job_with_timeout(
|
14
|
-
list_name: str,
|
14
|
+
list_name: str,
|
15
|
+
redis: Redis,
|
16
|
+
timeout: int | None = None,
|
15
17
|
) -> str:
|
16
|
-
"""
|
17
|
-
Get the next job ID from a Redis list and raise a :class:``Timeout`` error if one
|
18
|
+
"""Get the next job ID from a Redis list and raise a :class:``Timeout`` error if one
|
18
19
|
is not found in ``timeout`` seconds.
|
19
20
|
|
20
21
|
:param list_name: the name of the list to pop from
|
@@ -24,35 +25,39 @@ async def get_next_job_with_timeout(
|
|
24
25
|
|
25
26
|
"""
|
26
27
|
logger.info(
|
27
|
-
"Waiting for a job",
|
28
|
+
"Waiting for a job",
|
29
|
+
timeout=f"{timeout if timeout else 'infinity'} seconds",
|
28
30
|
)
|
29
31
|
|
30
32
|
return await asyncio.wait_for(get_next_job(list_name, redis), timeout)
|
31
33
|
|
32
34
|
|
33
35
|
async def get_next_job(list_name: str, redis: Redis) -> str:
|
34
|
-
"""
|
35
|
-
Get the next job ID from a Redis list.
|
36
|
+
"""Get the next job ID from a Redis list.
|
36
37
|
|
37
38
|
:param list_name: the name of the list to pop from
|
38
39
|
:param redis: the Redis client
|
39
40
|
:return: the next job ID
|
40
41
|
|
41
42
|
"""
|
42
|
-
|
43
|
-
|
44
|
-
if result is not None:
|
45
|
-
job_id = str(result[1], encoding="utf-8")
|
43
|
+
if (job_id := await redis.blpop(list_name)) is not None:
|
46
44
|
logger.info("pulled job id from redis", id=job_id)
|
47
45
|
return job_id
|
48
46
|
|
47
|
+
raise ValueError("Unexpected None from job id list")
|
48
|
+
|
49
49
|
|
50
50
|
async def wait_for_cancellation(redis: Redis, job_id: str, func: Callable):
|
51
|
-
|
51
|
+
"""Call a function ``func`` when a job matching ``job_id`` is cancelled.
|
52
52
|
|
53
|
+
:param redis: the Redis client
|
54
|
+
:param job_id: the job ID to watch for
|
55
|
+
:param func: the function to call when the job is cancelled
|
56
|
+
|
57
|
+
"""
|
53
58
|
try:
|
54
|
-
async for cancelled_job_id in
|
55
|
-
if cancelled_job_id
|
59
|
+
async for cancelled_job_id in redis.subscribe(CANCELLATION_CHANNEL):
|
60
|
+
if cancelled_job_id == job_id:
|
56
61
|
return func()
|
57
62
|
|
58
63
|
except CancelledError:
|
@@ -6,12 +6,11 @@ from asyncio import CancelledError
|
|
6
6
|
from pathlib import Path
|
7
7
|
from typing import Callable
|
8
8
|
|
9
|
-
import pkg_resources
|
10
9
|
import structlog
|
11
10
|
from pyfixtures import FixtureScope, runs_in_new_fixture_context
|
12
11
|
from structlog import get_logger
|
13
12
|
from virtool_core.models.job import JobState
|
14
|
-
from virtool_core.redis import
|
13
|
+
from virtool_core.redis import Redis
|
15
14
|
|
16
15
|
from virtool_workflow.api.acquire import acquire_job_by_id
|
17
16
|
from virtool_workflow.api.client import api_client
|
@@ -42,6 +41,7 @@ from virtool_workflow.runtime.redis import (
|
|
42
41
|
wait_for_cancellation,
|
43
42
|
)
|
44
43
|
from virtool_workflow.runtime.sentry import configure_sentry
|
44
|
+
from virtool_workflow.utils import get_virtool_workflow_version
|
45
45
|
from virtool_workflow.workflow import Workflow
|
46
46
|
|
47
47
|
logger = get_logger("runtime")
|
@@ -229,7 +229,7 @@ async def start_runtime(
|
|
229
229
|
|
230
230
|
logger.info(
|
231
231
|
"found virtool-workflow",
|
232
|
-
version=
|
232
|
+
version=get_virtool_workflow_version(),
|
233
233
|
)
|
234
234
|
|
235
235
|
workflow = workflow_loader()
|
@@ -239,7 +239,7 @@ async def start_runtime(
|
|
239
239
|
|
240
240
|
configure_sentry(sentry_dsn)
|
241
241
|
|
242
|
-
async with
|
242
|
+
async with Redis(redis_connection_string) as redis:
|
243
243
|
try:
|
244
244
|
job_id = await get_next_job_with_timeout(redis_list_name, redis, timeout)
|
245
245
|
except asyncio.TimeoutError:
|
@@ -277,7 +277,7 @@ async def start_runtime(
|
|
277
277
|
events.cancelled.set()
|
278
278
|
run_workflow_task.cancel()
|
279
279
|
|
280
|
-
async with
|
280
|
+
async with Redis(redis_connection_string) as redis:
|
281
281
|
cancellation_task = asyncio.create_task(
|
282
282
|
wait_for_cancellation(redis, job_id, cancel_workflow),
|
283
283
|
)
|
{virtool_workflow-6.0.0a11 → virtool_workflow-7.1.0}/virtool_workflow/runtime/run_subprocess.py
RENAMED
@@ -1,20 +1,21 @@
|
|
1
1
|
"""Code for running and managing subprocesses."""
|
2
|
+
|
2
3
|
import asyncio
|
3
4
|
from asyncio.subprocess import Process
|
4
5
|
from pathlib import Path
|
5
|
-
from typing import
|
6
|
+
from typing import Callable, Coroutine, Protocol
|
6
7
|
|
7
8
|
from pyfixtures import fixture
|
8
9
|
from structlog import get_logger
|
9
10
|
from virtool_core.utils import timestamp
|
10
11
|
|
11
|
-
from virtool_workflow.errors import
|
12
|
+
from virtool_workflow.errors import SubprocessFailedError
|
12
13
|
|
13
14
|
logger = get_logger("subprocess")
|
14
15
|
|
15
16
|
|
16
17
|
class LineOutputHandler(Protocol):
|
17
|
-
async def __call__(self, line:
|
18
|
+
async def __call__(self, line: bytes):
|
18
19
|
"""Handle input from stdin, or stderr, line by line.
|
19
20
|
|
20
21
|
:param line: A line of output from the stream.
|
@@ -45,7 +46,8 @@ class RunSubprocess(Protocol):
|
|
45
46
|
|
46
47
|
|
47
48
|
async def watch_pipe(
|
48
|
-
stream: asyncio.StreamReader,
|
49
|
+
stream: asyncio.StreamReader,
|
50
|
+
handler: LineOutputHandler,
|
49
51
|
):
|
50
52
|
"""Watch the stdout or stderr stream and pass lines to the `handler` callback function.
|
51
53
|
|
@@ -62,6 +64,21 @@ async def watch_pipe(
|
|
62
64
|
await handler(line)
|
63
65
|
|
64
66
|
|
67
|
+
def stderr_logger(line: bytes):
|
68
|
+
"""Log a line of stderr output and try to decode it as UTF-8.
|
69
|
+
|
70
|
+
If the line is not decodable, log it as a string.
|
71
|
+
|
72
|
+
:param line: a line of stderr output
|
73
|
+
"""
|
74
|
+
line = line.rstrip()
|
75
|
+
|
76
|
+
try:
|
77
|
+
logger.info("stderr", line=line.decode())
|
78
|
+
except UnicodeDecodeError:
|
79
|
+
logger.info("stderr", line=line)
|
80
|
+
|
81
|
+
|
65
82
|
async def _run_subprocess(
|
66
83
|
command: list[str],
|
67
84
|
stdout_handler: LineOutputHandler | None = None,
|
@@ -75,12 +92,17 @@ async def _run_subprocess(
|
|
75
92
|
|
76
93
|
stdout = asyncio.subprocess.PIPE if stdout_handler else asyncio.subprocess.DEVNULL
|
77
94
|
|
78
|
-
|
79
|
-
logger.info("stderr", line=line.rstrip())
|
95
|
+
if stderr_handler:
|
80
96
|
|
81
|
-
|
97
|
+
async def _stderr_handler(line):
|
98
|
+
stderr_logger(line)
|
82
99
|
await stderr_handler(line)
|
83
100
|
|
101
|
+
else:
|
102
|
+
|
103
|
+
async def _stderr_handler(line):
|
104
|
+
stderr_logger(line)
|
105
|
+
|
84
106
|
process = await asyncio.create_subprocess_exec(
|
85
107
|
*(str(arg) for arg in command),
|
86
108
|
cwd=cwd,
|
@@ -106,9 +128,12 @@ async def _run_subprocess(
|
|
106
128
|
|
107
129
|
process.terminate()
|
108
130
|
|
109
|
-
# Have to do this to avoid Event loop closed error.
|
131
|
+
# Have to do this in Python 3.10 to avoid Event loop closed error.
|
110
132
|
# https://github.com/python/cpython/issues/88050
|
111
|
-
|
133
|
+
try:
|
134
|
+
process._transport.close()
|
135
|
+
except AttributeError:
|
136
|
+
pass
|
112
137
|
|
113
138
|
await process.wait()
|
114
139
|
logger.info("subprocess exited", code=process.returncode)
|
@@ -122,7 +147,7 @@ async def _run_subprocess(
|
|
122
147
|
# Exit code 15 indicates that the process was terminated. This is expected
|
123
148
|
# when the workflow fails for some other reason, hence not an exception
|
124
149
|
if process.returncode not in [0, 15, -15]:
|
125
|
-
raise
|
150
|
+
raise SubprocessFailedError(
|
126
151
|
f"{command[0]} failed with exit code {process.returncode}\n"
|
127
152
|
f"arguments: {command}\n",
|
128
153
|
)
|
@@ -1,16 +1,16 @@
|
|
1
1
|
import logging
|
2
2
|
|
3
|
-
import pkg_resources
|
4
3
|
import sentry_sdk
|
5
4
|
from sentry_sdk.integrations.logging import LoggingIntegration
|
6
5
|
from structlog import get_logger
|
7
6
|
|
7
|
+
from virtool_workflow.utils import get_virtool_workflow_version
|
8
|
+
|
8
9
|
logger = get_logger("runtime")
|
9
10
|
|
10
11
|
|
11
12
|
def configure_sentry(dsn: str):
|
12
|
-
"""Initialize Sentry for log aggregation.
|
13
|
-
"""
|
13
|
+
"""Initialize Sentry for log aggregation."""
|
14
14
|
if dsn:
|
15
15
|
logger.info("initializing sentry", dsn=f"{dsn[:15]}...")
|
16
16
|
|
@@ -21,7 +21,7 @@ def configure_sentry(dsn: str):
|
|
21
21
|
event_level=logging.WARNING,
|
22
22
|
),
|
23
23
|
],
|
24
|
-
release=
|
24
|
+
release=get_virtool_workflow_version(),
|
25
25
|
traces_sample_rate=0.2,
|
26
26
|
)
|
27
27
|
else:
|
@@ -2,6 +2,7 @@ import asyncio
|
|
2
2
|
import tarfile
|
3
3
|
from collections.abc import Callable
|
4
4
|
from functools import wraps
|
5
|
+
from importlib import metadata
|
5
6
|
from inspect import iscoroutinefunction
|
6
7
|
from pathlib import Path
|
7
8
|
|
@@ -18,6 +19,14 @@ def coerce_to_coroutine_function(func: Callable):
|
|
18
19
|
return _func
|
19
20
|
|
20
21
|
|
22
|
+
def get_virtool_workflow_version() -> str:
|
23
|
+
"""Get the version of the installed virtool-workflow package."""
|
24
|
+
try:
|
25
|
+
return metadata.version("virtool-workflow")
|
26
|
+
except metadata.PackageNotFoundError:
|
27
|
+
return "0.0.0"
|
28
|
+
|
29
|
+
|
21
30
|
async def make_directory(path: Path):
|
22
31
|
await asyncio.to_thread(path.mkdir, exist_ok=True, parents=True)
|
23
32
|
|
@@ -1,119 +0,0 @@
|
|
1
|
-
Metadata-Version: 2.1
|
2
|
-
Name: virtool-workflow
|
3
|
-
Version: 6.0.0a11
|
4
|
-
Summary: A framework for developing bioinformatics workflows for Virtool.
|
5
|
-
Home-page: https://github.com/virtool/virtool-workflow
|
6
|
-
License: MIT
|
7
|
-
Author: Ian Boyes
|
8
|
-
Requires-Python: >=3.10,<3.11
|
9
|
-
Classifier: License :: OSI Approved :: MIT License
|
10
|
-
Classifier: Programming Language :: Python :: 3
|
11
|
-
Classifier: Programming Language :: Python :: 3.10
|
12
|
-
Classifier: Topic :: Software Development :: Libraries
|
13
|
-
Requires-Dist: aiofiles (>=0.7.0,<0.8.0)
|
14
|
-
Requires-Dist: aiohttp (>=3.8.1,<4.0.0)
|
15
|
-
Requires-Dist: aioredis (==1.3.1)
|
16
|
-
Requires-Dist: biopython (>=1.81,<2.0)
|
17
|
-
Requires-Dist: click (>=8.1.7,<9.0.0)
|
18
|
-
Requires-Dist: orjson (>=3.9.9,<4.0.0)
|
19
|
-
Requires-Dist: pydantic-factories (>=1.17.3,<2.0.0)
|
20
|
-
Requires-Dist: pyfixtures (>=1.0.0,<2.0.0)
|
21
|
-
Requires-Dist: sentry-sdk (>=1.5.7,<2.0.0)
|
22
|
-
Requires-Dist: virtool-core (>=11.0.0,<12.0.0)
|
23
|
-
Project-URL: Repository, https://github.com/virtool/virtool-workflow
|
24
|
-
Description-Content-Type: text/markdown
|
25
|
-
|
26
|
-
# Virtool Workflow
|
27
|
-
|
28
|
-

|
29
|
-
[](https://badge.fury.io/py/virtool-workflow)
|
30
|
-
|
31
|
-
A framework for developing bioinformatic workflows in Python.
|
32
|
-
|
33
|
-
```python
|
34
|
-
from virtool_workflow import step
|
35
|
-
|
36
|
-
|
37
|
-
@step
|
38
|
-
def step_function():
|
39
|
-
...
|
40
|
-
|
41
|
-
|
42
|
-
@step
|
43
|
-
def step_function_2():
|
44
|
-
...
|
45
|
-
```
|
46
|
-
|
47
|
-
- [Documentation](https://workflow.virtool.ca)
|
48
|
-
- [Website](https://www.virtool.ca/)
|
49
|
-
|
50
|
-
## Contributing
|
51
|
-
|
52
|
-
### Commits
|
53
|
-
|
54
|
-
All commits must follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0) specification.
|
55
|
-
|
56
|
-
These standardized commit messages are used to automatically publish releases using [`semantic-release`](https://semantic-release.gitbook.io/semantic-release)
|
57
|
-
after commits are merged to `main` from successful PRs.
|
58
|
-
|
59
|
-
**Example**
|
60
|
-
|
61
|
-
```text
|
62
|
-
feat: add API support for assigning labels to existing samples
|
63
|
-
```
|
64
|
-
|
65
|
-
Descriptive bodies and footers are required where necessary to describe the impact of the commit. Use bullets where appropriate.
|
66
|
-
|
67
|
-
Additional Requirements
|
68
|
-
|
69
|
-
1. **Write in the imperative**. For example, _"fix bug"_, not _"fixed bug"_ or _"fixes bug"_.
|
70
|
-
2. **Don't refer to issues or code reviews**. For example, don't write something like this: _"make style changes requested in review"_.
|
71
|
-
Instead, _"update styles to improve accessibility"_.
|
72
|
-
3. **Commits are not your personal journal**. For example, don't write something like this: _"got server running again"_
|
73
|
-
or _"oops. fixed my code smell"_.
|
74
|
-
|
75
|
-
From Tim Pope: [A Note About Git Commit Messages](https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)
|
76
|
-
|
77
|
-
### Tests
|
78
|
-
|
79
|
-
[Pytest](https://docs.pytest.org/en/7.1.x/ "Pytest") is used to implement unit
|
80
|
-
and integration tests.
|
81
|
-
|
82
|
-
A pytest plugin,
|
83
|
-
[pytest-docker-compose](https://github.com/pytest-docker-compose/pytest-docker-compose)
|
84
|
-
handles starting and stopping any required external services for integration
|
85
|
-
tests. [docker-compose](https://docs.docker.com/compose/) will need to be
|
86
|
-
installed on your system for this to work. It might also be necessary to setup a
|
87
|
-
`docker` user group on your system, so you can [use docker without
|
88
|
-
sudo](https://linoxide.com/use-docker-without-sudo-ubuntu/).
|
89
|
-
|
90
|
-
`virtool-workflow` depends on some external bioinformatics tools such as [Bowtie
|
91
|
-
2](http://bowtie-bio.sourceforge.net/bowtie2/index.shtml),
|
92
|
-
[FastQC](https://www.bioinformatics.babraham.ac.uk/projects/fastqc/), and
|
93
|
-
[Skewer](https://github.com/relipmoc/skewer). Installation of these tools can be
|
94
|
-
somewhat involved, so it's best to run the test suite using `docker`. The
|
95
|
-
[virtool/workflow-tools](https://github.com/virtool/workflow-tools) image
|
96
|
-
provides a base with all of the external dependencies pre-installed.
|
97
|
-
|
98
|
-
[./tests/docker-compose.yml](./tests/docker-compose.yml) will run the test suite
|
99
|
-
inside a container based on
|
100
|
-
[virtool/workflow-tools](https://github.com/virtool/workflow-tools) and mount
|
101
|
-
the local docker socket so that `pytest`, running inside the container, can
|
102
|
-
manage the other services required by the integration tests.
|
103
|
-
|
104
|
-
To run the entire test suite:
|
105
|
-
|
106
|
-
```sh
|
107
|
-
cd tests
|
108
|
-
docker-compose up --exit-code-from pytest
|
109
|
-
```
|
110
|
-
|
111
|
-
To run a subset of the tests, `tests/integration` only for example:
|
112
|
-
|
113
|
-
```sh
|
114
|
-
cd tests
|
115
|
-
TEST_PATH=tests/integration docker-compose up --exit-code-from pytest
|
116
|
-
```
|
117
|
-
|
118
|
-
:warning: The `TEST_PATH` is a relative path from the repository root, not the `tests` directory.
|
119
|
-
|
@@ -1,93 +0,0 @@
|
|
1
|
-
# Virtool Workflow
|
2
|
-
|
3
|
-

|
4
|
-
[](https://badge.fury.io/py/virtool-workflow)
|
5
|
-
|
6
|
-
A framework for developing bioinformatic workflows in Python.
|
7
|
-
|
8
|
-
```python
|
9
|
-
from virtool_workflow import step
|
10
|
-
|
11
|
-
|
12
|
-
@step
|
13
|
-
def step_function():
|
14
|
-
...
|
15
|
-
|
16
|
-
|
17
|
-
@step
|
18
|
-
def step_function_2():
|
19
|
-
...
|
20
|
-
```
|
21
|
-
|
22
|
-
- [Documentation](https://workflow.virtool.ca)
|
23
|
-
- [Website](https://www.virtool.ca/)
|
24
|
-
|
25
|
-
## Contributing
|
26
|
-
|
27
|
-
### Commits
|
28
|
-
|
29
|
-
All commits must follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0) specification.
|
30
|
-
|
31
|
-
These standardized commit messages are used to automatically publish releases using [`semantic-release`](https://semantic-release.gitbook.io/semantic-release)
|
32
|
-
after commits are merged to `main` from successful PRs.
|
33
|
-
|
34
|
-
**Example**
|
35
|
-
|
36
|
-
```text
|
37
|
-
feat: add API support for assigning labels to existing samples
|
38
|
-
```
|
39
|
-
|
40
|
-
Descriptive bodies and footers are required where necessary to describe the impact of the commit. Use bullets where appropriate.
|
41
|
-
|
42
|
-
Additional Requirements
|
43
|
-
|
44
|
-
1. **Write in the imperative**. For example, _"fix bug"_, not _"fixed bug"_ or _"fixes bug"_.
|
45
|
-
2. **Don't refer to issues or code reviews**. For example, don't write something like this: _"make style changes requested in review"_.
|
46
|
-
Instead, _"update styles to improve accessibility"_.
|
47
|
-
3. **Commits are not your personal journal**. For example, don't write something like this: _"got server running again"_
|
48
|
-
or _"oops. fixed my code smell"_.
|
49
|
-
|
50
|
-
From Tim Pope: [A Note About Git Commit Messages](https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)
|
51
|
-
|
52
|
-
### Tests
|
53
|
-
|
54
|
-
[Pytest](https://docs.pytest.org/en/7.1.x/ "Pytest") is used to implement unit
|
55
|
-
and integration tests.
|
56
|
-
|
57
|
-
A pytest plugin,
|
58
|
-
[pytest-docker-compose](https://github.com/pytest-docker-compose/pytest-docker-compose)
|
59
|
-
handles starting and stopping any required external services for integration
|
60
|
-
tests. [docker-compose](https://docs.docker.com/compose/) will need to be
|
61
|
-
installed on your system for this to work. It might also be necessary to setup a
|
62
|
-
`docker` user group on your system, so you can [use docker without
|
63
|
-
sudo](https://linoxide.com/use-docker-without-sudo-ubuntu/).
|
64
|
-
|
65
|
-
`virtool-workflow` depends on some external bioinformatics tools such as [Bowtie
|
66
|
-
2](http://bowtie-bio.sourceforge.net/bowtie2/index.shtml),
|
67
|
-
[FastQC](https://www.bioinformatics.babraham.ac.uk/projects/fastqc/), and
|
68
|
-
[Skewer](https://github.com/relipmoc/skewer). Installation of these tools can be
|
69
|
-
somewhat involved, so it's best to run the test suite using `docker`. The
|
70
|
-
[virtool/workflow-tools](https://github.com/virtool/workflow-tools) image
|
71
|
-
provides a base with all of the external dependencies pre-installed.
|
72
|
-
|
73
|
-
[./tests/docker-compose.yml](./tests/docker-compose.yml) will run the test suite
|
74
|
-
inside a container based on
|
75
|
-
[virtool/workflow-tools](https://github.com/virtool/workflow-tools) and mount
|
76
|
-
the local docker socket so that `pytest`, running inside the container, can
|
77
|
-
manage the other services required by the integration tests.
|
78
|
-
|
79
|
-
To run the entire test suite:
|
80
|
-
|
81
|
-
```sh
|
82
|
-
cd tests
|
83
|
-
docker-compose up --exit-code-from pytest
|
84
|
-
```
|
85
|
-
|
86
|
-
To run a subset of the tests, `tests/integration` only for example:
|
87
|
-
|
88
|
-
```sh
|
89
|
-
cd tests
|
90
|
-
TEST_PATH=tests/integration docker-compose up --exit-code-from pytest
|
91
|
-
```
|
92
|
-
|
93
|
-
:warning: The `TEST_PATH` is a relative path from the repository root, not the `tests` directory.
|
@@ -1,61 +0,0 @@
|
|
1
|
-
from subprocess import SubprocessError
|
2
|
-
|
3
|
-
|
4
|
-
class IllegalJobArguments(ValueError):
|
5
|
-
"""The `job.args` dict is in an illegal state."""
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
class InsufficientJobRights(Exception):
|
10
|
-
...
|
11
|
-
|
12
|
-
|
13
|
-
class JobAlreadyAcquired(Exception):
|
14
|
-
def __init__(self, job_id: str):
|
15
|
-
super(JobAlreadyAcquired, self).__init__(
|
16
|
-
f"Job {job_id} is has already been acquired.",
|
17
|
-
)
|
18
|
-
|
19
|
-
|
20
|
-
class JobAlreadyFinalized(Exception):
|
21
|
-
...
|
22
|
-
|
23
|
-
|
24
|
-
class JobsAPIError(Exception):
|
25
|
-
"""A base exception for errors due to HTTP errors from the jobs API."""
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
class JobsAPIBadRequest(JobsAPIError):
|
30
|
-
"""A ``400 Bad Request`` response from the jobs API."""
|
31
|
-
|
32
|
-
status = 400
|
33
|
-
|
34
|
-
|
35
|
-
class JobsAPIForbidden(JobsAPIError):
|
36
|
-
status = 403
|
37
|
-
|
38
|
-
|
39
|
-
class JobsAPINotFound(JobsAPIError):
|
40
|
-
status = 404
|
41
|
-
|
42
|
-
|
43
|
-
class JobsAPIConflict(JobsAPIError):
|
44
|
-
status = 409
|
45
|
-
|
46
|
-
|
47
|
-
class JobsAPIServerError(JobsAPIError):
|
48
|
-
status = 500
|
49
|
-
|
50
|
-
|
51
|
-
class MissingJobArgument(ValueError):
|
52
|
-
"""The `job.args` dict is missing a required key for some funcionality."""
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
class NotFound(KeyError):
|
57
|
-
...
|
58
|
-
|
59
|
-
|
60
|
-
class SubprocessFailed(SubprocessError):
|
61
|
-
"""Subprocess exited with non-zero status during a workflow."""
|
@@ -1,24 +0,0 @@
|
|
1
|
-
from dataclasses import dataclass
|
2
|
-
from datetime import datetime
|
3
|
-
from typing import Literal
|
4
|
-
|
5
|
-
VirtoolFileFormat = Literal[
|
6
|
-
"sam",
|
7
|
-
"bam",
|
8
|
-
"fasta",
|
9
|
-
"fastq",
|
10
|
-
"csv",
|
11
|
-
"tsv",
|
12
|
-
"json",
|
13
|
-
"unknown",
|
14
|
-
]
|
15
|
-
|
16
|
-
|
17
|
-
@dataclass
|
18
|
-
class VirtoolFile:
|
19
|
-
id: int
|
20
|
-
name: str
|
21
|
-
size: int
|
22
|
-
format: VirtoolFileFormat
|
23
|
-
name_on_disk: str = None
|
24
|
-
uploaded_at: datetime = None
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{virtool_workflow-6.0.0a11 → virtool_workflow-7.1.0}/virtool_workflow/pytest_plugin/__init__.py
RENAMED
File without changes
|
{virtool_workflow-6.0.0a11 → virtool_workflow-7.1.0}/virtool_workflow/pytest_plugin/utils.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|