Skip to content

Commit d3276bd

Browse files
authored
Merge pull request #21 from PennChopMicrobiomeProgram/codex/retrieve-machine-mapping-from-external-source-h6mpk7
Simplify machine type loading with TSV fetch and fallback
2 parents 3ce089d + 48f3b6a commit d3276bd

2 files changed

Lines changed: 116 additions & 13 deletions

File tree

seqBackupLib/illumina.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,45 @@
1+
import csv
12
import re
3+
import warnings
24
from io import TextIOWrapper
35
from pathlib import Path
6+
from urllib.error import URLError
7+
from urllib.request import urlopen
48

59

6-
MACHINE_TYPES = {
10+
MACHINE_TYPES_FALLBACK = {
711
"VH": "Illumina-NextSeq",
812
"D": "Illumina-HiSeq",
913
"M": "Illumina-MiSeq",
1014
"A": "Illumina-NovaSeq",
1115
"NB": "Illumina-MiniSeq",
1216
"LH": "Illumina-NovaSeqX",
1317
"SH": "Illumina-MiSeq",
14-
}
18+
} # Fallback mapping if machine_types.tsv is unavailable.
19+
MACHINE_TYPES_URL = (
20+
"https://raw.githubusercontent.com/PennChopMicrobiomeProgram/"
21+
"SampleRegistry/master/sample_registry/data/machine_types.tsv"
22+
)
23+
try:
24+
with urlopen(MACHINE_TYPES_URL, timeout=10) as response:
25+
rows = list(
26+
csv.reader(response.read().decode("utf-8").splitlines(), delimiter="\t")
27+
)
28+
if rows and rows[0] and rows[0][0].lower() in {"instrument_code", "code"}:
29+
rows = rows[1:]
30+
MACHINE_TYPES = {
31+
row[0].strip(): row[1].strip()
32+
for row in rows
33+
if len(row) >= 2 and row[0].strip() and row[1].strip()
34+
}
35+
if not MACHINE_TYPES:
36+
raise ValueError("machine_types.tsv contained no usable rows")
37+
except (URLError, TimeoutError, ValueError) as exc:
38+
warnings.warn(
39+
f"Falling back to bundled machine types; unable to load {MACHINE_TYPES_URL}: {exc}",
40+
RuntimeWarning,
41+
)
42+
MACHINE_TYPES = MACHINE_TYPES_FALLBACK
1543

1644

1745
def extract_instrument_code(instrument: str) -> str:

test/test_illumina.py

Lines changed: 86 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import gzip
2+
import importlib
3+
from urllib.error import URLError
4+
25
import pytest
3-
from pathlib import Path
6+
47
from seqBackupLib.backup import DEFAULT_MIN_FILE_SIZE
5-
from seqBackupLib.illumina import IlluminaDir, IlluminaFastq, MACHINE_TYPES
68

79

810
machine_fixtures = {
@@ -16,8 +18,44 @@
1618
}
1719

1820

