Skip to content

Commit c35ff75

Browse files
committed
Support DRBX Blending
- `blender.py` blends drb1 and drbx - Gherkin feature files as tests - Fixes in Makefile and cleanup
1 parent 0952a16 commit c35ff75

File tree

9 files changed

+354
-6
lines changed

9 files changed

+354
-6
lines changed

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
PROJECT_NAME := $(shell basename `pwd`)
2+
PACKAGE_NAME := pyard
3+
14
.PHONY: clean clean-test clean-pyc clean-build docs help
25
.DEFAULT_GOAL := help
36
define BROWSER_PYSCRIPT

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,14 @@ ard.redux_gl('B14', 'lg')
121121
| `exon` | Reduce/Expand to exon level |
122122
| `U2` | Reduce to 2 field unambiguous level |
123123

124+
## Perform DRB1 blending with DRB3, DRB4 and DRB5
125+
126+
```python
127+
import pyard
128+
129+
pyard.dr_blender(drb1='HLA-DRB1*03:01+DRB1*04:01', drb3='DRB3*01:01', drb4='DRB4*01:03')
130+
# >>> 'DRB3*01:01+DRB4*01:03'
131+
```
124132
# Command Line Tools
125133

126134
## Using `py-ard` from the command line

pyard/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
# > http://www.opensource.org/licenses/lgpl-license.php
2323
#
2424
from .pyard import ARD
25+
from .blender import blender as dr_blender
2526

2627
__author__ = """NMDP Bioinformatics"""
2728
__version__ = "0.8.0"

