diff --git a/.github/workflows/deploy-docs-openapi.yaml b/.github/workflows/deploy-docs-openapi.yaml
new file mode 100644
index 0000000..7ae7840
--- /dev/null
+++ b/.github/workflows/deploy-docs-openapi.yaml
@@ -0,0 +1,44 @@
+name: Deploy API Docs
+on:
+ push:
+ branches: [main]
+ paths: ['APISpecification.yaml'] # Only run when spec changes
+
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Create documentation page
+ run: |
+ cat > index.html << 'EOF'
+
+
+
+ API Documentation
+
+
+
+
+
+
+
+
+ EOF
+
+ - name: Deploy to GitHub Pages
+ uses: peaceiris/actions-gh-pages@v3
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ publish_dir: ./
+ publish_branch: gh-pages
\ No newline at end of file
diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml
new file mode 100644
index 0000000..944c16f
--- /dev/null
+++ b/.github/workflows/deploy-docs.yml
@@ -0,0 +1,42 @@
+name: Deploy OpenAPI docs to GitHub Pages
+
+on:
+ push:
+ branches:
+ - main
+ - add-genome-groups
+ paths:
+ - 'docs/**'
+ - '.github/workflows/deploy-docs.yml'
+
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+concurrency:
+ group: 'pages'
+ cancel-in-progress: true
+
+jobs:
+ deploy:
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup Pages
+ uses: actions/configure-pages@v5
+
+ - name: Upload artifact
+ uses: actions/upload-pages-artifact@v3
+ with:
+ path: docs
+
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v4
diff --git a/README.md b/README.md
index de23fad..9948d1c 100644
--- a/README.md
+++ b/README.md
@@ -13,4 +13,12 @@ docker-compose -f docker-compose.yml up
```bash
docker-compose -f docker-compose.yml run metadata_api python -m unittest
```
+
+### Run locally without Docker:
+```bash
+git clone https://github.com/Ensembl/ensembl-web-metadata-api
+cd ensembl-web-metadata-api
+pip install -r requirements.txt
+PYTHONPATH='app' uvicorn main:app --host 0.0.0.0 --port 8014 --reload
+```
diff --git a/app/api/models/genome.py b/app/api/models/genome.py
index 1b87684..dd22ccb 100644
--- a/app/api/models/genome.py
+++ b/app/api/models/genome.py
@@ -175,4 +175,16 @@ class DatasetAttributes(BaseModel):
class GenomeByKeyword(BaseModel):
genome_uuid: str = Field(alias="genomeUuid", default="")
release_version: float = Field(alias=AliasPath("release", "releaseVersion"), default=0)
- genome_tag: str = Field(alias=AliasPath("assembly", "urlName"), default="")
\ No newline at end of file
+ genome_tag: str = Field(alias=AliasPath("assembly", "urlName"), default="")
+
+class GenomeGroup(BaseModel):
+ id: str = Field(alias="groupId")
+ type: str = Field(alias="groupType")
+ name: str | None = Field(alias="groupName", default=None)
+ reference_genome: BaseGenomeDetails = Field(alias="referenceGenome")
+
+class GenomeGroupsResponse(BaseModel):
+ genome_groups: list[GenomeGroup] = Field(alias="genomeGroups")
+
+class GenomesInGroupResponse(BaseModel):
+ genomes: list[BaseGenomeDetails] = Field(alias="genomes")
\ No newline at end of file
diff --git a/app/api/resources/grpc_client.py b/app/api/resources/grpc_client.py
index f95f83d..3c0a977 100644
--- a/app/api/resources/grpc_client.py
+++ b/app/api/resources/grpc_client.py
@@ -169,3 +169,17 @@ def get_release(self, release_label: list[str], current_only: bool):
request_class = self.reflector.message_class("ensembl_metadata.ReleaseRequest")
request = request_class(release_label=release_label, current_only=current_only)
return self.stub.GetRelease(request)
+
+ def get_genome_groups_with_reference(self, group_type: str, release_label: str):
+ request_class = self.reflector.message_class(
+ "ensembl_metadata.GroupTypeRequest"
+ )
+ request = request_class(group_type=group_type, release_label=release_label)
+ return self.stub.GetGenomeGroupsWithReference(request)
+
+ def get_genomes_in_group(self, group_id: str, release_label: str):
+ request_class = self.reflector.message_class(
+ "ensembl_metadata.GenomesInGroupRequest"
+ )
+ request = request_class(group_id=group_id, release_label=release_label)
+ return self.stub.GetGenomesInGroup(request)
\ No newline at end of file
diff --git a/app/api/resources/metadata.py b/app/api/resources/metadata.py
index d68f3e0..7d1052e 100644
--- a/app/api/resources/metadata.py
+++ b/app/api/resources/metadata.py
@@ -17,7 +17,7 @@
import logging
from typing import Annotated
-from fastapi import APIRouter, Request, responses, Query, HTTPException
+from fastapi import APIRouter, Request, responses, Query, Path
from pydantic import ValidationError
from api.error_response import response_error_handler
@@ -25,7 +25,8 @@
from api.models.statistics import GenomeStatistics, ExampleObjectList
from api.models.popular_species import PopularSpeciesGroup
from api.models.karyotype import Karyotype
-from api.models.genome import BriefGenomeDetails, GenomeDetails, DatasetAttributes, GenomeByKeyword, Release
+from api.models.genome import BriefGenomeDetails, GenomeDetails, DatasetAttributes, GenomeByKeyword, Release, \
+ GenomeGroupsResponse, GenomesInGroupResponse
from api.models.ftplinks import FTPLinks
from api.models.vep import VepFilePaths
@@ -347,3 +348,57 @@ async def get_releases(
response_data = responses.JSONResponse(error_response, status_code=500)
return response_data
+
+@router.get("/genome_groups", name="genome_groups")
+@redis_cache("genome_groups", arg_keys=["group_type", "release_label"])
+async def get_genome_groups(
+ group_type: str = Query(..., description="Group type, e.g. 'structural_variant'"),
+ release_label: str | None = Query(None, description="Optional release label, e.g. '2025-02'")
+):
+ try:
+ genome_groups_dict = MessageToDict(
+ grpc_client.get_genome_groups_with_reference(
+ group_type=group_type,
+ release_label=release_label
+ )
+ )
+ logging.debug(f"genome_groups_dict: {genome_groups_dict}")
+ if len(genome_groups_dict.get("genomeGroups", [])) == 0:
+ return response_error_handler({
+ "status": 404,
+ "details": "No genome groups found matching criteria"
+ })
+
+ genome_groups = GenomeGroupsResponse(**genome_groups_dict)
+ response_dict = genome_groups.model_dump()
+ return responses.JSONResponse(response_dict, status_code=200)
+ except Exception as ex:
+ logging.exception("Error in get_genome_groups")
+ return response_error_handler({"status": 500})
+
+@router.get("/genome_groups/{group_id}/genomes", name="genomes_in_group")
+@redis_cache("genomes_in_group", arg_keys=["group_id", "release_label"])
+async def get_genomes_in_group(
+ group_id: str = Path(..., description="Group ID, e.g. 'grch38-group'"),
+ release_label: str | None = Query(None, description="Optional release label, e.g. '2025-02'")
+):
+ try:
+ genomes_in_group_dict = MessageToDict(
+ grpc_client.get_genomes_in_group(
+ group_id=group_id,
+ release_label=release_label
+ )
+ )
+ logging.debug(f"genomes_in_group_dict: {genomes_in_group_dict}")
+ if len(genomes_in_group_dict.get("genomes", [])) == 0:
+ return response_error_handler({
+ "status": 404,
+ "details": "No genomes found in specified group"
+ })
+
+ genomes_in_group = GenomesInGroupResponse(**genomes_in_group_dict)
+ response_dict = genomes_in_group.model_dump()
+ return responses.JSONResponse(response_dict, status_code=200)
+ except Exception as ex:
+ logging.exception("Error in get_genomes_in_group")
+ return response_error_handler({"status": 500})
diff --git a/APISpecification.yaml b/docs/APISpecification.yaml
similarity index 93%
rename from APISpecification.yaml
rename to docs/APISpecification.yaml
index 7e657f4..4445d61 100644
--- a/APISpecification.yaml
+++ b/docs/APISpecification.yaml
@@ -382,6 +382,62 @@ paths:
'500':
$ref: '#/components/responses/500InternalServerError'
+ /api/metadata/genome_groups:
+ get:
+ summary: List genome groups with their reference genome
+ parameters:
+ - in: query
+ name: group_type
+ required: true
+ schema:
+ type: string
+ - in: query
+ name: release_label
+ required: false
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ genome_groups:
+ type: array
+ items:
+ $ref: '#/components/schemas/GenomeGroup'
+ "400": { description: Bad Request }
+
+ /api/metadata/genome_groups/{group_id}/genomes:
+ get:
+ summary: List genomes in a group
+ parameters:
+ - in: path
+ name: group_id
+ required: true
+ schema:
+ type: string
+ - in: query
+ name: release_label
+ required: false
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ genomes:
+ type: array
+ items:
+ $ref: '#/components/schemas/Genome'
+ "404": { description: Not Found }
+
components:
schemas:
GenomeIDFromAssemblyAccessionResponse:
@@ -1072,6 +1128,35 @@ components:
- name
- type
- is_current
+ Genome:
+ type: object
+ properties:
+ genome_uuid:
+ type: string
+ format: uuid
+ common_name:
+ type: string
+ scientific_name:
+ type: string
+ assembly_name:
+ type: string
+ is_reference:
+ type: boolean
+ is_group_reference:
+ type: boolean
+ release:
+ $ref: '#/components/schemas/Release'
+ GenomeGroup:
+ type: object
+ properties:
+ group_id:
+ type: string
+ group_type:
+ type: string
+ group_name:
+ type: string
+ reference_genome:
+ $ref: '#/components/schemas/Genome'
responses:
404NotFound:
description: The specified resource was not found
diff --git a/docs/index.html b/docs/index.html
new file mode 100644
index 0000000..f1448a3
--- /dev/null
+++ b/docs/index.html
@@ -0,0 +1,30 @@
+
+
+
+
+ API Documentation
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file