Skip to content

Commit ddeea5d

Browse files
authored
Merge pull request #78 from Ensembl/add-genome-groups
Add genome groups
2 parents c1cf5eb + 2d039f3 commit ddeea5d

File tree

8 files changed

+293
-3
lines changed

8 files changed

+293
-3
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: Deploy API Docs
2+
on:
3+
push:
4+
branches: [main]
5+
paths: ['APISpecification.yaml'] # Only run when spec changes
6+
7+
jobs:
8+
deploy:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- uses: actions/checkout@v4
12+
13+
- name: Create documentation page
14+
run: |
15+
cat > index.html << 'EOF'
16+
<!DOCTYPE html>
17+
<html>
18+
<head>
19+
<title>API Documentation</title>
20+
<link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@3/swagger-ui.css" />
21+
</head>
22+
<body>
23+
<div id="swagger-ui"></div>
24+
<script src="https://unpkg.com/swagger-ui-dist@3/swagger-ui-bundle.js"></script>
25+
<script>
26+
SwaggerUIBundle({
27+
url: './APISpecification.yaml',
28+
dom_id: '#swagger-ui',
29+
presets: [
30+
SwaggerUIBundle.presets.apis,
31+
SwaggerUIBundle.presets.standalone
32+
]
33+
});
34+
</script>
35+
</body>
36+
</html>
37+
EOF
38+
39+
- name: Deploy to GitHub Pages
40+
uses: peaceiris/actions-gh-pages@v3
41+
with:
42+
github_token: ${{ secrets.GITHUB_TOKEN }}
43+
publish_dir: ./
44+
publish_branch: gh-pages

.github/workflows/deploy-docs.yml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
name: Deploy OpenAPI docs to GitHub Pages
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
- add-genome-groups
8+
paths:
9+
- 'docs/**'
10+
- '.github/workflows/deploy-docs.yml'
11+
12+
permissions:
13+
contents: read
14+
pages: write
15+
id-token: write
16+
17+
concurrency:
18+
group: 'pages'
19+
cancel-in-progress: true
20+
21+
jobs:
22+
deploy:
23+
environment:
24+
name: github-pages
25+
url: ${{ steps.deployment.outputs.page_url }}
26+
runs-on: ubuntu-latest
27+
28+
steps:
29+
- name: Checkout
30+
uses: actions/checkout@v4
31+
32+
- name: Setup Pages
33+
uses: actions/configure-pages@v5
34+
35+
- name: Upload artifact
36+
uses: actions/upload-pages-artifact@v3
37+
with:
38+
path: docs
39+
40+
- name: Deploy to GitHub Pages
41+
id: deployment
42+
uses: actions/deploy-pages@v4

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,12 @@ docker-compose -f docker-compose.yml up
1313
```bash
1414
docker-compose -f docker-compose.yml run metadata_api python -m unittest
1515
```
16+
17+
### Run locally without Docker:
18+
```bash
19+
git clone https://github.com/Ensembl/ensembl-web-metadata-api
20+
cd ensembl-web-metadata-api
21+
pip install -r requirements.txt
22+
PYTHONPATH='app' uvicorn main:app --host 0.0.0.0 --port 8014 --reload
23+
```
1624

app/api/models/genome.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,4 +195,16 @@ class DatasetAttributes(BaseModel):
195195
class GenomeByKeyword(BaseModel):
196196
genome_uuid: str = Field(alias="genomeUuid", default="")
197197
release_version: float = Field(alias=AliasPath("release", "releaseVersion"), default=0)
198-
genome_tag: str = Field(alias=AliasPath("assembly", "urlName"), default="")
198+
genome_tag: str = Field(alias=AliasPath("assembly", "urlName"), default="")
199+
200+
class GenomeGroup(BaseModel):
201+
id: str = Field(alias="groupId")
202+
type: str = Field(alias="groupType")
203+
name: str | None = Field(alias="groupName", default=None)
204+
reference_genome: BaseGenomeDetails = Field(alias="referenceGenome")
205+
206+
class GenomeGroupsResponse(BaseModel):
207+
genome_groups: list[GenomeGroup] = Field(alias="genomeGroups")
208+
209+
class GenomesInGroupResponse(BaseModel):
210+
genomes: list[BaseGenomeDetails] = Field(alias="genomes")

