variantgrid-api 1.5.0__tar.gz → 1.6.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 (25) hide show
  1. {variantgrid_api-1.5.0/src/variantgrid_api.egg-info → variantgrid_api-1.6.0}/PKG-INFO +1 -1
  2. {variantgrid_api-1.5.0 → variantgrid_api-1.6.0}/pyproject.toml +1 -1
  3. {variantgrid_api-1.5.0 → variantgrid_api-1.6.0}/src/variantgrid_api/api_client.py +22 -19
  4. {variantgrid_api-1.5.0 → variantgrid_api-1.6.0}/src/variantgrid_api/data_models.py +41 -0
  5. {variantgrid_api-1.5.0 → variantgrid_api-1.6.0}/src/variantgrid_api/mock_variantgrid_api.py +22 -21
  6. {variantgrid_api-1.5.0 → variantgrid_api-1.6.0/src/variantgrid_api.egg-info}/PKG-INFO +1 -1
  7. {variantgrid_api-1.5.0 → variantgrid_api-1.6.0}/tests/test_api_client.py +29 -1
  8. {variantgrid_api-1.5.0 → variantgrid_api-1.6.0}/tests/test_api_client_capabilities.py +28 -1
  9. {variantgrid_api-1.5.0 → variantgrid_api-1.6.0}/tests/test_mock_variantgrid_api.py +10 -1
  10. {variantgrid_api-1.5.0 → variantgrid_api-1.6.0}/LICENSE +0 -0
  11. {variantgrid_api-1.5.0 → variantgrid_api-1.6.0}/README.md +0 -0
  12. {variantgrid_api-1.5.0 → variantgrid_api-1.6.0}/setup.cfg +0 -0
  13. {variantgrid_api-1.5.0 → variantgrid_api-1.6.0}/src/variantgrid_api/cli.py +0 -0
  14. {variantgrid_api-1.5.0 → variantgrid_api-1.6.0}/src/variantgrid_api.egg-info/SOURCES.txt +0 -0
  15. {variantgrid_api-1.5.0 → variantgrid_api-1.6.0}/src/variantgrid_api.egg-info/dependency_links.txt +0 -0
  16. {variantgrid_api-1.5.0 → variantgrid_api-1.6.0}/src/variantgrid_api.egg-info/entry_points.txt +0 -0
  17. {variantgrid_api-1.5.0 → variantgrid_api-1.6.0}/src/variantgrid_api.egg-info/requires.txt +0 -0
  18. {variantgrid_api-1.5.0 → variantgrid_api-1.6.0}/src/variantgrid_api.egg-info/top_level.txt +0 -0
  19. {variantgrid_api-1.5.0 → variantgrid_api-1.6.0}/tests/test_api_client_annotation.py +0 -0
  20. {variantgrid_api-1.5.0 → variantgrid_api-1.6.0}/tests/test_api_client_bulk.py +0 -0
  21. {variantgrid_api-1.5.0 → variantgrid_api-1.6.0}/tests/test_api_client_patients.py +0 -0
  22. {variantgrid_api-1.5.0 → variantgrid_api-1.6.0}/tests/test_api_client_validation.py +0 -0
  23. {variantgrid_api-1.5.0 → variantgrid_api-1.6.0}/tests/test_cli.py +0 -0
  24. {variantgrid_api-1.5.0 → variantgrid_api-1.6.0}/tests/test_data_models.py +0 -0
  25. {variantgrid_api-1.5.0 → variantgrid_api-1.6.0}/tests/test_sequencer_model_from_name.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: variantgrid_api
3
- Version: 1.5.0
3
+ Version: 1.6.0
4
4
  Summary: A Python API client for VariantGrid
5
5
  Author-email: Dave Lawrence <davmlaw@gmail.com>
6
6
  License: MIT License
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "variantgrid_api"
7
- version = "1.5.0"
7
+ version = "1.6.0"
8
8
  description = "A Python API client for VariantGrid"
