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