variantgrid-api 1.1.1__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.1.1/src/variantgrid_api.egg-info → variantgrid_api-1.2.0}/PKG-INFO +1 -1
- {variantgrid_api-1.1.1 → variantgrid_api-1.2.0}/pyproject.toml +1 -1
- {variantgrid_api-1.1.1 → variantgrid_api-1.2.0}/src/variantgrid_api/data_models.py +24 -0
- variantgrid_api-1.2.0/src/variantgrid_api/mock_variantgrid_api.py +134 -0
- {variantgrid_api-1.1.1 → variantgrid_api-1.2.0/src/variantgrid_api.egg-info}/PKG-INFO +1 -1
- {variantgrid_api-1.1.1 → variantgrid_api-1.2.0}/src/variantgrid_api.egg-info/SOURCES.txt +4 -1
- 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.1.1 → variantgrid_api-1.2.0}/LICENSE +0 -0
- {variantgrid_api-1.1.1 → variantgrid_api-1.2.0}/README.md +0 -0
- {variantgrid_api-1.1.1 → variantgrid_api-1.2.0}/setup.cfg +0 -0
- {variantgrid_api-1.1.1 → variantgrid_api-1.2.0}/src/variantgrid_api/api_client.py +0 -0
- {variantgrid_api-1.1.1 → variantgrid_api-1.2.0}/src/variantgrid_api.egg-info/dependency_links.txt +0 -0
- {variantgrid_api-1.1.1 → variantgrid_api-1.2.0}/src/variantgrid_api.egg-info/requires.txt +0 -0
- {variantgrid_api-1.1.1 → variantgrid_api-1.2.0}/src/variantgrid_api.egg-info/top_level.txt +0 -0
- {variantgrid_api-1.1.1 → variantgrid_api-1.2.0}/tests/test_api_client.py +0 -0
- {variantgrid_api-1.1.1 → variantgrid_api-1.2.0}/tests/test_api_client_bulk.py +0 -0
- {variantgrid_api-1.1.1 → variantgrid_api-1.2.0}/tests/test_api_client_validation.py +0 -0
- {variantgrid_api-1.1.1 → variantgrid_api-1.2.0}/tests/test_data_models.py +0 -0
|
@@ -25,6 +25,30 @@ class SequencerModel:
|
|
|
25
25
|
manufacturer: Manufacturer
|
|
26
26
|
data_naming_convention: str # 'M' or 'H' for (HiSeq only)
|
|
27
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
|
+
|
|
28
52
|
|
|
29
53
|
@dataclass_json
|
|
30
54
|
@dataclass
|
|
@@ -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)
|
|
@@ -3,6 +3,7 @@ README.md
|
|
|
3
3
|
pyproject.toml
|
|
4
4
|
src/variantgrid_api/api_client.py
|
|
5
5
|
src/variantgrid_api/data_models.py
|
|
6
|
+
src/variantgrid_api/mock_variantgrid_api.py
|
|
6
7
|
src/variantgrid_api.egg-info/PKG-INFO
|
|
7
8
|
src/variantgrid_api.egg-info/SOURCES.txt
|
|
8
9
|
src/variantgrid_api.egg-info/dependency_links.txt
|
|
@@ -11,4 +12,6 @@ src/variantgrid_api.egg-info/top_level.txt
|
|
|
11
12
|
tests/test_api_client.py
|
|
12
13
|
tests/test_api_client_bulk.py
|
|
13
14
|
tests/test_api_client_validation.py
|
|
14
|
-
tests/test_data_models.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,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"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{variantgrid_api-1.1.1 → variantgrid_api-1.2.0}/src/variantgrid_api.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|