Skip to content

Commit 21ccae1

Browse files
committed
Strategies
- Refactored so that each reduction mode is a strategy.
1 parent de49ecf commit 21ccae1

18 files changed

+323
-138
lines changed

pyard/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def init(
3636
cache_size: int = DEFAULT_CACHE_SIZE,
3737
config: dict = None,
3838
):
39-
from .ard import ARD
39+
from .ard_refactored import ARD
4040

4141
ard = ARD(
4242
imgt_version=imgt_version,

pyard/ard_refactored.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99
from . import smart_sort
1010
from .constants import (
1111
HLA_regex,
12-
VALID_REDUCTION_TYPES,
1312
DEFAULT_CACHE_SIZE,
1413
G_GROUP_LOCI,
14+
VALID_REDUCTION_TYPE,
1515
)
1616
from .exceptions import InvalidMACError, InvalidTypingError
1717
from .handlers import (
@@ -156,7 +156,7 @@ def __del__(self):
156156

157157
@functools.lru_cache(maxsize=DEFAULT_CACHE_SIZE)
158158
def _redux_allele(
159-
self, allele: str, redux_type: VALID_REDUCTION_TYPES, re_ping=True
159+
self, allele: str, redux_type: VALID_REDUCTION_TYPE, re_ping=True
160160
) -> str:
161161
"""Core allele reduction with ping logic"""
162162
# Handle HLA- prefix
@@ -210,7 +210,7 @@ def _redux_allele(
210210
return self.allele_reducer.reduce_allele(allele, redux_type, re_ping)
211211

212212
@functools.lru_cache(maxsize=DEFAULT_CACHE_SIZE)
213-
def redux(self, glstring: str, redux_type: VALID_REDUCTION_TYPES = "lgx") -> str:
213+
def redux(self, glstring: str, redux_type: VALID_REDUCTION_TYPE = "lgx") -> str:
214214
"""Main redux method using specialized handlers"""
215215
# Handle GL string delimiters first
216216
processed_gl = self.gl_processor.process_gl_string(glstring, redux_type)

pyard/constants.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,15 @@
2020
# > http://www.opensource.org/licenses/lgpl-license.php
2121
#
2222
import re
23+
import typing
2324

2425
DEFAULT_CACHE_SIZE = 1_000
2526

2627
HLA_regex = re.compile("^HLA-")
2728

28-
VALID_REDUCTION_TYPES = ("G", "P", "lg", "lgx", "W", "exon", "U2", "S")
29+
VALID_REDUCTION_MODES = ("G", "P", "lg", "lgx", "W", "exon", "U2", "S")
30+
VALID_REDUCTION_TYPE = typing.Literal[VALID_REDUCTION_MODES]
31+
2932
expression_chars = ("N", "Q", "L", "S")
3033
# List of P and G characters
3134
P_and_G_chars = ("P", "G")

pyard/handlers/allele_reducer.py

Lines changed: 9 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -1,147 +1,34 @@
11
# -*- coding: utf-8 -*-
22

3-
import functools
43
from typing import TYPE_CHECKING
54

6-
from ..constants import VALID_REDUCTION_TYPES, expression_chars
7-
from ..exceptions import InvalidAlleleError
8-
from ..misc import get_n_field_allele
5+
from ..constants import VALID_REDUCTION_TYPE
6+
from ..strategies.strategy_factory import StrategyFactory
97

108
if TYPE_CHECKING:
119
from ..ard import ARD
1210

1311

1412
class AlleleReducer:
15-
"""Handles core allele reduction logic"""
13+
"""Handles core allele reduction logic using Strategy Pattern"""
1614

1715
def __init__(self, ard_instance: "ARD"):
1816
self.ard = ard_instance
17+
self.strategy_factory = StrategyFactory(ard_instance)
1918

2019
def reduce_allele(
21-
self, allele: str, redux_type: VALID_REDUCTION_TYPES, re_ping=True
20+
self, allele: str, redux_type: VALID_REDUCTION_TYPE, re_ping=True
2221
) -> str:
23-
"""Core allele reduction logic extracted from _redux_allele"""
24-
25-
if redux_type == "G" and allele in self.ard.ars_mappings.g_group:
26-
if allele in self.ard.ars_mappings.dup_g:
27-
return self.ard.ars_mappings.dup_g[allele]
28-
else:
29-
return self.ard.ars_mappings.g_group[allele]
30-
31-
elif redux_type == "P" and allele in self.ard.ars_mappings.p_group:
32-
return self.ard.ars_mappings.p_group[allele]
33-
34-
elif redux_type in ["lgx", "lg"]:
35-
if allele in self.ard.ars_mappings.lgx_group:
36-
redux_allele = self.ard.ars_mappings.lgx_group[allele]
37-
else:
38-
redux_allele = ":".join(allele.split(":")[0:2])
39-
if redux_type == "lg":
40-
return self._add_lg_suffix(redux_allele)
41-
return redux_allele
42-
43-
elif redux_type == "W":
44-
if self.ard._is_who_allele(allele):
45-
return allele
46-
if allele in self.ard.code_mappings.who_group:
47-
return self.ard.redux(
48-
"/".join(self.ard.code_mappings.who_group[allele]), redux_type
49-
)
50-
else:
51-
return allele
52-
53-
elif redux_type == "exon":
54-
return self._handle_exon_reduction(allele)
55-
56-
elif redux_type == "U2":
57-
return self._handle_u2_reduction(allele)
58-
59-
elif redux_type == "S":
60-
return self._handle_serology_reduction(allele)
61-
62-
else:
63-
return self._handle_default_reduction(allele)
22+
"""Core allele reduction logic using Strategy Pattern"""
23+
strategy = self.strategy_factory.get_strategy(redux_type)
24+
return strategy.reduce(allele)
6425

6526
def _add_lg_suffix(self, redux_allele):
66-
"""Add lg suffix to reduced allele"""
27+
"""Add lg suffix to reduced allele - kept for backward compatibility"""
6728
if "/" in redux_allele:
6829
return "/".join(
6930
[self._add_lg_suffix(allele) for allele in redux_allele.split("/")]
7031
)
7132
if self.ard._config["ARS_as_lg"]:
7233
return redux_allele + "ARS"
7334
return redux_allele + "g"
74-
75-
def _handle_exon_reduction(self, allele):
76-
"""Handle exon reduction type"""
77-
if allele in self.ard.ars_mappings.exon_group:
78-
exon_group_allele = self.ard.ars_mappings.exon_group[allele]
79-
last_char = allele[-1]
80-
if last_char in expression_chars:
81-
exon_short_null_allele = exon_group_allele + last_char
82-
if self.ard.is_shortnull(exon_short_null_allele):
83-
return exon_short_null_allele
84-
return exon_group_allele
85-
else:
86-
w_redux = self.ard.redux(allele, "W")
87-
if w_redux == allele or len(w_redux.split(":")) == 2:
88-
return allele
89-
else:
90-
return self.ard.redux(w_redux, "exon")
91-
92-
def _handle_u2_reduction(self, allele):
93-
"""Handle U2 reduction type"""
94-
allele_fields = allele.split(":")
95-
if len(allele_fields) == 2:
96-
return allele
97-
allele_2_fields = get_n_field_allele(allele, 2, preserve_expression=True)
98-
if self.ard._is_allele_in_db(allele_2_fields):
99-
return allele_2_fields
100-
else:
101-
return self.reduce_allele(allele, "lgx")
102-
103-
def _handle_serology_reduction(self, allele):
104-
"""Handle serology reduction type"""
105-
from .. import db
106-
from ..misc import is_2_field_allele
107-
108-
if is_2_field_allele(allele):
109-
allele = self.reduce_allele(allele, "lgx")
110-
serology_mapping = db.find_serology_for_allele(
111-
self.ard.db_connection, allele, "lgx_allele_list"
112-
)
113-
else:
114-
serology_mapping = db.find_serology_for_allele(
115-
self.ard.db_connection, allele
116-
)
117-
118-
serology_set = set()
119-
for serology, allele_list in serology_mapping.items():
120-
if allele in allele_list.split("/"):
121-
serology_set.add(serology)
122-
123-
if not serology_set and is_2_field_allele(allele):
124-
for serology, allele_list in serology_mapping.items():
125-
allele_list_lgx = self.ard.redux(allele_list, "lgx")
126-
if allele in allele_list_lgx.split("/"):
127-
serology_set.add(serology)
128-
129-
return "/".join(
130-
sorted(
131-
serology_set, key=functools.cmp_to_key(self.ard.smart_sort_comparator)
132-
)
133-
)
134-
135-
def _handle_default_reduction(self, allele):
136-
"""Handle default reduction cases"""
137-
if allele.endswith("P"):
138-
if allele in self.ard.ars_mappings.p_group.values():
139-
return allele
140-
elif allele.endswith("G"):
141-
if allele in self.ard.ars_mappings.g_group.values():
142-
return allele
143-
144-
if self.ard._is_allele_in_db(allele):
145-
return allele
146-
else:
147-
raise InvalidAlleleError(f"{allele} is an invalid allele.")

pyard/handlers/gl_string_processor.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import functools
44
from typing import List, TYPE_CHECKING
55

6-
from ..constants import VALID_REDUCTION_TYPES
6+
from ..constants import VALID_REDUCTION_TYPE
77
from ..misc import validate_reduction_type
88

99
if TYPE_CHECKING:
@@ -17,7 +17,7 @@ def __init__(self, ard_instance: "ARD"):
1717
self.ard = ard_instance
1818

1919
def process_gl_string(
20-
self, glstring: str, redux_type: VALID_REDUCTION_TYPES = "lgx"
20+
self, glstring: str, redux_type: VALID_REDUCTION_TYPE = "lgx"
2121
) -> str:
2222
"""Main GL string processing logic extracted from redux method"""
2323
validate_reduction_type(redux_type)

pyard/misc.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
import tempfile
2525
from typing import List
2626

27-
from pyard.constants import VALID_REDUCTION_TYPES, expression_chars, P_and_G_chars
27+
from pyard.constants import VALID_REDUCTION_MODES, expression_chars, P_and_G_chars
2828

2929

3030
def get_n_field_allele(allele: str, n: int, preserve_expression=False) -> str:
@@ -151,5 +151,5 @@ def get_default_db_directory():
151151

152152

153153
def validate_reduction_type(ars_type):
154-
if ars_type not in VALID_REDUCTION_TYPES:
155-
raise ValueError(f"Reduction type needs to be one of {VALID_REDUCTION_TYPES}")
154+
if ars_type not in VALID_REDUCTION_MODES:
155+
raise ValueError(f"Reduction type needs to be one of {VALID_REDUCTION_MODES}")

pyard/strategies/__init__.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# -*- coding: utf-8 -*-
2+
3+
from .base_strategy import ReductionStrategy
4+
from .g_strategy import GGroupStrategy
5+
from .p_strategy import PGroupStrategy
6+
from .lg_strategy import LGStrategy, LGXStrategy
7+
from .w_strategy import WStrategy
8+
from .exon_strategy import ExonStrategy
9+
from .u2_strategy import U2Strategy
10+
from .s_strategy import SStrategy
11+
from .default_strategy import DefaultStrategy
12+
from .strategy_factory import StrategyFactory
13+
14+
__all__ = [
15+
"ReductionStrategy",
16+
"GGroupStrategy",
17+
"PGroupStrategy",
18+
"LGStrategy",
19+
"LGXStrategy",
20+
"WStrategy",
21+
"ExonStrategy",
22+
"U2Strategy",
23+
"SStrategy",
24+
"DefaultStrategy",
25+
"StrategyFactory",
26+
]

pyard/strategies/base_strategy.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# -*- coding: utf-8 -*-
2+
3+
from abc import ABC, abstractmethod
4+
from typing import TYPE_CHECKING
5+
6+
if TYPE_CHECKING:
7+
from ..ard import ARD
8+
9+
10+
class ReductionStrategy(ABC):
11+
"""Base class for all reduction strategies"""
12+
13+
def __init__(self, ard_instance: "ARD"):
14+
self.ard = ard_instance
15+
16+
@abstractmethod
17+
def reduce(self, allele: str) -> str:
18+
"""Reduce allele according to this strategy"""
19+
pass
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# -*- coding: utf-8 -*-
2+
from typing_extensions import override
3+
4+
from .base_strategy import ReductionStrategy
5+
6+
7+
class DefaultStrategy(ReductionStrategy):
8+
"""Default strategy for handling P/G suffixes and validation"""
9+
10+
@override
11+
def reduce(self, allele: str) -> str:
12+
# Make this an explicit lookup to the g_group or p_group table
13+
# for stringent validation
14+
if allele.endswith("P"):
15+
if allele in self.ard.ars_mappings.p_group.values():
16+
return allele
17+
elif allele.endswith("G"):
18+
if allele in self.ard.ars_mappings.g_group.values():
19+
return allele
20+
21+
if self.ard._is_allele_in_db(allele):
22+
return allele
23+
else:
24+
from ..exceptions import InvalidAlleleError
25+
26+
raise InvalidAlleleError(f"{allele} is an invalid allele.")

pyard/strategies/exon_strategy.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# -*- coding: utf-8 -*-
2+
from typing import override
3+
4+
from .base_strategy import ReductionStrategy
5+
6+
7+
class ExonStrategy(ReductionStrategy):
8+
"""Strategy for exon reduction"""
9+
10+
@override
11+
def reduce(self, allele: str) -> str:
12+
if allele in self.ard.ars_mappings.exon_group:
13+
exon_group_allele = self.ard.ars_mappings.exon_group[allele]
14+
# Check if the 3 field exon allele has a 4 field alleles
15+
# that all have the same expression characters
16+
from ..constants import expression_chars
17+
18+
last_char = allele[-1]
19+
if last_char in expression_chars:
20+
exon_short_null_allele = exon_group_allele + last_char
21+
if self.ard.is_shortnull(exon_short_null_allele):
22+
return exon_short_null_allele
23+
return exon_group_allele
24+
else:
25+
# Expand to W level and then reduce to exon
26+
w_redux = self.ard.redux(allele, "W")
27+
# If the W redux produces 2 field allele or the same allele, don't recurse
28+
if w_redux == allele or len(w_redux.split(":")) == 2:
29+
return allele
30+
else:
31+
# recurse with the W fields
32+
return self.ard.redux(w_redux, "exon")

0 commit comments

Comments
 (0)