pyard/blender.py

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
# drbx blender
2+
3+
4+
def blender(drb1, drb3="", drb4="", drb5=""):
5+
try:
6+
drb1_1, drb1_2 = drb1.split("+")
7+
drb1_allele_1 = drb1_1.split("*")[1]
8+
drb1_allele_2 = drb1_2.split("*")[1]
9+
drb1_fam_1 = drb1_allele_1.split(":")[0]
10+
drb1_fam_2 = drb1_allele_2.split(":")[0]
11+
except Exception:
12+
return ""
13+
x1 = expdrbx(drb1_fam_1)
14+
x2 = expdrbx(drb1_fam_2)
15+
xx = "".join(sorted([x1, x2]))
16+
17+
if xx == "00":
18+
if drb3 != "":
19+
raise DRBXBlenderError("DRB3", "none")
20+
if drb4 != "":
21+
raise DRBXBlenderError("DRB4", "none")
22+
if drb5 != "":
23+
raise DRBXBlenderError("DRB5", "none")
24+
return ""
25+
26+
# handle 03
27+
if xx == "03":
28+
if drb4 != "":
29+
raise DRBXBlenderError("DRB4", "none")
30+
if drb5 != "":
31+
raise DRBXBlenderError("DRB5", "none")
32+
if drb3 != "":
33+
# if 2 copies
34+
drb3_g = drb3.split("+")
35+
if len(drb3_g) == 2:
36+
# homozygous, return one copy
37+
if drb3_g[1] == drb3_g[0]:
38+
return drb3_g[0]
39+
else:
40+
raise DRBXBlenderError("DRB3 het", "hom")
41+
else:
42+
return drb3
43+
return ""
44+
45+
# handle 04
46+
if xx == "04":
47+
if drb3 != "":
48+
raise DRBXBlenderError("DRB3", "none")
49+
if drb5 != "":
50+
raise DRBXBlenderError("DRB5", "none")
51+
if drb4 != "":
52+
# if 2 copies
53+
drb4_g = drb4.split("+")
54+
if len(drb4_g) == 2:
55+
# homozygous, return one copy
56+
if drb4_g[1] == drb4_g[0]:
57+
return drb4_g[0]
58+
else:
59+
raise DRBXBlenderError("DRB4 het", "hom")
60+
else:
61+
return drb4
62+
return ""
63+
64+
# handle 05
65+
if xx == "05":
66+
if drb3 != "":
67+
raise DRBXBlenderError("DRB3", "none")
68+
if drb4 != "":
69+
raise DRBXBlenderError("DRB4", "none")
70+
if drb5 != "":
71+
# if 2 copies
72+
drb5_g = drb5.split("+")
73+
if len(drb5_g) == 2:
74+
# homozygous, return one copy
75+
if drb5_g[1] == drb5_g[0]:
76+
return drb5_g[0]
77+
else:
78+
raise DRBXBlenderError("DRB5 het", "hom")
79+
else:
80+
return drb5
81+
return ""
82+
# handle 33
83+
if xx == "33":
84+
if drb4 != "":
85+
raise DRBXBlenderError("DRB4", "none")
86+
if drb5 != "":
87+
raise DRBXBlenderError("DRB5", "none")
88+
if drb3 != "":
89+
return drb3
90+
return ""
91+
92+
# handle 44
93+
if xx == "44":
94+
if drb3 != "":
95+
raise DRBXBlenderError("DRB3", "none")
96+
if drb5 != "":
97+
raise DRBXBlenderError("DRB5", "none")
98+
if drb4 != "":
99+
return drb4
100+
return ""
101+
102+
# handle 55
103+
if xx == "55":
104+
if drb3 != "":
105+
raise DRBXBlenderError("DRB3", "none")
106+
if drb4 != "":
107+
raise DRBXBlenderError("DRB4", "none")
108+
if drb5 != "":
109+
return drb5
110+
return ""
111+
112+
# handle 34
113+
if xx == "34":
114+
if drb5 != "":
115+
raise DRBXBlenderError("DRB5", "none")
116+
retg = []
117+
118+
if drb3 != "":
119+
# if 2 copies
120+
drb3_g = drb3.split("+")
121+
if len(drb3_g) == 2:
122+
# homozygous, return one copy
123+
if drb3_g[1] == drb3_g[0]:
124+
retg.append(drb3_g[0])
125+
else:
126+
raise DRBXBlenderError("DRB3 het", "hom")
127+
elif len(drb3_g) == 1:
128+
retg.append(drb3_g[0])
129+
if drb4 != "":
130+
# if 2 copies
131+
drb4_g = drb4.split("+")
132+
if len(drb4_g) == 2:
133+
# homozygous, return one copy
134+
if drb4_g[1] == drb4_g[0]:
135+
retg.append(drb4_g[0])
136+
else:
137+
raise DRBXBlenderError("DRB4 het", "hom")
138+
elif len(drb4_g) == 1:
139+
retg.append(drb4_g[0])
140+
141+
return "+".join(retg)
142+
143+
# handle 35
144+
if xx == "35":
145+
if drb4 != "":
146+
raise DRBXBlenderError("DRB4", "none")
147+
retg = []
148+
149+
if drb3 != "":
150+
# if 2 copies
151+
drb3_g = drb3.split("+")
152+
if len(drb3_g) == 2:
153+
# homozygous, return one copy
154+
if drb3_g[1] == drb3_g[0]:
155+
retg.append(drb3_g[0])
156+
else:
157+
raise DRBXBlenderError("DRB3 het", "hom")
158+
elif len(drb3_g) == 1:
159+
retg.append(drb3_g[0])
160+
if drb5 != "":
161+
# if 2 copies
162+
drb5_g = drb5.split("+")
163+
if len(drb5_g) == 2:
164+
# homozygous, return one copy
165+
if drb5_g[1] == drb5_g[0]:
166+
retg.append(drb5_g[0])
167+
else:
168+
raise DRBXBlenderError("DRB5 het", "hom")
169+
elif len(drb5_g) == 1:
170+
retg.append(drb5_g[0])
171+
172+
return "+".join(retg)
173+
174+
# handle 45
175+
if xx == "45":
176+
if drb3 != "":
177+
raise DRBXBlenderError("DRB3", "none")
178+
retg = []
179+
180+
if drb4 != "":
181+
# if 2 copies
182+
drb4_g = drb4.split("+")
183+
if len(drb4_g) == 2:
184+
# homozygous, return one copy
185+
if drb4_g[1] == drb4_g[0]:
186+
retg.append(drb4_g[0])
187+
else:
188+
raise DRBXBlenderError("DRB4 het", "hom")
189+
elif len(drb4_g) == 1:
190+
retg.append(drb4_g[0])
191+
if drb5 != "":
192+
# if 2 copies
193+
drb5_g = drb5.split("+")
194+
if len(drb5_g) == 2:
195+
# homozygous, return one copy
196+
if drb5_g[1] == drb5_g[0]:
197+
retg.append(drb5_g[0])
198+
else:
199+
raise DRBXBlenderError("DRB5 het", "hom")
200+
elif len(drb5_g) == 1:
201+
retg.append(drb5_g[0])
202+
203+
return "+".join(retg)
204+
205+
print("blender fail", xx, drb1_fam_1, drb1_fam_2)
206+
return ""
207+
208+
209+
def expdrbx(drb1_fam):
210+
if drb1_fam in ["03", "05", "06", "11", "12", "13", "14"]:
211+
return "3"
212+
if drb1_fam in ["04", "07", "09"]:
213+
return "4"
214+
if drb1_fam in ["02", "15", "16"]:
215+
return "5"
216+
return "0"
217+
218+
219+
class DRBXBlenderError(Exception):
220+
def __init__(self, found, expected):
221+
self.found = found
222+
self.expected = expected
223+
224+
def __str__(self):
225+
return f"{self.found} where {self.expected} expected"

