Source code for galaxy.webapps.galaxy.api.dynamic_tools
import logging
from datetime import datetime
from typing import (
Any,
Literal,
Optional,
Union,
)
from fastapi import Response
from pydantic import BaseModel
from galaxy.exceptions import (
MessageException,
ObjectNotFound,
RequestParameterInvalidException,
)
from galaxy.managers.context import (
ProvidesHistoryContext,
ProvidesUserContext,
)
from galaxy.managers.tools import (
DynamicToolManager,
tool_payload_to_tool,
)
from galaxy.model import User
from galaxy.schema.fields import (
DecodedDatabaseIdField,
EncodedDatabaseIdField,
)
from galaxy.tool_util.parameters import input_models_for_tool_source
from galaxy.tool_util.parameters.convert import cwl_runtime_model
from galaxy.tool_util.parser.yaml import YamlToolSource
from galaxy.tool_util_models import (
lift_user_tool_source,
UserToolSource,
)
from galaxy.tool_util_models.dynamic_tool_models import (
DynamicToolPayload,
DynamicUnprivilegedToolCreatePayload,
)
from galaxy.webapps.galaxy.api import (
depends,
DependsOnTrans,
DependsOnUser,
Router,
)
from galaxy.webapps.galaxy.api.common import lift_headers
log = logging.getLogger(__name__)
router = Router(tags=["dynamic_tools"])
DatabaseIdOrUUID = Union[DecodedDatabaseIdField, str]
[docs]
class UnprivilegedToolResponse(BaseModel):
id: EncodedDatabaseIdField
uuid: str
active: bool
hidden: bool
tool_id: Optional[str]
tool_format: Optional[str]
create_time: datetime
# Either a strict UserToolSource (status="ok" or "lifted") or the raw
# stored dict (status="invalid"). Consumers narrow on `representation_status`.
representation: Union[UserToolSource, dict[str, Any]]
representation_status: Literal["ok", "lifted", "invalid"] = "ok"
representation_errors: list[str] = []
def _build_unprivileged_tool_response(d: dict[str, Any]) -> UnprivilegedToolResponse:
"""Run the lift over the stored representation and assemble the response.
This is the single place where the lift result is unpacked, so the helper's
invariants (status='ok' → strict model, 'lifted' → strict model + dropped
paths, 'invalid' → raw dict + error summary) are enforced exactly once."""
raw_representation = d.get("representation") or {}
status, representation, errors = lift_user_tool_source(raw_representation)
return UnprivilegedToolResponse(
id=d["id"],
uuid=d["uuid"],
active=d["active"],
hidden=d["hidden"],
tool_id=d.get("tool_id"),
tool_format=d.get("tool_format"),
create_time=d["create_time"],
representation=representation,
representation_status=status,
representation_errors=errors,
)
[docs]
@router.cbv
class UnprivilegedToolsApi:
# Almost identical to dynamic tools api, but operates with tool ids
# and is scoped to to individual user and never adds to global toolbox
dynamic_tools_manager: DynamicToolManager = depends(DynamicToolManager)
[docs]
@router.get(
"/api/unprivileged_tools",
summary="List the user-defined tools owned by the current user.",
response_model_exclude_defaults=True,
)
def index(
self,
active: bool = True,
trans: ProvidesUserContext = DependsOnTrans,
) -> list[UnprivilegedToolResponse]:
# No lift headers here: an aggregate across all of a user's tools is
# unbounded and has no per-tool attribution. Per-tool status and errors
# are in the body.
if not trans.user:
return []
return [
_build_unprivileged_tool_response(t.to_dict())
for t in self.dynamic_tools_manager.list_unprivileged_tools(trans.user, active=active)
]
[docs]
@router.get(
"/api/unprivileged_tools/{uuid}",
summary="Show one user-defined tool owned by the current user.",
response_model_exclude_defaults=True,
)
def show(self, response: Response, uuid: str, user: User = DependsOnUser) -> UnprivilegedToolResponse:
dynamic_tool = self.dynamic_tools_manager.get_unprivileged_tool_by_uuid(user, uuid)
if dynamic_tool is None:
raise ObjectNotFound()
tool = _build_unprivileged_tool_response(dynamic_tool.to_dict())
response.headers.update(lift_headers(tool.representation_status, tool.representation_errors))
return tool
[docs]
@router.post(
"/api/unprivileged_tools",
summary="Create a user-defined tool.",
description=(
"Creates a tool owned by the calling user from a `GalaxyUserTool` representation. The "
"representation is validated and linted before it is stored, and a failure is returned as a 400 "
"listing each problem. See the authoring reference at "
"https://docs.galaxyproject.org/en/master/dev/user_defined_tools_authoring.html for the tool format, "
"and run the resulting tool with `POST /api/tools` passing `tool_uuid`."
),
response_model_exclude_defaults=True,
)
def create(
self, payload: DynamicUnprivilegedToolCreatePayload, user: User = DependsOnUser
) -> UnprivilegedToolResponse:
dynamic_tool = self.dynamic_tools_manager.create_unprivileged_tool(
user,
payload,
)
# Just-created tools are validated through strict input, so the lift
# is a no-op here, but going through the same builder keeps behavior
# uniform.
return _build_unprivileged_tool_response(dynamic_tool.to_dict())
[docs]
@router.post(
"/api/unprivileged_tools/build",
summary="Render the tool form for a representation without storing the tool.",
description=(
"Builds the tool against a history and returns the same payload the tool form consumes, without "
"creating anything. Useful for checking that a draft's input interface is what you intended."
),
)
def build(
self,
payload: DynamicUnprivilegedToolCreatePayload,
history_id: DecodedDatabaseIdField,
user: User = DependsOnUser,
trans: ProvidesHistoryContext = DependsOnTrans,
):
self.dynamic_tools_manager.ensure_beta_tool_formats_enabled()
self.dynamic_tools_manager.ensure_can_use_unprivileged_tool(user)
history = trans.app.history_manager.get_owned(history_id, trans.user)
tool = tool_payload_to_tool(trans.app, payload.representation.model_dump(by_alias=True))
if tool:
return tool.to_json(trans=trans, history=history or trans.history)
[docs]
@router.post(
"/api/unprivileged_tools/runtime_model",
summary="Return an OpenAPI model of a representation's inputs.",
description=(
"Returns an OpenAPI document describing the inputs the tool would accept at runtime, so a client "
"can validate or generate a request body for it without storing the tool."
),
)
def runtime_model(self, payload: DynamicUnprivilegedToolCreatePayload, user: User = DependsOnUser):
self.dynamic_tools_manager.ensure_beta_tool_formats_enabled()
self.dynamic_tools_manager.ensure_can_use_unprivileged_tool(user)
represention = payload.representation.model_dump(by_alias=True)
tool_source = YamlToolSource(root_dict=represention)
input_bundle = input_models_for_tool_source(tool_source)
return cwl_runtime_model(input_bundle)
[docs]
@router.delete(
"/api/unprivileged_tools/{uuid}",
summary="Deactivate a user-defined tool owned by the current user.",
)
def delete(self, uuid: str, user: User = DependsOnUser):
"""
DELETE /api/unprivileged_tools/{encoded_dynamic_tool_id|tool_uuid}
Deactivate the specified dynamic tool. Deactivated tools will not
be loaded into the toolbox.
"""
dynamic_tool = self.dynamic_tools_manager.get_unprivileged_tool_by_uuid(user, uuid)
if not dynamic_tool:
raise ObjectNotFound()
self.dynamic_tools_manager.deactivate_unprivileged_tool(user, dynamic_tool)
[docs]
@router.cbv
class DynamicToolApi:
dynamic_tools_manager: DynamicToolManager = depends(DynamicToolManager)
[docs]
@router.get("/api/dynamic_tools", public=True)
def index(self):
return [t.to_dict() for t in self.dynamic_tools_manager.list_tools()]
[docs]
@router.get("/api/dynamic_tools/{dynamic_tool_id}", public=True)
def show(self, dynamic_tool_id: Union[DatabaseIdOrUUID, str]):
dynamic_tool = self.dynamic_tools_manager.get_tool_by_id_or_uuid(dynamic_tool_id)
if dynamic_tool is None:
raise ObjectNotFound()
return dynamic_tool.to_dict()
[docs]
@router.post("/api/dynamic_tools", require_admin=True)
def create(self, payload: DynamicToolPayload):
try:
dynamic_tool = self.dynamic_tools_manager.create_tool(payload)
except MessageException:
raise
except Exception as e:
raise RequestParameterInvalidException(str(e))
return dynamic_tool.to_dict()
[docs]
@router.delete("/api/dynamic_tools/{dynamic_tool_id}", require_admin=True)
def delete(self, dynamic_tool_id: DatabaseIdOrUUID) -> dict[str, Any]:
"""
DELETE /api/dynamic_tools/{encoded_dynamic_tool_id|tool_uuid}
Deactivate the specified dynamic tool. Deactivated tools will not
be loaded into the toolbox.
"""
dynamic_tool = self.dynamic_tools_manager.get_tool_by_id_or_uuid(dynamic_tool_id)
if dynamic_tool is None:
raise ObjectNotFound()
updated_dynamic_tool = self.dynamic_tools_manager.deactivate(dynamic_tool)
return updated_dynamic_tool.to_dict()