app/api/resources/grpc_client.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,3 +169,17 @@ def get_release(self, release_label: list[str], current_only: bool):
169169
request_class = self.reflector.message_class("ensembl_metadata.ReleaseRequest")
170170
request = request_class(release_label=release_label, current_only=current_only)
171171
return self.stub.GetRelease(request)
172+
173+
def get_genome_groups_with_reference(self, group_type: str, release_label: str):
174+
request_class = self.reflector.message_class(
175+
"ensembl_metadata.GroupTypeRequest"
176+
)
177+
request = request_class(group_type=group_type, release_label=release_label)
178+
return self.stub.GetGenomeGroupsWithReference(request)
179+
180+
def get_genomes_in_group(self, group_id: str, release_label: str):
181+
request_class = self.reflector.message_class(
182+
"ensembl_metadata.GenomesInGroupRequest"
183+
)
184+
request = request_class(group_id=group_id, release_label=release_label)
185+
return self.stub.GetGenomesInGroup(request)

app/api/resources/metadata.py

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,16 @@
1717
import logging
1818
from typing import Annotated
1919

20-
from fastapi import APIRouter, Request, responses, Query, HTTPException
20+
from fastapi import APIRouter, Request, responses, Query, Path
2121
from pydantic import ValidationError
2222

2323
from api.error_response import response_error_handler
2424
from api.models.checksums import Checksum
2525
from api.models.statistics import GenomeStatistics, ExampleObjectList
2626
from api.models.popular_species import PopularSpeciesGroup
2727
from api.models.karyotype import Karyotype
28-
from api.models.genome import BriefGenomeDetails, GenomeDetails, DatasetAttributes, GenomeByKeyword, Release
28+
from api.models.genome import BriefGenomeDetails, GenomeDetails, DatasetAttributes, GenomeByKeyword, Release, \
29+
GenomeGroupsResponse, GenomesInGroupResponse
2930
from api.models.ftplinks import FTPLinks
3031
from api.models.vep import VepFilePaths
3132