9
9
  authors = [
10
10
  { name = "Dave Lawrence", email = "davmlaw@gmail.com" }
@@ -14,7 +14,7 @@ import requests
14
14
  from variantgrid_api.data_models import EnrichmentKit, SequencingRun, SampleSheet, JointCalledVCF, \
15
15
  SampleSheetLookup, SequencingFile, QCGeneList, QCExecStats, QCGeneCoverage, SequencerModel, Sequencer, \
16
16
  SequencingSampleLookup, Patient, Specimen, Extraction, SpecimenMeasure, ExternalReference, ReferenceLike, \
17
- reference_json, ServerCapabilities
17
+ reference_json, ServerCapabilities, ServerFeature, UploadFileType
18
18
 
19
19
 
20
20
  _UNSET = object()
@@ -161,11 +161,11 @@ class VariantGridAPI:
161
161
  self._capabilities = ServerCapabilities.from_json(data)
162
162
  return self._capabilities
163
163
 
164
- def supports(self, feature: str) -> bool:
164
+ def supports(self, feature: Union[ServerFeature, str]) -> bool:
165
165
  return feature in self.capabilities.features
166
166
 
167
- def accepts_upload(self, file_type: str) -> bool:
168
- """ file_type is the server's UploadedFileTypes name in lower case, eg 'dragen_tso500_combined_variant_output' """
167
+ def accepts_upload(self, file_type: Union[UploadFileType, str]) -> bool:
168
+ """ file_type is an UploadFileType, or the server's UploadedFileTypes name in lower case """
169
169
  return file_type in self.capabilities.upload_file_types
170
170
 
171
171
  def _unsupported(self, message: str) -> bool:
@@ -177,11 +177,11 @@ class VariantGridAPI:
177
177
  return False
178
178
  raise UnsupportedFeatureError(message, capabilities)
179
179
 
180
- def _require(self, feature: str) -> bool:
180
+ def _require(self, feature: Union[ServerFeature, str]) -> bool:
181
181
  """ True if the server supports feature, otherwise applies unsupported_feature_policy """
182
182
  return self.supports(feature) or self._unsupported(f"server doesn't support feature '{feature}'")
183
183
 
184
- def _require_upload(self, file_type: str) -> bool:
184
+ def _require_upload(self, file_type: Union[UploadFileType, str]) -> bool:
185
185
  return self.accepts_upload(file_type) or self._unsupported(f"server doesn't accept upload file type '{file_type}'")
186
186
 
187
187
  def create_experiment(self, experiment: str):
@@ -241,6 +241,9 @@ class VariantGridAPI:
241
241
  self._validate_list("sequencing_files", sequencing_files)
242
242
  records = []
243
243
  for sf in sequencing_files:
244
+ # The server requires both paths - catch it here, naming the record, rather than a 400 for the batch
245
+ self._validate_string(f"SequencingFile '{sf.sample_name}' bam_file.path", sf.bam_file and sf.bam_file.path)
246
+ self._validate_string(f"SequencingFile '{sf.sample_name}' vcf_file.path", sf.vcf_file and sf.vcf_file.path)
244
247
  data = sf.to_dict()
245
248
  # put into hierarchial JSON DRF expects
246
249
  fastq_r1 = data.pop("fastq_r1", None)
@@ -310,28 +313,28 @@ class VariantGridAPI:
310
313
  ## Creates are upserts keyed on the identifiers sent, so re-posting returns the same rows
311
314
 
312
315
  def create_patient(self, patient: Patient):
313
- if not self._require("patients"):
316
+ if not self._require(ServerFeature.PATIENTS):
314
317
  return None
315
318
  self._validate_object("patient", patient)
316
319
  return self._post("patients/api/v1/patient/", patient.to_dict())
317
320
 
318
321
  def create_specimen(self, specimen: Specimen):
319
322
  """ The specimen's patient must already exist on the server, otherwise this is a 400 """
320
- if not self._require("patients"):
323
+ if not self._require(ServerFeature.PATIENTS):
321
324
  return None
322
325
  self._validate_object("specimen", specimen)
323
326
  return self._post("patients/api/v1/specimen/", specimen.to_dict())
324
327
 
325
328
  def create_extraction(self, extraction: Extraction):
326
329
  """ The extraction's specimen must already exist on the server, otherwise this is a 400 """
327
- if not self._require("patients"):
330
+ if not self._require(ServerFeature.PATIENTS):
328
331
  return None
329
332
  self._validate_object("extraction", extraction)
330
333
  return self._post("patients/api/v1/extraction/", extraction.to_dict())
331
334
 
332
335
  def create_specimen_measure(self, specimen_reference: ReferenceLike, measure: SpecimenMeasure):
333
336
  """ An unknown specimen is a 400. Replaces any existing measure of the same type for the specimen """
334
- if not self._require("specimen_measures"):
337
+ if not self._require(ServerFeature.SPECIMEN_MEASURES):
335
338
  return None
336
339
  self._validate_reference("specimen_reference", specimen_reference)
337
340
  self._validate_object("measure", measure)
@@ -340,7 +343,7 @@ class VariantGridAPI:
340
343
 
341
344
  def create_specimen_measures(self, specimen_reference: ReferenceLike, measures: List[SpecimenMeasure]):
342
345
  """ A run's measures (TMB, MSI, GIS etc) against one specimen in one call """
343
- if not self._require("specimen_measures"):
346
+ if not self._require(ServerFeature.SPECIMEN_MEASURES):
344
347
  return None
345
348
  self._validate_reference("specimen_reference", specimen_reference)
346
349
  self._validate_list("measures", measures)
@@ -360,7 +363,7 @@ class VariantGridAPI:
360
363
  sequencing sample is a 400, but an extraction the server doesn't have yet is not an error:
361
364
  the response is a 202 with match_status 'Pending', and the link attaches itself once the
362
365
  extraction is created - there's no need to re-send. """
363
- if not self._require("link_extraction"):
366
+ if not self._require(ServerFeature.LINK_EXTRACTION):
364
367
  return None
365
368
  self._validate_object("sequencing_sample_lookup", sequencing_sample_lookup)
366
369
  self._validate_reference("extraction_reference", extraction_reference)
@@ -371,7 +374,7 @@ class VariantGridAPI:
371
374
  return self._post("seqauto/api/v1/sequencing_sample/link_extraction", json_data)
372
375
 
373
376
  def upload_file(self, filename: str, path=_UNSET, metadata: Optional[dict] = None,
374
- file_type: Optional[str] = None):
377
+ file_type: Optional[Union[UploadFileType, str]] = None):
375
378
  """ Upload a file via multipart POST to upload/api/v1/file_upload.
376
379
 
377
380
  Returns {"uploaded_file_id": <id>, "sha256_hash": <hash>, ...}; identify the upload by
@@ -393,10 +396,10 @@ class VariantGridAPI:
393
396
  Needs the server feature 'upload_metadata'. Without it, SKIP uploads the file without the
394
397
  metadata (as older clients did) rather than not at all, and ERROR raises
395
398
 
396
- file_type: the server's name for what this file is, eg 'dragen_tso500_combined_variant_output'.
399
+ file_type: the server's name for what this file is, eg UploadFileType.DRAGEN_TSO500_COMBINED_VARIANT_OUTPUT.
397
400
  Not sent (the server decides from the filename) - if given, the upload only happens when
398
401
  accepts_upload(file_type), so an older server doesn't mis-import it as something else """
