Skip to content

Commit 05cea72

Browse files
committed
update client and add test
1 parent 3a3bce3 commit 05cea72

File tree

3 files changed

+89
-74
lines changed

3 files changed

+89
-74
lines changed

src/anyvlm/anyvar/base_client.py

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@
22

33
import abc
44

5-
from anyvar.utils.types import VrsObject
6-
7-
from anyvlm.schemas.domain import AlleleFrequencyAnnotation
5+
from anyvar.utils.types import VrsVariation
86

97
# define constant to use as AnyVar annotation type
108
AF_ANNOTATION_TYPE = "cohort_allele_frequency"
@@ -18,24 +16,17 @@ class BaseAnyVarClient(abc.ABC):
1816
"""Interface elements for an AnyVar client"""
1917

2018
@abc.abstractmethod
21-
def put_objects(self, objects: list[VrsObject]) -> None:
19+
def put_objects(self, objects: list[VrsVariation]) -> list[VrsVariation]:
2220
"""Register objects with AnyVar
2321
2422
:param objects: variation objects to register
25-
"""
26-
27-
@abc.abstractmethod
28-
def put_af_annotation(self, key: str, af: AlleleFrequencyAnnotation) -> None:
29-
"""Add an allele frequency annotation to a variation
30-
31-
:param key: VRS ID for variation being annotated
32-
:param af: frequency data for for annotation
23+
:return: completed VRS objects
3324
"""
3425

3526
@abc.abstractmethod
3627
def search_by_interval(
3728
self, accession: str, start: int, end: int
38-
) -> list[VrsObject]:
29+
) -> list[VrsVariation]:
3930
"""Get all variation IDs located within the specified range
4031
4132
:param accession: sequence accession
@@ -44,15 +35,6 @@ def search_by_interval(
4435
:return: list of matching variant objects
4536
"""
4637

47-
@abc.abstractmethod
48-
def get_af_annotation(self, key: str) -> AlleleFrequencyAnnotation | None:
49-
"""Get AF annotation for a key (object ID)
50-
51-
:param key: object ID (presumably VRS ID)
52-
:return: AF object if available, `None` otherwise
53-
:raise KeyError: if object with given ID doesn't exist
54-
"""
55-
5638
@abc.abstractmethod
5739
def close(self) -> None:
5840
"""Clean up AnyVar connection."""

src/anyvlm/anyvar/http_client.py

Lines changed: 18 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
11
"""Provide abstraction for a VLM-to-AnyVar connection."""
22

3-
import abc
4-
from http import HTTPStatus
5-
63
import requests
7-
from anyvar.utils.types import VrsObject
4+
from anyvar.utils.types import VrsVariation
5+
from ga4gh.vrs import models
86

9-
from anyvlm.anyvar.base_client import AF_ANNOTATION_TYPE, AmbiguousAnnotationError
10-
from anyvlm.schemas.domain import AlleleFrequencyAnnotation
7+
from anyvlm.anyvar.base_client import BaseAnyVarClient
118

129

13-
class HttpAnyVarClient(abc.ABC):
10+
class HttpAnyVarClient(BaseAnyVarClient):
1411
"""AnyVar HTTP-based client"""
1512

