Skip to content

Commit 1b520b6

Browse files
resolve merge conflicts
2 parents f3587de + 60be88e commit 1b520b6

21 files changed

+990
-17
lines changed

.github/workflows/python-package.yaml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,45 @@ permissions:
88
contents: read
99

1010
jobs:
11+
test:
12+
name: test
13+
runs-on: ubuntu-latest
14+
strategy:
15+
matrix:
16+
python-version: ["3.11", "3.12", "3.13"]
17+
services:
18+
postgres:
19+
image: postgres:17
20+
env:
21+
POSTGRES_PASSWORD: postgres
22+
options: >-
23+
--health-cmd pg_isready
24+
--health-interval 10s
25+
--health-timeout 5s
26+
--health-retries 5
27+
ports:
28+
- 5432:5432
29+
steps:
30+
- uses: actions/checkout@v4
31+
with:
32+
submodules: 'true'
33+
- name: Set up Python
34+
uses: actions/setup-python@v5
35+
with:
36+
python-version: "${{ matrix.python-version }}"
37+
38+
- name: Install uv
39+
uses: astral-sh/setup-uv@v6
40+
with:
41+
enable-cache: true
42+
43+
- name: Install dependencies
44+
run: uv sync --extra test
45+
46+
- name: Run tests
47+
run: uv run pytest
48+
env:
49+
ANYVLM_ANYVAR_TEST_STORAGE_URI: postgresql://postgres:postgres@localhost:5432/postgres
1150
lint:
1251
name: lint
1352
runs-on: ubuntu-latest

pyproject.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ classifiers = [
1717
"Operating System :: OS Independent",
1818
]
1919
dependencies = [
20-
"ga4gh.va_spec",
20+
"ga4gh.va_spec~=0.4.2",
2121
"biocommons.anyvar@git+https://github.com/biocommons/anyvar.git@storage-refactor-epic",
2222
"fastapi>=0.95.0",
2323
"python-multipart", # required for fastapi file uploads
@@ -26,6 +26,7 @@ dependencies = [
2626
"anyio",
2727
"python-dotenv",
2828
"pydantic-settings",
29+
"requests",
2930
]
3031
dynamic = ["version"]
3132

@@ -36,12 +37,14 @@ test = [
3637
"httpx",
3738
"jsonschema",
3839
"pyyaml",
40+
"pytest-recording",
3941
]
4042
dev = [
4143
"ruff==0.12.8",
4244
"pre-commit>=4.2.0",
4345
"ipykernel",
4446
"fastapi[standard]",
47+
"seqrepo-rest-service", # for generating SeqRepo-based tests fixtures
4548
]
4649

4750
[project.urls]

src/anyvlm/anyvar/base_client.py

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,40 +2,43 @@
22

33
import abc
44

5-
from anyvar.utils.types import VrsObject
5+
from anyvar.utils.types import VrsVariation
66

7-
from anyvlm.schemas.domain import AlleleFrequencyAnnotation
7+
8+
class AnyVarClientError(Exception):
9+
"""Generic client-related exception."""
10+
11+
12+
class UnidentifiedObjectError(AnyVarClientError):
13+
"""Raise if input object lacks an ID property"""
814

915

1016
class BaseAnyVarClient(abc.ABC):
1117
"""Interface elements for an AnyVar client"""
1218

1319
@abc.abstractmethod
14-
def put_objects(self, objects: list[VrsObject]) -> None:
20+
def put_objects(self, objects: list[VrsVariation]) -> None:
1521
"""Register objects with AnyVar
1622
17-
:param objects: variation objects to register
18-
"""
23+
All input objects must have a populated ID field. A validation check for this is
24+
performed before any variants are registered.
1925
20-
@abc.abstractmethod
21-
def put_af_annotation(self, key: str, af: AlleleFrequencyAnnotation) -> None:
22-
"""Add an allele frequency annotation to a variation
23-
24-
25-
:param key: VRS ID for variation being annotated
26-
:param af: frequency data for for annotation
26+
:param objects: variation objects to register
27+
:raise AnyVarClientError: for errors relating to specifics of client interface
28+
:raise UnidentifiedObjectError: if *any* provided object lacks a VRS ID
2729
"""
2830

2931
@abc.abstractmethod
3032
def search_by_interval(
3133
self, accession: str, start: int, end: int
32-
) -> list[VrsObject]:
34+
) -> list[VrsVariation]:
3335
"""Get all variation IDs located within the specified range
3436
3537
:param accession: sequence accession
3638
:param start: start position for genomic region
3739
:param end: end position for genomic region
3840
:return: list of matching variant objects
41+
:raise AnyVarClientError: if connection is unsuccessful during search query
3942
"""
4043

