Skip to content

Commit 1d7de3c

Browse files
committed
stash
1 parent 4114530 commit 1d7de3c

5 files changed

Lines changed: 112 additions & 105 deletions

File tree

src/cool_seq_tool/mappers/exon_genomic_coords.py

Lines changed: 8 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -944,10 +944,10 @@ async def _genomic_to_tx_segment(
944944
)
945945
# gene is not required to liftover coordinates if tx_ac and genomic_ac are given, but we should set the associated gene
946946
if not gene:
947-
_gene, err_msg = await self._get_tx_ac_gene(transcript)
948-
if err_msg:
949-
return GenomicTxSeg(errors=[err_msg])
950-
gene = _gene
947+
async with self.uta_db.repository() as uta:
948+
gene = await uta.get_gene_from_tx_ac(transcript)
949+
if not gene:
950+
return GenomicTxSeg(errors=[f"No gene(s) found given {transcript}"])
951951

952952
tx_exons = await self._get_all_exon_coords(
953953
tx_ac=transcript, genomic_ac=genomic_ac
@@ -967,9 +967,10 @@ async def _genomic_to_tx_segment(
967967

968968
# Validate that the breakpoint occurs within 150 bp of the first and last exon for the selected transcript.
969969
# A breakpoint beyond this range is likely erroneous.
970-
coordinate_check = await self._validate_genomic_breakpoint(
971-
pos=genomic_pos, genomic_ac=genomic_ac, tx_ac=transcript
972-
)
970+
async with self.uta_db.repository() as uta:
971+
coordinate_check = await uta.validate_genomic_breakpoint(
972+
pos=genomic_pos, genomic_ac=genomic_ac, tx_ac=transcript
973+
)
973974
if not coordinate_check:
974975
msg = (
975976
f"{genomic_pos} on {genomic_ac} occurs more than 150 bp outside the "
@@ -1053,69 +1054,6 @@ async def _get_grch38_pos(
10531054
)
10541055
return liftover_data[1] if liftover_data else None
10551056

1056-
async def _validate_genomic_breakpoint(
1057-
self,
1058-
pos: int,
1059-
genomic_ac: str,
1060-
tx_ac: str,
1061-
) -> bool:
1062-
"""Validate that a genomic coordinate falls within the first and last exon
1063-
for a transcript on a given accession
1064-
1065-
:param pos: Genomic position on ``genomic_ac``
1066-
:param genomic_ac: RefSeq genomic accession, e.g. ``"NC_000007.14"``
1067-
:param transcript: A transcript accession
1068-
:return: ``True`` if the coordinate falls within 150bp of the first and last exon
1069-
for the transcript, ``False`` if not. Breakpoints past this threshold
1070-
are likely erroneous.
1071-
"""
1072-
query = """
1073-
WITH tx_boundaries AS (
1074-
SELECT
1075-
MIN(alt_start_i) AS min_start,
1076-
MAX(alt_end_i) AS max_end
1077-
FROM tx_exon_aln_mv
1078-
WHERE tx_ac = %(tx_ac)s
1079-
AND alt_ac = %(genomic_ac)s
1080-
)
1081-
SELECT * FROM tx_boundaries
1082-
WHERE %(pos)s between (tx_boundaries.min_start - 150) and (tx_boundaries.max_end + 150);
1083-
"""
1084-
async with self.uta_db.repository() as uta:
1085-
cursor = await uta.execute_query(
1086-
query, {"tx_ac": tx_ac, "genomic_ac": genomic_ac, "pos": pos}
1087-
)
1088-
result = await cursor.fetchone()
1089-
return bool(result)
1090-
1091-
async def _get_tx_ac_gene(
1092-
self,
1093-
tx_ac: str,
1094-
) -> tuple[str | None, str | None]:
1095-
"""Get gene given a transcript.
1096-
1097-
If multiple genes are found for a given ``tx_ac``, only one
1098-
gene will be returned.
1099-
1100-
:param tx_ac: RefSeq transcript, e.g. ``"NM_004333.6"``
1101-
:return: HGNC gene symbol associated to transcript and
1102-
warning
1103-
"""
1104-
query = """
1105-
SELECT DISTINCT hgnc
1106-
FROM tx_exon_aln_mv
1107-
WHERE tx_ac = %(tx_ac)s
1108-
ORDER BY hgnc
1109-
LIMIT 1;
1110-
"""
1111-
async with self.uta_db.repository() as uta:
1112-
cursor = await uta.execute_query(query, {"tx_ac": tx_ac})
1113-
result = await cursor.fetchone()
1114-
if not result:
1115-
return None, f"No gene(s) found given {tx_ac}"
1116-
1117-
return result[0], None
1118-
11191057
@staticmethod
11201058
def _is_exonic_breakpoint(pos: int, tx_genomic_coords: list[_ExonCoord]) -> bool:
11211059
"""Check if a breakpoint occurs on an exon

src/cool_seq_tool/mappers/mane_transcript.py

Lines changed: 4 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -236,15 +236,7 @@ async def _liftover_to_38(self, genomic_tx_data: GenomicTxMetadata) -> None:
236236
chromosome, _ = descr
237237

238238
async with self.uta_db.repository() as uta:
239-
cursor = await uta.execute_query(
240-
"""SELECT DISTINCT alt_ac
241-
FROM tx_exon_aln_mv
242-
WHERE tx_ac = %(tx_ac)s;
243-
""",
244-
{"tx_ac": genomic_tx_data.tx_ac},
245-
)
246-
nc_acs = await cursor.fetchall()
247-
nc_acs = [nc_ac[0] for nc_ac in nc_acs]
239+
nc_acs = await uta.get_alt_acs_for_tx(genomic_tx_data.tx_ac)
248240
if nc_acs == [genomic_tx_data.alt_ac]:
249241
_logger.warning(
250242
"UTA does not have GRCh38 assembly for %s",
@@ -263,26 +255,9 @@ async def _liftover_to_38(self, genomic_tx_data: GenomicTxMetadata) -> None:
263255
genomic_tx_data, "alt_pos_change_range", chromosome, Assembly.GRCH38
264256
)
265257

266-
# Change alt_ac to most recent
267-
if genomic_tx_data.alt_ac.startswith("EN"):
268-
order_by_cond = "ORDER BY alt_ac DESC;"
269-
else:
270-
order_by_cond = """
271-
ORDER BY CAST(SUBSTR(alt_ac, position('.' in alt_ac) + 1,
272-
LENGTH(alt_ac)) AS INT) DESC;
273-
"""
274-
query = f"""
275-
SELECT alt_ac
276-
FROM genomic
277-
WHERE alt_ac LIKE %(ac_pattern)s
278-
{order_by_cond}
279-
""" # noqa: S608
280258
async with self.uta_db.repository() as uta:
281-
cursor = await uta.execute_query(
282-
query, {"ac_pattern": f"{genomic_tx_data.alt_ac.split('.')[0]}%"}
283-
)
284-
nc_acs = await cursor.fetchall()
285-
genomic_tx_data.alt_ac = nc_acs[0][0]
259+
alt_ac = await uta.get_newest_assembly_ac(genomic_tx_data.alt_ac)
260+
genomic_tx_data.alt_ac = alt_ac[0]
286261

287262
def _set_liftover(
288263
self,
@@ -1280,7 +1255,7 @@ async def g_to_mane_c(
12801255
coordinate_type = CoordinateType.INTER_RESIDUE
12811256

12821257
async with self.uta_db.repository() as uta:
1283-
validation_result = uta.validate_genomic_ac(ac)
1258+
validation_result = await uta.validate_genomic_ac(ac)
12841259
if not validation_result:
12851260
_logger.warning("Genomic accession does not exist: %s", ac)
12861261
return None

src/cool_seq_tool/schemas.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Defines attribute constants, useful object structures, and API response schemas."""
22

33
import datetime
4-
from enum import Enum, IntEnum
4+
from enum import IntEnum, StrEnum
55
from typing import Literal
66

77
from ga4gh.vrs.models import SequenceLocation
@@ -17,7 +17,7 @@
1717
_now = str(datetime.datetime.now(tz=datetime.UTC))
1818

1919

20-
class AnnotationLayer(str, Enum):
20+
class AnnotationLayer(StrEnum):
2121
"""Create enum for supported annotation layers"""
2222

2323
PROTEIN = "p"
@@ -32,7 +32,7 @@ class Strand(IntEnum):
3232
NEGATIVE = -1
3333

3434

35-
class Assembly(str, Enum):
35+
class Assembly(StrEnum):
3636
"""Define supported genomic assemblies. Must be defined in ascending order"""
3737

3838
GRCH37 = "GRCh37"
@@ -44,14 +44,14 @@ def values(cls) -> list[str]:
4444
return [item.value for item in cls]
4545

4646

47-
class ManeStatus(str, Enum):
47+
class ManeStatus(StrEnum):
4848
"""Define constraints for mane status"""
4949

5050
SELECT = "mane_select"
5151
PLUS_CLINICAL = "mane_plus_clinical"
5252

5353

54-
class TranscriptPriority(str, Enum):
54+
class TranscriptPriority(StrEnum):
5555
"""Create Enum for Transcript Priority labels"""
5656

5757
MANE_SELECT = ManeStatus.SELECT.value
@@ -60,7 +60,7 @@ class TranscriptPriority(str, Enum):
6060
GRCH38 = "grch38"
6161

6262

63-
class CoordinateType(str, Enum):
63+
class CoordinateType(StrEnum):
6464
"""Create Enum for coordinate types.
6565
6666
It is preferred to operate in inter-residue coordinates, but users should be

src/cool_seq_tool/sources/uta_database.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,10 @@ async def execute_query(
113113
queries using the same DB connection. However, that means they are responsible
114114
for managing the cursor themselves.
115115
116+
For the sake of compactness and separation of concerns, other modules in CoolSeqTool
117+
should avoid use of this method, and should instead add new methods to the repository
118+
class itself.
119+
116120
:param q: raw query. May need to specify schema depending on connection context.
117121
:param params: query variables, if needed. These should not be hard-coded into the query.
118122
:return: query result cursor
@@ -845,6 +849,73 @@ async def get_transcripts_from_genomic_pos(
845849
results = await cursor.fetchall()
846850
return [item for sublist in results for item in sublist]
847851

852+
async def get_alt_acs_for_tx(self, tx_ac: str) -> list[str]:
853+
"""Return genomic reference sequences associated with transcript accession
854+
855+
:param tx_ac: transcript accession
856+
:return: list of genomic accessions for which alignments exist to ``tx_ac``
857+
"""
858+
query = """
859+
SELECT DISTINCT alt_ac
860+
FROM exon_set
861+
WHERE tx_ac = %(tx_ac)s
862+
AND alt_aln_method != 'transcript';
863+
"""
864+
cursor = await self.execute_query(query, {"tx_ac": tx_ac})
865+
results = await cursor.fetchall()
866+
return [r[0] for r in results]
867+
868+
async def get_gene_from_tx_ac(self, tx_ac: str) -> str | None:
869+
"""Return HGNC gene name for a transcript accession
870+
871+
:param tx_ac: transcript accession
872+
:return: gene name, if found
873+
"""
874+
query = """
875+
SELECT hgnc
876+
FROM tx_exon_aln_mv
877+
WHERE tx_ac = %(tx_ac)s;
878+
"""
879+
cursor = await self.execute_query(query, {"tx_ac": tx_ac})
880+
result = await cursor.fetchone()
881+
if result:
882+
return result[0]
883+
return None
884+
885+
async def validate_genomic_breakpoint(
886+
self,
887+
pos: int,
888+
genomic_ac: str,
889+
tx_ac: str,
890+
) -> bool:
891+
"""Validate that a genomic coordinate falls within the first and last exon
892+
for a transcript on a given accession
893+
894+
:param pos: Genomic position on ``genomic_ac``
895+
:param genomic_ac: RefSeq genomic accession, e.g. ``"NC_000007.14"``
896+
:param transcript: A transcript accession
897+
:return: ``True`` if the coordinate falls within 150bp of the first and last exon
898+
for the transcript, ``False`` if not. Breakpoints past this threshold
899+
are likely erroneous.
900+
"""
901+
query = """
902+
WITH tx_boundaries AS (
903+
SELECT
904+
MIN(alt_start_i) AS min_start,
905+
MAX(alt_end_i) AS max_end
906+
FROM tx_exon_aln_mv
907+
WHERE tx_ac = %(tx_ac)s
908+
AND alt_ac = %(genomic_ac)s
909+
)
910+
SELECT * FROM tx_boundaries
911+
WHERE %(pos)s between (tx_boundaries.min_start - 150) and (tx_boundaries.max_end + 150);
912+
"""
913+
cursor = await self.execute_query(
914+
query, {"tx_ac": tx_ac, "genomic_ac": genomic_ac, "pos": pos}
915+
)
916+
result = await cursor.fetchone()
917+
return bool(result)
918+
848919

849920
class ParseResult(UrlLibParseResult):
850921
"""Subclass of url.ParseResult that adds database and schema methods, and provides stringification.

tests/sources/test_uta_database.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,15 @@ async def test_get_alt_ac_start_or_end(
366366
await uta_repo.get_alt_ac_start_or_end("NM_152263.63", 822, 892, None)
367367

368368

369+
@pytest.mark.asyncio
370+
async def test_get_alt_acs_for_tx(uta_repo: UtaRepository):
371+
resp = await uta_repo.get_alt_acs_for_tx("NM_004333.6")
372+
assert set(resp) == {"NC_000007.13", "NC_000007.14", "NC_060931.1"}
373+
374+
resp = await uta_repo.get_alt_acs_for_tx("NM_99999999.99")
375+
assert resp == []
376+
377+
369378
@pytest.mark.asyncio
370379
async def test_get_mane_transcripts_from_genomic_pos(uta_repo: UtaRepository):
371380
"""Test that get_mane_transcripts_from_genomic_pos works correctly"""
@@ -398,6 +407,20 @@ async def test_get_mane_transcripts_from_genomic_pos(uta_repo: UtaRepository):
398407
assert resp == []
399408

400409

410+
@pytest.mark.asyncio
411+
async def test_get_gene_from_tx_ac(uta_repo: UtaRepository):
412+
resp = await uta_repo.get_gene_from_tx_ac("NM_002529.3")
413+
assert resp == "NTRK1"
414+
415+
resp = await uta_repo.get_gene_from_tx_ac("NM_99999999.99")
416+
assert resp is None
417+
418+
419+
@pytest.mark.asyncio
420+
async def test_validate_genomic_breakpoint(uta_repo: UtaRepository):
421+
raise NotImplementedError
422+
423+
401424
@pytest.mark.parametrize(
402425
("raw_url", "expected"),
403426
[

0 commit comments

Comments
 (0)