pyard/db.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ def similar_alleles(connection: sqlite3.Connection, allele_name: str) -> Set[str
315315
:param allele_name: Allele name to use as a prefix to find similar alleles
316316
:return: list of similar alleles
317317
"""
318-
query = f"SELECT allele FROM alleles WHERE allele LIKE ?"
318+
query = "SELECT allele FROM alleles WHERE allele LIKE ?"
319319
cursor = connection.execute(query, (f"{allele_name}%",))
320320
result = cursor.fetchall()
321321
# fetchall() returns a list of tuples of results

pyard/misc.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,3 @@ def get_P_name(a: str) -> str:
5959
if last_char in PandG_chars + expression_chars:
6060
a = a[:-1]
6161
return ":".join(a.split(":")[0:2]) + "P"
62-
63-
64-
def number_of_fields(allele: str) -> int:
65-
return len(allele.split(":"))

setup.cfg

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,10 @@ replace = version: "{new_version}"
1919
universal = 1
2020

2121
[flake8]
22-
exclude = docs
22+
exclude =
23+
venv,
24+
.git,
25+
__pycache__,
26+
build,
27+
dist,
28+
docs
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
Feature: DRB1 blends with DRB3, DRB4, DRB5
2+
3+
Scenario Outline: DRB1 blends
4+
5+
Given a subject has <DRB1_SLUG> DRB1 SLUG
6+
Given a subject has <DRB3> DRB3 allele
7+
And a subject has <DRB4> DRB4 allele
8+
And a subject has <DRB5> DRB5 allele
9+
When I blend the DRBX alleles with DRB1 allele
10+
Then it should blend as <DRBX_BLEND>
11+
12+
Examples: All blends with DRB1
13+
| DRB1_SLUG | DRB3 | DRB4 | DRB5 | DRBX_BLEND |
14+
| HLA-DRB1*03:01+DRB1*04:01 | DRB3*01:01 | DRB4*01:03 | no | DRB3*01:01+DRB4*01:03 |
15+
| HLA-DRB1*03:01+DRB1*04:01 | DRB3*01:01 | DRB4*01:03 | no | DRB3*01:01+DRB4*01:03 |
16+
| HLA-DRB1*03:01+DRB1*04:01 | no | DRB4*01:03 | no | DRB4*01:03 |
17+
| HLA-DRB1*03:01+DRB1*04:01 | DRB3*01:03 | no | no | DRB3*01:03 |
18+
| HLA-DRB1*01:01+DRB1*08:01 | no | no | no | nothing |
19+
| HLA-DRB1*01:01+DRB1*03:01 | no | no | no | nothing |
20+
| HLA-DRB1*01:01+DRB1*04:01 | no | no | no | nothing |
21+
| HLA-DRB1*03:01+DRB1*13:01 | DRB3*01:01 | no | no | DRB3*01:01 |
22+
| HLA-DRB1*15:01+DRB1*16:01 | no | no | DRB5*01:03 | DRB5*01:03 |
23+
24+
Scenario Outline: DRB1 doesn't blend
25+
26+
Given a subject has <DRB1_SLUG> DRB1 SLUG
27+
Given a subject has <DRB3> DRB3 allele
28+
And a subject has <DRB4> DRB4 allele
29+
And a subject has <DRB5> DRB5 allele
30+
When I blend the DRBX alleles with DRB1 allele, it shouldn't blend
31+
Then <Expected> was expected, but found <Found>
32+
33+
Examples: Doesn't blend with DRB1
34+
| DRB1_SLUG | DRB3 | DRB4 | DRB5 | Expected | Found |
35+
| HLA-DRB1*03:01+DRB1*04:01 | DRB3*01:01 | DRB4*01:03 | DRB5*01:05 | none | DRB5 |
36+
| HLA-DRB1*03:01+DRB1*04:01 | DRB3*01:01+DRB3*02:01 | DRB4*01:03 | no | hom | DRB3 het |
37+
| HLA-DRB1*03:01+DRB1*04:01 | DRB3*01:01 | DRB4*01:03+DRB4*01:05 | no | hom | DRB4 het |
38+
| HLA-DRB1*01:01+DRB1*08:01 | DRB3*01:01 | no | no | none | DRB3 |
39+
| HLA-DRB1*01:01+DRB1*08:01 | no | DRB4*01:01 | no | none | DRB4 |
40+
| HLA-DRB1*01:01+DRB1*08:01 | no | no | DRB5*01:01 | none | DRB5 |
41+
| HLA-DRB1*01:01+DRB1*03:01 | DRB3*01:01 | DRB4*01:03 | no | none | DRB4 |
42+
| HLA-DRB1*01:01+DRB1*03:01 | DRB3*01:01 | no | DRB5*01:03 | none | DRB5 |
43+
| HLA-DRB1*01:01+DRB1*04:01 | no | DRB4*01:01+DRB4*01:03 | no | hom | DRB4 het |
44+
| HLA-DRB1*01:01+DRB1*04:01 | DRB3*01:01 | DRB4*01:03 | no | none | DRB3 |
45+
| HLA-DRB1*01:01+DRB1*04:01 | no | DRB4*01:01 | DRB5*01:03 | none | DRB5 |
46+
| HLA-DRB1*03:01+DRB1*13:01 | DRB3*01:01 | DRB4*01:03 | no | none | DRB4 |
47+
| HLA-DRB1*03:01+DRB1*13:01 | DRB3*01:01 | no | DRB5*01:03 | none | DRB5 |
48+
| HLA-DRB1*03:01+DRB1*13:01 | no | DRB4*01:01 | DRB5*01:03 | none | DRB4 |
49+
| HLA-DRB1*04:01+DRB1*09:01 | DRB3*01:01 | no | no | none | DRB3 |
50+
| HLA-DRB1*04:01+DRB1*09:01 | DRB3*01:01 | DRB4*01:03 | no | none | DRB3 |
51+
| HLA-DRB1*04:01+DRB1*09:01 | DRB3*01:01 | no | DRB5*01:03 | none | DRB3 |
52+
| HLA-DRB1*15:01+DRB1*16:01 | no | DRB4*01:01 | DRB5*01:03 | none | DRB4 |
53+
| HLA-DRB1*15:01+DRB1*16:01 | DRB3*01:01 | no | no | none | DRB3 |
54+
| HLA-DRB1*15:01+DRB1*16:01 | DRB3*01:01 | DRB4*01:03 | no | none | DRB3 |
55+
| HLA-DRB1*15:01+DRB1*16:01 | DRB3*01:01 | no | DRB5*01:03 | none | DRB3 |
56+
| HLA-DRB1*15:01+DRB1*16:01 | no | DRB4*01:01 | DRB5*01:03 | none | DRB4 |

tests/steps/drbx_blender.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
from behave import *
2+
from hamcrest import assert_that, is_, calling, raises
3+
4+
import pyard
5+
from pyard.blender import DRBXBlenderError
6+
7+
8+
@given("a subject has {DRB1_SLUG} DRB1 SLUG")
9+
def step_impl(context, DRB1_SLUG):
10+
context.drb1_slug = DRB1_SLUG
11+
12+
13+
@given("a subject has {DRB3} DRB3 allele")
14+
def step_impl(context, DRB3):
15+
context.drb3 = DRB3 if DRB3 != "no" else ""
16+
17+
18+
@step("a subject has {DRB4} DRB4 allele")
19+
def step_impl(context, DRB4):
20+
context.drb4 = DRB4 if DRB4 != "no" else ""
21+
22+
23+
@step("a subject has {DRB5} DRB5 allele")
24+
def step_impl(context, DRB5):
25+
context.drb5 = DRB5 if DRB5 != "no" else ""
26+
27+
28+
@when("I blend the DRBX alleles with DRB1 allele")
29+
def step_impl(context):
30+
context.blended_drbx = pyard.dr_blender(
31+
context.drb1_slug, context.drb3, context.drb4, context.drb5
32+
)
33+
34+
35+
@then("it should blend as {DRBX_BLEND}")
36+
def step_impl(context, DRBX_BLEND):
37+
DRBX_BLEND = DRBX_BLEND if DRBX_BLEND != "nothing" else ""
38+
assert_that(context.blended_drbx, is_(DRBX_BLEND))
39+
40+
41+
@when("I blend the DRBX alleles with DRB1 allele, it shouldn't blend")
42+
def step_impl(context):
43+
pass
44+
45+
46+
@then("{expected} was expected, but found {found}")
47+
def step_impl(context, expected, found):
48+
assert_that(
49+
calling(pyard.dr_blender).with_args(
50+
context.drb1_slug, context.drb3, context.drb4, context.drb5
51+
),
52+
raises(DRBXBlenderError, f"{found} where {expected} expected"),
53+
)

0 commit comments

Comments
 (0)