1613
def __init__(
@@ -24,9 +21,13 @@ def __init__(
2421
self.hostname = hostname
2522
self.request_timeout = request_timeout
2623

27-
def put_objects(self, objects: list[VrsObject]) -> list[VrsObject]:
24+
def put_objects(self, objects: list[VrsVariation]) -> list[VrsVariation]:
2825
"""Register objects with AnyVar
2926
27+
This method is intentionally naive. Future improvements could include
28+
* A better way to batch requests together to mitigate HTTP latency
29+
* A smarter dispatch for reconstructing variation model instances
30+
3031
:param objects: variation objects to register
3132
:return: completed VRS objects
3233
"""
@@ -39,29 +40,18 @@ def put_objects(self, objects: list[VrsObject]) -> list[VrsObject]:
3940
timeout=self.request_timeout,
4041
)
4142
response.raise_for_status()
42-
results.append(response.json()["object"])
43+
result_object = response.json()["object"]
44+
if result_object.get("type") == "Allele":
45+
results.append(models.Allele(**result_object))
46+
else:
47+
raise NotImplementedError(
48+
f"Unsupported object type: {result_object.get('type')}"
49+
)
4350
return results
4451

45-
def put_af_annotation(self, key: str, af: AlleleFrequencyAnnotation) -> None:
46-
"""Add an allele frequency annotation to a variation
47-
48-
:param key: VRS ID for variation being annotated
49-
:param af: frequency data for for annotation
50-
"""
51-
response = requests.post(
52-
f"{self.hostname}/variation/{key}/annotations",
53-
json={
54-
"annotation_type": AF_ANNOTATION_TYPE,
55-
"annotation_value": af.model_dump(mode="json", exclude_none=True),
56-
},
57-
timeout=self.request_timeout,
58-
)
59-
response.raise_for_status()
60-
61-
@abc.abstractmethod
6252
def search_by_interval(
6353
self, accession: str, start: int, end: int
64-
) -> list[VrsObject]:
54+
) -> list[VrsVariation]:
6555
"""Get all variation IDs located within the specified range
6656
6757
:param accession: sequence accession
@@ -76,31 +66,7 @@ def search_by_interval(
7666
response.raise_for_status()
7767
return response.json()["variations"]
7868

79-
def get_af_annotation(self, key: str) -> AlleleFrequencyAnnotation | None:
80-
"""Get AF annotation for a key (object ID)
81-
82-
:param key: object ID (presumably VRS ID)
83-
:return: AF object if available, `None` otherwise
84-
:raise KeyError: if object with given ID doesn't exist
85-
"""
86-
response = requests.get(
87-
f"{self.hostname}/variation/{key}/annotations/{AF_ANNOTATION_TYPE}",
88-
timeout=self.request_timeout,
89-
)
90-
try:
91-
response.raise_for_status()
92-
except requests.HTTPError as e:
93-
if response.status_code == HTTPStatus.NOT_FOUND:
94-
raise KeyError from e
95-
raise
96-
data = response.json()
97-
if len(data["annotations"]) == 0:
98-
return None
99-
if len(data["annotations"]) > 1:
100-
raise AmbiguousAnnotationError
101-
return AlleleFrequencyAnnotation(**data["annotations"][0]["annotation_value"])
102-
103-
def close(self) -> None: # noqa: B027
69+
def close(self) -> None:
10470
"""Clean up AnyVar connection.
10571
10672
This is a no-op for this class.
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
from collections import namedtuple
2+
from http import HTTPStatus
3+
4+
import pytest
5+
import responses
6+
from anyvar.utils.types import recursive_identify
7+
from ga4gh.vrs import models
8+
from responses import matchers
9+
10+
from anyvlm.anyvar.http_client import HttpAnyVarClient
11+
12+
13+
@pytest.fixture
14+
def client() -> HttpAnyVarClient:
15+
return HttpAnyVarClient()
16+
17+
18+
# simple namedtuple to make accessing different parts of the fixture easier
19+
_InputVariant = namedtuple(
20+
"InputVariant", ["fn_input", "anyvar_input", "anyvar_return", "expected"]
21+
)
22+
23+
24+
@pytest.fixture
25+
def input_variants() -> list[_InputVariant]:
26+
alleles = [
27+
models.Allele(
28+
location=models.SequenceLocation(
29+
sequenceReference=models.SequenceReference(
30+
refgetAccession="SQ.ss8r_wB0-b9r44TQTMmVTI92884QvBiB"
31+
),
32+
start=87894076,
33+
end=87894077,
34+
),
35+
state=models.LiteralSequenceExpression(sequence=models.sequenceString("T")),
36+
)
37+
]
38+
return [
39+
_InputVariant(
40+
fn_input=a,
41+
anyvar_input=a.model_dump(exclude_none=True, mode="json"),
42+
anyvar_return=recursive_identify(a).model_dump(
43+
exclude_none=True, mode="json"
44+
),
45+
expected=recursive_identify(a),
46+
)
47+
for a in alleles
48+
]
49+
50+
51+
@responses.activate
52+
def test_put_objects(client: HttpAnyVarClient, input_variants: list[_InputVariant]):
53+
for input_variant in input_variants:
54+
responses.add(
55+
responses.PUT,
56+
"http://localhost:8000/vrs_variation",
57+
headers={"Content-Type": "application/json"},
58+
match=[matchers.json_params_matcher(input_variant.anyvar_input)],
59+
json={
60+
"messages": [],
61+
"object": input_variant.anyvar_return,
62+
"object_id": input_variant.anyvar_return["id"],
63+
},
64+
status=HTTPStatus.ACCEPTED,
65+
)
66+
result = client.put_objects([input_variant.fn_input])
67+
assert result == [input_variant.expected]

0 commit comments

Comments
 (0)