virtool-workflow 0.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. virtool_workflow/__init__.py +13 -0
  2. virtool_workflow/analysis/__init__.py +1 -0
  3. virtool_workflow/analysis/fastqc.py +467 -0
  4. virtool_workflow/analysis/skewer.py +265 -0
  5. virtool_workflow/analysis/trimming.py +56 -0
  6. virtool_workflow/analysis/utils.py +27 -0
  7. virtool_workflow/api/__init__.py +0 -0
  8. virtool_workflow/api/acquire.py +66 -0
  9. virtool_workflow/api/client.py +132 -0
  10. virtool_workflow/api/utils.py +109 -0
  11. virtool_workflow/cli.py +66 -0
  12. virtool_workflow/data/__init__.py +22 -0
  13. virtool_workflow/data/analyses.py +106 -0
  14. virtool_workflow/data/hmms.py +109 -0
  15. virtool_workflow/data/indexes.py +319 -0
  16. virtool_workflow/data/jobs.py +62 -0
  17. virtool_workflow/data/ml.py +82 -0
  18. virtool_workflow/data/samples.py +190 -0
  19. virtool_workflow/data/subtractions.py +244 -0
  20. virtool_workflow/data/uploads.py +35 -0
  21. virtool_workflow/decorators.py +47 -0
  22. virtool_workflow/errors.py +62 -0
  23. virtool_workflow/files.py +40 -0
  24. virtool_workflow/hooks.py +140 -0
  25. virtool_workflow/pytest_plugin/__init__.py +35 -0
  26. virtool_workflow/pytest_plugin/data.py +197 -0
  27. virtool_workflow/pytest_plugin/utils.py +9 -0
  28. virtool_workflow/runtime/__init__.py +0 -0
  29. virtool_workflow/runtime/config.py +21 -0
  30. virtool_workflow/runtime/discover.py +95 -0
  31. virtool_workflow/runtime/events.py +7 -0
  32. virtool_workflow/runtime/hook.py +129 -0
  33. virtool_workflow/runtime/path.py +19 -0
  34. virtool_workflow/runtime/ping.py +54 -0
  35. virtool_workflow/runtime/redis.py +65 -0
  36. virtool_workflow/runtime/run.py +276 -0
  37. virtool_workflow/runtime/run_subprocess.py +168 -0
  38. virtool_workflow/runtime/sentry.py +28 -0
  39. virtool_workflow/utils.py +90 -0
  40. virtool_workflow/workflow.py +90 -0
  41. virtool_workflow-0.0.0.dist-info/LICENSE +21 -0
  42. virtool_workflow-0.0.0.dist-info/METADATA +71 -0
  43. virtool_workflow-0.0.0.dist-info/RECORD +45 -0
  44. virtool_workflow-0.0.0.dist-info/WHEEL +4 -0
  45. virtool_workflow-0.0.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,82 @@
