variantgrid-api 0.3.0__tar.gz → 1.1.1__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.
- {variantgrid_api-0.3.0/src/variantgrid_api.egg-info → variantgrid_api-1.1.1}/PKG-INFO +38 -2
- variantgrid_api-1.1.1/README.md +32 -0
- {variantgrid_api-0.3.0 → variantgrid_api-1.1.1}/pyproject.toml +5 -2
- variantgrid_api-1.1.1/src/variantgrid_api/api_client.py +251 -0
- {variantgrid_api-0.3.0 → variantgrid_api-1.1.1}/src/variantgrid_api/data_models.py +51 -20
- {variantgrid_api-0.3.0 → variantgrid_api-1.1.1/src/variantgrid_api.egg-info}/PKG-INFO +38 -2
- {variantgrid_api-0.3.0 → variantgrid_api-1.1.1}/src/variantgrid_api.egg-info/SOURCES.txt +5 -1
- variantgrid_api-1.1.1/src/variantgrid_api.egg-info/requires.txt +7 -0
- variantgrid_api-1.1.1/tests/test_api_client.py +228 -0
- variantgrid_api-1.1.1/tests/test_api_client_bulk.py +46 -0
- variantgrid_api-1.1.1/tests/test_api_client_validation.py +75 -0
- variantgrid_api-1.1.1/tests/test_data_models.py +11 -0
- variantgrid_api-0.3.0/README.md +0 -1
- variantgrid_api-0.3.0/src/variantgrid_api/api_client.py +0 -126
- variantgrid_api-0.3.0/src/variantgrid_api.egg-info/requires.txt +0 -2
- {variantgrid_api-0.3.0 → variantgrid_api-1.1.1}/LICENSE +0 -0
- {variantgrid_api-0.3.0 → variantgrid_api-1.1.1}/setup.cfg +0 -0
- {variantgrid_api-0.3.0 → variantgrid_api-1.1.1}/src/variantgrid_api.egg-info/dependency_links.txt +0 -0
- {variantgrid_api-0.3.0 → variantgrid_api-1.1.1}/src/variantgrid_api.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: variantgrid_api
|
|
3
|
-
Version:
|
|
3
|
+
Version: 1.1.1
|
|
4
4
|
Summary: A Python API client for VariantGrid
|
|
5
5
|
Author-email: Dave Lawrence <davmlaw@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -35,5 +35,41 @@ Description-Content-Type: text/markdown
|
|
|
35
35
|
License-File: LICENSE
|
|
36
36
|
Requires-Dist: requests
|
|
37
37
|
Requires-Dist: dataclasses-json
|
|
38
|
+
Provides-Extra: test
|
|
39
|
+
Requires-Dist: pytest>=8; extra == "test"
|
|
40
|
+
Requires-Dist: pytest-cov>=5; extra == "test"
|
|
41
|
+
Requires-Dist: responses; extra == "test"
|
|
42
|
+
Dynamic: license-file
|
|
38
43
|
|
|
39
44
|
# variantgrid_api
|
|
45
|
+
|
|
46
|
+
[](https://pypi.org/project/variantgrid_api/) [](https://pypi.org/project/variantgrid_api/)
|
|
47
|
+
|
|
48
|
+
Python API client for [VariantGrid](https://github.com/SACGF/variantgrid) Open source Variant database and analysis platform
|
|
49
|
+
|
|
50
|
+
See [changelog](https://github.com/SACGF/variantgrid_api/blob/main/CHANGELOG.md)
|
|
51
|
+
|
|
52
|
+
## Install
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
python3 -m pip install variantgrid_api
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Example
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
from variantgrid_api.api_client import VariantGridAPI
|
|
62
|
+
from variantgrid_api.data_models import EnrichmentKit
|
|
63
|
+
|
|
64
|
+
api = VariantGridAPI(server="https://variantgrid.com", api_token="YOUR_API_TOKEN")
|
|
65
|
+
enrichment_kit = EnrichmentKit(name="idt_haem", version=1)
|
|
66
|
+
result = api.create_enrichment_kit(enrichment_kit)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Testing
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
# Install required testing packages
|
|
73
|
+
python3 -m pip install -e ".[test]"
|
|
74
|
+
python3 -m pytest --cov=variantgrid_api
|
|
75
|
+
```
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# variantgrid_api
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/variantgrid_api/) [](https://pypi.org/project/variantgrid_api/)
|
|
4
|
+
|
|
5
|
+
Python API client for [VariantGrid](https://github.com/SACGF/variantgrid) Open source Variant database and analysis platform
|
|
6
|
+
|
|
7
|
+
See [changelog](https://github.com/SACGF/variantgrid_api/blob/main/CHANGELOG.md)
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
python3 -m pip install variantgrid_api
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Example
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
from variantgrid_api.api_client import VariantGridAPI
|
|
19
|
+
from variantgrid_api.data_models import EnrichmentKit
|
|
20
|
+
|
|
21
|
+
api = VariantGridAPI(server="https://variantgrid.com", api_token="YOUR_API_TOKEN")
|
|
22
|
+
enrichment_kit = EnrichmentKit(name="idt_haem", version=1)
|
|
23
|
+
result = api.create_enrichment_kit(enrichment_kit)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Testing
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
# Install required testing packages
|
|
30
|
+
python3 -m pip install -e ".[test]"
|
|
31
|
+
python3 -m pytest --cov=variantgrid_api
|
|
32
|
+
```
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "variantgrid_api"
|
|
7
|
-
version = "
|
|
7
|
+
version = "1.1.1"
|
|
8
8
|
description = "A Python API client for VariantGrid"
|
|
9
9
|
authors = [
|
|
10
10
|
{ name = "Dave Lawrence", email = "davmlaw@gmail.com" }
|
|
@@ -20,9 +20,12 @@ classifiers = [
|
|
|
20
20
|
]
|
|
21
21
|
dependencies = [
|
|
22
22
|
"requests",
|
|
23
|
-
"dataclasses-json"
|
|
23
|
+
"dataclasses-json",
|
|
24
24
|
]
|
|
25
25
|
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
test = ["pytest>=8", "pytest-cov>=5", "responses"]
|
|
28
|
+
|
|
26
29
|
[project.urls]
|
|
27
30
|
"Homepage" = "https://github.com/SACGF/variantgrid_api"
|
|
28
31
|
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import urllib
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import List, Optional, Callable
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
|
|
10
|
+
from variantgrid_api.data_models import EnrichmentKit, SequencingRun, SampleSheet, SampleSheetCombinedVCFFile, \
|
|
11
|
+
SampleSheetLookup, SequencingFile, QCGeneList, QCExecStats, QCGeneCoverage, SequencerModel, Sequencer
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DateTimeEncoder(json.JSONEncoder):
|
|
15
|
+
def default(self,o):
|
|
16
|
+
if isinstance(o,(datetime.date, datetime.datetime)):
|
|
17
|
+
return o.isoformat()
|
|
18
|
+
return super().default(o)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class EmptyInputPolicy(Enum):
|
|
22
|
+
IGNORE = "ignore"
|
|
23
|
+
WARN = "warn"
|
|
24
|
+
ERROR = "error"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class VariantGridAPI:
|
|
28
|
+
def __init__(self, server, api_token,
|
|
29
|
+
empty_input_policy=EmptyInputPolicy.ERROR,
|
|
30
|
+
logger: Optional[logging.Logger] = None,
|
|
31
|
+
log_request=False, log_response=False
|
|
32
|
+
):
|
|
33
|
+
self.server = server
|
|
34
|
+
self.headers = {"Authorization": f"Token {api_token}"}
|
|
35
|
+
if logger is None:
|
|
36
|
+
logger = logging.getLogger(__name__)
|
|
37
|
+
self.logger = logger
|
|
38
|
+
self.validation_handler = self._get_validation_handler(empty_input_policy, logger)
|
|
39
|
+
self.log_request = log_request
|
|
40
|
+
self.log_response = log_response
|
|
41
|
+
|
|
42
|
+
def _get_url(self, url):
|
|
43
|
+
return urllib.parse.urljoin(self.server, url)
|
|
44
|
+
|
|
45
|
+
def _post(self, path, json_data):
|
|
46
|
+
url = self._get_url(path)
|
|
47
|
+
json_string = json.dumps(json_data, cls=DateTimeEncoder)
|
|
48
|
+
if self.log_request:
|
|
49
|
+
self.logger.info("POST to '%s', JSON: %s", url, json_string)
|
|
50
|
+
response = requests.post(url,
|
|
51
|
+
headers={**self.headers, "Content-Type": "application/json"},
|
|
52
|
+
data=json_string)
|
|
53
|
+
|
|
54
|
+
extra_error_message = f"{url=}, {json_string=}"
|
|
55
|
+
return self._handle_json_response(response, extra_error_message)
|
|
56
|
+
|
|
57
|
+
def _handle_json_response(self, response, extra_error_message: Optional[str] = None):
|
|
58
|
+
try:
|
|
59
|
+
json_response = response.json()
|
|
60
|
+
if self.log_response:
|
|
61
|
+
self.logger.info("Response from '%s', JSON: %s", response.url, json_response)
|
|
62
|
+
except Exception as e:
|
|
63
|
+
json_response = f"Couldn't convert JSON: {e}"
|
|
64
|
+
if not response.ok:
|
|
65
|
+
if extra_error_message:
|
|
66
|
+
self.logger.error(extra_error_message)
|
|
67
|
+
self.logger.error("Response: %s", json_response)
|
|
68
|
+
response.raise_for_status()
|
|
69
|
+
return json_response
|
|
70
|
+
|
|
71
|
+
@staticmethod
|
|
72
|
+
def _get_validation_handler(empty_input_policy: EmptyInputPolicy, logger: logging.Logger) -> Callable:
|
|
73
|
+
def _ignore(msg):
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
def _warn(msg):
|
|
77
|
+
logger.warning(msg)
|
|
78
|
+
|
|
79
|
+
def _error(msg):
|
|
80
|
+
raise ValueError(msg)
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
EmptyInputPolicy.IGNORE: _ignore,
|
|
84
|
+
EmptyInputPolicy.WARN: _warn,
|
|
85
|
+
EmptyInputPolicy.ERROR: _error,
|
|
86
|
+
}[empty_input_policy]
|
|
87
|
+
|
|
88
|
+
def _validate_string(self, info: str, s: Optional[str]) -> None:
|
|
89
|
+
if s is None:
|
|
90
|
+
self.validation_handler(f"{info}: is None")
|
|
91
|
+
elif s == "":
|
|
92
|
+
self.validation_handler(f"{info}: empty string")
|
|
93
|
+
|
|
94
|
+
def _validate_object(self, info: str, obj):
|
|
95
|
+
if obj is None:
|
|
96
|
+
self.validation_handler(f"{info}: is None")
|
|
97
|
+
|
|
98
|
+
def _validate_list(self, info: str, list_obj: List):
|
|
99
|
+
if not list_obj:
|
|
100
|
+
self.validation_handler(f"{info}: empty list")
|
|
101
|
+
|
|
102
|
+
def create_experiment(self, experiment: str):
|
|
103
|
+
self._validate_string("experiment", experiment)
|
|
104
|
+
json_data = {
|
|
105
|
+
"name": experiment
|
|
106
|
+
}
|
|
107
|
+
return self._post("seqauto/api/v1/experiment/", json_data)
|
|
108
|
+
|
|
109
|
+
def create_enrichment_kit(self, enrichment_kit: EnrichmentKit):
|
|
110
|
+
self._validate_object("enrichment_kit", enrichment_kit)
|
|
111
|
+
return self._post("seqauto/api/v1/enrichment_kit/",
|
|
112
|
+
enrichment_kit.to_dict())
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def create_sequencer_model(self, sequencer_model: SequencerModel):
|
|
116
|
+
self._validate_object("sequencer_model", sequencer_model)
|
|
117
|
+
return self._post("seqauto/api/v1/sequencer_model/",
|
|
118
|
+
sequencer_model.to_dict())
|
|
119
|
+
|
|
120
|
+
def create_sequencer(self, sequencer: Sequencer):
|
|
121
|
+
self._validate_object("sequencer", sequencer)
|
|
122
|
+
return self._post("seqauto/api/v1/sequencer/",
|
|
123
|
+
sequencer.to_dict())
|
|
124
|
+
|
|
125
|
+
def create_sequencing_run(self, sequencing_run: SequencingRun):
|
|
126
|
+
self._validate_object("sequencing_run", sequencing_run)
|
|
127
|
+
return self._post("seqauto/api/v1/sequencing_run/",
|
|
128
|
+
sequencing_run.to_dict())
|
|
129
|
+
|
|
130
|
+
def create_sample_sheet(self, sample_sheet: SampleSheet):
|
|
131
|
+
self._validate_object("sample_sheet", sample_sheet)
|
|
132
|
+
json_data = sample_sheet.to_dict()
|
|
133
|
+
# We don't want all sequencing_run just the name
|
|
134
|
+
sequencing_run = json_data.pop("sequencing_run")
|
|
135
|
+
json_data["sequencing_run"] = sequencing_run["name"]
|
|
136
|
+
return self._post("seqauto/api/v1/sample_sheet/",
|
|
137
|
+
json_data)
|
|
138
|
+
|
|
139
|
+
def create_sample_sheet_combined_vcf_file(self, sample_sheet_combined_vcf_file: SampleSheetCombinedVCFFile):
|
|
140
|
+
self._validate_object("sample_sheet_combined_vcf_file", sample_sheet_combined_vcf_file)
|
|
141
|
+
json_data = sample_sheet_combined_vcf_file.to_dict()
|
|
142
|
+
return self._post("seqauto/api/v1/sample_sheet_combined_vcf_file/",
|
|
143
|
+
json_data)
|
|
144
|
+
|
|
145
|
+
def create_sequencing_data(self, sample_sheet_lookup: SampleSheetLookup, sequencing_files: List[SequencingFile]):
|
|
146
|
+
self._validate_object("sample_sheet_lookup", sample_sheet_lookup)
|
|
147
|
+
self._validate_list("sequencing_files", sequencing_files)
|
|
148
|
+
records = []
|
|
149
|
+
for sf in sequencing_files:
|
|
150
|
+
data = sf.to_dict()
|
|
151
|
+
# put into hierarchial JSON DRF expects
|
|
152
|
+
fastq_r1 = data.pop("fastq_r1")
|
|
153
|
+
fastq_r2 = data.pop("fastq_r2")
|
|
154
|
+
data["unaligned_reads"] = {
|
|
155
|
+
"fastq_r1": {"path": fastq_r1},
|
|
156
|
+
"fastq_r2": {"path": fastq_r2}
|
|
157
|
+
}
|
|
158
|
+
records.append(data)
|
|
159
|
+
|
|
160
|
+
json_data = {
|
|
161
|
+
"sample_sheet": sample_sheet_lookup.to_dict(),
|
|
162
|
+
"records": records
|
|
163
|
+
}
|
|
164
|
+
return self._post("seqauto/api/v1/sequencing_files/bulk_create",
|
|
165
|
+
json_data)
|
|
166
|
+
|
|
167
|
+
def create_qc_gene_list(self, qc_gene_list: QCGeneList):
|
|
168
|
+
self._validate_object("qc_gene_list", qc_gene_list)
|
|
169
|
+
json_data = qc_gene_list.to_dict()
|
|
170
|
+
return self._post("seqauto/api/v1/qc_gene_list/",
|
|
171
|
+
json_data)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def create_multiple_qc_gene_lists(self, qc_gene_lists: List[QCGeneList]):
|
|
175
|
+
self._validate_list("qc_gene_lists", qc_gene_lists)
|
|
176
|
+
json_data = {
|
|
177
|
+
"records": [
|
|
178
|
+
qcgl.to_dict() for qcgl in qc_gene_lists
|
|
179
|
+
]
|
|
180
|
+
}
|
|
181
|
+
return self._post("seqauto/api/v1/qc_gene_list/bulk_create",
|
|
182
|
+
json_data)
|
|
183
|
+
|
|
184
|
+
def create_qc_exec_stats(self, qc_exec_stats: QCExecStats):
|
|
185
|
+
self._validate_object("qc_exec_stats", qc_exec_stats)
|
|
186
|
+
json_data = qc_exec_stats.to_dict()
|
|
187
|
+
return self._post("seqauto/api/v1/qc_exec_summary/",
|
|
188
|
+
json_data)
|
|
189
|
+
|
|
190
|
+
def create_multiple_qc_exec_stats(self, qc_exec_stats: List[QCExecStats]):
|
|
191
|
+
self._validate_list("qc_exec_stats", qc_exec_stats)
|
|
192
|
+
json_data = {
|
|
193
|
+
"records": [
|
|
194
|
+
qces.to_dict() for qces in qc_exec_stats
|
|
195
|
+
]
|
|
196
|
+
}
|
|
197
|
+
return self._post("seqauto/api/v1/qc_exec_summary/bulk_create",
|
|
198
|
+
json_data)
|
|
199
|
+
|
|
200
|
+
def create_multiple_qc_gene_coverage(self, qc_gene_coverage_list: List[QCGeneCoverage]):
|
|
201
|
+
self._validate_list("qc_gene_coverage_list", qc_gene_coverage_list)
|
|
202
|
+
json_data = {
|
|
203
|
+
"records": [
|
|
204
|
+
qcgc.to_dict() for qcgc in qc_gene_coverage_list
|
|
205
|
+
]
|
|
206
|
+
}
|
|
207
|
+
return self._post("seqauto/api/v1/qc_gene_coverage/bulk_create",
|
|
208
|
+
json_data)
|
|
209
|
+
|
|
210
|
+
def upload_file(self, filename: str):
|
|
211
|
+
url = self._get_url("upload/api/v1/file_upload")
|
|
212
|
+
with open(filename, "rb") as f:
|
|
213
|
+
kwargs = {
|
|
214
|
+
"files": {"file": f},
|
|
215
|
+
"params": {"path": filename}
|
|
216
|
+
}
|
|
217
|
+
response = requests.post(url, headers=self.headers, **kwargs)
|
|
218
|
+
extra_error_message = f"{filename=}"
|
|
219
|
+
return self._handle_json_response(response, extra_error_message)
|
|
220
|
+
|
|
221
|
+
###############
|
|
222
|
+
## Get methods
|
|
223
|
+
|
|
224
|
+
def _get(self, path, params=None):
|
|
225
|
+
url = self._get_url(path)
|
|
226
|
+
response = requests.get(url,
|
|
227
|
+
params,
|
|
228
|
+
headers=self.headers)
|
|
229
|
+
|
|
230
|
+
extra_error_message = f"{url=}, {params=}"
|
|
231
|
+
return self._handle_json_response(response, extra_error_message)
|
|
232
|
+
|
|
233
|
+
def sequencing_run_has_vcf(self, sequencing_run: SequencingRun, path: Optional[str] = None):
|
|
234
|
+
""" Returns whether the SequencingRun has a VCF associated with it. If path is set, VCF must match that path
|
|
235
|
+
otherwise - any VCF present will retur True """
|
|
236
|
+
return self.sequencing_run_name_has_vcf(sequencing_run.name, path)
|
|
237
|
+
|
|
238
|
+
def sequencing_run_name_has_vcf(self, sequencing_run_name: str, path: Optional[str] = None):
|
|
239
|
+
""" Returns whether the SequencingRun has a VCF associated with it. If path is set, VCF must match that path
|
|
240
|
+
otherwise - any VCF present will retur True """
|
|
241
|
+
data = self._get(f"seqauto/api/v1/sequencing_run/{sequencing_run_name}/")
|
|
242
|
+
found_vcf = False
|
|
243
|
+
for vcf in data["vcf_set"]:
|
|
244
|
+
if path is None or path in vcf["path"]:
|
|
245
|
+
found_vcf = True
|
|
246
|
+
break
|
|
247
|
+
return found_vcf
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import re
|
|
1
2
|
from datetime import date, datetime
|
|
2
3
|
from dataclasses import dataclass, field
|
|
3
4
|
from typing import Optional, List
|
|
@@ -12,6 +13,26 @@ class EnrichmentKit:
|
|
|
12
13
|
version: int
|
|
13
14
|
|
|
14
15
|
|
|
16
|
+
@dataclass_json
|
|
17
|
+
@dataclass
|
|
18
|
+
class Manufacturer:
|
|
19
|
+
name: str
|
|
20
|
+
|
|
21
|
+
@dataclass_json
|
|
22
|
+
@dataclass
|
|
23
|
+
class SequencerModel:
|
|
24
|
+
model: str
|
|
25
|
+
manufacturer: Manufacturer
|
|
26
|
+
data_naming_convention: str # 'M' or 'H' for (HiSeq only)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass_json
|
|
30
|
+
@dataclass
|
|
31
|
+
class Sequencer:
|
|
32
|
+
name: str
|
|
33
|
+
sequencer_model: SequencerModel
|
|
34
|
+
|
|
35
|
+
|
|
15
36
|
@dataclass_json
|
|
16
37
|
@dataclass
|
|
17
38
|
class SequencingRun:
|
|
@@ -22,15 +43,25 @@ class SequencingRun:
|
|
|
22
43
|
experiment: str
|
|
23
44
|
enrichment_kit: EnrichmentKit
|
|
24
45
|
|
|
46
|
+
@staticmethod
|
|
47
|
+
def get_date_from_name(name) -> Optional[datetime.date]:
|
|
48
|
+
date = None
|
|
49
|
+
if m := re.match(r".*_?([12]\d{5})_", name):
|
|
50
|
+
date_str = m.group(1)
|
|
51
|
+
dt = datetime.strptime(date_str, "%y%m%d")
|
|
52
|
+
date = dt.date()
|
|
53
|
+
return date
|
|
54
|
+
|
|
55
|
+
|
|
25
56
|
|
|
26
57
|
@dataclass_json
|
|
27
58
|
@dataclass
|
|
28
59
|
class SequencingSample:
|
|
29
60
|
sample_id: str
|
|
30
|
-
sample_number: int
|
|
31
|
-
lane: int
|
|
61
|
+
sample_number: int # Sample sheet row (to keep in order)
|
|
32
62
|
barcode: str
|
|
33
63
|
enrichment_kit: EnrichmentKit
|
|
64
|
+
lane: Optional[int] = None
|
|
34
65
|
sample_project: Optional[str] = None
|
|
35
66
|
is_control: bool = False
|
|
36
67
|
failed: bool = False
|
|
@@ -42,7 +73,7 @@ class SequencingSample:
|
|
|
42
73
|
class SampleSheet:
|
|
43
74
|
path: str
|
|
44
75
|
sequencing_run: SequencingRun
|
|
45
|
-
file_last_modified:
|
|
76
|
+
file_last_modified: float
|
|
46
77
|
hash: str
|
|
47
78
|
sequencing_samples: List[SequencingSample] = field(metadata=config(field_name="sequencingsample_set"))
|
|
48
79
|
|
|
@@ -144,34 +175,34 @@ class QCExecStats:
|
|
|
144
175
|
modified: datetime
|
|
145
176
|
hash: str
|
|
146
177
|
is_valid: bool
|
|
147
|
-
deduplicated_reads: int
|
|
148
|
-
indels_dbsnp_percent: float
|
|
149
178
|
mean_coverage_across_genes: float
|
|
150
179
|
mean_coverage_across_kit: float
|
|
151
180
|
median_insert: float
|
|
152
|
-
number_indels: int
|
|
153
|
-
number_snps: int
|
|
154
|
-
percent_10x_goi: float
|
|
155
|
-
percent_20x_goi: float
|
|
156
|
-
percent_20x_kit: float
|
|
157
|
-
percent_error_rate: float
|
|
158
|
-
percent_map_to_diff_chr: float
|
|
159
181
|
percent_read_enrichment: float
|
|
160
|
-
percent_reads: float
|
|
161
|
-
percent_softclip: float
|
|
162
182
|
percent_duplication: float
|
|
163
|
-
reads: int
|
|
164
|
-
sample_id_lod: float
|
|
165
|
-
sex_match: str
|
|
166
|
-
snp_dbsnp_percent: float
|
|
167
|
-
ts_to_tv_ratio: float
|
|
168
183
|
uniformity_of_coverage: float
|
|
184
|
+
deduplicated_reads: Optional[int] = None
|
|
185
|
+
indels_dbsnp_percent: Optional[float] = None
|
|
186
|
+
number_indels: Optional[int] = None
|
|
187
|
+
number_snps: Optional[int] = None
|
|
188
|
+
percent_10x_goi: Optional[float] = None
|
|
189
|
+
percent_20x_goi: Optional[float] = None
|
|
190
|
+
percent_20x_kit: Optional[float] = None
|
|
169
191
|
percent_100x_goi: Optional[float] = None
|
|
170
192
|
percent_100x_kit: Optional[float] = None
|
|
171
193
|
percent_250x_goi: Optional[float] = None
|
|
172
194
|
percent_250x_kit: Optional[float] = None
|
|
173
195
|
percent_500x_goi: Optional[float] = None
|
|
174
196
|
percent_500x_kit: Optional[float] = None
|
|
197
|
+
percent_error_rate: Optional[float] = None
|
|
198
|
+
percent_map_to_diff_chr: Optional[float] = None
|
|
199
|
+
percent_reads: Optional[float] = None
|
|
200
|
+
percent_softclip: Optional[float] = None
|
|
201
|
+
reads: Optional[int] = None
|
|
202
|
+
sample_id_lod: Optional[float] = None
|
|
203
|
+
sex_match: Optional[str] = None
|
|
204
|
+
snp_dbsnp_percent: Optional[float] = None
|
|
205
|
+
ts_to_tv_ratio: Optional[float] = None
|
|
175
206
|
|
|
176
207
|
|
|
177
208
|
@dataclass_json
|
|
@@ -180,4 +211,4 @@ class QCGeneCoverage:
|
|
|
180
211
|
""" We send this up to associate coverage file path with sequencing sample
|
|
181
212
|
Then, later we upload the coverage file - and match via path """
|
|
182
213
|
qc: QC
|
|
183
|
-
path: str
|
|
214
|
+
path: str
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: variantgrid_api
|
|
3
|
-
Version:
|
|
3
|
+
Version: 1.1.1
|
|
4
4
|
Summary: A Python API client for VariantGrid
|
|
5
5
|
Author-email: Dave Lawrence <davmlaw@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -35,5 +35,41 @@ Description-Content-Type: text/markdown
|
|
|
35
35
|
License-File: LICENSE
|
|
36
36
|
Requires-Dist: requests
|
|
37
37
|
Requires-Dist: dataclasses-json
|
|
38
|
+
Provides-Extra: test
|
|
39
|
+
Requires-Dist: pytest>=8; extra == "test"
|
|
40
|
+
Requires-Dist: pytest-cov>=5; extra == "test"
|
|
41
|
+
Requires-Dist: responses; extra == "test"
|
|
42
|
+
Dynamic: license-file
|
|
38
43
|
|
|
39
44
|
# variantgrid_api
|
|
45
|
+
|
|
46
|
+
[](https://pypi.org/project/variantgrid_api/) [](https://pypi.org/project/variantgrid_api/)
|
|
47
|
+
|
|
48
|
+
Python API client for [VariantGrid](https://github.com/SACGF/variantgrid) Open source Variant database and analysis platform
|
|
49
|
+
|
|
50
|
+
See [changelog](https://github.com/SACGF/variantgrid_api/blob/main/CHANGELOG.md)
|
|
51
|
+
|
|
52
|
+
## Install
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
python3 -m pip install variantgrid_api
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Example
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
from variantgrid_api.api_client import VariantGridAPI
|
|
62
|
+
from variantgrid_api.data_models import EnrichmentKit
|
|
63
|
+
|
|
64
|
+
api = VariantGridAPI(server="https://variantgrid.com", api_token="YOUR_API_TOKEN")
|
|
65
|
+
enrichment_kit = EnrichmentKit(name="idt_haem", version=1)
|
|
66
|
+
result = api.create_enrichment_kit(enrichment_kit)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Testing
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
# Install required testing packages
|
|
73
|
+
python3 -m pip install -e ".[test]"
|
|
74
|
+
python3 -m pytest --cov=variantgrid_api
|
|
75
|
+
```
|
|
@@ -7,4 +7,8 @@ src/variantgrid_api.egg-info/PKG-INFO
|
|
|
7
7
|
src/variantgrid_api.egg-info/SOURCES.txt
|
|
8
8
|
src/variantgrid_api.egg-info/dependency_links.txt
|
|
9
9
|
src/variantgrid_api.egg-info/requires.txt
|
|
10
|
-
src/variantgrid_api.egg-info/top_level.txt
|
|
10
|
+
src/variantgrid_api.egg-info/top_level.txt
|
|
11
|
+
tests/test_api_client.py
|
|
12
|
+
tests/test_api_client_bulk.py
|
|
13
|
+
tests/test_api_client_validation.py
|
|
14
|
+
tests/test_data_models.py
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
import responses
|
|
7
|
+
|
|
8
|
+
from variantgrid_api.api_client import VariantGridAPI, DateTimeEncoder
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _last_json():
|
|
12
|
+
r = responses.calls[-1].request
|
|
13
|
+
return json.loads(r.body)
|
|
14
|
+
|
|
15
|
+
@responses.activate
|
|
16
|
+
def test_create_experiment(api, server, vg_objects):
|
|
17
|
+
url = f"{server}/seqauto/api/v1/experiment/"
|
|
18
|
+
responses.add(responses.POST, url, json={"id": 1}, status=200)
|
|
19
|
+
out = api.create_experiment(vg_objects["experiment"])
|
|
20
|
+
assert out == {"id": 1}
|
|
21
|
+
assert responses.calls[-1].request.headers["Authorization"] == "Token TKN"
|
|
22
|
+
|
|
23
|
+
@responses.activate
|
|
24
|
+
def test_create_sample_sheet_rewrites_sequencing_run_to_name(api, server, vg_objects):
|
|
25
|
+
url = f"{server}/seqauto/api/v1/sample_sheet/"
|
|
26
|
+
responses.add(responses.POST, url, json={"id": 2}, status=200)
|
|
27
|
+
api.create_sample_sheet(vg_objects["sample_sheet"])
|
|
28
|
+
body = _last_json()
|
|
29
|
+
assert isinstance(body["sequencing_run"], str)
|
|
30
|
+
|
|
31
|
+
@responses.activate
|
|
32
|
+
def test_create_sequencing_data_builds_unaligned_reads(api, server, vg_objects):
|
|
33
|
+
url = f"{server}/seqauto/api/v1/sequencing_files/bulk_create"
|
|
34
|
+
responses.add(responses.POST, url, json={"created": 2}, status=200)
|
|
35
|
+
api.create_sequencing_data(vg_objects["sample_sheet_lookup"], vg_objects["sequencing_files"])
|
|
36
|
+
body = _last_json()
|
|
37
|
+
assert "records" in body and len(body["records"]) == 2
|
|
38
|
+
r0 = body["records"][0]
|
|
39
|
+
assert "unaligned_reads" in r0
|
|
40
|
+
assert "fastq_r1" in r0["unaligned_reads"] and "path" in r0["unaligned_reads"]["fastq_r1"]
|
|
41
|
+
|
|
42
|
+
def assert_post(api_call, url):
|
|
43
|
+
responses.add(responses.POST, url, json={"ok": True}, status=200)
|
|
44
|
+
out = api_call()
|
|
45
|
+
assert out == {"ok": True}
|
|
46
|
+
body = json.loads(responses.calls[-1].request.body)
|
|
47
|
+
assert body
|
|
48
|
+
return body
|
|
49
|
+
|
|
50
|
+
@responses.activate
|
|
51
|
+
def test_create_enrichment_kit_posts_json(api, server, vg_objects):
|
|
52
|
+
url = f"{server}/seqauto/api/v1/enrichment_kit/"
|
|
53
|
+
body = assert_post(
|
|
54
|
+
lambda: api.create_enrichment_kit(vg_objects["enrichment_kit"]),
|
|
55
|
+
url
|
|
56
|
+
)
|
|
57
|
+
assert body["name"] == "idt_haem"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@responses.activate
|
|
61
|
+
def test_create_sequencer_model_posts_json(api, server, vg_objects):
|
|
62
|
+
url = f"{server}/seqauto/api/v1/sequencer_model/"
|
|
63
|
+
body = assert_post(
|
|
64
|
+
lambda: api.create_sequencer_model(vg_objects["sequencer_model"]),
|
|
65
|
+
url
|
|
66
|
+
)
|
|
67
|
+
assert body["model"] == "HiSeq 2500"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@responses.activate
|
|
71
|
+
def test_create_sequencer_posts_json(api, server, vg_objects):
|
|
72
|
+
url = f"{server}/seqauto/api/v1/sequencer/"
|
|
73
|
+
body = assert_post(
|
|
74
|
+
lambda: api.create_sequencer(vg_objects["sequencer"]),
|
|
75
|
+
url
|
|
76
|
+
)
|
|
77
|
+
assert body["name"] == "SN1101"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@responses.activate
|
|
81
|
+
def test_create_sequencing_run_posts_json(api, server, vg_objects):
|
|
82
|
+
url = f"{server}/seqauto/api/v1/sequencing_run/"
|
|
83
|
+
body = assert_post(
|
|
84
|
+
lambda: api.create_sequencing_run(vg_objects["sequencing_run"]),
|
|
85
|
+
url
|
|
86
|
+
)
|
|
87
|
+
assert body["name"] == vg_objects["SEQUENCING_RUN_NAME"]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@responses.activate
|
|
91
|
+
def test_create_sample_sheet_combined_vcf_file_posts_json(api, server, vg_objects):
|
|
92
|
+
url = f"{server}/seqauto/api/v1/sample_sheet_combined_vcf_file/"
|
|
93
|
+
body = assert_post(
|
|
94
|
+
lambda: api.create_sample_sheet_combined_vcf_file(
|
|
95
|
+
vg_objects["sample_sheet_combined_vcf_file"]
|
|
96
|
+
),
|
|
97
|
+
url
|
|
98
|
+
)
|
|
99
|
+
assert body["path"].endswith(".vcf.gz")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@responses.activate
|
|
103
|
+
def test_upload_file_calls_expected_url_and_query_param(api, server, vg_objects):
|
|
104
|
+
filename = vg_objects["qc_gene_coverage_list"][0].path
|
|
105
|
+
url = f"{server}/upload/api/v1/file_upload"
|
|
106
|
+
responses.add(responses.POST, url, json={"uploaded": True}, status=200)
|
|
107
|
+
out = api.upload_file(filename)
|
|
108
|
+
assert out == {"uploaded": True}
|
|
109
|
+
assert responses.calls[-1].request.url.startswith(url)
|
|
110
|
+
assert "path=" in responses.calls[-1].request.url
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@responses.activate
|
|
114
|
+
def test_sequencing_run_has_vcf(api, server, vg_objects):
|
|
115
|
+
sequencing_run = vg_objects["sequencing_run"]
|
|
116
|
+
name = sequencing_run.name
|
|
117
|
+
url = f"{server}/seqauto/api/v1/sequencing_run/{name}/"
|
|
118
|
+
responses.add(responses.GET, url, json={"vcf_set":[{"path":"/x/a.vcf.gz"}]}, status=200)
|
|
119
|
+
|
|
120
|
+
assert api.sequencing_run_has_vcf(sequencing_run) is True
|
|
121
|
+
assert api.sequencing_run_has_vcf(sequencing_run, "a.vcf.gz") is True
|
|
122
|
+
assert api.sequencing_run_has_vcf(sequencing_run, "no_vcf_here") is False
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@responses.activate
|
|
126
|
+
def test_create_qc_gene_list_posts(api, server, vg_objects):
|
|
127
|
+
url = f"{server}/seqauto/api/v1/qc_gene_list/"
|
|
128
|
+
responses.add(responses.POST, url, json={"ok": True}, status=200)
|
|
129
|
+
|
|
130
|
+
out = api.create_qc_gene_list(vg_objects["qc_gene_lists"][0])
|
|
131
|
+
assert out == {"ok": True}
|
|
132
|
+
body = _last_json()
|
|
133
|
+
assert "gene_list" in body and len(body["gene_list"]) > 0
|
|
134
|
+
|
|
135
|
+
@responses.activate
|
|
136
|
+
def test_create_qc_exec_stats_posts(api, server, vg_objects):
|
|
137
|
+
url = f"{server}/seqauto/api/v1/qc_exec_summary/"
|
|
138
|
+
responses.add(responses.POST, url, json={"ok": True}, status=200)
|
|
139
|
+
|
|
140
|
+
out = api.create_qc_exec_stats(vg_objects["qc_exec_stats"][0])
|
|
141
|
+
assert out == {"ok": True}
|
|
142
|
+
body = _last_json()
|
|
143
|
+
assert "reads" in body
|
|
144
|
+
|
|
145
|
+
import responses
|
|
146
|
+
|
|
147
|
+
@responses.activate
|
|
148
|
+
def test_log_request_emits_log(server, api_token, vg_objects, caplog):
|
|
149
|
+
logger = logging.getLogger("variantgrid_api.test")
|
|
150
|
+
caplog.set_level(logging.INFO, logger=logger.name)
|
|
151
|
+
|
|
152
|
+
api = VariantGridAPI(
|
|
153
|
+
server, api_token,
|
|
154
|
+
logger=logger,
|
|
155
|
+
log_request=True
|
|
156
|
+
)
|
|
157
|
+
url = f"{server}/seqauto/api/v1/experiment/"
|
|
158
|
+
responses.add(responses.POST, url, json={"ok": True}, status=200)
|
|
159
|
+
|
|
160
|
+
api.create_experiment(vg_objects["experiment"])
|
|
161
|
+
|
|
162
|
+
assert any("POST to" in r.message for r in caplog.records)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@responses.activate
|
|
167
|
+
def test_log_response_emits_log(server, api_token, vg_objects, caplog):
|
|
168
|
+
logger = logging.getLogger("variantgrid_api.test")
|
|
169
|
+
caplog.set_level(logging.INFO, logger=logger.name)
|
|
170
|
+
|
|
171
|
+
api = VariantGridAPI(
|
|
172
|
+
server, api_token,
|
|
173
|
+
logger=logger,
|
|
174
|
+
log_response=True
|
|
175
|
+
)
|
|
176
|
+
url = f"{server}/seqauto/api/v1/experiment/"
|
|
177
|
+
responses.add(responses.POST, url, json={"ok": True}, status=200)
|
|
178
|
+
|
|
179
|
+
api.create_experiment(vg_objects["experiment"])
|
|
180
|
+
|
|
181
|
+
assert any("Response from" in r.message for r in caplog.records)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@responses.activate
|
|
185
|
+
def test_bad_response_raises_and_logs(server, api_token, vg_objects, caplog):
|
|
186
|
+
logger = logging.getLogger("variantgrid_api.test")
|
|
187
|
+
caplog.set_level(logging.INFO, logger=logger.name)
|
|
188
|
+
|
|
189
|
+
api = VariantGridAPI(server, api_token, logger=logger)
|
|
190
|
+
url = f"{server}/seqauto/api/v1/experiment/"
|
|
191
|
+
responses.add(
|
|
192
|
+
responses.POST,
|
|
193
|
+
url,
|
|
194
|
+
json={"detail": "bad request"},
|
|
195
|
+
status=400
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
with pytest.raises(Exception):
|
|
199
|
+
api.create_experiment(vg_objects["experiment"])
|
|
200
|
+
|
|
201
|
+
assert any("Response:" in r.message for r in caplog.records)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@responses.activate
|
|
205
|
+
def test_bad_response_non_json_body(server, api_token, vg_objects):
|
|
206
|
+
api = VariantGridAPI(server, api_token)
|
|
207
|
+
url = f"{server}/seqauto/api/v1/experiment/"
|
|
208
|
+
responses.add(
|
|
209
|
+
responses.POST,
|
|
210
|
+
url,
|
|
211
|
+
body="not json",
|
|
212
|
+
status=500,
|
|
213
|
+
content_type="text/plain"
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
with pytest.raises(Exception):
|
|
217
|
+
api.create_experiment(vg_objects["experiment"])
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def test_datetime_encoder_handles_date_and_rejects_unknown():
|
|
221
|
+
s = json.dumps(
|
|
222
|
+
{"d": datetime.date(2026, 1, 14)},
|
|
223
|
+
cls=DateTimeEncoder
|
|
224
|
+
)
|
|
225
|
+
assert s == '{"d": "2026-01-14"}'
|
|
226
|
+
|
|
227
|
+
with pytest.raises(TypeError):
|
|
228
|
+
json.dumps({"x": object()}, cls=DateTimeEncoder)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import json, responses
|
|
2
|
+
|
|
3
|
+
def assert_bulk_post(api_call, url, n):
|
|
4
|
+
responses.add(responses.POST, url, json={"ok": True}, status=200)
|
|
5
|
+
out = api_call()
|
|
6
|
+
assert out == {"ok": True}
|
|
7
|
+
body = json.loads(responses.calls[-1].request.body)
|
|
8
|
+
assert "records" in body
|
|
9
|
+
assert isinstance(body["records"], list)
|
|
10
|
+
assert len(body["records"]) == n
|
|
11
|
+
assert all(isinstance(r, dict) and r for r in body["records"])
|
|
12
|
+
return body["records"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@responses.activate
|
|
16
|
+
def test_create_multiple_qc_gene_lists(api, server, vg_objects):
|
|
17
|
+
url = f"{server}/seqauto/api/v1/qc_gene_list/bulk_create"
|
|
18
|
+
records = assert_bulk_post(
|
|
19
|
+
lambda: api.create_multiple_qc_gene_lists(vg_objects["qc_gene_lists"]),
|
|
20
|
+
url,
|
|
21
|
+
n=len(vg_objects["qc_gene_lists"])
|
|
22
|
+
)
|
|
23
|
+
assert "gene_list" in records[0]
|
|
24
|
+
assert len(records[0]["gene_list"]) > 0
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@responses.activate
|
|
28
|
+
def test_create_multiple_qc_exec_stats(api, server, vg_objects):
|
|
29
|
+
url = f"{server}/seqauto/api/v1/qc_exec_summary/bulk_create"
|
|
30
|
+
records = assert_bulk_post(
|
|
31
|
+
lambda: api.create_multiple_qc_exec_stats(vg_objects["qc_exec_stats"]),
|
|
32
|
+
url,
|
|
33
|
+
n=len(vg_objects["qc_exec_stats"])
|
|
34
|
+
)
|
|
35
|
+
assert "reads" in records[0]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@responses.activate
|
|
39
|
+
def test_create_multiple_qc_gene_coverage(api, server, vg_objects):
|
|
40
|
+
url = f"{server}/seqauto/api/v1/qc_gene_coverage/bulk_create"
|
|
41
|
+
records = assert_bulk_post(
|
|
42
|
+
lambda: api.create_multiple_qc_gene_coverage(vg_objects["qc_gene_coverage_list"]),
|
|
43
|
+
url,
|
|
44
|
+
n=len(vg_objects["qc_gene_coverage_list"])
|
|
45
|
+
)
|
|
46
|
+
assert "path" in records[0]
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import pytest, responses
|
|
2
|
+
from variantgrid_api.api_client import VariantGridAPI, EmptyInputPolicy
|
|
3
|
+
|
|
4
|
+
import responses
|
|
5
|
+
|
|
6
|
+
def run_validation_call(api_call, url=None, method=responses.POST, response_json={"ok": True}):
|
|
7
|
+
if url:
|
|
8
|
+
responses.add(method, url, json=response_json, status=200)
|
|
9
|
+
return api_call()
|
|
10
|
+
|
|
11
|
+
@responses.activate
|
|
12
|
+
def test_error_policy_empty_list_raises(server, api_token):
|
|
13
|
+
api = VariantGridAPI(server, api_token, empty_input_policy=EmptyInputPolicy.ERROR)
|
|
14
|
+
|
|
15
|
+
with pytest.raises(ValueError):
|
|
16
|
+
run_validation_call(
|
|
17
|
+
lambda: api.create_multiple_qc_gene_lists([])
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
assert len(responses.calls) == 0
|
|
21
|
+
|
|
22
|
+
@responses.activate
|
|
23
|
+
def test_warn_policy_empty_list_logs_and_posts(server, api_token, caplog):
|
|
24
|
+
api = VariantGridAPI(server, api_token, empty_input_policy=EmptyInputPolicy.WARN)
|
|
25
|
+
url = f"{server}/seqauto/api/v1/qc_gene_list/bulk_create"
|
|
26
|
+
|
|
27
|
+
out = run_validation_call(
|
|
28
|
+
lambda: api.create_multiple_qc_gene_lists([]),
|
|
29
|
+
url=url
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
assert out == {"ok": True}
|
|
33
|
+
assert len(responses.calls) == 1
|
|
34
|
+
assert any("empty list" in r.message for r in caplog.records)
|
|
35
|
+
|
|
36
|
+
@responses.activate
|
|
37
|
+
def test_ignore_policy_empty_list_posts_silently(server, api_token, caplog):
|
|
38
|
+
api = VariantGridAPI(server, api_token, empty_input_policy=EmptyInputPolicy.IGNORE)
|
|
39
|
+
url = f"{server}/seqauto/api/v1/qc_gene_list/bulk_create"
|
|
40
|
+
|
|
41
|
+
out = run_validation_call(
|
|
42
|
+
lambda: api.create_multiple_qc_gene_lists([]),
|
|
43
|
+
url=url
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
assert out == {"ok": True}
|
|
47
|
+
assert len(responses.calls) == 1
|
|
48
|
+
assert not caplog.records
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
import pytest, responses
|
|
52
|
+
from variantgrid_api.api_client import VariantGridAPI, EmptyInputPolicy
|
|
53
|
+
|
|
54
|
+
@responses.activate
|
|
55
|
+
def test_validate_string_empty_raises(server, api_token):
|
|
56
|
+
api = VariantGridAPI(server, api_token, empty_input_policy=EmptyInputPolicy.ERROR)
|
|
57
|
+
with pytest.raises(ValueError):
|
|
58
|
+
api.create_experiment("")
|
|
59
|
+
assert len(responses.calls) == 0
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@responses.activate
|
|
63
|
+
def test_validate_string_none_raises(server, api_token):
|
|
64
|
+
api = VariantGridAPI(server, api_token, empty_input_policy=EmptyInputPolicy.ERROR)
|
|
65
|
+
with pytest.raises(ValueError):
|
|
66
|
+
api.create_experiment(None)
|
|
67
|
+
assert len(responses.calls) == 0
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@responses.activate
|
|
71
|
+
def test_validate_object_none_raises(server, api_token):
|
|
72
|
+
api = VariantGridAPI(server, api_token, empty_input_policy=EmptyInputPolicy.ERROR)
|
|
73
|
+
with pytest.raises(ValueError):
|
|
74
|
+
api.create_enrichment_kit(None)
|
|
75
|
+
assert len(responses.calls) == 0
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from variantgrid_api.data_models import SequencingRun, SampleSheetLookup
|
|
2
|
+
|
|
3
|
+
def test_get_date_from_name(vg_objects):
|
|
4
|
+
d = SequencingRun.get_date_from_name(vg_objects["SEQUENCING_RUN_NAME"])
|
|
5
|
+
assert d is not None
|
|
6
|
+
|
|
7
|
+
def test_sample_sheet_lookup_from_sample_sheet(vg_objects):
|
|
8
|
+
ss = vg_objects["sample_sheet"]
|
|
9
|
+
ssl = SampleSheetLookup.from_sample_sheet(ss)
|
|
10
|
+
d = ssl.to_dict()
|
|
11
|
+
assert d
|
variantgrid_api-0.3.0/README.md
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
# variantgrid_api
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import logging
|
|
3
|
-
import os
|
|
4
|
-
from typing import List
|
|
5
|
-
|
|
6
|
-
import requests
|
|
7
|
-
|
|
8
|
-
from variantgrid_api.data_models import EnrichmentKit, SequencingRun, SampleSheet, SampleSheetCombinedVCFFile, \
|
|
9
|
-
SampleSheetLookup, SequencingFile, QCGeneList, QCExecStats, QCGeneCoverage
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class VariantGridAPI:
|
|
13
|
-
def __init__(self, server, api_token):
|
|
14
|
-
self.server = server
|
|
15
|
-
self.headers = {"Authorization": f"Token {api_token}"}
|
|
16
|
-
|
|
17
|
-
def _get_url(self, url):
|
|
18
|
-
return os.path.join(self.server, url)
|
|
19
|
-
|
|
20
|
-
def _post(self, path, json_data):
|
|
21
|
-
url = self._get_url(path)
|
|
22
|
-
response = requests.post(url, headers=self.headers, json=json_data)
|
|
23
|
-
try:
|
|
24
|
-
json_response = response.json()
|
|
25
|
-
except Exception as e:
|
|
26
|
-
json_response = f"Couldn't convert JSON: {e}"
|
|
27
|
-
if not response.ok:
|
|
28
|
-
logging.info("url='%s', JSON data:", url)
|
|
29
|
-
logging.info(json.dumps(json_data))
|
|
30
|
-
logging.info("Response: %s", json_response)
|
|
31
|
-
response.raise_for_status()
|
|
32
|
-
return json_response
|
|
33
|
-
|
|
34
|
-
def create_experiment(self, experiment: str):
|
|
35
|
-
json_data = {
|
|
36
|
-
"name": experiment
|
|
37
|
-
}
|
|
38
|
-
return self._post("seqauto/api/v1/experiment/", json_data)
|
|
39
|
-
|
|
40
|
-
def create_enrichment_kit(self, enrichment_kit: EnrichmentKit):
|
|
41
|
-
return self._post("seqauto/api/v1/enrichment_kit/",
|
|
42
|
-
enrichment_kit.to_dict())
|
|
43
|
-
|
|
44
|
-
def create_sequencing_run(self, sequencing_run: SequencingRun):
|
|
45
|
-
return self._post("seqauto/api/v1/sequencing_run/",
|
|
46
|
-
sequencing_run.to_dict())
|
|
47
|
-
|
|
48
|
-
def create_sample_sheet(self, sample_sheet: SampleSheet):
|
|
49
|
-
json_data = sample_sheet.to_dict()
|
|
50
|
-
# We don't want all sequencing_run just the name
|
|
51
|
-
sequencing_run = json_data.pop("sequencing_run")
|
|
52
|
-
json_data["sequencing_run"] = sequencing_run["name"]
|
|
53
|
-
return self._post("seqauto/api/v1/sample_sheet/",
|
|
54
|
-
json_data)
|
|
55
|
-
|
|
56
|
-
def create_sample_sheet_combined_vcf_file(self, sample_sheet_combined_vcf_file: SampleSheetCombinedVCFFile):
|
|
57
|
-
json_data = sample_sheet_combined_vcf_file.to_dict()
|
|
58
|
-
return self._post("seqauto/api/v1/sample_sheet_combined_vcf_file/",
|
|
59
|
-
json_data)
|
|
60
|
-
|
|
61
|
-
def create_sequencing_data(self, sample_sheet_lookup: SampleSheetLookup, sequencing_files: List[SequencingFile]):
|
|
62
|
-
records = []
|
|
63
|
-
for sf in sequencing_files:
|
|
64
|
-
data = sf.to_dict()
|
|
65
|
-
# put into hierarchial JSON DRF expects
|
|
66
|
-
fastq_r1 = data.pop("fastq_r1")
|
|
67
|
-
fastq_r2 = data.pop("fastq_r2")
|
|
68
|
-
data["unaligned_reads"] = {
|
|
69
|
-
"fastq_r1": {"path": fastq_r1},
|
|
70
|
-
"fastq_r2": {"path": fastq_r2}
|
|
71
|
-
}
|
|
72
|
-
records.append(data)
|
|
73
|
-
|
|
74
|
-
json_data = {
|
|
75
|
-
"sample_sheet": sample_sheet_lookup.to_dict(),
|
|
76
|
-
"records": records
|
|
77
|
-
}
|
|
78
|
-
return self._post("seqauto/api/v1/sequencing_files/bulk_create",
|
|
79
|
-
json_data)
|
|
80
|
-
|
|
81
|
-
def create_qc_gene_list(self, qc_gene_list: QCGeneList):
|
|
82
|
-
json_data = qc_gene_list.to_dict()
|
|
83
|
-
return self._post("seqauto/api/v1/qc_gene_list/",
|
|
84
|
-
json_data)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
def create_multiple_qc_gene_lists(self, qc_gene_lists: List[QCGeneList]):
|
|
88
|
-
json_data = {
|
|
89
|
-
"records": [
|
|
90
|
-
qcgl.to_dict() for qcgl in qc_gene_lists
|
|
91
|
-
]
|
|
92
|
-
}
|
|
93
|
-
return self._post("seqauto/api/v1/qc_gene_list/bulk_create",
|
|
94
|
-
json_data)
|
|
95
|
-
|
|
96
|
-
def create_qc_exec_stats(self, qc_exec_stats: QCExecStats):
|
|
97
|
-
json_data = qc_exec_stats.to_dict()
|
|
98
|
-
return self._post("seqauto/api/v1/qc_exec_summary/",
|
|
99
|
-
json_data)
|
|
100
|
-
|
|
101
|
-
def create_multiple_qc_exec_stats(self, qc_exec_stats: List[QCExecStats]):
|
|
102
|
-
json_data = {
|
|
103
|
-
"records": [
|
|
104
|
-
qces.to_dict() for qces in qc_exec_stats
|
|
105
|
-
]
|
|
106
|
-
}
|
|
107
|
-
return self._post("seqauto/api/v1/qc_exec_summary/bulk_create",
|
|
108
|
-
json_data)
|
|
109
|
-
|
|
110
|
-
def create_multiple_qc_gene_coverage(self, qc_gene_coverage_list: List[QCGeneCoverage]):
|
|
111
|
-
json_data = {
|
|
112
|
-
"records": [
|
|
113
|
-
qcgc.to_dict() for qcgc in qc_gene_coverage_list
|
|
114
|
-
]
|
|
115
|
-
}
|
|
116
|
-
return self._post("seqauto/api/v1/qc_gene_coverage/bulk_create",
|
|
117
|
-
json_data)
|
|
118
|
-
|
|
119
|
-
def upload_file(self, filename: str):
|
|
120
|
-
url = self._get_url("upload/api/v1/file_upload")
|
|
121
|
-
with open(filename, "rb") as f:
|
|
122
|
-
kwargs = {
|
|
123
|
-
"files": {"file": f},
|
|
124
|
-
"params": {"path": filename}
|
|
125
|
-
}
|
|
126
|
-
return requests.post(url, headers=self.headers, **kwargs)
|
|
File without changes
|
|
File without changes
|
{variantgrid_api-0.3.0 → variantgrid_api-1.1.1}/src/variantgrid_api.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|