@@ -347,3 +348,57 @@ async def get_releases(
347348
response_data = responses.JSONResponse(error_response, status_code=500)
348349

349350
return response_data
351+
352+
@router.get("/genome_groups", name="genome_groups")
353+
@redis_cache("genome_groups", arg_keys=["group_type", "release_label"])
354+
async def get_genome_groups(
355+
group_type: str = Query(..., description="Group type, e.g. 'structural_variant'"),
356+
release_label: str | None = Query(None, description="Optional release label, e.g. '2025-02'")
357+
):
358+
try:
359+
genome_groups_dict = MessageToDict(
360+
grpc_client.get_genome_groups_with_reference(
361+
group_type=group_type,
362+
release_label=release_label
363+
)
364+
)
365+
logging.debug(f"genome_groups_dict: {genome_groups_dict}")
366+
if len(genome_groups_dict.get("genomeGroups", [])) == 0:
367+
return response_error_handler({
368+
"status": 404,
369+
"details": "No genome groups found matching criteria"
370+
})
371+
372+
genome_groups = GenomeGroupsResponse(**genome_groups_dict)
373+
response_dict = genome_groups.model_dump()
374+
return responses.JSONResponse(response_dict, status_code=200)
375+
except Exception as ex:
376+
logging.exception("Error in get_genome_groups")
377+
return response_error_handler({"status": 500})
378+
379+
@router.get("/genome_groups/{group_id}/genomes", name="genomes_in_group")
380+
@redis_cache("genomes_in_group", arg_keys=["group_id", "release_label"])
381+
async def get_genomes_in_group(
382+
group_id: str = Path(..., description="Group ID, e.g. 'grch38-group'"),
383+
release_label: str | None = Query(None, description="Optional release label, e.g. '2025-02'")
384+
):
385+
try:
386+
genomes_in_group_dict = MessageToDict(
387+
grpc_client.get_genomes_in_group(
388+
group_id=group_id,
389+
release_label=release_label
390+
)
391+
)
392+
logging.debug(f"genomes_in_group_dict: {genomes_in_group_dict}")
393+
if len(genomes_in_group_dict.get("genomes", [])) == 0:
394+
return response_error_handler({
395+
"status": 404,
396+
"details": "No genomes found in specified group"
397+
})
398+
399+
genomes_in_group = GenomesInGroupResponse(**genomes_in_group_dict)
400+
response_dict = genomes_in_group.model_dump()
401+
return responses.JSONResponse(response_dict, status_code=200)
402+
except Exception as ex:
403+
logging.exception("Error in get_genomes_in_group")
404+
return response_error_handler({"status": 500})
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,62 @@ paths:
382382
'500':
383383
$ref: '#/components/responses/500InternalServerError'
384384

385+
/api/metadata/genome_groups:
386+
get:
387+
summary: List genome groups with their reference genome
388+
parameters:
389+
- in: query
390+
name: group_type
391+
required: true
392+
schema:
393+
type: string
394+
- in: query
395+
name: release_label
396+
required: false
397+
schema:
398+
type: string
399+
responses:
400+
"200":
401+
description: OK
402+
content:
403+
application/json:
404+
schema:
405+
type: object
406+
properties:
407+
genome_groups:
408+
type: array
409+
items:
410+
$ref: '#/components/schemas/GenomeGroup'
411+
"400": { description: Bad Request }
412+
413+
/api/metadata/genome_groups/{group_id}/genomes:
414+
get:
415+
summary: List genomes in a group
416+
parameters:
417+
- in: path
418+
name: group_id
419+
required: true
420+
schema:
421+
type: string
422+
- in: query
423+
name: release_label
424+
required: false
425+
schema:
426+
type: string
427+
responses:
428+
"200":
429+
description: OK
430+
content:
431+
application/json:
432+
schema:
433+
type: object
434+
properties:
435+
genomes:
436+
type: array
437+
items:
438+
$ref: '#/components/schemas/Genome'
439+
"404": { description: Not Found }
440+
385441
components:
386442
schemas:
387443
GenomeIDFromAssemblyAccessionResponse:
@@ -1072,6 +1128,35 @@ components:
10721128
- name
10731129
- type
10741130
- is_current
1131+
Genome:
1132+
type: object
1133+
properties:
1134+
genome_uuid:
1135+
type: string
1136+
format: uuid
1137+
common_name:
1138+
type: string
1139+
scientific_name:
1140+
type: string
1141+
assembly_name:
1142+
type: string
1143+
is_reference:
1144+
type: boolean
1145+
is_group_reference:
1146+
type: boolean
1147+
release:
1148+
$ref: '#/components/schemas/Release'
1149+
GenomeGroup:
1150+
type: object
1151+
properties:
1152+
group_id:
1153+
type: string
1154+
group_type:
1155+
type: string
1156+
group_name:
1157+
type: string
1158+
reference_genome:
1159+
$ref: '#/components/schemas/Genome'
10751160
responses:
10761161
404NotFound:
10771162
description: The specified resource was not found

docs/index.html

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<title>API Documentation</title>
6+
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist/swagger-ui.css" />
7+
<style>
8+
body { margin: 0; padding: 0; }
9+
</style>
10+
</head>
11+
<body>
12+
<div id="swagger-ui"></div>
13+
14+
<script src="https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js"></script>
15+
<script src="https://unpkg.com/swagger-ui-dist/swagger-ui-standalone-preset.js"></script>
16+
<script>
17+
window.onload = () => {
18+
SwaggerUIBundle({
19+
url: "APISpecification.yaml", // IMPORTANT: relative path
20+
dom_id: '#swagger-ui',
21+
presets: [
22+
SwaggerUIBundle.presets.apis,
23+
SwaggerUIStandalonePreset
24+
],
25+
layout: "StandaloneLayout"
26+
});
27+
};
28+
</script>
29+
</body>
30+
</html>

0 commit comments

Comments
 (0)