19-
@pytest.mark.parametrize("machine_type", MACHINE_TYPES.keys())
20-
def test_illumina_fastq(machine_type, request):
21+
@pytest.fixture
22+
def illumina_module(monkeypatch):
23+
tsv_rows = ["instrument_code\tmachine_type"]
24+
fallback = {
25+
"VH": "Illumina-NextSeq",
26+
"D": "Illumina-HiSeq",
27+
"M": "Illumina-MiSeq",
28+
"A": "Illumina-NovaSeq",
29+
"NB": "Illumina-MiniSeq",
30+
"LH": "Illumina-NovaSeqX",
31+
"SH": "Illumina-MiSeq",
32+
}
33+
tsv_rows.extend(f"{code}\t{machine}" for code, machine in fallback.items())
34+
tsv = "\n".join(tsv_rows) + "\n"
35+
36+
class FakeResponse:
37+
def __init__(self, data: str):
38+
self._data = data
39+
40+
def read(self):
41+
return self._data.encode("utf-8")
42+
43+
def __enter__(self):
44+
return self
45+
46+
def __exit__(self, exc_type, exc, tb):
47+
return False
48+
49+
monkeypatch.setattr(
50+
"urllib.request.urlopen", lambda *args, **kwargs: FakeResponse(tsv)
51+
)
52+
import seqBackupLib.illumina as illumina
53+
54+
return importlib.reload(illumina)
55+
56+
57+
@pytest.mark.parametrize("machine_type", machine_fixtures.keys())
58+
def test_illumina_fastq(machine_type, request, illumina_module):
2159
fixture_name = machine_fixtures.get(machine_type)
2260
if not fixture_name:
2361
raise ValueError(
@@ -27,18 +65,18 @@ def test_illumina_fastq(machine_type, request):
2765
fp = request.getfixturevalue(fixture_name)
2866

2967
with gzip.open(fp / "Undetermined_S0_L001_R1_001.fastq.gz", "rt") as f:
30-
r1 = IlluminaFastq(f)
68+
r1 = illumina_module.IlluminaFastq(f)
3169

3270
print("FASTQ info: ", r1.fastq_info, "\nFolder info: ", r1.folder_info)
33-
assert r1.machine_type == MACHINE_TYPES[machine_type]
71+
assert r1.machine_type == illumina_module.MACHINE_TYPES[machine_type]
3472
assert r1.check_fp_vs_content()[0], r1.check_fp_vs_content()
3573
assert not r1.check_file_size(DEFAULT_MIN_FILE_SIZE)
3674
assert r1.check_file_size(100)
3775
assert r1.check_index_read_exists()
3876

3977

40-
@pytest.mark.parametrize("machine_type", MACHINE_TYPES.keys())
41-
def test_illumina_dir(machine_type, request):
78+
@pytest.mark.parametrize("machine_type", machine_fixtures.keys())
79+
def test_illumina_dir(machine_type, request, illumina_module):
4280
fixture_name = machine_fixtures.get(machine_type)
4381
if not fixture_name:
4482
raise ValueError(
@@ -47,14 +85,51 @@ def test_illumina_dir(machine_type, request):
4785

4886
fp = request.getfixturevalue(fixture_name)
4987

50-
d = IlluminaDir(fp.name)
88+
d = illumina_module.IlluminaDir(fp.name)
5189

5290

53-
def test_illumina_fastq_without_lane(novaseq_dir):
91+
def test_illumina_fastq_without_lane(novaseq_dir, illumina_module):
5492
original = novaseq_dir / "Undetermined_S0_L001_R1_001.fastq.gz"
5593
renamed = novaseq_dir / "Undetermined_S0_R1_001.fastq.gz"
5694
original.rename(renamed)
5795
with gzip.open(renamed, "rt") as f:
58-
r1 = IlluminaFastq(f)
96+
r1 = illumina_module.IlluminaFastq(f)
5997
assert r1.check_fp_vs_content()[0]
6098
assert r1.build_archive_dir().endswith("L001")
99+
100+
101+
def test_load_machine_types_from_tsv(monkeypatch):
102+
tsv = "instrument_code\tmachine_type\nZZ\tIllumina-Test\n"
103+
104+
class FakeResponse:
105+
def __init__(self, data: str):
106+
self._data = data
107+
108+
def read(self):
109+
return self._data.encode("utf-8")
110+
111+
def __enter__(self):
112+
return self
113+
114+
def __exit__(self, exc_type, exc, tb):
115+
return False
116+
117+
monkeypatch.setattr(
118+
"urllib.request.urlopen", lambda *args, **kwargs: FakeResponse(tsv)
119+
)
120+
import seqBackupLib.illumina as illumina
121+
122+
illumina = importlib.reload(illumina)
123+
assert illumina.MACHINE_TYPES["ZZ"] == "Illumina-Test"
124+
125+
126+
def test_load_machine_types_fallback_warning(monkeypatch):
127+
def raise_url_error(*args, **kwargs):
128+
raise URLError("network down")
129+
130+
monkeypatch.setattr("urllib.request.urlopen", raise_url_error)
131+
import seqBackupLib.illumina as illumina
132+
133+
with pytest.warns(RuntimeWarning, match="Falling back to bundled machine types"):
134+
illumina = importlib.reload(illumina)
135+
assert illumina.MACHINE_TYPES == illumina.MACHINE_TYPES_FALLBACK

0 commit comments

Comments
 (0)