variantgrid-api 1.0.0__tar.gz → 1.2.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.
- {variantgrid_api-1.0.0/src/variantgrid_api.egg-info → variantgrid_api-1.2.0}/PKG-INFO +32 -2
- variantgrid_api-1.2.0/README.md +32 -0
- {variantgrid_api-1.0.0 → variantgrid_api-1.2.0}/pyproject.toml +4 -2
- {variantgrid_api-1.0.0 → variantgrid_api-1.2.0}/src/variantgrid_api/api_client.py +82 -7
- {variantgrid_api-1.0.0 → variantgrid_api-1.2.0}/src/variantgrid_api/data_models.py +44 -0
- variantgrid_api-1.2.0/src/variantgrid_api/mock_variantgrid_api.py +134 -0
- {variantgrid_api-1.0.0 → variantgrid_api-1.2.0/src/variantgrid_api.egg-info}/PKG-INFO +32 -2
- variantgrid_api-1.2.0/src/variantgrid_api.egg-info/SOURCES.txt +17 -0
- variantgrid_api-1.2.0/src/variantgrid_api.egg-info/requires.txt +7 -0
- variantgrid_api-1.2.0/tests/test_api_client.py +228 -0
- variantgrid_api-1.2.0/tests/test_api_client_bulk.py +46 -0
- variantgrid_api-1.2.0/tests/test_api_client_validation.py +75 -0
- variantgrid_api-1.2.0/tests/test_data_models.py +11 -0
- variantgrid_api-1.2.0/tests/test_mock_variantgrid_api.py +141 -0
- variantgrid_api-1.2.0/tests/test_sequencer_model_from_name.py +32 -0
- variantgrid_api-1.0.0/README.md +0 -5
- variantgrid_api-1.0.0/src/variantgrid_api.egg-info/SOURCES.txt +0 -10
- variantgrid_api-1.0.0/src/variantgrid_api.egg-info/requires.txt +0 -3
- {variantgrid_api-1.0.0 → variantgrid_api-1.2.0}/LICENSE +0 -0
- {variantgrid_api-1.0.0 → variantgrid_api-1.2.0}/setup.cfg +0 -0
- {variantgrid_api-1.0.0 → variantgrid_api-1.2.0}/src/variantgrid_api.egg-info/dependency_links.txt +0 -0
- {variantgrid_api-1.0.0 → variantgrid_api-1.2.0}/src/variantgrid_api.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: variantgrid_api
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: A Python API client for VariantGrid
|
|
5
5
|
Author-email: Dave Lawrence <davmlaw@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -35,11 +35,41 @@ Description-Content-Type: text/markdown
|
|
|
35
35
|
License-File: LICENSE
|
|
36
36
|
Requires-Dist: requests
|
|
37
37
|
Requires-Dist: dataclasses-json
|
|
38
|
-
|
|
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"
|
|
39
42
|
Dynamic: license-file
|
|
40
43
|
|
|
41
44
|
# variantgrid_api
|
|
42
45
|
|
|
46
|
+
[](https://pypi.org/project/variantgrid_api/) [](https://pypi.org/project/variantgrid_api/)
|
|
47
|
+
|
|
43
48
|
Python API client for [VariantGrid](https://github.com/SACGF/variantgrid) Open source Variant database and analysis platform
|
|
44
49
|
|
|
45
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 = "1.
|
|
7
|
+
version = "1.2.0"
|
|
8
8
|
description = "A Python API client for VariantGrid"
|
|
9
9
|
authors = [
|
|
10
10
|
{ name = "Dave Lawrence", email = "davmlaw@gmail.com" }
|
|
@@ -21,9 +21,11 @@ classifiers = [
|
|
|
21
21
|
dependencies = [
|
|
22
22
|
"requests",
|
|
23
23
|
"dataclasses-json",
|
|
24
|
-
"samshee"
|
|
25
24
|
]
|
|
26
25
|
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
test = ["pytest>=8", "pytest-cov>=5", "responses"]
|
|
28
|
+
|
|
27
29
|
[project.urls]
|
|
28
30
|
"Homepage" = "https://github.com/SACGF/variantgrid_api"
|
|
29
31
|
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import datetime
|
|
2
2
|
import json
|
|
3
3
|
import logging
|
|
4
|
-
import
|
|
5
|
-
from
|
|
4
|
+
import urllib
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import List, Optional, Callable
|
|
6
7
|
|
|
7
8
|
import requests
|
|
8
9
|
|
|
9
10
|
from variantgrid_api.data_models import EnrichmentKit, SequencingRun, SampleSheet, SampleSheetCombinedVCFFile, \
|
|
10
|
-
SampleSheetLookup, SequencingFile, QCGeneList, QCExecStats, QCGeneCoverage
|
|
11
|
+
SampleSheetLookup, SequencingFile, QCGeneList, QCExecStats, QCGeneCoverage, SequencerModel, Sequencer
|
|
12
|
+
|
|
11
13
|
|
|
12
14
|
class DateTimeEncoder(json.JSONEncoder):
|
|
13
15
|
def default(self,o):
|
|
@@ -16,18 +18,35 @@ class DateTimeEncoder(json.JSONEncoder):
|
|
|
16
18
|
return super().default(o)
|
|
17
19
|
|
|
18
20
|
|
|
21
|
+
class EmptyInputPolicy(Enum):
|
|
22
|
+
IGNORE = "ignore"
|
|
23
|
+
WARN = "warn"
|
|
24
|
+
ERROR = "error"
|
|
25
|
+
|
|
19
26
|
|
|
20
27
|
class VariantGridAPI:
|
|
21
|
-
def __init__(self, server, api_token
|
|
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
|
+
):
|
|
22
33
|
self.server = server
|
|
23
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
|
|
24
41
|
|
|
25
42
|
def _get_url(self, url):
|
|
26
|
-
return
|
|
43
|
+
return urllib.parse.urljoin(self.server, url)
|
|
27
44
|
|
|
28
45
|
def _post(self, path, json_data):
|
|
29
46
|
url = self._get_url(path)
|
|
30
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)
|
|
31
50
|
response = requests.post(url,
|
|
32
51
|
headers={**self.headers, "Content-Type": "application/json"},
|
|
33
52
|
data=json_string)
|
|
@@ -38,30 +57,78 @@ class VariantGridAPI:
|
|
|
38
57
|
def _handle_json_response(self, response, extra_error_message: Optional[str] = None):
|
|
39
58
|
try:
|
|
40
59
|
json_response = response.json()
|
|
60
|
+
if self.log_response:
|
|
61
|
+
self.logger.info("Response from '%s', JSON: %s", response.url, json_response)
|
|
41
62
|
except Exception as e:
|
|
42
63
|
json_response = f"Couldn't convert JSON: {e}"
|
|
43
64
|
if not response.ok:
|
|
44
65
|
if extra_error_message:
|
|
45
|
-
|
|
46
|
-
|
|
66
|
+
self.logger.error(extra_error_message)
|
|
67
|
+
self.logger.error("Response: %s", json_response)
|
|
47
68
|
response.raise_for_status()
|
|
48
69
|
return json_response
|
|
49
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
|
+
|
|
50
102
|
def create_experiment(self, experiment: str):
|
|
103
|
+
self._validate_string("experiment", experiment)
|
|
51
104
|
json_data = {
|
|
52
105
|
"name": experiment
|
|
53
106
|
}
|
|
54
107
|
return self._post("seqauto/api/v1/experiment/", json_data)
|
|
55
108
|
|
|
56
109
|
def create_enrichment_kit(self, enrichment_kit: EnrichmentKit):
|
|
110
|
+
self._validate_object("enrichment_kit", enrichment_kit)
|
|
57
111
|
return self._post("seqauto/api/v1/enrichment_kit/",
|
|
58
112
|
enrichment_kit.to_dict())
|
|
59
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
|
+
|
|
60
125
|
def create_sequencing_run(self, sequencing_run: SequencingRun):
|
|
126
|
+
self._validate_object("sequencing_run", sequencing_run)
|
|
61
127
|
return self._post("seqauto/api/v1/sequencing_run/",
|
|
62
128
|
sequencing_run.to_dict())
|
|
63
129
|
|
|
64
130
|
def create_sample_sheet(self, sample_sheet: SampleSheet):
|
|
131
|
+
self._validate_object("sample_sheet", sample_sheet)
|
|
65
132
|
json_data = sample_sheet.to_dict()
|
|
66
133
|
# We don't want all sequencing_run just the name
|
|
67
134
|
sequencing_run = json_data.pop("sequencing_run")
|
|
@@ -70,11 +137,14 @@ class VariantGridAPI:
|
|
|
70
137
|
json_data)
|
|
71
138
|
|
|
72
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)
|
|
73
141
|
json_data = sample_sheet_combined_vcf_file.to_dict()
|
|
74
142
|
return self._post("seqauto/api/v1/sample_sheet_combined_vcf_file/",
|
|
75
143
|
json_data)
|
|
76
144
|
|
|
77
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)
|
|
78
148
|
records = []
|
|
79
149
|
for sf in sequencing_files:
|
|
80
150
|
data = sf.to_dict()
|
|
@@ -95,12 +165,14 @@ class VariantGridAPI:
|
|
|
95
165
|
json_data)
|
|
96
166
|
|
|
97
167
|
def create_qc_gene_list(self, qc_gene_list: QCGeneList):
|
|
168
|
+
self._validate_object("qc_gene_list", qc_gene_list)
|
|
98
169
|
json_data = qc_gene_list.to_dict()
|
|
99
170
|
return self._post("seqauto/api/v1/qc_gene_list/",
|
|
100
171
|
json_data)
|
|
101
172
|
|
|
102
173
|
|
|
103
174
|
def create_multiple_qc_gene_lists(self, qc_gene_lists: List[QCGeneList]):
|
|
175
|
+
self._validate_list("qc_gene_lists", qc_gene_lists)
|
|
104
176
|
json_data = {
|
|
105
177
|
"records": [
|
|
106
178
|
qcgl.to_dict() for qcgl in qc_gene_lists
|
|
@@ -110,11 +182,13 @@ class VariantGridAPI:
|
|
|
110
182
|
json_data)
|
|
111
183
|
|
|
112
184
|
def create_qc_exec_stats(self, qc_exec_stats: QCExecStats):
|
|
185
|
+
self._validate_object("qc_exec_stats", qc_exec_stats)
|
|
113
186
|
json_data = qc_exec_stats.to_dict()
|
|
114
187
|
return self._post("seqauto/api/v1/qc_exec_summary/",
|
|
115
188
|
json_data)
|
|
116
189
|
|
|
117
190
|
def create_multiple_qc_exec_stats(self, qc_exec_stats: List[QCExecStats]):
|
|
191
|
+
self._validate_list("qc_exec_stats", qc_exec_stats)
|
|
118
192
|
json_data = {
|
|
119
193
|
"records": [
|
|
120
194
|
qces.to_dict() for qces in qc_exec_stats
|
|
@@ -124,6 +198,7 @@ class VariantGridAPI:
|
|
|
124
198
|
json_data)
|
|
125
199
|
|
|
126
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)
|
|
127
202
|
json_data = {
|
|
128
203
|
"records": [
|
|
129
204
|
qcgc.to_dict() for qcgc in qc_gene_coverage_list
|
|
@@ -13,6 +13,50 @@ class EnrichmentKit:
|
|
|
13
13
|
version: int
|
|
14
14
|
|
|
15
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
|
+
# (prefix, model_name, data_naming_convention)
|
|
29
|
+
_ILLUMINA_PREFIXES = [
|
|
30
|
+
("LH", "NovaSeq X", "M"),
|
|
31
|
+
("NB", "NextSeq", "M"),
|
|
32
|
+
("NS", "NextSeq", "M"),
|
|
33
|
+
("A", "NovaSeq 6000", "M"),
|
|
34
|
+
("H", "HiSeq", "H"),
|
|
35
|
+
("M", "MiSeq", "M"),
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def from_sequencer_name(cls, sequencer_name: str) -> 'SequencerModel':
|
|
40
|
+
"""Infer SequencerModel from an Illumina sequencer instrument name.
|
|
41
|
+
|
|
42
|
+
Illumina instrument names begin with a letter code that identifies the
|
|
43
|
+
platform (e.g. ``M02027`` → MiSeq, ``A01234`` → NovaSeq 6000).
|
|
44
|
+
Returns an Unknown model when no prefix matches.
|
|
45
|
+
"""
|
|
46
|
+
illumina = Manufacturer(name="Illumina")
|
|
47
|
+
for prefix, model_name, convention in cls._ILLUMINA_PREFIXES:
|
|
48
|
+
if sequencer_name.startswith(prefix):
|
|
49
|
+
return cls(model=model_name, manufacturer=illumina, data_naming_convention=convention)
|
|
50
|
+
return cls(model="Unknown", manufacturer=illumina, data_naming_convention="U")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass_json
|
|
54
|
+
@dataclass
|
|
55
|
+
class Sequencer:
|
|
56
|
+
name: str
|
|
57
|
+
sequencer_model: SequencerModel
|
|
58
|
+
|
|
59
|
+
|
|
16
60
|
@dataclass_json
|
|
17
61
|
@dataclass
|
|
18
62
|
class SequencingRun:
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Mock replacement for variantgrid_api.api_client.VariantGridAPI.
|
|
3
|
+
|
|
4
|
+
Records every call so tests can assert on what was sent without making any
|
|
5
|
+
|
|
6
|
+
Usage::
|
|
7
|
+
|
|
8
|
+
api = MockVariantGridAPI()
|
|
9
|
+
api.create_experiment("HAEM_25_080")
|
|
10
|
+
api.assert_called_once("create_experiment")
|
|
11
|
+
api.assert_not_called("sequencing_run_has_vcf")
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import copy
|
|
16
|
+
from typing import Any, List, Optional, Tuple
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class MockVariantGridAPI:
|
|
20
|
+
"""Drop-in replacement for VariantGridAPI that records all calls."""
|
|
21
|
+
|
|
22
|
+
def __init__(self):
|
|
23
|
+
# List of (method_name, args, kwargs) in call order
|
|
24
|
+
self.calls: List[Tuple[str, tuple, dict]] = []
|
|
25
|
+
self._return_values: dict = {}
|
|
26
|
+
|
|
27
|
+
# ------------------------------------------------------------------ #
|
|
28
|
+
# Introspection helpers #
|
|
29
|
+
# ------------------------------------------------------------------ #
|
|
30
|
+
|
|
31
|
+
def _record(self, method: str, *args, **kwargs):
|
|
32
|
+
self.calls.append((method, args, kwargs))
|
|
33
|
+
|
|
34
|
+
def _ret(self, method: str, default: Any) -> Any:
|
|
35
|
+
return copy.deepcopy(self._return_values.get(method, default))
|
|
36
|
+
|
|
37
|
+
def set_return(self, method: str, value: Any) -> None:
|
|
38
|
+
"""Override the default return value for a given method."""
|
|
39
|
+
self._return_values[method] = value
|
|
40
|
+
|
|
41
|
+
def get_calls(self, method: str) -> List[Tuple[tuple, dict]]:
|
|
42
|
+
"""Return all (args, kwargs) pairs recorded for *method*."""
|
|
43
|
+
return [(a, kw) for name, a, kw in self.calls if name == method]
|
|
44
|
+
|
|
45
|
+
def call_count(self, method: str) -> int:
|
|
46
|
+
return len(self.get_calls(method))
|
|
47
|
+
|
|
48
|
+
def assert_called_once(self, method: str) -> None:
|
|
49
|
+
n = self.call_count(method)
|
|
50
|
+
assert n == 1, f"Expected {method!r} called once, was called {n} times"
|
|
51
|
+
|
|
52
|
+
def assert_not_called(self, method: str) -> None:
|
|
53
|
+
n = self.call_count(method)
|
|
54
|
+
assert n == 0, f"Expected {method!r} not called, was called {n} times"
|
|
55
|
+
|
|
56
|
+
def reset(self) -> None:
|
|
57
|
+
self.calls.clear()
|
|
58
|
+
|
|
59
|
+
# ------------------------------------------------------------------ #
|
|
60
|
+
# API surface — mirrors VariantGridAPI public methods exactly #
|
|
61
|
+
# ------------------------------------------------------------------ #
|
|
62
|
+
|
|
63
|
+
def create_experiment(self, experiment):
|
|
64
|
+
self._record("create_experiment", experiment)
|
|
65
|
+
return self._ret("create_experiment", {"name": experiment})
|
|
66
|
+
|
|
67
|
+
def create_enrichment_kit(self, enrichment_kit):
|
|
68
|
+
self._record("create_enrichment_kit", enrichment_kit)
|
|
69
|
+
return self._ret("create_enrichment_kit", {
|
|
70
|
+
"name": enrichment_kit.name,
|
|
71
|
+
"version": enrichment_kit.version,
|
|
72
|
+
"gene_list": {
|
|
73
|
+
"genelistgenesymbol_set": [
|
|
74
|
+
{"gene_symbol": "BRCA1"},
|
|
75
|
+
{"gene_symbol": "BRCA2"},
|
|
76
|
+
]
|
|
77
|
+
},
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
def create_sequencer_model(self, sequencer_model):
|
|
81
|
+
self._record("create_sequencer_model", sequencer_model)
|
|
82
|
+
return self._ret("create_sequencer_model", {"model": sequencer_model.model})
|
|
83
|
+
|
|
84
|
+
def create_sequencer(self, sequencer):
|
|
85
|
+
self._record("create_sequencer", sequencer)
|
|
86
|
+
return self._ret("create_sequencer", {"name": sequencer.name})
|
|
87
|
+
|
|
88
|
+
def create_sequencing_run(self, sequencing_run):
|
|
89
|
+
self._record("create_sequencing_run", sequencing_run)
|
|
90
|
+
return self._ret("create_sequencing_run", {"name": sequencing_run.name})
|
|
91
|
+
|
|
92
|
+
def create_sample_sheet(self, sample_sheet):
|
|
93
|
+
self._record("create_sample_sheet", sample_sheet)
|
|
94
|
+
return self._ret("create_sample_sheet", {"path": sample_sheet.path})
|
|
95
|
+
|
|
96
|
+
def create_sample_sheet_combined_vcf_file(self, sscvf):
|
|
97
|
+
self._record("create_sample_sheet_combined_vcf_file", sscvf)
|
|
98
|
+
return self._ret("create_sample_sheet_combined_vcf_file", {"path": sscvf.path})
|
|
99
|
+
|
|
100
|
+
def create_sequencing_data(self, sample_sheet_lookup, sequencing_files):
|
|
101
|
+
self._record("create_sequencing_data", sample_sheet_lookup, sequencing_files)
|
|
102
|
+
return self._ret("create_sequencing_data", {"created": len(sequencing_files)})
|
|
103
|
+
|
|
104
|
+
def create_qc_gene_list(self, qc_gene_list):
|
|
105
|
+
self._record("create_qc_gene_list", qc_gene_list)
|
|
106
|
+
return self._ret("create_qc_gene_list", {})
|
|
107
|
+
|
|
108
|
+
def create_multiple_qc_gene_lists(self, qc_gene_lists):
|
|
109
|
+
self._record("create_multiple_qc_gene_lists", qc_gene_lists)
|
|
110
|
+
return self._ret("create_multiple_qc_gene_lists", {"created": len(qc_gene_lists)})
|
|
111
|
+
|
|
112
|
+
def create_qc_exec_stats(self, qc_exec_stats):
|
|
113
|
+
self._record("create_qc_exec_stats", qc_exec_stats)
|
|
114
|
+
return self._ret("create_qc_exec_stats", {})
|
|
115
|
+
|
|
116
|
+
def create_multiple_qc_exec_stats(self, qc_exec_stats):
|
|
117
|
+
self._record("create_multiple_qc_exec_stats", qc_exec_stats)
|
|
118
|
+
return self._ret("create_multiple_qc_exec_stats", {"created": len(qc_exec_stats)})
|
|
119
|
+
|
|
120
|
+
def create_multiple_qc_gene_coverage(self, qc_gene_coverage_list):
|
|
121
|
+
self._record("create_multiple_qc_gene_coverage", qc_gene_coverage_list)
|
|
122
|
+
return self._ret("create_multiple_qc_gene_coverage", {"created": len(qc_gene_coverage_list)})
|
|
123
|
+
|
|
124
|
+
def upload_file(self, filename):
|
|
125
|
+
self._record("upload_file", filename)
|
|
126
|
+
return self._ret("upload_file", {"path": filename, "status": "ok"})
|
|
127
|
+
|
|
128
|
+
def sequencing_run_has_vcf(self, sequencing_run, path=None):
|
|
129
|
+
self._record("sequencing_run_has_vcf", sequencing_run, path)
|
|
130
|
+
return self._ret("sequencing_run_has_vcf", True)
|
|
131
|
+
|
|
132
|
+
def sequencing_run_name_has_vcf(self, sequencing_run_name, path=None):
|
|
133
|
+
self._record("sequencing_run_name_has_vcf", sequencing_run_name, path)
|
|
134
|
+
return self._ret("sequencing_run_name_has_vcf", True)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: variantgrid_api
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: A Python API client for VariantGrid
|
|
5
5
|
Author-email: Dave Lawrence <davmlaw@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -35,11 +35,41 @@ Description-Content-Type: text/markdown
|
|
|
35
35
|
License-File: LICENSE
|
|
36
36
|
Requires-Dist: requests
|
|
37
37
|
Requires-Dist: dataclasses-json
|
|
38
|
-
|
|
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"
|
|
39
42
|
Dynamic: license-file
|
|
40
43
|
|
|
41
44
|
# variantgrid_api
|
|
42
45
|
|
|
46
|
+
[](https://pypi.org/project/variantgrid_api/) [](https://pypi.org/project/variantgrid_api/)
|
|
47
|
+
|
|
43
48
|
Python API client for [VariantGrid](https://github.com/SACGF/variantgrid) Open source Variant database and analysis platform
|
|
44
49
|
|
|
45
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,17 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/variantgrid_api/api_client.py
|
|
5
|
+
src/variantgrid_api/data_models.py
|
|
6
|
+
src/variantgrid_api/mock_variantgrid_api.py
|
|
7
|
+
src/variantgrid_api.egg-info/PKG-INFO
|
|
8
|
+
src/variantgrid_api.egg-info/SOURCES.txt
|
|
9
|
+
src/variantgrid_api.egg-info/dependency_links.txt
|
|
10
|
+
src/variantgrid_api.egg-info/requires.txt
|
|
11
|
+
src/variantgrid_api.egg-info/top_level.txt
|
|
12
|
+
tests/test_api_client.py
|
|
13
|
+
tests/test_api_client_bulk.py
|
|
14
|
+
tests/test_api_client_validation.py
|
|
15
|
+
tests/test_data_models.py
|
|
16
|
+
tests/test_mock_variantgrid_api.py
|
|
17
|
+
tests/test_sequencer_model_from_name.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
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Tests for MockVariantGridAPI — verifies call recording, return-value
|
|
2
|
+
overrides, and assertion helpers work correctly."""
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from variantgrid_api.mock_variantgrid_api import MockVariantGridAPI
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@pytest.fixture
|
|
9
|
+
def mock_api():
|
|
10
|
+
return MockVariantGridAPI()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# ------------------------------------------------------------------ #
|
|
14
|
+
# Call recording #
|
|
15
|
+
# ------------------------------------------------------------------ #
|
|
16
|
+
|
|
17
|
+
def test_calls_are_recorded(mock_api, vg_objects):
|
|
18
|
+
mock_api.create_experiment(vg_objects["experiment"])
|
|
19
|
+
assert mock_api.call_count("create_experiment") == 1
|
|
20
|
+
assert mock_api.call_count("create_sequencing_run") == 0
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_multiple_calls_accumulate(mock_api, vg_objects):
|
|
24
|
+
mock_api.create_experiment(vg_objects["experiment"])
|
|
25
|
+
mock_api.create_experiment(vg_objects["experiment"])
|
|
26
|
+
assert mock_api.call_count("create_experiment") == 2
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_get_calls_returns_args(mock_api, vg_objects):
|
|
30
|
+
mock_api.create_experiment(vg_objects["experiment"])
|
|
31
|
+
calls = mock_api.get_calls("create_experiment")
|
|
32
|
+
assert len(calls) == 1
|
|
33
|
+
args, kwargs = calls[0]
|
|
34
|
+
assert args[0] == vg_objects["experiment"]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_calls_from_different_methods_are_independent(mock_api, vg_objects):
|
|
38
|
+
mock_api.create_experiment(vg_objects["experiment"])
|
|
39
|
+
mock_api.create_sequencing_run(vg_objects["sequencing_run"])
|
|
40
|
+
assert mock_api.call_count("create_experiment") == 1
|
|
41
|
+
assert mock_api.call_count("create_sequencing_run") == 1
|
|
42
|
+
assert mock_api.call_count("create_sample_sheet") == 0
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ------------------------------------------------------------------ #
|
|
46
|
+
# Assertion helpers #
|
|
47
|
+
# ------------------------------------------------------------------ #
|
|
48
|
+
|
|
49
|
+
def test_assert_called_once_passes(mock_api, vg_objects):
|
|
50
|
+
mock_api.create_experiment(vg_objects["experiment"])
|
|
51
|
+
mock_api.assert_called_once("create_experiment") # should not raise
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_assert_called_once_fails_when_not_called(mock_api):
|
|
55
|
+
with pytest.raises(AssertionError):
|
|
56
|
+
mock_api.assert_called_once("create_experiment")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_assert_called_once_fails_when_called_twice(mock_api, vg_objects):
|
|
60
|
+
mock_api.create_experiment(vg_objects["experiment"])
|
|
61
|
+
mock_api.create_experiment(vg_objects["experiment"])
|
|
62
|
+
with pytest.raises(AssertionError):
|
|
63
|
+
mock_api.assert_called_once("create_experiment")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_assert_not_called_passes(mock_api):
|
|
67
|
+
mock_api.assert_not_called("create_experiment") # should not raise
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_assert_not_called_fails_when_called(mock_api, vg_objects):
|
|
71
|
+
mock_api.create_experiment(vg_objects["experiment"])
|
|
72
|
+
with pytest.raises(AssertionError):
|
|
73
|
+
mock_api.assert_not_called("create_experiment")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# ------------------------------------------------------------------ #
|
|
77
|
+
# Default return values #
|
|
78
|
+
# ------------------------------------------------------------------ #
|
|
79
|
+
|
|
80
|
+
def test_default_return_create_experiment(mock_api, vg_objects):
|
|
81
|
+
result = mock_api.create_experiment(vg_objects["experiment"])
|
|
82
|
+
assert result == {"name": vg_objects["experiment"]}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_default_return_sequencing_run_has_vcf(mock_api, vg_objects):
|
|
86
|
+
result = mock_api.sequencing_run_has_vcf(vg_objects["sequencing_run"])
|
|
87
|
+
assert result is True
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_default_return_create_sequencing_data(mock_api, vg_objects):
|
|
91
|
+
result = mock_api.create_sequencing_data(
|
|
92
|
+
vg_objects["sample_sheet_lookup"], vg_objects["sequencing_files"]
|
|
93
|
+
)
|
|
94
|
+
assert result == {"created": len(vg_objects["sequencing_files"])}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_default_return_create_multiple_qc_gene_lists(mock_api, vg_objects):
|
|
98
|
+
result = mock_api.create_multiple_qc_gene_lists(vg_objects["qc_gene_lists"])
|
|
99
|
+
assert result == {"created": len(vg_objects["qc_gene_lists"])}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# ------------------------------------------------------------------ #
|
|
103
|
+
# set_return overrides #
|
|
104
|
+
# ------------------------------------------------------------------ #
|
|
105
|
+
|
|
106
|
+
def test_set_return_overrides_default(mock_api, vg_objects):
|
|
107
|
+
mock_api.set_return("create_experiment", {"name": "overridden", "id": 99})
|
|
108
|
+
result = mock_api.create_experiment(vg_objects["experiment"])
|
|
109
|
+
assert result == {"name": "overridden", "id": 99}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_set_return_is_deep_copied(mock_api, vg_objects):
|
|
113
|
+
sentinel = {"name": "exp", "extra": []}
|
|
114
|
+
mock_api.set_return("create_experiment", sentinel)
|
|
115
|
+
r1 = mock_api.create_experiment(vg_objects["experiment"])
|
|
116
|
+
r1["extra"].append("mutated")
|
|
117
|
+
r2 = mock_api.create_experiment(vg_objects["experiment"])
|
|
118
|
+
assert r2["extra"] == [], "set_return should return independent copies each call"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_set_return_sequencing_run_has_vcf_false(mock_api, vg_objects):
|
|
122
|
+
mock_api.set_return("sequencing_run_has_vcf", False)
|
|
123
|
+
assert mock_api.sequencing_run_has_vcf(vg_objects["sequencing_run"]) is False
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# ------------------------------------------------------------------ #
|
|
127
|
+
# reset #
|
|
128
|
+
# ------------------------------------------------------------------ #
|
|
129
|
+
|
|
130
|
+
def test_reset_clears_calls(mock_api, vg_objects):
|
|
131
|
+
mock_api.create_experiment(vg_objects["experiment"])
|
|
132
|
+
mock_api.reset()
|
|
133
|
+
assert mock_api.call_count("create_experiment") == 0
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_reset_preserves_return_overrides(mock_api, vg_objects):
|
|
137
|
+
mock_api.set_return("create_experiment", {"id": 7})
|
|
138
|
+
mock_api.create_experiment(vg_objects["experiment"])
|
|
139
|
+
mock_api.reset()
|
|
140
|
+
result = mock_api.create_experiment(vg_objects["experiment"])
|
|
141
|
+
assert result == {"id": 7}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from variantgrid_api.data_models import SequencerModel
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@pytest.mark.parametrize("name,expected_model,expected_convention", [
|
|
6
|
+
("M02027", "MiSeq", "M"),
|
|
7
|
+
("H001", "HiSeq", "H"),
|
|
8
|
+
("NB551234","NextSeq", "M"),
|
|
9
|
+
("NS500123","NextSeq", "M"),
|
|
10
|
+
("A01234", "NovaSeq 6000", "M"),
|
|
11
|
+
("LH00999", "NovaSeq X", "M"),
|
|
12
|
+
("XYZ999", "Unknown", "U"),
|
|
13
|
+
])
|
|
14
|
+
def test_from_sequencer_name(name, expected_model, expected_convention):
|
|
15
|
+
sm = SequencerModel.from_sequencer_name(name)
|
|
16
|
+
assert sm.model == expected_model
|
|
17
|
+
assert sm.data_naming_convention == expected_convention
|
|
18
|
+
assert sm.manufacturer.name == "Illumina"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_from_sequencer_name_matches_conftest_example():
|
|
22
|
+
# The conftest uses sequencer "SN1101" which doesn't match any prefix → Unknown
|
|
23
|
+
# But the run name contains "M02027" as the instrument — verify MiSeq detection
|
|
24
|
+
sm = SequencerModel.from_sequencer_name("M02027")
|
|
25
|
+
assert sm.model == "MiSeq"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_lh_prefix_not_matched_as_h():
|
|
29
|
+
"""NovaSeq X instruments start with LH — must not be classified as HiSeq."""
|
|
30
|
+
sm = SequencerModel.from_sequencer_name("LH00187")
|
|
31
|
+
assert sm.model == "NovaSeq X"
|
|
32
|
+
assert sm.data_naming_convention == "M"
|
variantgrid_api-1.0.0/README.md
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
LICENSE
|
|
2
|
-
README.md
|
|
3
|
-
pyproject.toml
|
|
4
|
-
src/variantgrid_api/api_client.py
|
|
5
|
-
src/variantgrid_api/data_models.py
|
|
6
|
-
src/variantgrid_api.egg-info/PKG-INFO
|
|
7
|
-
src/variantgrid_api.egg-info/SOURCES.txt
|
|
8
|
-
src/variantgrid_api.egg-info/dependency_links.txt
|
|
9
|
-
src/variantgrid_api.egg-info/requires.txt
|
|
10
|
-
src/variantgrid_api.egg-info/top_level.txt
|
|
File without changes
|
|
File without changes
|
{variantgrid_api-1.0.0 → variantgrid_api-1.2.0}/src/variantgrid_api.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|