Merge pull request #11224 from davelopez/fastapi_route_tags

FastAPI route for tags (Disabled)
This commit is contained in:
John Chilton
2021-02-02 14:26:49 -05:00
committed by GitHub
6 changed files with 140 additions and 32 deletions
+66
View File
@@ -0,0 +1,66 @@
from enum import Enum
from typing import (
List,
Optional,
)
from pydantic import (
BaseModel,
Field,
)
from galaxy.managers.context import ProvidesUserContext
from galaxy.model import ItemTagAssociation
from galaxy.schema.fields import EncodedDatabaseIdField
taggable_item_names = {item: item for item in ItemTagAssociation.associated_item_names}
# This Enum is generated dynamically and mypy can not statically infer it's real type
# so it should be ignored. See:https://github.com/python/mypy/issues/4865#issuecomment-592560696
TaggableItemClass = Enum('TaggableItemClass', taggable_item_names) # type: ignore
class ItemTagsPayload(BaseModel):
item_id: EncodedDatabaseIdField = Field(
..., # This field is required
title="Item ID",
description="The `encoded identifier` of the item whose tags will be updated.",
)
item_class: TaggableItemClass = Field(
..., # This field is required
title="Item class",
description="The name of the class of the item that will be tagged.",
)
item_tags: Optional[List[str]] = Field(
default=None,
title="Item tags",
description="The list of tags that will replace the current tags associated with the item.",
)
class Config:
use_enum_values = True
class TagsManager:
"""Interface/service object shared by controllers for interacting with tags."""
def update(self, trans: ProvidesUserContext, payload: ItemTagsPayload) -> None:
"""Apply a new set of tags to an item; previous tags are deleted."""
tag_handler = trans.app.tag_handler
new_tags: Optional[str] = None
if payload.item_tags and len(payload.item_tags) > 0:
new_tags = ",".join(payload.item_tags)
item = self._get_item(trans, payload)
user = trans.user
tag_handler.delete_item_tags(user, item)
tag_handler.apply_item_tags(user, item, new_tags)
trans.sa_session.flush()
def _get_item(self, trans: ProvidesUserContext, payload: ItemTagsPayload):
"""
Get an item based on type and id.
"""
tag_handler = trans.app.tag_handler
id = trans.security.decode_id(payload.item_id)
item_class = tag_handler.item_tag_assoc_info[payload.item_class].item_class
item = trans.sa_session.query(item_class).filter(item_class.id == id).first()
return item
+6 -1
View File
@@ -19,7 +19,7 @@ from collections import defaultdict
from datetime import datetime, timedelta
from enum import Enum
from string import Template
from typing import Optional, TYPE_CHECKING
from typing import List, Optional, TYPE_CHECKING
from uuid import UUID, uuid4
from boltons.iterutils import remap
@@ -6350,6 +6350,11 @@ class Tag(RepresentById):
class ItemTagAssociation(Dictifiable):
dict_collection_visible_keys = ['id', 'user_tname', 'user_value']
dict_element_visible_keys = dict_collection_visible_keys
associated_item_names: List[str] = []
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
cls.associated_item_names.append(cls.__name__.replace("TagAssociation", ""))
def __init__(self, id=None, user=None, item_id=None, tag_id=None, user_tname=None, value=None):
self.id = id
+1 -1
View File
@@ -1515,7 +1515,7 @@ class UsesQuotaMixin:
class UsesTagsMixin(SharableItemSecurityMixin):
def get_tag_handler(self, trans):
def get_tag_handler(self, trans) -> tags.GalaxyTagHandler:
return trans.app.tag_handler
def _get_user_tags(self, trans, item_class_name, id):
+58 -27
View File
@@ -3,43 +3,74 @@ API Controller providing Galaxy Tags
"""
import logging
from galaxy.exceptions import MessageException
from fastapi import (
Body,
status,
)
# TODO: replace with Router after merging #11219
from fastapi_utils.cbv import cbv
from fastapi_utils.inferring_router import InferringRouter as APIRouter
from galaxy.managers.context import ProvidesUserContext
from galaxy.managers.tags import (
ItemTagsPayload,
TagsManager,
)
from galaxy.web import expose_api
from galaxy.webapps.base.controller import BaseAPIController, UsesTagsMixin
from galaxy.webapps.base.webapp import GalaxyWebTransaction
from galaxy.webapps.base.controller import BaseAPIController
from . import (
Depends,
get_trans,
)
log = logging.getLogger(__name__)
# TODO: This FastAPI router is disabled. Please rename it to `router` when the database session issues are fixed.
_router = APIRouter(tags=['tags'])
class TagsController(BaseAPIController, UsesTagsMixin):
def get_tags_manager() -> TagsManager:
return TagsManager() # TODO: remove/refactor after merging #11180
@cbv(_router)
class FastAPITags:
manager: TagsManager = Depends(get_tags_manager)
@_router.put(
'/api/tags',
summary="Apply a new set of tags to an item.",
status_code=status.HTTP_204_NO_CONTENT,
)
def update(
self,
trans: ProvidesUserContext = Depends(get_trans),
payload: ItemTagsPayload = Body(
..., # Required
title="Payload",
description="Request body containing the item and the tags to be assigned.",
),
):
"""Replaces the tags associated with an item with the new ones specified in the payload.
- The previous tags will be __deleted__.
- If no tags are provided in the request body, the currently associated tags will also be __deleted__.
"""
self.manager.update(trans, payload)
class TagsController(BaseAPIController):
def __init__(self, app):
super().__init__(app)
self.manager = TagsManager()
# Retag an item. All previous tags are deleted and new tags are applied.
@expose_api
def update(self, trans: GalaxyWebTransaction, payload: dict, **kwd):
def update(self, trans: ProvidesUserContext, payload: dict, **kwd):
"""
PUT /api/tags/
Apply a new set of tags to an item; previous tags are deleted.
"""
item_id = payload.get("item_id")
item_class = payload.get("item_class")
item_tags = payload.get("item_tags")
if item_id is None:
raise MessageException("Please provide the item id (item_id).")
if item_class is None:
raise MessageException("Please provide the item class (item_class).")
if item_tags and len(item_tags) > 0:
item_tags = ",".join(item_tags)
item = self._get_item(trans, item_class, trans.security.decode_id(item_id))
user = trans.user
self.get_tag_handler(trans).delete_item_tags(user, item)
self.get_tag_handler(trans).apply_item_tags(user, item, item_tags)
trans.sa_session.flush()
def _get_item(self, trans: GalaxyWebTransaction, item_class_name, id):
"""
Get an item based on type and id.
"""
item_class = self.get_tag_handler(trans).item_tag_assoc_info[item_class_name].item_class
item = trans.sa_session.query(item_class).filter(item_class.id == id).first()
return item
self.manager.update(trans, ItemTagsPayload(**payload))
+4
View File
@@ -28,6 +28,10 @@ api_tags_metadata = [
"name": "licenses",
"description": "Operations with [SPDX licenses](https://spdx.org/licenses/).",
},
{
"name": "tags",
"description": "Operations with tags.",
},
{
"name": "tool data tables",
"description": "Operations with tool [Data Tables](https://galaxyproject.org/admin/tools/data-tables/).",
+5 -3
View File
@@ -1,3 +1,4 @@
import json
import textwrap
from galaxy_test.base.populators import (
@@ -122,12 +123,13 @@ class DatasetsApiTestCase(ApiTestCase):
def test_tag_change(self):
hda_id = self.dataset_populator.new_dataset(self.history_id)['id']
payload = {
payload = json.dumps({
'item_id': hda_id,
'item_class': 'HistoryDatasetAssociation',
'item_tags': ['cool:tag_a', 'cool:tag_b', 'tag_c', 'name:tag_d', '#tag_e'],
}
self._put("tags", payload).json()
})
put_response = self._put("tags", payload)
self._assert_status_code_is_ok(put_response)
updated_hda = self._get(
f"histories/{self.history_id}/contents/{hda_id}").json()
assert 'cool:tag_a' in updated_hda['tags']