399
- if metadata and not self.supports("upload_metadata"):
402
+ if metadata and not self.supports(ServerFeature.UPLOAD_METADATA):
400
403
  self._unsupported(f"upload metadata for '{filename}' (server doesn't support feature 'upload_metadata')")
401
404
  metadata = None
402
405
  if file_type and not self._require_upload(file_type):
@@ -473,7 +476,7 @@ class VariantGridAPI:
473
476
 
474
477
  Keyed by uploaded_file_id (returned from upload_file) or the SHA-256 of the uploaded file.
475
478
  See wait_for_annotation to block until annotation is complete. Needs server feature 'upload_status' """
476
- if not self._require("upload_status"):
479
+ if not self._require(ServerFeature.UPLOAD_STATUS):
477
480
  return None
478
481
  segment = self._upload_key_segment(uploaded_file_id, sha256)
479
482
  return self._get(f"upload/api/v1/upload_status/{segment}")
@@ -490,7 +493,7 @@ class VariantGridAPI:
490
493
  server is still creating the upload record) are tolerated: up to 'max_transient_errors' *consecutive*
491
494
  failures are retried before giving up. A 4xx response is treated as a real error and raised immediately.
492
495
  The success counter resets whenever a poll succeeds. Needs server feature 'upload_status' """
493
- if not self._require("upload_status"):
496
+ if not self._require(ServerFeature.UPLOAD_STATUS):
494
497
  return None
