Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate library folder contents API to FastAPI #14223

Merged
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
77 changes: 77 additions & 0 deletions lib/galaxy/schema/schema.py
Expand Up @@ -5,6 +5,7 @@
from datetime import datetime
from enum import Enum
from typing import (
Annotated,
Any,
Dict,
List,
Expand Down Expand Up @@ -2528,6 +2529,82 @@ class LibraryFolderCurrentPermissions(BaseModel):
)


class LibraryFolderContentsIndexQueryPayload(Model):
limit: int = 10
offset: int = 0
search_text: Optional[str] = None
include_deleted: Optional[bool] = None


class LibraryFolderItemBase(Model):
id: EncodedDatabaseIdField
davelopez marked this conversation as resolved.
Show resolved Hide resolved
name: str
type: str
create_time: datetime = CreateTimeField
update_time: datetime = UpdateTimeField
can_manage: bool
deleted: bool


class FolderLibraryFolderItem(LibraryFolderItemBase):
type: Literal["folder"]
can_modify: bool
description: Optional[str] = FolderDescriptionField


class FileLibraryFolderItem(LibraryFolderItemBase):
type: Literal["file"]
file_ext: str
date_uploaded: datetime
is_unrestricted: bool
is_private: bool
state: Dataset.states = DatasetStateField
file_size: str
raw_size: int
ldda_id: EncodedDatabaseIdField
davelopez marked this conversation as resolved.
Show resolved Hide resolved
tags: str
message: Optional[str]


AnyLibraryFolderItem = Annotated[Union[FileLibraryFolderItem, FolderLibraryFolderItem], Field(discriminator="type")]


class LibraryFolderMetadata(Model):
parent_library_id: EncodedDatabaseIdField
davelopez marked this conversation as resolved.
Show resolved Hide resolved
folder_name: str
folder_description: str
total_rows: int
can_modify_folder: bool
can_add_library_item: bool
full_path: List[List[str]]


class LibraryFolderContentsIndexResult(Model):
metadata: LibraryFolderMetadata
folder_contents: List[AnyLibraryFolderItem]


class CreateLibraryFilePayload(Model):
from_hda_id: Optional[DecodedDatabaseIdField] = Field(
default=None,
title="From HDA ID",
description="The ID of an accessible HDA to copy into the library.",
)
from_hdca_id: Optional[DecodedDatabaseIdField] = Field(
default=None,
title="From HDCA ID",
description=(
"The ID of an accessible HDCA to copy into the library. "
"Nested collections are not allowed, you must flatten the collection first."
),
)
ldda_message: Optional[str] = Field(
default="",
title="LDDA Message",
description="The new message attribute of the LDDA created.",
)