4144
@abc.abstractmethod

src/anyvlm/anyvar/http_client.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
"""Provide abstraction for a VLM-to-AnyVar connection."""
2+
3+
import logging
4+
5+
import requests
6+
from anyvar.utils.types import VrsVariation
7+
from ga4gh.vrs import models
8+
9+
from anyvlm.anyvar.base_client import (
10+
AnyVarClientError,
11+
BaseAnyVarClient,
12+
UnidentifiedObjectError,
13+
)
14+
15+
_logger = logging.getLogger(__name__)
16+
17+
18+
class HttpAnyVarClient(BaseAnyVarClient):
19+
"""AnyVar HTTP-based client"""
20+
21+
def __init__(
22+
self, hostname: str = "http://localhost:8000", request_timeout: int = 30
23+
) -> None:
24+
"""Initialize client instance
25+
26+
:param hostname: service API root
27+
:param request_timeout: timeout value, in seconds, for HTTP requests
28+
"""
29+
self.hostname = hostname
30+
self.request_timeout = request_timeout
31+
32+
def put_objects(self, objects: list[VrsVariation]) -> None:
33+
"""Register objects with AnyVar
34+
35+
All input objects must have a populated ID field. A validation check for this is
36+
performed before any variants are registered.
37+
38+
:param objects: variation objects to register
39+
:return: completed VRS objects
40+
:raise AnyVarClientError: if connection is unsuccessful during registration request
41+
:raise UnidentifiedObjectError: if *any* provided object lacks a VRS ID
42+
"""
43+
objects_to_submit = []
44+
for vrs_object in objects:
45+
if not vrs_object.id:
46+
_logger.error("Provided variant %s has no VRS ID", vrs_object)
47+
raise UnidentifiedObjectError
48+
objects_to_submit.append(
49+
vrs_object.model_dump(exclude_none=True, mode="json")
50+
)
51+
for vrs_object in objects_to_submit:
52+
response = requests.put(
53+
f"{self.hostname}/vrs_variation",
54+
json=vrs_object,
55+
timeout=self.request_timeout,
56+
)
57+
try:
58+
response.raise_for_status()
59+
except requests.HTTPError as e:
60+
raise AnyVarClientError from e
61+
62+
def search_by_interval(
63+
self, accession: str, start: int, end: int
64+
) -> list[VrsVariation]:
65+
"""Get all variation IDs located within the specified range
66+
67+
:param accession: sequence accession
68+
:param start: start position for genomic region
69+
:param end: end position for genomic region
70+
:return: list of matching variant objects
71+
:raise AnyVarClientError: if connection is unsuccessful during search query
72+
"""
73+
response = requests.get(
74+
f"{self.hostname}/search?accession={accession}&start={start}&end={end}",
75+
timeout=self.request_timeout,
76+
)
77+
try:
78+
response.raise_for_status()
79+
except requests.HTTPError as e:
80+
if response.json() == {
81+
"detail": "Unable to dereference provided accession ID"
82+
}:
83+
return []
84+
raise AnyVarClientError from e
85+
return [models.Allele(**v) for v in response.json()["variations"]]
86+
87+
def close(self) -> None:
88+
"""Clean up AnyVar connection.
89+
90+
This is a no-op for this class.
91+
"""