495
498
  deadline = time.monotonic() + timeout
496
499
  transient_errors = 0
@@ -542,7 +545,7 @@ class VariantGridAPI:
542
545
  otherwise it is treated as the full destination path. Returns the Path written.
543
546
 
544
547
  Raises TimeoutError if the file isn't ready within 'timeout' seconds. Needs server feature 'upload_status' """
545
- if not self._require("upload_status"):
548
+ if not self._require(ServerFeature.UPLOAD_STATUS):
546
549
  return None
547
550
  if export_type not in ("vcf", "csv"):
548
551
  raise ValueError(f"export_type must be 'vcf' or 'csv', got {export_type!r}")
@@ -590,7 +593,7 @@ class VariantGridAPI:
590
593
 
591
594
  Chains upload_file -> wait_for_annotation -> download_annotated and returns the Path written.
592
595
  Needs server feature 'upload_status' - checked before uploading """
593
- if not self._require("upload_status"):
596
+ if not self._require(ServerFeature.UPLOAD_STATUS):
594
597
  return None
595
598
  # path is SeqAuto-only and makes ad-hoc uploads fail the import - omit it for the annotate flow
596
599
  upload = self.upload_file(filename, path=None)
@@ -442,6 +442,47 @@ class SpecimenMeasure:
442
442
  extraction: Optional[ReferenceLike] = _reference_field(default=None) # the arm that produced it
443
443
 
444
444
 
445
+ ############################################################
446
+ ## Server capabilities (SACGF/variantgrid_api#22)
447
+
448
+ class _ServerName(str, Enum):
449
+ """ A name from the server's vocabulary. Subclasses str so plain strings and these are interchangeable,
450
+ and formats as its value so messages read 'patients' rather than 'ServerFeature.PATIENTS' """
451
+
452
+ def __str__(self):
453
+ return self.value
454
+
455
+ def __format__(self, format_spec):
456
+ return format(self.value, format_spec)
457
+
458
+
459
+ class ServerFeature(_ServerName):
460
+ """ Features a server reports in capabilities (API_FEATURES in the variantgrid repo's
461
+ variantgrid/views_rest.py). Names are never removed - an older server just doesn't list a newer one """
462
+ PATIENTS = "patients"
463
+ SPECIMEN_MEASURES = "specimen_measures"
464
+ LINK_EXTRACTION = "link_extraction"
465
+ UPLOAD_STATUS = "upload_status"
466
+ JOINT_CALLED_VCF_CROSS_RUN = "joint_called_vcf_cross_run"
467
+ UPLOAD_METADATA = "upload_metadata"
468
+
469
+
470
+ class UploadFileType(_ServerName):
471
+ """ File types a server imports from an upload - the server's upload.models.UploadedFileTypes names in
472
+ lower case, less the internal ones it drives itself. A server only reports those it has an importer for """
473
+ BED = "bed"
474
+ DRAGEN_TSO500_ALL_FUSIONS = "dragen_tso500_all_fusions"
475
+ DRAGEN_TSO500_COMBINED_VARIANT_OUTPUT = "dragen_tso500_combined_variant_output"
476
+ GENE_COVERAGE = "gene_coverage"
477
+ GENE_LIST = "gene_list"
478
+ GENE_LEVEL_CNV_VCF = "gene_level_cnv_vcf"
479
+ GENE_LEVEL_INSERT_VARIANTS_ONLY = "gene_level_insert_variants_only"
480
+ PATIENT_RECORDS = "patient_records"
481
+ PED = "ped"
482
+ VCF = "vcf"
483
+ VCF_INSERT_VARIANTS_ONLY = "vcf_insert_variants_only"
484
+
485
+
445
486
  @dataclass(frozen=True)
