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.
Files changed (22) hide show
  1. {variantgrid_api-1.0.0/src/variantgrid_api.egg-info → variantgrid_api-1.2.0}/PKG-INFO +32 -2
  2. variantgrid_api-1.2.0/README.md +32 -0
  3. {variantgrid_api-1.0.0 → variantgrid_api-1.2.0}/pyproject.toml +4 -2
  4. {variantgrid_api-1.0.0 → variantgrid_api-1.2.0}/src/variantgrid_api/api_client.py +82 -7
  5. {variantgrid_api-1.0.0 → variantgrid_api-1.2.0}/src/variantgrid_api/data_models.py +44 -0
  6. variantgrid_api-1.2.0/src/variantgrid_api/mock_variantgrid_api.py +134 -0
  7. {variantgrid_api-1.0.0 → variantgrid_api-1.2.0/src/variantgrid_api.egg-info}/PKG-INFO +32 -2
  8. variantgrid_api-1.2.0/src/variantgrid_api.egg-info/SOURCES.txt +17 -0
  9. variantgrid_api-1.2.0/src/variantgrid_api.egg-info/requires.txt +7 -0
  10. variantgrid_api-1.2.0/tests/test_api_client.py +228 -0
  11. variantgrid_api-1.2.0/tests/test_api_client_bulk.py +46 -0
  12. variantgrid_api-1.2.0/tests/test_api_client_validation.py +75 -0
  13. variantgrid_api-1.2.0/tests/test_data_models.py +11 -0
  14. variantgrid_api-1.2.0/tests/test_mock_variantgrid_api.py +141 -0
  15. variantgrid_api-1.2.0/tests/test_sequencer_model_from_name.py +32 -0
  16. variantgrid_api-1.0.0/README.md +0 -5
  17. variantgrid_api-1.0.0/src/variantgrid_api.egg-info/SOURCES.txt +0 -10
  18. variantgrid_api-1.0.0/src/variantgrid_api.egg-info/requires.txt +0 -3
  19. {variantgrid_api-1.0.0 → variantgrid_api-1.2.0}/LICENSE +0 -0
  20. {variantgrid_api-1.0.0 → variantgrid_api-1.2.0}/setup.cfg +0 -0
  21. {variantgrid_api-1.0.0 → variantgrid_api-1.2.0}/src/variantgrid_api.egg-info/dependency_links.txt +0 -0
  22. {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.0.0
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
- Requires-Dist: samshee
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
+ [![PyPi version](https://img.shields.io/pypi/v/variantgrid_api.svg)](https://pypi.org/project/variantgrid_api/) [![Python versions](https://img.shields.io/pypi/pyversions/variantgrid_api.svg)](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
+ [![PyPi version](https://img.shields.io/pypi/v/variantgrid_api.svg)](https://pypi.org/project/variantgrid_api/) [![Python versions](https://img.shields.io/pypi/pyversions/variantgrid_api.svg)](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.0.0"
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 os
5
- from typing import List, Optional
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 os.path.join(self.server, url)
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
- logging.error(extra_error_message)
46
- logging.error("Response: %s", json_response)
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.0.0
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
- Requires-Dist: samshee
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
+ [![PyPi version](https://img.shields.io/pypi/v/variantgrid_api.svg)](https://pypi.org/project/variantgrid_api/) [![Python versions](https://img.shields.io/pypi/pyversions/variantgrid_api.svg)](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,7 @@
1
+ requests
2
+ dataclasses-json
3
+
4
+ [test]
5
+ pytest>=8
6
+ pytest-cov>=5
7
+ responses
@@ -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"
@@ -1,5 +0,0 @@
1
- # variantgrid_api
2
-
3
- Python API client for [VariantGrid](https://github.com/SACGF/variantgrid) Open source Variant database and analysis platform
4
-
5
- See [changelog](https://github.com/SACGF/variantgrid_api/blob/main/CHANGELOG.md)
@@ -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
@@ -1,3 +0,0 @@
1
- requests
2
- dataclasses-json
3
- samshee
File without changes