1
+ """A fixture and dataclass for working with machine learning models in workflows."""
2
+
3
+ import asyncio
4
+ import shutil
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+
8
+ from pyfixtures import fixture
9
+ from structlog import get_logger
10
+ from virtool.ml.models import MLModelRelease
11
+
12
+ from virtool_workflow.api.client import APIClient
13
+ from virtool_workflow.data.analyses import WFAnalysis
14
+ from virtool_workflow.utils import make_directory, move_all_model_files, untar
15
+
16
+ logger = get_logger("api")
17
+
18
+
19
+ @dataclass
20
+ class WFMLModelRelease:
21
+ """A machine learning model.
22
+
23
+ This class represents a machine learning model and the selected release of that
24
+ model in the workflow.
25
+ """
26
+
27
+ id: int
28
+ """The unique ID for the model release."""
29
+
30
+ name: str
31
+ """The name of the model release."""
32
+
33
+ path: Path
34
+ """The path to the model directory."""
35
+
36
+ @property
37
+ def file_path(self) -> Path:
38
+ """The path to the model data file."""
39
+ return self.path / "model.tar.gz"
40
+
41
+
42
+ @fixture
43
+ async def ml(
44
+ _api: APIClient,
45
+ analysis: WFAnalysis,
46
+ work_path: Path,
47
+ ) -> WFMLModelRelease | None:
48
+ if analysis.ml is None:
49
+ return None
50
+
51
+ model_id = analysis.ml.model.id
52
+ model_release_id = analysis.ml.id
53
+
54
+ log = logger.bind(model_id=analysis.ml.id, model_release_id=model_release_id)
55
+
56
+ model_release_json = await _api.get_json(
57
+ f"/ml/{model_id}/releases/{model_release_id}",
58
+ )
59
+ model_release = MLModelRelease(**model_release_json)
60
+
61
+ log.info("fetched ml model release json")
62
+
63
+ release = WFMLModelRelease(
64
+ id=model_release.id,
65
+ name=model_release.name,
66
+ path=work_path / "ml" / str(model_release.model.id) / str(model_release_id),
67
+ )
68
+
69
+ await make_directory(release.path)
70
+
71
+ await _api.get_file(
72
+ f"/ml/{model_id}/releases/{model_release_id}/model.tar.gz",
73
+ release.file_path,
74
+ )
75
+
76
+ await asyncio.to_thread(untar, release.file_path, release.path)
77
+ await asyncio.to_thread(move_all_model_files, release.path / "model", release.path)
78
+ await asyncio.to_thread(shutil.rmtree, release.path / "model")
79
+
80
+ log.info("downloaded ml model release file")
81
+
82
+ return release
@@ -0,0 +1,190 @@
1
+ import asyncio
2
+ from collections.abc import Callable, Coroutine
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from pyfixtures import fixture
8
+ from structlog import get_logger
9
+ from virtool.jobs.models import Job
10
+ from virtool.models.enums import LibraryType
11
+ from virtool.samples.models import Quality, Sample
12
+
13
+ from virtool_workflow.analysis.utils import ReadPaths
14
+ from virtool_workflow.api.client import APIClient
15
+ from virtool_workflow.data.uploads import WFUploads
16
+ from virtool_workflow.errors import JobsAPINotFoundError
17
+ from virtool_workflow.files import VirtoolFileFormat
18
+
19
+ logger = get_logger("api")
20
+
21
+
22
+ @dataclass
23
+ class WFSample:
24
+ """A sample whose data is being used in a workflow."""
25
+
26
+ id: str
27
+ """The unique ID of the sample."""
28
+
29
+ library_type: LibraryType
30
+ """The library type of the sample."""
31
+
32
+ name: str
33
+ """The sample's name."""
34
+
35
+ paired: bool
36
+ """Whether the sample consists of paired reads."""
37
+
38
+ quality: Quality
39
+ """The quality data for the sample."""
40
+
41
+ read_paths: ReadPaths
42
+ """The paths to the raw sample reads."""
43
+
44
+ @property
45
+ def min_length(self) -> int | None:
46
+ """The minimum observed read length in the sample sequencing data.
47
+
48
+ Returns ``None`` if the sample is still being created and no quality data is available.
49
+
50
+ """
51
+ return self.quality.length[0] if self.quality else None
52
+
53
+ @property
54
+ def max_length(self) -> int | None:
55
+ """The maximum observed read length in the sample sequencing data.
56
+
57
+ Returns ``None`` if the sample is still being created and no quality data is available.
58
+
59
+ """
60
+ return self.quality.length[1] if self.quality else None
61
+
62
+
63
+ @dataclass
64
+ class WFNewSampleUpload:
65
+ id: int
66
+ name: str
67
+ size: int
68
+ path: Path
69
+
70
+
71
+ @dataclass
72
+ class WFNewSample:
73
+ """A sample that is being created in the workflow."""
74
+
75
+ id: str
76
+ name: str
77
+ paired: bool
78
+ uploads: tuple[WFNewSampleUpload] | tuple[WFNewSampleUpload, WFNewSampleUpload]
79
+
80
+ delete: Callable[[], Coroutine[None, None, None]]
81
+ finalize: Callable[[dict[str, Any]], Coroutine[None, None, None]]
82
+ upload: Callable[[Path, VirtoolFileFormat], Coroutine[None, None, None]]
83
+
84
+
85
+ @fixture
86
+ async def sample(
87
+ _api: APIClient,
88
+ job: Job,
89
+ uploads: WFUploads,
90
+ work_path: Path,
91
+ ) -> WFSample:
92
+ """The sample associated with the current job."""
93
+ id_ = job.args["sample_id"]
94
+
95
+ base_url_path = f"/samples/{id_}"
96
+
97
+ try:
98
+ sample_json = await _api.get_json(base_url_path)
99
+ except JobsAPINotFoundError:
100
+ raise JobsAPINotFoundError("Sample not found")
101
+
102
+ sample = Sample(**sample_json)
103
+
104
+ reads_path = work_path / "reads"
105
+ await asyncio.to_thread(reads_path.mkdir, exist_ok=True, parents=True)
106
+
107
+ await _api.get_file(
108
+ f"{base_url_path}/reads/reads_1.fq.gz",
109
+ reads_path / "reads_1.fq.gz",
110
+ )
111
+
112
+ if sample.paired:
113
+ read_paths = (
114
+ reads_path / "reads_1.fq.gz",
115
+ reads_path / "reads_2.fq.gz",
116
+ )
117
+ await _api.get_file(
118
+ f"{base_url_path}/reads/reads_2.fq.gz",
119
+ reads_path / "reads_2.fq.gz",
120
+ )
121
+ else:
122
+ read_paths = (reads_path / "reads_1.fq.gz",)
123
+
124
+ return WFSample(
125
+ id=sample.id,
126
+ library_type=sample.library_type,
127
+ name=sample.name,
128
+ paired=sample.paired,
129
+ quality=sample.quality,
130
+ read_paths=read_paths,
131
+ )
132
+
133
+
134
+ @fixture
135
+ async def new_sample(
136
+ _api: APIClient,
137
+ job: Job,
138
+ uploads: WFUploads,
139
+ work_path: Path,
140
+ ) -> WFNewSample:
141
+ """The sample associated with the current job."""
142
+ id_ = job.args["sample_id"]
143
+
144
+ log = logger.bind(resource="sample", id=id_)
145
+ log.info("loading sample for sample creation")
146
+
147
+ base_url_path = f"/samples/{id_}"
148
+
149
+ sample_dict = await _api.get_json(base_url_path)
150
+ sample = Sample(**sample_dict)
151
+
152
+ log.info("got sample json")
153
+
154
+ uploads_path = work_path / "uploads"
155
+ await asyncio.to_thread(uploads_path.mkdir, exist_ok=True, parents=True)
156
+
157
+ log.info("created uploads directory")
158
+
159
+ files = tuple(
160
+ WFNewSampleUpload(
161
+ id=f["id"],
162
+ name=f["name"],
163
+ path=Path(uploads_path / f["name"]),
164
+ size=f["size"],
165
+ )
166
+ for f in job.args["files"]
167
+ )
168
+
169
+ await asyncio.gather(*[uploads.download(f.id, f.path) for f in files])
170
+
171
+ log.info("downloaded sample files")
172
+
173
+ async def finalize(quality: dict[str, Any]):
174
+ await _api.patch_json(base_url_path, data={"quality": quality})
175
+
176
+ async def delete():
177
+ await _api.delete(base_url_path)
178
+
179
+ async def upload(path: Path, fmt: VirtoolFileFormat = "fastq"):
180
+ await _api.put_file(f"{base_url_path}/reads/{path.name}", path, "fastq")
181
+
182
+ return WFNewSample(
183
+ id=sample.id,
184
+ delete=delete,
185
+ finalize=finalize,
186
+ name=sample.name,
187
+ paired=sample.paired,
188
+ upload=upload,
189
+ uploads=files,
190
+ )
@@ -0,0 +1,244 @@
1
+ import asyncio
2
+ from collections.abc import Callable, Coroutine
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+
6
+ from pyfixtures import fixture
7
+ from structlog import get_logger
8
+ from virtool.jobs.models import Job
9
+ from virtool.subtractions.models import (
10
+ NucleotideComposition,
11
+ Subtraction,
12
+ SubtractionFile,
13
+ )
14
+
15
+ from virtool_workflow.api.client import APIClient
16
+ from virtool_workflow.data.analyses import WFAnalysis
17
+ from virtool_workflow.data.uploads import WFUploads
18
+ from virtool_workflow.errors import MissingJobArgumentError
19
+
20
+ logger = get_logger("api")
21
+
22
+
23
+ @dataclass
24
+ class WFSubtraction:
25
+ """A Virtool subtraction that has been loaded into the workflow environment.
26
+
27
+ The subtraction files are downloaded to the workflow's local work path so they can
28
+ be used for analysis.
29
+ """
30
+
31
+ id: str
32
+ """The unique ID for the subtraction."""
33
+
34
+ files: list[SubtractionFile]
35
+ """The files associated with the subtraction."""
36
+
37
+ gc: NucleotideComposition
38
+ """The nucleotide composition of the subtraction."""
39
+
40
+ nickname: str
41
+ """The nickname for the subtraction."""
42
+
43
+ name: str
44
+ """The display name for the subtraction."""
45
+
46
+ path: Path
47
+ """
48
+ The path to the subtraction directory.
49
+
50
+ The subtraction directory contains the FASTA and Bowtie2 files for the subtraction.
51
+ """
52
+
53
+ @property
54
+ def fasta_path(self) -> Path:
55
+ """The path to the gzipped FASTA file for the subtraction."""
56
+ return self.path / "subtraction.fa.gz"
57
+
58
+ @property
59
+ def bowtie2_index_path(self) -> Path:
60
+ """The path to Bowtie2 prefix in the running workflow's work_path
61
+
62
+ For example, ``/work/subtractions/<id>/subtraction`` refers to the Bowtie2
63
+ index that comprises the files:
64
+
65
+ - ``/work/subtractions/<id>/subtraction.1.bt2``
66
+ - ``/work/subtractions/<id>/subtraction.2.bt2``
67
+ - ``/work/subtractions/<id>/subtraction.3.bt2``
68
+ - ``/work/subtractions/<id>/subtraction.4.bt2``
69
+ - ``/work/subtractions/<id>/subtraction.rev.1.bt2``
70
+ - ``/work/subtractions/<id>/subtraction.rev.2.bt2``
71
+
72
+ """
73
+ return self.path / "subtraction"
74
+
75
+
76
+ @dataclass
77
+ class WFNewSubtraction:
78
+ id: str
79
+ """The unique ID for the subtraction."""
80
+
81
+ delete: Callable[[], Coroutine[None, None, None]]
82
+ """
83
+ A callable that deletes the subtraction from Virtool.
84
+
85
+ This should be called if the subtraction creation fails before the subtraction is
86
+ finalized.
87
+ """
88
+
89
+ finalize: Callable[[dict[str, int | float], int], Coroutine[None, None, None]]
90
+ """
91
+ A callable that finalizes the subtraction in Virtool.
92
+
93
+ This makes it impossible to further alter the files and ready state of the
94
+ subtraction. This must be called before the workflow ends to make the subtraction
95
+ usable.
96
+ """
97
+
98
+ name: str
99
+ """The display name for the subtraction."""
100
+
101
+ nickname: str
102
+ """An optional nickname for the subtraction."""
103
+
104
+ path: Path
105
+ """
106
+ The path to the subtraction directory.
107
+
108
+ The data files for the subtraction should be created here.
109
+ """
110
+
111
+ upload: Callable[[Path], Coroutine[None, None, None]]
112
+
113
+ @property
114
+ def fasta_path(self) -> Path:
115
+ """The path to the FASTA file that should be used to create the subtraction."""
116
+ return self.path / "subtraction.fa.gz"
117
+
118
+
119
+ @fixture
120
+ async def subtractions(
121
+ _api: APIClient,
122
+ analysis: WFAnalysis,
123
+ work_path: Path,
124
+ ) -> list[WFSubtraction]:
125
+ """The subtractions to be used for the current analysis job."""
126
+ subtraction_work_path = work_path / "subtractions"
127
+ await asyncio.to_thread(subtraction_work_path.mkdir)
128
+
129
+ subtractions_ = []
130
+
131
+ for subtraction_id in [s.id for s in analysis.subtractions]:
132
+ subtraction_json = await _api.get_json(f"/subtractions/{subtraction_id}")
133
+ subtraction = Subtraction(**subtraction_json)
134
+ subtraction = WFSubtraction(
135
+ id=subtraction.id,
136
+ files=subtraction.files,
137
+ gc=subtraction.gc,
138
+ nickname=subtraction.nickname,
139
+ name=subtraction.name,
140
+ path=subtraction_work_path / subtraction.id,
141
+ )
142
+
143
+ await asyncio.to_thread(subtraction.path.mkdir, parents=True, exist_ok=True)
144
+
145
+ subtractions_.append(subtraction)
146
+
147
+ # Do this in a separate loop in case fetching the JSON fails. This prevents
148
+ # expensive and unnecessary file downloads.
149
+ for subtraction in subtractions_:
150
+ logger.info("downloading subtraction files", id=subtraction.id)
151
+
152
+ for subtraction_file in subtraction.files:
153
+ await _api.get_file(
154
+ f"/subtractions/{subtraction.id}/files/{subtraction_file.name}",
155
+ subtraction.path / subtraction_file.name,
156
+ )
157
+
158
+ return subtractions_
159
+
160
+
161
+ @fixture
162
+ async def new_subtraction(
163
+ _api: APIClient,
164
+ job: Job,
165
+ uploads: WFUploads,
166
+ work_path: Path,
167
+ ) -> WFNewSubtraction:
168
+ """A new subtraction that will be created during the current job.
169
+
170
+ Currently only used for the `create-subtraction` workflow.
171
+ """
172
+ try:
173
+ id_ = job.args["subtraction_id"]
174
+ except KeyError:
175
+ raise MissingJobArgumentError("subtraction_id")
176
+
177
+ try:
178
+ upload_id = job.args["files"][0]["id"]
179
+ except KeyError:
180
+ raise MissingJobArgumentError("files")
181
+
182
+ subtraction_json = await _api.get_json(f"/subtractions/{id_}")
183
+ subtraction_ = Subtraction(**subtraction_json)
184
+
185
+ subtraction_work_path = work_path / "subtractions" / subtraction_.id
186
+ await asyncio.to_thread(subtraction_work_path.mkdir, parents=True, exist_ok=True)
187
+
188
+ await uploads.download(upload_id, subtraction_work_path / "subtraction.fa.gz")
189
+
190
+ url_path = f"/subtractions/{subtraction_.id}"
191
+
192
+ async def delete():
193
+ """Delete the subtraction if the job fails."""
194
+ await _api.delete(f"/subtractions/{subtraction_.id}")
195
+
196
+ async def finalize(gc: dict[str, int | float], count: int):
197
+ """Finalize the subtraction by setting the gc.
198
+
199
+ :param gc: the nucleotide composition of the subtraction
200
+ :param count: the number of sequences in the FASTA file
201
+ :return: the updated subtraction.
202
+ """
203
+ gc_ = NucleotideComposition(**{"n": 0.0, **gc})
204
+
205
+ await _api.patch_json(url_path, {"gc": gc_.dict(), "count": count})
206
+
207
+ async def upload(path: Path):
208
+ """Upload a file relating to this subtraction.
209
+
210
+ Filenames must be one of:
211
+ - subtraction.fa.gz
212
+ - subtraction.1.bt2
213
+ - subtraction.2.bt2
214
+ - subtraction.3.bt2
215
+ - subtraction.4.bt2
216
+ - subtraction.rev.1.bt2
217
+ - subtraction.rev.2.bt2
218
+
219
+ :param path: The path to the file
220
+
221
+ """
222
+ filename = path.name
223
+
224
+ log = logger.bind(id=id_, filename=filename)
225
+
226
+ log.info("Uploading subtraction file")
227
+
228
+ await _api.put_file(
229
+ f"/subtractions/{subtraction_.id}/files/{filename}",
230
+ path,
231
+ "unknown",
232
+ )
233
+
234
+ log.info("Finished uploading subtraction file")
235
+
236
+ return WFNewSubtraction(
237
+ id=subtraction_.id,
238
+ name=subtraction_.name,
239
+ nickname=subtraction_.nickname,
240
+ path=subtraction_work_path,
241
+ delete=delete,
242
+ finalize=finalize,
243
+ upload=upload,
244
+ )
@@ -0,0 +1,35 @@
1
+ from dataclasses import dataclass
2
+ from pathlib import Path
3
+
4
+ from pyfixtures import fixture
5
+
6
+ from virtool_workflow.api.client import APIClient
7
+
8
+
9
+ @dataclass
10
+ class WFUploads:
11
+ def __init__(self, api: APIClient):
12
+ self._api = api
13
+
14
+ async def download(self, upload_id: int, path: Path):
15
+ """Download the upload with the given ID to the given path."""
16
+ await self._api.get_file(f"/uploads/{upload_id}", path)
17
+
18
+
19
+ @fixture
20
+ async def uploads(_api: APIClient) -> WFUploads:
21
+ """Provides access to files that have been uploaded to the Virtool instance.
22
+
23
+ Files can be downloaded into the workflow environment be calling
24
+ :meth:`.WFUploads.download`.
25
+
26
+ Example:
27
+ -------
28
+ .. code-block:: python
29
+
30
+ @step
31
+ async def step_one(uploads: WFUploads, work_path: Path):
32
+ await uploads.download(1, work_path / "file.txt")
33
+
34
+ """
35
+ return WFUploads(_api)
@@ -0,0 +1,47 @@
1
+ """Create Workflows by decorating module scope functions."""
2
+
3
+ from collections.abc import Callable
4
+ from types import ModuleType
5
+
6
+ from virtool_workflow.errors import WorkflowStepsError
7
+ from virtool_workflow.workflow import Workflow
8
+
9
+
10
+ def step(func: Callable | None = None, *, name: str | None = None) -> Callable:
11
+ """Mark a function as a workflow step function.
12
+
13
+ :param func: the workflow step function
14
+ :param name: the display name of the workflow step. A name
15
+ will be generated based on the function name if not provided.
16
+ """
17
+ if func is None:
18
+ return lambda _f: step(_f, name=name)
19
+
20
+ func.__workflow_marker__ = "step"
21
+ func.__workflow_step_props__ = {"name": name}
22
+
23
+ return func
24
+
25
+
26
+ def collect(module: ModuleType) -> Workflow:
27
+ """Build a :class:`.Workflow` object from a workflow module.
28
+
29
+ :param module: A workflow module
30
+ :return: A workflow object
31
+ """
32
+ workflow = Workflow()
33
+
34
+ markers = [
35
+ value
36
+ for value in module.__dict__.values()
37
+ if hasattr(value, "__workflow_marker__")
38
+ ]
39
+
40
+ for marked in markers:
41
+ if marked.__workflow_marker__ == "step":
42
+ workflow.step(marked, **marked.__workflow_step_props__)
43
+
44
+ if not workflow.steps:
45
+ raise WorkflowStepsError(str(module))
46
+
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."""