446
487
  class ServerCapabilities:
447
488
  """ What a server accepts, from GET api/v1/capabilities (SACGF/variantgrid_sapath#443).
@@ -16,20 +16,21 @@ import copy
16
16
  import logging
17
17
  import warnings
18
18
  from pathlib import Path
19
- from typing import Any, List, Optional, Tuple
19
+ from typing import Any, List, Optional, Tuple, Union
20
20
 
21
21
  from variantgrid_api.api_client import UnsupportedFeaturePolicy, UnsupportedFeatureError
22
- from variantgrid_api.data_models import reference_json, ServerCapabilities
22
+ from variantgrid_api.data_models import reference_json, ServerCapabilities, ServerFeature, UploadFileType
23
23
 
24
24
  _UNSET = object()
25
25
 
26
26
  # Every feature and upload file type the real client gates on - the shape of a current (VG4) server
27
27
  MOCK_CAPABILITIES = ServerCapabilities(
28
28
  version="mock",
29
- features=frozenset({"patients", "specimen_measures", "link_extraction", "upload_status",
30
- "joint_called_vcf_cross_run", "upload_metadata"}),
31
- upload_file_types=frozenset({"vcf", "gene_coverage", "dragen_tso500_all_fusions",
32
- "dragen_tso500_combined_variant_output", "gene_level_cnv_vcf"}),
29
+ features=frozenset(ServerFeature),
30
+ upload_file_types=frozenset({UploadFileType.VCF, UploadFileType.GENE_COVERAGE,
31
+ UploadFileType.DRAGEN_TSO500_ALL_FUSIONS,
32
+ UploadFileType.DRAGEN_TSO500_COMBINED_VARIANT_OUTPUT,
33
+ UploadFileType.GENE_LEVEL_CNV_VCF}),
33
34
  )
34
35
 
35
36
 
@@ -86,10 +87,10 @@ class MockVariantGridAPI:
86
87
  # Capabilities — same gating as VariantGridAPI #
87
88
  # ------------------------------------------------------------------ #
88
89
 
89
- def supports(self, feature: str) -> bool:
90
+ def supports(self, feature: Union[ServerFeature, str]) -> bool:
90
91
  return feature in self.capabilities.features
91
92
 
92
- def accepts_upload(self, file_type: str) -> bool:
93
+ def accepts_upload(self, file_type: Union[UploadFileType, str]) -> bool:
93
94
  return file_type in self.capabilities.upload_file_types
94
95
 
95
96
  def _unsupported(self, message: str) -> bool:
@@ -99,10 +100,10 @@ class MockVariantGridAPI:
99
100
  return False
100
101
  raise UnsupportedFeatureError(message, self.capabilities)
101
102
 
102
- def _require(self, feature: str) -> bool:
103
+ def _require(self, feature: Union[ServerFeature, str]) -> bool:
103
104
  return self.supports(feature) or self._unsupported(f"server doesn't support feature '{feature}'")
104
105
 
105
- def _require_upload(self, file_type: str) -> bool:
106
+ def _require_upload(self, file_type: Union[UploadFileType, str]) -> bool:
106
107
  return self.accepts_upload(file_type) or self._unsupported(f"server doesn't accept upload file type '{file_type}'")
107
108
 
108
109
  # ------------------------------------------------------------------ #
@@ -181,39 +182,39 @@ class MockVariantGridAPI:
181
182
  return self._ret("create_multiple_qc_gene_coverage", {"created": len(qc_gene_coverage_list)})
182
183
 
183
184
  def create_patient(self, patient):
184
- if not self._require("patients"):
185
+ if not self._require(ServerFeature.PATIENTS):
185
186
  return None
186
187
  self._record("create_patient", patient)
187
188
  return self._ret("create_patient", {"id": 1, **patient.to_dict()})
188
189
 
189
190
  def create_specimen(self, specimen):
190
- if not self._require("patients"):
191
+ if not self._require(ServerFeature.PATIENTS):
191
192
  return None
192
193
  self._record("create_specimen", specimen)
193
194
  return self._ret("create_specimen", {"id": 1, **specimen.to_dict()})
194
195
 
195
196
  def create_extraction(self, extraction):
196
- if not self._require("patients"):
197
+ if not self._require(ServerFeature.PATIENTS):
197
198
  return None
198
199
  self._record("create_extraction", extraction)
199
200
  return self._ret("create_extraction", {"id": 1, **extraction.to_dict()})
200
201
 
201
202
  def create_specimen_measure(self, specimen_reference, measure):
202
- if not self._require("specimen_measures"):
203
+ if not self._require(ServerFeature.SPECIMEN_MEASURES):
203
204
  return None
204
205
  self._record("create_specimen_measure", specimen_reference, measure)
205
206
  return self._ret("create_specimen_measure", {"id": 1, "specimen": reference_json(specimen_reference),
206
207
  **measure.to_dict()})
207
208
 
208
209
  def create_specimen_measures(self, specimen_reference, measures):
209
- if not self._require("specimen_measures"):
210
+ if not self._require(ServerFeature.SPECIMEN_MEASURES):
210
211
  return None
211
212
  self._record("create_specimen_measures", specimen_reference, measures)
212
213
  return self._ret("create_specimen_measures", {"specimen": reference_json(specimen_reference),
213
214
  "measures": [m.to_dict() for m in measures]})
214
215
 
215
216
  def link_sequencing_sample_extraction(self, sequencing_sample_lookup, extraction_reference):
216
- if not self._require("link_extraction"):
217
+ if not self._require(ServerFeature.LINK_EXTRACTION):
217
218
  return None
218
219
  self._record("link_sequencing_sample_extraction", sequencing_sample_lookup, extraction_reference)
219
220
  return self._ret("link_sequencing_sample_extraction", {
@@ -224,7 +225,7 @@ class MockVariantGridAPI:
224
225
  })
225
226
 
226
227
  def upload_file(self, filename, path=_UNSET, metadata=None, file_type=None):
227
- if metadata and not self.supports("upload_metadata"):
228
+ if metadata and not self.supports(ServerFeature.UPLOAD_METADATA):
228
229
  self._unsupported(f"upload metadata for '{filename}' (server doesn't support feature 'upload_metadata')")
229
230
  metadata = None
230
231
  if file_type and not self._require_upload(file_type):
@@ -240,7 +241,7 @@ class MockVariantGridAPI:
240
241
  "path": path, "status": "ok"})
241
242
 
242
243
  def poll_upload_status(self, uploaded_file_id=None, sha256=None):
243
- if not self._require("upload_status"):
244
+ if not self._require(ServerFeature.UPLOAD_STATUS):
244
245
  return None
245
246
  self._record("poll_upload_status", uploaded_file_id, sha256)
246
247
  return self._ret("poll_upload_status", {
@@ -252,7 +253,7 @@ class MockVariantGridAPI:
252
253
 
253
254
  def wait_for_annotation(self, uploaded_file_id=None, sha256=None,
254
255
  timeout=3600, poll_interval=10, sleep=None, max_transient_errors=5):
255
- if not self._require("upload_status"):
256
+ if not self._require(ServerFeature.UPLOAD_STATUS):
256
257
  return None
257
258
  self._record("wait_for_annotation", uploaded_file_id, sha256,
258
259
  timeout=timeout, poll_interval=poll_interval, sleep=sleep,
@@ -265,7 +266,7 @@ class MockVariantGridAPI:
265
266
 
266
267
  def download_annotated(self, uploaded_file_id=None, sha256=None, export_type="vcf",
267
268
  dest_path=None, timeout=3600, poll_interval=10, sleep=None):
268
- if not self._require("upload_status"):
269
+ if not self._require(ServerFeature.UPLOAD_STATUS):
269
270
  return None
270
271
  self._record("download_annotated", uploaded_file_id, sha256, export_type=export_type,
271
272
  dest_path=dest_path, timeout=timeout, poll_interval=poll_interval, sleep=sleep)
@@ -274,7 +275,7 @@ class MockVariantGridAPI:
274
275
 
275
276
  def annotate_vcf(self, filename, export_type="vcf", dest_path=None,
276
277
  timeout=3600, poll_interval=10, sleep=None):
277
- if not self._require("upload_status"):
278
+ if not self._require(ServerFeature.UPLOAD_STATUS):
278
279
  return None
279
280
  self._record("annotate_vcf", filename, export_type=export_type, dest_path=dest_path,
280
281
  timeout=timeout, poll_interval=poll_interval, sleep=sleep)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: variantgrid_api
3
- Version: 1.5.0
3
+ Version: 1.6.0
4
4
  Summary: A Python API client for VariantGrid
5
5
  Author-email: Dave Lawrence <davmlaw@gmail.com>
6
6
  License: MIT License
@@ -6,7 +6,8 @@ import logging
6
6
  import pytest
7
7
  import responses
8
8
 
9
- from variantgrid_api.api_client import VariantGridAPI, DateTimeEncoder
9
+ from variantgrid_api.api_client import VariantGridAPI, DateTimeEncoder, EmptyInputPolicy
10
+ from variantgrid_api.data_models import BamFile, SingleSampleVCF
10
11
 
11
12
 
12
13
  def _last_json():
@@ -66,6 +67,33 @@ def test_create_sequencing_data_fastq_r2_without_r1_raises(api, vg_objects):
66
67
  with pytest.raises(ValueError):
67
68
  api.create_sequencing_data(vg_objects["sample_sheet_lookup"], [sf])
68
69
 
70
+ @pytest.mark.parametrize("field, value", [
71
+ ("vcf_file", None),
72
+ ("vcf_file", SingleSampleVCF(path=None)),
73
+ ("vcf_file", SingleSampleVCF(path="")),
74
+ ("bam_file", None),
75
+ ("bam_file", BamFile(path=None)),
76
+ ])
77
+ @responses.activate
78
+ def test_create_sequencing_data_missing_path_names_record(api, vg_objects, field, value):
79
+ """ SACGF/variantgrid_api#23 - caught before sending, naming the record, rather than a 400 for the batch """
80
+ sequencing_files = list(vg_objects["sequencing_files"])
81
+ sequencing_files[1] = dataclasses.replace(sequencing_files[1], **{field: value})
82
+ with pytest.raises(ValueError, match=f"SequencingFile 'fake_sample_2' {field}.path"):
83
+ api.create_sequencing_data(vg_objects["sample_sheet_lookup"], sequencing_files)
84
+ assert len(responses.calls) == 0
85
+
86
+ @responses.activate
87
+ def test_create_sequencing_data_missing_vcf_path_warns_and_posts(server, api_token, vg_objects, caplog):
88
+ api = VariantGridAPI(server, api_token, empty_input_policy=EmptyInputPolicy.WARN)
89
+ url = f"{server}/seqauto/api/v1/sequencing_files/bulk_create"
90
+ responses.add(responses.POST, url, json={"created": 2}, status=200)
91
+ sequencing_files = list(vg_objects["sequencing_files"])
92
+ sequencing_files[0] = dataclasses.replace(sequencing_files[0], vcf_file=SingleSampleVCF(path=None))
93
+ api.create_sequencing_data(vg_objects["sample_sheet_lookup"], sequencing_files)
94
+ assert len(responses.calls) == 1
95
+ assert any("SequencingFile 'fake_sample_1' vcf_file.path" in r.message for r in caplog.records)
96
+
69
97
  def assert_post(api_call, url):