src/anyvlm/anyvar/python_client.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
"""Implement AnyVar client interface for direct Python-based access."""
2+
3+
import logging
4+
5+
from anyvar import AnyVar
6+
from anyvar.storage.base_storage import Storage
7+
from anyvar.translate.translate import Translator
8+
from anyvar.utils.types import VrsVariation
9+
10+
from anyvlm.anyvar.base_client import BaseAnyVarClient, UnidentifiedObjectError
11+
12+
_logger = logging.getLogger(__name__)
13+
14+
15+
class PythonAnyVarClient(BaseAnyVarClient):
16+
"""A Python-based AnyVar client."""
17+
18+
def __init__(self, translator: Translator, storage: Storage) -> None:
19+
"""Initialize directly-connected AnyVar client
20+
21+
:param translator: AnyVar translator instance
22+
:param storage: AnyVar storage instance
23+
"""
24+
self.av = AnyVar(translator, storage)
25+
26+
def put_objects(self, objects: list[VrsVariation]) -> None:
27+
"""Register objects with AnyVar
28+
29+
All input objects must have a populated ID field. A validation check for this is
30+
performed before any variants are registered.
31+
32+
:param objects: variation objects to register
33+
:raise UnidentifiedObjectError: if *any* provided object lacks a VRS ID
34+
"""
35+
for variant in objects:
36+
if not variant.id:
37+
_logger.error("Provided variant %s has no VRS ID", variant)
38+
raise UnidentifiedObjectError
39+
self.av.put_objects(objects) # type: ignore[reportArgumentType]
40+
41+
def search_by_interval(
42+
self, accession: str, start: int, end: int
43+
) -> list[VrsVariation]:
44+
"""Get all variation IDs located within the specified range
45+
46+
:param accession: sequence accession
47+
:param start: start position for genomic region
48+
:param end: end position for genomic region
49+
:return: list of matching variant objects
50+
"""
51+
try:
52+
if accession.startswith("ga4gh:"):
53+
ga4gh_id = accession
54+
else:
55+
ga4gh_id = self.av.translator.get_sequence_id(accession)
56+
except KeyError:
57+
return []
58+
59+
alleles = []
60+
if ga4gh_id:
61+
refget_accession = ga4gh_id.split("ga4gh:")[-1]
62+
alleles = self.av.object_store.search_alleles(refget_accession, start, end)
63+
64+
return alleles # type: ignore[reportReturnType]
65+
66+
def close(self) -> None:
67+
"""Clean up AnyVar instance."""
68+
self.av.object_store.close()

src/anyvlm/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ class Settings(BaseSettings):
2222
)
2323

2424
env: ServiceEnvironment = ServiceEnvironment.LOCAL
25+
anyvar_uri: str = "http://localhost:8000"
2526

2627

2728
@cache

src/anyvlm/main.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
"""Define core FastAPI app"""
22

3+
import logging
34
from collections.abc import AsyncGenerator
45
from contextlib import asynccontextmanager
56

7+
from anyvar.anyvar import create_storage, create_translator
68
from fastapi import FastAPI
79

810
from anyvlm import __version__
911
from anyvlm.anyvar.base_client import BaseAnyVarClient
12+
from anyvlm.anyvar.http_client import HttpAnyVarClient
13+
from anyvlm.anyvar.python_client import PythonAnyVarClient
1014
from anyvlm.config import get_config
1115
from anyvlm.schemas.common import (
1216
SERVICE_DESCRIPTION,
@@ -18,16 +22,33 @@
1822
EndpointTag,
1923
)
2024

25+
_logger = logging.getLogger(__name__)
26+
2127

2228
def create_anyvar_client(
23-
connection_string: str = "http://localhost:8000",
29+
connection_string: str | None = None,
2430
) -> BaseAnyVarClient:
2531
"""Construct new AnyVar client instance
2632
33+
If given a string for connecting to an AnyVar instance via HTTP requests, then
34+
create an HTTP-based client. Otherwise, try to use AnyVar resource factory functions
35+
for standing up a Python-based client. In the latter case, see the AnyVar documentation
36+
for configuration info (i.e. environment variables)
37+
2738
:param connection_string: description of connection param
2839
:return: client instance
2940
"""
30-
raise NotImplementedError
41+
if not connection_string:
42+
connection_string = get_config().anyvar_uri
43+
if connection_string.startswith("http://"):
44+
_logger.info(
45+
"Initializing HTTP-based AnyVar client under hostname %s", connection_string
46+
)
47+
return HttpAnyVarClient(connection_string)
48+
_logger.info("Initializing AnyVar instance directly")
49+
storage = create_storage()
50+
translator = create_translator()
51+
return PythonAnyVarClient(translator, storage)
3152

3253

3354
@asynccontextmanager

src/anyvlm/utils/types.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ class UcscAssemblyBuild(StrEnum):
3030
NucleotideSequence = Annotated[
3131
str,
3232
BeforeValidator(str.upper),
33-
StringConstraints(pattern=r"^[ACGTURYKMSWBDHVN]*$"),
33+
StringConstraints(pattern=r"^[ACGTURYKMSWBDHVN.-]*$"),
3434
]
3535

3636

0 commit comments

Comments
 (0)