Skip to content

Commit 423f71e

Browse files
authored
Breadbox releases api phase2 - release_versions CRUD, CRUD tests, and api (#660)
1 parent 8427545 commit 423f71e

File tree

11 files changed

+1161
-15
lines changed

11 files changed

+1161
-15
lines changed

breadbox/breadbox/api/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
from .uploads import router as uploads_router
55
from .datasets import router as datasets_router
6+
from .release_versions import router as release_versions_router
7+
from .release_files import router as release_files_router
68
from .dataset_uploads import router as dataset_uploads_router
79
from .downloads import router as downloads_router
810
from .groups import router as groups_router
@@ -19,6 +21,8 @@
1921

2022
api_router = APIRouter(responses=ERROR_RESPONSES) # type: ignore
2123
api_router.include_router(datasets_router)
24+
api_router.include_router(release_versions_router)
25+
api_router.include_router(release_files_router)
2226
api_router.include_router(dataset_uploads_router)
2327
api_router.include_router(uploads_router)
2428
api_router.include_router(downloads_router)
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from typing import List
2+
3+
from fastapi import APIRouter, Depends, Query
4+
5+
from breadbox.schemas.release_version import ReleaseFileSearchResponse
6+
from ..crud import release_version as release_version_crud
7+
from breadbox.api.dependencies import get_db_with_user
8+
from breadbox.db.session import SessionWithUser
9+
10+
# Separated from release-versions to reduce confusion about the level
11+
# of granularity of the full text search. Search returns release file
12+
# level data.
13+
router = APIRouter(prefix="/release-files", tags=["release-files"])
14+
15+
16+
@router.get(
17+
"/search",
18+
response_model=List[ReleaseFileSearchResponse],
19+
operation_id="search_release_files",
20+
)
21+
def search_release_files(
22+
q: str = Query(
23+
...,
24+
min_length=1,
25+
description="Search query as the user types in the global searchbar.",
26+
),
27+
limit: int = Query(
28+
50, ge=1, le=100, description="Number of results to return per page. Max 100.",
29+
), # ge "greater than or equal to", le "less than or equal to"
30+
offset: int = Query(
31+
0,
32+
ge=0,
33+
description="Number of results to skip from the beginning (used for pagination).",
34+
),
35+
db: SessionWithUser = Depends(get_db_with_user),
36+
):
37+
"""
38+
Search for individual files across all releases using the FTS5 index.
39+
Returns denormalized metadata for each matching file.
40+
41+
If you have 150 results:
42+
43+
Page 1: limit=50, offset=0 (Gets results 1-50)
44+
45+
Page 2: limit=50, offset=50 (Gets results 51-100)
46+
47+
Page 3: limit=50, offset=100 (Gets results 101-150)
48+
"""
49+
# This uses the SQLite FTS5 'MATCH' operator
50+
results = release_version_crud.search_release_files(
51+
db=db, q=q, limit=limit, offset=offset
52+
)
53+
54+
return results
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
from datetime import date
2+
from typing import List, Optional, Annotated
3+
from logging import getLogger
4+
from fastapi import APIRouter, Depends, HTTPException, Request, Response, Body, Query
5+
6+
from breadbox.api.dependencies import get_db_with_user
7+
from breadbox.db.session import SessionWithUser
8+
from breadbox.db.util import transaction
9+
from breadbox.schemas.custom_http_exception import UserError
10+
11+
from ..crud import release_version as release_version_crud
12+
from ..models.release_version import ReleaseVersion
13+
from ..schemas.release_version import (
14+
ReleaseVersionResponse,
15+
CreateReleaseVersionParams,
16+
)
17+
18+
router = APIRouter(prefix="/release-versions", tags=["release-versions"])
19+
log = getLogger(__name__)
20+
21+
22+
def _get_required_release_version(
23+
db: SessionWithUser, release_version_id: str, include_files: bool = False
24+
) -> ReleaseVersion:
25+
"""Helper to fetch a release_version or raise 404"""
26+
release_version = release_version_crud.get_release_version(
27+
db=db, release_version_id=release_version_id, include_files=include_files
28+
)
29+
if release_version is None:
30+
raise HTTPException(
31+
status_code=404,
32+
detail=f"Release version with id {release_version_id} not found",
33+
)
34+
return release_version
35+
36+
37+
@router.get(
38+
"/",
39+
operation_id="get_release_versions",
40+
response_model=List[ReleaseVersionResponse],
41+
)
42+
def get_release_versions(
43+
release_name: Optional[str] = Query(
44+
None, description="Filter by general release name (e.g. Depmap Public)"
45+
),
46+
datatype: Optional[str] = Query(
47+
None, description="Filter by file datatype (e.g. 'crispr')"
48+
),
49+
start_date: Optional[date] = Query(
50+
None, description="Filter releases published on or after this date (YYYY-MM-DD)"
51+
),
52+
end_date: Optional[date] = Query(
53+
None,
54+
description="Filter releases published on or before this date (YYYY-MM-DD)",
55+
),
56+
include_files: bool = Query(
57+
False,
58+
description="If true, includes the list of files for each release in the response.",
59+
),
60+
db: SessionWithUser = Depends(get_db_with_user),
61+
):
62+
"""
63+
Get a list of available release versions, filtered by metadata or date ranges.
64+
"""
65+
return release_version_crud.get_release_versions(
66+
db,
67+
release_name=release_name,
68+
datatype=datatype,
69+
start_date=start_date,
70+
end_date=end_date,
71+
include_files=include_files,
72+
)
73+
74+
75+
@router.get(
76+
"/{release_version_id}",
77+
operation_id="get_release_version",
78+
response_model=ReleaseVersionResponse,
79+
)
80+
def get_release_version(
81+
release_version_id: str,
82+
response: Response,
83+
request: Request,
84+
include_files: bool = Query(
85+
True,
86+
description="If false, leaves off the list of files for each release in the response.",
87+
),
88+
db: SessionWithUser = Depends(get_db_with_user),
89+
):
90+
"""
91+
Get full metadata for a specific release version.
92+
Includes ETag support via content_hash.
93+
"""
94+
if_none_match = request.headers.get("If-None-Match")
95+
release = _get_required_release_version(
96+
db=db, release_version_id=release_version_id, include_files=include_files
97+
)
98+
99+
# ETag / 304 logic
100+
etag = f'"{release.content_hash}"'
101+
if if_none_match == etag:
102+
return Response(status_code=304)
103+
104+
response.headers["ETag"] = etag
105+
return release
106+
107+
108+
@router.post(
109+
"/", operation_id="create_release_version", response_model=ReleaseVersionResponse,
110+
)
111+
def create_release_version(
112+
params: Annotated[CreateReleaseVersionParams, Body()],
113+
db: SessionWithUser = Depends(get_db_with_user),
114+
):
115+
"""
116+
Register a new release version and its associated files/pipelines.
117+
This is typically called by the data loader.
118+
"""
119+
# Check for existing version/release name combo - must be unique
120+
existing = release_version_crud.get_release_version_by_release_name_and_version(
121+
db, params.release_name, params.version_name
122+
)
123+
if existing:
124+
raise HTTPException(
125+
status_code=409,
126+
detail=f"Release '{params.release_name}' version '{params.version_name}' already exists.",
127+
)
128+
129+
try:
130+
with transaction(db):
131+
new_release_version = release_version_crud.create_release_version(
132+
db, params
133+
)
134+
return new_release_version
135+
except Exception as e:
136+
raise UserError(f"Failed to create release version: {str(e)}")
137+
138+
139+
@router.delete(
140+
"/{release_version_id}", operation_id="delete_release_version",
141+
)
142+
def delete_release_version(
143+
release_version_id: str, db: SessionWithUser = Depends(get_db_with_user),
144+
):
145+
"""
146+
Delete a release version. Associated files and pipelines will be
147+
deleted automatically via cascade.
148+
"""
149+
release = _get_required_release_version(
150+
db=db, release_version_id=release_version_id
151+
)
152+
153+
with transaction(db):
154+
release_version_crud.delete_release_version(db, release)
155+
return {
156+
"message": f"Release version {release_version_id} deleted successfully."
157+
}

0 commit comments

Comments
 (0)