variantgrid-api 1.0.0__tar.gz → 1.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: variantgrid_api
3
- Version: 1.0.0
3
+ Version: 1.1.1
4
4
  Summary: A Python API client for VariantGrid
5
5
  Author-email: Dave Lawrence <davmlaw@gmail.com>
6
6
  License: MIT License
@@ -35,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.1.1"
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,26 @@ 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
+
29
+ @dataclass_json
30
+ @dataclass
31
+ class Sequencer:
32
+ name: str
33
+ sequencer_model: SequencerModel
34
+
35
+
16
36
  @dataclass_json
17
37
  @dataclass
18
38
  class SequencingRun:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: variantgrid_api
3
- Version: 1.0.0
3
+ Version: 1.1.1
4
4
  Summary: A Python API client for VariantGrid
5
5
  Author-email: Dave Lawrence <davmlaw@gmail.com>
6
6
  License: MIT License
@@ -35,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
+ ```
@@ -7,4 +7,8 @@ src/variantgrid_api.egg-info/PKG-INFO
7
7
  src/variantgrid_api.egg-info/SOURCES.txt
8
8
  src/variantgrid_api.egg-info/dependency_links.txt
9
9
  src/variantgrid_api.egg-info/requires.txt
10
- src/variantgrid_api.egg-info/top_level.txt
10
+ src/variantgrid_api.egg-info/top_level.txt
11
+ tests/test_api_client.py
12
+ tests/test_api_client_bulk.py
13
+ tests/test_api_client_validation.py
14
+ tests/test_data_models.py
@@ -0,0 +1,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
@@ -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,3 +0,0 @@
1
- requests
2
- dataclasses-json
3
- samshee
File without changes