70
98
  responses.add(responses.POST, url, json={"ok": True}, status=200)
71
99
  out = api_call()
@@ -6,7 +6,7 @@ import requests
6
6
  import responses
7
7
 
8
8
  from variantgrid_api.api_client import VariantGridAPI, UnsupportedFeaturePolicy, UnsupportedFeatureError
9
- from variantgrid_api.data_models import ServerCapabilities
9
+ from variantgrid_api.data_models import ServerCapabilities, ServerFeature, UploadFileType
10
10
 
11
11
  CVO = "dragen_tso500_combined_variant_output"
12
12
 
@@ -53,6 +53,33 @@ def test_capabilities_parsed_and_fetched_once(api, capabilities_url, capabilitie
53
53
  assert len(_capabilities_calls(capabilities_url)) == 1
54
54
 
55
55
 
56
+ @responses.activate
57
+ def test_capabilities_accept_enums(api, capabilities_url, capabilities_json):
58
+ """ SACGF/variantgrid_api#22 - the enums are str, so they match the plain names the server sends """
59
+ responses.add(responses.GET, capabilities_url, json=capabilities_json, status=200)
60
+
61
+ assert all(api.supports(feature) for feature in ServerFeature)
62
+ assert api.accepts_upload(UploadFileType.DRAGEN_TSO500_COMBINED_VARIANT_OUTPUT)
63
+ assert not api.accepts_upload(UploadFileType.GENE_LIST)
64
+
65
+
66
+ def test_enums_are_their_server_names():
67
+ assert ServerFeature.PATIENTS == "patients"
68
+ assert str(UploadFileType.DRAGEN_TSO500_COMBINED_VARIANT_OUTPUT) == CVO
69
+ assert f"'{ServerFeature.UPLOAD_METADATA}'" == "'upload_metadata'"
70
+ for enum_class in (ServerFeature, UploadFileType):
71
+ for member in enum_class:
72
+ assert member.value == member.name.lower()
73
+
74
+
75
+ @responses.activate
76
+ def test_unsupported_message_names_feature_value(api, capabilities_url, vg_objects):
77
+ responses.add(responses.GET, capabilities_url, json={"detail": "Not found."}, status=404)
78
+
79
+ with pytest.raises(UnsupportedFeatureError, match="feature 'patients'"):
80
+ api.create_patient(vg_objects["patient"])
81
+
82
+
56
83
  @responses.activate
57
84
  def test_capabilities_404_is_legacy(api, capabilities_url):
58
85
  responses.add(responses.GET, capabilities_url, json={"detail": "Not found."}, status=404)
@@ -3,7 +3,7 @@ overrides, and assertion helpers work correctly."""
3
3
  import pytest
4
4
 
5
5
  from variantgrid_api.api_client import UnsupportedFeaturePolicy, UnsupportedFeatureError
6
- from variantgrid_api.data_models import ServerCapabilities
6
+ from variantgrid_api.data_models import ServerCapabilities, ServerFeature, UploadFileType
7
7
  from variantgrid_api.mock_variantgrid_api import MockVariantGridAPI
8
8
 
9
9
 
@@ -240,6 +240,15 @@ def test_mock_default_capabilities_support_everything_gated(mock_api, vg_objects
240
240
  (("cvo.tsv",), {"path": None, "file_type": "dragen_tso500_combined_variant_output"})]
241
241
 
242
242
 
243
+ def test_mock_default_capabilities_accept_enums(mock_api):
244
+ assert all(mock_api.supports(feature) for feature in ServerFeature)
245
+ assert mock_api.supports("upload_status")
246
+ assert mock_api.accepts_upload(UploadFileType.DRAGEN_TSO500_COMBINED_VARIANT_OUTPUT)
247
+ assert not mock_api.accepts_upload(UploadFileType.PED)
248
+ mock_api.upload_file("cvo.tsv", path=None, file_type=UploadFileType.DRAGEN_TSO500_COMBINED_VARIANT_OUTPUT)
249
+ mock_api.assert_called_once("upload_file")
250
+
251
+
243
252
  def test_mock_legacy_skips_under_skip(vg_objects):
244
253
  mock_api = MockVariantGridAPI(capabilities=ServerCapabilities.LEGACY,
245
254
  unsupported_feature_policy=UnsupportedFeaturePolicy.SKIP)
File without changes