class DatasetAssociationRoles(Model):
access_dataset_roles: List[RoleNameIdTuple] = Field(
default=[],
Expand Down
150 changes: 78 additions & 72 deletions lib/galaxy/webapps/galaxy/api/folder_contents.py
Expand Up @@ -2,93 +2,99 @@
API operations on the contents of a library folder.
"""
import logging
from typing import Optional

from galaxy import util
from galaxy.web import (
expose_api,
expose_api_anonymous,
from fastapi import (
Body,
Path,
Query,
)

from galaxy.managers.context import ProvidesUserContext
from galaxy.schema.fields import EncodedDatabaseIdField
davelopez marked this conversation as resolved.
Show resolved Hide resolved
from galaxy.schema.schema import (
CreateLibraryFilePayload,
LibraryFolderContentsIndexQueryPayload,
LibraryFolderContentsIndexResult,
)
from galaxy.webapps.galaxy.services.library_folder_contents import LibraryFolderContentsService
from . import (
BaseGalaxyAPIController,
depends,
DependsOnTrans,
Router,
)

log = logging.getLogger(__name__)

router = Router(tags=["data libraries folders"])

class FolderContentsController(BaseGalaxyAPIController):
"""
Class controls retrieval, creation and updating of folder contents.
"""

service: LibraryFolderContentsService = depends(LibraryFolderContentsService)

@expose_api_anonymous
def index(self, trans, folder_id, limit=None, offset=None, search_text=None, **kwd):
"""
GET /api/folders/{encoded_folder_id}/contents?limit={limit}&offset={offset}

Displays a collection (list) of a folder's contents
(files and folders). Encoded folder ID is prepended
with 'F' if it is a folder as opposed to a data set
which does not have it. Full path is provided in
response as a separate object providing data for
breadcrumb path building.

..example:
limit and offset can be combined. Skip the first two and return five:
'?limit=3&offset=5'

:param folder_id: encoded ID of the folder which
contents should be library_dataset_dict
:type folder_id: encoded string

:param offset: offset for returned library folder datasets
:type folder_id: encoded string

:param limit: limit for returned library folder datasets
contents should be library_dataset_dict
:type folder_id: encoded string

:param kwd: keyword dictionary with other params
:type kwd: dict
FolderIdPathParam: EncodedDatabaseIdField = Path(
davelopez marked this conversation as resolved.
Show resolved Hide resolved
..., title="Folder ID", description="The encoded identifier of the library folder."
)

:returns: dictionary containing all items and metadata
:type: dict
LimitQueryParam: int = Query(default=10, title="Limit", description="Maximum number of contents to return.")

:raises: MalformedId, InconsistentDatabase, ObjectNotFound,
InternalServerError
"""
include_deleted = util.asbool(kwd.get("include_deleted", False))
return self.service.index(trans, folder_id, limit, offset, search_text, include_deleted)
OffsetQueryParam: int = Query(
default=0,
title="Offset",
description="Return contents from this specified position. For example, if ``limit`` is set to 100 and ``offset`` to 200, contents between position 200-299 will be returned.",
)

@expose_api
def create(self, trans, encoded_folder_id, payload, **kwd):
"""
POST /api/folders/{encoded_id}/contents
SearchQueryParam: Optional[str] = Query(
default=None,
title="Search Text",
description="Used to filter the contents. Only the folders and files which name contains this text will be returned.",
)

Create a new library file from an HDA.
IncludeDeletedQueryParam: Optional[bool] = Query(
default=False,
title="Include Deleted",
description="Returns also deleted contents. Deleted contents can only be retrieved by Administrators or users with",
)

:param encoded_folder_id: the encoded id of the folder to import dataset(s) to
:type encoded_folder_id: an encoded id string
:param payload: dictionary structure containing:
:param from_hda_id: (optional) the id of an accessible HDA to copy into the library
:type from_hda_id: encoded id
:param from_hdca_id: (optional) the id of an accessible HDCA to copy into the library
:type from_hdca_id: encoded id
:param ldda_message: (optional) the new message attribute of the LDDA created
:type ldda_message: str
:param extended_metadata: (optional) dub-dictionary containing any extended metadata to associate with the item
:type extended_metadata: dict
:type payload: dict

:returns: a dictionary describing the new item if ``from_hda_id`` is supplied or a list of
such dictionaries describing the new items if ``from_hdca_id`` is supplied.
:rtype: object
@router.cbv
class FastAPILibraryFoldersContents:
service: LibraryFolderContentsService = depends(LibraryFolderContentsService)

:raises: ObjectAttributeInvalidException,
InsufficientPermissionsException, ItemAccessibilityException,
InternalServerError
@router.get(
"/api/folders/{folder_id}/contents",
summary="Returns a list of a folder's contents (files and sub-folders) with additional metadata about the folder.",
responses={
200: {
"description": "The contents of the folder that match the query parameters.",
"model": LibraryFolderContentsIndexResult,
},
},
)
def index(
self,
trans: ProvidesUserContext = DependsOnTrans,
folder_id: EncodedDatabaseIdField = FolderIdPathParam,
davelopez marked this conversation as resolved.
Show resolved Hide resolved
limit: int = LimitQueryParam,
offset: int = OffsetQueryParam,
search_text: Optional[str] = SearchQueryParam,
include_deleted: Optional[bool] = IncludeDeletedQueryParam,
):
"""Returns a list of a folder's contents (files and sub-folders).

Additional metadata for the folder is provided in the response as a separate object containing data
for breadcrumb path building, permissions and other folder's details.
"""
return self.service.create(trans, encoded_folder_id, payload)
payload = LibraryFolderContentsIndexQueryPayload(
limit=limit, offset=offset, search_text=search_text, include_deleted=include_deleted
)
return self.service.index(trans, folder_id, payload)

@router.post(
"/api/folders/{folder_id}/contents",
name="add_history_datasets_to_library",
summary="Creates a new library file from an existing HDA/HDCA.",
)
def create(
self,
trans: ProvidesUserContext = DependsOnTrans,
folder_id: EncodedDatabaseIdField = FolderIdPathParam,
davelopez marked this conversation as resolved.
Show resolved Hide resolved
payload: CreateLibraryFilePayload = Body(...),
):
return self.service.create(trans, folder_id, payload)
2 changes: 1 addition & 1 deletion lib/galaxy/webapps/galaxy/api/folders.py
Expand Up @@ -34,7 +34,7 @@

log = logging.getLogger(__name__)

router = Router(tags=["folders"])
router = Router(tags=["data libraries folders"])

FolderIdPathParam: EncodedDatabaseIdField = Path(
..., title="Folder ID", description="The encoded identifier of the library folder."
Expand Down
22 changes: 0 additions & 22 deletions lib/galaxy/webapps/galaxy/buildapp.py
Expand Up @@ -1029,28 +1029,6 @@ def connect_invocation_endpoint(endpoint_name, endpoint_suffix, action, conditio
webapp, name_prefix="library_dataset_", path_prefix="/api/libraries/{library_id}/contents/{library_content_id}"
)

# =======================
# ===== FOLDERS API =====
# =======================

webapp.mapper.connect(
"add_history_datasets_to_library",
"/api/folders/{encoded_folder_id}/contents",
controller="folder_contents",
action="create",
conditions=dict(method=["POST"]),
)

webapp.mapper.resource(
"content",
"contents",
controller="folder_contents",
name_prefix="folder_",
path_prefix="/api/folders/{folder_id}",
parent_resources=dict(member_name="folder", collection_name="folders"),
conditions=dict(method=["GET"]),
)

webapp.mapper.resource("job", "jobs", path_prefix="/api")
webapp.mapper.connect(
"job_search", "/api/jobs/search", controller="jobs", action="search", conditions=dict(method=["POST"])
Expand Down
2 changes: 1 addition & 1 deletion lib/galaxy/webapps/galaxy/fast_app.py
Expand Up @@ -51,7 +51,7 @@
},
{"name": "histories"},
{"name": "libraries"},
{"name": "folders"},
{"name": "data libraries folders"},
{"name": "job_lock"},
{"name": "metrics"},
{"name": "default"},
Expand Down