diff --git a/.ebextensions/01_download_data.config b/.ebextensions/01_download_data.config index 2fad9b5..979218b 100644 --- a/.ebextensions/01_download_data.config +++ b/.ebextensions/01_download_data.config @@ -25,7 +25,7 @@ container_commands: test: test -f "/usr/local/share/seqrepo.tar.gz" command: "rm /usr/local/share/seqrepo.tar.gz" - 04_install_package: + 05_install_package: leader_only: true command: | set -euo pipefail diff --git a/README.md b/README.md index a0149bd..59802bd 100644 --- a/README.md +++ b/README.md @@ -32,14 +32,14 @@ A set of Docker Compose resources are provided as part of the AnyVLM project. Se Given an available AnyVLM node, submit a VCF which contains allele frequency data: ```bash -curl -X POST "http://localhost:8080/ingest_vcf?assembly=grch38" \ +curl -X POST "http://localhost:8080/anyvlm/ingest_vcf?assembly=grch38" \ -F "file=@/path/to/variants.vcf.gz" ``` Then, submit a query for allele frequency ```bash -curl "http://localhost:8080/variant_counts?assemblyId=GRCh38&referenceName=7&start=140714556&referenceBases=A&alternateBases=T" +curl "http://localhost:8080/anyvlm/variant_counts?assemblyId=GRCh38&referenceName=7&start=140714556&referenceBases=A&alternateBases=T" ``` A successful query returns a response like the following: diff --git a/docs/source/usage.rst b/docs/source/usage.rst index e57af62..1bb14f3 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -12,7 +12,7 @@ Given a VCF describing cohort-level allele frequency, submit a ``POST`` request .. code-block:: console - % curl -X POST "http://localhost:8080/ingest_vcf?assembly=grch38" \ + % curl -X POST "http://localhost:8080/anyvlm/ingest_vcf?assembly=grch38" \ -F "file=@/path/to/variants.vcf.gz" VCF Requirements @@ -42,7 +42,7 @@ Issue a ``GET`` request to ``/variant_counts`` with arguments for reference sequ .. code-block:: console - % curl "http://localhost:8080/variant_counts?assemblyId=GRCh38&referenceName=22&start=10510105&referenceBases=T&alternateBases=A" + % curl "http://localhost:8080/anyvlm/variant_counts?assemblyId=GRCh38&referenceName=22&start=10510105&referenceBases=T&alternateBases=A" Parameters ---------- diff --git a/src/anyvlm/main.py b/src/anyvlm/main.py index c45056d..c6cfbf5 100644 --- a/src/anyvlm/main.py +++ b/src/anyvlm/main.py @@ -144,11 +144,14 @@ async def lifespan(app: FastAPI) -> AsyncGenerator: app.state.anyvlm_storage.close() +API_PREFIX = "/anyvlm" + app = FastAPI( title="AnyVLM", description=SERVICE_DESCRIPTION, version=__version__, - docs_url="/", + docs_url=API_PREFIX, + openapi_url=f"{API_PREFIX}/openapi.json", license={ "name": "Apache 2.0", "url": "https://github.com/genomicmedlab/anyvlm/blob/main/LICENSE", @@ -161,7 +164,7 @@ async def lifespan(app: FastAPI) -> AsyncGenerator: swagger_ui_parameters={"tryItOutEnabled": True}, lifespan=lifespan, ) -app.include_router(vlm_router) +app.include_router(vlm_router, prefix=API_PREFIX) @app.exception_handler(RequestValidationError) @@ -188,7 +191,7 @@ async def validation_exception_handler( @app.get( - "/service-info", + f"{API_PREFIX}/service-info", summary="Get basic service information", description="Retrieve service metadata, such as versioning and contact info. Structured in conformance with the [GA4GH service info API specification](https://www.ga4gh.org/product/service-info/)", tags=[EndpointTag.META], diff --git a/tests/unit/test_restapi.py b/tests/unit/test_restapi.py index e057cbd..6e6e2c8 100644 --- a/tests/unit/test_restapi.py +++ b/tests/unit/test_restapi.py @@ -16,7 +16,7 @@ def restapi_client(): def test_service_info(restapi_client: TestClient, test_data_dir: Path): - response = restapi_client.get("/service-info") + response = restapi_client.get("/anyvlm/service-info") response.raise_for_status() with (test_data_dir / "ga4gh-service-info" / "service-info.yaml").open() as f: diff --git a/tests/unit/test_variant_counts_endpoint.py b/tests/unit/test_variant_counts_endpoint.py index 8ff3a2b..c1af0c8 100644 --- a/tests/unit/test_variant_counts_endpoint.py +++ b/tests/unit/test_variant_counts_endpoint.py @@ -16,7 +16,7 @@ "referenceBases": TEST_VARIANT.ref, "alternateBases": TEST_VARIANT.alt, } -ENDPOINT = "/variant_counts" +ENDPOINT = "/anyvlm/variant_counts" @pytest.fixture(scope="session") diff --git a/tests/unit/test_vcf_upload_endpoint.py b/tests/unit/test_vcf_upload_endpoint.py index 1fbcd88..03ca9f0 100644 --- a/tests/unit/test_vcf_upload_endpoint.py +++ b/tests/unit/test_vcf_upload_endpoint.py @@ -14,6 +14,7 @@ # Constants for testing MAX_FILE_SIZE = 5 * 1024 * 1024 * 1024 # 5GB UPLOAD_CHUNK_SIZE = 1024 * 1024 # 1MB +ENDPOINT = "/anyvlm/ingest_vcf" @pytest.fixture(scope="module") @@ -160,14 +161,14 @@ class TestIngestVcfEndpoint: def test_endpoint_exists(self, client: TestClient): """Test that the endpoint exists and accepts POST.""" - response = client.post("/ingest_vcf") + response = client.post(ENDPOINT) # Should not be 404 assert response.status_code != 404 def test_missing_file_parameter(self, client: TestClient): """Test request without file parameter.""" response = client.post( - "/ingest_vcf", + ENDPOINT, params={"assembly": "GRCh38"}, ) assert response.status_code == 422 # Unprocessable Entity @@ -177,7 +178,7 @@ def test_missing_assembly_parameter(self, client: TestClient, valid_vcf_gz: Path """Test request without assembly parameter.""" with valid_vcf_gz.open("rb") as f: files = {"file": ("test.vcf.gz", f, "application/gzip")} - response = client.post("/ingest_vcf", files=files) + response = client.post(ENDPOINT, files=files) assert response.status_code == 422 assert ( @@ -189,7 +190,7 @@ def test_invalid_assembly_value(self, client: TestClient, valid_vcf_gz: Path): with valid_vcf_gz.open("rb") as f: files = {"file": ("test.vcf.gz", f, "application/gzip")} response = client.post( - "/ingest_vcf", + ENDPOINT, params={"assembly": "GRCh99"}, # Invalid files=files, ) @@ -202,7 +203,7 @@ def test_invalid_file_extension(self, client: TestClient, valid_vcf_gz: Path): # Use .vcf extension (should be .vcf.gz) files = {"file": ("test.vcf", f, "application/gzip")} response = client.post( - "/ingest_vcf", + ENDPOINT, params={"assembly": "GRCh38"}, files=files, ) @@ -219,7 +220,7 @@ def test_not_gzipped_file(self, client: TestClient): files = {"file": ("test.vcf.gz", io.BytesIO(content), "application/gzip")} response = client.post( - "/ingest_vcf", + ENDPOINT, params={"assembly": "GRCh38"}, files=files, ) @@ -234,7 +235,7 @@ def test_not_a_vcf_file(self, client: TestClient, not_vcf_gz: Path): with not_vcf_gz.open("rb") as f: files = {"file": ("test.vcf.gz", f, "application/gzip")} response = client.post( - "/ingest_vcf", + ENDPOINT, params={"assembly": "GRCh38"}, files=files, ) @@ -251,7 +252,7 @@ def test_vcf_missing_required_fields( with missing_fields_vcf_gz.open("rb") as f: files = {"file": ("test.vcf.gz", f, "application/gzip")} response = client.post( - "/ingest_vcf", + ENDPOINT, params={"assembly": "GRCh38"}, files=files, ) @@ -275,7 +276,7 @@ def test_successful_upload_and_ingestion( with valid_vcf_gz.open("rb") as f: files = {"file": ("test.vcf.gz", f, "application/gzip")} response = client.post( - "/ingest_vcf", + ENDPOINT, params={"assembly": "GRCh38"}, files=files, ) @@ -312,7 +313,7 @@ def test_ingestion_failure_propagates( with valid_vcf_gz.open("rb") as f: files = {"file": ("test.vcf.gz", f, "application/gzip")} response = client.post( - "/ingest_vcf", + ENDPOINT, params={"assembly": "GRCh38"}, files=files, ) @@ -330,7 +331,7 @@ def test_temp_file_cleanup_on_success(self, client: TestClient, valid_vcf_gz: Pa with valid_vcf_gz.open("rb") as f: files = {"file": ("test.vcf.gz", f, "application/gzip")} response = client.post( - "/ingest_vcf", + ENDPOINT, params={"assembly": "GRCh38"}, files=files, ) @@ -350,7 +351,7 @@ def test_temp_file_cleanup_on_error(self, client: TestClient, valid_vcf_gz: Path with valid_vcf_gz.open("rb") as f: files = {"file": ("test.vcf.gz", f, "application/gzip")} response = client.post( - "/ingest_vcf", + ENDPOINT, params={"assembly": "GRCh38"}, files=files, ) @@ -372,7 +373,7 @@ def test_assembly_grch37_parameter(self, client: TestClient, valid_vcf_gz: Path) with valid_vcf_gz.open("rb") as f: files = {"file": ("test.vcf.gz", f, "application/gzip")} response = client.post( - "/ingest_vcf", + ENDPOINT, params={"assembly": "GRCh37"}, files=files, )